diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp/CSharpAutobuilder.cs b/csharp/autobuilder/Semmle.Autobuild.CSharp/CSharpAutobuilder.cs index dafda9a8f2ab..577eed2a0c8c 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp/CSharpAutobuilder.cs +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp/CSharpAutobuilder.cs @@ -15,6 +15,7 @@ public class CSharpAutobuildOptions : AutobuildOptionsShared private const string extractorOptionPrefix = "CODEQL_EXTRACTOR_CSHARP_OPTION_"; public bool Buildless { get; } + public string? Binlog { get; } public override Language Language => Language.CSharp; @@ -29,7 +30,7 @@ public CSharpAutobuildOptions(IBuildActions actions) : base(actions) actions.GetEnvironmentVariable(extractorOptionPrefix + "BUILDLESS").AsBool("buildless", false) || actions.GetEnvironmentVariable(buildModeEnvironmentVariable)?.ToLower() == "none"; - + Binlog = actions.GetEnvironmentVariable(extractorOptionPrefix + "BINLOG"); } } diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp/Semmle.Autobuild.CSharp.csproj b/csharp/autobuilder/Semmle.Autobuild.CSharp/Semmle.Autobuild.CSharp.csproj index 7c4f1d681773..515fecd5bec6 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp/Semmle.Autobuild.CSharp.csproj +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp/Semmle.Autobuild.CSharp.csproj @@ -6,6 +6,7 @@ + diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp/StandaloneBuildRule.cs b/csharp/autobuilder/Semmle.Autobuild.CSharp/StandaloneBuildRule.cs index 5b844e6cf6c8..489ecccfd40b 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp/StandaloneBuildRule.cs +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp/StandaloneBuildRule.cs @@ -10,7 +10,14 @@ internal class StandaloneBuildRule : IBuildRule { public BuildScript Analyse(IAutobuilder builder, bool auto) { - return BuildScript.Create(_ => Semmle.Extraction.CSharp.Standalone.Program.Main([])); + if (builder.Options.Binlog is string binlog) + { + return BuildScript.Create(_ => Semmle.Extraction.CSharp.Driver.Main(["--binlog", binlog])); + } + else + { + return BuildScript.Create(_ => Semmle.Extraction.CSharp.Standalone.Program.Main([])); + } } } } diff --git a/csharp/codeql-extractor.yml b/csharp/codeql-extractor.yml index 6c3285c412b9..e33f8feae08b 100644 --- a/csharp/codeql-extractor.yml +++ b/csharp/codeql-extractor.yml @@ -65,3 +65,6 @@ options: - progress+++ type: string pattern: "^(off|errors|warnings|(info|progress)|(debug|progress\\+)|(trace|progress\\+\\+)|progress\\+\\+\\+)$" + binlog: + title: Binlog + type: string diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Analyser.cs b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Analyser.cs index 2f21716284f0..281ce9e93491 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Analyser.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Analyser.cs @@ -312,8 +312,6 @@ public virtual void Dispose() else Logger.Log(Severity.Info, "EXTRACTION SUCCEEDED in {0}", stopWatch.Elapsed); - Logger.Dispose(); - compilationTrapFile?.Dispose(); } diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/BinaryLogAnalyser.cs b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/BinaryLogAnalyser.cs new file mode 100644 index 000000000000..23a52f7fa1ec --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/BinaryLogAnalyser.cs @@ -0,0 +1,22 @@ +using Microsoft.CodeAnalysis.CSharp; +using Semmle.Util.Logging; + +namespace Semmle.Extraction.CSharp +{ + public class BinaryLogAnalyser : Analyser + { + public BinaryLogAnalyser(IProgressMonitor pm, ILogger logger, bool addAssemblyTrapPrefix, PathTransformer pathTransformer) + : base(pm, logger, addAssemblyTrapPrefix, pathTransformer) + { + } + + public void Initialize(string cwd, string[] args, string outputPath, CSharpCompilation compilationIn, CommonOptions options) + { + compilation = compilationIn; + extractor = new BinaryLogExtractor(cwd, args, outputPath, Logger, PathTransformer, options); + this.options = options; + LogExtractorInfo(Extraction.Extractor.Version); + SetReferencePaths(); + } + } +} diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Extractor.cs b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Extractor.cs index f6913103ad8c..536fb3404efd 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Extractor.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Extractor.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Basic.CompilerLog.Util; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; @@ -97,60 +98,120 @@ public static ExitCode Run(string[] args) stopwatch.Start(); var options = Options.CreateWithEnvironment(args); - var workingDirectory = Directory.GetCurrentDirectory(); - var compilerArgs = options.CompilerArguments.ToArray(); - using var logger = MakeLogger(options.Verbosity, options.Console); - var canonicalPathCache = CanonicalPathCache.Create(logger, 1000); - var pathTransformer = new PathTransformer(canonicalPathCache); - - using var analyser = new TracingAnalyser(new LogProgressMonitor(logger), logger, options.AssemblySensitiveTrap, pathTransformer); - try { - if (options.ProjectsToLoad.Any()) + var canonicalPathCache = CanonicalPathCache.Create(logger, 1000); + var pathTransformer = new PathTransformer(canonicalPathCache); + + if (options.BinaryLogPath is string binlogPath) { - AddSourceFilesFromProjects(options.ProjectsToLoad, options.CompilerArguments, logger); + logger.LogInfo(" Running binary log analysis."); + return RunBinaryLogAnalysis(stopwatch, binlogPath, options, logger, pathTransformer); } - - var compilerVersion = new CompilerVersion(options); - - if (compilerVersion.SkipExtraction) + else { - logger.Log(Severity.Warning, " Unrecognized compiler '{0}' because {1}", compilerVersion.SpecifiedCompiler, compilerVersion.SkipReason); - return ExitCode.Ok; + logger.LogInfo(" Running tracing analysis."); + return RunTracingAnalysis(stopwatch, options, logger, canonicalPathCache, pathTransformer); } + } + catch (Exception ex) // lgtm[cs/catch-of-all-exceptions] + { + logger.Log(Severity.Error, " Unhandled exception: {0}", ex); + return ExitCode.Errors; + } + } - var compilerArguments = CSharpCommandLineParser.Default.Parse( - compilerVersion.ArgsWithResponse, - workingDirectory, - compilerVersion.FrameworkPath, - compilerVersion.AdditionalReferenceDirectories - ); + private static ExitCode RunBinaryLogAnalysis(Stopwatch stopwatch, string binlogPath, Options options, ILogger logger, PathTransformer pathTransformer) + { + using var fileStream = new FileStream(binlogPath, FileMode.Open, FileAccess.Read, FileShare.Read); + // Filter out compiler calls that aren't interesting for examination + static bool filter(CompilerCall compilerCall) + { + return compilerCall.IsCSharp && + compilerCall.Kind == CompilerCallKind.Regular; + } - if (compilerArguments is null) + using var reader = BinaryLogReader.Create(fileStream); + var allCompilationData = reader.ReadAllCompilationData(filter); + + var exitCode = ExitCode.Ok; + + logger.LogInfo($" Found {allCompilationData.Count} compilations in binary log"); + + foreach (var compilationData in allCompilationData) + { + if (compilationData.GetCompilationAfterGenerators() is not CSharpCompilation compilation) { - var sb = new StringBuilder(); - sb.Append(" Failed to parse command line: ").AppendList(" ", compilerArgs); - logger.Log(Severity.Error, sb.ToString()); - ++analyser.CompilationErrors; - return ExitCode.Failed; + logger.LogError(" Compilation data is not C#"); + continue; } - if (!analyser.BeginInitialize(compilerVersion.ArgsWithResponse)) + var compilerCall = compilationData.CompilerCall; + var compilerArgs = compilerCall.GetArguments(); + var args = reader.ReadCommandLineArguments(compilerCall); + + using var analyser = new BinaryLogAnalyser(new LogProgressMonitor(logger), logger, options.AssemblySensitiveTrap, pathTransformer); + + var exit = Analyse(stopwatch, analyser, options, + references => [() => compilation.References.ForEach(r => references.Add(r))], + (analyser, syntaxTrees) => [() => syntaxTrees.AddRange(compilation.SyntaxTrees)], + (syntaxTrees, references) => compilation, + (compilation, options) => analyser.Initialize(compilerCall.ProjectDirectory, compilerArgs?.ToArray() ?? [], TracingAnalyser.GetOutputName(compilation, args), compilation, options), + () => { }); + + if (exitCode == ExitCode.Ok && exit != ExitCode.Ok) { - logger.Log(Severity.Info, "Skipping extraction since files have already been extracted"); - return ExitCode.Ok; + exitCode = ExitCode.Errors; } + } + return exitCode; + } - return AnalyseTracing(workingDirectory, compilerArgs, analyser, compilerArguments, options, canonicalPathCache, stopwatch); + private static ExitCode RunTracingAnalysis(Stopwatch stopwatch, Options options, ILogger logger, CanonicalPathCache canonicalPathCache, PathTransformer pathTransformer) + { + var workingDirectory = Directory.GetCurrentDirectory(); + var compilerArgs = options.CompilerArguments.ToArray(); + + using var analyser = new TracingAnalyser(new LogProgressMonitor(logger), logger, options.AssemblySensitiveTrap, pathTransformer); + + if (options.ProjectsToLoad.Any()) + { + AddSourceFilesFromProjects(options.ProjectsToLoad, options.CompilerArguments, logger); } - catch (Exception ex) // lgtm[cs/catch-of-all-exceptions] + + var compilerVersion = new CompilerVersion(options); + + if (compilerVersion.SkipExtraction) { - logger.Log(Severity.Error, " Unhandled exception: {0}", ex); - return ExitCode.Errors; + logger.Log(Severity.Warning, " Unrecognized compiler '{0}' because {1}", compilerVersion.SpecifiedCompiler, compilerVersion.SkipReason); + return ExitCode.Ok; } + + var compilerArguments = CSharpCommandLineParser.Default.Parse( + compilerVersion.ArgsWithResponse, + workingDirectory, + compilerVersion.FrameworkPath, + compilerVersion.AdditionalReferenceDirectories + ); + + if (compilerArguments is null) + { + var sb = new StringBuilder(); + sb.Append(" Failed to parse command line: ").AppendList(" ", compilerArgs); + logger.Log(Severity.Error, sb.ToString()); + ++analyser.CompilationErrors; + return ExitCode.Failed; + } + + if (!analyser.BeginInitialize(compilerVersion.ArgsWithResponse)) + { + logger.Log(Severity.Info, "Skipping extraction since files have already been extracted"); + return ExitCode.Ok; + } + + return AnalyseTracing(workingDirectory, compilerArgs, analyser, compilerArguments, options, canonicalPathCache, stopwatch); } private static void AddSourceFilesFromProjects(IEnumerable projectsToLoad, IList compilerArguments, ILogger logger) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Options.cs b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Options.cs index 4fafffe98333..7f3815520d62 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Options.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/Options.cs @@ -32,6 +32,11 @@ public sealed class Options : CommonOptions /// public bool AssemblySensitiveTrap { get; private set; } = false; + /// + /// The path to the binary log file, or null if unspecified. + /// + public string? BinaryLogPath { get; set; } + public static Options CreateWithEnvironment(string[] arguments) { var options = new Options(); @@ -65,6 +70,9 @@ public override bool HandleOption(string key, string value) case "load-sources-from-project": ProjectsToLoad.Add(value); return true; + case "binlog": + BinaryLogPath = value; + return true; default: return base.HandleOption(key, value); } diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/TracingAnalyser.cs b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/TracingAnalyser.cs index c609b2ba100a..963952c2b733 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Extractor/TracingAnalyser.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Extractor/TracingAnalyser.cs @@ -110,8 +110,8 @@ private bool LogRoslynArgs(IEnumerable roslynArgs, string extractorVersi /// Information about the compilation. /// Cancellation token required. /// The filename. - private static string GetOutputName(CSharpCompilation compilation, - CSharpCommandLineArguments commandLineArguments) + internal static string GetOutputName(CSharpCompilation compilation, + CommandLineArguments commandLineArguments) { // There's no apparent way to access the output filename from the compilation, // so we need to re-parse the command line arguments. diff --git a/csharp/extractor/Semmle.Extraction.CSharp/paket.references b/csharp/extractor/Semmle.Extraction.CSharp/paket.references index 70cd7de8821f..d53881096fdf 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/paket.references +++ b/csharp/extractor/Semmle.Extraction.CSharp/paket.references @@ -1,3 +1,3 @@ Microsoft.Build Microsoft.CodeAnalysis.CSharp - +Basic.CompilerLog.Util \ No newline at end of file diff --git a/csharp/extractor/Semmle.Extraction/Extractor/BinaryLogExtractor.cs b/csharp/extractor/Semmle.Extraction/Extractor/BinaryLogExtractor.cs new file mode 100644 index 000000000000..6788ae4dfd49 --- /dev/null +++ b/csharp/extractor/Semmle.Extraction/Extractor/BinaryLogExtractor.cs @@ -0,0 +1,19 @@ +using Semmle.Util.Logging; + +namespace Semmle.Extraction +{ + public class BinaryLogExtractor : Extractor + { + public override ExtractorMode Mode { get; } + + public BinaryLogExtractor(string cwd, string[] args, string outputPath, ILogger logger, PathTransformer pathTransformer, CommonOptions options) + : base(cwd, args, outputPath, [], logger, pathTransformer) + { + Mode = ExtractorMode.BinaryLog; + if (options.QlTest) + { + Mode |= ExtractorMode.QlTest; + } + } + } +} diff --git a/csharp/extractor/Semmle.Extraction/Extractor/ExtractorMode.cs b/csharp/extractor/Semmle.Extraction/Extractor/ExtractorMode.cs index 52ef15f52d4d..cc1f5cc04132 100644 --- a/csharp/extractor/Semmle.Extraction/Extractor/ExtractorMode.cs +++ b/csharp/extractor/Semmle.Extraction/Extractor/ExtractorMode.cs @@ -12,5 +12,6 @@ public enum ExtractorMode Standalone = 1, Pdb = 2, QlTest = 4, + BinaryLog = 8, } } diff --git a/csharp/paket.dependencies b/csharp/paket.dependencies index 3b93065a4fbd..4d76c4e918dc 100644 --- a/csharp/paket.dependencies +++ b/csharp/paket.dependencies @@ -4,6 +4,7 @@ source https://api.nuget.org/v3/index.json # behave like nuget in choosing transitive dependency versions strategy: min +nuget Basic.CompilerLog.Util nuget Mono.Posix.NETStandard nuget Newtonsoft.Json nuget xunit @@ -17,4 +18,4 @@ nuget System.Net.Primitives nuget System.Security.Principal nuget System.Threading.ThreadPool nuget System.IO.FileSystem -nuget GitInfo 3.3.3 +nuget GitInfo 3.3.3 \ No newline at end of file diff --git a/csharp/paket.lock b/csharp/paket.lock index 060187c166f2..e0ee9f4eb7f6 100644 --- a/csharp/paket.lock +++ b/csharp/paket.lock @@ -3,9 +3,21 @@ STRATEGY: MIN RESTRICTION: == net8.0 NUGET remote: https://api.nuget.org/v3/index.json + Basic.CompilerLog.Util (0.7.3) + MessagePack (>= 2.5.129) + Microsoft.CodeAnalysis (>= 4.8) + Microsoft.CodeAnalysis.CSharp (>= 4.8) + Microsoft.CodeAnalysis.VisualBasic (>= 4.8) + Microsoft.Extensions.ObjectPool (>= 7.0.13) + MSBuild.StructuredLogger (>= 2.2.235) GitInfo (3.3.3) ThisAssembly.Constants (>= 1.4.1) Humanizer.Core (2.14.1) + MessagePack (2.5.129) + MessagePack.Annotations (>= 2.5.129) + Microsoft.NET.StringTools (>= 17.6.3) + System.Runtime.CompilerServices.Unsafe (>= 6.0) + MessagePack.Annotations (2.5.129) Microsoft.Bcl.AsyncInterfaces (7.0) Microsoft.Build (17.8.3) Microsoft.Build.Framework (>= 17.8.3) @@ -17,6 +29,11 @@ NUGET System.Security.Principal.Windows (>= 5.0) System.Threading.Tasks.Dataflow (>= 7.0) Microsoft.Build.Framework (17.8.3) + Microsoft.Build.Utilities.Core (17.5) + Microsoft.Build.Framework (>= 17.5) + Microsoft.NET.StringTools (>= 17.5) + System.Collections.Immutable (>= 6.0) + System.Configuration.ConfigurationManager (>= 6.0) Microsoft.CodeAnalysis (4.8) Microsoft.CodeAnalysis.CSharp.Workspaces (4.8) Microsoft.CodeAnalysis.VisualBasic.Workspaces (4.8) @@ -48,6 +65,7 @@ NUGET System.Threading.Channels (>= 7.0) Microsoft.CodeCoverage (17.9) Microsoft.CSharp (4.7) + Microsoft.Extensions.ObjectPool (7.0.13) Microsoft.NET.StringTools (17.8.3) Microsoft.NET.Test.Sdk (17.9) Microsoft.CodeCoverage (>= 17.9) @@ -65,6 +83,9 @@ NUGET System.Runtime (>= 4.3) Microsoft.Win32.SystemEvents (7.0) Mono.Posix.NETStandard (1.0) + MSBuild.StructuredLogger (2.2.235) + Microsoft.Build.Framework (>= 17.5) + Microsoft.Build.Utilities.Core (>= 17.5) Newtonsoft.Json (13.0.3) System.Collections.Immutable (7.0) System.Composition (7.0) diff --git a/csharp/tools/run-dotnet.sh b/csharp/tools/run-dotnet.sh new file mode 100755 index 000000000000..b96be3162e50 --- /dev/null +++ b/csharp/tools/run-dotnet.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -eu + +echo Args: "$@" +$@ || exit $? diff --git a/csharp/tools/tracing-config.lua b/csharp/tools/tracing-config.lua index a48a93c073c8..68b8713c3e16 100644 --- a/csharp/tools/tracing-config.lua +++ b/csharp/tools/tracing-config.lua @@ -1,10 +1,28 @@ function RegisterExtractorPack(id) + -- local charset = {} do -- [0-9a-zA-Z] + -- for c = 48, 57 do table.insert(charset, string.char(c)) end + -- for c = 65, 90 do table.insert(charset, string.char(c)) end + -- for c = 97, 122 do table.insert(charset, string.char(c)) end + -- end + + -- function RandomString(length) + -- if not length or length <= 0 then return '' end + -- math.randomseed(os.clock()^5) + -- return RandomString(length - 1) .. charset[math.random(1, #charset)] + -- end + + function RandomString(length) + return '1234' + end + function Exify(path) if OperatingSystem == 'windows' then return path .. '.exe' else return path end end local extractor = Exify(GetPlatformToolsDirectory() .. 'Semmle.Extraction.CSharp.Driver') + local dotnetScript = 'tools' .. PathSep .. 'run-dotnet.sh' + local function isDotnet(name) return name == 'dotnet' or name == 'dotnet.exe' end @@ -27,6 +45,50 @@ function RegisterExtractorPack(id) return path:match('%.exe$') or path:match('%.dll') end + local function isBinLogArg(arg) + return arg:match('^[-/]bl:.*$') + -- TODO: handle other variants of bl, such as + -- bl without file name too, or + -- bl:output.binlog;ProjectImports=ZipFile, or + -- binaryLogger:... + end + + function DotnetMatcherBuildBinaryLog(compilerName, compilerPath, compilerArguments, + _languageId) + if not isDotnet(compilerName) then + return nil + end + + local filename = nil + local argv = compilerArguments.argv + if OperatingSystem == 'windows' then + -- let's hope that this split matches the escaping rules `dotnet` applies to command line arguments + -- or, at least, that it is close enough + argv = NativeArgumentsToArgv(compilerArguments.nativeArgumentPointer) + end + + -- TODO: do we need to be more specific about the command that we're intercepting? Is `-bl` specific to `dotnet build`? + for i, arg in ipairs(argv) do + if isBinLogArg(arg) then + filename = string.sub(arg, 5) -- TODO: fix getting the filename + break + end + end + + if filename then + local extractorArgs = { '--binlog', filename } + return { + order = ORDER_AFTER, + invocation = { + path = AbsolutifyExtractorPath(id, extractor), + arguments = { + argv = extractorArgs + } + } + } + end + end + function DotnetMatcherBuild(compilerName, compilerPath, compilerArguments, _languageId) if not isDotnet(compilerName) then @@ -59,6 +121,14 @@ function RegisterExtractorPack(id) argv = NativeArgumentsToArgv(compilerArguments.nativeArgumentPointer) end + + for i, arg in ipairs(argv) do + if isBinLogArg(arg) then + Log(1, 'Found binlog argument in dotnet command line, not changing command') + return nil + end + end + for i, arg in ipairs(argv) do -- if dotnet is being used to execute any application except dotnet itself, we should -- not inject any flags. @@ -115,29 +185,28 @@ function RegisterExtractorPack(id) end end if match then - local injections = { '-p:UseSharedCompilation=false', '-p:EmitCompilerGeneratedFiles=true' } + -- TODO: Add real temporary file name + local injections = { '-bl:codeql_' .. RandomString(15) .. '.binlog' } if dotnetRunNeedsSeparator then table.insert(injections, '--') end - if dotnetRunInjectionIndex == nil then - -- Simple case; just append at the end - return { - order = ORDER_REPLACE, - invocation = BuildExtractorInvocation(id, compilerPath, compilerPath, compilerArguments, nil, - injections) - } - end - -- Complex case; splice injections into the middle of the command line + -- Adding compiler path to the arguments. This is needed to execute the same `dotnet` that we traced. + table.insert(argv, 1, compilerPath) + for i, injectionArg in ipairs(injections) do - table.insert(argv, dotnetRunInjectionIndex + i - 1, injectionArg) + if dotnetRunInjectionIndex == nil then + table.insert(argv, injectionArg) + else + table.insert(argv, dotnetRunInjectionIndex + i, injectionArg) + end end if OperatingSystem == 'windows' then return { order = ORDER_REPLACE, invocation = { - path = AbsolutifyExtractorPath(id, compilerPath), + path = AbsolutifyExtractorPath(id, compilerPath), -- TODO: support windows too arguments = { commandLineString = ArgvToCommandLineString(argv) } @@ -147,7 +216,7 @@ function RegisterExtractorPack(id) return { order = ORDER_REPLACE, invocation = { - path = AbsolutifyExtractorPath(id, compilerPath), + path = AbsolutifyExtractorPath(id, dotnetScript), arguments = { argv = argv } @@ -221,6 +290,7 @@ function RegisterExtractorPack(id) } local posixMatchers = { DotnetMatcherBuild, + DotnetMatcherBuildBinaryLog, CreatePatternMatcher({ '^mcs%.exe$', '^csc%.exe$' }, MatchCompilerName, extractor, { prepend = { '--compiler', '"${compiler}"' },