diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8b66545ed..053ce5566 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -74,6 +74,10 @@ updates: - dependency-name: "Microsoft.WindowsAppSDK.WinUI" - dependency-name: "Microsoft.WindowsAppSDK.Runtime" - dependency-name: "Microsoft.Graphics.Win2D" + # A floor Microsoft.UI.Reactor ships to its consumers; it moves by hand with the + # Windows App SDK (src/Reactor/Reactor.csproj). Also held back via IGNORE_PACKAGES + # in .github/workflows/windows-nuget-updates.yml. + - dependency-name: "Microsoft.Windows.SDK.BuildTools.MSIX" # MessagePack is a transitive dependency (Microsoft.VisualStudio.SDK -> # StreamJsonRpc) pinned in Directory.Packages.props to the 2.x line for # StreamJsonRpc compatibility, at >= 2.5.302 for the GHSA-hv8m-jj95-wg3x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1293e8d2..3e4cd0c58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -263,6 +263,29 @@ jobs: Write-Host "All $($checked.Count) packed assemblies are AnyCPU:" $checked | ForEach-Object { Write-Host " $_" } + # One lib/ folder per package, with ReactorApplication.xbf beside Reactor.dll. A file packed + # to a literal lib\$(TargetFramework)\ path lands in a second folder (…22621.0 vs NuGet's + # …22621). Checked here because only a pack of an already-built tree, like this one, shows it. + - name: Check package lib/ layout + shell: pwsh + run: | + Add-Type -AssemblyName System.IO.Compression.FileSystem + $packages = @(Get-ChildItem artifacts/nupkg -Filter *.nupkg -File) + if ($packages.Count -eq 0) { throw 'No .nupkg under artifacts/nupkg.' } + $problems = [System.Collections.Generic.List[string]]::new() + foreach ($nupkg in $packages) { + $zip = [System.IO.Compression.ZipFile]::OpenRead($nupkg.FullName) + try { $entries = @($zip.Entries | ForEach-Object FullName) } finally { $zip.Dispose() } + $libs = @($entries | Where-Object { $_ -like 'lib/*/*' } | ForEach-Object { $_.Split('/')[1] } | Sort-Object -Unique) + if ($libs.Count -ne 1) { + $problems.Add("$($nupkg.Name) has $($libs.Count) lib/ folders: $($libs -join ', ')") + } elseif ($entries -contains "lib/$($libs[0])/Reactor.dll" -and $entries -notcontains "lib/$($libs[0])/Reactor/Hosting/ReactorApplication.xbf") { + $problems.Add("$($nupkg.Name) has no lib/$($libs[0])/Reactor/Hosting/ReactorApplication.xbf beside Reactor.dll") + } + } + if ($problems.Count -gt 0) { throw "Package layout problems:`n$($problems -join "`n")" } + Write-Host "One lib/ folder in each of $($packages.Count) packages." + - name: Build VSIX shell: pwsh run: | diff --git a/.github/workflows/windows-nuget-updates.yml b/.github/workflows/windows-nuget-updates.yml index 65348a31b..0bfe80fbd 100644 --- a/.github/workflows/windows-nuget-updates.yml +++ b/.github/workflows/windows-nuget-updates.yml @@ -78,7 +78,9 @@ env: # Package ids to hold back (exact, case-insensitive; comma separated). Seeded # with MessagePack because its 3.x major was intentionally declined in #580 — # remove it here to let this workflow propose that major again. - IGNORE_PACKAGES: 'MessagePack' + # Microsoft.Windows.SDK.BuildTools.MSIX is a floor Microsoft.UI.Reactor ships to + # its consumers; it moves by hand with the Windows App SDK (src/Reactor/Reactor.csproj). + IGNORE_PACKAGES: 'MessagePack,Microsoft.Windows.SDK.BuildTools.MSIX' jobs: # ───────────────────────────────────────────────────────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ad437fa..2bba8b2e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -648,6 +648,17 @@ Conventions for contributors: `Cancel()` cleanup (PR #1124), that the source had been pooled and reset, was wrong: that crash was this same fault. +- **Apps that reference Reactor no longer fail to build in deeply nested folders, and their + publish output now includes the app's `.pri` (issue #1271, + [WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** + `Microsoft.UI.Reactor` now depends on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100, + which fixes both. If you copied the `_CopyWinUIResourcesForAot` target from + `docs/aot-support.md`, you can delete it. + +- **The `Microsoft.UI.Reactor` package has a single `lib/` folder and always contains + `ReactorApplication.xbf`.** A second `lib/` folder used to hold only that file, and packing + from a clean tree left it out. + ### Security ## [0.1.0-preview.16] — 2026-09-22 diff --git a/Directory.Packages.props b/Directory.Packages.props index a27132967..e3d67cf03 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,6 +28,8 @@ + + diff --git a/docs/aot-support.md b/docs/aot-support.md index e651c5efa..dd22c8392 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -16,8 +16,8 @@ This page enumerates what works, what doesn't, and what's planned. **If you publ - **Markdown** — md4c-backed parser and renderer. - **Commanding** — command records, focus-scoped accelerators. - **Animation** — compositor-layer transitions, keyframes. -- **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). -- **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). +- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT. +- **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. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). - **Markdown / Localization (read path)** — `IStringLoc`/`Loc.X.Y` lookups generated by `Reactor.Localization.Generator` are AOT-clean. Source-generated, no runtime reflection. ## What does *not* work under AOT (today) @@ -33,8 +33,7 @@ These subsystems compile cleanly with `IsAotCompatible=true` (the warnings are s | **Form validation** | `FormField`'s default editor resolution goes through `TypeRegistry`. Same caveat as PropertyGrid. | Issue #70 | | **Component discovery (`ReactorApp.Run` reflection paths)** | The instantiation of `TApp` itself is annotated and works. Devtools component enumeration is available only when the app opts into `Reactor.DevtoolsSupport` at build time and launches with `--devtools`; leave the switch off for retail/AOT builds. | Issue #70 | | **Devtools `properties` / `setProperty` DP discovery** | The tools locate `DependencyProperty` statics by reflection, and the trimmer keeps no reflection metadata for them unless something roots the members — so under AOT the lookups find nothing. Not specific to WinUI's CsWinRT-projected static properties: a C#-authored DP *field* on a custom control is equally undiscoverable. Works under JIT, which is where the devtools inner loop lives. The tool reports the likely cause rather than an empty result. See [Devtools DependencyProperty discovery](#devtools-dependencyproperty-discovery) for the measured cost of fixing it. Affected fixture: `Devtools_PropertyToolsDpDiscovery`. | Issue #1109 | -| **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 | -| **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`. The same trim hits a compiled `.xaml` page inside a `ReactorApp.Run` app with no `App.xaml`: `ReactorApplication` finds the app's generated provider only by reflecting over the entry assembly, nothing roots it, and AOT removes it, so the page fails to parse (`Hosting_HostControlXamlComponentType`). A vanilla WinUI app is unaffected: its `App` *is* the provider. | Issue #142 | +| **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now. 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`. The same trim hits a compiled `.xaml` page inside a `ReactorApp.Run` app with no `App.xaml`: `ReactorApplication` finds the app's generated provider only by reflecting over the entry assembly, nothing roots it, and AOT removes it, so the page fails to parse (`Hosting_HostControlXamlComponentType`). A vanilla WinUI app is unaffected: its `App` *is* the provider. | Issue #142 | ## Devtools DependencyProperty discovery @@ -94,38 +93,9 @@ Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed b - **The runtime canary.** CI's `AOT Selftests` job (`.github/workflows/ci.yml`) publishes `tests/Reactor.AppTests.Host` with `PublishAotInternal=true` and runs the full selftest suite against the NativeAOT binary on every PR, split across two runners with `--shard 1/2` and `--shard 2/2` (see TESTING.md, "Sharding the corpus across runners"). `SelfTestRunner.DefaultAotSkipPatterns` mutes the 17 fixtures known to fail there (PropertyGrid auto-discovery, the three Devtools fixtures whose reflection targets get trimmed — `fire`'s handler lookup, `state`'s hook read, and DP discovery for `properties`/`setProperty` — the Issue142-style XAML-metadata cases, hot-reload state migration, plus the `UseObservableTree` property walk); any *new* failure fails the job. Note that "uses reflection" is not the same as "fails under AOT": the Devtools fixtures that run here mostly rely on type-name reflection (`GetType().Name`), which trimming always preserves — what breaks is member-level reflection whose target the trimmer drops. If you intentionally need to add a skip, document the bucket in the comment above `DefaultAotSkipPatterns` and re-probe with `tests/Reactor.AppTests.Host/probe-aot-skips.ps1`. Prefer splitting a fixture over skipping it whole — an entry mutes *every* check in the fixture, including ones that do pass under AOT, which is why DP discovery lives in its own `Devtools_PropertyToolsDpDiscovery` fixture. Treat that list as a liability, not a parking lot: a stale entry is AOT coverage silently switched off, so re-probe periodically and delete whatever now passes. `SelfTestRunner.ValidateDefaultSkipPatterns` aborts the run (TAP `Bail out!`, exit 1, no fixtures executed) if an entry stops matching any registered fixture, so a rename can't quietly turn a skip into a no-op. - **Probes can mask the defect they measure.** Reflection metadata under NativeAOT is emitted from what ILCompiler can see statically, so diagnostic code that calls `typeof(T).GetProperties()` / `GetMembers()` on a *constant* type roots that metadata and makes the failure disappear. `DynamicallyAccessedMemberTypes.PublicProperties` covers inherited members too, so probing one type can revive an entire base chain — a probe added to the #1109 fixture turned all 112 of `Button`'s DP statics visible again and made the fixture pass. Always confirm an AOT verdict with the probe deleted. -## Required publish-time workarounds +## Publish output -### WindowsAppSDK#6394 — project `.pri` (and `.xbf`) missing from publish output - -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. - -The missing `.pri` has cascading runtime symptoms that look like separate bugs but all trace back to the same cause: - -- `TabView`'s constructor throws `FileNotFoundException` from `ResourceAccessor::GetLocalizedStringResource(SR_TabViewCloseButtonTooltipWithKA)`. -- `NavigationView` mounts but its template-apply throws the same `FileNotFoundException` (Reactor's error boundary catches it and renders an error `TextBlock`). -- `Application.Current.Resources` loads as an empty dictionary (`0` merged, `0` themed, `0` keys) because `Application.LoadComponent` cascades through MRT for type-info lookups. -- Every `ThemeRef.Resolve(...)` returns `null`; `.Foreground(Theme.X)` falls back to control defaults. - -Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — still OPEN against 1.8 and 2.0. - -**Workaround** (in `tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj`): - -```xml - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" - Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - -``` - -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. +Publishing an unpackaged app needs no extra setup. `Microsoft.UI.Reactor` versions after 0.1.0-preview.16 depend on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100, which copies the app's `.pri` into the publish output ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)); older versions leave it out, and the published app then crashes at startup or cannot load its own XAML. On 0.1.0-preview.16 or earlier, add `` to your app, and remove it when you upgrade Reactor. If you added the `_CopyWinUIResourcesForAot` target that earlier versions of this page recommended, you can delete it. ## Debugging an AOT selftest hang diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index f393424d3..214592e10 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -261,7 +261,7 @@ The Microsoft-internal security-review intake form gave succinct answers about R | "CLI/debug tooling runs as developer-invoked user-mode process" | Manifest review of `mur.exe`, install script behavior | No elevation, no scheduled-task install. | | "CLI can talk to app as a debugging tool using MCP protocol over HTTP, domain-validated for localhost, with token auth" | `PreviewCaptureServer.cs`, `DevtoolsMcpServer.cs` | Listener prefix bound to `http://127.0.0.1:/`; Host header allow-list at `IsAllowedHost`; bearer constant-time compare at `BearerMatches`. | | "No outbound network communication beyond the above" | Repo-wide grep | Only `System.Net.*` usage in `src/Reactor/` is the two `HttpListener` servers above and an `HttpClient` *in CLI Docs subsystem* that targets `127.0.0.1:` of a preview server `mur` itself spawned. | -| "No unsupported or third-party components" | `ThirdPartyNoticeText.txt`, `Directory.Build.props`, `nuget.config` | Five Microsoft.* / MIT NuGet deps; three source-ported MIT/ISC OSS components (Yoga, md4c, D3); no checked-in `.nupkg` binaries; `nuget.config` clears inherited feeds. | +| "No unsupported or third-party components" | `ThirdPartyNoticeText.txt`, `Directory.Build.props`, `nuget.config` | Six Microsoft.* / MIT NuGet deps; three source-ported MIT/ISC OSS components (Yoga, md4c, D3); no checked-in `.nupkg` binaries; `nuget.config` clears inherited feeds. | | "No prior threat models / security reviews" | Repo audit | This document is the first formal one. README references a 275-finding internal code review — engineering review, not a threat model. | ## 9. Out-of-scope non-surfaces (with rationale) diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 625385f9a..946ed2e85 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -141,9 +141,13 @@ The `` line stays — `mur --skill` s ``` Microsoft.UI.Reactor.1.0.0-preview.42.nupkg -├── lib/net10.0-windows10.0.22621.0/ +├── lib/net10.0-windows10.0.22621/ # exactly one lib folder; see below │ ├── Reactor.dll -│ └── Reactor.xml # XML doc comments +│ ├── Reactor.pri +│ ├── Reactor.xml # XML doc comments +│ └── Reactor/Hosting/ +│ ├── ReactorApplication.xaml # Application-level XamlControlsResources +│ └── ReactorApplication.xbf # compiled form, packed by _PackReactorApplicationXbf ├── analyzers/dotnet/cs/ │ ├── Reactor.Analyzers.dll │ └── Reactor.Localization.Generator.dll @@ -154,6 +158,33 @@ Microsoft.UI.Reactor.1.0.0-preview.42.nupkg └── Microsoft.UI.Reactor.nuspec ``` +### Exactly one `lib/` folder + +NuGet names the build-output folder `net10.0-windows10.0.22621`, while `$(TargetFramework)` is +`net10.0-windows10.0.22621.0`. A file packed to a literal `lib\$(TargetFramework)\` path +therefore lands in a second `lib/` folder for the same framework, which is how 0.1.0-preview.16 +shipped `ReactorApplication.xbf`. `_PackReactorApplicationXbf` adds the file as build output +instead, so NuGet places it beside `Reactor.dll`. It runs at pack time because the `` +item it replaced was evaluated before the build created the file, so a clean pack left it out. + +### Build-tools dependency + +`Microsoft.UI.Reactor` depends on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100. Apps +otherwise get that package only through `Microsoft.WindowsAppSDK.Base`, and every Base release +so far (2.0.4, 2.0.5-experimental2) pins 1.7.251221100, which has two bugs that affect Reactor apps: + +- Expanding a package's `.pri` with `makepri.exe Dump` fails past `MAX_PATH` (`PRI175`), so apps + failed to build in deeply nested folders (issue #1271, + [WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795)). Measured with + only the build-tools version changed, a consumer of all three Reactor packages at the same + depth fails on 1.7.251221100 and builds on 1.7.260930100. +- An unpackaged app's own `.pri` is left out of its publish output + ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)), so the + published app crashes at startup or cannot load its own XAML. + +Remove the dependency once the Windows App SDK version Reactor depends on resolves +1.7.260930100 or later by itself. + ### Packaging configuration Enable packing on `src/Reactor/Reactor.csproj`: diff --git a/samples/apps/command-palette-window/CommandPaletteWindow.csproj b/samples/apps/command-palette-window/CommandPaletteWindow.csproj index bebd3327e..181322caa 100644 --- a/samples/apps/command-palette-window/CommandPaletteWindow.csproj +++ b/samples/apps/command-palette-window/CommandPaletteWindow.csproj @@ -22,17 +22,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj b/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj index e302205f1..d24954de0 100644 --- a/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj +++ b/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj @@ -44,17 +44,6 @@ - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/hello-world-aot/HelloWorldAot.csproj b/samples/apps/hello-world-aot/HelloWorldAot.csproj index 299e2564a..ca8e8ceee 100644 --- a/samples/apps/hello-world-aot/HelloWorldAot.csproj +++ b/samples/apps/hello-world-aot/HelloWorldAot.csproj @@ -40,17 +40,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/particle-storm/ParticleStorm.csproj b/samples/apps/particle-storm/ParticleStorm.csproj index 077e65a47..de0e68599 100644 --- a/samples/apps/particle-storm/ParticleStorm.csproj +++ b/samples/apps/particle-storm/ParticleStorm.csproj @@ -40,17 +40,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/widget-creator/widget-creator.csproj b/samples/apps/widget-creator/widget-creator.csproj index 2c8342c10..ea1a5800f 100644 --- a/samples/apps/widget-creator/widget-creator.csproj +++ b/samples/apps/widget-creator/widget-creator.csproj @@ -57,21 +57,6 @@ 10.0.17763.0 - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - + + + + + + + $(TargetsForTfmSpecificBuildOutput);_PackReactorApplicationXbf + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.xbf + + + + + + + - - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - +/// The Reactor packages' layout, and the build-tools version Microsoft.UI.Reactor +/// guarantees its consumers (issue #1271, WindowsAppSDK#6394). +/// +/// Needs network access to restore the Windows App SDK, so it only runs where the feeds +/// are reachable (CI's "Integration Tests" job). +/// +[Collection(LocalPackageFeedCollection.Name)] +public sealed class PriPackagingTests : IDisposable +{ + private const string BuildToolsMsixId = "Microsoft.Windows.SDK.BuildTools.MSIX"; + + // What Microsoft.WindowsAppSDK.Base resolves on its own; it predates the fixes. + private const string BuildToolsMsixWithoutFixes = "1.7.251221100"; + + private readonly LocalPackageFeedFixture _fixture; + private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rpri-{Guid.NewGuid():N}"); + + public PriPackagingTests(LocalPackageFeedFixture fixture) + { + _fixture = fixture; + Directory.CreateDirectory(_tempRoot); + } + + /// + /// WinUI loads ReactorApplication.xaml (or its compiled .xbf) at startup. + /// The .xbf is packed by a pack-time target, because the <None> item it + /// replaced was evaluated before the build created the file. + /// + [Fact] + public void CorePackageShipsTheApplicationXamlSidecars() + { + var entries = ReadPackageEntries("Microsoft.UI.Reactor"); + + Assert.Contains(entries, e => e.EndsWith("Reactor/Hosting/ReactorApplication.xbf", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(entries, e => e.EndsWith("Reactor/Hosting/ReactorApplication.xaml", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Build tools before 1.7.260930100 leave an unpackaged app's own .pri out of its + /// publish output, and the published app crashes at startup (WindowsAppSDK#6394). Reactor's + /// dependency on the fixed version must reach its consumers. The control pins the same + /// consumer to the version the Windows App SDK resolves alone, and must lose the .pri. + /// + [Fact] + public void ConsumerPublishIncludesTheAppPri() + { + // Short names: the XAML compiler is not long-path aware. + var withFloorDir = Path.Join(_tempRoot, "on"); + var pinnedDir = Path.Join(_tempRoot, "off"); + WriteConsumer(withFloorDir); + WriteConsumer(pinnedDir, buildToolsMsixVersion: BuildToolsMsixWithoutFixes); + + var withFloor = Publish(withFloorDir); + Assert.True( + File.Exists(Path.Join(withFloor, "Consumer.pri")), + $"The app's own .pri is missing from the publish output (WindowsAppSDK#6394). " + + $"The consumer resolved {ResolvedBuildToolsMsix(withFloorDir)}."); + + var pinned = Publish(pinnedDir); + Assert.False( + File.Exists(Path.Join(pinned, "Consumer.pri")), + $"Pinned to {BuildToolsMsixId} {BuildToolsMsixWithoutFixes} (resolved " + + $"{ResolvedBuildToolsMsix(pinnedDir)}), the publish still produced the app .pri, so the " + + "assertion above does not show that Reactor's dependency is what puts it there."); + } + + private string[] ReadPackageEntries(string packageId) + { + var packagePath = Path.Join(_fixture.PackageSourceDir, $"{packageId}.{_fixture.PackageVersion}.nupkg"); + Assert.True(File.Exists(packagePath), $"Expected package '{packagePath}' to exist."); + + using var archive = ZipFile.OpenRead(packagePath); + return archive.Entries.Select(e => e.FullName).ToArray(); + } + + /// + /// A self-contained consumer of the core package: the deployment where a missing app + /// .pri crashes the published app, because it also carries WinUI's own XAML. + /// pins the build tools below Reactor's floor, which + /// NuGet reports as an NU1605 downgrade. + /// + private void WriteConsumer(string appDir, string? buildToolsMsixVersion = null) + { + Directory.CreateDirectory(appDir); + + var pin = buildToolsMsixVersion is null + ? string.Empty + : $""" + + + """; + + File.WriteAllText(Path.Join(appDir, "Consumer.csproj"), $""" + + + Exe + net10.0-windows10.0.22621.0 + enable + true + true + {_fixture.RunArchitecture} + {_fixture.RunArchitecture} + win-{_fixture.RunArchitecture} + $(NoWarn);NU1605 + + + {pin} + + + """); + + // Never run: the test asserts on the publish output. + File.WriteAllText(Path.Join(appDir, "Program.cs"), """ + using static Microsoft.UI.Reactor.Factories; + + System.Console.WriteLine(TextBlock("hello") is not null); + + """); + + CreateNuGetConfig(appDir); + } + + private string Publish(string appDir) + { + var publishDir = Path.Join(appDir, "pub"); + RunHelpers.RunDotnet( + $"publish -c Release -a {_fixture.RunArchitecture} -o \"{publishDir}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 600_000); + return publishDir; + } + + private static string ResolvedBuildToolsMsix(string appDir) + { + var assetsPath = Path.Join(appDir, "obj", "project.assets.json"); + if (!File.Exists(assetsPath)) + { + return "nothing (no project.assets.json)"; + } + + using var assets = JsonDocument.Parse(File.ReadAllText(assetsPath)); + return assets.RootElement.GetProperty("libraries").EnumerateObject() + .Select(library => library.Name) + .FirstOrDefault(name => name.StartsWith(BuildToolsMsixId + "/", StringComparison.OrdinalIgnoreCase)) + ?? $"no {BuildToolsMsixId} at all"; + } + + private void CreateNuGetConfig(string appDir) + { + RunHelpers.RunDotnet( + $"new nugetconfig --output \"{appDir}\" --force", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 30_000); + + var configPath = Path.Join(appDir, "nuget.config"); + + RunHelpers.RunDotnet( + $"nuget config set globalPackagesFolder \"{_fixture.NugetPackagesDir}\" --configfile \"{configPath}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 30_000); + + RunHelpers.RunDotnet( + $"nuget add source \"{_fixture.PackageSourceDir}\" --name reactor-local --configfile \"{configPath}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 30_000); + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempRoot)) + { + Directory.Delete(_tempRoot, recursive: true); + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // Best effort: a just-exited build can still hold a file. + } + } +} \ No newline at end of file diff --git a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj index 8f3ef70c0..feeb241db 100644 --- a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj +++ b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj @@ -27,17 +27,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj index a2b252323..3302c3e3f 100644 --- a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj +++ b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj @@ -34,17 +34,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj b/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj index e78a341e1..e17aeaa43 100644 --- a/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj +++ b/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj @@ -61,23 +61,6 @@ true - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" - Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - -