-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Fix hostfxr resolution failure on musl when HOSTFXR_PATH is not set
#55270
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
Changes from 5 commits
0d83a22
9238c3f
8345a54
c38bdd3
00341c3
436ebaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| // 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. | ||
| /// <para> | ||
| /// This file is source-shared (via <c><Compile Include></c>) into both the | ||
| /// <c>Microsoft.DotNet.NativeWrapper</c> resolver and the Native AOT <c>dn</c> host so | ||
| /// the two stay in lockstep; a prior divergence between separate copies is what caused | ||
| /// https://github.com/dotnet/sdk/issues/55238 to be fixed in only one place. | ||
| /// </para> | ||
| /// </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(); | ||
|
|
||
| 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET | ||
| 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 the source-shared | ||
| /// <see cref="HostFxrPathResolver"/> (under <c>src/Common</c>) so this resolver and the | ||
| /// Native AOT <c>dn</c> host stay in lockstep. | ||
| /// </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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,24 +180,126 @@ public void ResolveHostfxrPath_PicksHighestVersion() | |
| } | ||
|
|
||
| [TestMethod] | ||
| public void ResolveHostfxrPath_SkipsPrereleaseDirectories() | ||
| public void ResolveHostfxrPath_FindsPrereleaseVersionDirectory() | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reviewers: Note this change in behavior. It wasn't clear to me that skipping prerelease versions was intentional or just a bug that AI created a test case to match its behavior. So I'm proposing that prerelease versions be handled.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting catch - I think it's reasonable to support preview versions of hostfxr, but I think it might be most-compatible if the hostfxr location took the 'allow prerelease' setting into account, via:
Actually, it might be best to align with the way the runtime locates hostfxr - @elinor-fung do you know how/when the runtime will look for preview hostfxrs?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked the native host's
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome, thanks for doing that! |
||
| { | ||
| // On a preview-only install, host/fxr contains a single prerelease-named | ||
| // directory (e.g. "10.0.0-preview.5"). Version.TryParse can't parse that string, | ||
| // so the numeric core must be used instead of skipping the directory. | ||
| string dotnetRoot = BuildPath(true, "dotnet"); | ||
| string fxrDir = Path.Combine(dotnetRoot, "host", "fxr"); | ||
| string preview = Path.Combine(fxrDir, "10.0.0-preview.5"); | ||
| string expectedPath = Path.Combine(preview, "hostfxr.dll"); | ||
|
|
||
| string result = DotnetRootResolver.ResolveHostfxrPath( | ||
| dotnetRoot: dotnetRoot, | ||
| isWindows: true, | ||
| isMacOS: false, | ||
| directoryExists: _ => true, | ||
| getDirectories: _ => new[] { preview }, | ||
| fileExists: _ => true); | ||
|
|
||
| Assert.AreEqual(expectedPath, result); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| 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 expectedPath = Path.Combine(v900, "hostfxr.dll"); | ||
| string preview = Path.Combine(fxrDir, "10.0.0-preview.5"); | ||
| string expectedPath = Path.Combine(preview, "hostfxr.dll"); | ||
|
|
||
| string result = DotnetRootResolver.ResolveHostfxrPath( | ||
| dotnetRoot: dotnetRoot, | ||
| isWindows: true, | ||
| isMacOS: false, | ||
| directoryExists: _ => true, | ||
| getDirectories: _ => new[] { v900, preview }, | ||
| fileExists: _ => true); | ||
|
|
||
| Assert.AreEqual(expectedPath, result); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ResolveHostfxrPath_PrefersStableOverPrereleaseOfSameCore() | ||
| { | ||
| string dotnetRoot = BuildPath(true, "dotnet"); | ||
| string fxrDir = Path.Combine(dotnetRoot, "host", "fxr"); | ||
| string stable = Path.Combine(fxrDir, "10.0.0"); | ||
| string preview = Path.Combine(fxrDir, "10.0.0-preview.5"); | ||
| string expectedPath = Path.Combine(stable, "hostfxr.dll"); | ||
|
|
||
| string result = DotnetRootResolver.ResolveHostfxrPath( | ||
| dotnetRoot: dotnetRoot, | ||
| isWindows: true, | ||
| isMacOS: false, | ||
| directoryExists: _ => true, | ||
| getDirectories: _ => new[] { preview, stable }, | ||
| fileExists: _ => true); | ||
|
|
||
| Assert.AreEqual(expectedPath, result); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ResolveHostfxrPath_TreatsBuildMetadataAsStable() | ||
| { | ||
| string dotnetRoot = BuildPath(true, "dotnet"); | ||
| string fxrDir = Path.Combine(dotnetRoot, "host", "fxr"); | ||
| string stable = Path.Combine(fxrDir, "10.0.0+build.1"); | ||
| string preview = Path.Combine(fxrDir, "10.0.0-preview.5"); | ||
| string expectedPath = Path.Combine(stable, "hostfxr.dll"); | ||
|
|
||
| string result = DotnetRootResolver.ResolveHostfxrPath( | ||
| dotnetRoot: dotnetRoot, | ||
| isWindows: true, | ||
| isMacOS: false, | ||
| directoryExists: _ => true, | ||
| getDirectories: _ => new[] { preview, stable }, | ||
| fileExists: _ => true); | ||
|
|
||
| Assert.AreEqual(expectedPath, result); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ResolveHostfxrPath_OrdersPrereleaseSegmentsNumerically() | ||
| { | ||
| // "preview.10" must sort after "preview.6"; a plain ordinal compare gets this wrong. | ||
| string dotnetRoot = BuildPath(true, "dotnet"); | ||
| string fxrDir = Path.Combine(dotnetRoot, "host", "fxr"); | ||
| string preview6 = Path.Combine(fxrDir, "10.0.0-preview.6"); | ||
| string preview10 = Path.Combine(fxrDir, "10.0.0-preview.10"); | ||
| string expectedPath = Path.Combine(preview10, "hostfxr.dll"); | ||
|
|
||
| string result = DotnetRootResolver.ResolveHostfxrPath( | ||
| dotnetRoot: dotnetRoot, | ||
| isWindows: true, | ||
| isMacOS: false, | ||
| directoryExists: _ => true, | ||
| getDirectories: _ => new[] { preview6, preview10 }, | ||
| fileExists: _ => true); | ||
|
|
||
| Assert.AreEqual(expectedPath, result); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ResolveHostfxrPath_SkipsInvalidSemanticVersions() | ||
| { | ||
| string dotnetRoot = BuildPath(true, "dotnet"); | ||
| string fxrDir = Path.Combine(dotnetRoot, "host", "fxr"); | ||
| string twoPartVersion = Path.Combine(fxrDir, "99.0"); | ||
| string leadingZero = Path.Combine(fxrDir, "98.0.0-preview.01"); | ||
| string valid = Path.Combine(fxrDir, "10.0.0"); | ||
| string expectedPath = Path.Combine(valid, "hostfxr.dll"); | ||
|
|
||
| // Version.TryParse fails for prerelease strings like "10.0.0-preview.5" | ||
| string result = DotnetRootResolver.ResolveHostfxrPath( | ||
| dotnetRoot: dotnetRoot, | ||
| isWindows: true, | ||
| isMacOS: false, | ||
| directoryExists: _ => true, | ||
| getDirectories: _ => new[] { preview, v900 }, | ||
| getDirectories: _ => new[] { twoPartVersion, leadingZero, valid }, | ||
| fileExists: _ => true); | ||
|
|
||
| // Should pick 9.0.0 since the preview dir is skipped | ||
| Assert.AreEqual(expectedPath, result); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.