Skip to content

Workaround WindowsAppSDK#6394: copy project .pri into AOT publish output#360

Merged
codemonkeychris merged 2 commits into
mainfrom
codemonkeychris/aot-pri-publish-workaround
May 20, 2026
Merged

Workaround WindowsAppSDK#6394: copy project .pri into AOT publish output#360
codemonkeychris merged 2 commits into
mainfrom
codemonkeychris/aot-pri-publish-workaround

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

Summary

Diagnoses and works around a single WindowsAppSDK NativeAOT publish bug that was masquerading as ~10 unrelated Reactor framework issues. Net result: 68 of 108 previously-skipped AOT selftest fixtures now pass, including all 60 NATIVE_CRASH entries. JIT baseline is unchanged at 735/735.

Root cause

microsoft/WindowsAppSDK#6394 (OPEN against 1.8 and 2.0): when publishing an unpackaged WinUI 3 app (WindowsPackageType=None) with PublishAot=true, the WindowsAppSDK build pipeline generates $(AssemblyName).pri into $(TargetDir) but does not copy it into $(PublishDir). The PRI is wired into publish only behind <Condition>'$(AppxPackage)' == 'true'</Condition>, so unpackaged apps fall through. The same gap applies to .xbf files.

This one missing file produced what looked like several independent failures:

