Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 28 additions & 43 deletions src/MSBuildLocator/DotNetSdkLocationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Text.RegularExpressions;

#nullable enable

namespace Microsoft.Build.Locator
{
internal static class DotNetSdkLocationHelper
internal static partial 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<IList<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());
[GeneratedRegex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline)]
private static partial Regex VersionRegex();

private static string ExeName => OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet";
private static readonly Lazy<List<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());

public static VisualStudioInstance? GetInstance(string dotNetSdkPath, bool allowQueryAllRuntimeVersions)
{
Expand All @@ -38,7 +40,7 @@ internal static class DotNetSdkLocationHelper
}

// Preview versions contain a hyphen after the numeric part of the version. Version.TryParse doesn't accept that.
Match versionMatch = VersionRegex.Match(File.ReadAllText(versionPath));
Match versionMatch = VersionRegex().Match(File.ReadAllText(versionPath));

if (!versionMatch.Success)
{
Expand Down Expand Up @@ -116,12 +118,13 @@ public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirec
static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
{
bool foundSdks = false;
string[]? resolvedPaths = null;
int rc = 0;
StringBuilder? errorMessage = null;
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, out string[]? resolvedPaths, out errorMessage);

if (rc == 0 && resolvedPaths != null)
if (resolvedPaths != null)
{
foundSdks = true;

Expand All @@ -140,23 +143,19 @@ static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
// 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)));
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks), rc, errorMessage));
}
}

// Determines the directory location of the SDK accounting for global.json and multi-level lookup policy.
static string? GetSdkFromGlobalSettings(string workingDirectory)
{
string? resolvedSdk = null;
int rc = 0;
StringBuilder? errorMessage = 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;
}
});
rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, out resolvedSdk, out _, out errorMessage);

if (rc == 0)
{
Expand All @@ -166,7 +165,7 @@ static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
}

return string.IsNullOrEmpty(resolvedSdk)
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2), rc, errorMessage))
: resolvedSdk;
}
}
Expand All @@ -178,7 +177,7 @@ static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
private static void ModifyUnmanagedDllResolver(Action<AssemblyLoadContext> resolverAction)
{
// For Windows hostfxr is loaded in the process.
if (!IsWindows)
if (!OperatingSystem.IsWindows())
{
var loadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
if (loadContext != null)
Expand All @@ -197,9 +196,9 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
}

string hostFxrLibName =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
OperatingSystem.IsWindows() ?
"hostfxr.dll" :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "libhostfxr.dylib" : "libhostfxr.so";
OperatingSystem.IsMacOS() ? "libhostfxr.dylib" : "libhostfxr.so";
string hostFxrRoot = string.Empty;

// Get the dotnet path candidates
Expand Down Expand Up @@ -235,14 +234,15 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
throw new InvalidOperationException(error);
}

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.";
private static string SdkResolutionExceptionMessage(string methodName, int rc, StringBuilder? errorMessage) =>
$"Error while calling hostfxr function {methodName}. Error code: {rc} Detailed error: {errorMessage}";

private static IList<string> ResolveDotnetPathCandidates()
private static List<string> ResolveDotnetPathCandidates()
{
var pathCandidates = new List<string>();
AddIfValid(GetDotnetPathFromROOT());

string? dotnetExePath = GetCurrentProcessPath();
string? dotnetExePath = Environment.ProcessPath;
bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase);

Expand All @@ -254,9 +254,9 @@ private static IList<string> ResolveDotnetPathCandidates()
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
{
if (!IsWindows)
if (!OperatingSystem.IsWindows())
{
hostPath = realpath(hostPath) ?? hostPath;
hostPath = File.ResolveLinkTarget(hostPath, true)?.FullName ?? hostPath;
}

AddIfValid(Path.GetDirectoryName(hostPath));
Expand Down Expand Up @@ -289,8 +289,6 @@ void AddIfValid(string? path)
return dotnetPath;
}

private static string? GetCurrentProcessPath() => Environment.ProcessPath;

