Skip to content
Merged
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
74 changes: 74 additions & 0 deletions src/Common/HostFxrPathResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using NuGet.Versioning;

namespace Microsoft.DotNet.HostFxr;

/// <summary>
/// Locates the native <c>hostfxr</c> library underneath a .NET installation root.
/// This is used as a fallback when the host does not publish the <c>HOSTFXR_PATH</c>
/// runtime property (for example when the SDK is launched via <c>dotnet exec dotnet.dll</c>
/// rather than as a first-class SDK command, which is how the <c>dnx</c> script works).
/// Dependencies are injected so the pure path-resolution logic can be unit tested.
/// </summary>
internal static class HostFxrPathResolver
{
/// <summary>
/// Finds the highest-versioned <c>hostfxr</c> library under
/// <paramref name="dotnetRoot"/>/host/fxr, returning its full path or
/// <see cref="string.Empty"/> if it cannot be found.
/// </summary>
internal static string ResolveHostFxrPath(
string? dotnetRoot,
bool isWindows,
bool isMacOS,
Func<string, bool> directoryExists,
Func<string, string[]> getDirectories,
Func<string, bool> fileExists)
{
if (string.IsNullOrEmpty(dotnetRoot))
{
return string.Empty;
}

string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
if (!directoryExists(fxrDir))
{
return string.Empty;
}

// Match the native host's get_latest_fxr behavior: consider every valid
// semantic version, including prerelease versions, and select the highest.
// SDK and runtime roll-forward settings apply after hostfxr is loaded and
// do not affect hostfxr selection.
string? latestFxr = getDirectories(fxrDir)
.Select(path =>
{
SemanticVersion.TryParse(Path.GetFileName(path), out SemanticVersion? version);
return new
{
Path = path,
Version = version
};
})
.Where(candidate => candidate.Version is not null)
.OrderByDescending(candidate => candidate.Version)
.Select(candidate => candidate.Path)
.FirstOrDefault();
Comment thread
mthalman marked this conversation as resolved.

if (latestFxr is null)
{
return string.Empty;
}

string hostfxrName = isWindows
? "hostfxr.dll"
: isMacOS
? "libhostfxr.dylib"
: "libhostfxr.so";

string hostfxrPath = Path.Combine(latestFxr, hostfxrName);
return fileExists(hostfxrPath) ? hostfxrPath : string.Empty;
}
}
50 changes: 50 additions & 0 deletions src/Resolvers/Microsoft.DotNet.NativeWrapper/HostFxrLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NETCOREAPP
using Microsoft.DotNet.HostFxr;

namespace Microsoft.DotNet.NativeWrapper
{
/// <summary>
/// NativeWrapper-facing entry point for locating the native <c>hostfxr</c> library.
/// The actual path-resolution logic lives in <see cref="HostFxrPathResolver"/>
/// under <c>src/Common</c>.
/// </summary>
internal static class HostFxrLocator
{
/// <summary>
/// Finds the highest-versioned <c>hostfxr</c> library under
/// <paramref name="dotnetRoot"/>/host/fxr, returning its full path or
/// <see cref="string.Empty"/> if it cannot be found.
/// </summary>
internal static string ResolveHostFxrPath(
string? dotnetRoot,
bool isWindows,
bool isMacOS,
Func<string, bool> directoryExists,
Func<string, string[]> getDirectories,
Func<string, bool> fileExists)
=> HostFxrPathResolver.ResolveHostFxrPath(
dotnetRoot,
isWindows,
isMacOS,
directoryExists,
getDirectories,
fileExists);

/// <summary>
/// Resolves the <c>hostfxr</c> path against the live filesystem, deriving the .NET
/// installation root from the current process (the running <c>dotnet</c> host).
/// </summary>
internal static string ResolveHostFxrPath()
=> ResolveHostFxrPath(
EnvironmentProvider.GetDotnetExeDirectory(),
isWindows: OperatingSystem.IsWindows(),
isMacOS: OperatingSystem.IsMacOS(),
directoryExists: Directory.Exists,
getDirectories: Directory.GetDirectories,
fileExists: File.Exists);
}
}
#endif
18 changes: 15 additions & 3 deletions src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,26 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
return IntPtr.Zero;
}

if (string.IsNullOrEmpty(HostFxrPath))
string? hostFxrPath = HostFxrPath;

// The host only publishes the HOSTFXR_PATH runtime property for first-class SDK
// commands (e.g. `dotnet build`). When the SDK is launched via `dotnet exec dotnet.dll`
// (as the `dnx` script does), the property is absent. On glibc the bare `libhostfxr`
// load succeeds against the already-loaded library so this resolver never runs, but on
// musl it does not, so fall back to locating hostfxr under the running .NET root.
if (string.IsNullOrEmpty(hostFxrPath))
{
hostFxrPath = HostFxrLocator.ResolveHostFxrPath();
}

if (string.IsNullOrEmpty(hostFxrPath))
{
throw new HostFxrRuntimePropertyNotSetException();
}

if (!NativeLibrary.TryLoad(HostFxrPath, out var handle))
if (!NativeLibrary.TryLoad(hostFxrPath, out var handle))
{
throw new HostFxrNotFoundException(HostFxrPath);
throw new HostFxrNotFoundException(hostFxrPath);
}

return handle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

