Skip to content

Commit 185c9cc

Browse files
committed
Enhanced assembly resolution for windows and aspnetcore
1 parent 924d5b1 commit 185c9cc

File tree

4 files changed

+139
-224
lines changed

4 files changed

+139
-224
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: 49 additions & 94 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,20 +23,9 @@ 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;
28-
2926
// Our Strategies for resolving references
3027
List<ResolutionStrategy> ResolutionStrategies;
3128

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-
3929
public TestAssemblyResolver(AssemblyLoadContext loadContext, string testAssemblyPath)
4030
{
4131
_loadContext = loadContext;
@@ -47,34 +37,37 @@ public TestAssemblyResolver(AssemblyLoadContext loadContext, string testAssembly
4737

4838
private void InitializeResolutionStrategies(AssemblyLoadContext loadContext, string testAssemblyPath)
4939
{
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.
40+
// Decide whether to try WindowsDeskTop and/or AspNetCore runtimes before any others.
41+
// We base this on direct references only, so we will eventually try each of them
42+
// later in case there are any indirect references.
5243
AssemblyDefinition assemblyDef = AssemblyDefinition.ReadAssembly(testAssemblyPath);
53-
bool isWindowsDesktop = false;
54-
bool isAspNetCore = false;
44+
bool tryWindowsDesktopFirst = false;
45+
bool tryAspNetCoreFirst = false;
5546
foreach (var reference in assemblyDef.MainModule.GetTypeReferences())
5647
{
5748
string fn = reference.FullName;
5849
if (fn.StartsWith("System.Windows.") || fn.StartsWith("PresentationFramework"))
59-
isWindowsDesktop = true;
50+
tryWindowsDesktopFirst = true;
6051
if (fn.StartsWith("Microsoft.AspNetCore."))
61-
isAspNetCore = true;
52+
tryAspNetCoreFirst = true;
6253
}
6354

6455
// Initialize the list of ResolutionStrategies in the best order depending on
6556
// what we learned.
6657
ResolutionStrategies = new List<ResolutionStrategy>();
6758

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));
59+
if (tryWindowsDesktopFirst)
60+
ResolutionStrategies.Add(new WindowsDesktopStrategy());
61+
if (tryAspNetCoreFirst)
62+
ResolutionStrategies.Add(new AspNetCoreStrategy());
63+
7264
ResolutionStrategies.Add(new TrustedPlatformAssembliesStrategy());
7365
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));
66+
67+
if (!tryWindowsDesktopFirst)
68+
ResolutionStrategies.Add(new WindowsDesktopStrategy());
69+
if (!tryAspNetCoreFirst)
70+
ResolutionStrategies.Add(new AspNetCoreStrategy());
7871
}
7972

8073
public void Dispose()
@@ -201,96 +194,58 @@ public override bool TryToResolve(
201194
}
202195
}
203196

204-
public class AdditionalDirectoryStrategy : ResolutionStrategy
197+
public class AdditionalRuntimesStrategy : ResolutionStrategy
205198
{
206-
private string _frameworkDirectory;
199+
private IEnumerable<DotNet.RuntimeInfo> _additionalRuntimes;
207200

208-
public AdditionalDirectoryStrategy(string frameworkDirectory)
201+
public AdditionalRuntimesStrategy(string runtimeName)
209202
{
210-
_frameworkDirectory = frameworkDirectory;
203+
_additionalRuntimes = DotNet.GetRuntimes(runtimeName);
211204
}
212205

213-
public override bool TryToResolve(
214-
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
206+
public override bool TryToResolve(AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
215207
{
216208
loadedAssembly = null;
217-
if (assemblyName.Version == null)
218-
return false;
219-
220-
var versionDir = FindBestVersionDir(_frameworkDirectory, assemblyName.Version);
221209

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);
210+
DotNet.RuntimeInfo runtime;
211+
if (!FindBestRuntime(assemblyName, out runtime))
212+
return false;
233213

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-
}
241-
}
214+
string candidate = Path.Combine(runtime.Path, runtime.Version.ToString(), assemblyName.Name + ".dll");
215+
if (!File.Exists(candidate))
216+
return false;
242217

243-
return false;
218+
loadedAssembly = loadContext.LoadFromAssemblyPath(candidate);
219+
return true;
244220
}
245-
}
246221

247-
#endregion
222+
private bool FindBestRuntime(AssemblyName assemblyName, out DotNet.RuntimeInfo bestRuntime)
223+
{
224+
bestRuntime = null;
225+
var targetVersion = assemblyName.Version;
248226

249-
#region HelperMethods
227+
if (targetVersion is null)
228+
return false;
250229

251-
private static string FindBestVersionDir(string libraryDir, Version targetVersion)
252-
{
253-
string target = targetVersion.ToString();
254-
Version bestVersion = new Version(0, 0);
255-
foreach (var subdir in Directory.GetDirectories(libraryDir))
256-
{
257-
Version version;
258-
if (TryGetVersionFromString(Path.GetFileName(subdir), out version))
230+
foreach (var candidate in _additionalRuntimes)
259231
{
260-
if (version >= targetVersion)
261-
if (bestVersion.Major == 0 || bestVersion > version)
262-
bestVersion = version;
232+
if (candidate.Version >= targetVersion)
233+
if (bestRuntime is null || bestRuntime.Version > candidate.Version)
234+
bestRuntime = candidate;
263235
}
264-
}
265236

266-
return bestVersion.Major > 0
267-
? bestVersion.ToString()
268-
: null;
237+
return bestRuntime is not null;
238+
}
269239
}
270240

271-
private static bool TryGetVersionFromString(string text, out Version newVersion)
241+
public class WindowsDesktopStrategy : AdditionalRuntimesStrategy
272242
{
273-
const string VERSION_CHARS = ".0123456789";
274-
275-
int len = 0;
276-
foreach (char c in text)
277-
{
278-
if (VERSION_CHARS.IndexOf(c) >= 0)
279-
len++;
280-
else
281-
break;
282-
}
243+
public WindowsDesktopStrategy() : base("Microsoft.WindowsDesktop.App") { }
244+
}
283245

284-
try
285-
{
286-
newVersion = new Version(text.Substring(0, len));
287-
return true;
288-
}
289-
catch
290-
{
291-
newVersion = new Version();
292-
return false;
293-
}
246+
public class AspNetCoreStrategy : AdditionalRuntimesStrategy
247+
{
248+
public AspNetCoreStrategy() : base("Microsoft.AspNetCore.App") { }
294249
}
295250

296251
#endregion

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)