forked from microsoft/MSBuildLocator
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDotNetSdkLocationHelper.cs
More file actions
363 lines (307 loc) · 15.3 KB
/
DotNetSdkLocationHelper.cs
File metadata and controls
363 lines (307 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#if NETCOREAPP
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Enumeration;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text.RegularExpressions;
#nullable enable
namespace Microsoft.Build.Locator
{
internal static class DotNetSdkLocationHelper
{
private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline);
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet";
private static readonly Lazy<IList<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());
public static VisualStudioInstance? GetInstance(string dotNetSdkPath, bool allowQueryAllRuntimeVersions)
{
if (string.IsNullOrWhiteSpace(dotNetSdkPath) || !File.Exists(Path.Combine(dotNetSdkPath, "Microsoft.Build.dll")))
{
return null;
}
string versionPath = Path.Combine(dotNetSdkPath, ".version");
if (!File.Exists(versionPath))
{
return null;
}
// Preview versions contain a hyphen after the numeric part of the version. Version.TryParse doesn't accept that.
Match versionMatch = VersionRegex.Match(File.ReadAllText(versionPath));
if (!versionMatch.Success)
{
return null;
}
if (!int.TryParse(versionMatch.Groups[1].Value, out int major) ||
!int.TryParse(versionMatch.Groups[2].Value, out int minor) ||
!int.TryParse(versionMatch.Groups[3].Value, out int patch))
{
return null;
}
// Components of the SDK often have dependencies on the runtime they shipped with, including that several tasks that shipped
// in the .NET 5 SDK rely on the .NET 5.0 runtime. Assuming the runtime that shipped with a particular SDK has the same version,
// this ensures that we don't choose an SDK that doesn't work with the runtime of the chosen application. This is not guaranteed
// to always work but should work for now.
if (!allowQueryAllRuntimeVersions &&
(major > Environment.Version.Major ||
(major == Environment.Version.Major && minor > Environment.Version.Minor)))
{
return null;
}
return new VisualStudioInstance(
name: ".NET Core SDK",
path: dotNetSdkPath,
version: new Version(major, minor, patch),
discoveryType: DiscoveryType.DotNetSdk);
}
public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
{
string? bestSdkPath;
string[] allAvailableSdks;
try
{
AddUnmanagedDllResolver();
bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
}
finally
{
RemoveUnmanagedDllResolver();
}
Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
foreach (var basePath in allAvailableSdks)
{
var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
if (dotnetSdk != null)
{
// We want to return the best SDK first
if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
{
// We will add a null entry to the map to ensure we do not add the same SDK from a different location.
versionInstanceMap[dotnetSdk.Version] = null;
yield return dotnetSdk;
}
// Only add an SDK once, even if it's installed in multiple locations.
versionInstanceMap.TryAdd(dotnetSdk.Version, dotnetSdk);
}
}
// We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
foreach (var instance in instances)
{
yield return instance;
}
// Returns the list of all available SDKs ordered by ascending version.
static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
{
bool foundSdks = false;
string[]? resolvedPaths = null;
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
if (rc == 0 && resolvedPaths != null)
{
foundSdks = true;
foreach (string path in resolvedPaths)
{
yield return path;
}
if (resolvedPaths.Length > 0 && !allowAllDotnetLocations)
{
break;
}
}
}
// Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
if (!foundSdks)
{
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
}
}
// Determines the directory location of the SDK accounting for global.json and multi-level lookup policy.
static string? GetSdkFromGlobalSettings(string workingDirectory)
{
string? resolvedSdk = null;
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
{
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
{
resolvedSdk = value;
}
});
if (rc == 0)
{
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
return resolvedSdk;
}
}
return string.IsNullOrEmpty(resolvedSdk)
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
: resolvedSdk;
}
}
private static void AddUnmanagedDllResolver() => ModifyUnmanagedDllResolver(loadContext => loadContext.ResolvingUnmanagedDll += HostFxrResolver);
private static void RemoveUnmanagedDllResolver() => ModifyUnmanagedDllResolver(loadContext => loadContext.ResolvingUnmanagedDll -= HostFxrResolver);
private static void ModifyUnmanagedDllResolver(Action<AssemblyLoadContext> resolverAction)
{
// For Windows hostfxr is loaded in the process.
if (!IsWindows)
{
var loadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
if (loadContext != null)
{
resolverAction(loadContext);
}
}
}
private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
{
// the DllImport hardcoded the name as hostfxr.
if (!libraryName.Equals(NativeMethods.HostFxrName, StringComparison.Ordinal))
{
return IntPtr.Zero;
}
string hostFxrLibName =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
"hostfxr.dll" :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "libhostfxr.dylib" : "libhostfxr.so";
string hostFxrRoot = string.Empty;
// Get the dotnet path candidates
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr");
if (Directory.Exists(hostFxrRoot))
{
var fileEnumerable = new FileSystemEnumerable<SemanticVersion?>(
directory: hostFxrRoot,
transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null)
{
ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory
};
var orderedVersions = fileEnumerable.Where(v => v != null).Select(v => v!).OrderByDescending(f => f).ToList();
foreach (SemanticVersion hostFxrVersion in orderedVersions)
{
string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
if (NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle))
{
return handle;
}
}
}
}
string error = $".NET SDK cannot be resolved, because {hostFxrLibName} cannot be found inside {hostFxrRoot}." +
Environment.NewLine +
$"This might indicate a corrupted SDK installation on the machine.";
throw new InvalidOperationException(error);
}
private static string SdkResolutionExceptionMessage(string methodName) => $"Failed to find all versions of .NET Core MSBuild. Call to {methodName}. There may be more details in stderr.";
private static IList<string> ResolveDotnetPathCandidates()
{
var pathCandidates = new List<string>();
AddIfValid(GetDotnetPathFromROOT());
string? dotnetExePath = GetCurrentProcessPath();
bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase);
if (isRunFromDotnetExecutable)
{
AddIfValid(Path.GetDirectoryName(dotnetExePath));
}
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
{
if (!IsWindows)
{
hostPath = realpath(hostPath) ?? hostPath;
}
AddIfValid(Path.GetDirectoryName(hostPath));
}
AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"));
AddIfValid(GetDotnetPathFromPATH());
return pathCandidates.Count == 0
? throw new InvalidOperationException("Path to dotnet executable is not set. " +
"The probed variables are: DOTNET_ROOT, DOTNET_HOST_PATH, DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR and PATH. " +
"Make sure, that at least one of the listed variables points to the existing dotnet executable.")
: pathCandidates;
void AddIfValid(string? path)
{
if (!string.IsNullOrEmpty(path))
{
pathCandidates.Add(path);
}
}
}
private static string? GetDotnetPathFromROOT()
{
// 32-bit architecture has (x86) suffix
string envVarName = (IntPtr.Size == 4) ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT";
var dotnetPath = FindDotnetPathFromEnvVariable(envVarName);
return dotnetPath;
}
private static string? GetCurrentProcessPath() => Environment.ProcessPath;
private static string? GetDotnetPathFromPATH()
{
string? dotnetPath = null;
// We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink (possibly even to more symlinks) that we have to resolve
// to the real dotnet executable.
// This will work as often as just invoking dotnet from the command line, but we can be more confident in finding a dotnet executable by following
// https://github.com/dotnet/designs/blob/main/accepted/2021/install-location-per-architecture.md
// This could be done using the nethost library, but this is currently shipped as metadata package (Microsoft.NETCore.DotNetAppHost) and requires the customers
// to specify <RuntimeIdentifier> for resolving runtime assembly.
var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (string dir in paths)
{
string? filePath = ValidatePath(dir);
if (!string.IsNullOrEmpty(filePath))
{
dotnetPath = filePath;
break;
}
}
return dotnetPath;
}
/// <summary>
/// This native method call determines the actual location of path, including
/// resolving symbolic links.
/// </summary>
private static string? realpath(string path)
{
IntPtr ptr = NativeMethods.realpath(path, IntPtr.Zero);
string? result = Marshal.PtrToStringAuto(ptr);
NativeMethods.free(ptr);
return result;
}
private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
{
string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath);
}
private static void SetEnvironmentVariableIfEmpty(string name, string value)
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(name)))
{
Environment.SetEnvironmentVariable(name, value);
}
}
private static string? ValidatePath(string dotnetPath)
{
string fullPathToDotnetFromRoot = Path.Combine(dotnetPath, ExeName);
if (File.Exists(fullPathToDotnetFromRoot))
{
if (!IsWindows)
{
fullPathToDotnetFromRoot = realpath(fullPathToDotnetFromRoot) ?? fullPathToDotnetFromRoot;
return File.Exists(fullPathToDotnetFromRoot) ? Path.GetDirectoryName(fullPathToDotnetFromRoot) : null;
}
return dotnetPath;
}
return null;
}
}
}
#endif