Skip to content

Commit 771e7d0

Browse files
committed
Enhanced assembly resolution for windows and aspnetcore
1 parent 924d5b1 commit 771e7d0

File tree

4 files changed

+160
-140
lines changed

4 files changed

+160
-140
lines changed

src/NUnitEngine/nunit.engine.core/DotNetHelper.cs

Lines changed: 85 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,113 @@
22

33
using Microsoft.Win32;
44
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Drawing.Drawing2D;
58
using System.IO;
9+
using System.Linq;
610
using System.Runtime.InteropServices;
711

812
namespace NUnit.Engine
913
{
1014
public static class DotNet
1115
{
12-
public static string GetInstallDirectory() => Environment.Is64BitProcess
13-
? GetX64InstallDirectory() : GetX86InstallDirectory();
16+
private const string X64_SUBKEY1 = @"SOFTWARE\dotnet\SetUp\InstalledVersions\x64\sharedHost\";
17+
private const string X64_SUBKEY2 = @"SOFTWARE\WOW6432Node\dotnet\SetUp\InstalledVersions\x64\";
18+
private const string X86_SUBKEY1 = @"SOFTWARE\dotnet\SetUp\InstalledVersions\x86\InstallLocation\";
19+
private const string X86_SUBKEY2 = @"SOFTWARE\WOW6432Node\dotnet\SetUp\InstalledVersions\x86\";
1420

15-
public static string GetInstallDirectory(bool x86) => x86
16-
? GetX86InstallDirectory() : GetX64InstallDirectory();
21+
public static readonly string X64InstallDirectory;
22+
public static readonly string X86InstallDirectory;
23+
public static readonly List<RuntimeInfo> Runtimes;
1724

18-
private static string _x64InstallDirectory;
19-
public static string GetX64InstallDirectory()
25+
public class RuntimeInfo
2026
{
21-
if (_x64InstallDirectory == null)
22-
_x64InstallDirectory = Environment.GetEnvironmentVariable("DOTNET_ROOT");
27+
public string Name;
28+
public Version Version;
29+
public string Path;
30+
31+
public RuntimeInfo(string name, string version, string path)
32+
: this(name, new Version(version), path) { }
2333

24-
if (_x64InstallDirectory == null)
34+
public RuntimeInfo(string name, Version version, string path)
2535
{
26-
#if NETFRAMEWORK
27-
if (Path.DirectorySeparatorChar == '\\')
28-
#else
29-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
30-
#endif
31-
{
32-
RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\dotnet\SetUp\InstalledVersions\x64\sharedHost\");
33-
_x64InstallDirectory = (string)key?.GetValue("Path");
34-
}
35-
else
36-
_x64InstallDirectory = "/usr/shared/dotnet/";
36+
Name = name;
37+
Version = version;
38+
Path = path;
3739
}
38-
39-
return _x64InstallDirectory;
4040
}
4141

42-
private static string _x86InstallDirectory;
43-
public static string GetX86InstallDirectory()
42+
/// <summary>
43+
/// Static constructor initializes everything once
44+
/// </summary>
45+
static DotNet()
4446
{
45-
if (_x86InstallDirectory == null)
46-
_x86InstallDirectory = Environment.GetEnvironmentVariable("DOTNET_ROOT_X86");
47+
#pragma warning disable CA1416
48+
X64InstallDirectory =
49+
Environment.GetEnvironmentVariable("DOTNET_ROOT") ?? (
50+
IsWindows
51+
? (string)Registry.LocalMachine.OpenSubKey(X64_SUBKEY1)?.GetValue("Path") ??
52+
(string)Registry.LocalMachine.OpenSubKey(X64_SUBKEY2)?.GetValue("Path") ?? @"C:\Program Files\dotnet"
53+
: "/usr/shared/dotnet/");
54+
X86InstallDirectory =
55+
Environment.GetEnvironmentVariable("DOTNET_ROOT_X86") ?? (
56+
IsWindows
57+
? (string)Registry.LocalMachine.OpenSubKey(X86_SUBKEY1)?.GetValue("InstallLocation") ??
58+
(string)Registry.LocalMachine.OpenSubKey(X86_SUBKEY2)?.GetValue("InstallLocation") ?? @"C:\Program Files (x86)\dotnet"
59+
: "/usr/shared/dotnet/");
60+
#pragma warning restore CA1416
61+
Runtimes = new List<RuntimeInfo>();
62+
foreach (string line in DotnetCommand("--list-runtimes"))
63+
{
64+
string[] parts = line.Trim().Split([' '], 3);
65+
Runtimes.Add(new RuntimeInfo(parts[0], parts[1], parts[2].Trim(['[', ']'])));
66+
}
67+
}
68+
69+
/// <summary>
70+
/// Get the correct install directory, depending on whether we need X86 or X64 architecture.
71+
/// </summary>
72+
/// <param name="x86">Flag indicating whether the X86 architecture is needed</param>
73+
/// <returns></returns>
74+
public static string GetInstallDirectory(bool x86) => x86
75+
? X86InstallDirectory : X64InstallDirectory;
76+
77+
public static IEnumerable<RuntimeInfo> GetRuntimes(string name) => Runtimes.Where(r => r.Name == name);
4778

48-
if (_x86InstallDirectory == null)
79+
private static IEnumerable<string> DotnetCommand(string arguments)
80+
{
81+
var process = new Process
4982
{
50-
#if NETFRAMEWORK
51-
if (Path.DirectorySeparatorChar == '\\')
52-
#else
53-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
54-
#endif
83+
StartInfo = new ProcessStartInfo
5584
{
56-
RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\dotnet\SetUp\InstalledVersions\x86\");
57-
_x86InstallDirectory = (string)key?.GetValue("InstallLocation");
85+
FileName = "dotnet",
86+
Arguments = arguments,
87+
UseShellExecute = false,
88+
RedirectStandardOutput = true,
89+
CreateNoWindow = true
5890
}
59-
else
60-
_x86InstallDirectory = "/usr/shared/dotnet/";
91+
};
92+
93+
try
94+
{
95+
process.Start();
96+
}
97+
catch (Exception)
98+
{
99+
// Failed to start dotnet command. Assume no versions are installed and just return
100+
yield break;
61101
}
62102

63-
return _x86InstallDirectory;
103+
while (!process.StandardOutput.EndOfStream)
104+
yield return process.StandardOutput.ReadLine();
64105
}
106+
107+
#if NETFRAMEWORK
108+
private static bool IsWindows => Path.DirectorySeparatorChar == '\\';
109+
#else
110+
private static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
111+
#endif
112+
65113
}
66114
}

