Skip to content

Commit b8af587

Browse files
authored
Improve .Net Framework version detection (#2682)
* Retrieve current .Net Framework version from TargetFrameworkAttribute. * Get TargetFrameworkAttribute from the benchmark assembly. * Fix MultipleFrameworksTest * Use RoslynToolchain if the runtime was not explicitly set.
1 parent 8fed488 commit b8af587

File tree

7 files changed

+100
-77
lines changed

7 files changed

+100
-77
lines changed

src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System;
2+
using System.Reflection;
23
using BenchmarkDotNet.Detectors;
34
using BenchmarkDotNet.Helpers;
45
using BenchmarkDotNet.Jobs;
5-
using BenchmarkDotNet.Portability;
66

77
namespace BenchmarkDotNet.Environments
88
{
@@ -47,27 +47,41 @@ internal static ClrRuntime GetCurrentVersion()
4747
{
4848
if (!OsDetector.IsWindows())
4949
{
50-
throw new NotSupportedException(".NET Framework supports Windows OS only.");
50+
throw new PlatformNotSupportedException(".NET Framework supports Windows OS only.");
5151
}
5252

53-
// this logic is put to a separate method to avoid any assembly loading issues on non Windows systems
54-
string sdkVersion = FrameworkVersionHelper.GetLatestNetDeveloperPackVersion();
55-
56-
string version = sdkVersion
53+
string version = FrameworkVersionHelper.GetLatestNetDeveloperPackVersion()
5754
?? FrameworkVersionHelper.GetFrameworkReleaseVersion(); // .NET Developer Pack is not installed
55+
return GetRuntimeFromVersion(version);
56+
}
5857

59-
switch (version)
58+
internal static ClrRuntime GetTargetOrCurrentVersion(Assembly? assembly)
59+
{
60+
if (!OsDetector.IsWindows())
6061
{
61-
case "4.6.1": return Net461;
62-
case "4.6.2": return Net462;
63-
case "4.7": return Net47;
64-
case "4.7.1": return Net471;
65-
case "4.7.2": return Net472;
66-
case "4.8": return Net48;
67-
case "4.8.1": return Net481;
68-
default: // unlikely to happen but theoretically possible
69-
return new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}");
62+
throw new PlatformNotSupportedException(".NET Framework supports Windows OS only.");
7063
}
64+
65+
// Try to determine the Framework version that the assembly was compiled for.
66+
string? version = FrameworkVersionHelper.GetTargetFrameworkVersion(assembly);
67+
return version != null
68+
? GetRuntimeFromVersion(version)
69+
// Fallback to the current running Framework version.
70+
: GetCurrentVersion();
7171
}
72+
73+
private static ClrRuntime GetRuntimeFromVersion(string version)
74+
=> version switch
75+
{
76+
"4.6.1" => Net461,
77+
"4.6.2" => Net462,
78+
"4.7" => Net47,
79+
"4.7.1" => Net471,
80+
"4.7.2" => Net472,
81+
"4.8" => Net48,
82+
"4.8.1" => Net481,
83+
// unlikely to happen but theoretically possible
84+
_ => new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}"),
85+
};
7286
}
7387
}

src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.IO;
33
using System.Linq;
4+
using System.Reflection;
5+
using System.Runtime.Versioning;
46
using Microsoft.Win32;
57

68
namespace BenchmarkDotNet.Helpers
@@ -10,15 +12,41 @@ internal static class FrameworkVersionHelper
1012
// magic numbers come from https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
1113
// should be ordered by release number
1214
private static readonly (int minReleaseNumber, string version)[] FrameworkVersions =
13-
{
15+
[
1416
(533320, "4.8.1"), // value taken from Windows 11 arm64 insider build
1517
(528040, "4.8"),
1618
(461808, "4.7.2"),
1719
(461308, "4.7.1"),
1820
(460798, "4.7"),
1921
(394802, "4.6.2"),
2022
(394254, "4.6.1")
21-
};
23+
];
24+
25+
internal static string? GetTargetFrameworkVersion(Assembly? assembly)
26+
{
27+
if (assembly is null)
28+
{
29+
return null;
30+
}
31+
32+
// Look for a TargetFrameworkAttribute with a supported Framework version.
33+
foreach (var attribute in assembly.GetCustomAttributes<TargetFrameworkAttribute>())
34+
{
35+
switch (attribute.FrameworkName)
36+
{
37+
case ".NETFramework,Version=v4.6.1": return "4.6.1";
38+
case ".NETFramework,Version=v4.6.2": return "4.6.2";
39+
case ".NETFramework,Version=v4.7": return "4.7";
40+
case ".NETFramework,Version=v4.7.1": return "4.7.1";
41+
case ".NETFramework,Version=v4.7.2": return "4.7.2";
42+
case ".NETFramework,Version=v4.8": return "4.8";
43+
case ".NETFramework,Version=v4.8.1": return "4.8.1";
44+
}
45+
}
46+
47+
// TargetFrameworkAttribute not found, or the assembly targeted a version older than we support.
48+
return null;
49+
}
2250