private static string? GetDotnetPathFromPATH()
{
string? dotnetPath = null;
Expand All @@ -314,19 +312,6 @@ void AddIfValid(string? path)
return dotnetPath;
}

/// <summary>
/// This native method call determines the actual location of path, including
/// resolving symbolic links.
/// </summary>
private static string? realpath(string path)
{
IntPtr ptr = NativeMethods.realpath(path, IntPtr.Zero);
string? result = Marshal.PtrToStringAuto(ptr);
NativeMethods.free(ptr);

return result;
}

private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
{
string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
Expand All @@ -347,9 +332,9 @@ private static void SetEnvironmentVariableIfEmpty(string name, string value)
string fullPathToDotnetFromRoot = Path.Combine(dotnetPath, ExeName);
if (File.Exists(fullPathToDotnetFromRoot))
{
if (!IsWindows)
if (!OperatingSystem.IsWindows())
{
fullPathToDotnetFromRoot = realpath(fullPathToDotnetFromRoot) ?? fullPathToDotnetFromRoot;
fullPathToDotnetFromRoot = File.ResolveLinkTarget(fullPathToDotnetFromRoot, true)?.FullName ?? fullPathToDotnetFromRoot;
return File.Exists(fullPathToDotnetFromRoot) ? Path.GetDirectoryName(fullPathToDotnetFromRoot) : null;
}

Expand Down
1 change: 1 addition & 0 deletions src/MSBuildLocator/Microsoft.Build.Locator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageTags>msbuildlocator;locator;buildlocator</PackageTags>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.6.1</PackageValidationBaselineVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net46'">
<DefineConstants>$(DefineConstants);FEATURE_VISUALSTUDIOSETUP</DefineConstants>
Expand Down
158 changes: 137 additions & 21 deletions src/MSBuildLocator/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETCOREAPP
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Text;

namespace Microsoft.Build.Locator
{
internal class NativeMethods
internal partial class NativeMethods
{
internal const string HostFxrName = "hostfxr";

Expand All @@ -15,37 +20,148 @@ internal enum hostfxr_resolve_sdk2_flags_t
disallow_prerelease = 0x1,
};

internal enum hostfxr_resolve_sdk2_result_key_t
private enum hostfxr_resolve_sdk2_result_key_t
{
resolved_sdk_dir = 0,
global_json_path = 1,
};

[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)]
internal delegate void hostfxr_resolve_sdk2_result_fn(
hostfxr_resolve_sdk2_result_key_t key,
string value);

[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)]
internal delegate void hostfxr_get_available_sdks_result_fn(
hostfxr_resolve_sdk2_result_key_t key,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
string[] value);
internal static int hostfxr_resolve_sdk2(string exe_dir, string working_dir, hostfxr_resolve_sdk2_flags_t flags, out string resolved_sdk_dir, out string global_json_path, out StringBuilder errorMessage)
{
Debug.Assert(t_resolve_sdk2_resolved_sdk_dir is null);
Debug.Assert(t_resolve_sdk2_global_json_path is null);
try
{
unsafe
{
using var errorHandler = new ErrorHandler();
int result = hostfxr_resolve_sdk2(exe_dir, working_dir, flags, &hostfxr_resolve_sdk2_callback);
resolved_sdk_dir = t_resolve_sdk2_resolved_sdk_dir;
global_json_path = t_resolve_sdk2_global_json_path;
errorMessage = t_hostfxr_error_builder;
return result;
}
}
finally
{
t_resolve_sdk2_resolved_sdk_dir = null;
t_resolve_sdk2_global_json_path = null;
}
}

[DllImport(HostFxrName, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern int hostfxr_resolve_sdk2(
[LibraryImport(HostFxrName, StringMarshallingCustomType = typeof(AutoStringMarshaller))]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe partial int hostfxr_resolve_sdk2(
string exe_dir,
string working_dir,
hostfxr_resolve_sdk2_flags_t flags,
hostfxr_resolve_sdk2_result_fn result);
delegate* unmanaged[Cdecl]<hostfxr_resolve_sdk2_result_key_t, void*, void> result);

[ThreadStatic]
private static string t_resolve_sdk2_resolved_sdk_dir, t_resolve_sdk2_global_json_path;

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void hostfxr_resolve_sdk2_callback(hostfxr_resolve_sdk2_result_key_t key, void* value)
{
string str = AutoStringMarshaller.ConvertToManaged(value);
switch (key)
{
case hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir:
t_resolve_sdk2_resolved_sdk_dir = str;
break;
case hostfxr_resolve_sdk2_result_key_t.global_json_path:
t_resolve_sdk2_global_json_path = str;
break;
}
}

internal static int hostfxr_get_available_sdks(string exe_dir, out string[] sdks, out StringBuilder errorMessage)
{
Debug.Assert(t_get_available_sdks_result is null);
try
{
unsafe
{
using var errorHandler = new ErrorHandler();
int result = hostfxr_get_available_sdks(exe_dir, &hostfxr_get_available_sdks_callback);
sdks = t_get_available_sdks_result;
errorMessage = t_hostfxr_error_builder;
return result;
}
}
finally
{
t_get_available_sdks_result = null;
}
}

[LibraryImport(HostFxrName, StringMarshallingCustomType = typeof(AutoStringMarshaller))]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe partial int hostfxr_get_available_sdks(string exe_dir, delegate* unmanaged[Cdecl]<int, void**, void> result);

[ThreadStatic]
private static string[] t_get_available_sdks_result;

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void hostfxr_get_available_sdks_callback(int count, void** sdks)
{
string[] result = new string[count];
for (int i = 0; i < count; i++)
{
result[i] = AutoStringMarshaller.ConvertToManaged(sdks[i]);
}
t_get_available_sdks_result = result;
}

[LibraryImport(HostFxrName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe partial delegate* unmanaged[Cdecl]<void*, void> hostfxr_set_error_writer(delegate* unmanaged[Cdecl]<void*, void> error_writer);

[ThreadStatic]
private static StringBuilder t_hostfxr_error_builder;

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void hostfxr_error_writer_callback(void* message)
{
t_hostfxr_error_builder ??= new StringBuilder();
if (OperatingSystem.IsWindows())
{
// Avoid allocating temporary string on Windows.
t_hostfxr_error_builder.Append(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)message));
t_hostfxr_error_builder.AppendLine();
}
else
{
t_hostfxr_error_builder.AppendLine(Utf8StringMarshaller.ConvertToManaged((byte*)message));
}
}

[DllImport(HostFxrName, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern int hostfxr_get_available_sdks(string exe_dir, hostfxr_get_available_sdks_result_fn result);
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(AutoStringMarshaller))]
internal static unsafe class AutoStringMarshaller
{
public static void* ConvertToUnmanaged(string s) => (void*)Marshal.StringToCoTaskMemAuto(s);

public static void Free(void* ptr) => Marshal.FreeCoTaskMem((nint)ptr);

public static string ConvertToManaged(void* ptr) => Marshal.PtrToStringAuto((nint)ptr);
}

private unsafe readonly ref struct ErrorHandler
{
private readonly delegate* unmanaged[Cdecl]<void*, void> _previousCallback;

[DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr realpath(string path, IntPtr buffer);
public ErrorHandler()
{
Debug.Assert(t_hostfxr_error_builder is null);
_previousCallback = hostfxr_set_error_writer(&hostfxr_error_writer_callback);
}

[DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern void free(IntPtr ptr);
public void Dispose()
{
hostfxr_set_error_writer(_previousCallback);
t_hostfxr_error_builder = null;
}
}
}
}
#endif
2 changes: 1 addition & 1 deletion src/MSBuildLocator/build/Microsoft.Build.Locator.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<AutoGenerateBindingRedirects Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">true</AutoGenerateBindingRedirects>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading