From 89397955d8b8176b8d4a31747d0894832764065b Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:41:46 +0900 Subject: [PATCH 1/8] feat: add custom props/targets to customize project build --- .../Templates/BenchmarkDotNet.Build.props.txt | 16 +++++++++++ .../BenchmarkDotNet.Build.targets.txt | 16 +++++++++++ src/BenchmarkDotNet/Templates/CsProj.txt | 2 ++ .../Templates/MonoAOTLLVMCsProj.txt | 6 +++-- src/BenchmarkDotNet/Templates/R2RCsProj.txt | 3 +++ src/BenchmarkDotNet/Templates/WasmCsProj.txt | 5 +++- .../Toolchains/CsProj/CsProjGenerator.cs | 27 +++++++++++++++++-- .../Toolchains/GeneratorBase.cs | 6 +++++ .../Toolchains/NativeAot/Generator.cs | 2 ++ 9 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt create mode 100644 src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt diff --git a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt new file mode 100644 index 0000000000..442be350a9 --- /dev/null +++ b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt new file mode 100644 index 0000000000..442be350a9 --- /dev/null +++ b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/BenchmarkDotNet/Templates/CsProj.txt b/src/BenchmarkDotNet/Templates/CsProj.txt index 9afef8cf5e..2add1180ed 100644 --- a/src/BenchmarkDotNet/Templates/CsProj.txt +++ b/src/BenchmarkDotNet/Templates/CsProj.txt @@ -1,4 +1,5 @@ + $PROGRAMNAME$ @@ -36,4 +37,5 @@ $RUNTIMESETTINGS$ + diff --git a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt index f005cd82b4..0f937bcfcf 100644 --- a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt +++ b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt @@ -1,4 +1,6 @@ - + + + $CSPROJPATH$ $([System.IO.Path]::ChangeExtension('$(OriginalCSProjPath)', '.Mono.props')) @@ -73,7 +75,6 @@ - + diff --git a/src/BenchmarkDotNet/Templates/R2RCsProj.txt b/src/BenchmarkDotNet/Templates/R2RCsProj.txt index e881418b1b..7341a78522 100644 --- a/src/BenchmarkDotNet/Templates/R2RCsProj.txt +++ b/src/BenchmarkDotNet/Templates/R2RCsProj.txt @@ -1,4 +1,5 @@ + @@ -61,4 +62,6 @@ + + diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index 68915fe4f6..cb18258565 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -1,4 +1,6 @@ - + + + $CSPROJPATH$ $([System.IO.Path]::ChangeExtension('$(OriginalCSProjPath)', '.Wasm.props')) @@ -60,4 +62,5 @@ $CORECLR_OVERRIDES$ + diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 08956ba6ea..4e22cd16d8 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -19,6 +19,8 @@ namespace BenchmarkDotNet.Toolchains.CsProj public class CsProjGenerator : DotNetCliGenerator, IEquatable { private const string DefaultSdkName = "Microsoft.NET.Sdk"; + private const string DllGathererProjectName = "DllGatherer.csproj"; + private const string AutogeneratedProjectName = "BenchmarkDotNet.Autogenerated.csproj"; private static readonly ImmutableArray SettingsWeWantToCopy = new[] { @@ -68,11 +70,32 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar } protected override string GetProjectFilePath(string buildArtifactsDirectoryPath) - => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.csproj"); + => Path.Combine(buildArtifactsDirectoryPath, AutogeneratedProjectName); protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker); + protected override async ValueTask GenerateCustomBuildHooksAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) + { + var benchmark = buildPartition.RepresentativeBenchmarkCase; + var benchmarkProjectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); + + var propsPath = Path.Combine(artifactsPaths.BuildArtifactsDirectoryPath, "BenchmarkDotNet.Build.props"); + var props = GetTemplateContent("BenchmarkDotNet.Build.props.txt"); + await File.WriteAllTextAsync(propsPath, props.ToString(), cancellationToken).ConfigureAwait(false); + + var targetsPath = Path.Combine(artifactsPaths.BuildArtifactsDirectoryPath, "BenchmarkDotNet.Build.targets"); + var targets = GetTemplateContent("BenchmarkDotNet.Build.targets.txt"); + await File.WriteAllTextAsync(targetsPath, targets.ToString(), cancellationToken).ConfigureAwait(false); + + string GetTemplateContent(string templateName) + => new StringBuilder(ResourceHelper.LoadTemplate(templateName)) + .Replace("$BENCHMARK_PROJ$", benchmarkProjectFile.Name) + .Replace("$GATHERER_PROJ$", DllGathererProjectName) + .Replace("$AUTOGENERATED_PROJ$", AutogeneratedProjectName) + .ToString(); + } + protected override ValueTask GenerateBuildScriptAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, CancellationToken cancellationToken) { string projectFilePath = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, NullLogger.Instance).FullName; @@ -125,7 +148,7 @@ private async ValueTask GenerateBuildProject(BuildPartition buildPartiti } private static string GetDllGathererPath(string filePath) - => Path.Combine(Path.GetDirectoryName(filePath)!, $"DllGatherer{Path.GetExtension(filePath)}"); + => Path.Combine(Path.GetDirectoryName(filePath)!, $"{Path.GetFileNameWithoutExtension(DllGathererProjectName)}{Path.GetExtension(filePath)}"); protected async ValueTask GatherReferencesAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) { diff --git a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs index 177d0985af..8183c79dfb 100644 --- a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs +++ b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs @@ -34,6 +34,7 @@ public async ValueTask GenerateProjectAsync(BuildPartition build // There is no async file copy API, so we just do it synchronously. We are likely on a ThreadPool thread here anyway if this generator is ran in parallel. CopyAllRequiredFiles(artifactsPaths); + await GenerateCustomBuildHooksAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); await GenerateCodeAsync(buildPartition, artifactsPaths, cancellationToken).ConfigureAwait(false); await GenerateAppConfigAsync(buildPartition, artifactsPaths, cancellationToken).ConfigureAwait(false); await GenerateNuGetConfigAsync(artifactsPaths, cancellationToken).ConfigureAwait(false); @@ -101,6 +102,11 @@ [PublicAPI] protected virtual void CopyAllRequiredFiles(ArtifactsPaths artifacts /// [PublicAPI] protected virtual ValueTask GenerateProjectAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) => new(); + /// + /// generates .props and .targets files to customize build. + /// + [PublicAPI] protected virtual ValueTask GenerateCustomBuildHooksAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) => new(); + /// /// generates a script can be used when debugging compilation issues /// diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index 7ce4b32a5e..63ee511528 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -137,6 +137,7 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) => $""" + Exe {TargetFrameworkMoniker} @@ -177,6 +178,7 @@ private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartiti latest + """; From 700a5d32690fc8466380a15a16585daef1d95b9c Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:54:21 +0900 Subject: [PATCH 2/8] chore: add validation for net8 sdk as minimum requirements --- .../Running/BenchmarkRunnerClean.cs | 9 +-- .../Toolchains/DotNetCli/DotNetCliCommand.cs | 65 ++++++++++++------- .../Validators/DotNetSdkValidator.cs | 27 ++++++++ 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 9a744e6ee1..35ab2d9368 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -103,14 +103,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu var buildPartitions = BenchmarkPartitioner.CreateForBuild(supportedBenchmarks, resolver); eventProcessor.OnStartBuildStage(buildPartitions); - var sequentialBuildPartitions = buildPartitions.Where(partition => - partition.Benchmarks.Any(x => x.Config.Options.IsSet(ConfigOptions.DisableParallelBuild)) - // .Net SDK 8+ supports ArtifactsPath for proper parallel builds. - // Older SDKs may produce builds with incorrect bindings if more than 1 partition is built in parallel. - || (partition.RepresentativeBenchmarkCase.GetToolchain().Generator is DotNetCliGenerator - && partition.RepresentativeBenchmarkCase.GetRuntime().RuntimeMoniker.GetRuntimeVersion().Major < 8) - ) - .ToArray(); + var sequentialBuildPartitions = buildPartitions.Where(partition => partition.Benchmarks.Any(x => x.Config.Options.IsSet(ConfigOptions.DisableParallelBuild))).ToArray(); var parallelBuildPartitions = buildPartitions.Except(sequentialBuildPartitions).ToArray(); Dictionary buildResults = parallelBuildPartitions.Length > 0 diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index 01005e450f..c83deb7779 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -148,22 +148,22 @@ public Task PublishNoRestoreAsync(CancellationToken canc cancellationToken); internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) - => new StringBuilder() + => new StringBuilder(256) .AppendArgument("restore") - .AppendArgument($"\"{filePath}\"") + .AppendArgument($"\"{filePath.ToRelativePath(artifactsPaths)}\"") // restore doesn't support -f argument. .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"--packages \"{artifactsPaths.PackagesDirectoryName}\"") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) .AppendArgument(GetMsBuildBinLogArgument(buildPartition, binLogSuffix)) - .MaybeAppendOutputPaths(artifactsPaths, true, excludeOutput) + .MaybeAppendExtraArguments(artifactsPaths, isRestore: true, excludeOutput: excludeOutput) .ToString(); internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) - => new StringBuilder() + => new StringBuilder(256) .AppendArgument("build") - .AppendArgument($"\"{filePath}\"") + .AppendArgument($"\"{filePath.ToRelativePath(artifactsPaths)}\"") .AppendArgument($"-f {tfm}") .AppendArgument($"-c {buildPartition.BuildConfiguration}") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) @@ -171,13 +171,13 @@ internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildParti .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"/p:NuGetPackageRoot=\"{artifactsPaths.PackagesDirectoryName}\"") .AppendArgument(GetMsBuildBinLogArgument(buildPartition, binLogSuffix)) - .MaybeAppendOutputPaths(artifactsPaths, excludeOutput: excludeOutput) + .MaybeAppendExtraArguments(artifactsPaths, isBuild: true, excludeOutput: excludeOutput) .ToString(); internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null) - => new StringBuilder() + => new StringBuilder(256) .AppendArgument("publish") - .AppendArgument($"\"{filePath}\"") + .AppendArgument($"\"{filePath.ToRelativePath(artifactsPaths)}\"") .AppendArgument($"-f {tfm}") .AppendArgument($"-c {buildPartition.BuildConfiguration}") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) @@ -185,7 +185,7 @@ internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPar .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"/p:NuGetPackageRoot=\"{artifactsPaths.PackagesDirectoryName}\"") .AppendArgument(GetMsBuildBinLogArgument(buildPartition, binLogSuffix)) - .MaybeAppendOutputPaths(artifactsPaths) + .MaybeAppendExtraArguments(artifactsPaths, isPublish: true) .ToString(); private static string GetMsBuildBinLogArgument(BuildPartition buildPartition, string? suffix) @@ -193,7 +193,7 @@ private static string GetMsBuildBinLogArgument(BuildPartition buildPartition, st if (!buildPartition.GenerateMSBuildBinLog || suffix.IsBlank()) return string.Empty; - return $"\"-bl:{buildPartition.ProgramName}-{suffix}.binlog\""; + return $"-bl:\"{buildPartition.ProgramName}-{suffix}.binlog\""; } private static string GetCustomMsBuildArguments(BenchmarkCase benchmarkCase, IResolver resolver) @@ -228,18 +228,37 @@ internal static class DotNetCliCommandExtensions // We force the project to output binaries to a new directory. // Specifying --output and --no-dependencies breaks the build (because the previous build was not done using the custom output path), // so we don't include it if we're building no-deps (only supported for integration tests). - internal static StringBuilder MaybeAppendOutputPaths(this StringBuilder stringBuilder, ArtifactsPaths artifactsPaths, bool isRestore = false, bool excludeOutput = false) - => excludeOutput - ? stringBuilder - : stringBuilder - // Use AltDirectorySeparatorChar so it's not interpreted as an escaped quote `\"`. - // Use a subdirectory for ArtifactsPath so that DefaultItemExcludes (which the SDK - // sets to $(ArtifactsPath)/**) doesn't cover project-level files like wwwroot/. - .AppendArgument($"/p:ArtifactsPath=\"{artifactsPaths.BuildArtifactsDirectoryPath}{Path.AltDirectorySeparatorChar}.artifacts{Path.AltDirectorySeparatorChar}\"") - .AppendArgument($"/p:OutDir=\"{artifactsPaths.BinariesDirectoryPath}{Path.AltDirectorySeparatorChar}\"") - // OutputPath is legacy, per-project version of OutDir. We set both just in case. https://github.com/dotnet/msbuild/issues/87 - .AppendArgument($"/p:OutputPath=\"{artifactsPaths.BinariesDirectoryPath}{Path.AltDirectorySeparatorChar}\"") - .AppendArgument($"/p:PublishDir=\"{artifactsPaths.PublishDirectoryPath}{Path.AltDirectorySeparatorChar}\"") - .AppendArgument(isRestore ? string.Empty : $"--output \"{artifactsPaths.BinariesDirectoryPath}{Path.AltDirectorySeparatorChar}\""); + internal static StringBuilder MaybeAppendExtraArguments( + this StringBuilder stringBuilder, + ArtifactsPaths artifactsPaths, + bool isRestore = false, + bool isBuild = false, + bool isPublish = false, + bool excludeOutput = false) + { + if (!excludeOutput) + { + // Use a subdirectory for ArtifactsPath so that DefaultItemExcludes (which the SDK + // sets to $(ArtifactsPath)/**) doesn't cover project-level files like wwwroot/. + stringBuilder.AppendArgument($"--artifacts-path .artifacts"); + + if (!isRestore) + stringBuilder.AppendArgument($"--output \"{artifactsPaths.BinariesDirectoryPath.ToRelativePath(artifactsPaths)}\""); + } + + if (isPublish) + stringBuilder.AppendArgument($"/p:PublishDir=\"{artifactsPaths.PublishDirectoryPath}\""); + + return stringBuilder; + } + + internal static string ToRelativePath(this string path, ArtifactsPaths artifactsPaths) + { + var buildArtifactsDirectoryPath = $"{artifactsPaths.BuildArtifactsDirectoryPath}{Path.DirectorySeparatorChar}"; + if (path.StartsWith(buildArtifactsDirectoryPath)) + return path.Substring(buildArtifactsDirectoryPath.Length); + + return path; + } } } diff --git a/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs b/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs index 369265cb92..4a994e484c 100644 --- a/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs +++ b/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using System.ComponentModel; @@ -20,10 +21,19 @@ public static IEnumerable ValidateCoreSdks(string? customDotNet yield return cliPathError; yield break; } + var requiredSdkVersion = benchmark.GetRuntime().RuntimeMoniker.GetRuntimeVersion(); if (!GetInstalledDotNetSdks(customDotNetCliPath).Any(sdk => sdk >= requiredSdkVersion)) { yield return new ValidationError(true, $"The required .NET Core SDK version {requiredSdkVersion} or higher for runtime moniker {benchmark.Job.Environment.Runtime!.RuntimeMoniker} is not installed.", benchmark); + yield break; + } + + // Validate actual .NET SDK version. (.NET 8 SDK is minimum requirement to use ArtifactsPath) + if (TryGetDotNetSdkVersion(customDotNetCliPath, out string rawVersionText, out Version? sdkVersion)) + { + if (sdkVersion.Major < 8) + yield return new ValidationError(true, $"The .NET 8 SDK is the minimum requirement for building the project. Resolved SDK version: {rawVersionText}", benchmark); } } @@ -182,5 +192,22 @@ private static string CheckFor45PlusVersion(int releaseKey) return ""; } + + private static bool TryGetDotNetSdkVersion( + string? customDotNetCliPath, + out string rawSdkVersion, + [NotNullWhen(true)] out Version? sdkVersion) + { + string exePath = customDotNetCliPath.IsBlank() + ? "dotnet" + : customDotNetCliPath!; + + rawSdkVersion = ProcessHelper.RunAndReadOutput(exePath, "--version") ?? ""; + if (Version.TryParse(CoreRuntime.GetParsableVersionPart(rawSdkVersion), out sdkVersion)) + return true; + + sdkVersion = null; + return false; + } } } \ No newline at end of file From f2c32f56cb6d793a309bef5f7aa11f781e48b55b Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:09:56 +0900 Subject: [PATCH 3/8] chore: modify build logics --- .../Extensions/XDocumentExtensions.cs | 17 ++ .../Templates/BenchmarkDotNet.Build.props.txt | 13 +- .../BenchmarkDotNet.Build.targets.txt | 14 -- .../Templates/MonoAOTLLVMCsProj.txt | 1 - src/BenchmarkDotNet/Templates/R2RCsProj.txt | 2 - src/BenchmarkDotNet/Templates/WasmCsProj.txt | 1 - .../Toolchains/CsProj/CsProjGenerator.cs | 112 ++-------- .../Toolchains/DotNetCli/DotNetCliCommand.cs | 192 +++++++++++------- .../Toolchains/Mono/MonoGenerator.cs | 5 +- .../MonoAotLLVM/MonoAotLLVMGenerator.cs | 2 - .../Toolchains/MonoWasm/WasmGenerator.cs | 2 - .../Toolchains/NativeAot/Generator.cs | 3 +- .../Toolchains/R2R/R2RGenerator.cs | 2 - 13 files changed, 158 insertions(+), 208 deletions(-) create mode 100644 src/BenchmarkDotNet/Extensions/XDocumentExtensions.cs diff --git a/src/BenchmarkDotNet/Extensions/XDocumentExtensions.cs b/src/BenchmarkDotNet/Extensions/XDocumentExtensions.cs new file mode 100644 index 0000000000..d8b3497757 --- /dev/null +++ b/src/BenchmarkDotNet/Extensions/XDocumentExtensions.cs @@ -0,0 +1,17 @@ +#if NETSTANDARD2_0 +using System.Xml.Linq; + +namespace BenchmarkDotNet.Extensions; + +internal static class XDocumentExtensions +{ + /// + /// Helper extension method for netstandard2.0. + /// It provides an API `SaveAsync`, but the actual processing is performed synchronously. + /// + public static async ValueTask SaveAsync(this XDocument doc, Stream stream, SaveOptions options, CancellationToken cancellationToken) + { + doc.Save(stream, options); + } +} +#endif diff --git a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt index 442be350a9..4d57435584 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt @@ -1,16 +1,7 @@ - - - - - - - - - - - + $OUTPUT_PATH$ + $PUBLISH_DIR$ diff --git a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt index 442be350a9..8c119d5413 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt @@ -1,16 +1,2 @@ - - - - - - - - - - - - - - diff --git a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt index 0f937bcfcf..10fbf761fc 100644 --- a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt +++ b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt @@ -21,7 +21,6 @@ false $PROGRAMNAME$ false - false true BenchmarkDotNet.Autogenerated.UniqueProgramName true diff --git a/src/BenchmarkDotNet/Templates/R2RCsProj.txt b/src/BenchmarkDotNet/Templates/R2RCsProj.txt index 7341a78522..9dc424cd22 100644 --- a/src/BenchmarkDotNet/Templates/R2RCsProj.txt +++ b/src/BenchmarkDotNet/Templates/R2RCsProj.txt @@ -17,8 +17,6 @@ false BenchmarkDotNet.Autogenerated.UniqueProgramName - - false diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index cb18258565..975a5ca47e 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -25,7 +25,6 @@ $CORECLR_OVERRIDES$ true $RUN_AOT$ $(RunAOTCompilation) - false false false BenchmarkDotNet.Autogenerated.UniqueProgramName diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 4e22cd16d8..2e9c26e465 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -5,13 +5,11 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; -using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; using System.Collections.Immutable; using System.Reflection; using System.Text; using System.Xml; -using System.Xml.Linq; namespace BenchmarkDotNet.Toolchains.CsProj { @@ -19,8 +17,7 @@ namespace BenchmarkDotNet.Toolchains.CsProj public class CsProjGenerator : DotNetCliGenerator, IEquatable { private const string DefaultSdkName = "Microsoft.NET.Sdk"; - private const string DllGathererProjectName = "DllGatherer.csproj"; - private const string AutogeneratedProjectName = "BenchmarkDotNet.Autogenerated.csproj"; + internal const string AutogeneratedProjectName = "BenchmarkDotNet.Autogenerated.csproj"; private static readonly ImmutableArray SettingsWeWantToCopy = new[] { @@ -80,19 +77,26 @@ protected override async ValueTask GenerateCustomBuildHooksAsync(BuildPartition var benchmark = buildPartition.RepresentativeBenchmarkCase; var benchmarkProjectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); - var propsPath = Path.Combine(artifactsPaths.BuildArtifactsDirectoryPath, "BenchmarkDotNet.Build.props"); + var buildArtifactsDirectoryPath = artifactsPaths.BuildArtifactsDirectoryPath; + + // Trim tfm part from binariesDirectoryPath on IntegrationTests. (When ArtifactsPath is not used, it's appended automatically) + var binariesDirectoryPath = buildPartition.ForcedNoDependenciesForIntegrationTests + ? artifactsPaths.BinariesDirectoryPath.Substring(0, artifactsPaths.BinariesDirectoryPath.Length - TargetFrameworkMoniker.Length - 1) + : artifactsPaths.BinariesDirectoryPath; + + var propsPath = Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Build.props"); var props = GetTemplateContent("BenchmarkDotNet.Build.props.txt"); await File.WriteAllTextAsync(propsPath, props.ToString(), cancellationToken).ConfigureAwait(false); - var targetsPath = Path.Combine(artifactsPaths.BuildArtifactsDirectoryPath, "BenchmarkDotNet.Build.targets"); + var targetsPath = Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Build.targets"); var targets = GetTemplateContent("BenchmarkDotNet.Build.targets.txt"); await File.WriteAllTextAsync(targetsPath, targets.ToString(), cancellationToken).ConfigureAwait(false); string GetTemplateContent(string templateName) => new StringBuilder(ResourceHelper.LoadTemplate(templateName)) - .Replace("$BENCHMARK_PROJ$", benchmarkProjectFile.Name) - .Replace("$GATHERER_PROJ$", DllGathererProjectName) .Replace("$AUTOGENERATED_PROJ$", AutogeneratedProjectName) + .Replace("$OUTPUT_PATH$", binariesDirectoryPath) + .Replace("$PUBLISH_DIR$", artifactsPaths.PublishDirectoryPath) .ToString(); } @@ -112,18 +116,13 @@ protected override ValueTask GenerateBuildScriptAsync(BuildPartition buildPartit protected override async ValueTask GenerateProjectAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) { + // Generate Autogenerated project file. await File.WriteAllTextAsync( artifactsPaths.ProjectFilePath, await GenerateBuildProject(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false), cancellationToken ) .ConfigureAwait(false); - - // Integration tests are built without dependencies, so we skip gathering dlls. - if (!buildPartition.ForcedNoDependenciesForIntegrationTests) - { - await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); - } } private async ValueTask GenerateBuildProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) @@ -147,91 +146,6 @@ private async ValueTask GenerateBuildProject(BuildPartition buildPartiti .ToString(); } - private static string GetDllGathererPath(string filePath) - => Path.Combine(Path.GetDirectoryName(filePath)!, $"{Path.GetFileNameWithoutExtension(DllGathererProjectName)}{Path.GetExtension(filePath)}"); - - protected async ValueTask GatherReferencesAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) - { - // Create a project using the default template to build the original project for all necessary runtime dlls. - // We can't just build the original project directly because it could be a library project, so we need an exe project to reference it. - var doc = XDocument.Parse(await GenerateBuildProject(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false)); - - // Replace the default C# file with an empty Main method to satisfy the exe build. - var compileNode = doc.Root!.Descendants("Compile").First(); - string emptyMainFile = GetDllGathererPath(artifactsPaths.ProgramCodePath); - compileNode.SetAttributeValue("Include", emptyMainFile); - string gathererProject = GetDllGathererPath(artifactsPaths.ProjectFilePath); - - using (var gathererStream = File.Create(gathererProject)) - { -#if NETSTANDARD2_0 - doc.Save(gathererStream, SaveOptions.None); -#else - await doc.SaveAsync(gathererStream, SaveOptions.None, cancellationToken).ConfigureAwait(false); -#endif - } - - await File.WriteAllTextAsync(emptyMainFile, """ - namespace BenchmarkDotNet.Autogenerated - { - public class UniqueProgramName - { - public static int Main(string[] args) - { - return 0; - } - } - } - """, cancellationToken).ConfigureAwait(false); - - // Build the original project then reference all of the built dlls. - BuildResult buildResult = await new DotNetCliCommand( - CliPath, - gathererProject, - TargetFrameworkMoniker, - arguments: "", - GenerateResult.Success(artifactsPaths, []), - logger, - buildPartition, - [], - buildPartition.Timeout - ) - .RestoreThenBuildAsync(cancellationToken) - .ConfigureAwait(false); - - if (!buildResult.IsBuildSuccess) - { - if (!buildResult.TryToExplainFailureReason(buildPartition.GetInProcessDiagnoserHandlerTypes(), out string? reason)) - { - reason = buildResult.ErrorMessage; - } - logger.WriteLineWarning($"Failed to gather assembly references, reason:{Environment.NewLine}{reason}"); - return; - } - - // Delete the dll from the gatherer project to prevent duplicate references. - File.Delete(Path.Combine(artifactsPaths.BinariesDirectoryPath, $"{artifactsPaths.ProgramName}.dll")); - - doc = XDocument.Load(artifactsPaths.ProjectFilePath); - var itemGroup = new XElement("ItemGroup"); - doc.Root!.Add(itemGroup); - foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll")) - { - itemGroup.Add(new XElement("Reference", - new XAttribute("Include", Path.GetFileNameWithoutExtension(assemblyFile)), - new XElement("HintPath", assemblyFile) - // TODO: Add Aliases here for extern alias #2289 - )); - } - - using var projectStream = File.Create(artifactsPaths.ProjectFilePath); -#if NETSTANDARD2_0 - doc.Save(projectStream, SaveOptions.None); -#else - await doc.SaveAsync(projectStream, SaveOptions.None, cancellationToken).ConfigureAwait(false); -#endif - } - /// /// returns an MSBuild string that defines Runtime settings /// diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index c83deb7779..5014710066 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -67,32 +67,29 @@ public async Task RestoreThenBuildAsync(CancellationToken cancellat return result.ToBuildResult(GenerateResult); } - // On our CI, Integration tests take too much time, because each benchmark run rebuilds BenchmarkDotNet itself. - // To reduce the total duration of the CI workflows, we build all the projects without dependencies + // Run `dotnet restore` and `dotnet build` command. if (BuildPartition.ForcedNoDependenciesForIntegrationTests) { - var restoreResult = await DotNetCliCommandExecutor.ExecuteAsync( - WithArguments(GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true)), - cancellationToken).ConfigureAwait(false); + // On our CI, Integration tests take too much time, because each benchmark run rebuilds BenchmarkDotNet itself. + // To reduce the total duration of the CI workflows, we build all the projects without dependencies + + // Run `dotnet restore` command with `--no-dependency`. + var restoreResult = await RestoreNoDependenciesAsync(cancellationToken).ConfigureAwait(false); if (!restoreResult.IsSuccess) return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); - var result = await DotNetCliCommandExecutor.ExecuteAsync( - WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true) - ), - cancellationToken).ConfigureAwait(false); - - return result.ToBuildResult(GenerateResult); + // Run `dotnet build` command with `--no-restore --no-dependency`. + var buildResult = await BuildNoRestoreNoDependenciesAsync(cancellationToken).ConfigureAwait(false); + return buildResult.ToBuildResult(GenerateResult); } else { + // Run `dotnet restore` command. var restoreResult = await RestoreAsync(cancellationToken).ConfigureAwait(false); if (!restoreResult.IsSuccess) return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); - // We no longer retry with --no-dependencies, because it fails with --output set at the same time, - // and the artifactsPaths.BinariesDirectoryPath is set before we try to build, so we cannot overwrite it. + // Run `dotnet build` command with `--no-restore` command. var result = await BuildNoRestoreAsync(cancellationToken).ConfigureAwait(false); return result.ToBuildResult(GenerateResult); } @@ -112,13 +109,38 @@ public async Task RestoreThenBuildThenPublishAsync(CancellationToke return result.ToBuildResult(GenerateResult); } - var restoreResult = await RestoreAsync(cancellationToken).ConfigureAwait(false); - if (!restoreResult.IsSuccess) - return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); + if (BuildPartition.ForcedNoDependenciesForIntegrationTests) + { + // Run `dotnet restore` command with `--no-dependencies`. + var restoreResult = await RestoreNoDependenciesAsync(cancellationToken).ConfigureAwait(false); + if (!restoreResult.IsSuccess) + return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); + + // Run `dotnet build` command with `--no-restore --no-dependencies`. + var buildResult = await BuildNoRestoreNoDependenciesAsync(cancellationToken).ConfigureAwait(false); + if (!buildResult.IsSuccess) + return BuildResult.Failure(GenerateResult, buildResult.AllInformation); + + // Run `dotnet publish` command with `--no-build`. + var publishResult = await PublishNoBuildAsync(cancellationToken).ConfigureAwait(false); + return publishResult.ToBuildResult(GenerateResult); + } + else + { + // Run `dotnet restore` command. + var restoreResult = await RestoreAsync(cancellationToken).ConfigureAwait(false); + if (!restoreResult.IsSuccess) + return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); + + // Run `dotnet build` command with `--no-restore`. + var buildResult = await BuildNoRestoreAsync(cancellationToken).ConfigureAwait(false); + if (!buildResult.IsSuccess) + return BuildResult.Failure(GenerateResult, buildResult.AllInformation); - // We use the implicit build in the publish command. We stopped doing a separate build step because we set the --output. - var publishResult = await PublishNoRestoreAsync(cancellationToken).ConfigureAwait(false); - return publishResult.ToBuildResult(GenerateResult); + // Run `dotnet publish` command with `--no-build`. + var publishResult = await PublishNoBuildAsync(cancellationToken).ConfigureAwait(false); + return publishResult.ToBuildResult(GenerateResult); + } } public Task RestoreAsync(CancellationToken cancellationToken = default) @@ -126,6 +148,11 @@ public Task RestoreAsync(CancellationToken cancellationT WithArguments(GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "restore")), cancellationToken); + public Task RestoreNoDependenciesAsync(CancellationToken cancellationToken = default) + => DotNetCliCommandExecutor.ExecuteAsync( + WithArguments(GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-dependencies", "restore-no-deps")), + cancellationToken); + public Task BuildAsync(CancellationToken cancellationToken = default) => DotNetCliCommandExecutor.ExecuteAsync( WithArguments(GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, Arguments, "build")), @@ -136,64 +163,81 @@ public Task BuildNoRestoreAsync(CancellationToken cancel WithArguments(GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore", "build-no-restore")), cancellationToken); + public Task BuildNoRestoreNoDependenciesAsync(CancellationToken cancellationToken = default) + => DotNetCliCommandExecutor.ExecuteAsync( + WithArguments(GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps")), + cancellationToken); + public Task PublishAsync(CancellationToken cancellationToken = default) => DotNetCliCommandExecutor.ExecuteAsync( WithArguments(GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, Arguments, "publish")), cancellationToken); - // PublishNoBuildAndNoRestore was removed because we set --output in the build step. We use the implicit build included in the publish command. - public Task PublishNoRestoreAsync(CancellationToken cancellationToken = default) + public Task PublishNoBuildAsync(CancellationToken cancellationToken = default) => DotNetCliCommandExecutor.ExecuteAsync( - WithArguments(GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-restore", "publish-no-restore")), + WithArguments(GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, TargetFrameworkMoniker, $"{Arguments} --no-build", "publish-no-build")), cancellationToken); - internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, 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) => new StringBuilder(256) .AppendArgument("restore") - .AppendArgument($"\"{filePath.ToRelativePath(artifactsPaths)}\"") + .AppendArgument(filePath.ToRelativePath(artifactsPaths).QuoteIfNeeded()) // restore doesn't support -f argument. - .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"--packages \"{artifactsPaths.PackagesDirectoryName}\"") + .AppendArgument(GetArtifactsPathArguments(buildPartition)) + .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"--packages {artifactsPaths.PackagesDirectoryName.QuoteIfNeeded()}") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) - .AppendArgument(GetMsBuildBinLogArgument(buildPartition, binLogSuffix)) - .MaybeAppendExtraArguments(artifactsPaths, isRestore: true, excludeOutput: excludeOutput) + .AppendArgument(GetMsBuildBinLogArgument(buildPartition, filePath, binLogSuffix)) .ToString(); - internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, 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) => new StringBuilder(256) .AppendArgument("build") - .AppendArgument($"\"{filePath.ToRelativePath(artifactsPaths)}\"") + .AppendArgument(filePath.ToRelativePath(artifactsPaths).QuoteIfNeeded()) .AppendArgument($"-f {tfm}") .AppendArgument($"-c {buildPartition.BuildConfiguration}") + .AppendArgument(GetArtifactsPathArguments(buildPartition)) .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) - .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"/p:NuGetPackageRoot=\"{artifactsPaths.PackagesDirectoryName}\"") - .AppendArgument(GetMsBuildBinLogArgument(buildPartition, binLogSuffix)) - .MaybeAppendExtraArguments(artifactsPaths, isBuild: true, excludeOutput: excludeOutput) + .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"/p:NuGetPackageRoot={artifactsPaths.PackagesDirectoryName.QuoteIfNeeded()}") + .AppendArgument(GetMsBuildBinLogArgument(buildPartition, filePath, binLogSuffix)) .ToString(); internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null) => new StringBuilder(256) .AppendArgument("publish") - .AppendArgument($"\"{filePath.ToRelativePath(artifactsPaths)}\"") + .AppendArgument(filePath.ToRelativePath(artifactsPaths).QuoteIfNeeded()) .AppendArgument($"-f {tfm}") .AppendArgument($"-c {buildPartition.BuildConfiguration}") + .AppendArgument(GetArtifactsPathArguments(buildPartition)) .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) - .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"/p:NuGetPackageRoot=\"{artifactsPaths.PackagesDirectoryName}\"") - .AppendArgument(GetMsBuildBinLogArgument(buildPartition, binLogSuffix)) - .MaybeAppendExtraArguments(artifactsPaths, isPublish: true) + .AppendArgument(artifactsPaths.PackagesDirectoryName.IsBlank() ? string.Empty : $"/p:NuGetPackageRoot={artifactsPaths.PackagesDirectoryName.QuoteIfNeeded()}") + .AppendArgument(GetMsBuildBinLogArgument(buildPartition, filePath, binLogSuffix)) .ToString(); - private static string GetMsBuildBinLogArgument(BuildPartition buildPartition, string? suffix) + private static string GetArtifactsPathArguments(BuildPartition buildPartition) + { + // Don't use `--artifacts-path` for integration tests. Because it build with `no-dependencies`. + if (buildPartition.ForcedNoDependenciesForIntegrationTests) + return ""; + + var artifactsPath = ".artifacts"; + return $"--artifacts-path {artifactsPath}"; + } + + private static string GetMsBuildBinLogArgument(BuildPartition buildPartition, string projectPath, string? suffix) { if (!buildPartition.GenerateMSBuildBinLog || suffix.IsBlank()) return string.Empty; - return $"-bl:\"{buildPartition.ProgramName}-{suffix}.binlog\""; + var projectName = Path.GetFileNameWithoutExtension(projectPath); + + var fileName = $"{projectName}-{suffix}.binlog".QuoteIfNeeded(); + return $"-bl:{fileName}"; } private static string GetCustomMsBuildArguments(BenchmarkCase benchmarkCase, IResolver resolver) @@ -222,36 +266,8 @@ private static string GetMandatoryMsBuildSettings(string buildConfiguration) } } - internal static class DotNetCliCommandExtensions + file static class DotNetCliCommandExtensions { - // Fix #1377 (see comments in #1773). - // We force the project to output binaries to a new directory. - // Specifying --output and --no-dependencies breaks the build (because the previous build was not done using the custom output path), - // so we don't include it if we're building no-deps (only supported for integration tests). - internal static StringBuilder MaybeAppendExtraArguments( - this StringBuilder stringBuilder, - ArtifactsPaths artifactsPaths, - bool isRestore = false, - bool isBuild = false, - bool isPublish = false, - bool excludeOutput = false) - { - if (!excludeOutput) - { - // Use a subdirectory for ArtifactsPath so that DefaultItemExcludes (which the SDK - // sets to $(ArtifactsPath)/**) doesn't cover project-level files like wwwroot/. - stringBuilder.AppendArgument($"--artifacts-path .artifacts"); - - if (!isRestore) - stringBuilder.AppendArgument($"--output \"{artifactsPaths.BinariesDirectoryPath.ToRelativePath(artifactsPaths)}\""); - } - - if (isPublish) - stringBuilder.AppendArgument($"/p:PublishDir=\"{artifactsPaths.PublishDirectoryPath}\""); - - return stringBuilder; - } - internal static string ToRelativePath(this string path, ArtifactsPaths artifactsPaths) { var buildArtifactsDirectoryPath = $"{artifactsPaths.BuildArtifactsDirectoryPath}{Path.DirectorySeparatorChar}"; @@ -260,5 +276,45 @@ internal static string ToRelativePath(this string path, ArtifactsPaths artifacts return path; } + + internal static string QuoteIfNeeded(this string commandArg) + { + ArgumentNullException.ThrowIfNull(commandArg); + + if (commandArg.Length == 0) + return "\"\""; + + if (!commandArg.Any(char.IsWhiteSpace) && !commandArg.Contains('"')) + return commandArg; + + var builder = new StringBuilder(commandArg.Length + 2); + builder.Append('"'); + + var backslashCount = 0; + foreach (var c in commandArg) + { + switch (c) + { + case '\\': + backslashCount++; + continue; + case '"': + builder.Append('\\', backslashCount * 2 + 1); + builder.Append('"'); + break; + default: + builder.Append('\\', backslashCount); + builder.Append(c); + break; + } + + backslashCount = 0; + } + + builder.Append('\\', backslashCount * 2); + builder.Append('"'); + + return builder.ToString(); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoGenerator.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoGenerator.cs index 543283bd0c..2de05e173d 100644 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoGenerator.cs @@ -12,13 +12,10 @@ public MonoGenerator(string targetFrameworkMoniker, string cliPath, string packa protected override string GetRuntimeSettings(GcMode gcMode, IResolver resolver) { - // Workaround for following issues. - // 1. 'Found multiple publish output files with the same relative path' error - // 2. NU1102 error occurs when passing /p:UseMonoRuntime=true to the dotnet cli with projects containing .NET 9.0 or higher. #3000 + // NU1102 error occurs when passing /p:UseMonoRuntime=true to the dotnet cli with projects containing .NET 9.0 or higher. #3000 return base.GetRuntimeSettings(gcMode, resolver) + """ - false true """; diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs index 0253e4fc85..64181b4131 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs @@ -51,8 +51,6 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart .ToString(); await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, content, cancellationToken).ConfigureAwait(false); - - await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } 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 2692eb6f1b..3a4e09e211 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -86,8 +86,6 @@ protected async ValueTask GenerateProjectFileAsync(BuildPartition buildPartition .ToString(); await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, content, cancellationToken).ConfigureAwait(false); - - await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } protected async ValueTask GenerateMainJS(FileInfo? mainJsTemplate, string targetMainJsPath, CancellationToken cancellationToken) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index 63ee511528..f4159dc251 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -131,7 +131,7 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, GenerateProjectForNuGetBuild(projectFile, buildPartition, artifactsPaths, logger), cancellationToken).ConfigureAwait(false); - await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); + // Generate `bdn_generated.rd.xml` await GenerateReflectionFileAsync(artifactsPaths, cancellationToken).ConfigureAwait(false); } @@ -158,7 +158,6 @@ private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartiti {ilcGenerateStackTraceData} {ilcGenerateStackTraceData} false - false false {GetInstructionSetSettings(buildPartition)} diff --git a/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs b/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs index 506d22ac33..71d6a842cd 100644 --- a/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs @@ -46,8 +46,6 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart .ToString(); await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, content, cancellationToken).ConfigureAwait(false); - - await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } protected override string GetExecutableExtension() => OsDetector.ExecutableExtension; From cab8dc62e8bb2f487fe111a105cfdeaec0c1d9e8 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:08:36 +0900 Subject: [PATCH 4/8] chore: add NETSDK1085 error ignore setting --- src/BenchmarkDotNet/Templates/WasmCsProj.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index 975a5ca47e..0e3170127f 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -28,6 +28,11 @@ $CORECLR_OVERRIDES$ false false BenchmarkDotNet.Autogenerated.UniqueProgramName + + + + + false From 7e64641d53aba7945f40c8916a753c708d0742b6 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:34:34 +0900 Subject: [PATCH 5/8] chore: increase wasm related tests timeouts for macos(x64) --- .../CancellationTokenTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs index 8e37f112dd..dc4a9a5127 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs @@ -66,7 +66,7 @@ public void BenchmarkWithCancellationTokenProperty_ReceivesToken_Wasm(string jav .AddJob(Job.Dry .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet10_0, "wasm", false, javaScriptEngine)) .WithToolchain(WasmToolchain.From(netCoreAppSettings))) - .WithBuildTimeout(TimeSpan.FromSeconds(240)) + .WithBuildTimeout(TimeSpan.FromSeconds(480)) .WithOption(ConfigOptions.LogBuildOutput, true) .WithOption(ConfigOptions.GenerateMSBuildBinLog, false); @@ -133,7 +133,7 @@ public async Task RunWithCancellationTokenIsCancelled_Wasm(string javaScriptEngi .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet10_0, "wasm", false, javaScriptEngine)) .WithToolchain(WasmToolchain.From(netCoreAppSettings))) .AddDiagnoser(diagnoser) - .WithBuildTimeout(TimeSpan.FromSeconds(240)) + .WithBuildTimeout(TimeSpan.FromSeconds(480)) .WithOption(ConfigOptions.LogBuildOutput, true) .WithOption(ConfigOptions.GenerateMSBuildBinLog, false); From 319690fe85f8809428f3bada925d59a36469bf47 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:56:07 +0900 Subject: [PATCH 6/8] chore: re-add DllGatherer steps and modify related build logics --- .../Templates/BenchmarkDotNet.Build.props.txt | 4 + .../BenchmarkDotNet.Build.targets.txt | 7 ++ .../Toolchains/CsProj/CsProjGenerator.cs | 91 +++++++++++++++++++ .../Toolchains/DotNetCli/DotNetCliCommand.cs | 8 +- .../MonoAotLLVM/MonoAotLLVMGenerator.cs | 6 ++ .../Toolchains/MonoWasm/WasmGenerator.cs | 6 ++ .../Toolchains/NativeAot/Generator.cs | 6 ++ .../Toolchains/R2R/R2RGenerator.cs | 6 ++ 8 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt index 4d57435584..bfe8a19491 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt @@ -1,4 +1,8 @@ + + + + $OUTPUT_PATH$ diff --git a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt index 8c119d5413..4ae78eb8ad 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.targets.txt @@ -1,2 +1,9 @@ + + + + diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 2e9c26e465..a0d197e3f4 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -5,11 +5,13 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; using System.Collections.Immutable; using System.Reflection; using System.Text; using System.Xml; +using System.Xml.Linq; namespace BenchmarkDotNet.Toolchains.CsProj { @@ -17,6 +19,7 @@ namespace BenchmarkDotNet.Toolchains.CsProj public class CsProjGenerator : DotNetCliGenerator, IEquatable { private const string DefaultSdkName = "Microsoft.NET.Sdk"; + internal const string DllGathererProjectName = "DllGatherer.csproj"; internal const string AutogeneratedProjectName = "BenchmarkDotNet.Autogenerated.csproj"; private static readonly ImmutableArray SettingsWeWantToCopy = new[] @@ -94,6 +97,7 @@ protected override async ValueTask GenerateCustomBuildHooksAsync(BuildPartition string GetTemplateContent(string templateName) => new StringBuilder(ResourceHelper.LoadTemplate(templateName)) + .Replace("$DLLGATHERER_PROJ$", DllGathererProjectName) .Replace("$AUTOGENERATED_PROJ$", AutogeneratedProjectName) .Replace("$OUTPUT_PATH$", binariesDirectoryPath) .Replace("$PUBLISH_DIR$", artifactsPaths.PublishDirectoryPath) @@ -123,6 +127,93 @@ await GenerateBuildProject(buildPartition, artifactsPaths, logger, cancellationT cancellationToken ) .ConfigureAwait(false); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (buildPartition.ForcedNoDependenciesForIntegrationTests) + return; + + // Gather DLL references and modify Autogenerated project file + await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); + } + + private static string GetDllGathererPath(string filePath) + => Path.Combine(Path.GetDirectoryName(filePath)!, $"{Path.GetFileNameWithoutExtension(DllGathererProjectName)}{Path.GetExtension(filePath)}"); + + protected async ValueTask GatherReferencesAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) + { + // Create a project using the default template to build the original project for all necessary runtime dlls. + // We can't just build the original project directly because it could be a library project, so we need an exe project to reference it. + var doc = XDocument.Parse(await GenerateBuildProject(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false)); + + // Replace the default C# file with an empty Main method to satisfy the exe build. + var compileNode = doc.Root!.Descendants("Compile").First(); + string emptyMainFile = GetDllGathererPath(artifactsPaths.ProgramCodePath); + compileNode.SetAttributeValue("Include", emptyMainFile); + string gathererProject = GetDllGathererPath(artifactsPaths.ProjectFilePath); + + using (var gathererStream = File.Create(gathererProject)) + { + await doc.SaveAsync(gathererStream, SaveOptions.None, cancellationToken).ConfigureAwait(false); + } + + await File.WriteAllTextAsync(emptyMainFile, """ + namespace BenchmarkDotNet.Autogenerated + { + public class UniqueProgramName + { + public static int Main(string[] args) + { + return 0; + } + } + } + """, cancellationToken).ConfigureAwait(false); + + // Build the original project then reference all of the built dlls. + BuildResult buildResult = await new DotNetCliCommand( + CliPath, + gathererProject, + TargetFrameworkMoniker, + arguments: "", + GenerateResult.Success(artifactsPaths, []), + logger, + buildPartition, + [], + buildPartition.Timeout + ) + .RestoreThenBuildAsync(cancellationToken) + .ConfigureAwait(false); + + if (!buildResult.IsBuildSuccess) + { + if (!buildResult.TryToExplainFailureReason(buildPartition.GetInProcessDiagnoserHandlerTypes(), out string? reason)) + { + reason = buildResult.ErrorMessage; + } + logger.WriteLineWarning($"Failed to gather assembly references, reason:{Environment.NewLine}{reason}"); + return; + } + + // Gets bin directory of DllGather project from output file that generated by custom MSBuild target. + var binDirectory = File.ReadAllText(Path.Combine(artifactsPaths.BuildArtifactsDirectoryPath, "DllGatherer.txt")).Trim(); + + // Delete the dll from the gatherer project to prevent duplicate references. + File.Delete(Path.Combine(binDirectory, $"{artifactsPaths.ProgramName}.dll")); + + doc = XDocument.Load(artifactsPaths.ProjectFilePath); + var itemGroup = new XElement("ItemGroup"); + doc.Root!.Add(itemGroup); + foreach (var assemblyFile in Directory.GetFiles(binDirectory, "*.dll")) + { + itemGroup.Add(new XElement("Reference", + new XAttribute("Include", Path.GetFileNameWithoutExtension(assemblyFile)), + new XElement("HintPath", assemblyFile) + // TODO: Add Aliases here for extern alias #2289 + )); + } + + using var projectStream = File.Create(artifactsPaths.ProjectFilePath); + await doc.SaveAsync(projectStream, SaveOptions.None, cancellationToken).ConfigureAwait(false); } private async ValueTask GenerateBuildProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken) diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index 5014710066..d7d2c33833 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; using System.Text; @@ -53,6 +54,9 @@ public DotNetCliCommand WithArguments(string arguments) public DotNetCliCommand WithCliPath(string cliPath) => new(cliPath, FilePath, TargetFrameworkMoniker, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput); + private bool UseNoDependencies + => BuildPartition.IsCustomBuildConfiguration || Path.GetFileName(FilePath) == CsProjGenerator.AutogeneratedProjectName; + [PublicAPI] public async Task RestoreThenBuildAsync(CancellationToken cancellationToken = default) { @@ -68,7 +72,7 @@ public async Task RestoreThenBuildAsync(CancellationToken cancellat } // Run `dotnet restore` and `dotnet build` command. - if (BuildPartition.ForcedNoDependenciesForIntegrationTests) + if (UseNoDependencies) { // On our CI, Integration tests take too much time, because each benchmark run rebuilds BenchmarkDotNet itself. // To reduce the total duration of the CI workflows, we build all the projects without dependencies @@ -109,7 +113,7 @@ public async Task RestoreThenBuildThenPublishAsync(CancellationToke return result.ToBuildResult(GenerateResult); } - if (BuildPartition.ForcedNoDependenciesForIntegrationTests) + if (UseNoDependencies) { // Run `dotnet restore` command with `--no-dependencies`. var restoreResult = await RestoreNoDependenciesAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs index 64181b4131..03cf712600 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs @@ -51,6 +51,12 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart .ToString(); await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, content, cancellationToken).ConfigureAwait(false); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (buildPartition.ForcedNoDependenciesForIntegrationTests) + return; + + await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } 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 3a4e09e211..2e7d74fb7f 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -86,6 +86,12 @@ protected async ValueTask GenerateProjectFileAsync(BuildPartition buildPartition .ToString(); await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, content, cancellationToken).ConfigureAwait(false); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (buildPartition.ForcedNoDependenciesForIntegrationTests) + return; + + await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } protected async ValueTask GenerateMainJS(FileInfo? mainJsTemplate, string targetMainJsPath, CancellationToken cancellationToken) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index f4159dc251..a2c725628e 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -133,6 +133,12 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart // Generate `bdn_generated.rd.xml` await GenerateReflectionFileAsync(artifactsPaths, cancellationToken).ConfigureAwait(false); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (buildPartition.ForcedNoDependenciesForIntegrationTests) + return; + + await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) => $""" diff --git a/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs b/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs index 71d6a842cd..8c0ba96402 100644 --- a/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/R2R/R2RGenerator.cs @@ -46,6 +46,12 @@ protected override async ValueTask GenerateProjectAsync(BuildPartition buildPart .ToString(); await File.WriteAllTextAsync(artifactsPaths.ProjectFilePath, content, cancellationToken).ConfigureAwait(false); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (buildPartition.ForcedNoDependenciesForIntegrationTests) + return; + + await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false); } protected override string GetExecutableExtension() => OsDetector.ExecutableExtension; From 7f18102488fb3c0ebf41a52ca73b83b7e829d692 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:42:47 +0900 Subject: [PATCH 7/8] chore: re-enable NETSDK1152 error suppression for wasm project --- src/BenchmarkDotNet/Templates/WasmCsProj.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index 0e3170127f..37072f47bf 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -29,6 +29,9 @@ $CORECLR_OVERRIDES$ false BenchmarkDotNet.Autogenerated.UniqueProgramName + + false + From 75973bff459385218e39a5bb5f9215944065548d Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:50:34 +0900 Subject: [PATCH 8/8] chore: increase timeout for WakeLockTests --- tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs index f9cc6fef84..4fc7a4cb5e 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs @@ -13,7 +13,7 @@ public class WakeLockTests : BenchmarkTestExecutor { private const string PingEventName = @"Global\WakeLockTests-ping"; private const string PongEventName = @"Global\WakeLockTests-pong"; - private static readonly TimeSpan testTimeout = TimeSpan.FromMinutes(1); + private static readonly TimeSpan testTimeout = TimeSpan.FromMinutes(5); // Set 5 minutes for running tests with `ForcedNoDependenciesForIntegrationTests=false` private readonly OutputLogger logger; public WakeLockTests(ITestOutputHelper output) : base(output)