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 @@