<ItemGroup>
<PackageReference Include="Microsoft.Deployment.DotNet.Releases" />
<PackageReference Include="NuGet.Versioning" Condition="'$(TargetFramework)' != 'net472'" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)src\Common\HostFxrPathResolver.cs" LinkBase="Common" Condition="'$(TargetFramework)' != 'net472'" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="dotnet.Tests" PublicKey="0024000004800000940000000602000000240000525341310004000001000100F33A29044FA9D740C9B3213A93E57C84B472C84E0B8A0E1AE48E67A9F8F6DE9D5F7F3D52AC23E48AC51801F1DC950ABE901DA34D2A9E3BAADB141A17C77EF3C565DD5EE5054B91CF63BB3C6AB83F72AB3AAFE93D0FC3C2348B764FAFB0B1C0733DE51459AEAB46580384BF9D74C4E28164B7CDE247F891BA07891C9D872AD2BB" />
</ItemGroup>

</Project>
227 changes: 227 additions & 0 deletions test/dotnet.Tests/NativeWrapperTests/HostFxrLocatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.NativeWrapper;

namespace Microsoft.DotNet.Cli.Tests;

public class HostFxrLocatorTests
{
private static string BuildPath(bool isWindows, params string[] segments)
{
string root = isWindows ? @"C:\" : "/";
return segments.Length == 0 ? root : Path.Combine(root, Path.Combine(segments));
}

[Fact]
public void ResolveHostFxrPath_WithValidFxrDir_ReturnsPath()
{
string dotnetRoot = BuildPath(true, "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string fxrVersion = Path.Combine(fxrDir, "11.0.0");
string expectedPath = Path.Combine(fxrVersion, "hostfxr.dll");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: true,
isMacOS: false,
directoryExists: path => path == fxrDir,
getDirectories: _ => [fxrVersion],
fileExists: path => path == expectedPath);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_PicksHighestVersion()
{
string dotnetRoot = BuildPath(true, "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string v800 = Path.Combine(fxrDir, "8.0.0");
string v901 = Path.Combine(fxrDir, "9.0.1");
string v900 = Path.Combine(fxrDir, "9.0.0");
string expectedPath = Path.Combine(v901, "hostfxr.dll");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [v800, v901, v900],
fileExists: _ => true);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_NullOrEmptyDotnetRoot_ReturnsEmpty()
{
string resultNull = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: null,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [],
fileExists: _ => true);

string resultEmpty = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: string.Empty,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [],
fileExists: _ => true);

resultNull.Should().BeEmpty();
resultEmpty.Should().BeEmpty();
}

[Fact]
public void ResolveHostFxrPath_MissingFxrDirectory_ReturnsEmpty()
{
string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: BuildPath(true, "dotnet"),
isWindows: true,
isMacOS: false,
directoryExists: _ => false,
getDirectories: _ => [],
fileExists: _ => false);

result.Should().BeEmpty();
}

[Fact]
public void ResolveHostFxrPath_FxrDirExistsButNoHostfxrFile_ReturnsEmpty()
{
string dotnetRoot = BuildPath(true, "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string fxrVersion = Path.Combine(fxrDir, "9.0.0");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [fxrVersion],
fileExists: _ => false);

result.Should().BeEmpty();
}

[Fact]
public void ResolveHostFxrPath_OnMacOS_LooksForDylib()
{
string dotnetRoot = Path.Combine("/", "usr", "local", "share", "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string fxrVersion = Path.Combine(fxrDir, "9.0.0");
string expectedPath = Path.Combine(fxrVersion, "libhostfxr.dylib");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: false,
isMacOS: true,
directoryExists: path => path == fxrDir,
getDirectories: _ => [fxrVersion],
fileExists: path => path == expectedPath);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_OnLinux_LooksForSo()
{
string dotnetRoot = Path.Combine("/", "usr", "share", "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string fxrVersion = Path.Combine(fxrDir, "9.0.0");
string expectedPath = Path.Combine(fxrVersion, "libhostfxr.so");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: false,
isMacOS: false,
directoryExists: path => path == fxrDir,
getDirectories: _ => [fxrVersion],
fileExists: path => path == expectedPath);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_FindsPrereleaseVersionDirectory()
{
string dotnetRoot = Path.Combine("/", "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string fxrVersion = Path.Combine(fxrDir, "11.0.0-preview.6.26359.118");
string expectedPath = Path.Combine(fxrVersion, "libhostfxr.so");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: false,
isMacOS: false,
directoryExists: path => path == fxrDir,
getDirectories: _ => [fxrVersion],
fileExists: path => path == expectedPath);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_PicksHighestVersion_IncludingPrerelease()
{
string dotnetRoot = BuildPath(true, "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string v900 = Path.Combine(fxrDir, "9.0.0");
string v11Preview = Path.Combine(fxrDir, "11.0.0-preview.6.26359.118");
string expectedPath = Path.Combine(v11Preview, "hostfxr.dll");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [v900, v11Preview],
fileExists: _ => true);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_PrefersStableOverPrereleaseOfSameCore()
{
string dotnetRoot = BuildPath(true, "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string stable = Path.Combine(fxrDir, "11.0.0");
string preview = Path.Combine(fxrDir, "11.0.0-preview.6.26359.118");
string expectedPath = Path.Combine(stable, "hostfxr.dll");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [preview, stable],
fileExists: _ => true);

result.Should().Be(expectedPath);
}

[Fact]
public void ResolveHostFxrPath_OrdersPrereleaseSegmentsNumerically()
{
string dotnetRoot = BuildPath(true, "dotnet");
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
string preview6 = Path.Combine(fxrDir, "11.0.0-preview.6.26359.118");
string preview10 = Path.Combine(fxrDir, "11.0.0-preview.10.26400.1");
string expectedPath = Path.Combine(preview10, "hostfxr.dll");

string result = HostFxrLocator.ResolveHostFxrPath(
dotnetRoot: dotnetRoot,
isWindows: true,
isMacOS: false,
directoryExists: _ => true,
getDirectories: _ => [preview6, preview10],
fileExists: _ => true);

result.Should().Be(expectedPath);
}
}
Loading
Loading