From 84045ea00a767440d13e5173146fa2de976f2360 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 14 Sep 2023 11:01:57 +0200 Subject: [PATCH 1/6] add probing of all available path candidates + return missed DOTNET_HOST_PATH set change --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 103 ++++++++++++------ 1 file changed, 69 insertions(+), 34 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index b12ca840..a2aa1521 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -22,7 +22,7 @@ internal static class DotNetSdkLocationHelper private static readonly Regex s_versionRegex = new(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline); private static readonly bool s_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); private static readonly string s_exeName = s_isWindows ? "dotnet.exe" : "dotnet"; - private static readonly Lazy s_dotnetPath = new(() => ResolveDotnetPath()); + private static readonly Lazy> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates()); public static VisualStudioInstance? GetInstance(string dotNetSdkPath) { @@ -139,24 +139,27 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) return IntPtr.Zero; } - string hostFxrRoot = Path.Combine(s_dotnetPath.Value, "host", "fxr"); - if (Directory.Exists(hostFxrRoot)) + foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - // Load hostfxr from the highest version, because it should be backward-compatible - SemanticVersion? hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) - .Max(str => SemanticVersionParser.TryParse(str, out SemanticVersion? version) ? version : null); - - if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) + string hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr"); + if (Directory.Exists(hostFxrRoot)) { - string hostFxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension)); + // Load hostfxr from the highest version, because it should be backward-compatible + SemanticVersion? hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) + .Max(str => SemanticVersionParser.TryParse(str, out SemanticVersion? version) ? version : null); - if (File.Exists(hostFxrAssembly)) + if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) { - return NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle) ? handle : IntPtr.Zero; + string hostFxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension)); + + if (File.Exists(hostFxrAssembly)) + { + return NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle) ? handle : IntPtr.Zero; + } } } } - + return IntPtr.Zero; } @@ -169,39 +172,55 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) private static string? GetSdkFromGlobalSettings(string workingDirectory) { string? resolvedSdk = null; - int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: s_dotnetPath.Value, working_dir: workingDirectory, flags: 0, result: (key, value) => + foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir) + int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => { - resolvedSdk = value; - } - }); + if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir) + { + resolvedSdk = value; + } + }); - return rc != 0 + if (rc == 0 && !string.IsNullOrEmpty(resolvedSdk)) + { + SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", dotnetPath); + break; + } + } + + return string.IsNullOrEmpty(resolvedSdk) ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2))) : resolvedSdk; } - private static string ResolveDotnetPath() + private static IList ResolveDotnetPathCandidates() { - string? dotnetPath = GetDotnetPathFromROOT(); + var pathCandidates = new List + { + GetDotnetPathFromROOT() + }; + + string? dotnetExePath = GetCurrentProcessPath(); + bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) + && Path.GetFileName(dotnetExePath).Equals(s_exeName, StringComparison.InvariantCultureIgnoreCase); - if (string.IsNullOrEmpty(dotnetPath)) + if (isRunFromDotnetExecutable) { - string? dotnetExePath = GetCurrentProcessPath(); - bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) - && Path.GetFileName(dotnetExePath).Equals(s_exeName, StringComparison.InvariantCultureIgnoreCase); - - dotnetPath = isRunFromDotnetExecutable - ? Path.GetDirectoryName(dotnetExePath) - : FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH") - ?? FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR") - ?? GetDotnetPathFromPATH(); + pathCandidates.Add(Path.GetDirectoryName(dotnetExePath)); } - return string.IsNullOrEmpty(dotnetPath) + pathCandidates.Add(FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH")); + pathCandidates.Add(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR")); + pathCandidates.Add(GetDotnetPathFromPATH()); + + IList filteredPathCandidates = pathCandidates.Where(pk => !string.IsNullOrEmpty(pk)) + .Select(s => s!) + .ToList(); + + return filteredPathCandidates.Count == 0 ? throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?") - : dotnetPath; + : filteredPathCandidates; } private static string? GetDotnetPathFromROOT() @@ -244,10 +263,18 @@ private static string ResolveDotnetPath() private static string[] GetAllAvailableSDKs() { string[]? resolvedPaths = null; - int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: s_dotnetPath.Value, result: (key, value) => resolvedPaths = value); + 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 rc != 0 + return resolvedPaths == null ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))) : resolvedPaths ?? Array.Empty(); } @@ -265,6 +292,14 @@ private static string[] GetAllAvailableSDKs() return result; } + private static void SetEnvironmentVariableIfEmpty(string name, string value) + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(name))) + { + Environment.SetEnvironmentVariable(name, value); + } + } + private static string? FindDotnetPathFromEnvVariable(string environmentVariable) { string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable); From 56640c73e6afc69bf49dbfc87c74218f561175d5 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 14 Sep 2023 13:10:17 +0200 Subject: [PATCH 2/6] fix review comments --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index a2aa1521..b87b3fe0 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -131,38 +131,51 @@ private static void ModifyUnmanagedDllResolver(Action resol private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) { + // Library name for libhostfxr string hostFxrLibName = "libhostfxr"; + // Library extension for the current platform string libExtension = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "dylib" : "so"; + // If the requested library name is not libhostfxr, return IntPtr.Zero if (!hostFxrLibName.Equals(libraryName)) { return IntPtr.Zero; } + // Get the dotnet path candidates foreach (string dotnetPath in s_dotnetPathCandidates.Value) { string hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr"); + + // Check if the host/fxr directory exists if (Directory.Exists(hostFxrRoot)) { - // Load hostfxr from the highest version, because it should be backward-compatible - SemanticVersion? hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) - .Max(str => SemanticVersionParser.TryParse(str, out SemanticVersion? version) ? version : null); - - if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) + // Get a list of hostfxr assembly directories (e.g., 6.0.3, 7.0.1-preview.2.4) + IList hostFxrAssemblyDirs = Directory.GetDirectories(hostFxrRoot) + .Select(path => SemanticVersionParser.TryParse(Path.GetFileName(path), out SemanticVersion? version) ? version : null) + .Select(v => v!) + .OrderByDescending(v => v) + .ToList(); + + foreach (SemanticVersion hostFxrDir in hostFxrAssemblyDirs) { - string hostFxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension)); + string hostFxrAssemblyPath = Path.Combine(hostFxrRoot, hostFxrDir.OriginalValue, $"{hostFxrLibName}.{libExtension}"); - if (File.Exists(hostFxrAssembly)) + if (File.Exists(hostFxrAssemblyPath)) { - return NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle) ? handle : IntPtr.Zero; + if (NativeLibrary.TryLoad(hostFxrAssemblyPath, out IntPtr handle)) + { + return handle; + } } } } } - + return IntPtr.Zero; } + 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."; /// @@ -196,10 +209,8 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) private static IList ResolveDotnetPathCandidates() { - var pathCandidates = new List - { - GetDotnetPathFromROOT() - }; + var pathCandidates = new List(); + AddIfValid(GetDotnetPathFromROOT()); string? dotnetExePath = GetCurrentProcessPath(); bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) @@ -207,20 +218,24 @@ private static IList ResolveDotnetPathCandidates() if (isRunFromDotnetExecutable) { - pathCandidates.Add(Path.GetDirectoryName(dotnetExePath)); + AddIfValid(Path.GetDirectoryName(dotnetExePath)); } - pathCandidates.Add(FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH")); - pathCandidates.Add(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR")); - pathCandidates.Add(GetDotnetPathFromPATH()); + AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH")); + AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR")); + AddIfValid(GetDotnetPathFromPATH()); - IList filteredPathCandidates = pathCandidates.Where(pk => !string.IsNullOrEmpty(pk)) - .Select(s => s!) - .ToList(); + return pathCandidates.Count == 0 + ? throw new InvalidOperationException("Path to dotnet executable is not set. Make sure it is added it either to DOTNET_HOST_PATH or PATH environment variable.") + : pathCandidates; - return filteredPathCandidates.Count == 0 - ? throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?") - : filteredPathCandidates; + void AddIfValid(string? path) + { + if (!string.IsNullOrEmpty(path)) + { + pathCandidates.Add(path); + } + } } private static string? GetDotnetPathFromROOT() @@ -274,9 +289,7 @@ private static string[] GetAllAvailableSDKs() } // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. - return resolvedPaths == null - ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))) - : resolvedPaths ?? Array.Empty(); + return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))); } /// From e4bf4b9ea6c4374770debb30873ee4b86f3c70b1 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 14 Sep 2023 14:18:53 +0200 Subject: [PATCH 3/6] fix review comments --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index b87b3fe0..ce7f437b 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -195,10 +195,10 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) } }); - if (rc == 0 && !string.IsNullOrEmpty(resolvedSdk)) + if (rc == 0) { SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", dotnetPath); - break; + return resolvedSdk; } } @@ -226,7 +226,7 @@ private static IList ResolveDotnetPathCandidates() AddIfValid(GetDotnetPathFromPATH()); return pathCandidates.Count == 0 - ? throw new InvalidOperationException("Path to dotnet executable is not set. Make sure it is added it either to DOTNET_HOST_PATH or PATH environment variable.") + ? throw new InvalidOperationException("Path to dotnet executable is not set. Make sure it is added either to DOTNET_HOST_PATH or PATH environment variable.") : pathCandidates; void AddIfValid(string? path) From f9842e94f716665969b28bafab0516feddd5d2b6 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:16:15 +0200 Subject: [PATCH 4/6] Update src/MSBuildLocator/DotNetSdkLocationHelper.cs Co-authored-by: Ladi Prosek --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index ce7f437b..4e347b5f 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -153,7 +153,8 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) // Get a list of hostfxr assembly directories (e.g., 6.0.3, 7.0.1-preview.2.4) IList hostFxrAssemblyDirs = Directory.GetDirectories(hostFxrRoot) .Select(path => SemanticVersionParser.TryParse(Path.GetFileName(path), out SemanticVersion? version) ? version : null) - .Select(v => v!) + .Where(v => v != null) + .Cast() .OrderByDescending(v => v) .ToList(); From 533b2967a9233535f1281b9656883e1d0919e050 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 14 Sep 2023 18:17:45 +0200 Subject: [PATCH 5/6] update warning message --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index ce7f437b..8587d821 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -226,7 +226,9 @@ private static IList ResolveDotnetPathCandidates() AddIfValid(GetDotnetPathFromPATH()); return pathCandidates.Count == 0 - ? throw new InvalidOperationException("Path to dotnet executable is not set. Make sure it is added either to DOTNET_HOST_PATH or PATH environment variable.") + ? 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) From 63259a23e3a8faaa025577589a08f4adea2b699d Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 15 Sep 2023 09:40:59 +0200 Subject: [PATCH 6/6] add new section to readme --- README.md | 14 ++++++++++++++ src/MSBuildLocator/DotNetSdkLocationHelper.cs | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd817e0c..6f9a58c2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ That additional build logic is distributed with Visual Studio, with Visual Studi Loading MSBuild from Visual Studio also ensures that your application gets the same view of projects as `MSBuild.exe`, `dotnet build`, or Visual Studio, including bug fixes, feature additions, and performance improvements that may come from a newer MSBuild release. +## How Locator searches for .NET SDK? + +MSBuild.Locator searches for the locally installed SDK based on the following priority: + +1. DOTNET_ROOT +2. Current process path if MSBuild.Locator is called from dotnet.exe +3. DOTNET_HOST_PATH +4. DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR +5. PATH + +Note that probing stops when the first dotnet executable is found among the listed variables. + +Documentation describing the definition of these variables can be found here: [.NET Environment Variables](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables). + ## Documentation Documentation is located on the official Microsoft documentation site: [Use Microsoft.Build.Locator](https://docs.microsoft.com/visualstudio/msbuild/updating-an-existing-application#use-microsoftbuildlocator). diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index e42c10fd..f8da9c3d 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -176,7 +176,6 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) return IntPtr.Zero; } - 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."; ///