// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Threading; using System.Diagnostics; using System.Reflection; using UnrealBuildTool; using System.Text.RegularExpressions; namespace AutomationTool { #region UAT Internal Utils /// /// AutomationTool internal Utilities. /// public static class InternalUtils { /// /// Gets environment variable value. /// /// Variable name. /// Default value to be returned if the variable does not exist. /// Variable value or the default value if the variable did not exist. public static string GetEnvironmentVariable(string VarName, string Default, bool bQuiet = false) { var Value = Environment.GetEnvironmentVariable(VarName); if (Value == null) { Value = Default; } if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "GetEnvironmentVariable {0}={1}", VarName, Value); } return Value; } /// /// Creates a directory. /// /// Directory name. /// True if the directory was created, false otherwise. public static bool SafeCreateDirectory(string Path, bool bQuiet = false) { if( !bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeCreateDirectory {0}", Path); } bool Result = true; try { Result = Directory.CreateDirectory(Path).Exists; } catch (Exception) { if (Directory.Exists(Path) == false) { Result = false; } } return Result; } /// /// Deletes a file (will remove read-only flag if necessary). /// /// Filename /// if true, then do not print errors, also in quiet mode do not retry /// True if the file does not exist, false otherwise. public static bool SafeDeleteFile(string Path, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeDeleteFile {0}", Path); } int MaxAttempts = bQuiet ? 1 : 10; int Attempts = 0; bool Result = true; Exception LastException = null; do { Result = true; try { if (File.Exists(Path)) { FileAttributes Attributes = File.GetAttributes(Path); if ((Attributes & FileAttributes.ReadOnly) != 0) { File.SetAttributes(Path, Attributes & ~FileAttributes.ReadOnly); } File.Delete(Path); } } catch (Exception Ex) { if (File.Exists(Path)) { Result = false; } LastException = Ex; } if (Result == false && Attempts + 1 < MaxAttempts) { System.Threading.Thread.Sleep(1000); } } while (Result == false && ++Attempts < MaxAttempts); if (Result == false && LastException != null) { if (bQuiet) { Log.WriteLine(TraceEventType.Information, "Failed to delete file {0} in {1} attempts.", Path, MaxAttempts); } else { Log.WriteLine(TraceEventType.Warning, "Failed to delete file {0} in {1} attempts.", Path, MaxAttempts); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(LastException)); } } return Result; } /// /// Recursively deletes a directory and all its files and subdirectories. /// /// Path to delete. /// Whether the deletion was succesfull. private static bool RecursivelyDeleteDirectory(string Path, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "RecursivelyDeleteDirectory {0}", Path); } // Delete all files. This will also delete read-only files. var FilesInDirectory = Directory.EnumerateFiles(Path); foreach (string Filename in FilesInDirectory) { if (SafeDeleteFile(Filename, bQuiet) == false) { return false; } } // Recursively delete all files from sub-directories. var FoldersInDirectory = Directory.EnumerateDirectories(Path); foreach (string Folder in FoldersInDirectory) { if (RecursivelyDeleteDirectory(Folder, bQuiet) == false) { return false; } } // At this point there should be no read-only files in any of the directories and // this directory should be empty too. return SafeDeleteEmptyDirectory(Path, bQuiet); } /// /// Deletes an empty directory. /// /// Path to the Directory. /// True if deletion was successful, otherwise false. public static bool SafeDeleteEmptyDirectory(string Path, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeDeleteEmptyDirectory {0}", Path); } const int MaxAttempts = 10; int Attempts = 0; bool Result = true; Exception LastException = null; do { Result = !Directory.Exists(Path); if (!Result) { try { Directory.Delete(Path, true); } catch (Exception Ex) { if (Directory.Exists(Path)) { Thread.Sleep(3000); } Result = !Directory.Exists(Path); LastException = Ex; } } } while (Result == false && ++Attempts < MaxAttempts); if (Result == false && LastException != null) { Log.WriteLine(TraceEventType.Warning, "Failed to delete directory {0} in {1} attempts.", Path, MaxAttempts); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(LastException)); } return Result; } /// /// Deletes a directory and all its contents. Will delete read-only files. /// /// Directory name. /// True if the directory no longer exists, false otherwise. public static bool SafeDeleteDirectory(string Path, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeDeleteDirectory {0}", Path); } if (Directory.Exists(Path)) { return RecursivelyDeleteDirectory(Path, bQuiet); } else { return true; } } /// /// Renames/moves a file. /// /// Old name /// New name /// True if the operation was successful, false otherwise. public static bool SafeRenameFile(string OldName, string NewName, bool bQuiet = false) { if( !bQuiet ) { Log.WriteLine(TraceEventType.Information, "SafeRenameFile {0} {1}", OldName, NewName); } const int MaxAttempts = 10; int Attempts = 0; bool Result = true; do { Result = true; try { if (File.Exists(OldName)) { FileAttributes Attributes = File.GetAttributes(OldName); if ((Attributes & FileAttributes.ReadOnly) != 0) { File.SetAttributes(OldName, Attributes & ~FileAttributes.ReadOnly); } } File.Move(OldName, NewName); } catch (Exception Ex) { if (File.Exists(OldName) == true || File.Exists(NewName) == false) { Log.WriteLine(TraceEventType.Warning, "Failed to rename {0} to {1}", OldName, NewName); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); Result = false; } } } while (Result == false && ++Attempts < MaxAttempts); return Result; } // @todo: This could be passed in from elsewhere, and this should be somehow done per ini section // but this will get it so that games won't ship passwords private static string[] LinesToFilter = new string[] { "KeyStorePassword", "KeyPassword", }; private static void FilterIniFile(string SourceName, string TargetName) { string[] Lines = File.ReadAllLines(SourceName); StringBuilder NewLines = new StringBuilder(""); foreach (string Line in Lines) { // look for each filter on each line bool bFiltered = false; foreach (string Filter in LinesToFilter) { if (Line.StartsWith(Filter + "=")) { bFiltered = true; break; } } // write out if it's not filtered out if (!bFiltered) { NewLines.AppendLine(Line); } } // now write out the final .ini file if (File.Exists(TargetName)) { File.Delete(TargetName); } File.WriteAllText(TargetName, NewLines.ToString()); // other code assumes same timestamp for source and dest File.SetLastWriteTimeUtc(TargetName, File.GetLastWriteTimeUtc(SourceName)); } /// /// Copies a file. /// /// Source name /// Target name /// True if the operation was successful, false otherwise. public static bool SafeCopyFile(string SourceName, string TargetName, bool bQuiet = false, bool bFilterSpecialLinesFromIniFiles = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeCopyFile {0} {1}", SourceName, TargetName); } const int MaxAttempts = 10; int Attempts = 0; bool Result = true; do { Result = true; bool Retry = true; try { bool bSkipSizeCheck = false; if (bFilterSpecialLinesFromIniFiles && Path.GetExtension(SourceName) == ".ini") { FilterIniFile(SourceName, TargetName); // ini files may change size, don't check bSkipSizeCheck = true; } else { File.Copy(SourceName, TargetName, overwrite: true); } Retry = !File.Exists(TargetName); if (!Retry) { FileInfo SourceInfo = new FileInfo(SourceName); FileInfo TargetInfo = new FileInfo(TargetName); if (!bSkipSizeCheck && SourceInfo.Length != TargetInfo.Length) { Log.WriteLine(TraceEventType.Warning, "Size mismatch {0} = {1} to {2} = {3}", SourceName, SourceInfo.Length, TargetName, TargetInfo.Length); Retry = true; } // Timestamps should be no more than 2 seconds out - assuming this as exFAT filesystems store timestamps at 2 second intervals: // http://ntfs.com/exfat-time-stamp.htm if (!((SourceInfo.LastWriteTimeUtc - TargetInfo.LastWriteTimeUtc).TotalSeconds < 2 && (SourceInfo.LastWriteTimeUtc - TargetInfo.LastWriteTimeUtc).TotalSeconds > -2)) { Log.WriteLine(TraceEventType.Warning, "Date mismatch {0} = {1} to {2} = {3}", SourceName, SourceInfo.LastWriteTimeUtc, TargetName, TargetInfo.LastWriteTimeUtc); Retry = true; } } } catch (Exception Ex) { Log.WriteLine(System.Diagnostics.TraceEventType.Warning, "SafeCopyFile Exception was {0}", LogUtils.FormatException(Ex)); Retry = true; } if (Retry) { if (Attempts + 1 < MaxAttempts) { Log.WriteLine(TraceEventType.Warning, "Failed to copy {0} to {1}, deleting, waiting 10s and retrying.", SourceName, TargetName); if (File.Exists(TargetName)) { SafeDeleteFile(TargetName); } Thread.Sleep(10000); } else { Log.WriteLine(TraceEventType.Warning, "Failed to copy {0} to {1}", SourceName, TargetName); } Result = false; } } while (Result == false && ++Attempts < MaxAttempts); return Result; } /// /// Reads all lines from a file. /// /// Filename /// An array containing all lines read from the file or null if the file could not be read. public static string[] SafeReadAllLines(string Filename) { Log.WriteLine(TraceEventType.Information, "SafeReadAllLines {0}", Filename); string[] Result = null; try { Result = File.ReadAllLines(Filename); } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Failed to load {0}", Filename); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Reads all text from a file. /// /// Filename /// String containing all text read from the file or null if the file could not be read. public static string SafeReadAllText(string Filename) { Log.WriteLine(TraceEventType.Information, "SafeReadAllLines {0}", Filename); string Result = null; try { Result = File.ReadAllText(Filename); } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Failed to load {0}", Filename); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Finds files in the specified path. /// /// Path /// Search pattern /// Whether to search recursively or not. /// List of all files found (can be empty) or null if the operation failed. public static string[] FindFiles(string Path, string SearchPattern, bool Recursive, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "FindFiles {0} {1} {2}", Path, SearchPattern, Recursive); } // On Linux, filter out symlinks since we (usually) create them to fix mispelled case-sensitive filenames in content, and if they aren't filtered, // UAT picks up both the symlink and the original file and considers them duplicates when packaging (pak files are case-insensitive). // Windows needs the symlinks though because that's how deduplication works on Windows server, // see https://answers.unrealengine.com/questions/212888/automated-buildjenkins-failing-due-to-symlink-chec.html // FIXME: ZFS, JFS and other fs that can be case-insensitive on Linux should use the faster path as well. if (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Linux) { return Directory.GetFiles(Path, SearchPattern, Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); } else { List FileNames = new List(); DirectoryInfo DirInfo = new DirectoryInfo(Path); foreach( FileInfo File in DirInfo.EnumerateFiles(SearchPattern, Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) { if (File.Attributes.HasFlag(FileAttributes.ReparsePoint)) { if (!bQuiet) { Log.WriteLine(TraceEventType.Warning, "Ignoring symlink {0}", File.FullName); } continue; } FileNames.Add(File.FullName); } return FileNames.ToArray(); } } /// /// Finds directories in the specified path. /// /// Path /// Search pattern /// Whether to search recursively or not. /// List of all directories found (can be empty) or null if the operation failed. public static string[] FindDirectories(string Path, string SearchPattern, bool Recursive, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "FindDirectories {0} {1} {2}", Path, SearchPattern, Recursive); } return Directory.GetDirectories(Path, SearchPattern, Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); } /// /// Finds files in the specified path. /// /// Path /// Search pattern /// Whether to search recursively or not. /// List of all files found (can be empty) or null if the operation failed. public static string[] SafeFindFiles(string Path, string SearchPattern, bool Recursive, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeFindFiles {0} {1} {2}", Path, SearchPattern, Recursive); } string[] Files = null; try { Files = FindFiles(Path, SearchPattern, Recursive, bQuiet); } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to Find Files in {0}", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Files; } /// /// Finds directories in the specified path. /// /// Path /// Search pattern /// Whether to search recursively or not. /// List of all files found (can be empty) or null if the operation failed. public static string[] SafeFindDirectories(string Path, string SearchPattern, bool Recursive, bool bQuiet = false) { if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeFindDirectories {0} {1} {2}", Path, SearchPattern, Recursive); } string[] Directories = null; try { Directories = FindDirectories(Path, SearchPattern, Recursive, bQuiet); } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to Find Directories in {0}", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Directories; } /// /// Checks if a file exists. /// /// Filename /// if true, do not print a message /// True if the file exists, false otherwise. public static bool SafeFileExists(string Path, bool bQuiet = false) { bool Result = false; try { Result = File.Exists(Path); if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeFileExists {0}={1}", Path, Result); } } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to check if file {0} exists.", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Checks if a directory exists. /// /// Directory /// if true, no longging /// True if the directory exists, false otherwise. public static bool SafeDirectoryExists(string Path, bool bQuiet = false) { bool Result = false; try { Result = Directory.Exists(Path); if (!bQuiet) { Log.WriteLine(TraceEventType.Information, "SafeDirectoryExists {0}={1}", Path, Result); } } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to check if directory {0} exists.", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Writes lines to a file. /// /// Filename /// Text /// True if the operation was successful, false otherwise. public static bool SafeWriteAllLines(string Path, string[] Text) { Log.WriteLine(TraceEventType.Information, "SafeWriteAllLines {0}", Path); bool Result = false; try { File.WriteAllLines(Path, Text); Result = true; } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to write text to {0}", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Writes text to a file. /// /// Filename /// Text /// True if the operation was successful, false otherwise. public static bool SafeWriteAllText(string Path, string Text) { Log.WriteLine(TraceEventType.Information, "SafeWriteAllText {0}", Path); bool Result = false; try { File.WriteAllText(Path, Text); Result = true; } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to write text to {0}", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Writes text to a file. /// /// Filename /// Text /// True if the operation was successful, false otherwise. public static bool SafeWriteAllBytes(string Path, byte[] Bytes) { Log.WriteLine(TraceEventType.Information, "SafeWriteAllBytes {0}", Path); bool Result = false; try { File.WriteAllBytes(Path, Bytes); Result = true; } catch (Exception Ex) { Log.WriteLine(TraceEventType.Warning, "Unable to write text to {0}", Path); Log.WriteLine(TraceEventType.Warning, LogUtils.FormatException(Ex)); } return Result; } /// /// Delegate use by RunSingleInstance /// /// /// public delegate int MainProc(object Param); /// /// Runs the specified delegate checking if this is the only instance of the application. /// /// /// /// Exit code. public static int RunSingleInstance(MainProc Main, object Param) { if (Environment.GetEnvironmentVariable("uebp_UATMutexNoWait") == "1") { return Main(Param); } var Result = 1; var bCreatedMutex = false; var LocationHash = InternalUtils.ExecutingAssemblyLocation.GetHashCode(); var MutexName = "Global/" + Path.GetFileNameWithoutExtension(ExecutingAssemblyLocation) + "_" + LocationHash.ToString() + "_Mutex"; using (Mutex SingleInstanceMutex = new Mutex(true, MutexName, out bCreatedMutex)) { if (!bCreatedMutex) { Log.WriteLine(TraceEventType.Warning, "Another instance of {0} is already running. Waiting until it exists.", ExecutingAssemblyLocation); // If this instance didn't create the mutex, wait for the existing mutex to be released by the mutex's creator. SingleInstanceMutex.WaitOne(); } else { Log.WriteLine(TraceEventType.Verbose, "No other instance of {0} is running.", ExecutingAssemblyLocation); } Result = Main(Param); SingleInstanceMutex.ReleaseMutex(); } return Result; } /// /// Path to the executable which runs this code. /// public static string ExecutingAssemblyDirectory { get { return CommandUtils.CombinePaths(Path.GetDirectoryName(ExecutingAssemblyLocation)); } } /// /// Filename of the executable which runs this code. /// public static string ExecutingAssemblyLocation { get { return CommandUtils.CombinePaths(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath); } } /// /// Version info of the executable which runs this code. /// public static FileVersionInfo ExecutableVersion { get { return FileVersionInfo.GetVersionInfo(ExecutingAssemblyLocation); } } } #endregion #region VersionFileReader /// /// This is to ensure that UAT can produce version strings precisely compatible /// with FEngineVersion. /// /// WARNING: If FEngineVersion compatibility changes, this code needs to be updated. /// public class FEngineVersionSupport { /// /// The version info read from the Version header. The populated fields will be Major, Minor, and Build from the MAJOR, MINOR, and PATCH lines, respectively. /// Expects lines like: /// #define APP_MAJOR_VERSION 0 /// #define APP_MINOR_VERSION 0 /// #define APP_PATCH_VERSION 0 /// public readonly Version Version; /// /// The changelist this version is associated with /// public readonly int Changelist; /// /// The Branch name associated with the version /// public readonly string BranchName; /// /// Reads a Version.h file, looking for macros that define the MAJOR/MINOR/PATCH version fields. Expected to match the Version.h in the engine. /// /// Version.h file to read. /// Version that puts the Major/Minor/Patch fields in the Major/Minor/Build fields, respectively. public static Version ReadVersionFromFile(string Filename) { var regex = new Regex(@"#define.+_(?MAJOR|MINOR|PATCH)_VERSION\s+(?.+)", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); var foundElements = new Dictionary(3); foreach (var line in File.ReadLines(Filename)) { try { var match = regex.Match(line); if (match.Success) { foundElements.Add(match.Groups["Type"].Value, int.Parse(match.Groups["Value"].Value)); } } catch (Exception ex) { throw new AutomationException(string.Format("Failed to parse line {0} in version file {1}", line, Filename), ex); } } // must find all three parts to accept the version file. if (foundElements.Keys.Intersect(new[] { "MAJOR", "MINOR", "PATCH" }).Count() != 3) { throw new AutomationException("Failed to find MAJOR, MINOR, and PATCH fields from version file {0}", Filename); } CommandUtils.Log("Read {0}.{1}.{2} from {3}", foundElements["MAJOR"], foundElements["MINOR"], foundElements["PATCH"], Filename); return new Version(foundElements["MAJOR"], foundElements["MINOR"], foundElements["PATCH"]); } /// /// Ctor that takes a pre-determined Version. Gets the Changelist and BranchName from the current . /// /// Predetermined version. /// Predetermined changelist (optional) /// Predetermined branch name (optional) public FEngineVersionSupport(Version InVersion, int InChangelist = -1, string InBranchName = null) { Version = InVersion; if (InChangelist <= 0) { Changelist = CommandUtils.P4Enabled ? CommandUtils.P4Env.Changelist : 0; } else { Changelist = InChangelist; } if (String.IsNullOrEmpty(InBranchName)) { BranchName = CommandUtils.P4Enabled ? CommandUtils.P4Env.BuildRootEscaped : "UnknownBranch"; } else { BranchName = InBranchName; } } /// /// Gets a version string compatible with FEngineVersion's native parsing code. /// /// /// The format looks like: Major.Minor.Build-Changelist+BranchName. /// /// public override string ToString() { return String.Format("{0}.{1}.{2}-{3}+{4}", Version.Major, Version.Minor, Version.Build, Changelist.ToString("0000000"), BranchName); } /// /// Ctor initializes with the values from the supplied Version file. The BranchName and CL are also taken from the current . /// /// Full path to the file with the version info. /// Predetermined changelist (optional) /// Predetermined branch name (optional) public static FEngineVersionSupport FromVersionFile(string Filename, int InChangelist = -1, string InBranchName = null) { return new FEngineVersionSupport(ReadVersionFromFile(Filename), InChangelist, InBranchName); } /// /// Creates a from a string that matches the format given in . /// /// Version string that should match the FEngineVersion::ToString() format. /// Optional parameter which if set to true, allows version strings with no version number specified. /// a new instance with fields initialized to the match those given in the string. public static FEngineVersionSupport FromString(string versionString, bool bAllowNoVersion = false) { try { if (bAllowNoVersion && versionString.StartsWith("++depot")) { // This form of version is used when a product has no major.minor.patch version // E.g. ++depot+UE4-ProdName-CL-12345678 var clSplit = versionString.Split(new string[] { "-CL-" }, 2, StringSplitOptions.None); var dashSplit = clSplit[1].Split(new[] { '-' }, 2); var changelist = int.Parse(dashSplit[0]); var branchName = clSplit[0]; return new FEngineVersionSupport(new Version(0, 0, 0), changelist, branchName); } else { // This is the standard Rocket versioning scheme, e.g. "4.5.0-12345678+++depot+UE4" var dotSplit = versionString.Split(new[] { '.' }, 3); var dashSplit = dotSplit[2].Split(new[] { '-' }, 2); var plusSplit = dashSplit[1].Split(new[] { '+' }, 2); var major = int.Parse(dotSplit[0]); var minor = int.Parse(dotSplit[1]); var patch = int.Parse(dashSplit[0]); var changelist = int.Parse(plusSplit[0]); var branchName = plusSplit[1]; return new FEngineVersionSupport(new Version(major, minor, patch), changelist, branchName); } } catch (Exception ex) { throw new AutomationException(string.Format("Failed to parse {0} as an FEngineVersion compatible string", versionString), ex); } } public static DateTime BuildTime() { string VerFile = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "Engine", "Build", "build.properties"); var VerLines = CommandUtils.ReadAllText(VerFile); var SearchFor = "TimestampForBVT="; int Index = VerLines.IndexOf(SearchFor); if (Index < 0) { throw new AutomationException("Could not find {0} in {1} for file {2}", SearchFor, VerLines, VerFile); } Index = Index + SearchFor.Length; var Parts = VerLines.Substring(Index).Split('.', '_', '-', '\n', '\r', '\t'); DateTime Result = new DateTime(int.Parse(Parts[0]), int.Parse(Parts[1]), int.Parse(Parts[2]), int.Parse(Parts[3]), int.Parse(Parts[4]), int.Parse(Parts[5])); CommandUtils.Log("Current Build Time is {0}", Result); return Result; } } #endregion #region VersionFileUpdater /// /// VersionFileUpdater. /// public class VersionFileUpdater { /// /// Constructor /// public VersionFileUpdater(string Filename) { MyFile = new FileInfo(Filename); Lines = new List(InternalUtils.SafeReadAllLines(Filename)); if (CommandUtils.IsNullOrEmpty(Lines)) { throw new AutomationException("Version file {0} was empty or not found!", Filename); } } /// /// Doc /// public void ReplaceLine(string StartOfLine, string ReplacementRHS) { for (int Index = 0; Index < Lines.Count; ++Index) { if (Lines[Index].StartsWith(StartOfLine)) { Lines[Index] = StartOfLine + ReplacementRHS; return; } } throw new AutomationException("Unable to find line {0} in {1}", StartOfLine, MyFile.FullName); } /// /// Doc /// public void ReplaceOrAddLine(string StartOfLine, string ReplacementRHS) { if (Contains(StartOfLine)) { ReplaceLine(StartOfLine, ReplacementRHS); } else { AddLine(""); AddLine(StartOfLine + ReplacementRHS); } } /// /// Adds a new line to the version file /// /// public void AddLine(string Line) { Lines.Add(Line); } /// /// Doc /// public void SetAssemblyInformationalVersion(string NewInformationalVersion) { // This searches for the AssemblyInformationalVersion string. Most the mess is to allow whitespace in places that are possible. // Captures the string into a group called "Ver" for replacement. var regex = new Regex(@"\[assembly:\s+AssemblyInformationalVersion\s*\(\s*""(?.*)""\s*\)\s*]", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); foreach (var Index in Enumerable.Range(0, Lines.Count)) { var line = Lines[Index]; var match = regex.Match(line); if (match.Success) { var verGroup = match.Groups["Ver"]; var sb = new StringBuilder(line); sb.Remove(verGroup.Index, verGroup.Length); sb.Insert(verGroup.Index, NewInformationalVersion); Lines[Index] = sb.ToString(); return; } } throw new AutomationException("Failed to find the AssemblyInformationalVersion attribute in {1}", MyFile.FullName); } /// /// Doc /// public void Commit() { MyFile.IsReadOnly = false; if (!InternalUtils.SafeWriteAllLines(MyFile.FullName, Lines.ToArray())) { throw new AutomationException("Unable to update version info in {0}", MyFile.FullName); } } /// /// Checks if the version file contains the specified string. /// /// String to look for. /// public bool Contains(string Text, bool CaseSensitive = true) { foreach (var Line in Lines) { if (Line.IndexOf(Text, CaseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase) >= 0) { return true; } } return false; } /// /// Doc /// protected FileInfo MyFile; /// /// Doc /// protected List Lines; } #endregion #region Case insensitive dictionary /// /// Equivalent of case insensitve Dictionary /// /// public class CaselessDictionary : Dictionary { public CaselessDictionary() : base(StringComparer.InvariantCultureIgnoreCase) { } public CaselessDictionary(int Capacity) : base(Capacity, StringComparer.InvariantCultureIgnoreCase) { } public CaselessDictionary(IDictionary Dict) : base(Dict, StringComparer.InvariantCultureIgnoreCase) { } } #endregion }