// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; namespace Flax.Build { /// /// The utilities. /// public static class Utilities { /// /// Gets the hash code for the string (the same for all platforms). Matches Engine algorithm for string hashing. /// /// The input string. /// The file size text. public static uint GetHashCode(string str) { uint hash = 5381; if (str != null) { for (int i = 0; i < str.Length; i++) { char c = str[i]; hash = ((hash << 5) + hash) + (uint)c; } } return hash; } /// /// Gets the empty array of the given type (shared one). /// /// The type. /// The empty array object. public static T[] GetEmptyArray() { #if USE_NETCORE return Array.Empty(); #else return Enumerable.Empty() as T[]; #endif } /// /// Gets the static field value from a given type. /// /// Name of the type. /// Name of the field. /// The field value. public static object GetStaticValue(string typeName, string fieldName) { var type = Type.GetType(typeName); if (type == null) throw new Exception($"Cannot find type \'{typeName}\'."); var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Static); if (field == null) throw new Exception($"Cannot find static public field \'{fieldName}\' in \'{typeName}\'."); return field.GetValue(null); } /// /// Gets the size of the file as a readable string. /// /// The path. /// The file size text. public static string GetFileSize(string path) { return (Math.Floor(new FileInfo(path).Length / 1024.0f / 1024 * 100) / 100) + " MB"; } /// /// Adds the range of the items to the hash set. /// /// The item type. /// The hash set to modify. /// The items collection to append. public static void AddRange(this HashSet source, IEnumerable items) { foreach (T item in items) { source.Add(item); } } /// /// Gets the two way enumerator for the given enumerable collection. /// /// The item type. /// The source collection. /// The enumerator. public static ITwoWayEnumerator GetTwoWayEnumerator(this IEnumerable source) { if (source == null) throw new ArgumentNullException(nameof(source)); return new TwoWayEnumerator(source.GetEnumerator()); } /// /// Copies the file. /// /// The source file path. /// The destination file path. /// if the destination file can be overwritten; otherwise, . public static void FileCopy(string srcFilePath, string dstFilePath, bool overwrite = true) { if (string.IsNullOrEmpty(srcFilePath)) throw new ArgumentNullException(nameof(srcFilePath)); if (string.IsNullOrEmpty(dstFilePath)) throw new ArgumentNullException(nameof(dstFilePath)); Log.Verbose(srcFilePath + " -> " + dstFilePath); try { File.Copy(srcFilePath, dstFilePath, overwrite); } catch (Exception ex) { if (!File.Exists(srcFilePath)) Log.Error("Failed to copy file. Missing source file."); else Log.Error("Failed to copy file. " + ex.Message); throw; } } /// /// Copies the directories. /// /// The source directory path. /// The destination directory path. /// If set to true copy sub-directories (recursive copy operation). /// If set to true override target files if any is existing. /// The custom filter for the filenames to copy. Can be used to select files to copy. Null if unused. public static void DirectoryCopy(string srcDirectoryPath, string dstDirectoryPath, bool copySubdirs = true, bool overrideFiles = false, string searchPattern = null) { Log.Verbose(srcDirectoryPath + " -> " + dstDirectoryPath); var dir = new DirectoryInfo(srcDirectoryPath); if (!dir.Exists) { throw new DirectoryNotFoundException("Missing source directory to copy. " + srcDirectoryPath); } if (!Directory.Exists(dstDirectoryPath)) { Directory.CreateDirectory(dstDirectoryPath); } var files = searchPattern != null ? dir.GetFiles(searchPattern) : dir.GetFiles(); for (int i = 0; i < files.Length; i++) { string tmp = Path.Combine(dstDirectoryPath, files[i].Name); File.Copy(files[i].FullName, tmp, overrideFiles); } if (copySubdirs) { var dirs = dir.GetDirectories(); for (int i = 0; i < dirs.Length; i++) { string tmp = Path.Combine(dstDirectoryPath, dirs[i].Name); DirectoryCopy(dirs[i].FullName, tmp, true, overrideFiles, searchPattern); } } } private static void DirectoryDelete(DirectoryInfo dir) { var subdirs = dir.GetDirectories(); foreach (var subdir in subdirs) { DirectoryDelete(subdir); } var files = dir.GetFiles(); foreach (var file in files) { try { file.Delete(); } catch (UnauthorizedAccessException) { File.SetAttributes(file.FullName, FileAttributes.Normal); file.Delete(); } } dir.Delete(); } /// /// Deletes the file. /// /// The file path. public static void FileDelete(string filePath) { var file = new FileInfo(filePath); if (!file.Exists) return; file.Delete(); } /// /// Deletes the directory. /// /// The directory path. /// if set to true with sub-directories (recursive delete operation). public static void DirectoryDelete(string directoryPath, bool withSubdirs = true) { var dir = new DirectoryInfo(directoryPath); if (!dir.Exists) return; if (withSubdirs) DirectoryDelete(dir); else dir.Delete(); } /// /// Deletes the files inside a directory. /// /// The directory path. /// The custom filter for the filenames to delete. Can be used to select files to delete. Null if unused. /// if set to true with sub-directories (recursive delete operation). public static void FilesDelete(string directoryPath, string searchPattern = null, bool withSubdirs = true) { if (!Directory.Exists(directoryPath)) return; if (searchPattern == null) searchPattern = "*"; var files = Directory.GetFiles(directoryPath, searchPattern, withSubdirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); for (int i = 0; i < files.Length; i++) { File.Delete(files[i]); } } /// /// Deletes the directories inside a directory. /// /// The directory path. /// The custom filter for the directories to delete. Can be used to select files to delete. Null if unused. /// if set to true with sub-directories (recursive delete operation). public static void DirectoriesDelete(string directoryPath, string searchPattern = null, bool withSubdirs = true) { if (!Directory.Exists(directoryPath)) return; if (searchPattern == null) searchPattern = "*"; var directories = Directory.GetDirectories(directoryPath, searchPattern, withSubdirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); for (int i = 0; i < directories.Length; i++) { DirectoryDelete(directories[i]); } } /// /// The process run options. /// [Flags] public enum RunOptions { /// /// The none. /// None = 0, /// /// The application must exist. /// AppMustExist = 1 << 0, /// /// Skip waiting for exit. /// NoWaitForExit = 1 << 1, /// /// Skip standard output redirection to log. /// NoStdOutRedirect = 1 << 2, /// /// Skip logging of command run. /// NoLoggingOfRunCommand = 1 << 3, /// /// Uses UTF-8 output encoding format. /// UTF8Output = 1 << 4, /// /// Skips logging of run duration. /// NoLoggingOfRunDuration = 1 << 5, /// /// Throws exception when app returns non-zero return code. /// ThrowExceptionOnError = 1 << 6, /// /// Logs program output to the console, otherwise only when using verbose log. /// ConsoleLogOutput = 1 << 7, /// /// The default options. /// Default = AppMustExist, } private static void StdLogInfo(object sender, DataReceivedEventArgs e) { if (e.Data != null) { Log.Info(e.Data); } } private static void StdLogVerbose(object sender, DataReceivedEventArgs e) { if (e.Data != null) { Log.Verbose(e.Data); } } /// /// Runs the external program. /// /// Program filename. /// Commandline /// Optional Input for the program (will be provided as stdin) /// Optional custom workspace directory. Use null to keep the same directory. /// Defines the options how to run. See RunOptions. /// Custom environment variables to pass to the child process. /// The exit code of the program. public static int Run(string app, string commandLine = null, string input = null, string workspace = null, RunOptions options = RunOptions.Default, Dictionary envVars = null) { if (string.IsNullOrEmpty(app)) throw new ArgumentNullException(nameof(app), "Missing app to run."); // Check if the application exists, including the PATH directories if (options.HasFlag(RunOptions.AppMustExist) && !File.Exists(app)) { bool existsInPath = false; if (!app.Contains(Path.DirectorySeparatorChar) && !app.Contains(Path.AltDirectorySeparatorChar)) { string[] pathDirectories = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator); foreach (string pathDirectory in pathDirectories) { string tryApp = Path.Combine(pathDirectory, app); if (File.Exists(tryApp)) { app = tryApp; existsInPath = true; break; } } } if (!existsInPath) { throw new Exception(string.Format("Couldn't find the executable to run: {0}", app)); } } Stopwatch stopwatch = Stopwatch.StartNew(); if (!options.HasFlag(RunOptions.NoLoggingOfRunCommand)) { var msg = "Running: " + app + (string.IsNullOrEmpty(commandLine) ? "" : " " + commandLine); if (options.HasFlag(RunOptions.ConsoleLogOutput)) Log.Info(msg); else Log.Verbose(msg); } bool redirectStdOut = (options & RunOptions.NoStdOutRedirect) != RunOptions.NoStdOutRedirect; Process proc = new Process(); proc.StartInfo.FileName = app; proc.StartInfo.Arguments = commandLine != null ? commandLine : ""; proc.StartInfo.UseShellExecute = false; proc.StartInfo.RedirectStandardInput = input != null; proc.StartInfo.CreateNoWindow = true; if (workspace != null) { proc.StartInfo.WorkingDirectory = workspace; } if (redirectStdOut) { proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardError = true; if (options.HasFlag(RunOptions.ConsoleLogOutput)) { proc.OutputDataReceived += StdLogInfo; proc.ErrorDataReceived += StdLogInfo; } else { proc.OutputDataReceived += StdLogVerbose; proc.ErrorDataReceived += StdLogVerbose; } } if (envVars != null) { foreach (var env in envVars) { if (env.Key == "PATH") { proc.StartInfo.EnvironmentVariables[env.Key] = env.Value + ';' + proc.StartInfo.EnvironmentVariables[env.Key]; } else { proc.StartInfo.EnvironmentVariables[env.Key] = env.Value; } } } if ((options & RunOptions.UTF8Output) == RunOptions.UTF8Output) { proc.StartInfo.StandardOutputEncoding = new UTF8Encoding(false, false); } proc.Start(); if (redirectStdOut) { proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); } if (string.IsNullOrEmpty(input) == false) { proc.StandardInput.WriteLine(input); proc.StandardInput.Close(); } if (!options.HasFlag(RunOptions.NoWaitForExit)) { proc.WaitForExit(); } int result = -1; if (!options.HasFlag(RunOptions.NoWaitForExit)) { stopwatch.Stop(); result = proc.ExitCode; if (!options.HasFlag(RunOptions.NoLoggingOfRunCommand) || options.HasFlag(RunOptions.NoLoggingOfRunDuration)) { Log.Info(string.Format("Took {0}s to run {1}, ExitCode={2}", stopwatch.Elapsed.TotalSeconds, Path.GetFileName(app), result)); } if (result != 0 && options.HasFlag(RunOptions.ThrowExceptionOnError)) { var format = options.HasFlag(RunOptions.NoLoggingOfRunCommand) ? "App failed with exit code {2}." : "{0} {1} failed with exit code {2}"; throw new Exception(string.Format(format, app, commandLine, result)); } } return result; } /// /// Runs the process and reds its standard output as a string. /// /// The executable file path. /// The custom arguments. /// Returned process output. public static string ReadProcessOutput(string filename, string args = null) { Process p = new Process { StartInfo = { FileName = filename, Arguments = args, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, } }; p.Start(); return p.StandardOutput.ReadToEnd().Trim(); } /// /// Constructs a relative path from the given base directory. /// /// The source path to convert from absolute into a relative format. /// The directory to create a relative path from. /// Thew relative path from the given directory. public static string MakePathRelativeTo(string path, string directory) { int sharedDirectoryLength = -1; for (int i = 0;; i++) { if (i == path.Length) { // Paths are the same if (i == directory.Length) { return string.Empty; } // Finished on a complete directory if (directory[i] == Path.DirectorySeparatorChar) { sharedDirectoryLength = i; } break; } if (i == directory.Length) { // End of the directory name starts with a boundary for the current name if (path[i] == Path.DirectorySeparatorChar) { sharedDirectoryLength = i; } break; } if (string.Compare(path, i, directory, i, 1, StringComparison.OrdinalIgnoreCase) != 0) { break; } if (path[i] == Path.DirectorySeparatorChar) { sharedDirectoryLength = i; } } // No shared path found if (sharedDirectoryLength == -1) { return path; } // Add all the '..' separators to get back to the shared directory, StringBuilder result = new StringBuilder(); for (int i = sharedDirectoryLength + 1; i < directory.Length; i++) { // Move up a directory result.Append(".."); result.Append(Path.DirectorySeparatorChar); // Scan to the next directory separator while (i < directory.Length && directory[i] != Path.DirectorySeparatorChar) { i++; } } if (sharedDirectoryLength + 1 < path.Length) { result.Append(path, sharedDirectoryLength + 1, path.Length - sharedDirectoryLength - 1); } return result.ToString(); } /// /// Normalizes the path to the standard Flax format (all separators are '/' except for drive 'C:\'). /// /// The path. /// The normalized path. public static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) return path; var chars = path.ToCharArray(); // Convert all '\' to '/' for (int i = 0; i < chars.Length; i++) { if (chars[i] == '\\') chars[i] = '/'; } // Fix case 'C:/' to 'C:\' if (chars.Length > 2 && !char.IsDigit(chars[0]) && chars[1] == ':') { chars[2] = '\\'; } return new string(chars); } /// /// Removes the relative parts from the path. For instance it replaces 'xx/yy/../zz' with 'xx/zz'. /// /// The input path. /// The output path. public static string RemovePathRelativeParts(string path) { if (string.IsNullOrEmpty(path)) return path; path = NormalizePath(path); string[] components = path.Split('/'); Stack stack = new Stack(); foreach (var bit in components) { if (bit == "..") { if (stack.Count != 0) { var popped = stack.Pop(); var windowsDriveStart = popped.IndexOf('\\'); if (popped == "..") { stack.Push(popped); stack.Push(bit); } else if (windowsDriveStart != -1) { stack.Push(popped.Substring(windowsDriveStart + 1)); } } else { stack.Push(bit); } } else if (bit == ".") { // Skip /./ } else { stack.Push(bit); } } bool isRooted = path.StartsWith("/"); string result = string.Join(Path.DirectorySeparatorChar.ToString(), stack.Reverse()); if (isRooted && result[0] != '/') result = result.Insert(0, "/"); return result; } /// /// Writes the file contents. Before writing reads the existing file and discards operation if contents are the same. /// /// The path. /// The file contents. /// True if file has been modified, otherwise false. public static bool WriteFileIfChanged(string path, string contents) { if (File.Exists(path)) { string oldContents = null; try { oldContents = File.ReadAllText(path); } catch (Exception) { Log.Warning(string.Format("Failed to read file contents while trying to save it.", path)); } if (string.Equals(contents, oldContents, StringComparison.OrdinalIgnoreCase)) { Log.Verbose(string.Format("Skipped saving file to {0}", path)); return false; } } try { Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllText(path, contents, new UTF8Encoding()); Log.Verbose(string.Format("Saved file to {0}", path)); } catch { Log.Error(string.Format("Failed to save file {0}", path)); throw; } return true; } /// /// Replaces the given text with other one in the files. /// /// The relative or absolute path to the directory to search. /// The search string to match against the names of files in . This parameter can contain a combination of valid literal path and wildcard (* and ?) characters (see Remarks), but doesn't support regular expressions. /// One of the enumeration values that specifies whether the search operation should include all subdirectories or only the current directory. /// The text to replace. /// The value to replace to. public static void ReplaceInFiles(string folderPath, string searchPattern, SearchOption searchOption, string findWhat, string replaceWith) { var files = Directory.GetFiles(folderPath, searchPattern, searchOption); ReplaceInFiles(files, findWhat, replaceWith); } /// /// Replaces the given text with other one in the files. /// /// The list of the files to process. /// The text to replace. /// The value to replace to. public static void ReplaceInFiles(string[] files, string findWhat, string replaceWith) { foreach (var file in files) { ReplaceInFile(file, findWhat, replaceWith); } } /// /// Replaces the given text with other one in the file. /// /// The file to process. /// The text to replace. /// The value to replace to. public static void ReplaceInFile(string file, string findWhat, string replaceWith) { var text = File.ReadAllText(file); text = text.Replace(findWhat, replaceWith); File.WriteAllText(file, text); } /// /// Returns back the exe ext for the current platform /// public static string GetPlatformExecutableExt() { var extEnding = ".exe"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { extEnding = ""; } return extEnding; } /// /// Sorts the directories by name assuming they contain version text. Sorted from lowest to the highest version. /// /// The paths array to sort. public static void SortVersionDirectories(string[] paths) { if (paths == null || paths.Length == 0) return; Array.Sort(paths, (a, b) => { Version va, vb; if (Version.TryParse(a, out va)) { if (Version.TryParse(b, out vb)) return va.CompareTo(vb); return 1; } if (Version.TryParse(b, out vb)) return -1; return 0; }); } } }