Skip to content

Improve .Net Framework version check #2682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;

namespace BenchmarkDotNet.Environments
{
Expand All @@ -16,6 +15,13 @@ public class ClrRuntime : Runtime, IEquatable<ClrRuntime>
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<ClrRuntime> Current = new (RetrieveCurrentVersion, true);

public string Version { get; }

private ClrRuntime(RuntimeMoniker runtimeMoniker, string msBuildMoniker, string displayName, string? version = null)
Expand Down Expand Up @@ -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}"),
};
}
}
}
80 changes: 65 additions & 15 deletions src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,15 +14,63 @@ 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"),
(461308, "4.7.1"),
(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<TargetFrameworkAttribute>())
{
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<Assembly> 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<Assembly>()
{
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()
{
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,6 @@ public class MultipleFrameworksTest : BenchmarkTestExecutor
[InlineData(RuntimeMoniker.Net80)]
public void EachFrameworkIsRebuilt(RuntimeMoniker runtime)
{
#if NET461
// 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<ValuePerTfm>(config);
}
Expand Down