Skip to content

Fix hostfxr resolution for .NET Framework tasks#85

Merged
baronfel merged 7 commits into
dotnet:mainfrom
MattKotsenas:feature/hostfxr-netframework
Jun 17, 2026
Merged

Fix hostfxr resolution for .NET Framework tasks#85
baronfel merged 7 commits into
dotnet:mainfrom
MattKotsenas:feature/hostfxr-netframework

Conversation

@MattKotsenas

Copy link
Copy Markdown
Member

Fixes #79

In 2.0.2 HostFxrResolver was only compiled into the net6.0 build of the task. When MSBuild on .NET Framework loads the net472 build of the task (which is what happens in VS 2022+'s in-process build via SdkResolverService), the resolver never runs and the [DllImport("hostfxr")] falls through to Windows' default DLL search. hostfxr.dll lives in a versioned subdirectory of the dotnet install and isn't on PATH or in System32, so the P/Invoke throws DllNotFoundException.

Register HostFxrResolver on .NET Framework too. Since AssemblyLoadContext.ResolvingUnmanagedDll isn't available there, the netfx branch eagerly LoadLibraryExs the highest-version hostfxr.dll from 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 StubTaskHarness subprocess with all DOTNET_* discovery shortcuts cleared and a synthesized PATH that contains dotnet.exe but no copy of hostfxr.dll. The harness reflects hostfxr_set_error_writer on the task to trigger the P/Invoke and reports the outcome via exit code.

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.
@MattKotsenas MattKotsenas requested a review from a team as a code owner June 12, 2026 18:03
@MattKotsenas MattKotsenas requested a review from baronfel June 17, 2026 04:28

@baronfel baronfel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is annoying but comprehensive and seems correct - thanks for filling the gap!

@baronfel baronfel merged commit 1fdaef6 into dotnet:main Jun 17, 2026
5 checks passed
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>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=DotNet.ReproducibleBuilds&package-manager=nuget&previous-version=1.2.39&new-version=2.0.5)](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>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=DotNet.ReproducibleBuilds&package-manager=nuget&previous-version=1.2.39&new-version=2.0.5)](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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DotNet.ReproducibleBuilds.Isolated 2.0.2 breaks builds in Visual Studio 2026

2 participants