From 4025481eba2453482b0edd429f4e77dfc3219f8b Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jan 2024 20:23:52 -0500 Subject: [PATCH 01/12] Split builds into 2 steps to gather assembly references. --- .../Diagnosers/PerfCollectProfiler.cs | 1 + .../Toolchains/ArtifactsPaths.cs | 5 +- .../Toolchains/CsProj/CsProjGenerator.cs | 51 ++++++++++---- .../Toolchains/DotNetCli/DotNetCliBuilder.cs | 67 ++++++++++++++++--- .../Toolchains/DotNetCli/DotNetCliCommand.cs | 49 +++++++++----- .../DotNetCli/DotNetCliCommandExecutor.cs | 1 + .../DotNetCli/DotNetCliGenerator.cs | 6 +- .../DotNetCli/DotNetCliPublisher.cs | 29 +++++--- .../Toolchains/GeneratorBase.cs | 8 +++ .../Emit/InProcessEmitArtifactsPath.cs | 1 + .../InProcess/Emit/InProcessEmitGenerator.cs | 1 + .../Toolchains/Mono/MonoPublisher.cs | 34 +++++++--- .../MonoAotLLVM/MonoAotLLVMGenerator.cs | 2 + .../Toolchains/MonoWasm/WasmGenerator.cs | 2 + .../Toolchains/NativeAot/Generator.cs | 14 +++- 15 files changed, 211 insertions(+), 60 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index 9a3d348665..1da98599d7 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -229,6 +229,7 @@ private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters) // We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs. string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols"); DotNetCliCommand cliCommand = new( + projPath: string.Empty, cliPath: cliPath, arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"", generateResult: null, diff --git a/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs b/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs index a9467494fa..08ea04fef9 100644 --- a/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs +++ b/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs @@ -4,7 +4,7 @@ namespace BenchmarkDotNet.Toolchains { public class ArtifactsPaths { - public static readonly ArtifactsPaths Empty = new ArtifactsPaths("", "", "", "", "", "", "", "", "", "", "", ""); + public static readonly ArtifactsPaths Empty = new("", "", "", "", "", "", "", "", "", "", "", "", ""); [PublicAPI] public string RootArtifactsFolderPath { get; } [PublicAPI] public string BuildArtifactsDirectoryPath { get; } @@ -13,6 +13,7 @@ public class ArtifactsPaths [PublicAPI] public string ProgramCodePath { get; } [PublicAPI] public string AppConfigPath { get; } [PublicAPI] public string NuGetConfigPath { get; } + [PublicAPI] public string BuildForReferencesProjectFilePath { get; } [PublicAPI] public string ProjectFilePath { get; } [PublicAPI] public string BuildScriptFilePath { get; } [PublicAPI] public string ExecutablePath { get; } @@ -27,6 +28,7 @@ public ArtifactsPaths( string programCodePath, string appConfigPath, string nuGetConfigPath, + string buildForReferencesProjectFilePath, string projectFilePath, string buildScriptFilePath, string executablePath, @@ -40,6 +42,7 @@ public ArtifactsPaths( ProgramCodePath = programCodePath; AppConfigPath = appConfigPath; NuGetConfigPath = nuGetConfigPath; + BuildForReferencesProjectFilePath = buildForReferencesProjectFilePath; ProjectFilePath = projectFilePath; BuildScriptFilePath = buildScriptFilePath; ExecutablePath = executablePath; diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 7b5a642744..2f62baa366 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -61,33 +61,60 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar protected override string GetProjectFilePath(string buildArtifactsDirectoryPath) => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.csproj"); + protected override string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath) + => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.ForReferences.csproj"); + protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker); [SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$ - protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) - { - var benchmark = buildPartition.RepresentativeBenchmarkCase; - var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); - - var xmlDoc = new XmlDocument(); - xmlDoc.Load(projectFile.FullName); - var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); - - var content = new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) + private string LoadCsProj(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName) + => new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) - .Replace("$CSPROJPATH$", projectFile.FullName) + .Replace("$CSPROJPATH$", projectFile) .Replace("$TFM$", TargetFrameworkMoniker) .Replace("$PROGRAMNAME$", artifactsPaths.ProgramName) - .Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(benchmark.Job.Environment.Gc, buildPartition.Resolver)) + .Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(buildPartition.RepresentativeBenchmarkCase.Job.Environment.Gc, buildPartition.Resolver)) .Replace("$COPIEDSETTINGS$", customProperties) .Replace("$SDKNAME$", sdkName) .ToString(); + protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) + { + var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger); + + var xmlDoc = new XmlDocument(); + xmlDoc.Load(projectFile.FullName); + var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + + var content = LoadCsProj(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + File.WriteAllText(artifactsPaths.ProjectFilePath, content); } + protected void GenerateBuildForReferencesProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName) + { + var content = LoadCsProj(buildPartition, artifactsPaths, projectFile, customProperties, sdkName); + + // We don't include the generated .notcs file when building the reference dlls, only in the final build. + var xmlDoc = new XmlDocument(); + xmlDoc.Load(new StringReader(content)); + XmlElement projectElement = xmlDoc.DocumentElement; + projectElement.RemoveChild(projectElement.SelectSingleNode("ItemGroup/Compile").ParentNode); + + var startupObjectElement = projectElement.SelectSingleNode("PropertyGroup/StartupObject"); + startupObjectElement.ParentNode.RemoveChild(startupObjectElement); + + // We need to change the output type to library since we're only compiling for dlls. + var outputTypeElement = projectElement.SelectSingleNode("PropertyGroup/OutputType"); + outputTypeElement.InnerText = "Library"; + + xmlDoc.Save(artifactsPaths.BuildForReferencesProjectFilePath); + } + /// /// returns an MSBuild string that defines Runtime settings /// diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs index 7a7afc3184..fa8e05e9c7 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Xml; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; @@ -25,16 +27,34 @@ public DotNetCliBuilder(string targetFrameworkMoniker, string? customDotNetCliPa public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) { - BuildResult buildResult = new DotNetCliCommand( - CustomDotNetCliPath, - string.Empty, - generateResult, - logger, - buildPartition, - Array.Empty(), - buildPartition.Timeout, - logOutput: LogOutput) + var cliCommand = new DotNetCliCommand( + generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + CustomDotNetCliPath, + string.Empty, + generateResult, + logger, + buildPartition, + Array.Empty(), + buildPartition.Timeout, + logOutput: LogOutput); + + BuildResult buildResult; + // Integration tests are built without dependencies, so we skip the first step. + if (!buildPartition.ForcedNoDependenciesForIntegrationTests) + { + // We build the original project first to obtain all dlls. + buildResult = cliCommand.RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + return buildResult; + + // After the dlls are built, we gather the assembly references, then build the benchmark project. + GatherReferences(generateResult.ArtifactsPaths); + } + + buildResult = cliCommand.WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath) .RestoreThenBuild(); + if (buildResult.IsBuildSuccess && buildPartition.RepresentativeBenchmarkCase.Job.Environment.LargeAddressAware) { @@ -42,5 +62,34 @@ public BuildResult Build(GenerateResult generateResult, BuildPartition buildPart } return buildResult; } + + internal static void GatherReferences(ArtifactsPaths artifactsPaths) + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(artifactsPaths.ProjectFilePath); + XmlElement projectElement = xmlDoc.DocumentElement; + + // Add reference to every dll. + var itemGroup = xmlDoc.CreateElement("ItemGroup"); + projectElement.AppendChild(itemGroup); + foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll")) + { + var assemblyName = Path.GetFileNameWithoutExtension(assemblyFile); + // The dummy csproj was used to build the original project, but it also outputs a dll for itself which we need to ignore because it's not valid. + if (assemblyName == artifactsPaths.ProgramName) + { + continue; + } + var referenceElement = xmlDoc.CreateElement("Reference"); + itemGroup.AppendChild(referenceElement); + referenceElement.SetAttribute("Include", assemblyName); + var hintPath = xmlDoc.CreateElement("HintPath"); + referenceElement.AppendChild(hintPath); + var locationNode = xmlDoc.CreateTextNode(assemblyFile); + hintPath.AppendChild(locationNode); + } + + xmlDoc.Save(artifactsPaths.ProjectFilePath); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index 55852a318d..d667ac98b1 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; using System.Text; using BenchmarkDotNet.Characteristics; -using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -20,6 +18,10 @@ public class DotNetCliCommand { [PublicAPI] public string CliPath { get; } + [PublicAPI] public string FilePath { get; } + + [PublicAPI] public string TargetFrameworkMoniker { get; } + [PublicAPI] public string Arguments { get; } [PublicAPI] public GenerateResult GenerateResult { get; } @@ -34,11 +36,13 @@ public class DotNetCliCommand [PublicAPI] public bool LogOutput { get; } - public DotNetCliCommand(string cliPath, string arguments, GenerateResult generateResult, ILogger logger, + public DotNetCliCommand(string cliPath, string filePath, string tfm, string arguments, GenerateResult generateResult, ILogger logger, BuildPartition buildPartition, IReadOnlyList environmentVariables, TimeSpan timeout, bool logOutput = false) { CliPath = cliPath ?? DotNetCliCommandExecutor.DefaultDotNetCliPath.Value; Arguments = arguments; + FilePath = filePath; + TargetFrameworkMoniker = tfm; GenerateResult = generateResult; Logger = logger; BuildPartition = buildPartition; @@ -48,10 +52,13 @@ public DotNetCliCommand(string cliPath, string arguments, GenerateResult generat } public DotNetCliCommand WithArguments(string arguments) - => new(CliPath, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + => new(CliPath, arguments, FilePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); + + public DotNetCliCommand WithFilePath(string filePath) + => new(CliPath, Arguments, filePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); public DotNetCliCommand WithCliPath(string cliPath) - => new(cliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + => new(cliPath, Arguments, FilePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); [PublicAPI] public BuildResult RestoreThenBuild() @@ -69,12 +76,12 @@ public BuildResult RestoreThenBuild() if (BuildPartition.ForcedNoDependenciesForIntegrationTests) { var restoreResult = DotNetCliCommandExecutor.Execute(WithArguments( - GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true))); + GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true))); if (!restoreResult.IsSuccess) return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); return DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true))) + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true))) .ToBuildResult(GenerateResult); } else @@ -110,28 +117,30 @@ public BuildResult RestoreThenBuildThenPublish() public DotNetCliCommandResult Restore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "restore"))); + GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "restore"))); public DotNetCliCommandResult Build() => DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "build"))); + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "build"))); public DotNetCliCommandResult BuildNoRestore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore", "build-no-restore"))); + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore", "build-no-restore"))); public DotNetCliCommandResult Publish() => DotNetCliCommandExecutor.Execute(WithArguments( - GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "publish"))); + GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "publish"))); // PublishNoBuildAndNoRestore was removed because we set --output in the build step. We use the implicit build included in the publish command. public DotNetCliCommandResult PublishNoRestore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore", "publish-no-restore"))); + GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore", "publish-no-restore"))); - internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) + internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) => new StringBuilder() .AppendArgument("restore") + .AppendArgument($"-f {tfm}") + .AppendArgument($"\"{filePath}\"") .AppendArgument(string.IsNullOrEmpty(artifactsPaths.PackagesDirectoryName) ? string.Empty : $"--packages \"{artifactsPaths.PackagesDirectoryName}\"") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) @@ -140,9 +149,12 @@ internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPar .MaybeAppendOutputPaths(artifactsPaths, true, excludeOutput) .ToString(); - internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) + internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) => new StringBuilder() - .AppendArgument($"build -c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one + .AppendArgument("build") + .AppendArgument($"\"{filePath}\"") + .AppendArgument($"-f {tfm}") + .AppendArgument($"-c {buildPartition.BuildConfiguration}") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) @@ -151,9 +163,12 @@ internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildParti .MaybeAppendOutputPaths(artifactsPaths, excludeOutput: excludeOutput) .ToString(); - internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null) + internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null) => new StringBuilder() - .AppendArgument($"publish -c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one + .AppendArgument("publish") + .AppendArgument($"\"{filePath}\"") + .AppendArgument($"-f {tfm}") + .AppendArgument($"-c {buildPartition.BuildConfiguration}") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs index 10df4af9b3..d7809b3879 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs @@ -158,6 +158,7 @@ private static string GetDefaultDotNetCliPath() internal static string GetSdkPath(string cliPath) { DotNetCliCommand cliCommand = new( + projPath: string.Empty, cliPath: cliPath, arguments: "--info", generateResult: null, diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs index aaa2cda154..1b847525ae 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs @@ -103,8 +103,10 @@ protected override void CopyAllRequiredFiles(ArtifactsPaths artifactsPaths) protected override void GenerateBuildScript(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) { var content = new StringBuilder(300) - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs index 83158e0b1e..72dc14aecc 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs @@ -25,14 +25,27 @@ public DotNetCliPublisher( private IReadOnlyList? EnvironmentVariables { get; } public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - => new DotNetCliCommand( - CustomDotNetCliPath, - ExtraArguments, - generateResult, - logger, - buildPartition, - EnvironmentVariables, - buildPartition.Timeout) + { + var cliCommand = new DotNetCliCommand( + generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + CustomDotNetCliPath, + ExtraArguments, + generateResult, + logger, + buildPartition, + EnvironmentVariables, + buildPartition.Timeout); + + // We build the original project first to obtain all dlls. + var buildResult = cliCommand.RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + return buildResult; + + // After the dlls are built, we gather the assembly references, then build the benchmark project. + DotNetCliBuilder.GatherReferences(generateResult.ArtifactsPaths); + return cliCommand.WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath) .RestoreThenBuildThenPublish(); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs index 165dedcbba..8610012d53 100644 --- a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs +++ b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs @@ -69,6 +69,13 @@ [PublicAPI] protected virtual string GetExecutableExtension() [PublicAPI] protected virtual string GetProjectFilePath(string buildArtifactsDirectoryPath) => string.Empty; + /// + /// returns a path to the auto-generated .csproj file that is used to build the reference dlls + /// + [PublicAPI] + protected virtual string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath) + => string.Empty; + /// /// returns a list of artifacts that should be removed after running the benchmarks /// @@ -143,6 +150,7 @@ private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string r appConfigPath: $"{executablePath}.config", nuGetConfigPath: Path.Combine(buildArtifactsDirectoryPath, "NuGet.config"), projectFilePath: GetProjectFilePath(buildArtifactsDirectoryPath), + buildForReferencesProjectFilePath: GetProjectFilePathForReferences(buildArtifactsDirectoryPath), buildScriptFilePath: Path.Combine(buildArtifactsDirectoryPath, $"{programName}{OsDetector.ScriptFileExtension}"), executablePath: executablePath, programName: programName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs index ca8ca60974..2a8a84bce3 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs @@ -17,6 +17,7 @@ public InProcessEmitArtifactsPath( baseArtifacts.AppConfigPath, baseArtifacts.NuGetConfigPath, baseArtifacts.ProjectFilePath, + baseArtifacts.BuildForReferencesProjectFilePath, baseArtifacts.BuildScriptFilePath, baseArtifacts.ExecutablePath, baseArtifacts.ProgramName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs index 5e74d65f12..0e8fc92fa6 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs @@ -52,6 +52,7 @@ private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string r appConfigPath: null, nuGetConfigPath: null, projectFilePath: null, + buildForReferencesProjectFilePath: null, buildScriptFilePath: null, executablePath: executablePath, programName: programName, diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs index ff98540cbf..9ee3b54e82 100644 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs @@ -25,14 +25,30 @@ public MonoPublisher(string customDotNetCliPath) private IReadOnlyList EnvironmentVariables { get; } public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - => new DotNetCliCommand( - CustomDotNetCliPath, - ExtraArguments, - generateResult, - logger, - buildPartition, - EnvironmentVariables, - buildPartition.Timeout) - .Publish().ToBuildResult(generateResult); + { + var cliCommand = new DotNetCliCommand( + generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + CustomDotNetCliPath, + string.Empty, + generateResult, + logger, + buildPartition, + EnvironmentVariables, + buildPartition.Timeout); + + // We build the original project first to obtain all dlls. + var buildResult = cliCommand.RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + return buildResult; + + // After the dlls are built, we gather the assembly references, then build the benchmark project. + DotNetCliBuilder.GatherReferences(generateResult.ArtifactsPaths); + return cliCommand + .WithArguments(ExtraArguments) + .WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath) + .Publish() + .ToBuildResult(generateResult); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs index 6ba5968617..a7fce6b40e 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs @@ -36,6 +36,8 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + string content = new StringBuilder(ResourceHelper.LoadTemplate("MonoAOTLLVMCsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index f213277880..227b1d5c05 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -48,6 +48,8 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + string content = new StringBuilder(ResourceHelper.LoadTemplate("WasmCsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index 59e0e6cffd..f1ee30e543 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -74,8 +74,10 @@ protected override void GenerateBuildScript(BuildPartition buildPartition, Artif string extraArguments = NativeAotToolchain.GetExtraArguments(runtimeIdentifier); var content = new StringBuilder(300) - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, extraArguments)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, extraArguments)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); @@ -112,6 +114,14 @@ protected override void GenerateNuGetConfig(ArtifactsPaths artifactsPaths) protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { + var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger); + + var xmlDoc = new XmlDocument(); + xmlDoc.Load(projectFile.FullName); + var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + File.WriteAllText(artifactsPaths.ProjectFilePath, GenerateProjectForNuGetBuild(buildPartition, artifactsPaths, logger)); GenerateReflectionFile(artifactsPaths); } From ce206fb16403149377c6b178a93cfce1825b572e Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Wed, 10 Dec 2025 02:41:45 -0500 Subject: [PATCH 02/12] Build source project directly. --- .../Diagnosers/PerfCollectProfiler.cs | 5 +- .../Toolchains/ArtifactsPaths.cs | 5 +- .../Toolchains/CoreRun/CoreRunPublisher.cs | 18 +--- .../Toolchains/CoreRun/CoreRunToolchain.cs | 2 +- .../Toolchains/CsProj/CsProjGenerator.cs | 99 +++++++++++++------ .../Toolchains/DotNetCli/DotNetCliBuilder.cs | 59 ++--------- .../Toolchains/DotNetCli/DotNetCliCommand.cs | 28 +++--- .../DotNetCli/DotNetCliCommandExecutor.cs | 5 +- .../DotNetCli/DotNetCliGenerator.cs | 4 +- .../DotNetCli/DotNetCliPublisher.cs | 58 +++-------- .../Toolchains/GeneratorBase.cs | 8 -- .../Emit/InProcessEmitArtifactsPath.cs | 1 - .../InProcess/Emit/InProcessEmitGenerator.cs | 1 - .../Toolchains/Mono/MonoPublisher.cs | 54 ---------- .../Toolchains/Mono/MonoToolchain.cs | 15 ++- .../MonoAotLLVM/MonoAotLLVMGenerator.cs | 4 +- .../Toolchains/MonoWasm/WasmGenerator.cs | 4 +- .../Toolchains/NativeAot/Generator.cs | 21 ++-- .../NativeAot/NativeAotToolchain.cs | 2 +- 19 files changed, 140 insertions(+), 253 deletions(-) delete mode 100644 src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index 1da98599d7..cb745092ba 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -229,13 +229,14 @@ private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters) // We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs. string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols"); DotNetCliCommand cliCommand = new( - projPath: string.Empty, cliPath: cliPath, + filePath: string.Empty, + tfm: string.Empty, arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"", generateResult: null, logger: logger, buildPartition: null, - environmentVariables: Array.Empty(), + environmentVariables: [], timeout: TimeSpan.FromMinutes(3), logOutput: true); // the following commands might take a while and fail, let's log them diff --git a/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs b/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs index 08ea04fef9..033a531f5a 100644 --- a/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs +++ b/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs @@ -4,7 +4,7 @@ namespace BenchmarkDotNet.Toolchains { public class ArtifactsPaths { - public static readonly ArtifactsPaths Empty = new("", "", "", "", "", "", "", "", "", "", "", "", ""); + public static readonly ArtifactsPaths Empty = new("", "", "", "", "", "", "", "", "", "", "", ""); [PublicAPI] public string RootArtifactsFolderPath { get; } [PublicAPI] public string BuildArtifactsDirectoryPath { get; } @@ -13,7 +13,6 @@ public class ArtifactsPaths [PublicAPI] public string ProgramCodePath { get; } [PublicAPI] public string AppConfigPath { get; } [PublicAPI] public string NuGetConfigPath { get; } - [PublicAPI] public string BuildForReferencesProjectFilePath { get; } [PublicAPI] public string ProjectFilePath { get; } [PublicAPI] public string BuildScriptFilePath { get; } [PublicAPI] public string ExecutablePath { get; } @@ -28,7 +27,6 @@ public ArtifactsPaths( string programCodePath, string appConfigPath, string nuGetConfigPath, - string buildForReferencesProjectFilePath, string projectFilePath, string buildScriptFilePath, string executablePath, @@ -42,7 +40,6 @@ public ArtifactsPaths( ProgramCodePath = programCodePath; AppConfigPath = appConfigPath; NuGetConfigPath = nuGetConfigPath; - BuildForReferencesProjectFilePath = buildForReferencesProjectFilePath; ProjectFilePath = projectFilePath; BuildScriptFilePath = buildScriptFilePath; ExecutablePath = executablePath; diff --git a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunPublisher.cs b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunPublisher.cs index c77b56b039..930307066b 100644 --- a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunPublisher.cs @@ -9,21 +9,11 @@ namespace BenchmarkDotNet.Toolchains.CoreRun { - public class CoreRunPublisher : IBuilder + public class CoreRunPublisher(string tfm, FileInfo coreRun, FileInfo? customDotNetCliPath = null) : DotNetCliPublisher(tfm, customDotNetCliPath?.FullName) { - public CoreRunPublisher(FileInfo coreRun, FileInfo? customDotNetCliPath = null) + public override BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) { - CoreRun = coreRun; - DotNetCliPublisher = new DotNetCliPublisher(customDotNetCliPath?.FullName); - } - - private FileInfo CoreRun { get; } - - private DotNetCliPublisher DotNetCliPublisher { get; } - - public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - { - var buildResult = DotNetCliPublisher.Build(generateResult, buildPartition, logger); + var buildResult = base.Build(generateResult, buildPartition, logger); if (buildResult.IsBuildSuccess) UpdateDuplicatedDependencies(buildResult.ArtifactsPaths, logger); @@ -37,7 +27,7 @@ public BuildResult Build(GenerateResult generateResult, BuildPartition buildPart private void UpdateDuplicatedDependencies(ArtifactsPaths artifactsPaths, ILogger logger) { var publishedDirectory = new DirectoryInfo(artifactsPaths.BinariesDirectoryPath); - var coreRunDirectory = CoreRun.Directory; + var coreRunDirectory = coreRun.Directory; foreach (var publishedDependency in publishedDirectory .EnumerateFileSystemInfos() diff --git a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs index 3cbaa80f65..43337cdd4a 100644 --- a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs @@ -34,7 +34,7 @@ public CoreRunToolchain(FileInfo coreRun, bool createCopy = true, Name = displayName; Generator = new CoreRunGenerator(SourceCoreRun, CopyCoreRun, targetFrameworkMoniker, customDotNetCliPath?.FullName, restorePath?.FullName); - Builder = new CoreRunPublisher(CopyCoreRun, customDotNetCliPath); + Builder = new CoreRunPublisher(targetFrameworkMoniker, CopyCoreRun, customDotNetCliPath); Executor = new DotNetCliExecutor(customDotNetCliPath: CopyCoreRun.FullName); // instead of executing "dotnet $pathToDll" we do "CoreRun $pathToDll" } diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 2f62baa366..9ac5cc777d 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -14,6 +14,7 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; namespace BenchmarkDotNet.Toolchains.CsProj @@ -61,58 +62,94 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar protected override string GetProjectFilePath(string buildArtifactsDirectoryPath) => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.csproj"); - protected override string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath) - => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.ForReferences.csproj"); - protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker); - [SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$ - private string LoadCsProj(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName) - => new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) - .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) - .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) - .Replace("$CSPROJPATH$", projectFile) - .Replace("$TFM$", TargetFrameworkMoniker) - .Replace("$PROGRAMNAME$", artifactsPaths.ProgramName) - .Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(buildPartition.RepresentativeBenchmarkCase.Job.Environment.Gc, buildPartition.Resolver)) - .Replace("$COPIEDSETTINGS$", customProperties) - .Replace("$SDKNAME$", sdkName) + protected override void GenerateBuildScript(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) + { + string projectFilePath = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, NullLogger.Instance).FullName; + + var content = new StringBuilder(300) + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, projectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, projectFilePath, TargetFrameworkMoniker)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, TargetFrameworkMoniker)}") .ToString(); + File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); + } + + [SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$ protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { - var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger); + var benchmark = buildPartition.RepresentativeBenchmarkCase; + var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); var xmlDoc = new XmlDocument(); xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); - GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); - - var content = LoadCsProj(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + var content = new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) + .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) + .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) + .Replace("$CSPROJPATH$", projectFile.FullName) + .Replace("$TFM$", TargetFrameworkMoniker) + .Replace("$PROGRAMNAME$", artifactsPaths.ProgramName) + .Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(benchmark.Job.Environment.Gc, buildPartition.Resolver)) + .Replace("$COPIEDSETTINGS$", customProperties) + .Replace("$SDKNAME$", sdkName) + .ToString(); File.WriteAllText(artifactsPaths.ProjectFilePath, content); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (!buildPartition.ForcedNoDependenciesForIntegrationTests) + { + GatherReferences(projectFile.FullName, buildPartition, artifactsPaths, logger); + } } - protected void GenerateBuildForReferencesProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName) + protected void GatherReferences(string projectFilePath, BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { - var content = LoadCsProj(buildPartition, artifactsPaths, projectFile, customProperties, sdkName); + // Build the original project then reference all of the built dlls. + var buildResult = new DotNetCliCommand( + CliPath, + projectFilePath, + TargetFrameworkMoniker, + null, + GenerateResult.Success(artifactsPaths, []), + logger, + buildPartition, + [], + buildPartition.Timeout + ) + .RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + { + throw buildResult.TryToExplainFailureReason(out string reason) + ? new Exception(reason) + : new Exception(buildResult.ErrorMessage); + } - // We don't include the generated .notcs file when building the reference dlls, only in the final build. var xmlDoc = new XmlDocument(); - xmlDoc.Load(new StringReader(content)); + xmlDoc.Load(artifactsPaths.ProjectFilePath); XmlElement projectElement = xmlDoc.DocumentElement; - projectElement.RemoveChild(projectElement.SelectSingleNode("ItemGroup/Compile").ParentNode); - - var startupObjectElement = projectElement.SelectSingleNode("PropertyGroup/StartupObject"); - startupObjectElement.ParentNode.RemoveChild(startupObjectElement); - - // We need to change the output type to library since we're only compiling for dlls. - var outputTypeElement = projectElement.SelectSingleNode("PropertyGroup/OutputType"); - outputTypeElement.InnerText = "Library"; + var itemGroup = xmlDoc.CreateElement("ItemGroup"); + projectElement.AppendChild(itemGroup); + foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll")) + { + var referenceElement = xmlDoc.CreateElement("Reference"); + itemGroup.AppendChild(referenceElement); + referenceElement.SetAttribute("Include", Path.GetFileNameWithoutExtension(assemblyFile)); + var hintPath = xmlDoc.CreateElement("HintPath"); + referenceElement.AppendChild(hintPath); + var locationNode = xmlDoc.CreateTextNode(assemblyFile); + hintPath.AppendChild(locationNode); + // TODO: Add Aliases here for extern alias #2289 + } - xmlDoc.Save(artifactsPaths.BuildForReferencesProjectFilePath); + xmlDoc.Save(artifactsPaths.ProjectFilePath); } /// diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs index fa8e05e9c7..79773c7be0 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Xml; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; @@ -27,34 +25,18 @@ public DotNetCliBuilder(string targetFrameworkMoniker, string? customDotNetCliPa public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) { - var cliCommand = new DotNetCliCommand( - generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + var buildResult = new DotNetCliCommand( CustomDotNetCliPath, + generateResult.ArtifactsPaths.ProjectFilePath, + TargetFrameworkMoniker, string.Empty, generateResult, logger, buildPartition, - Array.Empty(), + [], buildPartition.Timeout, - logOutput: LogOutput); - - BuildResult buildResult; - // Integration tests are built without dependencies, so we skip the first step. - if (!buildPartition.ForcedNoDependenciesForIntegrationTests) - { - // We build the original project first to obtain all dlls. - buildResult = cliCommand.RestoreThenBuild(); - - if (!buildResult.IsBuildSuccess) - return buildResult; - - // After the dlls are built, we gather the assembly references, then build the benchmark project. - GatherReferences(generateResult.ArtifactsPaths); - } - - buildResult = cliCommand.WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath) - .RestoreThenBuild(); - + logOutput: LogOutput + ).RestoreThenBuild(); if (buildResult.IsBuildSuccess && buildPartition.RepresentativeBenchmarkCase.Job.Environment.LargeAddressAware) { @@ -62,34 +44,5 @@ public BuildResult Build(GenerateResult generateResult, BuildPartition buildPart } return buildResult; } - - internal static void GatherReferences(ArtifactsPaths artifactsPaths) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(artifactsPaths.ProjectFilePath); - XmlElement projectElement = xmlDoc.DocumentElement; - - // Add reference to every dll. - var itemGroup = xmlDoc.CreateElement("ItemGroup"); - projectElement.AppendChild(itemGroup); - foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll")) - { - var assemblyName = Path.GetFileNameWithoutExtension(assemblyFile); - // The dummy csproj was used to build the original project, but it also outputs a dll for itself which we need to ignore because it's not valid. - if (assemblyName == artifactsPaths.ProgramName) - { - continue; - } - var referenceElement = xmlDoc.CreateElement("Reference"); - itemGroup.AppendChild(referenceElement); - referenceElement.SetAttribute("Include", assemblyName); - var hintPath = xmlDoc.CreateElement("HintPath"); - referenceElement.AppendChild(hintPath); - var locationNode = xmlDoc.CreateTextNode(assemblyFile); - hintPath.AppendChild(locationNode); - } - - xmlDoc.Save(artifactsPaths.ProjectFilePath); - } } } diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index d667ac98b1..23511986d1 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -22,7 +22,7 @@ public class DotNetCliCommand [PublicAPI] public string TargetFrameworkMoniker { get; } - [PublicAPI] public string Arguments { get; } + [PublicAPI] public string? Arguments { get; } [PublicAPI] public GenerateResult GenerateResult { get; } @@ -30,14 +30,14 @@ public class DotNetCliCommand [PublicAPI] public BuildPartition BuildPartition { get; } - [PublicAPI] public IReadOnlyList EnvironmentVariables { get; } + [PublicAPI] public IReadOnlyList? EnvironmentVariables { get; } [PublicAPI] public TimeSpan Timeout { get; } [PublicAPI] public bool LogOutput { get; } - public DotNetCliCommand(string cliPath, string filePath, string tfm, string arguments, GenerateResult generateResult, ILogger logger, - BuildPartition buildPartition, IReadOnlyList environmentVariables, TimeSpan timeout, bool logOutput = false) + public DotNetCliCommand(string cliPath, string filePath, string tfm, string? arguments, GenerateResult generateResult, ILogger logger, + BuildPartition buildPartition, IReadOnlyList? environmentVariables, TimeSpan timeout, bool logOutput = false) { CliPath = cliPath ?? DotNetCliCommandExecutor.DefaultDotNetCliPath.Value; Arguments = arguments; @@ -52,13 +52,13 @@ public DotNetCliCommand(string cliPath, string filePath, string tfm, string argu } public DotNetCliCommand WithArguments(string arguments) - => new(CliPath, arguments, FilePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); + => new(CliPath, FilePath, TargetFrameworkMoniker, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); public DotNetCliCommand WithFilePath(string filePath) - => new(CliPath, Arguments, filePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); + => new(CliPath, filePath, TargetFrameworkMoniker, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); public DotNetCliCommand WithCliPath(string cliPath) - => new(cliPath, Arguments, FilePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); + => new(cliPath, FilePath, TargetFrameworkMoniker, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); [PublicAPI] public BuildResult RestoreThenBuild() @@ -81,7 +81,7 @@ public BuildResult RestoreThenBuild() return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); return DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true))) + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true))) .ToBuildResult(GenerateResult); } else @@ -121,26 +121,26 @@ public DotNetCliCommandResult Restore() public DotNetCliCommandResult Build() => DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "build"))); + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, Arguments, "build"))); public DotNetCliCommandResult BuildNoRestore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore", "build-no-restore"))); + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore", "build-no-restore"))); public DotNetCliCommandResult Publish() => DotNetCliCommandExecutor.Execute(WithArguments( - GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "publish"))); + GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, Arguments, "publish"))); // PublishNoBuildAndNoRestore was removed because we set --output in the build step. We use the implicit build included in the publish command. public DotNetCliCommandResult PublishNoRestore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore", "publish-no-restore"))); + GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore", "publish-no-restore"))); - internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) + internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) => new StringBuilder() .AppendArgument("restore") - .AppendArgument($"-f {tfm}") .AppendArgument($"\"{filePath}\"") + // restore doesn't support -f argument. .AppendArgument(string.IsNullOrEmpty(artifactsPaths.PackagesDirectoryName) ? string.Empty : $"--packages \"{artifactsPaths.PackagesDirectoryName}\"") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs index d7809b3879..807a788ecb 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs @@ -158,13 +158,14 @@ private static string GetDefaultDotNetCliPath() internal static string GetSdkPath(string cliPath) { DotNetCliCommand cliCommand = new( - projPath: string.Empty, cliPath: cliPath, + filePath: string.Empty, + tfm: string.Empty, arguments: "--info", generateResult: null, logger: NullLogger.Instance, buildPartition: null, - environmentVariables: Array.Empty(), + environmentVariables: [], timeout: TimeSpan.FromMinutes(1), logOutput: false); diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs index 1b847525ae..9d0fae9d57 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs @@ -103,10 +103,8 @@ protected override void CopyAllRequiredFiles(ArtifactsPaths artifactsPaths) protected override void GenerateBuildScript(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) { var content = new StringBuilder(300) - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath)}") .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, TargetFrameworkMoniker)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs index 72dc14aecc..185d11b564 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs @@ -4,48 +4,20 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.Results; -namespace BenchmarkDotNet.Toolchains.DotNetCli -{ - public class DotNetCliPublisher : IBuilder - { - public DotNetCliPublisher( - string? customDotNetCliPath = null, - string? extraArguments = null, - IReadOnlyList? environmentVariables = null) - { - CustomDotNetCliPath = customDotNetCliPath; - ExtraArguments = extraArguments; - EnvironmentVariables = environmentVariables; - } - - private string? CustomDotNetCliPath { get; } - - private string? ExtraArguments { get; } - - private IReadOnlyList? EnvironmentVariables { get; } +namespace BenchmarkDotNet.Toolchains.DotNetCli; - public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - { - var cliCommand = new DotNetCliCommand( - generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, - CustomDotNetCliPath, - ExtraArguments, - generateResult, - logger, - buildPartition, - EnvironmentVariables, - buildPartition.Timeout); - - // We build the original project first to obtain all dlls. - var buildResult = cliCommand.RestoreThenBuild(); - - if (!buildResult.IsBuildSuccess) - return buildResult; - - // After the dlls are built, we gather the assembly references, then build the benchmark project. - DotNetCliBuilder.GatherReferences(generateResult.ArtifactsPaths); - return cliCommand.WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath) - .RestoreThenBuildThenPublish(); - } - } +public class DotNetCliPublisher(string tfm, string? customDotNetCliPath = null, string? extraArguments = null, IReadOnlyList? environmentVariables = null) : IBuilder +{ + public virtual BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) + => new DotNetCliCommand( + customDotNetCliPath, + generateResult.ArtifactsPaths.ProjectFilePath, + tfm, + extraArguments, + generateResult, + logger, + buildPartition, + environmentVariables, + buildPartition.Timeout + ).RestoreThenBuildThenPublish(); } diff --git a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs index 8610012d53..165dedcbba 100644 --- a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs +++ b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs @@ -69,13 +69,6 @@ [PublicAPI] protected virtual string GetExecutableExtension() [PublicAPI] protected virtual string GetProjectFilePath(string buildArtifactsDirectoryPath) => string.Empty; - /// - /// returns a path to the auto-generated .csproj file that is used to build the reference dlls - /// - [PublicAPI] - protected virtual string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath) - => string.Empty; - /// /// returns a list of artifacts that should be removed after running the benchmarks /// @@ -150,7 +143,6 @@ private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string r appConfigPath: $"{executablePath}.config", nuGetConfigPath: Path.Combine(buildArtifactsDirectoryPath, "NuGet.config"), projectFilePath: GetProjectFilePath(buildArtifactsDirectoryPath), - buildForReferencesProjectFilePath: GetProjectFilePathForReferences(buildArtifactsDirectoryPath), buildScriptFilePath: Path.Combine(buildArtifactsDirectoryPath, $"{programName}{OsDetector.ScriptFileExtension}"), executablePath: executablePath, programName: programName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs index 2a8a84bce3..ca8ca60974 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs @@ -17,7 +17,6 @@ public InProcessEmitArtifactsPath( baseArtifacts.AppConfigPath, baseArtifacts.NuGetConfigPath, baseArtifacts.ProjectFilePath, - baseArtifacts.BuildForReferencesProjectFilePath, baseArtifacts.BuildScriptFilePath, baseArtifacts.ExecutablePath, baseArtifacts.ProgramName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs index 0e8fc92fa6..5e74d65f12 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs @@ -52,7 +52,6 @@ private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string r appConfigPath: null, nuGetConfigPath: null, projectFilePath: null, - buildForReferencesProjectFilePath: null, buildScriptFilePath: null, executablePath: executablePath, programName: programName, diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs deleted file mode 100644 index 9ee3b54e82..0000000000 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains.DotNetCli; -using BenchmarkDotNet.Toolchains.Results; - -namespace BenchmarkDotNet.Toolchains.Mono -{ - public class MonoPublisher : IBuilder - { - public MonoPublisher(string customDotNetCliPath) - { - CustomDotNetCliPath = customDotNetCliPath; - var runtimeIdentifier = CustomDotNetCliToolchainBuilder.GetPortableRuntimeIdentifier(); - - // /p:RuntimeIdentifiers is set explicitly here because --self-contained requires it, see https://github.com/dotnet/sdk/issues/10566 - ExtraArguments = $"--self-contained -r {runtimeIdentifier} /p:UseMonoRuntime=true /p:RuntimeIdentifiers={runtimeIdentifier}"; - } - - private string CustomDotNetCliPath { get; } - - private string ExtraArguments { get; } - - private IReadOnlyList EnvironmentVariables { get; } - - public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - { - var cliCommand = new DotNetCliCommand( - generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, - CustomDotNetCliPath, - string.Empty, - generateResult, - logger, - buildPartition, - EnvironmentVariables, - buildPartition.Timeout); - - // We build the original project first to obtain all dlls. - var buildResult = cliCommand.RestoreThenBuild(); - - if (!buildResult.IsBuildSuccess) - return buildResult; - - // After the dlls are built, we gather the assembly references, then build the benchmark project. - DotNetCliBuilder.GatherReferences(generateResult.ArtifactsPaths); - return cliCommand - .WithArguments(ExtraArguments) - .WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath) - .Publish() - .ToBuildResult(generateResult); - } - } -} diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs index 222eb2111b..1e939af7e4 100644 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs @@ -21,12 +21,17 @@ private MonoToolchain(string name, IGenerator generator, IBuilder builder, IExec [PublicAPI] public static new IToolchain From(NetCoreAppSettings settings) + => new MonoToolchain(settings.Name, + new MonoGenerator(settings.TargetFrameworkMoniker, settings.CustomDotNetCliPath, settings.PackagesPath, settings.RuntimeFrameworkVersion), + new DotNetCliPublisher(settings.TargetFrameworkMoniker, settings.CustomDotNetCliPath, GetExtraArguments()), + new DotNetCliExecutor(settings.CustomDotNetCliPath), + settings.CustomDotNetCliPath); + + private static string GetExtraArguments() { - return new MonoToolchain(settings.Name, - new MonoGenerator(settings.TargetFrameworkMoniker, settings.CustomDotNetCliPath, settings.PackagesPath, settings.RuntimeFrameworkVersion), - new MonoPublisher(settings.CustomDotNetCliPath), - new DotNetCliExecutor(settings.CustomDotNetCliPath), - settings.CustomDotNetCliPath); + var runtimeIdentifier = CustomDotNetCliToolchainBuilder.GetPortableRuntimeIdentifier(); + // /p:RuntimeIdentifiers is set explicitly here because --self-contained requires it, see https://github.com/dotnet/sdk/issues/10566 + return $"--self-contained -r {runtimeIdentifier} /p:UseMonoRuntime=true /p:RuntimeIdentifiers={runtimeIdentifier}"; } public override bool Equals(object obj) => obj is MonoToolchain typed && Equals(typed); diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs index a7fce6b40e..242d5d4269 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs @@ -36,8 +36,6 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); - GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); - string content = new StringBuilder(ResourceHelper.LoadTemplate("MonoAOTLLVMCsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) @@ -53,6 +51,8 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts .ToString(); File.WriteAllText(artifactsPaths.ProjectFilePath, content); + + GatherReferences(projectFile.FullName, buildPartition, artifactsPaths, logger); } protected override string GetPublishDirectoryPath(string buildArtifactsDirectoryPath, string configuration) diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index 227b1d5c05..e340e994f3 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -48,8 +48,6 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); - GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); - string content = new StringBuilder(ResourceHelper.LoadTemplate("WasmCsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) @@ -64,6 +62,8 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths .ToString(); File.WriteAllText(artifactsPaths.ProjectFilePath, content); + + GatherReferences(projectFile.FullName, buildPartition, artifactsPaths, logger); } protected void GenerateLinkerDescriptionFile(ArtifactsPaths artifactsPaths) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index f1ee30e543..8461d96353 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -71,13 +71,14 @@ protected override string GetBinariesDirectoryPath(string buildArtifactsDirector protected override void GenerateBuildScript(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) { + string projectFilePath = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, NullLogger.Instance).FullName; string extraArguments = NativeAotToolchain.GetExtraArguments(runtimeIdentifier); var content = new StringBuilder(300) - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath, extraArguments)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, projectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, projectFilePath, TargetFrameworkMoniker, extraArguments)}") .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, extraArguments)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, TargetFrameworkMoniker, extraArguments)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); @@ -114,19 +115,15 @@ protected override void GenerateNuGetConfig(ArtifactsPaths artifactsPaths) protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { - var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger); - - var xmlDoc = new XmlDocument(); - xmlDoc.Load(projectFile.FullName); - var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger).FullName; - GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + File.WriteAllText(artifactsPaths.ProjectFilePath, GenerateProjectForNuGetBuild(projectFile, buildPartition, artifactsPaths, logger)); - File.WriteAllText(artifactsPaths.ProjectFilePath, GenerateProjectForNuGetBuild(buildPartition, artifactsPaths, logger)); + GatherReferences(projectFile, buildPartition, artifactsPaths, logger); GenerateReflectionFile(artifactsPaths); } - private string GenerateProjectForNuGetBuild(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) => $@" + private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) => $@" false @@ -162,7 +159,7 @@ private string GenerateProjectForNuGetBuild(BuildPartition buildPartition, Artif {GetILCompilerPackageReference()} - + {string.Join(Environment.NewLine, GetRdXmlFiles(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger).Select(file => $""))} diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs index 9d476bf0e0..33da4613ed 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs @@ -60,7 +60,7 @@ internal NativeAotToolchain(string displayName, runtimeIdentifier, feeds, useNuGetClearTag, useTempFolderForRestore, packagesRestorePath, rootAllApplicationAssemblies, ilcGenerateCompleteTypeMetadata, ilcGenerateStackTraceData, ilcOptimizationPreference, ilcInstructionSet), - new DotNetCliPublisher(customDotNetCliPath, GetExtraArguments(runtimeIdentifier)), + new DotNetCliPublisher(targetFrameworkMoniker, customDotNetCliPath, GetExtraArguments(runtimeIdentifier)), new Executor()) { CustomDotNetCliPath = customDotNetCliPath; From 621face7ce5856df87f90e1c3b7c5432f4821930 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Wed, 10 Dec 2025 02:46:24 -0500 Subject: [PATCH 03/12] Use property in templates. --- src/BenchmarkDotNet/Templates/CsProj.txt | 2 +- src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt | 2 +- src/BenchmarkDotNet/Templates/WasmCsProj.txt | 2 +- src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Templates/CsProj.txt b/src/BenchmarkDotNet/Templates/CsProj.txt index 99e536b985..d7ae3cae05 100644 --- a/src/BenchmarkDotNet/Templates/CsProj.txt +++ b/src/BenchmarkDotNet/Templates/CsProj.txt @@ -4,7 +4,7 @@ false false $PROGRAMNAME$ - $TFM$ + $TFM$ true $PLATFORM$ $PROGRAMNAME$ diff --git a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt index adcb87389e..e87eee2527 100644 --- a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt +++ b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt @@ -9,7 +9,7 @@ Exe - $TFM$ + $TFM$ $RUNTIMEPACK$ $RUNTIMEIDENTIFIER$ false diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index 3db39f5669..3af58fc514 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -13,7 +13,7 @@ Exe Release false - $TFM$ + $TFM$ true $(PublishDir) $PROGRAMNAME$ diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index 8461d96353..9ff8887bfb 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -129,7 +129,7 @@ private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartiti false false Exe - {TargetFrameworkMoniker} + {TargetFrameworkMoniker} {runtimeIdentifier} {RuntimeFrameworkVersion} {artifactsPaths.ProgramName} From b7780b40dc7e3938849c510e4b6ce2369df34932 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Wed, 10 Dec 2025 04:03:54 -0500 Subject: [PATCH 04/12] Re-add MonoPublisher. --- .../DotNetCli/DotNetCliPublisher.cs | 7 +++-- .../Toolchains/Mono/MonoPublisher.cs | 31 +++++++++++++++++++ .../Toolchains/Mono/MonoToolchain.cs | 9 +----- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs index 185d11b564..e46864b98a 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs @@ -8,11 +8,14 @@ namespace BenchmarkDotNet.Toolchains.DotNetCli; public class DotNetCliPublisher(string tfm, string? customDotNetCliPath = null, string? extraArguments = null, IReadOnlyList? environmentVariables = null) : IBuilder { + public string TargetFrameworkMoniker { get; } = tfm; + public string CustomDotNetCliPath { get; } = customDotNetCliPath; + public virtual BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) => new DotNetCliCommand( - customDotNetCliPath, + CustomDotNetCliPath, generateResult.ArtifactsPaths.ProjectFilePath, - tfm, + TargetFrameworkMoniker, extraArguments, generateResult, logger, diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs new file mode 100644 index 0000000000..2620f25c86 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.Results; + +namespace BenchmarkDotNet.Toolchains.Mono; + +public class MonoPublisher(string tfm, string customDotNetCliPath) : DotNetCliPublisher(tfm, customDotNetCliPath) +{ + public override BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) + => new DotNetCliCommand( + CustomDotNetCliPath, + generateResult.ArtifactsPaths.ProjectFilePath, + TargetFrameworkMoniker, + GetExtraArguments(), + generateResult, + logger, + buildPartition, + [], + buildPartition.Timeout + ).Publish().ToBuildResult(generateResult); + + private static string GetExtraArguments() + { + var runtimeIdentifier = CustomDotNetCliToolchainBuilder.GetPortableRuntimeIdentifier(); + // /p:RuntimeIdentifiers is set explicitly here because --self-contained requires it, see https://github.com/dotnet/sdk/issues/10566 + return $"--self-contained -r {runtimeIdentifier} /p:UseMonoRuntime=true /p:RuntimeIdentifiers={runtimeIdentifier}"; + } +} diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs index 1e939af7e4..a705034463 100644 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoToolchain.cs @@ -23,17 +23,10 @@ private MonoToolchain(string name, IGenerator generator, IBuilder builder, IExec public static new IToolchain From(NetCoreAppSettings settings) => new MonoToolchain(settings.Name, new MonoGenerator(settings.TargetFrameworkMoniker, settings.CustomDotNetCliPath, settings.PackagesPath, settings.RuntimeFrameworkVersion), - new DotNetCliPublisher(settings.TargetFrameworkMoniker, settings.CustomDotNetCliPath, GetExtraArguments()), + new MonoPublisher(settings.TargetFrameworkMoniker, settings.CustomDotNetCliPath), new DotNetCliExecutor(settings.CustomDotNetCliPath), settings.CustomDotNetCliPath); - private static string GetExtraArguments() - { - var runtimeIdentifier = CustomDotNetCliToolchainBuilder.GetPortableRuntimeIdentifier(); - // /p:RuntimeIdentifiers is set explicitly here because --self-contained requires it, see https://github.com/dotnet/sdk/issues/10566 - return $"--self-contained -r {runtimeIdentifier} /p:UseMonoRuntime=true /p:RuntimeIdentifiers={runtimeIdentifier}"; - } - public override bool Equals(object obj) => obj is MonoToolchain typed && Equals(typed); public bool Equals(MonoToolchain other) => Generator.Equals(other.Generator); From 296c111db0ec297cb94e7407d90388c8b29a5dff Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 01:55:43 -0500 Subject: [PATCH 05/12] Look for TargetFrameworkAttribute for CoreRuntime. --- .../Environments/Runtimes/CoreRuntime.cs | 33 +++++++++++++++++++ .../Portability/RuntimeInformation.cs | 15 +++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index 472b629405..69c8667ade 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -46,6 +46,12 @@ public static CoreRuntime CreateForNewVersion(string msBuildMoniker, string disp return new CoreRuntime(RuntimeMoniker.NotRecognized, msBuildMoniker, displayName); } + internal static CoreRuntime GetTargetOrCurrentVersion(Assembly? assembly) + // Try to determine the Framework version that the assembly was compiled for. + => GetTargetFrameworkVersion(assembly) + // Fallback to the current running version. + ?? GetCurrentVersion(); + internal static CoreRuntime GetCurrentVersion() { if (!RuntimeInformation.IsNetCore) @@ -243,5 +249,32 @@ private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback) return new CoreRuntime(fallback.RuntimeMoniker, $"{fallback.MsBuildMoniker}-{platformName}", fallback.Name); } + + private static CoreRuntime? GetTargetFrameworkVersion(Assembly? assembly) + { + if (assembly is null) + { + return null; + } + + // Look for a TargetFrameworkAttribute with a supported Framework version. + foreach (var attribute in assembly.GetCustomAttributes()) + { + //.NETCoreApp,Version=vX.Y + const string FrameworkPrefix = ".NETCoreApp,Version=v"; + var framework = attribute.FrameworkName; + if (framework?.StartsWith(FrameworkPrefix) == true + && Version.TryParse(framework[FrameworkPrefix.Length..], out var version) + // We don't support netcoreapp1.X + && version.Major >= 2) + { + return FromVersion(version); + } + } + + // TargetFrameworkAttribute not found, or the assembly targeted a version older than we support, + // or the assembly targeted a non-core framework (like netstandard2.0). + return null; + } } } diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index 73942a2551..29639c0f91 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -171,9 +171,18 @@ string GetDetailedVersion() } internal static Runtime GetTargetOrCurrentRuntime(Assembly? assembly) - => !IsMono && !IsWasm && IsFullFramework // Match order of checks in GetCurrentRuntime(). - ? ClrRuntime.GetTargetOrCurrentVersion(assembly) - : GetCurrentRuntime(); + { + // Match order of checks in GetCurrentRuntime(). + if (!IsMono && !IsWasm) + { + if (IsFullFramework) + return ClrRuntime.GetTargetOrCurrentVersion(assembly); + // 99% of the time the core runtime is the same as the target framework, but the runtime could roll forward if it's not self-contained. + if (IsNetCore) + return CoreRuntime.GetTargetOrCurrentVersion(assembly); + } + return GetCurrentRuntime(); + } internal static Runtime GetCurrentRuntime() { From b18f236611aa2d9cfb48cd63d736e0551baea63f Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 02:21:00 -0500 Subject: [PATCH 06/12] Simplify TargetFrameworkAttribute checks. --- .../Environments/Runtimes/CoreRuntime.cs | 28 ++++++--------- .../Helpers/FrameworkVersionHelper.cs | 34 +++++++------------ 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index 69c8667ade..cc9aa151a4 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -252,28 +252,20 @@ private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback) private static CoreRuntime? GetTargetFrameworkVersion(Assembly? assembly) { - if (assembly is null) - { - return null; - } - + //.NETCoreApp,Version=vX.Y + const string FrameworkPrefix = ".NETCoreApp,Version=v"; // Look for a TargetFrameworkAttribute with a supported Framework version. - foreach (var attribute in assembly.GetCustomAttributes()) + string? framework = assembly?.GetCustomAttribute()?.FrameworkName; + if (framework?.StartsWith(FrameworkPrefix) == true + && Version.TryParse(framework[FrameworkPrefix.Length..], out var version) + // We don't support netcoreapp1.X + && version.Major >= 2) { - //.NETCoreApp,Version=vX.Y - const string FrameworkPrefix = ".NETCoreApp,Version=v"; - var framework = attribute.FrameworkName; - if (framework?.StartsWith(FrameworkPrefix) == true - && Version.TryParse(framework[FrameworkPrefix.Length..], out var version) - // We don't support netcoreapp1.X - && version.Major >= 2) - { - return FromVersion(version); - } + return FromVersion(version); } - // TargetFrameworkAttribute not found, or the assembly targeted a version older than we support, - // or the assembly targeted a non-core framework (like netstandard2.0). + // Null assembly, or TargetFrameworkAttribute not found, or the assembly targeted a version older than we support, + // or the assembly targeted a non-core tfm (like netstandard2.0). return null; } } diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index 09558d4d74..3ab98f1c50 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -23,30 +23,20 @@ private static readonly (int minReleaseNumber, string version)[] FrameworkVersio ]; internal static string? GetTargetFrameworkVersion(Assembly? assembly) - { - if (assembly is null) - { - return null; - } - // Look for a TargetFrameworkAttribute with a supported Framework version. - foreach (var attribute in assembly.GetCustomAttributes()) + => assembly?.GetCustomAttribute()?.FrameworkName switch { - switch (attribute.FrameworkName) - { - case ".NETFramework,Version=v4.6.1": return "4.6.1"; - case ".NETFramework,Version=v4.6.2": return "4.6.2"; - case ".NETFramework,Version=v4.7": return "4.7"; - case ".NETFramework,Version=v4.7.1": return "4.7.1"; - case ".NETFramework,Version=v4.7.2": return "4.7.2"; - case ".NETFramework,Version=v4.8": return "4.8"; - case ".NETFramework,Version=v4.8.1": return "4.8.1"; - } - } - - // TargetFrameworkAttribute not found, or the assembly targeted a version older than we support. - return null; - } + ".NETFramework,Version=v4.6.1" => "4.6.1", + ".NETFramework,Version=v4.6.2" => "4.6.2", + ".NETFramework,Version=v4.7" => "4.7", + ".NETFramework,Version=v4.7.1" => "4.7.1", + ".NETFramework,Version=v4.7.2" => "4.7.2", + ".NETFramework,Version=v4.8" => "4.8", + ".NETFramework,Version=v4.8.1" => "4.8.1", + // Null assembly, or TargetFrameworkAttribute not found, or the assembly targeted a version older than we support, + // or the assembly targeted a non-framework tfm (like netstandard2.0). + _ => null, + }; internal static string GetFrameworkDescription() { From cc3a4e38998c4f0f908f5047553f4c5edfc2b283 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 02:29:57 -0500 Subject: [PATCH 07/12] Fail GatherReferences gracefully. --- src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 9ac5cc777d..dee8094eaa 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -127,9 +127,12 @@ protected void GatherReferences(string projectFilePath, BuildPartition buildPart if (!buildResult.IsBuildSuccess) { - throw buildResult.TryToExplainFailureReason(out string reason) - ? new Exception(reason) - : new Exception(buildResult.ErrorMessage); + if (!buildResult.TryToExplainFailureReason(out string reason)) + { + reason = buildResult.ErrorMessage; + } + logger.WriteLineWarning($"Failed to build source project to obtain dll references. Moving forward without it. Reason: {reason}"); + return; } var xmlDoc = new XmlDocument(); From ed48359f2352567cd23ee877b659de89b4ebd360 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 03:14:27 -0500 Subject: [PATCH 08/12] Build again with actual tfm if the first build failed. --- .../Environments/Runtimes/CoreRuntime.cs | 27 ++------- .../Helpers/FrameworkVersionHelper.cs | 58 +++++++++++++++++++ .../Toolchains/CsProj/CsProjGenerator.cs | 36 ++++++++---- 3 files changed, 87 insertions(+), 34 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index cc9aa151a4..030b6cc52a 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Versioning; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Portability; @@ -47,10 +48,11 @@ public static CoreRuntime CreateForNewVersion(string msBuildMoniker, string disp } internal static CoreRuntime GetTargetOrCurrentVersion(Assembly? assembly) - // Try to determine the Framework version that the assembly was compiled for. - => GetTargetFrameworkVersion(assembly) + // Try to determine the version that the assembly was compiled for. + => FrameworkVersionHelper.GetTargetCoreVersion(assembly) is { } version + ? FromVersion(version) // Fallback to the current running version. - ?? GetCurrentVersion(); + : GetCurrentVersion(); internal static CoreRuntime GetCurrentVersion() { @@ -249,24 +251,5 @@ private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback) return new CoreRuntime(fallback.RuntimeMoniker, $"{fallback.MsBuildMoniker}-{platformName}", fallback.Name); } - - private static CoreRuntime? GetTargetFrameworkVersion(Assembly? assembly) - { - //.NETCoreApp,Version=vX.Y - const string FrameworkPrefix = ".NETCoreApp,Version=v"; - // Look for a TargetFrameworkAttribute with a supported Framework version. - string? framework = assembly?.GetCustomAttribute()?.FrameworkName; - if (framework?.StartsWith(FrameworkPrefix) == true - && Version.TryParse(framework[FrameworkPrefix.Length..], out var version) - // We don't support netcoreapp1.X - && version.Major >= 2) - { - return FromVersion(version); - } - - // Null assembly, or TargetFrameworkAttribute not found, or the assembly targeted a version older than we support, - // or the assembly targeted a non-core tfm (like netstandard2.0). - return null; - } } } diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index 3ab98f1c50..d1447b299c 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -38,6 +39,26 @@ private static readonly (int minReleaseNumber, string version)[] FrameworkVersio _ => null, }; + internal static Version? GetTargetCoreVersion(Assembly? assembly) + { + //.NETCoreApp,Version=vX.Y + const string FrameworkPrefix = ".NETCoreApp,Version=v"; + + // Look for a TargetFrameworkAttribute with a supported Framework version. + string? framework = assembly?.GetCustomAttribute()?.FrameworkName; + if (framework?.StartsWith(FrameworkPrefix) == true + && Version.TryParse(framework[FrameworkPrefix.Length..], out var version) + // We don't support netcoreapp1.X + && version.Major >= 2) + { + return version; + } + + // Null assembly, or TargetFrameworkAttribute not found, or the assembly targeted a version older than we support, + // or the assembly targeted a non-core tfm (like netstandard2.0). + return null; + } + internal static string GetFrameworkDescription() { var fullName = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; // sth like .NET Framework 4.7.3324.0 @@ -102,5 +123,42 @@ private static bool IsDeveloperPackInstalled(string version) => Directory.Exists Environment.Is64BitOperatingSystem ? Environment.SpecialFolder.ProgramFilesX86 : Environment.SpecialFolder.ProgramFiles); + + internal static string? GetTfm(Assembly assembly) + { + // We don't support exotic frameworks like Silverlight, WindowsPhone, Xamarin.Mac, etc. + const string CorePrefix = ".NETCoreApp,Version=v"; + const string FrameworkPrefix = ".NETFramework,Version=v"; + const string StandardPrefix = ".NETStandard,Version=v"; + + // Look for a TargetFrameworkAttribute with a supported Framework version. + string? framework = assembly.GetCustomAttribute()?.FrameworkName; + if (TryParseVersion(CorePrefix, out var version)) + { + return version.Major < 5 + ? $"netcoreapp{version.Major}.{version.Minor}" + : $"net{version.Major}.{version.Minor}"; + } + if (TryParseVersion(FrameworkPrefix, out version)) + { + return version.Build > 0 + ? $"net{version.Major}{version.Minor}{version.Build}" + : $"net{version.Major}{version.Minor}"; + } + if (!TryParseVersion(StandardPrefix, out version)) + { + return $"netstandard{version.Major}.{version.Minor}"; + } + + // TargetFrameworkAttribute not found, or the assembly targeted a framework we don't support, + return null; + + bool TryParseVersion(string prefix, [NotNullWhen(true)] out Version? version) + { + version = null; + return framework?.StartsWith(prefix) == true + && Version.TryParse(framework[prefix.Length..], out version); + } + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index dee8094eaa..a5716bf130 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -112,18 +112,16 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts protected void GatherReferences(string projectFilePath, BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { // Build the original project then reference all of the built dlls. - var buildResult = new DotNetCliCommand( - CliPath, - projectFilePath, - TargetFrameworkMoniker, - null, - GenerateResult.Success(artifactsPaths, []), - logger, - buildPartition, - [], - buildPartition.Timeout - ) - .RestoreThenBuild(); + BuildResult buildResult = BuildProject(TargetFrameworkMoniker); + + // The build could fail because the project doesn't have a tfm that matches the runtime, e.g. netstandard2.0 vs net10.0, + // So we try to get the actual tfm of the assembly and build again. + if (!buildResult.IsBuildSuccess + && FrameworkVersionHelper.GetTfm(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type.Assembly) is { } actualTfm + && actualTfm != TargetFrameworkMoniker) + { + buildResult = BuildProject(actualTfm); + } if (!buildResult.IsBuildSuccess) { @@ -153,6 +151,20 @@ protected void GatherReferences(string projectFilePath, BuildPartition buildPart } xmlDoc.Save(artifactsPaths.ProjectFilePath); + + BuildResult BuildProject(string tfm) + => new DotNetCliCommand( + CliPath, + projectFilePath, + tfm, + null, + GenerateResult.Success(artifactsPaths, []), + logger, + buildPartition, + [], + buildPartition.Timeout + ) + .RestoreThenBuild(); } /// From 61eec2e7922be7ad12da6da5140d19dcece1e559 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 03:27:30 -0500 Subject: [PATCH 09/12] Added TODO comment. --- src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index d1447b299c..adc678e662 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -137,6 +137,7 @@ private static bool IsDeveloperPackInstalled(string version) => Directory.Exists { return version.Major < 5 ? $"netcoreapp{version.Major}.{version.Minor}" + // TODO: Support os-specific tfms (e.g. net10.0-windows) : $"net{version.Major}.{version.Minor}"; } if (TryParseVersion(FrameworkPrefix, out version)) From 343113ce797be7c6802b3de80fb460a177d51661 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 03:49:13 -0500 Subject: [PATCH 10/12] Add platform to fallback tfm. --- .../Environments/Runtimes/CoreRuntime.cs | 52 +++++++++++-------- .../Helpers/FrameworkVersionHelper.cs | 6 ++- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index 030b6cc52a..e1ace319a0 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -50,7 +51,7 @@ public static CoreRuntime CreateForNewVersion(string msBuildMoniker, string disp internal static CoreRuntime GetTargetOrCurrentVersion(Assembly? assembly) // Try to determine the version that the assembly was compiled for. => FrameworkVersionHelper.GetTargetCoreVersion(assembly) is { } version - ? FromVersion(version) + ? FromVersion(version, assembly) // Fallback to the current running version. : GetCurrentVersion(); @@ -66,10 +67,10 @@ internal static CoreRuntime GetCurrentVersion() throw new NotSupportedException("Unable to recognize .NET Core version, please report a bug at https://github.com/dotnet/BenchmarkDotNet"); } - return FromVersion(version); + return FromVersion(version, null); } - internal static CoreRuntime FromVersion(Version version) + internal static CoreRuntime FromVersion(Version version, Assembly? assembly) { switch (version) { @@ -78,12 +79,12 @@ internal static CoreRuntime FromVersion(Version version) case Version v when v.Major == 2 && v.Minor == 2: return Core22; case Version v when v.Major == 3 && v.Minor == 0: return Core30; case Version v when v.Major == 3 && v.Minor == 1: return Core31; - case Version v when v.Major == 5 && v.Minor == 0: return GetPlatformSpecific(Core50); - case Version v when v.Major == 6 && v.Minor == 0: return GetPlatformSpecific(Core60); - case Version v when v.Major == 7 && v.Minor == 0: return GetPlatformSpecific(Core70); - case Version v when v.Major == 8 && v.Minor == 0: return GetPlatformSpecific(Core80); - case Version v when v.Major == 9 && v.Minor == 0: return GetPlatformSpecific(Core90); - case Version v when v.Major == 10 && v.Minor == 0: return GetPlatformSpecific(Core10_0); + case Version v when v.Major == 5 && v.Minor == 0: return GetPlatformSpecific(Core50, assembly); + case Version v when v.Major == 6 && v.Minor == 0: return GetPlatformSpecific(Core60, assembly); + case Version v when v.Major == 7 && v.Minor == 0: return GetPlatformSpecific(Core70, assembly); + case Version v when v.Major == 8 && v.Minor == 0: return GetPlatformSpecific(Core80, assembly); + case Version v when v.Major == 9 && v.Minor == 0: return GetPlatformSpecific(Core90, assembly); + case Version v when v.Major == 10 && v.Minor == 0: return GetPlatformSpecific(Core10_0, assembly); default: return CreateForNewVersion($"net{version.Major}.{version.Minor}", $".NET {version.Major}.{version.Minor}"); } @@ -227,29 +228,36 @@ internal static bool TryGetVersionFromFrameworkName(string frameworkName, out Ve // Version.TryParse does not handle thing like 3.0.0-WORD internal static string GetParsableVersionPart(string fullVersionName) => new string(fullVersionName.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray()); - private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback) + private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback, Assembly? assembly) + => TryGetTargetPlatform(assembly ?? Assembly.GetEntryAssembly(), out var platform) + ? new CoreRuntime(fallback.RuntimeMoniker, $"{fallback.MsBuildMoniker}-{platform}", fallback.Name) + : fallback; + + internal static bool TryGetTargetPlatform(Assembly? assembly, [NotNullWhen(true)] out string? platform) { - // TargetPlatformAttribute is not part of .NET Standard 2.0 so as usuall we have to use some reflection hacks... + platform = null; + + if (assembly is null) + return false; + + // TargetPlatformAttribute is not part of .NET Standard 2.0 so as usual we have to use some reflection hacks. var targetPlatformAttributeType = typeof(object).Assembly.GetType("System.Runtime.Versioning.TargetPlatformAttribute", throwOnError: false); if (targetPlatformAttributeType is null) // an old preview version of .NET 5 - return fallback; - - var exe = Assembly.GetEntryAssembly(); - if (exe is null) - return fallback; + return false; - var attributeInstance = exe.GetCustomAttribute(targetPlatformAttributeType); + var attributeInstance = assembly.GetCustomAttribute(targetPlatformAttributeType); if (attributeInstance is null) - return fallback; + return false; var platformNameProperty = targetPlatformAttributeType.GetProperty("PlatformName"); if (platformNameProperty is null) - return fallback; + return false; - if (!(platformNameProperty.GetValue(attributeInstance) is string platformName)) - return fallback; + if (platformNameProperty.GetValue(attributeInstance) is not string platformName) + return false; - return new CoreRuntime(fallback.RuntimeMoniker, $"{fallback.MsBuildMoniker}-{platformName}", fallback.Name); + platform = platformName; + return true; } } } diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index adc678e662..d346faabf5 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Versioning; +using BenchmarkDotNet.Environments; using Microsoft.Win32; namespace BenchmarkDotNet.Helpers @@ -137,8 +138,9 @@ private static bool IsDeveloperPackInstalled(string version) => Directory.Exists { return version.Major < 5 ? $"netcoreapp{version.Major}.{version.Minor}" - // TODO: Support os-specific tfms (e.g. net10.0-windows) - : $"net{version.Major}.{version.Minor}"; + : CoreRuntime.TryGetTargetPlatform(assembly, out var platform) + ? $"net{version.Major}.{version.Minor}-{platform}" + : $"net{version.Major}.{version.Minor}"; } if (TryParseVersion(FrameworkPrefix, out version)) { From 426700ca2915522445da5d7e7f7f82d94f511ec8 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 04:07:54 -0500 Subject: [PATCH 11/12] Fix compile errors. --- .../Environments/Runtimes/CoreRuntime.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index e1ace319a0..fcc74b7f51 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -70,25 +70,21 @@ internal static CoreRuntime GetCurrentVersion() return FromVersion(version, null); } - internal static CoreRuntime FromVersion(Version version, Assembly? assembly) + internal static CoreRuntime FromVersion(Version version, Assembly? assembly = null) => version switch { - switch (version) - { - case Version v when v.Major == 2 && v.Minor == 0: return Core20; - case Version v when v.Major == 2 && v.Minor == 1: return Core21; - case Version v when v.Major == 2 && v.Minor == 2: return Core22; - case Version v when v.Major == 3 && v.Minor == 0: return Core30; - case Version v when v.Major == 3 && v.Minor == 1: return Core31; - case Version v when v.Major == 5 && v.Minor == 0: return GetPlatformSpecific(Core50, assembly); - case Version v when v.Major == 6 && v.Minor == 0: return GetPlatformSpecific(Core60, assembly); - case Version v when v.Major == 7 && v.Minor == 0: return GetPlatformSpecific(Core70, assembly); - case Version v when v.Major == 8 && v.Minor == 0: return GetPlatformSpecific(Core80, assembly); - case Version v when v.Major == 9 && v.Minor == 0: return GetPlatformSpecific(Core90, assembly); - case Version v when v.Major == 10 && v.Minor == 0: return GetPlatformSpecific(Core10_0, assembly); - default: - return CreateForNewVersion($"net{version.Major}.{version.Minor}", $".NET {version.Major}.{version.Minor}"); - } - } + { Major: 2, Minor: 0 } => Core20, + { Major: 2, Minor: 1 } => Core21, + { Major: 2, Minor: 2 } => Core22, + { Major: 3, Minor: 0 } => Core30, + { Major: 3, Minor: 1 } => Core31, + { Major: 5 } => GetPlatformSpecific(Core50, assembly), + { Major: 6 } => GetPlatformSpecific(Core60, assembly), + { Major: 7 } => GetPlatformSpecific(Core70, assembly), + { Major: 8 } => GetPlatformSpecific(Core80, assembly), + { Major: 9 } => GetPlatformSpecific(Core90, assembly), + { Major: 10 } => GetPlatformSpecific(Core10_0, assembly), + _ => CreateForNewVersion($"net{version.Major}.{version.Minor}", $".NET {version.Major}.{version.Minor}"), + }; internal static bool TryGetVersion(out Version? version) { From c55ac8f34c492219a0bf3ef9f3d18cde077af135 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 11 Dec 2025 04:53:43 -0500 Subject: [PATCH 12/12] Remove unused method. --- src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs | 2 +- src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index d346faabf5..0205c857ad 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -153,7 +153,7 @@ private static bool IsDeveloperPackInstalled(string version) => Directory.Exists return $"netstandard{version.Major}.{version.Minor}"; } - // TargetFrameworkAttribute not found, or the assembly targeted a framework we don't support, + // TargetFrameworkAttribute not found, or the assembly targeted a framework we don't support. return null; bool TryParseVersion(string prefix, [NotNullWhen(true)] out Version? version) diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index 8a27874bff..5506dfe8c6 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -54,9 +54,6 @@ public DotNetCliCommand(string cliPath, string filePath, string tfm, string? arg public DotNetCliCommand WithArguments(string arguments) => new(CliPath, FilePath, TargetFrameworkMoniker, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); - public DotNetCliCommand WithFilePath(string filePath) - => new(CliPath, filePath, TargetFrameworkMoniker, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); - public DotNetCliCommand WithCliPath(string cliPath) => new(cliPath, FilePath, TargetFrameworkMoniker, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput);