using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Xml; using Semmle.Util.Logging; namespace Semmle.Util { public delegate void BuildOutputHandler(string? data); /// /// Wrapper around system calls so that the build scripts can be unit-tested. /// public interface IBuildActions { /// /// Runs a process, captures its output, and provides it asynchronously. /// /// The exe to run. /// The other command line arguments. /// The working directory (null for current directory). /// Additional environment variables. /// A handler for stdout output. /// A handler for stderr output. /// The process exit code. int RunProcess(string exe, string args, string? workingDirectory, IDictionary? env, BuildOutputHandler onOutput, BuildOutputHandler onError); /// /// Runs a process and captures its output. /// /// The exe to run. /// The other command line arguments. /// The working directory (null for current directory). /// Additional environment variables. /// The lines of stdout. /// The process exit code. int RunProcess(string exe, string args, string? workingDirectory, IDictionary? env, out IList stdOut); /// /// Runs a process but does not capture its output. /// /// The exe to run. /// The other command line arguments. /// The working directory (null for current directory). /// Additional environment variables. /// The process exit code. int RunProcess(string exe, string args, string? workingDirectory, IDictionary? env); /// /// Tests whether a file exists, File.Exists(). /// /// The filename. /// True iff the file exists. bool FileExists(string file); /// /// Tests whether a directory exists, Directory.Exists(). /// /// The directory name. /// True iff the directory exists. bool DirectoryExists(string dir); /// /// Deletes a file, File.Delete(). /// /// The filename. void FileDelete(string file); /// /// Deletes a directory, Directory.Delete(). /// void DirectoryDelete(string dir, bool recursive); /// /// Creates all directories and subdirectories in the specified path unless they already exist. /// void CreateDirectory(string path); /// /// Gets an environment variable, Environment.GetEnvironmentVariable(). /// /// The name of the variable. /// The string value, or null if the variable is not defined. string? GetEnvironmentVariable(string name); /// /// Gets the current directory, Directory.GetCurrentDirectory(). /// /// The current directory. string GetCurrentDirectory(); /// /// Enumerates files in a directory, Directory.EnumerateFiles(). /// /// The directory to enumerate. /// A list of filenames, or an empty list. IEnumerable EnumerateFiles(string dir); /// /// Enumerates the directories in a directory, Directory.EnumerateDirectories(). /// /// The directory to enumerate. /// List of subdirectories, or empty list. IEnumerable EnumerateDirectories(string dir); /// /// True if we are running on Windows. /// bool IsWindows(); /// /// Gets a value indicating whether we are running on macOS. /// /// True if we are running on macOS. bool IsMacOs(); /// /// Gets a value indicating whether we are running on Apple Silicon. /// /// True if we are running on Apple Silicon. bool IsRunningOnAppleSilicon(); /// /// Checks if Mono is installed. /// bool IsMonoInstalled(); /// /// Combine path segments, Path.Combine(). /// /// The parts of the path. /// The combined path. string PathCombine(params string[] parts); /// /// Gets the full path for , Path.GetFullPath(). /// string GetFullPath(string path); /// /// Returns the file name and extension of the specified path string. /// [return: NotNullIfNotNull(nameof(path))] string? GetFileName(string? path); /// /// Returns the directory information for the specified path string. /// string? GetDirectoryName(string? path); /// /// Writes contents to file, File.WriteAllText(). /// /// The filename. /// The text. void WriteAllText(string filename, string contents); /// /// Loads the XML document from . /// XmlDocument LoadXml(string filename); string EnvironmentExpandEnvironmentVariables(string s); /// /// Downloads the resource with the specified URI to a local file. /// void DownloadFile(string address, string fileName, ILogger logger); /// /// Creates an for the given . /// /// /// The path suggesting where the diagnostics should be written to. /// /// /// A to which diagnostic entries can be added. /// IDiagnosticsWriter CreateDiagnosticsWriter(string filename); } /// /// An implementation of IBuildActions that actually performs the requested operations. /// public class SystemBuildActions : IBuildActions { void IBuildActions.FileDelete(string file) => File.Delete(file); bool IBuildActions.FileExists(string file) => File.Exists(file); private static ProcessStartInfo GetProcessStartInfo(string exe, string arguments, string? workingDirectory, IDictionary? environment) { var pi = new ProcessStartInfo(exe, arguments) { UseShellExecute = false, RedirectStandardOutput = true }; if (workingDirectory is not null) pi.WorkingDirectory = workingDirectory; environment?.ForEach(kvp => pi.Environment[kvp.Key] = kvp.Value); return pi; } int IBuildActions.RunProcess(string exe, string args, string? workingDirectory, System.Collections.Generic.IDictionary? env, BuildOutputHandler onOutput, BuildOutputHandler onError) { var pi = GetProcessStartInfo(exe, args, workingDirectory, env); pi.RedirectStandardError = true; return pi.ReadOutput(out _, onOut: s => onOutput(s), onError: s => onError(s)); } int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? environment) { var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment); return pi.ReadOutput(out _, onOut: Console.WriteLine, onError: null); } int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? environment, out IList stdOut) { var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment); return pi.ReadOutput(out stdOut, onOut: null, onError: null); } void IBuildActions.DirectoryDelete(string dir, bool recursive) => Directory.Delete(dir, recursive); bool IBuildActions.DirectoryExists(string dir) => Directory.Exists(dir); void IBuildActions.CreateDirectory(string path) => Directory.CreateDirectory(path); string? IBuildActions.GetEnvironmentVariable(string name) => Environment.GetEnvironmentVariable(name); string IBuildActions.GetCurrentDirectory() => Directory.GetCurrentDirectory(); IEnumerable IBuildActions.EnumerateFiles(string dir) => Directory.EnumerateFiles(dir); IEnumerable IBuildActions.EnumerateDirectories(string dir) => Directory.EnumerateDirectories(dir); bool IBuildActions.IsWindows() => Win32.IsWindows(); bool IBuildActions.IsMacOs() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); bool IBuildActions.IsRunningOnAppleSilicon() { var thisBuildActions = (IBuildActions)this; if (!thisBuildActions.IsMacOs()) { return false; } try { thisBuildActions.RunProcess("sysctl", "machdep.cpu.brand_string", workingDirectory: null, env: null, out var stdOut); return stdOut?.Any(s => s?.ToLowerInvariant().Contains("apple") == true) ?? false; } catch (Exception) { return false; } } bool IBuildActions.IsMonoInstalled() { var thisBuildActions = (IBuildActions)this; if (thisBuildActions.IsWindows()) { return false; } try { return 0 == thisBuildActions.RunProcess("mono", "--version", workingDirectory: null, env: null); } catch (Exception) { return false; } } string IBuildActions.PathCombine(params string[] parts) => Path.Combine(parts); void IBuildActions.WriteAllText(string filename, string contents) => File.WriteAllText(filename, contents); XmlDocument IBuildActions.LoadXml(string filename) { var ret = new XmlDocument(); ret.Load(filename); return ret; } string IBuildActions.GetFullPath(string path) => Path.GetFullPath(path); string? IBuildActions.GetFileName(string? path) => Path.GetFileName(path); string? IBuildActions.GetDirectoryName(string? path) => Path.GetDirectoryName(path); public string EnvironmentExpandEnvironmentVariables(string s) => Environment.ExpandEnvironmentVariables(s); public void DownloadFile(string address, string fileName, ILogger logger) => FileUtils.DownloadFile(address, fileName, logger); public IDiagnosticsWriter CreateDiagnosticsWriter(string filename) => new DiagnosticsStream(filename); public static IBuildActions Instance { get; } = new SystemBuildActions(); } public static class BuildActionExtensions { private static void FindFiles(this IBuildActions actions, string dir, int depth, int? maxDepth, IList<(string, int)> results) { foreach (var f in actions.EnumerateFiles(dir)) { results.Add((f, depth)); } if (depth < maxDepth) { foreach (var d in actions.EnumerateDirectories(dir)) { actions.FindFiles(d, depth + 1, maxDepth, results); } } } public static (string path, int depth)[] FindFiles(this IBuildActions actions, string dir, int? maxDepth) { var results = new List<(string, int)>(); actions.FindFiles(dir, 0, maxDepth, results); return results.OrderBy(f => f.Item2).ToArray(); } } }