Fix hostfxr resolution for .NET Framework tasks#85
Merged
Conversation
The net472 build of DotNet.ReproducibleBuilds.Isolated relies on Windows' default DLL search to locate hostfxr.dll. When VS or any other .NET Framework MSBuild host has not preloaded hostfxr, the P/Invoke in ValidateGlobalJsonSdkVersion throws DllNotFoundException. This adds a deterministic regression test: * tests/HostFxrProbe is a small net472 console app that loads the net472 task DLL by path and invokes hostfxr_set_error_writer via reflection. * HostFxrResolverTests spawns the probe with PATH stripped of any directory containing hostfxr.dll directly (only the dotnet install dir is kept on PATH so the resolver can locate dotnet.exe) and with all DOTNET_* discovery shortcuts cleared, then asserts exit code 0. The test currently fails with the exact DllNotFoundException reported in dotnet#79.
The task multi-targets net472 and net6.0. HostFxrResolver was wrapped in
`#if NET` so the net472 build had no resolver, and the
[DllImport("hostfxr")] in ValidateGlobalJsonSdkVersion fell through to
Windows' default DLL search. hostfxr.dll lives in a versioned
subdirectory of the dotnet install (`<dotnet>\host\fxr\<ver>\`), not on
PATH, so any MSBuild host that hadn't already preloaded hostfxr threw
DllNotFoundException at the first P/Invoke.
This change compiles HostFxrResolver into both TFMs and adds a .NET
Framework-specific code path that eagerly LoadLibraryW's the
highest-version hostfxr it can find via the existing probing logic.
On .NET the AssemblyLoadContext-based resolver is unchanged.
API surface (DllImport signatures, public types) is unchanged.
Fixes dotnet#79
Two defensive improvements that don't change the production fix:
* The probe now calls `GetModuleHandle("hostfxr.dll")` before loading
the task. If hostfxr is already mapped into the probe process
(e.g. via a future transitive dependency that pre-loads it), the probe
exits with a distinct code rather than silently letting the P/Invoke
bind to the pre-loaded module. Without this guard, the test could go
green without exercising HostFxrResolver at all.
* The probe-output collection target now explicitly DependsOnTargets on
ResolveProjectReferences, so the probe's bin dir is guaranteed to
exist before the wildcard expands on a clean checkout.
The original name conflated two ideas: `probe` suggested the project was discovering something about hostfxr (it isn't), and the project itself isn't really a probe but the smallest possible stand-in for an MSBuild host. Rename and reword the doc comments accordingly: * Folder/csproj: tests/HostFxrProbe -> tests/StubTaskHarness. * Class/namespace: HostFxrProbe -> StubTaskHarness. * MSBuild target name: _CollectHostFxrProbeOutputs -> _CollectStubTaskHarnessOutputs. * Output dir in test bin: probes/ -> harness/. * Source comments updated to `harness` where appropriate; the verb `probe` is kept where it correctly describes loader behavior. No behavior change.
The earlier `_CollectStubTaskHarnessOutputs` target globbed tests/StubTaskHarness/bin/$(Configuration)/net472/*.* and dropped those into the test bin via a <None CopyToOutputDirectory> entry. Two problems with that: * It bakes in the harness project's bin layout. If the harness ever sets BaseOutputPath, opts in to .NET 8+'s UseArtifactsOutput, or otherwise relocates its output, the glob silently misses and the harness/ dir in the test bin is empty. * Adding the harness outputs to @(None) with extra metadata can trip downstream batching targets that read %(TargetPath) across the whole None list. Switch to the MSBuild-native pattern: ask the harness project for its runtime closure via four output-group targets (primary, symbols, satellite DLLs, reference copy-locals) and use the Copy task to drop the results into $(OutDir)harness/. The <MSBuild> task call is synchronous, so the harness build is guaranteed complete before we query; SkipUnchangedFiles gives incremental-build behavior. Verified locally: clean build, no-op incremental build, full rebuild, and UseArtifactsOutput=true all produce the same 17-file harness/ directory and the test passes against the artifacts/bin/.../harness/ layout.
Upstream investigation across dotnet/sdk, dotnet/msbuild, microsoft/MSBuildLocator identified the actual mechanism for issue dotnet#79 and one real correctness fix: 1. The reason ValidateGlobalJsonSdkVersion's [DllImport("hostfxr")] fails inside VS 2022+'s IDE-hosted MSBuild (but not in dotnet build or command-line MSBuild.exe) is that VS's in-process MSBuild engine is compiled for .NET (8/9/10) and its SdkResolverService.ResolveSdk() contains a #if NET fast path (enabled by ChangeWave 17.10, see PR dotnet/msbuild#9335) that resolves in-box SDKs like Microsoft.NET.Sdk directly from MSBuildSDKsPath via DefaultSdkResolver. The plugin chain never runs, Microsoft.DotNet.MSBuildSdkResolver is never loaded, and the Interop.PreloadWindowsLibrary("hostfxr") call that would otherwise bring hostfxr into the process never fires. Standalone netfx MSBuild.exe lacks the fast path (it's net472), so its plugin chain runs and the preload happens; that's why CLI succeeds and VS IDE fails. Update HostFxrResolver's class remarks and the netfx-branch comment to describe this mechanism accurately rather than the previous "safety net" hand-wave. 2. Switch from LoadLibraryW to LoadLibraryExW with LOAD_WITH_ALTERED_SEARCH_PATH (0x8). hostfxr.dll has its own transitive dependencies (e.g. hostpolicy.dll in the same versioned directory). LOAD_WITH_ALTERED_SEARCH_PATH makes the loader resolve those deps from hostfxr's directory rather than the process default DLL search path - matching what Microsoft.DotNet.NativeWrapper.Interop does for the same reason. 3. Document the StubTaskHarness x64 coverage gaps in the csproj comment (VS 2019 x86 MSBuild, MSBuildTaskHost.exe, arm64-native MSBuild on arm64 Windows). All are acceptable misses for a regression-prevention test focused on the issue dotnet#79 scenario.
HostFxrResolverTests was inlining the OutputDataReceived/BeginOutputReadLine dance plus the PSI plumbing (UseShellExecute, RedirectStandard*, CreateNoWindow). Move that into a small ProcessHelpers.RunAndWaitForExit that returns the (ExitCode, Stdout, Stderr) tuple, so the test reads as the scenario it's exercising rather than the IPC mechanics that get there.
baronfel
approved these changes
Jun 17, 2026
baronfel
left a comment
Member
There was a problem hiding this comment.
This is annoying but comprehensive and seems correct - thanks for filling the gap!
This was referenced Jun 18, 2026
Open
This was referenced Jun 24, 2026
jas88
added a commit
to jas88/SynthEHR
that referenced
this pull request
Jun 25, 2026
Updated [DotNet.ReproducibleBuilds](https://github.com/dotnet/reproducible-builds) from 1.2.39 to 2.0.5. <details> <summary>Release notes</summary> _Sourced from [DotNet.ReproducibleBuilds's releases](https://github.com/dotnet/reproducible-builds/releases)._ ## 2.0.5 ## What's Changed * Update versions used in examples to be from latest major by @Frulfump in dotnet/reproducible-builds#80 * Fix hostfxr resolution for .NET Framework tasks by @MattKotsenas in dotnet/reproducible-builds#85 * Upgrade MSBuild.ProjectCreation to 18.0.0 by @MattKotsenas in dotnet/reproducible-builds#84 ## New Contributors * @Frulfump made their first contribution in dotnet/reproducible-builds#80 **Full Changelog**: dotnet/reproducible-builds@v2.0.2...v2.0.5 ## 2.0.2 ## What's Changed * Fix hostfxr probing path for musl-based runtimes by @MattKotsenas in dotnet/reproducible-builds#75 **Full Changelog**: dotnet/reproducible-builds@v2.0.1...v2.0.2 ## 2.0.1 ## What's Changed * Warn if missing global.json to enforce consistent SDK versions by @MattKotsenas in dotnet/reproducible-builds#73 **Full Changelog**: dotnet/reproducible-builds@v1.2.39...v2.0.1 Commits viewable in [compare view](dotnet/reproducible-builds@v1.2.39...v2.0.5). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Upgrade `DotNet.ReproducibleBuilds` from 1.2.39 to 2.0.5 to improve reproducibility and fix hostfxr resolution. Bumps the central version and removes an unnecessary `PackageReference` from `SynthEHR.SourceGenerators`. - **Bug Fixes** - Removed stray `PackageReference` from `SynthEHR.SourceGenerators` to keep the package centralized and fix TestPackagesDocumentCorrect. - **Migration** - Ensure a `global.json` is present to pin the .NET SDK version (2.x warns if missing). <sup>Written for commit a25fb68. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/jas88/SynthEHR/pull/88?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James A Sutherland <j@sutherland.pw>
jas88
added a commit
to jas88/DicomTypeTranslation
that referenced
this pull request
Jun 25, 2026
Updated [DotNet.ReproducibleBuilds](https://github.com/dotnet/reproducible-builds) from 1.2.39 to 2.0.5. <details> <summary>Release notes</summary> _Sourced from [DotNet.ReproducibleBuilds's releases](https://github.com/dotnet/reproducible-builds/releases)._ ## 2.0.5 ## What's Changed * Update versions used in examples to be from latest major by @Frulfump in dotnet/reproducible-builds#80 * Fix hostfxr resolution for .NET Framework tasks by @MattKotsenas in dotnet/reproducible-builds#85 * Upgrade MSBuild.ProjectCreation to 18.0.0 by @MattKotsenas in dotnet/reproducible-builds#84 ## New Contributors * @Frulfump made their first contribution in dotnet/reproducible-builds#80 **Full Changelog**: dotnet/reproducible-builds@v2.0.2...v2.0.5 ## 2.0.2 ## What's Changed * Fix hostfxr probing path for musl-based runtimes by @MattKotsenas in dotnet/reproducible-builds#75 **Full Changelog**: dotnet/reproducible-builds@v2.0.1...v2.0.2 ## 2.0.1 ## What's Changed * Warn if missing global.json to enforce consistent SDK versions by @MattKotsenas in dotnet/reproducible-builds#73 **Full Changelog**: dotnet/reproducible-builds@v1.2.39...v2.0.1 Commits viewable in [compare view](dotnet/reproducible-builds@v1.2.39...v2.0.5). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Upgrade `DotNet.ReproducibleBuilds` to 2.0.5 to improve reproducible build reliability, fix hostfxr resolution (including musl-based runtimes), and warn when `global.json` is missing. - **Dependencies** - Bumped `DotNet.ReproducibleBuilds` from 1.2.39 to 2.0.5 in `Directory.Packages.props`. <sup>Written for commit 9ec90d2. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/jas88/DicomTypeTranslation/pull/90?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James A Sutherland <j@sutherland.pw>
This was referenced Jun 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #79
In 2.0.2
HostFxrResolverwas only compiled into thenet6.0build of the task. When MSBuild on .NET Framework loads thenet472build of the task (which is what happens in VS 2022+'s in-process build viaSdkResolverService), the resolver never runs and the[DllImport("hostfxr")]falls through to Windows' default DLL search.hostfxr.dlllives in a versioned subdirectory of the dotnet install and isn't on PATH or in System32, so the P/Invoke throwsDllNotFoundException.Register
HostFxrResolveron .NET Framework too. SinceAssemblyLoadContext.ResolvingUnmanagedDllisn't available there, the netfx branch eagerlyLoadLibraryExs the highest-versionhostfxr.dllfrom the resolved dotnet root and lets the OS satisfy the subsequent P/Invoke against the already-mapped module.Added a Windows-only regression test that spawns a net472
StubTaskHarnesssubprocess with allDOTNET_*discovery shortcuts cleared and a synthesized PATH that containsdotnet.exebut no copy ofhostfxr.dll. The harness reflectshostfxr_set_error_writeron the task to trigger the P/Invoke and reports the outcome via exit code.