Skip to content

Commit 6bbaaa5

Browse files
Modernize DotNetSdkLocationHelper and hostfxr interop. (#339)
* Use framework API to resolve symlink. * Refactor hostfxr bindings and use the P/Invoke source generator. * Use newer APIs in `DotNetSdkLocationHelper`. * Use custom marshaller for auto-encoding strings. * Remove BOM. * Remove assertions inside native callbacks. * Some more cleanups.
1 parent ba5ddc1 commit 6bbaaa5

File tree

3 files changed

+111
-60
lines changed

3 files changed

+111
-60
lines changed

src/MSBuildLocator/DotNetSdkLocationHelper.cs

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717

1818
namespace Microsoft.Build.Locator
1919
{
20-
internal static class DotNetSdkLocationHelper
20+
internal static partial class DotNetSdkLocationHelper
2121
{
22-
private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline);
23-
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
24-
private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet";
25-
private static readonly Lazy<IList<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());
22+
[GeneratedRegex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline)]
23+
private static partial Regex VersionRegex();
24+
25+
private static string ExeName => OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet";
26+
private static readonly Lazy<List<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());
2627

2728
public static VisualStudioInstance? GetInstance(string dotNetSdkPath, bool allowQueryAllRuntimeVersions)
2829
{
@@ -38,7 +39,7 @@ internal static class DotNetSdkLocationHelper
3839
}
3940

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