src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Win32;
88
using System;
99
using System.Collections.Generic;
10+
using System.Diagnostics;
1011
using System.IO;
1112
using System.Linq;
1213
using System.Reflection;
@@ -22,59 +23,57 @@ internal sealed class TestAssemblyResolver : IDisposable
2223

2324
private readonly AssemblyLoadContext _loadContext;
2425

25-
private static readonly string INSTALL_DIR;
26-
private static readonly string WINDOWS_DESKTOP_DIR;
27-
private static readonly string ASP_NET_CORE_DIR;
26+
private readonly IEnumerable<DotNet.RuntimeInfo> _windowsDesktopRuntimes;
27+
private readonly IEnumerable<DotNet.RuntimeInfo> _aspNetCoreRuntimes;
2828

2929
// Our Strategies for resolving references
3030
List<ResolutionStrategy> ResolutionStrategies;
3131

32-
static TestAssemblyResolver()
33-
{
34-
INSTALL_DIR = DotNet.GetInstallDirectory();
35-
WINDOWS_DESKTOP_DIR = Path.Combine(INSTALL_DIR, "shared", "Microsoft.WindowsDesktop.App");
36-
ASP_NET_CORE_DIR = Path.Combine(INSTALL_DIR, "shared", "Microsoft.AspNetCore.App");
37-
}
38-
3932
public TestAssemblyResolver(AssemblyLoadContext loadContext, string testAssemblyPath)
4033
{
4134
_loadContext = loadContext;
4235

36+
_windowsDesktopRuntimes = DotNet.GetRuntimes("Microsoft.WindowsDesktop.App");
37+
_aspNetCoreRuntimes = DotNet.GetRuntimes("Microsoft.AspNetCore.App");
38+
4339
InitializeResolutionStrategies(loadContext, testAssemblyPath);
4440

4541
_loadContext.Resolving += OnResolving;
4642
}
4743

