diff --git a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs index 7eca3f97f0..8bdacf37a4 100644 --- a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs @@ -66,7 +66,7 @@ public IEnumerable Validate(BenchmarkCase benchmark, IResolver $"Provided CoreRun path does not exist, benchmark '{benchmark.DisplayInfo}' will not be executed. Please remember that BDN expects path to CoreRun.exe (corerun on Unix), not to Core_Root folder.", benchmark); } - else if (Toolchain.IsCliPathInvalid(CustomDotNetCliPath?.FullName, benchmark, out var invalidCliError)) + else if (DotNetSdkValidator.IsCliPathInvalid(CustomDotNetCliPath?.FullName, benchmark, out var invalidCliError)) { yield return invalidCliError; } diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs index e3c3f85125..187458be17 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs @@ -49,11 +49,17 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas yield return new ValidationError(true, $"Classic .NET toolchain is supported only for Windows, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", benchmarkCase); + yield break; } - else if (IsCliPathInvalid(CustomDotNetCliPath, benchmarkCase, out var invalidCliError)) + else if (DotNetSdkValidator.IsCliPathInvalid(CustomDotNetCliPath, benchmarkCase, out var invalidCliError)) { yield return invalidCliError; } + + foreach (var validationError in DotNetSdkValidator.ValidateFrameworkSdks(benchmarkCase)) + { + yield return validationError; + } } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs index 98bcd2ae8b..c6cf6248df 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs @@ -49,11 +49,6 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas yield return validationError; } - if (IsCliPathInvalid(CustomDotNetCliPath, benchmarkCase, out var invalidCliError)) - { - yield return invalidCliError; - } - if (benchmarkCase.Job.HasValue(EnvironmentMode.JitCharacteristic) && benchmarkCase.Job.ResolveValue(EnvironmentMode.JitCharacteristic, resolver) == Jit.LegacyJit) { yield return new ValidationError(true, @@ -80,6 +75,11 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas $"Currently CsProjCoreToolchain does not support LINQPad 6+. Please use {nameof(InProcessEmitToolchain)} instead.", benchmarkCase); } + + foreach (var validationError in DotNetSdkValidator.ValidateCoreSdks(CustomDotNetCliPath, benchmarkCase)) + { + yield return validationError; + } } public override bool Equals(object obj) => obj is CsProjCoreToolchain typed && Equals(typed); diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs index 0d77160e06..06ef8ea8c6 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs @@ -1,24 +1,45 @@ +using BenchmarkDotNet.Characteristics; +using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Validators; +using System.Collections.Generic; namespace BenchmarkDotNet.Toolchains.MonoAotLLVM { public class MonoAotLLVMToolChain : Toolchain { - public MonoAotLLVMToolChain(string name, IGenerator generator, IBuilder builder, IExecutor executor) + private readonly string _customDotNetCliPath; + + public MonoAotLLVMToolChain(string name, IGenerator generator, IBuilder builder, IExecutor executor, string customDotNetCliPath) : base(name, generator, builder, executor) { + _customDotNetCliPath = customDotNetCliPath; } public static IToolchain From(NetCoreAppSettings netCoreAppSettings) - => new MonoAotLLVMToolChain(netCoreAppSettings.Name, - new MonoAotLLVMGenerator(netCoreAppSettings.TargetFrameworkMoniker, - netCoreAppSettings.CustomDotNetCliPath, - netCoreAppSettings.PackagesPath, - netCoreAppSettings.CustomRuntimePack, - netCoreAppSettings.AOTCompilerPath, - netCoreAppSettings.AOTCompilerMode), - new DotNetCliBuilder(netCoreAppSettings.TargetFrameworkMoniker, - netCoreAppSettings.CustomDotNetCliPath), - new Executor()); + => new MonoAotLLVMToolChain(netCoreAppSettings.Name, + new MonoAotLLVMGenerator(netCoreAppSettings.TargetFrameworkMoniker, + netCoreAppSettings.CustomDotNetCliPath, + netCoreAppSettings.PackagesPath, + netCoreAppSettings.CustomRuntimePack, + netCoreAppSettings.AOTCompilerPath, + netCoreAppSettings.AOTCompilerMode), + new DotNetCliBuilder(netCoreAppSettings.TargetFrameworkMoniker, + netCoreAppSettings.CustomDotNetCliPath), + new Executor(), + netCoreAppSettings.CustomDotNetCliPath); + + public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + { + foreach (var validationError in base.Validate(benchmarkCase, resolver)) + { + yield return validationError; + } + + foreach (var validationError in DotNetSdkValidator.ValidateCoreSdks(_customDotNetCliPath, benchmarkCase)) + { + yield return validationError; + } + } } } diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index c5ede03e49..42e5c428d2 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -32,9 +32,10 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas $"{nameof(WasmToolchain)} is supported only on Unix, benchmark '{benchmarkCase.DisplayInfo}' might not work correctly", benchmarkCase); } - else if (IsCliPathInvalid(CustomDotNetCliPath, benchmarkCase, out var invalidCliError)) + + foreach (var validationError in DotNetSdkValidator.ValidateCoreSdks(CustomDotNetCliPath, benchmarkCase)) { - yield return invalidCliError; + yield return validationError; } } diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs index 485e32e16a..2ef6dd8a23 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; +using BenchmarkDotNet.Characteristics; +using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Validators; namespace BenchmarkDotNet.Toolchains.NativeAot { @@ -60,5 +63,18 @@ internal NativeAotToolchain(string displayName, public static NativeAotToolchainBuilder CreateBuilder() => NativeAotToolchainBuilder.Create(); public static string GetExtraArguments(string runtimeIdentifier) => $"-r {runtimeIdentifier}"; + + public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + { + foreach (var error in base.Validate(benchmarkCase, resolver)) + { + yield return error; + } + + foreach (var validationError in DotNetSdkValidator.ValidateCoreSdks(CustomDotNetCliPath, benchmarkCase)) + { + yield return validationError; + } + } } } diff --git a/src/BenchmarkDotNet/Toolchains/Toolchain.cs b/src/BenchmarkDotNet/Toolchains/Toolchain.cs index e680424de7..4ff773a797 100644 --- a/src/BenchmarkDotNet/Toolchains/Toolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/Toolchain.cs @@ -57,31 +57,6 @@ public virtual IEnumerable Validate(BenchmarkCase benchmarkCase } } - internal static bool IsCliPathInvalid(string customDotNetCliPath, BenchmarkCase benchmarkCase, out ValidationError? validationError) - { - validationError = null; - - if (string.IsNullOrEmpty(customDotNetCliPath) && !HostEnvironmentInfo.GetCurrent().IsDotNetCliInstalled()) - { - validationError = new ValidationError(true, - $"BenchmarkDotNet requires dotnet SDK to be installed or path to local dotnet cli provided in explicit way using `--cli` argument, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", - benchmarkCase); - - return true; - } - - if (!string.IsNullOrEmpty(customDotNetCliPath) && !File.Exists(customDotNetCliPath)) - { - validationError = new ValidationError(true, - $"Provided custom dotnet cli path does not exist, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", - benchmarkCase); - - return true; - } - - return false; - } - public override string ToString() => Name; } -} +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/DotNetSdkVersionValidator.cs b/src/BenchmarkDotNet/Validators/DotNetSdkVersionValidator.cs new file mode 100644 index 0000000000..72a3b54730 --- /dev/null +++ b/src/BenchmarkDotNet/Validators/DotNetSdkVersionValidator.cs @@ -0,0 +1,229 @@ +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace BenchmarkDotNet.Validators +{ + internal static class DotNetSdkValidator + { + private static readonly Lazy> cachedFrameworkSdks = new Lazy>(GetInstalledFrameworkSdks, true); + + public static IEnumerable ValidateCoreSdks(string? customDotNetCliPath, BenchmarkCase benchmark) + { + if (IsCliPathInvalid(customDotNetCliPath, benchmark, out ValidationError? cliPathError)) + { + yield return cliPathError; + } + else if (TryGetSdkVersion(benchmark, out string requiredSdkVersion)) + { + var installedSdks = GetInstalledDotNetSdks(customDotNetCliPath); + if (!installedSdks.Any(sdk => sdk.StartsWith(requiredSdkVersion))) + { + yield return new ValidationError(true, $"The required .NET Core SDK version {requiredSdkVersion} for runtime moniker {benchmark.Job.Environment.Runtime.RuntimeMoniker} is not installed.", benchmark); + } + } + } + + public static IEnumerable ValidateFrameworkSdks(BenchmarkCase benchmark) + { + if (!TryGetSdkVersion(benchmark, out string requiredSdkVersionString)) + { + yield break; + } + + if (!Version.TryParse(requiredSdkVersionString, out var requiredSdkVersion)) + { + yield return new ValidationError(true, $"Invalid .NET Framework SDK version format: {requiredSdkVersionString}", benchmark); + yield break; + } + + var installedVersionString = cachedFrameworkSdks.Value.FirstOrDefault(); + + if (installedVersionString == null || Version.TryParse(installedVersionString, out var installedVersion) && installedVersion < requiredSdkVersion) + { + yield return new ValidationError(true, $"The required .NET Framework SDK version {requiredSdkVersionString} or higher is not installed.", benchmark); + } + } + + public static bool IsCliPathInvalid(string customDotNetCliPath, BenchmarkCase benchmarkCase, out ValidationError? validationError) + { + validationError = null; + + if (string.IsNullOrEmpty(customDotNetCliPath) && !HostEnvironmentInfo.GetCurrent().IsDotNetCliInstalled()) + { + validationError = new ValidationError(true, + $"BenchmarkDotNet requires dotnet SDK to be installed or path to local dotnet cli provided in explicit way using `--cli` argument, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", + benchmarkCase); + + return true; + } + + if (!string.IsNullOrEmpty(customDotNetCliPath) && !File.Exists(customDotNetCliPath)) + { + validationError = new ValidationError(true, + $"Provided custom dotnet cli path does not exist, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", + benchmarkCase); + + return true; + } + + return false; + } + + private static bool TryGetSdkVersion(BenchmarkCase benchmark, out string sdkVersion) + { + sdkVersion = string.Empty; + if (benchmark?.Job?.Environment?.Runtime?.RuntimeMoniker != null) + { + sdkVersion = GetSdkVersionFromMoniker(benchmark.Job.Environment.Runtime.RuntimeMoniker); + return true; + } + return false; + } + + private static IEnumerable GetInstalledDotNetSdks(string? customDotNetCliPath) + { + string dotnetExecutable = string.IsNullOrEmpty(customDotNetCliPath) ? "dotnet" : customDotNetCliPath; + var startInfo = new ProcessStartInfo(dotnetExecutable, "--list-sdks") + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + try + { + using (var process = Process.Start(startInfo)) + { + if (process == null) + { + return Enumerable.Empty(); + } + + process.WaitForExit(); + + if (process.ExitCode == 0) + { + var output = process.StandardOutput.ReadToEnd(); + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + return lines.Select(line => line.Split(' ')[0]); // The SDK version is the first part of each line. + } + else + { + return Enumerable.Empty(); + } + } + } + catch (Win32Exception) // dotnet CLI is not installed or not found in the path. + { + return Enumerable.Empty(); + } + } + + public static List GetInstalledFrameworkSdks() + { + var versions = new List(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Get45PlusFromRegistry(versions); + } + + return versions; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "This code is protected with a runtime OS platform check")] + private static void Get45PlusFromRegistry(List versions) + { + const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; + + using (var ndpKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(subkey)) + { + if (ndpKey == null) + { + return; + } + + if (ndpKey.GetValue("Version") != null) + { + versions.Add(ndpKey.GetValue("Version").ToString()); + } + else + { + if (ndpKey.GetValue("Release") != null) + { + versions.Add(CheckFor45PlusVersion((int)ndpKey.GetValue("Release"))); + } + } + } + } + + private static string CheckFor45PlusVersion(int releaseKey) + { + if (releaseKey >= 533320) + return "4.8.1"; + if (releaseKey >= 528040) + return "4.8"; + if (releaseKey >= 461808) + return "4.7.2"; + if (releaseKey >= 461308) + return "4.7.1"; + if (releaseKey >= 460798) + return "4.7"; + if (releaseKey >= 394802) + return "4.6.2"; + if (releaseKey >= 394254) + return "4.6.1"; + if (releaseKey >= 393295) + return "4.6"; + if (releaseKey >= 379893) + return "4.5.2"; + if (releaseKey >= 378675) + return "4.5.1"; + if (releaseKey >= 378389) + return "4.5"; + + return ""; + } + + private static string GetSdkVersionFromMoniker(RuntimeMoniker runtimeMoniker) + { + return runtimeMoniker switch + { + RuntimeMoniker.Net461 => "4.6.1", + RuntimeMoniker.Net462 => "4.6.2", + RuntimeMoniker.Net47 => "4.7", + RuntimeMoniker.Net471 => "4.7.1", + RuntimeMoniker.Net472 => "4.7.2", + RuntimeMoniker.Net48 => "4.8", + RuntimeMoniker.Net481 => "4.8.1", + RuntimeMoniker.NetCoreApp20 => "2.0", + RuntimeMoniker.NetCoreApp21 => "2.1", + RuntimeMoniker.NetCoreApp22 => "2.2", + RuntimeMoniker.NetCoreApp30 => "3.0", + RuntimeMoniker.NetCoreApp31 => "3.1", + RuntimeMoniker.Net50 => "5.0", + RuntimeMoniker.Net60 => "6.0", + RuntimeMoniker.Net70 => "7.0", + RuntimeMoniker.Net80 => "8.0", + RuntimeMoniker.Net90 => "9.0", + RuntimeMoniker.NativeAot60 => "6.0", + RuntimeMoniker.NativeAot70 => "7.0", + RuntimeMoniker.NativeAot80 => "8.0", + RuntimeMoniker.NativeAot90 => "9.0", + RuntimeMoniker.Mono60 => "6.0", + RuntimeMoniker.Mono70 => "7.0", + RuntimeMoniker.Mono80 => "8.0", + RuntimeMoniker.Mono90 => "9.0", + _ => throw new NotImplementedException($"SDK version check not implemented for {runtimeMoniker}") + }; + } + } +} \ No newline at end of file