Skip to content

Commit 6235ee4

Browse files
JoeRobichrainersigwaldYuliiaKovalova
authored
Return SDKs from all dotnet location (#329)
* Return SDKs from all dotnet location * Preserve ordering of returned SDKs across dotnet instances. * Yield return all the available SDKs * Add query option to search all dotnet locations * Fix up comment wording Co-authored-by: Rainer Sigwald <[email protected]> * Use TryAdd * bump the version to 1.8 --------- Co-authored-by: Rainer Sigwald <[email protected]> Co-authored-by: YuliiaKovalova <[email protected]>
1 parent 5313383 commit 6235ee4

File tree

4 files changed

+109
-83
lines changed

4 files changed

+109
-83
lines changed

src/MSBuildLocator/DotNetSdkLocationHelper.cs

Lines changed: 87 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ internal static class DotNetSdkLocationHelper
5656
// 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,
5757
// this ensures that we don't choose an SDK that doesn't work with the runtime of the chosen application. This is not guaranteed
5858
// to always work but should work for now.
59-
if (!allowQueryAllRuntimeVersions &&
60-
(major > Environment.Version.Major ||
59+
if (!allowQueryAllRuntimeVersions &&
60+
(major > Environment.Version.Major ||
6161
(major == Environment.Version.Major && minor > Environment.Version.Minor)))
6262
{
6363
return null;
@@ -70,46 +70,104 @@ internal static class DotNetSdkLocationHelper
7070
discoveryType: DiscoveryType.DotNetSdk);
7171
}
7272

73-
public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
74-
{
75-
foreach (var basePath in GetDotNetBasePaths(workingDirectory))
73+
public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
74+
{
75+
string? bestSdkPath;
76+
string[] allAvailableSdks;
77+
try
78+
{
79+
AddUnmanagedDllResolver();
80+
81+
bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
82+
allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
83+
}
84+
finally
85+
{
86+
RemoveUnmanagedDllResolver();
87+
}
88+
89+
Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
90+
foreach (var basePath in allAvailableSdks)
7691
{
7792
var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
7893
if (dotnetSdk != null)
7994
{
80-
yield return dotnetSdk;
95+
// We want to return the best SDK first
96+
if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
97+
{
98+
// We will add a null entry to the map to ensure we do not add the same SDK from a different location.
99+
versionInstanceMap[dotnetSdk.Version] = null;
100+
yield return dotnetSdk;
101+
}
102+
103+
// Only add an SDK once, even if it's installed in multiple locations.
104+
versionInstanceMap.TryAdd(dotnetSdk.Version, dotnetSdk);
81105
}
82106
}
83-
}
84107

85-
private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
86-
{
87-
try
108+
// We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
109+
var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
110+
foreach (var instance in instances)
88111
{
89-
AddUnmanagedDllResolver();
112+
yield return instance;
113+
}
90114

91-
string? bestSDK = GetSdkFromGlobalSettings(workingDirectory);
92-
if (!string.IsNullOrEmpty(bestSDK))
115+
// Returns the list of all available SDKs ordered by ascending version.
116+
static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
117+
{
118+
bool foundSdks = false;
119+
string[]? resolvedPaths = null;
120+
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
93121
{
94-
yield return bestSDK;
95-
}
122+
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
96123

97-
string[] dotnetPaths = GetAllAvailableSDKs();
98-
// We want to return the newest SDKs first, however, so iterate over the list in reverse order.
99-
// If basePath is disqualified because it was later
100-
// than the runtime version, this ensures that RegisterDefaults will return the latest valid
101-
// SDK instead of the earliest installed.
102-
for (int i = dotnetPaths.Length - 1; i >= 0; i--)
103-
{
104-
if (dotnetPaths[i] != bestSDK)
124+
if (rc == 0 && resolvedPaths != null)
105125
{
106-
yield return dotnetPaths[i];
126+
foundSdks = true;
127+
128+
foreach (string path in resolvedPaths)
129+
{
130+
yield return path;
131+
}
132+
133+
if (resolvedPaths.Length > 0 && !allowAllDotnetLocations)
134+
{
135+
break;
136+
}
107137
}
108138
}
139+
140+
// Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
141+
if (!foundSdks)
142+
{
143+
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
144+
}
109145
}
110-
finally
146+
147+
// Determines the directory location of the SDK accounting for global.json and multi-level lookup policy.
148+
static string? GetSdkFromGlobalSettings(string workingDirectory)
111149
{
112-
RemoveUnmanagedDllResolver();
150+
string? resolvedSdk = null;
151+
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
152+
{
153+
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
154+
{
155+
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
156+
{
157+
resolvedSdk = value;
158+
}
159+
});
160+
161+
if (rc == 0)
162+
{
163+
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
164+
return resolvedSdk;
165+
}
166+
}
167+
168+
return string.IsNullOrEmpty(resolvedSdk)
169+
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
170+
: resolvedSdk;
113171
}
114172
}
115173

@@ -158,7 +216,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
158216
};
159217

160218
var orderedVersions = fileEnumerable.Where(v => v != null).Select(v => v!).OrderByDescending(f => f).ToList();
161-
219+
162220
foreach (SemanticVersion hostFxrVersion in orderedVersions)
163221
{
164222
string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
@@ -178,35 +236,6 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
178236
}
179237

180238
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.";
181-
182-
/// <summary>
183-
/// Determines the directory location of the SDK accounting for
184-
/// global.json and multi-level lookup policy.
185-
/// </summary>
186-
private static string? GetSdkFromGlobalSettings(string workingDirectory)
187-
{
188-
string? resolvedSdk = null;
189-
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
190-
{
191-
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
192-
{
193-
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
194-
{
195-
resolvedSdk = value;
196-
}
197-
});
198-
199-
if (rc == 0)
200-
{
201-
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
202-
return resolvedSdk;
203-
}
204-
}
205-
206-
return string.IsNullOrEmpty(resolvedSdk)
207-
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
208-
: resolvedSdk;
209-
}
210239