4844
private void InitializeResolutionStrategies(AssemblyLoadContext loadContext, string testAssemblyPath)
4945
{
50-
// First, looking only at direct references by the test assembly, try to determine if
51-
// this assembly is using WindowsDesktop (either SWF or WPF) and/or AspNetCore.
46+
// Decide whether to try WindowsDeskTop and/or AspNetCore runtimes before any others.
47+
// We base this on direct references only, so we will eventually try each of them
48+
// later in case there are any indirect references.
5249
AssemblyDefinition assemblyDef = AssemblyDefinition.ReadAssembly(testAssemblyPath);
53-
bool isWindowsDesktop = false;
54-
bool isAspNetCore = false;
50+
bool tryWindowsDesktopFirst = false;
51+
bool tryAspNetCoreFirst = false;
5552
foreach (var reference in assemblyDef.MainModule.GetTypeReferences())
5653
{
5754
string fn = reference.FullName;
5855
if (fn.StartsWith("System.Windows.") || fn.StartsWith("PresentationFramework"))
59-
isWindowsDesktop = true;
56+
tryWindowsDesktopFirst = true;
6057
if (fn.StartsWith("Microsoft.AspNetCore."))
61-
isAspNetCore = true;
58+
tryAspNetCoreFirst = true;
6259
}
6360

6461
// Initialize the list of ResolutionStrategies in the best order depending on
6562
// what we learned.
6663
ResolutionStrategies = new List<ResolutionStrategy>();
6764

68-
if (isWindowsDesktop && Directory.Exists(WINDOWS_DESKTOP_DIR))
69-
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(WINDOWS_DESKTOP_DIR));
70-
if (isAspNetCore && Directory.Exists(ASP_NET_CORE_DIR))
71-
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(ASP_NET_CORE_DIR));
65+
if (tryWindowsDesktopFirst)
66+
ResolutionStrategies.Add(new WindowsDesktopStrategy());
67+
if (tryAspNetCoreFirst)
68+
ResolutionStrategies.Add(new AspNetCoreStrategy());
69+
7270
ResolutionStrategies.Add(new TrustedPlatformAssembliesStrategy());
7371
ResolutionStrategies.Add(new RuntimeLibrariesStrategy(loadContext, testAssemblyPath));
74-
if (!isWindowsDesktop && Directory.Exists(WINDOWS_DESKTOP_DIR))
75-
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(WINDOWS_DESKTOP_DIR));
76-
if (!isAspNetCore && Directory.Exists(ASP_NET_CORE_DIR))
77-
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(ASP_NET_CORE_DIR));
72+
73+
if (!tryWindowsDesktopFirst)
74+
ResolutionStrategies.Add(new WindowsDesktopStrategy());
75+
if (!tryAspNetCoreFirst)
76+
ResolutionStrategies.Add(new AspNetCoreStrategy());
7877
}
7978

8079
public void Dispose()
@@ -201,49 +200,60 @@ public override bool TryToResolve(
201200
}
202201
}
203202

