diff --git a/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs index 21bb2821df..549d0ed252 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs @@ -2,7 +2,6 @@ using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Portability; namespace BenchmarkDotNet.Environments { @@ -16,6 +15,13 @@ public class ClrRuntime : Runtime, IEquatable public static readonly ClrRuntime Net48 = new ClrRuntime(RuntimeMoniker.Net48, "net48", ".NET Framework 4.8"); public static readonly ClrRuntime Net481 = new ClrRuntime(RuntimeMoniker.Net481, "net481", ".NET Framework 4.8.1"); + // Use a Lazy so that the value will be obtained from the first call which happens on the user's thread. + // When this is called again on a background thread from the BuildInParallel step, it will return the cached result. +#if NET6_0_OR_GREATER + [System.Runtime.Versioning.SupportedOSPlatform("windows")] +#endif + private static readonly Lazy Current = new (RetrieveCurrentVersion, true); + public string Version { get; } private ClrRuntime(RuntimeMoniker runtimeMoniker, string msBuildMoniker, string displayName, string? version = null) @@ -50,24 +56,35 @@ internal static ClrRuntime GetCurrentVersion() throw new NotSupportedException(".NET Framework supports Windows OS only."); } + return Current.Value; + } + + +#if NET6_0_OR_GREATER + [System.Runtime.Versioning.SupportedOSPlatform("windows")] +#endif + private static ClrRuntime RetrieveCurrentVersion() + { // this logic is put to a separate method to avoid any assembly loading issues on non Windows systems - string sdkVersion = FrameworkVersionHelper.GetLatestNetDeveloperPackVersion(); - string version = sdkVersion + // Try to determine the Framework version that the executable was compiled for. + string version = FrameworkVersionHelper.GetTargetFrameworkVersion() + // Fallback to the current running Framework version. + ?? FrameworkVersionHelper.GetLatestNetDeveloperPackVersion() ?? FrameworkVersionHelper.GetFrameworkReleaseVersion(); // .NET Developer Pack is not installed - switch (version) + return version switch { - case "4.6.1": return Net461; - case "4.6.2": return Net462; - case "4.7": return Net47; - case "4.7.1": return Net471; - case "4.7.2": return Net472; - case "4.8": return Net48; - case "4.8.1": return Net481; - default: // unlikely to happen but theoretically possible - return new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}"); - } + "4.6.1" => Net461, + "4.6.2" => Net462, + "4.7" => Net47, + "4.7.1" => Net471, + "4.7.2" => Net472, + "4.8" => Net48, + "4.8.1" => Net481, + // unlikely to happen but theoretically possible + _ => new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}"), + }; } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index 88093a6a52..9bd05e4d61 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; using Microsoft.Win32; namespace BenchmarkDotNet.Helpers @@ -10,7 +14,7 @@ internal static class FrameworkVersionHelper // magic numbers come from https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed // should be ordered by release number private static readonly (int minReleaseNumber, string version)[] FrameworkVersions = - { + [ (533320, "4.8.1"), // value taken from Windows 11 arm64 insider build (528040, "4.8"), (461808, "4.7.2"), @@ -18,7 +22,55 @@ private static readonly (int minReleaseNumber, string version)[] FrameworkVersio (460798, "4.7"), (394802, "4.6.2"), (394254, "4.6.1") - }; + ]; + + internal static string? GetTargetFrameworkVersion() + { + // Search assemblies until we find a TargetFrameworkAttribute with a supported Framework version. + // We don't search all assemblies, only the entry assembly and callers. + foreach (var assembly in EnumerateAssemblies()) + { + foreach (var attribute in assembly.GetCustomAttributes()) + { + switch (attribute.FrameworkName) + { + case ".NETFramework,Version=v4.6.1": return "4.6.1"; + case ".NETFramework,Version=v4.6.2": return "4.6.2"; + case ".NETFramework,Version=v4.7": return "4.7"; + case ".NETFramework,Version=v4.7.1": return "4.7.1"; + case ".NETFramework,Version=v4.7.2": return "4.7.2"; + case ".NETFramework,Version=v4.8": return "4.8"; + case ".NETFramework,Version=v4.8.1": return "4.8.1"; + } + } + } + + return null; + + static IEnumerable EnumerateAssemblies() + { + var entryAssembly = Assembly.GetEntryAssembly(); + // Assembly.GetEntryAssembly() returns null in unit test frameworks. + if (entryAssembly != null) + { + yield return entryAssembly; + } + // Search calling assemblies. + var stacktrace = new StackTrace(false); + var searchedAssemblies = new HashSet() + { + stacktrace.GetFrame(0).GetMethod().ReflectedType.Assembly + }; + for (int i = 1; i < stacktrace.FrameCount; i++) + { + var assembly = stacktrace.GetFrame(i).GetMethod().ReflectedType.Assembly; + if (searchedAssemblies.Add(assembly)) + { + yield return assembly; + } + } + } + } internal static string GetFrameworkDescription() { @@ -57,30 +109,28 @@ internal static string MapToReleaseVersion(string servicingVersion) #if NET6_0_OR_GREATER - [System.Runtime.Versioning.SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows")] #endif private static int? GetReleaseNumberFromWindowsRegistry() { - using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32)) - using (var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\")) - { - if (ndpKey == null) - return null; - return Convert.ToInt32(ndpKey.GetValue("Release")); - } + using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + using var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"); + if (ndpKey == null) + return null; + return Convert.ToInt32(ndpKey.GetValue("Release")); } #if NET6_0_OR_GREATER - [System.Runtime.Versioning.SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows")] #endif - internal static string GetLatestNetDeveloperPackVersion() + internal static string? GetLatestNetDeveloperPackVersion() { - if (!(GetReleaseNumberFromWindowsRegistry() is int releaseNumber)) + if (GetReleaseNumberFromWindowsRegistry() is not int releaseNumber) return null; return FrameworkVersions - .FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version)) - .version; + .FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version)) + .version; } // Reference Assemblies exists when Developer Pack is installed diff --git a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs index a73a47e7c2..c858eea489 100644 --- a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs @@ -3,7 +3,6 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Portability; using Xunit; namespace BenchmarkDotNet.IntegrationTests.ManualRunning @@ -19,20 +18,6 @@ public class MultipleFrameworksTest : BenchmarkTestExecutor [InlineData(RuntimeMoniker.Net80)] public void EachFrameworkIsRebuilt(RuntimeMoniker runtime) { -#if NET462 - // We cannot detect what target framework version the host was compiled for on full Framework, - // which causes the RoslynToolchain to be used instead of CsProjClassicNetToolchain when the host is full Framework - // (because full Framework always uses the version that's installed on the machine, unlike Core), - // which means if the machine has net48 installed (not net481), the net461 host with net48 runtime moniker - // will not be recompiled, causing the test to fail. - - // If we ever change the default toolchain to CsProjClassicNetToolchain instead of RoslynToolchain, we can remove this check. - if (runtime == RuntimeMoniker.Net48) - { - // XUnit doesn't provide Assert.Skip API yet. - return; - } -#endif var config = ManualConfig.CreateEmpty().AddJob(Job.Dry.WithRuntime(runtime.GetRuntime()).WithEnvironmentVariable(TfmEnvVarName, runtime.ToString())); CanExecute(config); } @@ -57,7 +42,7 @@ public void ThrowWhenWrong() { if (Environment.GetEnvironmentVariable(TfmEnvVarName) != moniker.ToString()) { - throw new InvalidOperationException($"Has not been recompiled, the value was {Environment.GetEnvironmentVariable(TfmEnvVarName)}"); + throw new InvalidOperationException($"Has not been recompiled, the value was {moniker}, expected {Environment.GetEnvironmentVariable(TfmEnvVarName)}"); } } }