From 44aa07e06d59a81cdefbdbc499f19c27a47c1f57 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 1/2] chore: add validation for net8 sdk as minimum requirements --- .../Running/BenchmarkRunnerClean.cs | 9 +-- .../Toolchains/DotNetCli/DotNetCliCommand.cs | 65 ++++++++++++------- .../Validators/DotNetSdkValidator.cs | 30 +++++++++ 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index f8a23c07ed..623fa48064 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -99,14 +99,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..2ae9d6d97d 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,25 @@ 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") ?? ""; + + // Trim `-preview`/`-rc` part. + var versionText = rawSdkVersion.Split('-')[0]; + if (Version.TryParse(versionText, out sdkVersion)) + return true; + + sdkVersion = null; + return false; + } } } \ No newline at end of file From a309aa0f85d31eb29cb8e30ca106ee8da39b0fff Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 11 Jul 2026 07:41:02 +0900 Subject: [PATCH 2/2] chore: use CoreRuntime.GetParsableVersionPart to parse version --- src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs b/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs index 2ae9d6d97d..4a994e484c 100644 --- a/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs +++ b/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs @@ -203,10 +203,7 @@ private static bool TryGetDotNetSdkVersion( : customDotNetCliPath!; rawSdkVersion = ProcessHelper.RunAndReadOutput(exePath, "--version") ?? ""; - - // Trim `-preview`/`-rc` part. - var versionText = rawSdkVersion.Split('-')[0]; - if (Version.TryParse(versionText, out sdkVersion)) + if (Version.TryParse(CoreRuntime.GetParsableVersionPart(rawSdkVersion), out sdkVersion)) return true; sdkVersion = null;