diff --git a/src/Common/HostFxrPathResolver.cs b/src/Common/HostFxrPathResolver.cs
new file mode 100644
index 000000000000..99d9525b41f3
--- /dev/null
+++ b/src/Common/HostFxrPathResolver.cs
@@ -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;
+
+///
+/// Locates the native hostfxr library underneath a .NET installation root.
+/// This is used as a fallback when the host does not publish the HOSTFXR_PATH
+/// runtime property (for example when the SDK is launched via dotnet exec dotnet.dll
+/// rather than as a first-class SDK command, which is how the dnx script works).
+/// Dependencies are injected so the pure path-resolution logic can be unit tested.
+///
+internal static class HostFxrPathResolver
+{
+ ///
+ /// Finds the highest-versioned hostfxr library under
+ /// /host/fxr, returning its full path or
+ /// if it cannot be found.
+ ///
+ internal static string ResolveHostFxrPath(
+ string? dotnetRoot,
+ bool isWindows,
+ bool isMacOS,
+ Func directoryExists,
+ Func getDirectories,
+ Func 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();
+
+ 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;
+ }
+}
diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/HostFxrLocator.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/HostFxrLocator.cs
new file mode 100644
index 000000000000..33f1dd496d56
--- /dev/null
+++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/HostFxrLocator.cs
@@ -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
+{
+ ///
+ /// NativeWrapper-facing entry point for locating the native hostfxr library.
+ /// The actual path-resolution logic lives in
+ /// under src/Common.
+ ///
+ internal static class HostFxrLocator
+ {
+ ///
+ /// Finds the highest-versioned hostfxr library under
+ /// /host/fxr, returning its full path or
+ /// if it cannot be found.
+ ///
+ internal static string ResolveHostFxrPath(
+ string? dotnetRoot,
+ bool isWindows,
+ bool isMacOS,
+ Func directoryExists,
+ Func getDirectories,
+ Func fileExists)
+ => HostFxrPathResolver.ResolveHostFxrPath(
+ dotnetRoot,
+ isWindows,
+ isMacOS,
+ directoryExists,
+ getDirectories,
+ fileExists);
+
+ ///
+ /// Resolves the hostfxr path against the live filesystem, deriving the .NET
+ /// installation root from the current process (the running dotnet host).
+ ///
+ internal static string ResolveHostFxrPath()
+ => ResolveHostFxrPath(
+ EnvironmentProvider.GetDotnetExeDirectory(),
+ isWindows: OperatingSystem.IsWindows(),
+ isMacOS: OperatingSystem.IsMacOS(),
+ directoryExists: Directory.Exists,
+ getDirectories: Directory.GetDirectories,
+ fileExists: File.Exists);
+ }
+}
+#endif
diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs
index 93be3037a8c8..f0e9c6ce0caa 100644
--- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs
+++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs
@@ -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;
diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Microsoft.DotNet.NativeWrapper.csproj b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Microsoft.DotNet.NativeWrapper.csproj
index ec780cd24208..89a840912688 100644
--- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Microsoft.DotNet.NativeWrapper.csproj
+++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Microsoft.DotNet.NativeWrapper.csproj
@@ -6,6 +6,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/dotnet.Tests/NativeWrapperTests/HostFxrLocatorTests.cs b/test/dotnet.Tests/NativeWrapperTests/HostFxrLocatorTests.cs
new file mode 100644
index 000000000000..90fcfdb56388
--- /dev/null
+++ b/test/dotnet.Tests/NativeWrapperTests/HostFxrLocatorTests.cs
@@ -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);
+ }
+}
diff --git a/test/dotnet.Tests/dotnet.Tests.csproj b/test/dotnet.Tests/dotnet.Tests.csproj
index 99b8a5c81e4d..614eec41df38 100644
--- a/test/dotnet.Tests/dotnet.Tests.csproj
+++ b/test/dotnet.Tests/dotnet.Tests.csproj
@@ -73,6 +73,7 @@
+