using Semmle.Util;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Net.Http;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace Semmle.Autobuild.Shared
{
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 arm.
///
/// True if we are running on arm.
bool IsArm();
///
/// 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("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);
///
/// 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, bool redirectStandardOutput)
{
var pi = new ProcessStartInfo(exe, arguments)
{
UseShellExecute = false,
RedirectStandardOutput = redirectStandardOutput
};
if (workingDirectory is not null)
pi.WorkingDirectory = workingDirectory;
// Environment variables can only be used when not redirecting stdout
if (!redirectStandardOutput)
{
if (environment is not null)
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, true);
using var p = new Process
{
StartInfo = pi
};
p.StartInfo.RedirectStandardError = true;
p.OutputDataReceived += new DataReceivedEventHandler((sender, e) => onOutput(e.Data));
p.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => onError(e.Data));
p.Start();
p.BeginErrorReadLine();
p.BeginOutputReadLine();
p.WaitForExit();
return p.ExitCode;
}
int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? environment)
{
var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment, false);
using var p = Process.Start(pi);
if (p is null)
{
return -1;
}
p.WaitForExit();
return p.ExitCode;
}
int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? environment, out IList stdOut)
{
var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment, true);
return pi.ReadOutput(out stdOut);
}
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.IsArm() =>
RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ||
RuntimeInformation.ProcessArchitecture == Architecture.Arm;
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);
private static async Task DownloadFileAsync(string address, string filename)
{
using var httpClient = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, address);
using var contentStream = await (await httpClient.SendAsync(request)).Content.ReadAsStreamAsync();
using var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await contentStream.CopyToAsync(stream);
}
public void DownloadFile(string address, string fileName) =>
DownloadFileAsync(address, fileName).Wait();
public IDiagnosticsWriter CreateDiagnosticsWriter(string filename) => new DiagnosticsStream(filename);
public static IBuildActions Instance { get; } = new SystemBuildActions();
}
}