204-
public class AdditionalDirectoryStrategy : ResolutionStrategy
203+
public class AdditionalRuntimesStrategy : ResolutionStrategy
205204
{
206-
private string _frameworkDirectory;
205+
private IEnumerable<DotNet.RuntimeInfo> _additionalRuntimes;
207206

208-
public AdditionalDirectoryStrategy(string frameworkDirectory)
207+
public AdditionalRuntimesStrategy(string runtimeName)
209208
{
210-
_frameworkDirectory = frameworkDirectory;
209+
_additionalRuntimes = DotNet.GetRuntimes(runtimeName);
211210
}
212211

213-
public override bool TryToResolve(
214-
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
212+
public override bool TryToResolve(AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
215213
{
216214
loadedAssembly = null;
217-
if (assemblyName.Version == null)
215+
216+
DotNet.RuntimeInfo runtime;
217+
if (!FindBestRuntime(assemblyName, out runtime))
218218
return false;
219219

220-
var versionDir = FindBestVersionDir(_frameworkDirectory, assemblyName.Version);
220+
string candidate = Path.Combine(runtime.Path, runtime.Version.ToString(), assemblyName.Name + ".dll");
221+
if (!File.Exists(candidate))
222+
return false;
221223

222-
if (versionDir != null)
223-
{
224-
string candidate = Path.Combine(_frameworkDirectory, versionDir, assemblyName.Name + ".dll");
225-
if (File.Exists(candidate))
226-
{
227-
loadedAssembly = loadContext.LoadFromAssemblyPath(candidate);
228-
log.Info("'{0}' ({1}) assembly is loaded from AdditionalFrameworkDirectory {2} dependencies with best candidate version {3}",
229-
assemblyName,
230-
loadedAssembly.Location,
231-
_frameworkDirectory,
232-
versionDir);
224+
loadedAssembly = loadContext.LoadFromAssemblyPath(candidate);
225+
return true;
226+
}
233227

234-
return true;
235-
}
236-
else
237-
{
238-
log.Debug("Best version dir for {0} is {1}, but there is no {2} file", _frameworkDirectory, versionDir, candidate);
239-
return false;
240-
}
228+
private bool FindBestRuntime(AssemblyName assemblyName, out DotNet.RuntimeInfo bestRuntime)
229+
{
230+
bestRuntime = null;
231+
var targetVersion = assemblyName.Version;
232+
233+
if (targetVersion is null)
234+
return false;
235+
236+
foreach (var candidate in _additionalRuntimes)
237+
{
238+
if (candidate.Version >= targetVersion)
239+
if (bestRuntime is null || bestRuntime.Version > candidate.Version)
240+
bestRuntime = candidate;
241241
}
242242

243-
return false;
243+
return bestRuntime is not null;
244244
}
245245
}
246246

247+
public class WindowsDesktopStrategy : AdditionalRuntimesStrategy
248+
{
249+
public WindowsDesktopStrategy() : base("Microsoft.WindowsDesktop.App") { }
250+
}
251+
252+
public class AspNetCoreStrategy : AdditionalRuntimesStrategy
253+
{
254+
public AspNetCoreStrategy() : base("Microsoft.AspNetCore.App") { }
255+
}
256+
247257
#endregion
248258

249259
#region HelperMethods

src/NUnitEngine/nunit.engine/Services/AgentProcess.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public AgentProcess(TestAgency agency, TestPackage package, Guid agentId)
7373
if (Path.DirectorySeparatorChar != '\\')
7474
throw new Exception("Running .NET Core as X86 is currently only supported on Windows");
7575

76-
var x86_dotnet_exe = Path.Combine(DotNet.GetX86InstallDirectory(), "dotnet.exe");
76+
var x86_dotnet_exe = Path.Combine(DotNet.X86InstallDirectory, "dotnet.exe");
7777
if (!File.Exists(x86_dotnet_exe))
7878
throw new Exception("The X86 version of dotnet.exe is not installed");
7979

0 commit comments

Comments
 (0)