4344
if (!versionMatch.Success)
4445
{
@@ -116,10 +117,9 @@ public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirec
116117
static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
117118
{
118119
bool foundSdks = false;
119-
string[]? resolvedPaths = null;
120120
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
121121
{
122-
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
122+
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, out string[]? resolvedPaths);
123123

124124
if (rc == 0 && resolvedPaths != null)
125125
{
@@ -150,13 +150,7 @@ static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
150150
string? resolvedSdk = null;
151151
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
152152
{
153-
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
154-
{
155-
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
156-
{
157-
resolvedSdk = value;
158-
}
159-
});
153+
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, out resolvedSdk, out _);
160154

161155
if (rc == 0)
162156
{
@@ -178,7 +172,7 @@ static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
178172
private static void ModifyUnmanagedDllResolver(Action<AssemblyLoadContext> resolverAction)
179173
{
180174
// For Windows hostfxr is loaded in the process.
181-
if (!IsWindows)
175+
if (!OperatingSystem.IsWindows())
182176
{
183177
var loadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
184178
if (loadContext != null)
@@ -197,9 +191,9 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
197191
}
198192

199193
string hostFxrLibName =
200-
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
194+
OperatingSystem.IsWindows() ?
201195
"hostfxr.dll" :
202-
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "libhostfxr.dylib" : "libhostfxr.so";
196+
OperatingSystem.IsMacOS() ? "libhostfxr.dylib" : "libhostfxr.so";
203197
string hostFxrRoot = string.Empty;
204198

205199
// Get the dotnet path candidates
@@ -237,12 +231,12 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
237231

238232
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.";
239233

240-
private static IList<string> ResolveDotnetPathCandidates()
234+
private static List<string> ResolveDotnetPathCandidates()
241235
{
242236
var pathCandidates = new List<string>();
243237
AddIfValid(GetDotnetPathFromROOT());
244238

245-
string? dotnetExePath = GetCurrentProcessPath();
239+
string? dotnetExePath = Environment.ProcessPath;
246240
bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
247241
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase);
248242

@@ -254,9 +248,9 @@ private static IList<string> ResolveDotnetPathCandidates()
254248
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
255249
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
256250
{
257-
if (!IsWindows)
251+
if (!OperatingSystem.IsWindows())
258252
{
259-
hostPath = realpath(hostPath) ?? hostPath;
253+
hostPath = File.ResolveLinkTarget(hostPath, true)?.FullName ?? hostPath;
260254
}
261255

262256
AddIfValid(Path.GetDirectoryName(hostPath));
@@ -289,8 +283,6 @@ void AddIfValid(string? path)
289283
return dotnetPath;
290284
}
291285

292-
private static string? GetCurrentProcessPath() => Environment.ProcessPath;
293-
294286
private static string? GetDotnetPathFromPATH()
295287
{
296288
string? dotnetPath = null;
@@ -314,19 +306,6 @@ void AddIfValid(string? path)
314306
return dotnetPath;
315307
}
316308

317-
/// <summary>
318-
/// This native method call determines the actual location of path, including
319-
/// resolving symbolic links.
320-
/// </summary>
321-
private static string? realpath(string path)
322-
{
323-
IntPtr ptr = NativeMethods.realpath(path, IntPtr.Zero);
324-
string? result = Marshal.PtrToStringAuto(ptr);
325-
NativeMethods.free(ptr);
326-
327-
return result;
328-
}
329-
330309
private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
331310
{
332311
string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
@@ -347,9 +326,9 @@ private static void SetEnvironmentVariableIfEmpty(string name, string value)
347326
string fullPathToDotnetFromRoot = Path.Combine(dotnetPath, ExeName);
348327
if (File.Exists(fullPathToDotnetFromRoot))
349328
{
350-
if (!IsWindows)
329+
if (!OperatingSystem.IsWindows())
351330
{
352-
fullPathToDotnetFromRoot = realpath(fullPathToDotnetFromRoot) ?? fullPathToDotnetFromRoot;
331+
fullPathToDotnetFromRoot = File.ResolveLinkTarget(fullPathToDotnetFromRoot, true)?.FullName ?? fullPathToDotnetFromRoot;
353332
return File.Exists(fullPathToDotnetFromRoot) ? Path.GetDirectoryName(fullPathToDotnetFromRoot) : null;
354333
}
355334

src/MSBuildLocator/Microsoft.Build.Locator.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<PackageTags>msbuildlocator;locator;buildlocator</PackageTags>
1515
<EnablePackageValidation>true</EnablePackageValidation>
1616
<PackageValidationBaselineVersion>1.6.1</PackageValidationBaselineVersion>
17+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1718
</PropertyGroup>
1819
<PropertyGroup Condition="'$(TargetFramework)'=='net46'">
1920
<DefineConstants>$(DefineConstants);FEATURE_VISUALSTUDIOSETUP</DefineConstants>
Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
#if NETCOREAPP
45
using System;
6+
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
58
using System.Runtime.InteropServices;
9+
using System.Runtime.InteropServices.Marshalling;
610

711
namespace Microsoft.Build.Locator
812
{
9-
internal class NativeMethods
13+
internal partial class NativeMethods
1014
{
1115
internal const string HostFxrName = "hostfxr";
1216

@@ -15,37 +19,104 @@ internal enum hostfxr_resolve_sdk2_flags_t
1519
disallow_prerelease = 0x1,
1620
};
1721

18-
internal enum hostfxr_resolve_sdk2_result_key_t
22+
private enum hostfxr_resolve_sdk2_result_key_t
1923
{
2024
resolved_sdk_dir = 0,
2125
global_json_path = 1,
2226
};
2327

24-
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)]
25-
internal delegate void hostfxr_resolve_sdk2_result_fn(
26-
hostfxr_resolve_sdk2_result_key_t key,
27-
string value);
28-
29-
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)]
30-
internal delegate void hostfxr_get_available_sdks_result_fn(
31-
int sdk_count,
32-
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
33-
string[] value);
28+
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)
29+
{
30+
Debug.Assert(t_resolve_sdk2_resolved_sdk_dir is null);
31+
Debug.Assert(t_resolve_sdk2_global_json_path is null);
32+
try
33+
{
34+
unsafe
35+
{
36+
int result = hostfxr_resolve_sdk2(exe_dir, working_dir, flags, &hostfxr_resolve_sdk2_callback);
37+
resolved_sdk_dir = t_resolve_sdk2_resolved_sdk_dir;
38+
global_json_path = t_resolve_sdk2_global_json_path;
39+
return result;
40+
}
41+
}
42+
finally
43+
{
44+
t_resolve_sdk2_resolved_sdk_dir = null;
45+
t_resolve_sdk2_global_json_path = null;
46+
}
47+
}
3448

35-
[DllImport(HostFxrName, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
36-
internal static extern int hostfxr_resolve_sdk2(
49+
[LibraryImport(HostFxrName, StringMarshallingCustomType = typeof(AutoStringMarshaller))]
50+
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
51+
private static unsafe partial int hostfxr_resolve_sdk2(
3752
string exe_dir,
3853
string working_dir,
3954
hostfxr_resolve_sdk2_flags_t flags,
40-
hostfxr_resolve_sdk2_result_fn result);
55+
delegate* unmanaged[Cdecl]<hostfxr_resolve_sdk2_result_key_t, void*, void> result);
56+
57+
[ThreadStatic]
58+
private static string t_resolve_sdk2_resolved_sdk_dir, t_resolve_sdk2_global_json_path;
59+
60+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
61+
private static unsafe void hostfxr_resolve_sdk2_callback(hostfxr_resolve_sdk2_result_key_t key, void* value)
62+
{
63+
string str = AutoStringMarshaller.ConvertToManaged(value);
64+
switch (key)
65+
{
66+
case hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir:
67+
t_resolve_sdk2_resolved_sdk_dir = str;
68+
break;
69+
case hostfxr_resolve_sdk2_result_key_t.global_json_path:
70+
t_resolve_sdk2_global_json_path = str;
71+
break;
72+
}
73+
}
74+
75+
internal static int hostfxr_get_available_sdks(string exe_dir, out string[] sdks)
76+
{
77+
Debug.Assert(t_get_available_sdks_result is null);
78+
try
79+
{
80+
unsafe
81+
{
82+
int result = hostfxr_get_available_sdks(exe_dir, &hostfxr_get_available_sdks_callback);
83+
sdks = t_get_available_sdks_result;
84+
return result;
85+
}
86+
}
87+
finally
88+
{
89+
t_get_available_sdks_result = null;
90+
}
91+
}
92+
93+
[LibraryImport(HostFxrName, StringMarshallingCustomType = typeof(AutoStringMarshaller))]
94+
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
95+
private static unsafe partial int hostfxr_get_available_sdks(string exe_dir, delegate* unmanaged[Cdecl]<int, void**, void> result);
96+
97+
[ThreadStatic]
98+
private static string[] t_get_available_sdks_result;
4199

42-
[DllImport(HostFxrName, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
43-
internal static extern int hostfxr_get_available_sdks(string exe_dir, hostfxr_get_available_sdks_result_fn result);
100+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
101+
private static unsafe void hostfxr_get_available_sdks_callback(int count, void** sdks)
102+
{
103+
string[] result = new string[count];
104+
for (int i = 0; i < count; i++)
105+
{
106+
result[i] = AutoStringMarshaller.ConvertToManaged(sdks[i]);
107+
}
108+
t_get_available_sdks_result = result;
109+
}
110+
111+
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(AutoStringMarshaller))]
112+
internal static unsafe class AutoStringMarshaller
113+
{
114+
public static void* ConvertToUnmanaged(string s) => (void*)Marshal.StringToCoTaskMemAuto(s);
44115

45-
[DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
46-
internal static extern IntPtr realpath(string path, IntPtr buffer);
116+
public static void Free(void* ptr) => Marshal.FreeCoTaskMem((nint)ptr);
47117

48-
[DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
49-
internal static extern void free(IntPtr ptr);
118+
public static string ConvertToManaged(void* ptr) => Marshal.PtrToStringAuto((nint)ptr);
119+
}
50120
}
51121
}
122+
#endif

0 commit comments

Comments
 (0)