211240
private static IList<string> ResolveDotnetPathCandidates()
212241
{
@@ -256,7 +285,7 @@ void AddIfValid(string? path)
256285
// 32-bit architecture has (x86) suffix
257286
string envVarName = (IntPtr.Size == 4) ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT";
258287
var dotnetPath = FindDotnetPathFromEnvVariable(envVarName);
259-
288+
260289
return dotnetPath;
261290
}
262291

@@ -285,26 +314,6 @@ void AddIfValid(string? path)
285314
return dotnetPath;
286315
}
287316

288-
/// <summary>
289-
/// Returns the list of all available SDKs ordered by ascending version.
290-
/// </summary>
291-
private static string[] GetAllAvailableSDKs()
292-
{
293-
string[]? resolvedPaths = null;
294-
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
295-
{
296-
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
297-
298-
if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
299-
{
300-
break;
301-
}
302-
}
303-
304-
// Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
305-
return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
306-
}
307-
308317
/// <summary>
309318
/// This native method call determines the actual location of path, including
310319
/// resolving symbolic links.
@@ -321,7 +330,7 @@ private static string[] GetAllAvailableSDKs()
321330
private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
322331
{
323332
string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
324-
333+
325334
return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath);
326335
}
327336

src/MSBuildLocator/MSBuildLocator.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ public static class MSBuildLocator
5252
/// </remarks.
5353
public static bool AllowQueryAllRuntimeVersions { get; set; } = false;
5454

55+
/// <summary>
56+
/// Allow discovery of .NET SDK versions from all discovered dotnet install locations.
57+
/// </summary>
58+
/// <remarks>
59+
/// Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if you do not mind behaving differently than the dotnet muxer.
60+
/// </remarks.
61+
public static bool AllowQueryAllDotnetLocations { get; set; } = false;
62+
5563
/// <summary>
5664
/// Gets a value indicating whether an instance of MSBuild can be registered.
5765
/// </summary>
@@ -200,7 +208,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths)
200208
{
201209
if (string.IsNullOrWhiteSpace(msbuildSearchPaths[i]))
202210
{
203-
nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i+1} may not be null or whitespace", nameof(msbuildSearchPaths)));
211+
nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i + 1} may not be null or whitespace", nameof(msbuildSearchPaths)));
204212
}
205213
}
206214
if (nullOrWhiteSpaceExceptions.Count > 0)
@@ -266,7 +274,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths)
266274

267275
AppDomain.CurrentDomain.AssemblyResolve += s_registeredHandler;
268276
#else
269-
s_registeredHandler = (_, assemblyName) =>
277+
s_registeredHandler = (_, assemblyName) =>
270278
{
271279
return TryLoadAssembly(assemblyName);
272280
};
@@ -377,7 +385,8 @@ private static IEnumerable<VisualStudioInstance> GetInstances(VisualStudioInstan
377385
#if NETCOREAPP
378386
// AllowAllRuntimeVersions was added to VisualStudioInstanceQueryOptions for fulfilling Roslyn's needs. One of the properties will be removed in v2.0.
379387
bool allowAllRuntimeVersions = AllowQueryAllRuntimeVersions || options.AllowAllRuntimeVersions;
380-
foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions))
388+
bool allowAllDotnetLocations = AllowQueryAllDotnetLocations || options.AllowAllDotnetLocations;
389+
foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions, allowAllDotnetLocations))
381390
yield return dotnetSdk;
382391
#endif
383392
}
@@ -404,7 +413,7 @@ private static VisualStudioInstance GetDevConsoleInstance()
404413
Version.TryParse(versionString, out version);
405414
}
406415

407-
if(version != null)
416+
if (version != null)
408417
{
409418
return new VisualStudioInstance("DEVCONSOLE", path, version, DiscoveryType.DeveloperConsole);
410419
}

src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public class VisualStudioInstanceQueryOptions
3737
/// Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if your application has special logic to handle loading an incompatible SDK, such as launching a new process with the target SDK's runtime.
3838
/// </remarks.
3939
public bool AllowAllRuntimeVersions { get; set; } = false;
40+
41+
/// <summary>
42+
/// Allow discovery of .NET SDK versions from all discovered dotnet install locations.
43+
/// </summary>
44+
/// <remarks>
45+
/// Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if you do not mind behaving differently than a command-line dotnet invocation.
46+
/// </remarks.
47+
public bool AllowAllDotnetLocations { get; set; } = false;
4048
#endif
4149

4250
/// <summary>

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "1.7",
2+
"version": "1.8",
33
"assemblyVersion": "1.0.0.0",
44
"publicReleaseRefSpec": [
55
"^refs/heads/release/.*"

0 commit comments

Comments
 (0)