Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add probing of all available path candidates + return missed DOTNET_HOST_PATH set change #243

121 changes: 85 additions & 36 deletions src/MSBuildLocator/DotNetSdkLocationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> s_dotnetPath = new(() => ResolveDotnetPath());
private static readonly Lazy<IList<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());

public static VisualStudioInstance? GetInstance(string dotNetSdkPath)
{
Expand Down Expand Up @@ -131,35 +131,52 @@ private static void ModifyUnmanagedDllResolver(Action<AssemblyLoadContext> 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;
}

string hostFxrRoot = Path.Combine(s_dotnetPath.Value, "host", "fxr");
if (Directory.Exists(hostFxrRoot))
// Get the dotnet path candidates
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);
string hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr");

if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue))
// Check if the host/fxr directory exists
if (Directory.Exists(hostFxrRoot))
{
string hostFxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension));

if (File.Exists(hostFxrAssembly))
// Get a list of hostfxr assembly directories (e.g., 6.0.3, 7.0.1-preview.2.4)
IList<SemanticVersion> hostFxrAssemblyDirs = Directory.GetDirectories(hostFxrRoot)
.Select(path => SemanticVersionParser.TryParse(Path.GetFileName(path), out SemanticVersion? version) ? version : null)
.Where(v => v != null)
.Cast<SemanticVersion>()
.OrderByDescending(v => v)
.ToList();

foreach (SemanticVersion hostFxrDir in hostFxrAssemblyDirs)
{
return NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle) ? handle : IntPtr.Zero;
string hostFxrAssemblyPath = Path.Combine(hostFxrRoot, hostFxrDir.OriginalValue, $"{hostFxrLibName}.{libExtension}");

if (File.Exists(hostFxrAssemblyPath))
{
if (NativeLibrary.TryLoad(hostFxrAssemblyPath, out IntPtr handle))
{
return handle;
}
}
}
}
}

return IntPtr.Zero;
}


YuliiaKovalova marked this conversation as resolved.
Show resolved Hide resolved
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.";

/// <summary>
Expand All @@ -169,39 +186,57 @@ 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)
{
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", dotnetPath);
return resolvedSdk;
}
}

return string.IsNullOrEmpty(resolvedSdk)
YuliiaKovalova marked this conversation as resolved.
Show resolved Hide resolved
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
: resolvedSdk;
}

private static string ResolveDotnetPath()
private static IList<string> ResolveDotnetPathCandidates()
{
string? dotnetPath = GetDotnetPathFromROOT();
var pathCandidates = new List<string>();
AddIfValid(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();
AddIfValid(Path.GetDirectoryName(dotnetExePath));
}

return string.IsNullOrEmpty(dotnetPath)
? throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?")
: dotnetPath;
AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH"));
AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"));
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.")
YuliiaKovalova marked this conversation as resolved.
Show resolved Hide resolved
: pathCandidates;

void AddIfValid(string? path)
{
if (!string.IsNullOrEmpty(path))
{
pathCandidates.Add(path);
}
}
}

private static string? GetDotnetPathFromROOT()
Expand Down Expand Up @@ -244,12 +279,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
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)))
: resolvedPaths ?? Array.Empty<string>();
return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
}

/// <summary>
Expand All @@ -265,6 +306,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);
Expand Down
Loading