Skip to content

Commit c0c206a

Browse files
Workaround WindowsAppSDK#6394: copy project .pri into AOT publish output (#360)
* Workaround WindowsAppSDK#6394: copy project .pri into AOT publish output 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> * fix(tests): serialize Use*ThreadingTests via [Collection] to avoid global 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> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e723334 commit c0c206a

6 files changed

Lines changed: 89 additions & 83 deletions

File tree

docs/aot-support.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ This page enumerates what works, what doesn't, and what's planned. **If you publ
1616
- **Markdown** — md4c-backed parser and renderer.
1717
- **Commanding** — command records, focus-scoped accelerators.
1818
- **Animation** — compositor-layer transitions, keyframes.
19-
- **Theming**`ThemeRef` tokens, style caching.
19+
- **Theming**`ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT once the WindowsAppSDK#6394 publish workaround is in place (see [Required publish-time workarounds](#required-publish-time-workarounds) below).
20+
- **Built-in WinUI controls**`NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT once WindowsAppSDK#6394 is mitigated. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142).
2021
- **Markdown / Localization (read path)**`IStringLoc`/`Loc.X.Y` lookups generated by `Reactor.Localization.Generator` are AOT-clean. Source-generated, no runtime reflection.
2122

2223
## What does *not* work under AOT (today)
@@ -33,15 +34,48 @@ These subsystems compile cleanly with `IsAotCompatible=true` (the warnings are s
3334
| **Form validation** | `FormField`'s default editor resolution goes through `TypeRegistry`. Same caveat as PropertyGrid. | Issue #70 |
3435
| **Navigation state JSON** | `NavigationHandle` serializes deep-link state via `JsonSerializer` without a source-generated context. Custom types that ride through navigation state will fail to serialize under AOT. | Issue #70 |
3536
| **Component discovery (`ReactorApp.Run<TApp>` reflection paths)** | The instantiation of `TApp` itself is annotated and works. The devtools-only `--list-components` enumeration scans `Assembly.GetTypes`; that path is gated to non-AOT builds. | Issue #70 |
36-
| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | `ThemeRef.Resolve` walks `Application.Current.Resources` + its merged/theme dictionaries. The token records (`Theme.Accent`, `Theme.PrimaryText`, …) construct fine, but at runtime the `XamlControlsResources` entries that `ReactorApplication.xaml` brings in aren't fully populated under AOT`Resolve` returns `null` for keys that exist under the JIT. Brushes applied via `.Foreground(Theme.X)` will fall back to control defaults. | Issue #70 |
37-
| **XAML-metadata-dependent controls (`NavigationView`, `TabView`, `TemplateBinding`)** | A subset of WinUI controls and the XAML-template parser need richer `IXamlMetadataProvider` data than what's reachable through trimmed AOT publish. `NavigationView` / `TabView` won't even reach `Mount` in this state, and `{TemplateBinding}` against custom DPs can't resolve the DP descriptor (see issue #142 reproductions). Use simpler containers under AOT. | Issue #70 |
37+
| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | Works once the WindowsAppSDK#6394 workaround target ships the project `.pri` into the publish output (see [Required publish-time workarounds](#required-publish-time-workarounds)). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 |
38+
| **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now (see [Required publish-time workarounds](#required-publish-time-workarounds)). What remains is `{TemplateBinding}` against private DPs in a third-party assembly that ships *no* `.xaml` file: the XAML compiler only emits an `IXamlMetadataProvider` for projects that have at least one `.xaml`, and AOT trims any implicit metadata path. Affected fixtures: `Issue142_CustomControlPrivateDp_Renders`, `Issue142_ThirdPartyControlPrivateDp_Renders`. The library-author workaround is to register a hand-written `IXamlMetadataProvider` via `RegisterControlAssembly`. | Issue #142 |
3839

3940
## Conventions
4041

4142
- **The library compiles AOT-clean.** Builds of `Reactor.csproj` produce zero IL2*/IL3* warnings. New code that reaches for reflection must either be source-generated, annotated with `DynamicallyAccessedMembers`, or — as a last resort — gated behind `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` so consumers see the warning at the call site.
4243
- **Suppressions are temporary.** Every `[UnconditionalSuppressMessage("Trimming", ...)]` or `("AOT", ...)` in this repo is a TODO. The justification field names the reflection use; tracking is folded into issue #70.
4344
- **The benchmark canary.** `tests/stress_perf/StressPerf.Reactor` (and the `StressPerf.Direct`/`ReactorGrid` siblings) set `PublishAot=true`. If they stop publishing, an AOT regression has landed in the framework.
4445

46+
## Required publish-time workarounds
47+
48+
### WindowsAppSDK#6394 — project `.pri` (and `.xbf`) missing from publish output
49+
50+
When publishing an unpackaged WinUI 3 app (`WindowsPackageType=None`) with `PublishAot=true`, the WindowsAppSDK build pipeline generates `$(AssemblyName).pri` into the intermediate output but does *not* copy it into the publish directory. The MakePRI step is conditioned on `AppxPackage=true`, so unpackaged apps fall through. Same applies to `.xbf` files generated by the XAML compiler.
51+
52+
The missing `.pri` has cascading runtime symptoms that look like separate bugs but all trace back to the same cause:
53+
54+
- `TabView`'s constructor throws `FileNotFoundException` from `ResourceAccessor::GetLocalizedStringResource(SR_TabViewCloseButtonTooltipWithKA)`.
55+
- `NavigationView` mounts but its template-apply throws the same `FileNotFoundException` (Reactor's error boundary catches it and renders an error `TextBlock`).
56+
- `Application.Current.Resources` loads as an empty dictionary (`0` merged, `0` themed, `0` keys) because `Application.LoadComponent` cascades through MRT for type-info lookups.
57+
- Every `ThemeRef.Resolve(...)` returns `null`; `.Foreground(Theme.X)` falls back to control defaults.
58+
59+
Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — still OPEN against 1.8 and 2.0.
60+
61+
**Workaround** (in `tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj`):
62+
63+
```xml
64+
<Target Name="_CopyWinUIResourcesForAot" AfterTargets="Publish"
65+
Condition="'$(PublishAot)' == 'true' and '$(AppxPackage)' != 'true'">
66+
<ItemGroup>
67+
<_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
68+
<_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri"
69+
Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
70+
</ItemGroup>
71+
<Copy SourceFiles="@(_WinUIResourcesForAot)"
72+
DestinationFiles="@(_WinUIResourcesForAot->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
73+
SkipUnchangedFiles="true" />
74+
</Target>
75+
```
76+
77+
Any Reactor app that publishes with `PublishAot=true` and `WindowsPackageType=None` should copy this target verbatim until WindowsAppSDK ships a fix. Remove the target once #6394 closes.
78+
4579
## Debugging an AOT selftest hang
4680

4781
`tests/Reactor.AppTests.Host` maintains an explicit allow-list of fixtures that hang, crash, or assert-fail under NativeAOT (`SelfTestRunner.DefaultAotSkipPatterns`). When you remove an entry from that list and the published Host hangs, crashes, or asserts instead of producing output, use the following workflow.

tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@
1616
<PublishAot>true</PublishAot>
1717
</PropertyGroup>
1818

19+
<!--
20+
Workaround for WindowsAppSDK#6394 (https://github.com/microsoft/WindowsAppSDK/issues/6394):
21+
NativeAOT publish of an unpackaged (WindowsPackageType=None) WinUI 3 app does not copy
22+
the generated project .pri (and any .xbf files) into the publish output. Without the
23+
.pri, MRT lookups fail (e.g. TabView/NavigationView throw FileNotFoundException from
24+
ResourceAccessor::GetLocalizedStringResource) and XamlControlsResources loads as an
25+
empty dictionary, breaking theme resolution.
26+
27+
The .pri is generated by _GenerateProjectPriFile into $(TargetDir); we copy it (and
28+
any .xbf files) into $(PublishDir) post-Publish so the AOT-published exe behaves
29+
the same as the JIT bin output. Remove this target once WindowsAppSDK#6394 ships.
30+
-->
31+
<Target Name="_CopyWinUIResourcesForAot" AfterTargets="Publish"
32+
Condition="'$(PublishAot)' == 'true' and '$(AppxPackage)' != 'true'">
33+
<ItemGroup>
34+
<_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
35+
<_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
36+
</ItemGroup>
37+
<Copy SourceFiles="@(_WinUIResourcesForAot)"
38+
DestinationFiles="@(_WinUIResourcesForAot->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
39+
SkipUnchangedFiles="true" />
40+
</Target>
41+
1942
<ItemGroup>
2043
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WindowsAppSDKVersion)" />
2144
<PackageReference Include="MessageFormat" Version="8.0.0" />

tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs

Lines changed: 26 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,14 @@ private static TimeSpan ResolveHangTimeout()
5656
private sealed record FixtureProgress(string Name, long StartTimestamp);
5757
private static FixtureProgress? _currentFixture;
5858

59-
// Fixtures known to crash or assert-fail under NativeAOT, captured by
60-
// running tests/Reactor.AppTests.Host/probe-aot-skips.ps1 against the
61-
// AOT-published Host.
59+
// Fixtures known to assert-fail under NativeAOT, captured by running
60+
// tests/Reactor.AppTests.Host/probe-aot-skips.ps1 against the AOT-published
61+
// Host. As of WindowsAppSDK#6394 workaround (see Reactor.AppTests.Host.csproj
62+
// _CopyWinUIResourcesForAot target), all NATIVE_CRASH skips are gone — the
63+
// remaining failures map to reflection-heavy subsystems (Devtools/MCP,
64+
// PropertyGrid auto-discovery) plus two control-collection assertions and
65+
// the Issue142 XAML-metadata-provider edge cases.
66+
//
6267
// Each name was verified to fail in isolation; wildcards from earlier
6368
// skip-list iterations have been replaced with explicit names so that
6469
// newly-passing siblings re-enter the run automatically.
@@ -69,78 +74,14 @@ private sealed record FixtureProgress(string Name, long StartTimestamp);
6974
// debugging workflow.
7075
private static readonly string[] DefaultAotSkipPatterns =
7176
{
72-
// -- Native crashes (process exits 0xC0000409 / STATUS_STACK_BUFFER_OVERRUN).
73-
// Capture a dump with DOTNET_DbgEnableMiniDump=1 to diagnose. --
74-
"Commanding_DisabledCommandDisablesControl",
75-
"Commanding_SplitButtonCommandInvokesExecute",
77+
// -- Reactor framework, control-collection assertions still under
78+
// investigation (no native crash; assertion fails inside the fixture). --
7679
"ControlUpdate_Collections",
77-
"ControlUpdate_Containers",
78-
"ControlUpdate_InputControls",
79-
"ControlUpdate_Navigation",
80-
"ControlUpdate_StatusControls",
81-
"ControlUpdate2_AdditionalControls",
82-
"ControlUpdate2_ButtonVariants",
83-
"ControlUpdate2_ExpanderContent",
84-
"ControlUpdate2_NavigationView",
85-
"CoreCov_AnnotatedScrollBarMount",
86-
"CoreCov_ExpanderChildUpdateDeep",
87-
"CoreCov_MediaPlayerMount",
88-
"CoreCov_MenuBarMountUpdate",
89-
"CoreCov_PopupRefreshContainerMount",
90-
"CoreCov_SelectorBarPipsPagerMount",
91-
"CoreCov_SwipeControlMount",
92-
"CoreCov_TreeViewUpdateExercise",
93-
"CoreCov2_CalendarPipsPagerUpdate",
94-
"CoreCov2_InfoBadgeMountUpdate",
95-
"CoreCov2_InfoBarActionButton",
96-
"CoreCov2_SelectorBarUpdate",
97-
"CovBoost_ElementPoolExercise",
98-
"CovBoost2_ElementPoolInteractiveReset",
99-
"CovBoost2_NavigationViewExercise",
100-
"CovBoost2_ReconcileChildPaths",
101-
"CovBoost2_TitleBarMountUpdate",
102-
"DataGrid_RowEditTemplatesAndEmptyState",
103-
"DslExt_FactoryMethods",
104-
"DslExt_MenuDslMethods",
105-
"EchoSuppress_ColorPicker",
106-
"EchoSuppress_NumberBox",
107-
"EchoSuppress_NumberBoxMinMaxCoercion",
108-
"EchoSuppress_RatingControl",
109-
"EchoSuppress_ToggleSplitButton",
110-
"Editors_ColorMounts",
111-
"Editors_NumberMounts",
112-
"IdentityPreserve_RadioButtons",
113-
"IdentityPreserve_SelectorBar",
114-
"Immediate_NumberBoxFiresOnTextChange",
115-
"RareControl_ColorPicker",
116-
"RareControl_ComboBoxRadioButtons",
117-
"RareControl_PersonPicture",
118-
"RareControl_TeachingTip",
119-
"RBC_ExpanderTemplateTransitionEvents",
120-
"RBC_HandlerWiringOnSecondRender",
121-
"RBC_InputControlsFireEvents",
122-
"RBC_SecondRenderCallbackInvocation",
123-
"RBC_SwipeControlItemsSwap",
124-
"RBC_TeachingTipMount",
125-
"RBC_TreeViewHandlerWiring",
126-
"RBC_TreeViewHandlerWiringFastPath",
127-
"RBC_TreeViewProgrammaticInvoke",
128-
"RBC_ValidationVisualizerStyles",
129-
"SelectionEvt_NavigationView",
130-
"SelectionEvt_RadioButtons",
131-
"ValCov_FormFieldRendering",
132-
"ValueEvt_ColorPicker",
133-
"ValueEvt_NumberBox",
134-
"ValueEvt_RatingControl",
135-
136-
// -- Assertion failures (fixture runs but "not ok" checks fail).
137-
// Most map to documented not-yet-AOT-clean subsystems (PropertyGrid
138-
// auto-discovery, devtools/MCP reflection, ThemeRef.Resolve, XAML
139-
// metadata for NavigationView/TabView/TemplateBinding). --
140-
"CoreCov_NavigationViewContentUpdate",
14180
"CoreCov2_UseObservableTreeHook",
142-
"CovBoost_ThemeRefExplicitResolution",
143-
"CovBoost_ThemeTokenResolution",
81+
82+
// -- Devtools / MCP server — JSON-RPC server uses reflection-heavy
83+
// tool discovery that is not AOT-safe. Documented in
84+
// docs/aot-support.md as a not-yet-AOT-clean subsystem. --
14485
"Devtools_ClickInvokesButton",
14586
"Devtools_ComponentsTool",
14687
"Devtools_FireInvokesNamedHandler",
@@ -168,9 +109,9 @@ private sealed record FixtureProgress(string Name, long StartTimestamp);
168109
"Devtools_WaitForTimeout",
169110
"Devtools_WaitForTimeoutLoggedAsErr",
170111
"Devtools_WindowsTool",
171-
"IdentityPreserve_TabView",
172-
"Issue142_CustomControlPrivateDp_Renders",
173-
"Issue142_ThirdPartyControlPrivateDp_Renders",
112+
113+
// -- PropertyGrid auto-discovery walks user types via reflection and is
114+
// not AOT-safe by design. Documented in docs/aot-support.md. --
174115
"PropertyGrid_Category_ExpandCollapse",
175116
"PropertyGrid_Custom_Editor",
176117
"PropertyGrid_DeepNesting_RecordInRecord",
@@ -180,10 +121,15 @@ private sealed record FixtureProgress(string Name, long StartTimestamp);
180121
"PropertyGrid_Reflection_EnumEditor",
181122
"PropertyGrid_Reflection_MutableObject",
182123
"PropertyGrid_Target_Switching",
183-
"RBC_ManyControlsHandlerWiring",
184-
"RBC_NavViewContentNullSwap",
185-
"RBC_TabViewGrowAndShrink",
186-
"SelectionEvt_TabView",
124+
125+
// -- Issue142 private-DP rendering: requires an IXamlMetadataProvider
126+
// for third-party / custom controls that is generated by the XAML
127+
// compiler only when the project has at least one .xaml file. AOT
128+
// tree-shaking removes the implicit metadata path even when one is
129+
// present, so these fixtures need a hand-written provider hooked up
130+
// before they can be re-enabled under AOT. --
131+
"Issue142_CustomControlPrivateDp_Renders",
132+
"Issue142_ThirdPartyControlPrivateDp_Renders",
187133
};
188134

189135
private static string[] GetAotSkipPatterns()

tests/Reactor.Tests/Core/UseInfiniteResourceThreadingTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace Microsoft.UI.Reactor.Tests.Core;
1717
/// from the main test thread, matching the production dispatcher model.</para>
1818
/// </remarks>
1919
[Trait("Category", "Threading")]
20+
[Collection("UnobservedTaskException")]
2021
public class UseInfiniteResourceThreadingTests : IDisposable
2122
{
2223
private int _unobserved;

tests/Reactor.Tests/Core/UseMutationThreadingTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Microsoft.UI.Reactor.Tests.Core;
99
/// invariant, concurrent cache invalidation, and the unobserved-task-exception guard.
1010
/// </summary>
1111
[Trait("Category", "Threading")]
12+
[Collection("UnobservedTaskException")]
1213
public class UseMutationThreadingTests : IDisposable
1314
{
1415
private int _unobserved;

tests/Reactor.Tests/Core/UseResourceThreadingTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Microsoft.UI.Reactor.Tests.Core;
99
/// after unmount, concurrent invalidation, and the global unobserved-task-exception invariant.
1010
/// </summary>
1111
[Trait("Category", "Threading")]
12+
[Collection("UnobservedTaskException")]
1213
public class UseResourceThreadingTests : IDisposable
1314
{
1415
private int _unobserved;

0 commit comments

Comments
 (0)