using System; using System.Collections.Generic; using System.IO; namespace Semmle.Autobuild.Shared { /// /// A build script. /// public abstract class BuildScript { /// /// Run this build script. /// /// /// The interface used to implement the build actions. /// /// /// A call back that is called every time a new process is started. The /// argument to the call back is a textual representation of the process. /// /// /// A call back that is called every time a new process exits. The first /// argument to the call back is the exit code, and the second argument is /// an exit message. /// /// The exit code from this build script. public abstract int Run(IBuildActions actions, Action startCallback, Action exitCallBack); /// /// Run this build command. /// /// /// The interface used to implement the build actions. /// /// /// A call back that is called every time a new process is started. The /// argument to the call back is a textual representation of the process. /// /// /// A call back that is called every time a new process exits. The first /// argument to the call back is the exit code, and the second argument is /// an exit message. /// /// Contents of standard out. /// The exit code from this build script. public abstract int Run(IBuildActions actions, Action startCallback, Action exitCallBack, out IList stdout); private class BuildCommand : BuildScript { private readonly string exe, arguments; private readonly string? workingDirectory; private readonly IDictionary? environment; private readonly bool silent; /// /// Create a simple build command. /// /// The executable to run. /// The arguments to the executable, or null. /// Whether this command should run silently. /// The working directory (null for current directory). /// Additional environment variables. public BuildCommand(string exe, string? argumentsOpt, bool silent, string? workingDirectory = null, IDictionary? environment = null) { this.exe = exe; this.arguments = argumentsOpt ?? ""; this.silent = silent; this.workingDirectory = workingDirectory; this.environment = environment; } public override string ToString() => exe + " " + arguments; public override int Run(IBuildActions actions, Action startCallback, Action exitCallBack) { startCallback(this.ToString(), silent); var ret = 1; var retMessage = ""; try { ret = actions.RunProcess(exe, arguments, workingDirectory, environment); } catch (Exception ex) when (ex is System.ComponentModel.Win32Exception || ex is FileNotFoundException) { retMessage = ex.Message; } exitCallBack(ret, retMessage, silent); return ret; } public override int Run(IBuildActions actions, Action startCallback, Action exitCallBack, out IList stdout) { startCallback(this.ToString(), silent); var ret = 1; var retMessage = ""; try { ret = actions.RunProcess(exe, arguments, workingDirectory, environment, out stdout); } catch (Exception ex) when (ex is System.ComponentModel.Win32Exception || ex is FileNotFoundException) { retMessage = ex.Message; stdout = Array.Empty(); } exitCallBack(ret, retMessage, silent); return ret; } } private class ReturnBuildCommand : BuildScript { private readonly Func func; public ReturnBuildCommand(Func func) { this.func = func; } public override int Run(IBuildActions actions, Action startCallback, Action exitCallBack) => func(actions); public override int Run(IBuildActions actions, Action startCallback, Action exitCallBack, out IList stdout) { stdout = Array.Empty(); return func(actions); } } private class BindBuildScript : BuildScript { private readonly BuildScript s1; private readonly Func, int, BuildScript>? s2a; private readonly Func? s2b; public BindBuildScript(BuildScript s1, Func, int, BuildScript> s2) { this.s1 = s1; this.s2a = s2; } public BindBuildScript(BuildScript s1, Func s2) { this.s1 = s1; this.s2b = s2; } public override int Run(IBuildActions actions, Action startCallback, Action exitCallBack) { int ret1; if (s2a is not null) { ret1 = s1.Run(actions, startCallback, exitCallBack, out var stdout1); return s2a(stdout1, ret1).Run(actions, startCallback, exitCallBack); } if (s2b is not null) { ret1 = s1.Run(actions, startCallback, exitCallBack); return s2b(ret1).Run(actions, startCallback, exitCallBack); } throw new InvalidOperationException("Unexpected error"); } public override int Run(IBuildActions actions, Action startCallback, Action exitCallBack, out IList stdout) { var ret1 = s1.Run(actions, startCallback, exitCallBack, out var stdout1); var ret2 = (s2a is not null ? s2a(stdout1, ret1) : s2b!(ret1)).Run(actions, startCallback, exitCallBack, out var stdout2); var @out = new List(); @out.AddRange(stdout1); @out.AddRange(stdout2); stdout = @out; return ret2; } } /// /// Creates a simple build script that runs the specified exe. /// /// The arguments to the executable, or null. /// Whether the executable should run silently. /// The working directory (null for current directory). /// Additional environment variables. public static BuildScript Create(string exe, string? argumentsOpt, bool silent, string? workingDirectory, IDictionary? environment) => new BuildCommand(exe, argumentsOpt, silent, workingDirectory, environment); /// /// Creates a simple build script that runs the specified function. /// public static BuildScript Create(Func func) => new ReturnBuildCommand(func); /// /// Creates a build script that downloads the specified file. /// public static BuildScript DownloadFile(string address, string fileName, Action exceptionCallback) => Create(actions => { if (actions.GetDirectoryName(fileName) is string dir && !string.IsNullOrWhiteSpace(dir)) actions.CreateDirectory(dir); try { actions.DownloadFile(address, fileName); return 0; } catch (Exception e) { exceptionCallback(e); return 1; } }); /// /// Creates a build script that runs , followed by running the script /// produced by on the exit code from . /// public static BuildScript Bind(BuildScript s1, Func s2) => new BindBuildScript(s1, s2); /// /// Creates a build script that runs , followed by running the script /// produced by on the exit code and standard output from /// . /// public static BuildScript Bind(BuildScript s1, Func, int, BuildScript> s2) => new BindBuildScript(s1, s2); private const int successCode = 0; /// /// The empty build script that always returns exit code 0. /// public static BuildScript Success { get; } = Create(actions => successCode); private const int failureCode = 1; /// /// The empty build script that always returns exit code 1. /// public static BuildScript Failure { get; } = Create(actions => failureCode); private static bool Succeeded(int i) => i == successCode; public static BuildScript operator &(BuildScript s1, BuildScript s2) => new BindBuildScript(s1, ret1 => Succeeded(ret1) ? s2 : Create(actions => ret1)); public static BuildScript operator &(BuildScript s1, Func s2) => new BindBuildScript(s1, ret1 => Succeeded(ret1) ? s2() : Create(actions => ret1)); public static BuildScript operator |(BuildScript s1, BuildScript s2) => new BindBuildScript(s1, ret1 => Succeeded(ret1) ? Success : s2); public static BuildScript operator |(BuildScript s1, Func s2) => new BindBuildScript(s1, ret1 => Succeeded(ret1) ? Success : s2()); /// /// Creates a build script that attempts to run the build script , /// always returning a successful exit code. /// public static BuildScript Try(BuildScript s) => s | Success; /// /// Creates a build script that deletes the given directory. /// public static BuildScript DeleteDirectory(string dir) => Create(actions => { if (string.IsNullOrEmpty(dir) || !actions.DirectoryExists(dir)) return failureCode; try { actions.DirectoryDelete(dir, true); } catch // lgtm[cs/catch-of-all-exceptions] { return failureCode; } return successCode; }); /// /// Creates a build script that deletes the given file. /// public static BuildScript DeleteFile(string file) => Create(actions => { if (string.IsNullOrEmpty(file) || !actions.FileExists(file)) return failureCode; try { actions.FileDelete(file); } catch // lgtm[cs/catch-of-all-exceptions] { return failureCode; } return successCode; }); } }