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/samples/BuilderApp/BuilderApp.csproj b/samples/BuilderApp/BuilderApp.csproj index 9035d7de..e15c0d4e 100644 --- a/samples/BuilderApp/BuilderApp.csproj +++ b/samples/BuilderApp/BuilderApp.csproj @@ -28,4 +28,6 @@ Not necessary if you use the package! --> + + diff --git a/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj b/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj index 4c8d0abf..8edfc2cd 100644 --- a/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj +++ b/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 1dec2c49..4d7cc0a3 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -22,7 +22,7 @@ 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 DotnetPath = new(() => ResolveDotnetPath()); + private static readonly Lazy> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates()); public static VisualStudioInstance? GetInstance(string dotNetSdkPath) { @@ -141,22 +141,31 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "hostfxr.dll" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "libhostfxr.dylib" : "libhostfxr.so"; + string hostFxrRoot = string.Empty; - string hostFxrRoot = Path.Combine(DotnetPath.Value, "host", "fxr"); - if (Directory.Exists(hostFxrRoot)) + // Get the dotnet path candidates + foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - var fileEnumerable = new FileSystemEnumerable( - directory: hostFxrRoot, - transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null) + hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr"); + if (Directory.Exists(hostFxrRoot)) { - ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory - }; + var fileEnumerable = new FileSystemEnumerable( + directory: hostFxrRoot, + transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null) + { + ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory + }; - // Load hostfxr from the highest version, because it should be backward-compatible - if (fileEnumerable.Max() is SemanticVersion hostFxrVersion) - { - string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName); - return NativeLibrary.Load(hostFxrAssembly); + 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; + } + } } } @@ -176,68 +185,69 @@ 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: 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; + } + }); - if (rc != 0) - { - throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2))); + if (rc == 0) + { + SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName)); + return resolvedSdk; + } } - return resolvedSdk; + 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(); + AddIfValid(GetDotnetPathFromROOT()); + + string? dotnetExePath = GetCurrentProcessPath(); + bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) + && Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase); + + if (isRunFromDotnetExecutable) + { + AddIfValid(Path.GetDirectoryName(dotnetExePath)); + } - if (string.IsNullOrEmpty(dotnetPath)) + string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath)) { - string? dotnetExePath = GetCurrentProcessPath(); - var isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) - && Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.OrdinalIgnoreCase); - - if (isRunFromDotnetExecutable) + if (!IsWindows) { - dotnetPath = Path.GetDirectoryName(dotnetExePath); + hostPath = realpath(hostPath) ?? hostPath; } - else - { - // DOTNET_HOST_PATH is pointing to the file, DOTNET_ROOT is the path of the folder - string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); - if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath)) - { - if (!IsWindows) - { - hostPath = realpath(hostPath) ?? hostPath; - } - - dotnetPath = Path.GetDirectoryName(hostPath); - if (dotnetPath is not null) - { - // don't overwrite DOTNET_HOST_PATH, if we use it. - return dotnetPath; - } - } - dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR") - ?? GetDotnetPathFromPATH(); - } + AddIfValid(Path.GetDirectoryName(hostPath)); } - if (string.IsNullOrEmpty(dotnetPath)) - { - throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?"); - } + AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR")); + AddIfValid(GetDotnetPathFromPATH()); - SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName)); + 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; - return dotnetPath; + void AddIfValid(string? path) + { + if (!string.IsNullOrEmpty(path)) + { + pathCandidates.Add(path); + } + } } private static string? GetDotnetPathFromROOT() @@ -280,15 +290,18 @@ private static string ResolveDotnetPath() private static string[] GetAllAvailableSDKs() { string[]? resolvedPaths = null; - int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: DotnetPath.Value, result: (key, value) => resolvedPaths = value); - - // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. - if (rc != 0) + foreach (string dotnetPath in s_dotnetPathCandidates.Value) { - throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))); + int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value); + + if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0) + { + break; + } } - return resolvedPaths ?? Array.Empty(); + // 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))); } /// diff --git a/src/MSBuildLocator/Microsoft.Build.Locator.csproj b/src/MSBuildLocator/Microsoft.Build.Locator.csproj index 056f7ca5..668ae489 100644 --- a/src/MSBuildLocator/Microsoft.Build.Locator.csproj +++ b/src/MSBuildLocator/Microsoft.Build.Locator.csproj @@ -14,6 +14,9 @@ MSBuild Locator Package that assists in locating and using a copy of MSBuild installed as part of Visual Studio 2017 or higher or .NET Core SDK 2.1 or higher. + + true + 1.6.1 diff --git a/src/MSBuildLocator/build/Microsoft.Build.Locator.targets b/src/MSBuildLocator/build/Microsoft.Build.Locator.targets index f17cd107..a117f136 100644 --- a/src/MSBuildLocator/build/Microsoft.Build.Locator.targets +++ b/src/MSBuildLocator/build/Microsoft.Build.Locator.targets @@ -3,8 +3,7 @@