Skip to content

Commit

Permalink
Retrieve current .Net Framework version from TargetFrameworkAttribute.
Browse files Browse the repository at this point in the history
  • Loading branch information
timcassell committed Dec 22, 2024
1 parent cd50f7b commit 22e7b54
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 45 deletions.
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 @@ -3,7 +3,6 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;
using Xunit;

namespace BenchmarkDotNet.IntegrationTests.ManualRunning
Expand All @@ -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<ValuePerTfm>(config);
}
Expand All @@ -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)}");
}
}
}
Expand Down

0 comments on commit 22e7b54

Please sign in to comment.