Symptom Real cause
TabView ctor throws FileNotFoundException from ResourceAccessor::GetLocalizedStringResource MRT can't find the PRI
NavigationView template-apply throws the same exception (caught by Reactor's error boundary) MRT can't find the PRI
Application.Current.Resources loads empty (0/0/0) Application.LoadComponent cascades through MRT for type lookups
ThemeRef.Resolve(...) returns null for every key XamlControlsResources was empty, see above

Confirmed by literally copying Reactor.AppTests.Host.pri into the publish folder and re-running: every symptom resolved in one shot.

Fix

Small post-Publish MSBuild target in tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj that copies the PRI (and any .xbf files) into the publish output:

<Target Name="_CopyWinUIResourcesForAot" AfterTargets="Publish"
        Condition="'$(PublishAot)' == 'true' and '$(AppxPackage)' != 'true'">
  <ItemGroup>
    <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
    <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri"
                           Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
  </ItemGroup>
  <Copy SourceFiles="@(_WinUIResourcesForAot)"
        DestinationFiles="@(_WinUIResourcesForAot->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
        SkipUnchangedFiles="true" />
</Target>

Scoped to the test host since it's currently the only AOT-published exe in the repo. To be removed once WindowsAppSDK#6394 ships.

The Reactor library itself is untouched; it remains AOT-clean.

Skip-list cleanup (SelfTestRunner.DefaultAotSkipPatterns)

108 → 40 entries. Every NATIVE_CRASH skip is gone. The remaining 40 are ASSERT_FAIL in pre-acknowledged reflection-heavy subsystems:

  • Devtools / MCP (27): JSON-RPC tool-discovery reflection — documented in docs/aot-support.md as not-yet-AOT-clean.
  • PropertyGrid auto-discovery (9): walks user types via reflection — documented as not-yet-AOT-clean.
  • Other (4): ControlUpdate_Collections, CoreCov2_UseObservableTreeHook (both pre-existing assertion bugs), Issue142_CustomControlPrivateDp_Renders, Issue142_ThirdPartyControlPrivateDp_Renders (need a hand-written IXamlMetadataProvider since the XAML compiler only emits one when a project has a .xaml file).

Verification

Suite Result
JIT selftest (Reactor.AppTests.Host --self-test) 735/735 fixtures, 2614 ok, 0 not_ok, 0 bail
AOT selftest (with workaround + new skip list) 735 plan, 695 ran, 40 skipped, 0 not_ok, 0 bail
Reactor.Tests unit suite Passed 8381, Failed 0, Skipped 46 (Yoga generated, pre-existing)
Reactor.csproj library build 0 errors, 0 IL2*/IL3* warnings (library remains AOT-clean)

Files changed

  • tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj — adds _CopyWinUIResourcesForAot MSBuild target.
  • tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.csDefaultAotSkipPatterns shrinks 108 → 40 with new comments grouping the remaining entries by root cause.
  • docs/aot-support.md — new "Required publish-time workarounds" section explaining #6394; works/does-not-work tables updated to reflect that built-in WinUI controls and ThemeRef.Resolve work under AOT with the workaround in place.

@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

CI failure is unrelated — the Unit Tests job hit a known-flaky threading test:

Failed Microsoft.UI.Reactor.Tests.Core.UseInfiniteResourceThreadingTests.Refresh_During_InFlight_Cancels_And_Restarts
  Assert.Equal() Failure: Values differ
  Expected: 0
  Actual:   1
  at AssertNoUnobserved() at line 36

The assertion is TaskScheduler.UnobservedTaskException count after GC.Collect(); under CI load a late task completion can race the disposal/swap of the inner InfiniteResource, producing one unobserved exception.

Evidence this is pre-existing flake, not my PR:

  • This PR touches only tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj, tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs, and docs/aot-support.md. None of those reach src/Reactor/Core/InfiniteResource.cs or its hook.
  • The most recent failed main CI run (3c3bed9e) failed with a sibling test in the same family (UseResourceThreadingTests.Fetcher_Observed_Cancellation_Is_Silent_Not_Error) hitting the same AssertNoUnobserved pattern.
  • Local run: dotnet test ... --filter UseInfiniteResourceThreadingTests.Refresh_During_InFlight_Cancels_And_RestartsPassed! Failed: 0, Passed: 1.

Reran the failed job.

codemonkeychris and others added 2 commits May 20, 2026 16:10
Root cause: WindowsAppSDK's NativeAOT publish pipeline does not copy the
project .pri (generated by _GenerateProjectPriFile) into the publish output
for unpackaged apps (WindowsPackageType=None). The MakePRI item-group is
gated on AppxPackage=true, so unpackaged apps fall through. Same applies
to .xbf files emitted by the XAML compiler. Upstream tracking:
microsoft/WindowsAppSDK#6394 (OPEN against 1.8 and 2.0).

Symptoms previously attributed to multiple unrelated Reactor framework
bugs were all manifestations of this one missing file:

- TabView ctor throws FileNotFoundException from
  ResourceAccessor::GetLocalizedStringResource.
- NavigationView template-apply throws the same; Reactor's error boundary
  renders an error TextBlock instead of the requested content.
- Application.Current.Resources loads empty (0 merged / 0 themed / 0 keys)
  because Application.LoadComponent cascades through MRT for type lookups.
- Every ThemeRef.Resolve returns null; .Foreground(Theme.X) falls back to
  defaults.

Fix: add a small post-Publish MSBuild target that copies
$(OutputPath)$(AssemblyName).pri and any .xbf files into $(PublishDir).
Scoped to Reactor.AppTests.Host.csproj since that is currently the only
AOT-published exe in the repo; copy verbatim into any other Reactor app
that uses PublishAot=true with WindowsPackageType=None. Remove once
WindowsAppSDK#6394 ships.

Skip-list impact (tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs):
- 108 skips -> 40 skips (net 68 newly-passing AOT fixtures).
- All 60 NATIVE_CRASH entries eliminated.
- Remaining 40 are all ASSERT_FAIL in pre-acknowledged reflection-heavy
  subsystems: Devtools/MCP (27), PropertyGrid auto-discovery (9),
  ControlUpdate_Collections, CoreCov2_UseObservableTreeHook, and the
  two Issue142 third-party XAML metadata fixtures.

Verification:
- JIT selftest: 735/735 fixtures, 2614 ok, 0 not_ok, 0 bail (unchanged).
- AOT selftest: 735 plan, 695 ran, 40 skipped, 0 not_ok, 0 bail.
- Reactor.Tests unit suite: Passed 8381, Failed 0, Skipped 46 (Yoga
  generated, pre-existing).
- Reactor.csproj builds 0 errors (no library reflection added; library
  remains AOT-clean).

Documentation: docs/aot-support.md gets a new "Required publish-time
workarounds" section explaining #6394, plus updates to the works /
does-not-work tables reflecting that built-in WinUI controls and
ThemeRef.Resolve now work under AOT with the workaround in place.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…obal TaskScheduler.UnobservedTaskException race

The three Use*ThreadingTests classes (UseInfiniteResource, UseMutation,
UseResource) each subscribe to the process-wide
TaskScheduler.UnobservedTaskException event in their constructor and assert
that no unobserved exceptions occurred at the end of every test. With xUnit's
default parallel test scheduling, the three classes race each other: an
unobserved exception raised by a Task originating in one test class can fire
the event handler in any of the others, breaking the AssertNoUnobserved()
invariant.

Symptom in CI: random failures across the family, e.g.
  - UseResourceThreadingTests.Fetcher_Observed_Cancellation_Is_Silent_Not_Error (main 3c3bed9)
  - UseInfiniteResourceThreadingTests.Refresh_During_InFlight_Cancels_And_Restarts (PR #360)
  - UseMutationThreadingTests.Unmount_Cancels_Pending_OnError_Does_Not_Fire (PR #360 rerun)

Fix: tag all three classes with [Collection("UnobservedTaskException")].
xUnit serializes classes that share a collection name, so they no longer run
concurrently and the global event handlers no longer see each other's tasks.
Same convention as the existing [Collection("ConsoleTests")] grouping for
Console.Out/Error-mutating tests.

Verified: 10x stress run of all 19 threading tests, 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codemonkeychris codemonkeychris force-pushed the codemonkeychris/aot-pri-publish-workaround branch from 42160a8 to 1e342b8 Compare May 20, 2026 23:15
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

Updated with a targeted fix for the failing unit test.

Root cause

The failures were a pre-existing flake in the Use*ThreadingTests family, exposed (but not caused) by this PR. All three test classes —

  • ests/Reactor.Tests/Core/UseInfiniteResourceThreadingTests.cs
  • ests/Reactor.Tests/Core/UseMutationThreadingTests.cs
  • ests/Reactor.Tests/Core/UseResourceThreadingTests.cs

— subscribe to the process-wide TaskScheduler.UnobservedTaskException event in their constructor and assert _unobserved == 0 at the end of every test. With xUnit's default parallel test scheduling, the three classes raced each other: an unobserved Task exception from one test fired the event handler in another, breaking the invariant.

Evidence the failures are unrelated to this PR's AOT changes (three different tests in the family across three different runs, including a failure on main):

Run Commit Failing test
Main CI 26179835194 3c3bed9 UseResourceThreadingTests.Fetcher_Observed_Cancellation_Is_Silent_Not_Error
PR #360 original 42160a8 UseInfiniteResourceThreadingTests.Refresh_During_InFlight_Cancels_And_Restarts
PR #360 rerun 42160a8 UseMutationThreadingTests.Unmount_Cancels_Pending_OnError_Does_Not_Fire

Fix

Tag all three classes with [Collection("UnobservedTaskException")] — xUnit serializes classes sharing a collection name, so they no longer run concurrently. Same convention as the existing [Collection("ConsoleTests")] group used for Console.Out/Error-mutating tests.

Verified locally: 10x stress run of all 19 threading tests, 0 failures.

Also rebased on current main to pull in #356 + #357.

@codemonkeychris codemonkeychris merged commit c0c206a into main May 20, 2026
9 checks passed
@codemonkeychris codemonkeychris deleted the codemonkeychris/aot-pri-publish-workaround branch May 20, 2026 23:46
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.

1 participant