diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index f5398ff..58cb54d 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; @@ -70,46 +70,104 @@ internal static class DotNetSdkLocationHelper discoveryType: DiscoveryType.DotNetSdk); } - public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes) - { - foreach (var basePath in GetDotNetBasePaths(workingDirectory)) + public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations) + { + string? bestSdkPath; + string[] allAvailableSdks; + try + { + AddUnmanagedDllResolver(); + + bestSdkPath = GetSdkFromGlobalSettings(workingDirectory); + allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray(); + } + finally + { + RemoveUnmanagedDllResolver(); + } + + Dictionary versionInstanceMap = new(); + foreach (var basePath in allAvailableSdks) { var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes); if (dotnetSdk != null) { - yield return dotnetSdk; + // 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); } } - } - 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 IEnumerable GetAllAvailableSDKs(bool allowAllDotnetLocations) + { + bool foundSdks = false; + string[]? resolvedPaths = null; + foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - yield return bestSDK; - } + int rc = 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--) - { - if (dotnetPaths[i] != bestSDK) + if (rc == 0 && resolvedPaths != null) { - yield return dotnetPaths[i]; + 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))); + } } - finally + + // Determines the directory location of the SDK accounting for global.json and multi-level lookup policy. + static string? GetSdkFromGlobalSettings(string workingDirectory) { - RemoveUnmanagedDllResolver(); + 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; } } @@ -158,7 +216,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,35 +236,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() { @@ -256,7 +285,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; } @@ -285,26 +314,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) - { - int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value); - - if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0) - { - break; - } - } - - // 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. @@ -321,7 +330,7 @@ private static string[] GetAllAvailableSDKs() private static string? FindDotnetPathFromEnvVariable(string environmentVariable) { string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable); - + return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath); } 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..116abb2 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 a command-line dotnet invocation. + /// 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/.*"