// 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;
});
}
}
}