mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
419 lines
17 KiB
C#
419 lines
17 KiB
C#
using Semmle.Util;
|
|
using Semmle.Util.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace Semmle.Autobuild.Shared
|
|
{
|
|
/// <summary>
|
|
/// A build rule analyses the files in "builder" and outputs a build script.
|
|
/// </summary>
|
|
public interface IBuildRule<TAutobuildOptions> where TAutobuildOptions : AutobuildOptionsShared
|
|
{
|
|
/// <summary>
|
|
/// Analyse the files and produce a build script.
|
|
/// </summary>
|
|
/// <param name="builder">The files and options relating to the build.</param>
|
|
/// <param name="auto">Whether this build rule is being automatically applied.</param>
|
|
BuildScript Analyse(IAutobuilder<TAutobuildOptions> builder, bool auto);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A delegate used to wrap a build script in an environment where an appropriate
|
|
/// version of .NET Core is automatically installed.
|
|
/// </summary>
|
|
public delegate BuildScript WithDotNet<TAutobuildOptions>(IAutobuilder<TAutobuildOptions> builder, Func<IDictionary<string, string>?, BuildScript> f) where TAutobuildOptions : AutobuildOptionsShared;
|
|
|
|
/// <summary>
|
|
/// Exception indicating that environment variables are missing or invalid.
|
|
/// </summary>
|
|
public class InvalidEnvironmentException : Exception
|
|
{
|
|
public InvalidEnvironmentException(string m) : base(m) { }
|
|
}
|
|
|
|
public interface IAutobuilder<out TAutobuildOptions> where TAutobuildOptions : AutobuildOptionsShared
|
|
{
|
|
/// <summary>
|
|
/// Full file paths of files found in the project directory, as well as
|
|
/// their distance from the project root folder. The list is sorted
|
|
/// by distance in ascending order.
|
|
/// </summary>
|
|
IEnumerable<(string, int)> Paths { get; }
|
|
|
|
/// <summary>
|
|
/// Gets all paths matching a particular filename, as well as
|
|
/// their distance from the project root folder. The list is sorted
|
|
/// by distance in ascending order.
|
|
/// </summary>
|
|
/// <param name="name">The filename to find.</param>
|
|
/// <returns>Possibly empty sequence of paths with the given filename.</returns>
|
|
IEnumerable<(string, int)> GetFilename(string name) =>
|
|
Paths.Where(p => Actions.GetFileName(p.Item1) == name);
|
|
|
|
/// <summary>
|
|
/// List of project/solution files to build.
|
|
/// </summary>
|
|
IList<IProjectOrSolution> ProjectsOrSolutionsToBuild { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the supplied build configuration.
|
|
/// </summary>
|
|
TAutobuildOptions Options { get; }
|
|
|
|
/// <summary>
|
|
/// The set of build actions used during the autobuilder.
|
|
/// Could be real system operations, or a stub for testing.
|
|
/// </summary>
|
|
IBuildActions Actions { get; }
|
|
|
|
/// <summary>
|
|
/// Log a given build event to the console.
|
|
/// </summary>
|
|
/// <param name="format">The format string.</param>
|
|
/// <param name="args">Inserts to the format string.</param>
|
|
void Log(Severity severity, string format, params object[] args);
|
|
|
|
/// <summary>
|
|
/// Value of CODEQL_EXTRACTOR_<LANG>_ROOT environment variable.
|
|
/// </summary>
|
|
string? CodeQLExtractorLangRoot { get; }
|
|
|
|
/// <summary>
|
|
/// Value of CODEQL_PLATFORM environment variable.
|
|
/// </summary>
|
|
string? CodeQlPlatform { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Main application logic, containing all data
|
|
/// gathered from the project and filesystem.
|
|
///
|
|
/// The overall design is intended to be extensible so that in theory,
|
|
/// it should be possible to add new build rules without touching this code.
|
|
/// </summary>
|
|
public abstract class Autobuilder<TAutobuildOptions> : IAutobuilder<TAutobuildOptions> where TAutobuildOptions : AutobuildOptionsShared
|
|
{
|
|
/// <summary>
|
|
/// Full file paths of files found in the project directory, as well as
|
|
/// their distance from the project root folder. The list is sorted
|
|
/// by distance in ascending order.
|
|
/// </summary>
|
|
public IEnumerable<(string, int)> Paths => pathsLazy.Value;
|
|
private readonly Lazy<IEnumerable<(string, int)>> pathsLazy;
|
|
|
|
/// <summary>
|
|
/// Gets a list of paths matching a set of extensions (including the "."),
|
|
/// as well as their distance from the project root folder.
|
|
/// The list is sorted by distance in ascending order.
|
|
/// </summary>
|
|
/// <param name="extensions">The extensions to find.</param>
|
|
/// <returns>The files matching the extension.</returns>
|
|
public IEnumerable<(string, int)> GetExtensions(params string[] extensions) =>
|
|
Paths.Where(p => extensions.Contains(Path.GetExtension(p.Item1)));
|
|
|
|
/// <summary>
|
|
/// Holds if a given path, relative to the root of the source directory
|
|
/// was found.
|
|
/// </summary>
|
|
/// <param name="path">The relative path.</param>
|
|
/// <returns>True iff the path was found.</returns>
|
|
public bool HasRelativePath(string path) => HasPath(Actions.PathCombine(RootDirectory, path));
|
|
|
|
/// <summary>
|
|
/// List of project/solution files to build.
|
|
/// </summary>
|
|
public IList<IProjectOrSolution> ProjectsOrSolutionsToBuild => projectsOrSolutionsToBuildLazy.Value;
|
|
private readonly Lazy<IList<IProjectOrSolution>> projectsOrSolutionsToBuildLazy;
|
|
|
|
/// <summary>
|
|
/// Holds if a given path was found.
|
|
/// </summary>
|
|
/// <param name="path">The path of the file.</param>
|
|
/// <returns>True iff the path was found.</returns>
|
|
public bool HasPath(string path) => Paths.Any(p => path == p.Item1);
|
|
|
|
private void FindFiles(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))
|
|
{
|
|
FindFiles(d, depth + 1, maxDepth, results);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The root of the source directory.
|
|
/// </summary>
|
|
private string RootDirectory => Options.RootDirectory;
|
|
|
|
/// <summary>
|
|
/// Gets the supplied build configuration.
|
|
/// </summary>
|
|
public TAutobuildOptions Options { get; }
|
|
|
|
/// <summary>
|
|
/// The set of build actions used during the autobuilder.
|
|
/// Could be real system operations, or a stub for testing.
|
|
/// </summary>
|
|
public IBuildActions Actions { get; }
|
|
|
|
private IEnumerable<IProjectOrSolution>? FindFiles(string extension, Func<string, ProjectOrSolution<TAutobuildOptions>> create)
|
|
{
|
|
var matchingFiles = GetExtensions(extension)
|
|
.Select(p => (ProjectOrSolution: create(p.Item1), DistanceFromRoot: p.Item2))
|
|
.Where(p => p.ProjectOrSolution.HasLanguage(this.Options.Language))
|
|
.ToArray();
|
|
|
|
if (matchingFiles.Length == 0)
|
|
return null;
|
|
|
|
if (Options.AllSolutions)
|
|
return matchingFiles.Select(p => p.ProjectOrSolution);
|
|
|
|
return matchingFiles
|
|
.Where(f => f.DistanceFromRoot == matchingFiles[0].DistanceFromRoot)
|
|
.Select(f => f.ProjectOrSolution);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find all the relevant files and picks the best
|
|
/// solution file and tools.
|
|
/// </summary>
|
|
/// <param name="options">The command line options.</param>
|
|
protected Autobuilder(IBuildActions actions, TAutobuildOptions options)
|
|
{
|
|
Actions = actions;
|
|
Options = options;
|
|
|
|
pathsLazy = new Lazy<IEnumerable<(string, int)>>(() =>
|
|
{
|
|
var files = new List<(string, int)>();
|
|
FindFiles(options.RootDirectory, 0, options.SearchDepth, files);
|
|
return files.OrderBy(f => f.Item2).ToArray();
|
|
});
|
|
|
|
projectsOrSolutionsToBuildLazy = new Lazy<IList<IProjectOrSolution>>(() =>
|
|
{
|
|
List<IProjectOrSolution>? ret;
|
|
if (options.Solution.Any())
|
|
{
|
|
ret = new List<IProjectOrSolution>();
|
|
foreach (var solution in options.Solution)
|
|
{
|
|
if (actions.FileExists(solution))
|
|
ret.Add(new Solution<TAutobuildOptions>(this, solution, true));
|
|
else
|
|
Log(Severity.Error, $"The specified project or solution file {solution} was not found");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// First look for `.proj` files
|
|
ret = FindFiles(".proj", f => new Project<TAutobuildOptions>(this, f))?.ToList();
|
|
if (ret is not null)
|
|
return ret;
|
|
|
|
// Then look for `.sln` files
|
|
ret = FindFiles(".sln", f => new Solution<TAutobuildOptions>(this, f, false))?.ToList();
|
|
if (ret is not null)
|
|
return ret;
|
|
|
|
// Finally look for language specific project files, e.g. `.csproj` files
|
|
ret = FindFiles(this.Options.Language.ProjectExtension, f => new Project<TAutobuildOptions>(this, f))?.ToList();
|
|
return ret ?? new List<IProjectOrSolution>();
|
|
});
|
|
|
|
CodeQLExtractorLangRoot = Actions.GetEnvironmentVariable(EnvVars.Root(this.Options.Language));
|
|
CodeQlPlatform = Actions.GetEnvironmentVariable(EnvVars.Platform);
|
|
|
|
TrapDir = RequireEnvironmentVariable(EnvVars.TrapDir(this.Options.Language));
|
|
SourceArchiveDir = RequireEnvironmentVariable(EnvVars.SourceArchiveDir(this.Options.Language));
|
|
DiagnosticsDir = RequireEnvironmentVariable(EnvVars.DiagnosticDir(this.Options.Language));
|
|
|
|
this.diagnostics = DiagnosticsStream.ForFile(Path.Combine(DiagnosticsDir, $"autobuilder-{DateTime.UtcNow:yyyyMMddHHmm}.jsonc"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the value of an environment variable named <paramref name="name"> or throws
|
|
/// an exception if no such environment variable has been set.
|
|
/// </summary>
|
|
/// <param name="name">The name of the environment variable.</param>
|
|
/// <returns>The value of the environment variable.</returns>
|
|
/// <exception cref="InvalidEnvironmentException">
|
|
/// Thrown if the environment variable is not set.
|
|
/// </exception>
|
|
protected string RequireEnvironmentVariable(string name)
|
|
{
|
|
return Actions.GetEnvironmentVariable(name) ??
|
|
throw new InvalidEnvironmentException($"The environment variable {name} has not been set.");
|
|
}
|
|
|
|
protected string TrapDir { get; }
|
|
|
|
protected string SourceArchiveDir { get; }
|
|
|
|
protected string DiagnosticsDir { get; }
|
|
|
|
protected abstract DiagnosticClassifier DiagnosticClassifier { get; }
|
|
|
|
private readonly ILogger logger = new ConsoleLogger(Verbosity.Info);
|
|
|
|
private readonly DiagnosticsStream diagnostics;
|
|
|
|
/// <summary>
|
|
/// Log a given build event to the console.
|
|
/// </summary>
|
|
/// <param name="format">The format string.</param>
|
|
/// <param name="args">Inserts to the format string.</param>
|
|
public void Log(Severity severity, string format, params object[] args)
|
|
{
|
|
logger.Log(severity, format, args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write <paramref name="diagnostic"/> to the diagnostics file.
|
|
/// </summary>
|
|
/// <param name="diagnostic">The diagnostics entry to write.</param>
|
|
public void AddDiagnostic(DiagnosticMessage diagnostic)
|
|
{
|
|
diagnostics.AddEntry(diagnostic);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to build this project.
|
|
/// </summary>
|
|
/// <returns>The exit code, 0 for success and non-zero for failures.</returns>
|
|
public int AttemptBuild()
|
|
{
|
|
Log(Severity.Info, $"Working directory: {Options.RootDirectory}");
|
|
|
|
var script = GetBuildScript();
|
|
|
|
if (Options.IgnoreErrors)
|
|
script |= BuildScript.Success;
|
|
|
|
void startCallback(string s, bool silent)
|
|
{
|
|
Log(silent ? Severity.Debug : Severity.Info, $"\nRunning {s}");
|
|
}
|
|
|
|
void exitCallback(int ret, string msg, bool silent)
|
|
{
|
|
Log(silent ? Severity.Debug : Severity.Info, $"Exit code {ret}{(string.IsNullOrEmpty(msg) ? "" : $": {msg}")}");
|
|
}
|
|
|
|
var onOutput = BuildOutputHandler(Console.Out);
|
|
var onError = BuildOutputHandler(Console.Error);
|
|
|
|
var buildResult = script.Run(Actions, startCallback, exitCallback, onOutput, onError);
|
|
|
|
// if the build succeeded, all diagnostics we captured from the build output should be warnings;
|
|
// otherwise they should all be errors
|
|
var diagSeverity = buildResult == 0 ? DiagnosticMessage.TspSeverity.Warning : DiagnosticMessage.TspSeverity.Error;
|
|
this.DiagnosticClassifier.Results.Select(result => result.ToDiagnosticMessage(this)).ForEach(result =>
|
|
{
|
|
result.Severity = diagSeverity;
|
|
AddDiagnostic(result);
|
|
});
|
|
|
|
return buildResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the build script to use for this project.
|
|
/// </summary>
|
|
public abstract BuildScript GetBuildScript();
|
|
|
|
/// <summary>
|
|
/// Constructs a standard <see cref="DiagnosticMessage" /> for some message with
|
|
/// <see cref="id" /> and a human-friendly <see cref="name" />.
|
|
/// </summary>
|
|
/// <param name="id">The last part of the message id.</param>
|
|
/// <param name="name">The human-friendly description of the message.</param>
|
|
/// <returns>The resulting <see cref="DiagnosticMessage" />.</returns>
|
|
public DiagnosticMessage MakeDiagnostic(string id, string name)
|
|
{
|
|
DiagnosticMessage diag = new(new(
|
|
$"{this.Options.Language.UpperCaseName.ToLower()}/autobuilder/{id}",
|
|
name,
|
|
Options.Language.UpperCaseName.ToLower()
|
|
));
|
|
diag.Visibility.StatusPage = true;
|
|
|
|
return diag;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Produces a diagnostic for the tool status page that we were unable to automatically
|
|
/// build the user's project and that they can manually specify a build command. This
|
|
/// can be overriden to implement more specific messages depending on the origin of
|
|
/// the failure.
|
|
/// </summary>
|
|
protected virtual void AutobuildFailureDiagnostic()
|
|
{
|
|
var message = MakeDiagnostic("autobuild-failure", "Unable to build project");
|
|
message.PlaintextMessage =
|
|
"We were unable to automatically build your project. " +
|
|
"You can manually specify a suitable build command for your project.";
|
|
message.Severity = DiagnosticMessage.TspSeverity.Error;
|
|
|
|
AddDiagnostic(message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a build script that can be run upon autobuild failure.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A build script that reports that we could not automatically detect a suitable build method.
|
|
/// </returns>
|
|
protected BuildScript AutobuildFailure() =>
|
|
BuildScript.Create(actions =>
|
|
{
|
|
Log(Severity.Error, "Could not auto-detect a suitable build method");
|
|
|
|
AutobuildFailureDiagnostic();
|
|
|
|
return 1;
|
|
});
|
|
|
|
/// <summary>
|
|
/// Constructs a <see cref="BuildOutputHandler" /> which uses the <see cref="DiagnosticClassifier" />
|
|
/// to classify build output. All data also gets written to <paramref name="writer" />.
|
|
/// </summary>
|
|
/// <param name="writer">
|
|
/// The <see cref="TextWriter" /> to which the build output would have normally been written to.
|
|
/// This is normally <see cref="Console.Out" /> or <see cref="Console.Error" />.
|
|
/// </param>
|
|
/// <returns>The constructed <see cref="BuildOutputHandler" />.</returns>
|
|
protected BuildOutputHandler BuildOutputHandler(TextWriter writer) => new(data =>
|
|
{
|
|
if (data is not null)
|
|
{
|
|
writer.WriteLine(data);
|
|
DiagnosticClassifier.ClassifyLine(data);
|
|
}
|
|
});
|
|
|
|
/// <summary>
|
|
/// Value of CODEQL_EXTRACTOR_<LANG>_ROOT environment variable.
|
|
/// </summary>
|
|
public string? CodeQLExtractorLangRoot { get; }
|
|
|
|
/// <summary>
|
|
/// Value of CODEQL_PLATFORM environment variable.
|
|
/// </summary>
|
|
public string? CodeQlPlatform { get; }
|
|
}
|
|
}
|