From de9afce2d595185155060c8a70f6d9f8f3968fb9 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 19 Mar 2025 16:01:29 -0700 Subject: [PATCH 1/7] Return SDKs from all dotnet location --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index f5398ff..141e62d 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -56,8 +56,8 @@ internal static class DotNetSdkLocationHelper // 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 || + if (!allowQueryAllRuntimeVersions && + (major > Environment.Version.Major || (major == Environment.Version.Major && minor > Environment.Version.Minor))) { return null; @@ -71,13 +71,18 @@ internal static class DotNetSdkLocationHelper } public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes) - { + { + HashSet versions = new(); foreach (var basePath in GetDotNetBasePaths(workingDirectory)) { var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes); if (dotnetSdk != null) { - yield return dotnetSdk; + // Only return an SDK once, even if it's installed in multiple locations. + if (versions.Add(dotnetSdk.Version)) + { + yield return dotnetSdk; + } } } } @@ -158,7 +163,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) }; 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); @@ -178,7 +183,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) } 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."; - + /// /// Determines the directory location of the SDK accounting for /// global.json and multi-level lookup policy. @@ -256,7 +261,7 @@ void AddIfValid(string? path) // 32-bit architecture has (x86) suffix string envVarName = (IntPtr.Size == 4) ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT"; var dotnetPath = FindDotnetPathFromEnvVariable(envVarName); - + return dotnetPath; } @@ -293,12 +298,7 @@ private static string[] GetAllAvailableSDKs() 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 && resolvedPaths.Length > 0) - { - break; - } + NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value); } // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. @@ -321,7 +321,7 @@ private static string[] GetAllAvailableSDKs() private static string? FindDotnetPathFromEnvVariable(string environmentVariable) { string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable); - + return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath); } From 6314d6769d7ca990a03944fd366f8be54363976e Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 19 Mar 2025 17:01:01 -0700 Subject: [PATCH 2/7] Preserve ordering of returned SDKs across dotnet instances. --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 129 +++++++++--------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 141e62d..0f369a8 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -72,49 +72,86 @@ internal static class DotNetSdkLocationHelper public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes) { - HashSet versions = new(); - foreach (var basePath in GetDotNetBasePaths(workingDirectory)) + string? bestSdkPath; + string[] allAvailableSdks; + try + { + AddUnmanagedDllResolver(); + + bestSdkPath = GetSdkFromGlobalSettings(workingDirectory); + allAvailableSdks = GetAllAvailableSDKs(); + } + finally + { + RemoveUnmanagedDllResolver(); + } + + Dictionary versionInstanceMap = new(); + foreach (var basePath in allAvailableSdks) { var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes); if (dotnetSdk != null) { - // Only return an SDK once, even if it's installed in multiple locations. - if (versions.Add(dotnetSdk.Version)) + // 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. + if (!versionInstanceMap.ContainsKey(dotnetSdk.Version)) + { + versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk); + } } } - } - private static IEnumerable GetDotNetBasePaths(string workingDirectory) - { - try + // 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().OrderByDescending(i => i.Version); + foreach (var instance in instances) { - AddUnmanagedDllResolver(); + yield return instance; + } - string? bestSDK = GetSdkFromGlobalSettings(workingDirectory); - if (!string.IsNullOrEmpty(bestSDK)) + // Returns the list of all available SDKs ordered by ascending version. + static string[] GetAllAvailableSDKs() + { + string[]? resolvedPaths = null; + foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - yield return bestSDK; + NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value); } - string[] dotnetPaths = GetAllAvailableSDKs(); - // We want to return the newest SDKs first, however, so iterate over the list in reverse order. - // If basePath is disqualified because it was later - // than the runtime version, this ensures that RegisterDefaults will return the latest valid - // SDK instead of the earliest installed. - for (int i = dotnetPaths.Length - 1; i >= 0; i--) + // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. + return resolvedPaths ?? 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) { - if (dotnetPaths[i] != bestSDK) + int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => { - yield return dotnetPaths[i]; + 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; } } - } - finally - { - RemoveUnmanagedDllResolver(); + + return string.IsNullOrEmpty(resolvedSdk) + ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2))) + : resolvedSdk; } } @@ -184,35 +221,6 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) 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."; - /// - /// Determines the directory location of the SDK accounting for - /// global.json and multi-level lookup policy. - /// - private 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 IList ResolveDotnetPathCandidates() { var pathCandidates = new List(); @@ -290,21 +298,6 @@ void AddIfValid(string? path) return dotnetPath; } - /// - /// Returns the list of all available SDKs ordered by ascending version. - /// - private static string[] GetAllAvailableSDKs() - { - string[]? resolvedPaths = null; - foreach (string dotnetPath in s_dotnetPathCandidates.Value) - { - NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value); - } - - // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. - return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))); - } - /// /// This native method call determines the actual location of path, including /// resolving symbolic links. From 24249863f566a9938a3b03a594283e43843fae33 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 19 Mar 2025 17:09:38 -0700 Subject: [PATCH 3/7] Yield return all the available SDKs --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 0f369a8..cabace0 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -79,7 +79,7 @@ public static IEnumerable GetInstances(string workingDirec AddUnmanagedDllResolver(); bestSdkPath = GetSdkFromGlobalSettings(workingDirectory); - allAvailableSdks = GetAllAvailableSDKs(); + allAvailableSdks = GetAllAvailableSDKs().ToArray(); } finally { @@ -116,16 +116,30 @@ public static IEnumerable GetInstances(string workingDirec } // Returns the list of all available SDKs ordered by ascending version. - static string[] GetAllAvailableSDKs() + static IEnumerable GetAllAvailableSDKs() { + bool foundSdks = false; string[]? resolvedPaths = null; foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = 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; + } + } } // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. - return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))); + 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. From 302e031c5fceb5860486ba61b436865adbf9cca1 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 24 Mar 2025 22:47:01 -0700 Subject: [PATCH 4/7] Add query option to search all dotnet locations --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 11 ++++++++--- src/MSBuildLocator/MSBuildLocator.cs | 17 +++++++++++++---- .../VisualStudioInstanceQueryOptions.cs | 8 ++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index cabace0..fd4777e 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -70,7 +70,7 @@ internal static class DotNetSdkLocationHelper discoveryType: DiscoveryType.DotNetSdk); } - public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes) + public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations) { string? bestSdkPath; string[] allAvailableSdks; @@ -79,7 +79,7 @@ public static IEnumerable GetInstances(string workingDirec AddUnmanagedDllResolver(); bestSdkPath = GetSdkFromGlobalSettings(workingDirectory); - allAvailableSdks = GetAllAvailableSDKs().ToArray(); + allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray(); } finally { @@ -116,7 +116,7 @@ public static IEnumerable GetInstances(string workingDirec } // Returns the list of all available SDKs ordered by ascending version. - static IEnumerable GetAllAvailableSDKs() + static IEnumerable GetAllAvailableSDKs(bool allowAllDotnetLocations) { bool foundSdks = false; string[]? resolvedPaths = null; @@ -132,6 +132,11 @@ static IEnumerable GetAllAvailableSDKs() { yield return path; } + + if (resolvedPaths.Length > 0 && !allowAllDotnetLocations) + { + break; + } } } diff --git a/src/MSBuildLocator/MSBuildLocator.cs b/src/MSBuildLocator/MSBuildLocator.cs index f501f49..9d76676 100644 --- a/src/MSBuildLocator/MSBuildLocator.cs +++ b/src/MSBuildLocator/MSBuildLocator.cs @@ -52,6 +52,14 @@ public static class MSBuildLocator /// + /// Allow discovery of .NET SDK versions from all discovered dotnet install locations. + /// + /// + /// Defaults to . Set this to only if you do not mind behaving differently than the dotnet muxer. + /// /// Gets a value indicating whether an instance of MSBuild can be registered. /// @@ -200,7 +208,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths) { if (string.IsNullOrWhiteSpace(msbuildSearchPaths[i])) { - nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i+1} may not be null or whitespace", nameof(msbuildSearchPaths))); + nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i + 1} may not be null or whitespace", nameof(msbuildSearchPaths))); } } if (nullOrWhiteSpaceExceptions.Count > 0) @@ -266,7 +274,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths) AppDomain.CurrentDomain.AssemblyResolve += s_registeredHandler; #else - s_registeredHandler = (_, assemblyName) => + s_registeredHandler = (_, assemblyName) => { return TryLoadAssembly(assemblyName); }; @@ -377,7 +385,8 @@ private static IEnumerable GetInstances(VisualStudioInstan #if NETCOREAPP // AllowAllRuntimeVersions was added to VisualStudioInstanceQueryOptions for fulfilling Roslyn's needs. One of the properties will be removed in v2.0. bool allowAllRuntimeVersions = AllowQueryAllRuntimeVersions || options.AllowAllRuntimeVersions; - foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions)) + bool allowAllDotnetLocations = AllowQueryAllDotnetLocations || options.AllowAllDotnetLocations; + foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions, allowAllDotnetLocations)) yield return dotnetSdk; #endif } @@ -404,7 +413,7 @@ private static VisualStudioInstance GetDevConsoleInstance() Version.TryParse(versionString, out version); } - if(version != null) + if (version != null) { return new VisualStudioInstance("DEVCONSOLE", path, version, DiscoveryType.DeveloperConsole); } diff --git a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs index c3e1660..07f0508 100644 --- a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs +++ b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs @@ -37,6 +37,14 @@ public class VisualStudioInstanceQueryOptions /// Defaults to . Set this to 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. /// + /// Allow discovery of .NET SDK versions from all discovered dotnet install locations. + /// + /// + /// Defaults to . Set this to only if you do not mind behaving differently than the dotnet muxer. + /// From 52a422e50aa27825bd5c084c5846f8ac6951ce72 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Fri, 28 Mar 2025 13:38:00 -0700 Subject: [PATCH 5/7] Fix up comment wording Co-authored-by: Rainer Sigwald --- src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs index 07f0508..116abb2 100644 --- a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs +++ b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs @@ -42,7 +42,7 @@ public class VisualStudioInstanceQueryOptions /// Allow discovery of .NET SDK versions from all discovered dotnet install locations. /// /// - /// Defaults to . Set this to only if you do not mind behaving differently than the dotnet muxer. + /// Defaults to . Set this to only if you do not mind behaving differently than a command-line dotnet invocation. /// Date: Fri, 28 Mar 2025 13:47:12 -0700 Subject: [PATCH 6/7] Use TryAdd --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index fd4777e..58cb54d 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -101,10 +101,7 @@ public static IEnumerable GetInstances(string workingDirec } // Only add an SDK once, even if it's installed in multiple locations. - if (!versionInstanceMap.ContainsKey(dotnetSdk.Version)) - { - versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk); - } + versionInstanceMap.TryAdd(dotnetSdk.Version, dotnetSdk); } } From 4a8eac4ca99595ccb2167b09416b8386f50bd7ba Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:01:03 +0200 Subject: [PATCH 7/7] bump the version to 1.8 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 1d01149..99db79e 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "1.7", + "version": "1.8", "assemblyVersion": "1.0.0.0", "publicReleaseRefSpec": [ "^refs/heads/release/.*"