2351
internal static string GetFrameworkDescription()
2452
{
@@ -57,30 +85,28 @@ internal static string MapToReleaseVersion(string servicingVersion)
5785

5886

5987
#if NET6_0_OR_GREATER
60-
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
88+
[SupportedOSPlatform("windows")]
6189
#endif
6290
private static int? GetReleaseNumberFromWindowsRegistry()
6391
{
64-
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
65-
using (var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"))
66-
{
67-
if (ndpKey == null)
68-
return null;
69-
return Convert.ToInt32(ndpKey.GetValue("Release"));
70-
}
92+
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
93+
using var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\");
94+
if (ndpKey == null)
95+
return null;
96+
return Convert.ToInt32(ndpKey.GetValue("Release"));
7197
}
7298

7399
#if NET6_0_OR_GREATER
74-
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
100+
[SupportedOSPlatform("windows")]
75101
#endif
76-
internal static string GetLatestNetDeveloperPackVersion()
102+
internal static string? GetLatestNetDeveloperPackVersion()
77103
{
78-
if (!(GetReleaseNumberFromWindowsRegistry() is int releaseNumber))
104+
if (GetReleaseNumberFromWindowsRegistry() is not int releaseNumber)
79105
return null;
80106

81107
return FrameworkVersions
82-
.FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version))
83-
.version;
108+
.FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version))
109+
.version;
84110
}
85111

86112
// Reference Assemblies exists when Developer Pack is installed

src/BenchmarkDotNet/Portability/RuntimeInformation.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ string GetDetailedVersion()
182182
}
183183
}
184184

185+
internal static Runtime GetTargetOrCurrentRuntime(Assembly? assembly)
186+
=> !IsMono && !IsWasm && IsFullFramework // Match order of checks in GetCurrentRuntime().
187+
? ClrRuntime.GetTargetOrCurrentVersion(assembly)
188+
: GetCurrentRuntime();
189+
185190
internal static Runtime GetCurrentRuntime()
186191
{
187192
//do not change the order of conditions because it may cause incorrect determination of runtime

src/BenchmarkDotNet/Running/BenchmarkCase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal BenchmarkCase(Descriptor descriptor, Job job, ParameterInstances parame
3030

3131
public Runtime GetRuntime() => Job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
3232
? Job.Environment.Runtime
33-
: RuntimeInformation.GetCurrentRuntime();
33+
: RuntimeInformation.GetTargetOrCurrentRuntime(Descriptor.WorkloadMethod.DeclaringType.Assembly);
3434

3535
public void Dispose() => Parameters.Dispose();
3636

src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using BenchmarkDotNet.Characteristics;
5-
using BenchmarkDotNet.Environments;
65
using BenchmarkDotNet.Jobs;
7-
using BenchmarkDotNet.Portability;
86
using BenchmarkDotNet.Toolchains;
97

108
namespace BenchmarkDotNet.Running
@@ -22,18 +20,16 @@ internal class BenchmarkRuntimePropertiesComparer : IEqualityComparer<BenchmarkC
2220
{
2321
internal static readonly IEqualityComparer<BenchmarkCase> Instance = new BenchmarkRuntimePropertiesComparer();
2422

25-
private static readonly Runtime Current = RuntimeInformation.GetCurrentRuntime();
26-
2723
public bool Equals(BenchmarkCase x, BenchmarkCase y)
2824
{
29-
if (x == null && y == null)
25+
if (x == y)
3026
return true;
3127
if (x == null || y == null)
3228
return false;
3329
var jobX = x.Job;
3430
var jobY = y.Job;
3531

36-
if (AreDifferent(GetRuntime(jobX), GetRuntime(jobY))) // Mono vs .NET vs Core
32+
if (AreDifferent(x.GetRuntime(), y.GetRuntime())) // Mono vs .NET vs Core
3733
return false;
3834
if (AreDifferent(x.GetToolchain(), y.GetToolchain())) // Mono vs .NET vs Core vs InProcess
3935
return false;
@@ -90,20 +86,8 @@ public int GetHashCode(BenchmarkCase obj)
9086
return hashCode.ToHashCode();
9187
}
9288

93-
private static Runtime GetRuntime(Job job)
94-
=> job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
95-
? job.Environment.Runtime
96-
: Current;
97-
9889
private static bool AreDifferent(object x, object y)
99-
{
100-
if (x == null && y == null)
101-
return false;
102-
if (x == null || y == null)
103-
return true;
104-
105-
return !x.Equals(y);
106-
}
90+
=> !Equals(x, y);
10791

10892
private static bool AreDifferent(IReadOnlyList<Argument> x, IReadOnlyList<Argument> y)
10993
{

src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using BenchmarkDotNet.Detectors;
33
using BenchmarkDotNet.Environments;
44
using BenchmarkDotNet.Extensions;
5-
using BenchmarkDotNet.Helpers;
65
using BenchmarkDotNet.Jobs;
76
using BenchmarkDotNet.Portability;
87
using BenchmarkDotNet.Running;
@@ -19,28 +18,37 @@ namespace BenchmarkDotNet.Toolchains
1918
{
2019
internal static class ToolchainExtensions
2120
{
22-
internal static IToolchain GetToolchain(this BenchmarkCase benchmarkCase) => GetToolchain(benchmarkCase.Job, benchmarkCase.Descriptor);
23-
24-
internal static IToolchain GetToolchain(this Job job) => GetToolchain(job, null);
21+
internal static IToolchain GetToolchain(this BenchmarkCase benchmarkCase)
22+
=> benchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain)
23+
? toolchain
24+
: GetToolchain(
25+
benchmarkCase.GetRuntime(),
26+
benchmarkCase.Descriptor,
27+
benchmarkCase.Job.HasDynamicBuildCharacteristic(),
28+
benchmarkCase.Job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
29+
);
2530

26-
private static IToolchain GetToolchain(Job job, Descriptor descriptor)
31+
internal static IToolchain GetToolchain(this Job job)
2732
=> job.Infrastructure.TryGetToolchain(out var toolchain)
2833
? toolchain
2934
: GetToolchain(
3035
job.ResolveValue(EnvironmentMode.RuntimeCharacteristic, EnvironmentResolver.Instance),
31-
descriptor,
32-
job.HasDynamicBuildCharacteristic());
36+
null,
37+
job.HasDynamicBuildCharacteristic(),
38+
job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
39+
);
3340

34-
internal static IToolchain GetToolchain(this Runtime runtime, Descriptor? descriptor = null, bool preferMsBuildToolchains = false)
41+
internal static IToolchain GetToolchain(this Runtime runtime, Descriptor? descriptor = null, bool preferMsBuildToolchains = false, bool isRuntimeExplicit = false)
3542
{
3643
switch (runtime)
3744
{
3845
case ClrRuntime clrRuntime:
39-
if (!preferMsBuildToolchains && RuntimeInformation.IsFullFramework
40-
&& RuntimeInformation.GetCurrentRuntime().MsBuildMoniker == runtime.MsBuildMoniker)
41-
{
46+
bool UseRoslyn()
47+
=> !isRuntimeExplicit
48+
|| runtime.MsBuildMoniker == ClrRuntime.GetTargetOrCurrentVersion(descriptor?.WorkloadMethod.DeclaringType.Assembly).MsBuildMoniker;
49+
50+
if (!preferMsBuildToolchains && RuntimeInformation.IsFullFramework && UseRoslyn())
4251
return RoslynToolchain.Instance;
43-
}
4452

4553
return clrRuntime.RuntimeMoniker != RuntimeMoniker.NotRecognized
4654
? GetToolchain(clrRuntime.RuntimeMoniker)

tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,6 @@ public class MultipleFrameworksTest : BenchmarkTestExecutor
1818
[InlineData(RuntimeMoniker.Net80)]
1919
public void EachFrameworkIsRebuilt(RuntimeMoniker runtime)
2020
{
21-
#if NET461
22-
// We cannot detect what target framework version the host was compiled for on full Framework,
23-
// which causes the RoslynToolchain to be used instead of CsProjClassicNetToolchain when the host is full Framework
24-
// (because full Framework always uses the version that's installed on the machine, unlike Core),
25-
// which means if the machine has net48 installed (not net481), the net461 host with net48 runtime moniker
26-
// will not be recompiled, causing the test to fail.
27-
28-
// If we ever change the default toolchain to CsProjClassicNetToolchain instead of RoslynToolchain, we can remove this check.
29-
if (runtime == RuntimeMoniker.Net48)
30-
{
31-
// XUnit doesn't provide Assert.Skip API yet.
32-
return;
33-
}
34-
#endif
3521
var config = ManualConfig.CreateEmpty().AddJob(Job.Dry.WithRuntime(runtime.GetRuntime()).WithEnvironmentVariable(TfmEnvVarName, runtime.ToString()));
3622
CanExecute<ValuePerTfm>(config);
3723
}

0 commit comments

Comments
 (0)