From a9ea83d862b595958d800803090d277a39679dff Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:47:25 -0700 Subject: [PATCH 1/7] Document the single-file path in Getting Started PR #1277 moved scaffolding onto the Windows App SDK templates and rewrote the setup story, but the third path from the plan was never written: a Reactor app does not need a .csproj at all. .NET 10 runs a lone .cs file whose #:package / #:property header supplies what a project file otherwise would. Both shapes are now covered, and the header is the switch between them: keep WindowsPackageType=None for a plain unpackaged window, or drop it and add Microsoft.Windows.SDK.BuildTools.WinApp, whose targets intercept `dotnet run` and launch with real package identity. That second half is winappCli#794 and #874 (the latter implements the issue we filed as winappCli#871); both shipped in winapp 0.7.0 on 2026-09-24, which is what unblocked this. Verified end to end against the currently published versions -- preview.16 and BuildTools.WinApp 0.7.0 -- not against the versions this was first prototyped with. All three commands were run: `dotnet run` unpackaged opens a window, `winapp run` opens a packaged one, and packaged `dotnet run` prints "[WinAppRunSupport] Intercepted 'dotnet run' for packaged app" and launches with identity. Test packages were unregistered afterwards. Re-verifying was necessary: the header prototyped in August no longer builds, because the Windows App SDK now defaults to self-contained and refuses an unspecified architecture. The fix is the one the docset already documents -- index.md.dt's minimal .csproj resolves RuntimeIdentifier from $(NETCoreSdkPortableRuntimeIdentifier), and that works verbatim as a #:property. An earlier draft instead pinned Microsoft.WindowsAppSDK, which built and ran but contradicted index.md.dt on why that package is ever added by hand, and implied `Platform=x64` was the only alternative when the docset's own guarded block shows an architecture-neutral one. Both pages now tell one story. The guide names the two failures whose error text does not point at the cause, because both cost time here: omitting WindowsPackageType=None builds clean and then dies at startup with REGDB_E_CLASSNOTREG (the bootstrapper ships but never auto-initializes), and a Windows version below 22621 compiles against nothing and reports CS0234 "the namespace 'Reactor' does not exist", which reads like a missing package. On verification: InlineSnippetLedgerTests correctly rejected these blocks, since nothing compiles inline C#. They cannot be snippet-backed -- a file-based app is defined by having no .csproj -- and they stay contiguous rather than splitting into a ledgered header plus a snippet-backed body, because the section's claim is "this is the entire file". What that costs is bounded and stated in the ledger: every construct in the body already appears in a compiled snippet on the same page (hello-world and usestate-counter), so an API rename reddens those first. The unverified remainder is the directives, and SingleFileGuideHeaderTests covers the one that can rot silently by pinning the documented TargetFramework to src/Reactor/Reactor.csproj. Mutation-checked, including the case a weaker assertion missed: a stale TFM reddens, deleting WindowsPackageType=None reddens, and *moving* it from the unpackaged block into the packaged one reddens -- the last of these is the realistic drift, since the two blocks differ by two lines, and a whole-file Contains check stays green through it while both examples become wrong. Reactor.slnx Release: 0 errors. Reactor.Tests 14,205 passed / 0 failed. DocPipeline.Tests 474 passed / 0 failed. WinAppSDKReferenceGuardTests 86 passed. No image churn. The two Win2D cross-link warnings are pre-existing in packaging.md.dt, which this does not touch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- CHANGELOG.md | 15 ++ .../_pipeline/templates/getting-started.md.dt | 89 ++++++++++ docs/guide/getting-started.md | 89 ++++++++++ .../InlineSnippetLedgerTests.cs | 64 +++++++ .../SingleFileGuideHeaderTests.cs | 167 ++++++++++++++++++ 5 files changed, 424 insertions(+) create mode 100644 tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9f4bbfb..ecb70d407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,21 @@ Conventions for contributors: ### Added +- **Getting Started documents the single-file path (`dotnet run counter.cs`).** + A Reactor app does not need a `.csproj`: .NET 10 runs a lone `.cs` file whose + `#:package` / `#:property` header supplies what a project file otherwise would. + The guide now covers both shapes — unpackaged via `WindowsPackageType=None`, + and packaged by dropping that directive and adding + `Microsoft.Windows.SDK.BuildTools.WinApp`, which intercepts `dotnet run` and + launches with real package identity (winappCli#794 / #874, shipped in winapp + 0.7.0). It also names the two failures whose error text does not point at the + cause: omitting `WindowsPackageType=None` builds clean and then dies at + startup with `REGDB_E_CLASSNOTREG`, and a Windows version below 22621 compiles + against nothing and reports `CS0234: the namespace 'Reactor' does not exist`, + which reads like a missing package. `SingleFileGuideHeaderTests` ties the + documented target framework to `src/Reactor/Reactor.csproj` so that second one + cannot rot back in. + - **Framework mechanics are searchable in the ReactorGallery index (spec 064, issue #1275).** `find-ui --source reactor` answered "what is control X" but not "how does mechanism Y work": `UseState hook` and `key down event handler` diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt index 41b354b8d..4a4bd0488 100644 --- a/docs/_pipeline/templates/getting-started.md.dt +++ b/docs/_pipeline/templates/getting-started.md.dt @@ -82,6 +82,95 @@ identity — the same thing F5 does in Visual Studio. That requires **Developer Mode** (Settings → System → For developers). See [Packaging](packaging.md) for the unpackaged alternative. +## One file, no project + +A Reactor app does not need a `.csproj`. .NET 10 runs a single `.cs` file +directly, and file-level `#:` directives supply the configuration a project file +otherwise would: + +```csharp +#:package Microsoft.UI.Reactor@{{reactorVersion}} +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None +#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Counter"); + +class Counter : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + + return VStack(12, + TextBlock($"Count: {count}").FontSize(24).Bold(), + Button("Increment", () => setCount(count + 1)) + ).Padding(24); + } +} +``` + +```powershell +dotnet run counter.cs +``` + +That is the whole app — one file, no scaffolding, no build output to manage. +It is the fastest way to try an idea, reproduce a bug, or paste a complete +runnable example into an issue. + +One package reference is enough, exactly as in the +[minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the +Windows App SDK in transitively. The four properties are the project-file +settings that have nowhere else to live, and each fails in a way that does not +name its own cause: + +| Directive | Why it is required | +|---|---| +| `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | +| `UseWinUI=true` | Brings in the WinUI 3 targets. | +| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | The Windows App SDK needs a concrete architecture; without one the build fails with *"WindowsAppSDKSelfContained requires a supported Windows architecture."* Resolving it from the SDK keeps the file working on x64 and ARM64 alike, rather than pinning it to one. Same line the minimal `.csproj` uses. | +| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | + +### Single file, packaged + +Dropping `WindowsPackageType=None` is also how you opt *in* to package identity — +you just need something to supply the manifest. Add the Windows App SDK build +tools and `dotnet run` hands off automatically: + +```csharp +#:package Microsoft.UI.Reactor@{{reactorVersion}} +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) +``` + +```powershell +dotnet run counter.cs +# [WinAppRunSupport] Intercepted 'dotnet run' for packaged app +# Launching packaged application... +``` + +The package synthesizes an appxmanifest from the `#:property` values, registers +a loose layout, and launches the app with real package identity — so +`Package.Current` and the `ms-appx:` scheme work from a single file. It bundles +the CLI it needs, so nothing has to be installed globally. + +If you would rather drive that explicitly, or you already have the +[Windows App SDK CLI](https://github.com/microsoft/winappCli) installed, +`winapp run counter.cs` does the same thing without the extra `#:package` line. +Both need **winapp 0.7.0 or later**. + +So the header is the switch: keep `WindowsPackageType=None` for a plain +unpackaged window, or drop it and add the build tools for a packaged app. + ## Contributor setup (one-time) Working *on* Reactor rather than with it? Clone and bootstrap: diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 4cc6ee722..25a15259c 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -70,6 +70,95 @@ identity — the same thing F5 does in Visual Studio. That requires **Developer Mode** (Settings → System → For developers). See [Packaging](packaging.md) for the unpackaged alternative. +## One file, no project + +A Reactor app does not need a `.csproj`. .NET 10 runs a single `.cs` file +directly, and file-level `#:` directives supply the configuration a project file +otherwise would: + +```csharp +#:package Microsoft.UI.Reactor@0.1.0-preview.16 +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None +#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Counter"); + +class Counter : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + + return VStack(12, + TextBlock($"Count: {count}").FontSize(24).Bold(), + Button("Increment", () => setCount(count + 1)) + ).Padding(24); + } +} +``` + +```powershell +dotnet run counter.cs +``` + +That is the whole app — one file, no scaffolding, no build output to manage. +It is the fastest way to try an idea, reproduce a bug, or paste a complete +runnable example into an issue. + +One package reference is enough, exactly as in the +[minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the +Windows App SDK in transitively. The four properties are the project-file +settings that have nowhere else to live, and each fails in a way that does not +name its own cause: + +| Directive | Why it is required | +|---|---| +| `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | +| `UseWinUI=true` | Brings in the WinUI 3 targets. | +| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | The Windows App SDK needs a concrete architecture; without one the build fails with *"WindowsAppSDKSelfContained requires a supported Windows architecture."* Resolving it from the SDK keeps the file working on x64 and ARM64 alike, rather than pinning it to one. Same line the minimal `.csproj` uses. | +| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | + +### Single file, packaged + +Dropping `WindowsPackageType=None` is also how you opt *in* to package identity — +you just need something to supply the manifest. Add the Windows App SDK build +tools and `dotnet run` hands off automatically: + +```csharp +#:package Microsoft.UI.Reactor@0.1.0-preview.16 +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) +``` + +```powershell +dotnet run counter.cs +# [WinAppRunSupport] Intercepted 'dotnet run' for packaged app +# Launching packaged application... +``` + +The package synthesizes an appxmanifest from the `#:property` values, registers +a loose layout, and launches the app with real package identity — so +`Package.Current` and the `ms-appx:` scheme work from a single file. It bundles +the CLI it needs, so nothing has to be installed globally. + +If you would rather drive that explicitly, or you already have the +[Windows App SDK CLI](https://github.com/microsoft/winappCli) installed, +`winapp run counter.cs` does the same thing without the extra `#:package` line. +Both need **winapp 0.7.0 or later**. + +So the header is the switch: keep `WindowsPackageType=None` for a plain +unpackaged window, or drop it and add the build tools for a packaged app. + ## Contributor setup (one-time) Working *on* Reactor rather than with it? Clone and bootstrap: diff --git a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs index 5a19dbe24..a7d79a28c 100644 --- a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs +++ b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs @@ -183,6 +183,70 @@ private static bool IsParameterDeclaration(string parameter) private static readonly Dictionary> AllowedInlineExamples = new(StringComparer.OrdinalIgnoreCase) { + ["getting-started"] = new(StringComparer.Ordinal) + { + // A .NET file-based app is *defined* by having no .csproj, so it cannot be a doc + // app — the snippet mechanism extracts from projects. It stays one contiguous + // block rather than a ledgered header plus a snippet-backed body because the + // section's whole claim is "this is the entire file"; splitting it would show the + // reader two fragments to reassemble. + // + // What that costs is smaller than it looks. Every construct in the body is + // exercised by a compiled snippet on this same page — ReactorApp.Run(string), + // Component/Render, UseState, VStack(spacing, …), TextBlock($"…").FontSize(…), + // .Bold() and .Padding(…) all appear in getting-started/hello-world, and + // UseState + Button(string, Action) in getting-started/usestate-counter. An API + // rename that broke this block would redden those first. + // + // The genuinely unverified part is the six #: directives, and they are covered + // separately: the Reactor version is the {{reactorVersion}} token the doc + // compiler substitutes, and SingleFileGuideHeaderTests pins the TargetFramework + // to src/Reactor/Reactor.csproj. That last one matters because a stale Windows + // version surfaces as CS0234 "the namespace 'Reactor' does not exist", which + // reads like a missing package rather than a wrong TFM. + [""" + #:package Microsoft.UI.Reactor@{{reactorVersion}} + #:property OutputType=WinExe + #:property TargetFramework=net10.0-windows10.0.22621.0 + #:property UseWinUI=true + #:property WindowsPackageType=None + #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) + + using Microsoft.UI.Reactor; + using Microsoft.UI.Reactor.Core; + using static Microsoft.UI.Reactor.Factories; + + ReactorApp.Run("Counter"); + + class Counter : Component + { + public override Element Render() + { + var (count, setCount) = UseState(0); + + return VStack(12, + TextBlock($"Count: {count}").FontSize(24).Bold(), + Button("Increment", () => setCount(count + 1)) + ).Padding(24); + } + } + """] = + "File-based app: no .csproj by definition, so it cannot be a doc app. Its DSL is covered by the compiled hello-world/usestate-counter snippets on the same page; its TFM by SingleFileGuideHeaderTests.", + + // The packaged variant, shown as a header-only fragment because the point is the + // two-line delta from the block above — add the build tools package, drop + // WindowsPackageType=None — not the component body, which is unchanged. + [""" + #:package Microsoft.UI.Reactor@{{reactorVersion}} + #:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 + #:property OutputType=WinExe + #:property TargetFramework=net10.0-windows10.0.22621.0 + #:property UseWinUI=true + #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) + """] = + "Header-only delta for the packaged variant; same file-based-app constraint, TFM guarded by SingleFileGuideHeaderTests.", + }, + ["source-mapping"] = new(StringComparer.Ordinal) { // The "before" half of the [ReactorSourceTransparent] explanation: a helper diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs new file mode 100644 index 000000000..c03a9e4c8 --- /dev/null +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Xunit; + +namespace Microsoft.UI.Reactor.Cli.Docs.Tests; + +/// +/// Ties the file-based-app header documented in the Getting Started guide to the framework's +/// own target framework. +/// +/// +/// +/// Why this needs a test at all. A .NET file-based app is defined by having no +/// .csproj, so the guide's #: header cannot be a compiled doc-app snippet the way +/// every other C# block on that page is — it is ledgered in +/// InlineSnippetLedgerTests.AllowedInlineExamples instead. That buys a hole: nothing +/// compiles those six lines, and the page renders them identically to the verified blocks around +/// them. +/// +/// +/// The value that actually rots. Two of the header's values are already safe — the Reactor +/// version is the {{reactorVersion}} token the doc compiler substitutes, and the +/// Microsoft.WindowsAppSDK pin is swept by +/// WinAppSDKReferenceGuardTests, which scans .md and .dt for exactly this +/// #:package shape. The target framework is the one left over, and it is the worst of the +/// three to get wrong: a reader who copies a stale Windows version compiles against nothing and +/// gets CS0234: the type or namespace name 'Reactor' does not exist, which reads like a +/// missing package rather than a wrong TFM. Measured, not assumed — that is the exact error +/// net10.0-windows10.0.19041.0 produces against this package. +/// +/// +public class SingleFileGuideHeaderTests +{ + private const string GuideTemplate = "docs/_pipeline/templates/getting-started.md.dt"; + private const string FrameworkProject = "src/Reactor/Reactor.csproj"; + + /// + /// Every #:property TargetFramework=… in the guide must name the TFM + /// src/Reactor/Reactor.csproj builds, because that is what the published package's + /// lib/ folder is keyed on. + /// + [Fact] + public void DocumentedTargetFramework_MatchesTheFrameworkProject() + { + var root = RepoRoot(); + var expected = FrameworkTfm(root); + + var guide = File.ReadAllText(Path.Combine(root, GuideTemplate)); + var documented = Regex.Matches(guide, @"^#:property\s+TargetFramework=(?\S+)\s*$", RegexOptions.Multiline) + .Select(m => m.Groups["tfm"].Value) + .ToArray(); + + // A rename or a restructure that drops the header entirely would otherwise leave this + // test passing over an empty set — the classic vacuous green. + Assert.NotEmpty(documented); + + foreach (var tfm in documented) + { + Assert.True( + string.Equals(tfm, expected, StringComparison.Ordinal), + $"{GuideTemplate} documents '#:property TargetFramework={tfm}' but {FrameworkProject} " + + $"builds '{expected}'. A reader copying the stale value compiles against no Reactor " + + "assembly and sees CS0234 \"the namespace 'Reactor' does not exist\", which looks " + + "like a missing package. Update the guide header (both the unpackaged and packaged " + + "blocks) to match."); + } + } + + /// + /// The unpackaged example must declare WindowsPackageType=None and the packaged one + /// must not — the directive's *placement* is what the section teaches. + /// + /// + /// Asserting the file merely contains the directive somewhere is too weak: the two blocks + /// differ by two lines, so the realistic drift is a copy-paste that moves it between them, + /// not one that deletes it. Moving it would leave a whole-file Contains green while + /// the unpackaged example lost the directive its own table says is mandatory *and* the + /// packaged example silently stopped being packaged. Without the directive an app builds + /// clean and then dies at startup with REGDB_E_CLASSNOTREG, because the WinAppSDK + /// bootstrapper ships in the output but never auto-initializes. + /// + [Fact] + public void PackageTypeDirective_SitsInTheUnpackagedExampleOnly() + { + var guide = File.ReadAllText(Path.Combine(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + // Two header blocks: the unpackaged example, then the packaged delta. If the section is + // restructured into a different number, this needs rereading rather than silently + // passing over whichever blocks happen to remain. + Assert.Equal(2, blocks.Count); + + Assert.True( + blocks[0].Contains("#:property WindowsPackageType=None", StringComparison.Ordinal), + "The first #: header block is the unpackaged example and must declare " + + "'#:property WindowsPackageType=None'. Without it the app builds clean and then " + + "dies at startup with REGDB_E_CLASSNOTREG."); + + Assert.False( + blocks[1].Contains("#:property WindowsPackageType=None", StringComparison.Ordinal), + "The second #: header block is the packaged example and must NOT declare " + + "'#:property WindowsPackageType=None' — dropping that directive is precisely what " + + "opts the app into package identity."); + + // The packaged block earns its name only if something supplies the manifest. + Assert.True( + blocks[1].Contains("Microsoft.Windows.SDK.BuildTools.WinApp", StringComparison.Ordinal), + "The packaged example must reference Microsoft.Windows.SDK.BuildTools.WinApp, whose " + + "targets synthesize the appxmanifest and intercept 'dotnet run'."); + } + + /// + /// The contiguous runs of #: directives in the guide, in document order. + /// + private static List HeaderBlocks(string guide) + { + var blocks = new List(); + var current = new List(); + + foreach (var line in guide.Split('\n').Select(l => l.TrimEnd('\r'))) + { + if (line.StartsWith("#:", StringComparison.Ordinal)) + { + current.Add(line); + } + else if (current.Count > 0) + { + blocks.Add(string.Join('\n', current)); + current.Clear(); + } + } + + if (current.Count > 0) + blocks.Add(string.Join('\n', current)); + + return blocks; + } + + private static string FrameworkTfm(string root) + { + var csproj = File.ReadAllText(Path.Combine(root, FrameworkProject)); + var match = Regex.Match(csproj, @"(?[^<]+)"); + + Assert.True(match.Success, $"No found in {FrameworkProject}."); + return match.Groups["tfm"].Value.Trim(); + } + + private static string RepoRoot() + { + // Anchor on the solution file, not on .git: in a git worktree .git is a *file*, so a + // Directory.Exists probe walks past the root and returns null. Mirrors FindRepoRoot in + // InlineSnippetLedgerTests. + var dir = AppContext.BaseDirectory; + while (dir is not null + && !File.Exists(Path.Combine(dir, "Reactor.slnx")) + && !Directory.Exists(Path.Combine(dir, ".git"))) + { + dir = Path.GetDirectoryName(dir); + } + + Assert.NotNull(dir); + return dir!; + } +} From 840d8ef54a5f0a8072c4063063d7a8016040a23b Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:36:53 -0700 Subject: [PATCH 2/7] Document OutputType=WinExe, the fifth header directive The prose said 'the four properties' while the header carries five, and the table omitted OutputType -- which made the table read as complete while missing a load-bearing line. It is the easiest of the five to drop, because nothing fails: the build succeeds and the app runs. It links for the wrong subsystem instead. Measured on this tree by reading the PE subsystem byte of both builds -- 3 (console) without the directive, 2 (Windows GUI) with it -- so omitting it leaves a console window sitting behind the app's UI for as long as it runs. Guarded in both examples, since the table now claims it. Mutation-checked: removing it from either header block reddens. DocPipeline.Tests 475 passed / 0 failed. Reactor.slnx Release: 0 errors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- .../_pipeline/templates/getting-started.md.dt | 3 ++- docs/guide/getting-started.md | 3 ++- .../SingleFileGuideHeaderTests.cs | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt index 4a4bd0488..9dbe3c94b 100644 --- a/docs/_pipeline/templates/getting-started.md.dt +++ b/docs/_pipeline/templates/getting-started.md.dt @@ -126,12 +126,13 @@ runnable example into an issue. One package reference is enough, exactly as in the [minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the -Windows App SDK in transitively. The four properties are the project-file +Windows App SDK in transitively. The properties below are the project-file settings that have nowhere else to live, and each fails in a way that does not name its own cause: | Directive | Why it is required | |---|---| +| `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | | `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | | `UseWinUI=true` | Brings in the WinUI 3 targets. | | `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | The Windows App SDK needs a concrete architecture; without one the build fails with *"WindowsAppSDKSelfContained requires a supported Windows architecture."* Resolving it from the SDK keeps the file working on x64 and ARM64 alike, rather than pinning it to one. Same line the minimal `.csproj` uses. | diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 25a15259c..1773029dc 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -114,12 +114,13 @@ runnable example into an issue. One package reference is enough, exactly as in the [minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the -Windows App SDK in transitively. The four properties are the project-file +Windows App SDK in transitively. The properties below are the project-file settings that have nowhere else to live, and each fails in a way that does not name its own cause: | Directive | Why it is required | |---|---| +| `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | | `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | | `UseWinUI=true` | Brings in the WinUI 3 targets. | | `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | The Windows App SDK needs a concrete architecture; without one the build fails with *"WindowsAppSDKSelfContained requires a supported Windows architecture."* Resolving it from the SDK keeps the file working on x64 and ARM64 alike, rather than pinning it to one. Same line the minimal `.csproj` uses. | diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs index c03a9e4c8..36c8843ff 100644 --- a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -112,6 +112,33 @@ public void PackageTypeDirective_SitsInTheUnpackagedExampleOnly() + "targets synthesize the appxmanifest and intercept 'dotnet run'."); } + /// + /// Both examples must declare OutputType=WinExe. + /// + /// + /// This one does not fail the build, which is why it is easy to drop: the app still runs. + /// It links for the wrong subsystem instead. Measured on this tree — the PE subsystem byte + /// reads 3 (console) without the directive and 2 (Windows GUI) with it — so omitting it + /// leaves a console window sitting behind the app's UI for its whole lifetime. + /// + [Fact] + public void BothExamples_DeclareWindowsSubsystemOutputType() + { + var guide = File.ReadAllText(Path.Combine(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + Assert.Equal(2, blocks.Count); + + foreach (var block in blocks) + { + Assert.True( + block.Contains("#:property OutputType=WinExe", StringComparison.Ordinal), + "Every single-file header must declare '#:property OutputType=WinExe'. Without it " + + "the build succeeds but links for the console subsystem, so a console window " + + "opens alongside the app's UI."); + } + } + /// /// The contiguous runs of #: directives in the guide, in document order. /// From 58ec80070b136a019414ec1ca1cc84ccc20349c9 Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:50:03 -0700 Subject: [PATCH 3/7] Align the shipped agent-kit skills with the documented single-file header Two findings from review round 2. SKILL.md ships inside the NuGet agent kit and told consumers a different single-file setup than the guide now does: pin Microsoft.WindowsAppSDK, and pass -p:Platform=ARM64 (or x64) on every run. Both work, but the arch flag has to be repeated on each invocation and pins the file to one architecture, and having two shapes for one task is how the docset drifts. Same defect class as the index.md.dt contradiction caught in round 1, except this one ships to users. It now uses the same RuntimeIdentifier header the guide documents and links to the packaged variant. reactor-build-and-check ships too, and its claim that single-file builds require -p:Platform became incomplete rather than wrong once the header can supply the architecture. Its build command and its MSB4025 / NETSDK1136 rows now mention the header alternative alongside the flag, which keeps the existing advice working while pointing at the less fragile option. Also switches the new test file from Path.Combine to Path.Join, which is what this suite already uses (105 occurrences to 36) and is the idiomatic answer to the CodeQL finding: Join concatenates, where Combine silently discards earlier segments if a later one is rooted. Behaviour here is identical, since both constants are compile-time relative. Reactor.Tests 14,205 passed / 0 failed. DocPipeline.Tests 475 passed / 0 failed. AgentKit + WinAppSDKReferenceGuard gates 222 passed. Release: 0 errors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- SKILL.md | 16 +++++++++++----- .../skills/reactor-build-and-check/SKILL.md | 10 ++++++++-- .../SingleFileGuideHeaderTests.cs | 12 ++++++------ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/SKILL.md b/SKILL.md index 86ae6220d..b237489ae 100644 --- a/SKILL.md +++ b/SKILL.md @@ -541,11 +541,11 @@ For lightweight demos, skip the `.csproj` entirely. Add a file-level header: ```csharp #:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.2.0 #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true #:property WindowsPackageType=None +#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) using Microsoft.UI.Reactor; using static Microsoft.UI.Reactor.Factories; @@ -557,10 +557,16 @@ ReactorApp.Run("Hello", ctx => }); ``` -Run with `dotnet run MyApp.cs -p:Platform=ARM64` (or `x64`). In selfhost -the version is `0.0.0-local` — run `mur pack-local` first if the package -isn't found. Outside the clone, replace the version with the published -release you depend on. +Run with `dotnet run MyApp.cs`. The `RuntimeIdentifier` line resolves the +architecture from the SDK, so the same file works on x64 and ARM64 without +`-p:Platform` on every invocation; without it the build fails with +*"WindowsAppSDKSelfContained requires a supported Windows architecture."* +In selfhost the version is `0.0.0-local` — run `mur pack-local` first if the +package isn't found. Outside the clone, replace the version with the published +release you depend on. See +[Getting Started](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) +for the packaged variant, which drops `WindowsPackageType=None` and adds +`Microsoft.Windows.SDK.BuildTools.WinApp` to launch with package identity. > **Always capture `dotnet run` output.** Build errors exit with code 1. > Read compiler output, fix, retry. Don't assume success without checking. diff --git a/plugins/reactor/skills/reactor-build-and-check/SKILL.md b/plugins/reactor/skills/reactor-build-and-check/SKILL.md index 80c1e3f51..313937172 100644 --- a/plugins/reactor/skills/reactor-build-and-check/SKILL.md +++ b/plugins/reactor/skills/reactor-build-and-check/SKILL.md @@ -13,6 +13,12 @@ Run after every non-trivial edit. **Read the output** — `dotnet run` exits wit dotnet run App.cs -p:Platform=ARM64 # or -p:Platform=x64 ``` +Or put `#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` in +the file header and just `dotnet run App.cs` — the architecture resolves from the +SDK, so the same file works on x64 and ARM64. That is the shape the +[Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) +documents. + Single-file builds **do not load analyzers**. You'll catch CS errors but not the Reactor-specific `REACTOR_*` warnings. ### `.csproj` (multi-file, analyzer coverage) @@ -136,8 +142,8 @@ Additional flags: | `CS1061` | error | "'X' does not contain a definition for 'Y'" | A type-specific modifier called on an element type that does not declare it — `.Bold()` / `.FontSize()` / `.TextWrapping()` are `TextBlockElement`-only, so `Button("Go").Bold()` fails. This is **not** an ordering problem: modifiers are generic (` where T : Element`) and preserve the concrete type, so `.Margin(16).Bold()` and `.Bold().Margin(16)` compile alike. Check the receiver's element type against `reactor-dsl/references/reactor.api.txt`. | | `CS0117` | error | "'Element' does not contain a definition for X" | You're calling a factory or static member that doesn't exist — confirm the name against `reactor-dsl/references/reactor.api.txt`. (For a missing *instance* modifier see `CS1061` above.) | | `CS1955` | error | "Non-invocable member 'Element.Margin' cannot be used like a method" | Missing `using Microsoft.UI.Reactor;`. The element records expose same-named *properties* (`Element.Margin`, `Element.Padding`, plus `CornerRadius`/`BorderThickness` on derived records), and C# only falls back to extension methods when none is in scope — so an absent import reports the property as non-invocable rather than saying the fluent modifier isn't imported. It reads like the API doesn't exist; it does. Add the `using`. (Distinct from `REACTOR_DYM_001` above, which is a genuine property invoked with parens, e.g. `GridSize.Auto()`.) | -| `MSB4025` | error | "The project file could not be loaded" | Single-file `.cs` build attempted without `-p:Platform=...` on a WinUI project. Add `-p:Platform=ARM64` (or x64). | -| `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`. | +| `MSB4025` | error | "The project file could not be loaded" | Single-file `.cs` build attempted without an architecture on a WinUI project. Add `-p:Platform=ARM64` (or x64), or set it once in the file header with `#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)`. | +| `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`, or add the `RuntimeIdentifier` header line above. | If a `REACTOR_*` ID isn't in this table, the bundled analyzer DLL has more docs. The descriptions ship in the warnings themselves. diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs index 36c8843ff..5d7af706b 100644 --- a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -48,7 +48,7 @@ public void DocumentedTargetFramework_MatchesTheFrameworkProject() var root = RepoRoot(); var expected = FrameworkTfm(root); - var guide = File.ReadAllText(Path.Combine(root, GuideTemplate)); + var guide = File.ReadAllText(Path.Join(root, GuideTemplate)); var documented = Regex.Matches(guide, @"^#:property\s+TargetFramework=(?\S+)\s*$", RegexOptions.Multiline) .Select(m => m.Groups["tfm"].Value) .ToArray(); @@ -85,7 +85,7 @@ public void DocumentedTargetFramework_MatchesTheFrameworkProject() [Fact] public void PackageTypeDirective_SitsInTheUnpackagedExampleOnly() { - var guide = File.ReadAllText(Path.Combine(RepoRoot(), GuideTemplate)); + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); var blocks = HeaderBlocks(guide); // Two header blocks: the unpackaged example, then the packaged delta. If the section is @@ -124,7 +124,7 @@ public void PackageTypeDirective_SitsInTheUnpackagedExampleOnly() [Fact] public void BothExamples_DeclareWindowsSubsystemOutputType() { - var guide = File.ReadAllText(Path.Combine(RepoRoot(), GuideTemplate)); + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); var blocks = HeaderBlocks(guide); Assert.Equal(2, blocks.Count); @@ -168,7 +168,7 @@ private static List HeaderBlocks(string guide) private static string FrameworkTfm(string root) { - var csproj = File.ReadAllText(Path.Combine(root, FrameworkProject)); + var csproj = File.ReadAllText(Path.Join(root, FrameworkProject)); var match = Regex.Match(csproj, @"(?[^<]+)"); Assert.True(match.Success, $"No found in {FrameworkProject}."); @@ -182,8 +182,8 @@ private static string RepoRoot() // InlineSnippetLedgerTests. var dir = AppContext.BaseDirectory; while (dir is not null - && !File.Exists(Path.Combine(dir, "Reactor.slnx")) - && !Directory.Exists(Path.Combine(dir, ".git"))) + && !File.Exists(Path.Join(dir, "Reactor.slnx")) + && !Directory.Exists(Path.Join(dir, ".git"))) { dir = Path.GetDirectoryName(dir); } From 0456c023ac4e1d0c65c6c414c572c42532315a4f Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:03:15 -0700 Subject: [PATCH 4/7] Guard every directive the header table claims The guard covered TargetFramework, WindowsPackageType and OutputType but not RuntimeIdentifier or UseWinUI, so deleting either from a header would leave the suite green while a reader's dotnet run failed with the architecture error the table documents. Each row of that table is a claim; each is now checked, and the three presence checks share one theory rather than accreting a method apiece. WindowsPackageType stays separate because it is the one directive that must differ between the blocks, so it is asserted for placement rather than presence. Also corrects a stale rationale. The class remarks still said the header's Microsoft.WindowsAppSDK pin was covered by WinAppSDKReferenceGuardTests -- true of an earlier draft, but the headers now carry Microsoft.Windows.SDK.BuildTools.WinApp, which that guard's regex does not match. Rather than claim a guard that does not exist, the remarks now say plainly that the build-tools version is unguarded and why: there is no central pin in this repo to check a doc against, since it is a consumer-side tool rather than a framework dependency. Its presence is asserted; its version is not. Mutation-checked: removing RuntimeIdentifier, UseWinUI, or the build-tools package from either block reddens. DocPipeline.Tests 478 passed / 0 failed. Reactor.slnx Release: 0 errors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- .../SingleFileGuideHeaderTests.cs | 82 +++++++++++++------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs index 5d7af706b..d86c56c5e 100644 --- a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -21,16 +21,23 @@ namespace Microsoft.UI.Reactor.Cli.Docs.Tests; /// them. /// /// -/// The value that actually rots. Two of the header's values are already safe — the Reactor -/// version is the {{reactorVersion}} token the doc compiler substitutes, and the -/// Microsoft.WindowsAppSDK pin is swept by -/// WinAppSDKReferenceGuardTests, which scans .md and .dt for exactly this -/// #:package shape. The target framework is the one left over, and it is the worst of the -/// three to get wrong: a reader who copies a stale Windows version compiles against nothing and +/// The values that actually rot. The Reactor version is the {{reactorVersion}} +/// token the doc compiler substitutes, so it cannot go stale. Every other directive is asserted +/// here, because nothing else looks at them: the target framework against the framework project, +/// and the rest for presence — plus, for WindowsPackageType, placement, since which block +/// carries it is what makes one example packaged and the other not. The target framework is the +/// worst to get wrong: a reader who copies a stale Windows version compiles against nothing and /// gets CS0234: the type or namespace name 'Reactor' does not exist, which reads like a /// missing package rather than a wrong TFM. Measured, not assumed — that is the exact error /// net10.0-windows10.0.19041.0 produces against this package. /// +/// +/// The Microsoft.Windows.SDK.BuildTools.WinApp pin in the packaged example is deliberately +/// not version-guarded. WinAppSDKReferenceGuardTests does sweep #:package +/// headers, but only for Microsoft.WindowsAppSDK…, and this repo has no central pin for +/// the build-tools package to check a doc against — it is a consumer-side tool rather than a +/// framework dependency. Its presence is asserted below; its version is not. +/// /// public class SingleFileGuideHeaderTests { @@ -104,41 +111,68 @@ public void PackageTypeDirective_SitsInTheUnpackagedExampleOnly() "The second #: header block is the packaged example and must NOT declare " + "'#:property WindowsPackageType=None' — dropping that directive is precisely what " + "opts the app into package identity."); - - // The packaged block earns its name only if something supplies the manifest. - Assert.True( - blocks[1].Contains("Microsoft.Windows.SDK.BuildTools.WinApp", StringComparison.Ordinal), - "The packaged example must reference Microsoft.Windows.SDK.BuildTools.WinApp, whose " - + "targets synthesize the appxmanifest and intercept 'dotnet run'."); } /// - /// Both examples must declare OutputType=WinExe. + /// Every directive the guide's table calls load-bearing must appear in both header blocks. /// /// - /// This one does not fail the build, which is why it is easy to drop: the app still runs. - /// It links for the wrong subsystem instead. Measured on this tree — the PE subsystem byte - /// reads 3 (console) without the directive and 2 (Windows GUI) with it — so omitting it - /// leaves a console window sitting behind the app's UI for its whole lifetime. + /// These are asserted together because they share a failure mode: the block is exempt from + /// compilation, so dropping any one of them leaves the whole suite green while a reader's + /// dotnet run fails. Each row of the table is a claim, and each claim is checked. + /// + /// OutputType=WinExe — does not fail the build at all; it links the app for the + /// console subsystem instead. Measured on this tree: the PE subsystem byte reads 3 (console) + /// without it and 2 (Windows GUI) with it, so a console window sits behind the UI. + /// UseWinUI=true — brings in the WinUI 3 targets. + /// RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) — supplies the + /// architecture the Windows App SDK requires, and resolves it from the SDK so the file stays + /// portable across x64 and ARM64. Without it the build fails with "WindowsAppSDKSelfContained + /// requires a supported Windows architecture". + /// + /// WindowsPackageType is deliberately absent from this list — it is the one directive + /// that must differ between the two blocks, so it is checked separately for placement. /// - [Fact] - public void BothExamples_DeclareWindowsSubsystemOutputType() + [Theory] + [InlineData("#:property OutputType=WinExe")] + [InlineData("#:property UseWinUI=true")] + [InlineData("#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)")] + public void BothExamples_DeclareTheLoadBearingDirectives(string directive) { var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); var blocks = HeaderBlocks(guide); Assert.Equal(2, blocks.Count); - foreach (var block in blocks) + for (var i = 0; i < blocks.Count; i++) { Assert.True( - block.Contains("#:property OutputType=WinExe", StringComparison.Ordinal), - "Every single-file header must declare '#:property OutputType=WinExe'. Without it " - + "the build succeeds but links for the console subsystem, so a console window " - + "opens alongside the app's UI."); + blocks[i].Contains(directive, StringComparison.Ordinal), + $"Header block {i + 1} of {GuideTemplate} is missing '{directive}'. The guide's " + + "directive table documents it as required, and this block is exempt from " + + "compilation, so nothing else would catch its removal."); } } + /// + /// The packaged example must reference the package that supplies its manifest. + /// + /// + /// Presence only. See the class remarks for why the version is not pinned here. + /// + [Fact] + public void PackagedExample_ReferencesTheBuildToolsPackage() + { + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + Assert.Equal(2, blocks.Count); + Assert.True( + blocks[1].Contains("Microsoft.Windows.SDK.BuildTools.WinApp", StringComparison.Ordinal), + "The packaged example must reference Microsoft.Windows.SDK.BuildTools.WinApp, whose " + + "targets synthesize the appxmanifest and intercept 'dotnet run'."); + } + /// /// The contiguous runs of #: directives in the guide, in document order. /// From e017beaf0af583e8c3f0bec780756d1bfea539ee Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:15:19 -0700 Subject: [PATCH 5/7] Match the blog's single-file app, header, and command The guide and the published blog quick-start showed the same feature two different ways. The blog's version is the better one, and it is what readers will have seen first, so the guide now matches it: same app, same four-directive header, same `winapp run counter.cs`. RuntimeIdentifier turned out to be conditional, not universal -- which is what prompted this. Measured all three ways: 4 directives + winapp run -> works (winapp passes the architecture through) 4 directives + dotnet run -> "WindowsAppSDKSelfContained requires a supported Windows architecture" 4 + RuntimeIdentifier + WindowsPackageType=None + dotnet run -> works So the line is needed only when `dotnet` drives the build, and the guide had been presenting it as part of the base header. It now sits where it belongs, in a "Running it with plain dotnet" delta, and the primary block is the shortest thing that actually runs. The packaged `dotnet run` path (BuildTools.WinApp intercepting the run) drops from a third code block to a sentence naming the one-line delta. It is a real capability and still documented, but it was the least common of the three and did not earn a full block once `winapp run` leads. Test changes follow the restructure rather than adding coverage: the directives are now asserted per-block -- OutputType/TargetFramework/UseWinUI in the primary header, RuntimeIdentifier/WindowsPackageType in the delta, and each absent from the other. That second half matters because a directive migrating into the primary block would silently fork it from the blog, and a whole-file Contains check stays green through exactly that move. Mutation-checked in both directions, plus the BuildTools mention which is now prose and otherwise unguarded. Also trims the changelog entry. REGDB_E_CLASSNOTREG and CS0234 are the right level of detail for the guide, where a reader is staring at the error; in a release note they were noise. Reactor.Tests 14,205 passed / 0 failed. DocPipeline.Tests 478 passed / 0 failed. Reactor.slnx Release: 0 errors. No image churn. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- CHANGELOG.md | 20 ++-- .../_pipeline/templates/getting-started.md.dt | 70 ++++++------ docs/guide/getting-started.md | 70 ++++++------ .../InlineSnippetLedgerTests.cs | 49 +++++---- .../SingleFileGuideHeaderTests.cs | 102 +++++++++--------- 5 files changed, 146 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb70d407..a6e4132f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,20 +28,12 @@ Conventions for contributors: ### Added -- **Getting Started documents the single-file path (`dotnet run counter.cs`).** - A Reactor app does not need a `.csproj`: .NET 10 runs a lone `.cs` file whose - `#:package` / `#:property` header supplies what a project file otherwise would. - The guide now covers both shapes — unpackaged via `WindowsPackageType=None`, - and packaged by dropping that directive and adding - `Microsoft.Windows.SDK.BuildTools.WinApp`, which intercepts `dotnet run` and - launches with real package identity (winappCli#794 / #874, shipped in winapp - 0.7.0). It also names the two failures whose error text does not point at the - cause: omitting `WindowsPackageType=None` builds clean and then dies at - startup with `REGDB_E_CLASSNOTREG`, and a Windows version below 22621 compiles - against nothing and reports `CS0234: the namespace 'Reactor' does not exist`, - which reads like a missing package. `SingleFileGuideHeaderTests` ties the - documented target framework to `src/Reactor/Reactor.csproj` so that second one - cannot rot back in. +- **Getting Started documents the single-file path.** A Reactor app does not need + a `.csproj`: .NET 10 runs a lone `.cs` file whose `#:package` / `#:property` + header supplies what a project file otherwise would. The guide shows the same + four-directive app the blog quick-start uses, run with `winapp run counter.cs`, + plus the two directives that let the same file run under plain `dotnet run` + (winappCli#794 / #874, shipped in winapp 0.7.0). - **Framework mechanics are searchable in the ReactorGallery index (spec 064, issue #1275).** `find-ui --source reactor` answered "what is control X" but not diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt index 9dbe3c94b..3c8c2be0e 100644 --- a/docs/_pipeline/templates/getting-started.md.dt +++ b/docs/_pipeline/templates/getting-started.md.dt @@ -93,84 +93,80 @@ otherwise would: #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true -#:property WindowsPackageType=None -#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) using Microsoft.UI.Reactor; using Microsoft.UI.Reactor.Core; using static Microsoft.UI.Reactor.Factories; -ReactorApp.Run("Counter"); +ReactorApp.Run("Hello Reactor"); -class Counter : Component +class MyApp : Component { public override Element Render() { var (count, setCount) = UseState(0); - return VStack(12, - TextBlock($"Count: {count}").FontSize(24).Bold(), - Button("Increment", () => setCount(count + 1)) - ).Padding(24); + return VStack( + Heading($"Count: {count}"), + HStack(8, + Button("-", () => setCount(count - 1)), + Button("+", () => setCount(count + 1)) + ) + ); } } ``` +Save it as `counter.cs` and run it: + ```powershell -dotnet run counter.cs +winapp run counter.cs ``` That is the whole app — one file, no scaffolding, no build output to manage. It is the fastest way to try an idea, reproduce a bug, or paste a complete -runnable example into an issue. +runnable example into an issue. `winapp run` builds the file, synthesizes an +appxmanifest from the `#:property` values, and launches it with package +identity. It needs **winapp 0.7.0 or later**. One package reference is enough, exactly as in the [minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the -Windows App SDK in transitively. The properties below are the project-file -settings that have nowhere else to live, and each fails in a way that does not -name its own cause: +Windows App SDK in transitively. The three properties are the project-file +settings that have nowhere else to live: | Directive | Why it is required | |---|---| | `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | | `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | | `UseWinUI=true` | Brings in the WinUI 3 targets. | -| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | The Windows App SDK needs a concrete architecture; without one the build fails with *"WindowsAppSDKSelfContained requires a supported Windows architecture."* Resolving it from the SDK keeps the file working on x64 and ARM64 alike, rather than pinning it to one. Same line the minimal `.csproj` uses. | -| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | -### Single file, packaged +### Running it with plain `dotnet` -Dropping `WindowsPackageType=None` is also how you opt *in* to package identity — -you just need something to supply the manifest. Add the Windows App SDK build -tools and `dotnet run` hands off automatically: +`winapp run` passes the target architecture through to the build. `dotnet run` +does not, so on its own it stops with *"WindowsAppSDKSelfContained requires a +supported Windows architecture"*. Two more directives make the file +self-sufficient: ```csharp -#:package Microsoft.UI.Reactor@{{reactorVersion}} -#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true +#:property WindowsPackageType=None #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) ``` ```powershell dotnet run counter.cs -# [WinAppRunSupport] Intercepted 'dotnet run' for packaged app -# Launching packaged application... ``` -The package synthesizes an appxmanifest from the `#:property` values, registers -a loose layout, and launches the app with real package identity — so -`Package.Current` and the `ms-appx:` scheme work from a single file. It bundles -the CLI it needs, so nothing has to be installed globally. - -If you would rather drive that explicitly, or you already have the -[Windows App SDK CLI](https://github.com/microsoft/winappCli) installed, -`winapp run counter.cs` does the same thing without the extra `#:package` line. -Both need **winapp 0.7.0 or later**. +| Directive | Why it is required | +|---|---| +| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | Supplies the architecture the Windows App SDK needs, resolved from the SDK so the same file works on x64 and ARM64 rather than being pinned to one. Same line the minimal `.csproj` uses. | +| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | -So the header is the switch: keep `WindowsPackageType=None` for a plain -unpackaged window, or drop it and add the build tools for a packaged app. +This shape is unpackaged, so there is no package identity: `Package.Current` and +`ms-appx:` URIs are unavailable. To keep those under `dotnet run`, drop +`WindowsPackageType=None` and add +`#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0` instead — its targets +intercept `dotnet run`, synthesize the manifest, and launch the app packaged, +which is the same thing `winapp run` does without the extra reference. ## Contributor setup (one-time) diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 1773029dc..62ef1d401 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -81,84 +81,80 @@ otherwise would: #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true -#:property WindowsPackageType=None -#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) using Microsoft.UI.Reactor; using Microsoft.UI.Reactor.Core; using static Microsoft.UI.Reactor.Factories; -ReactorApp.Run("Counter"); +ReactorApp.Run("Hello Reactor"); -class Counter : Component +class MyApp : Component { public override Element Render() { var (count, setCount) = UseState(0); - return VStack(12, - TextBlock($"Count: {count}").FontSize(24).Bold(), - Button("Increment", () => setCount(count + 1)) - ).Padding(24); + return VStack( + Heading($"Count: {count}"), + HStack(8, + Button("-", () => setCount(count - 1)), + Button("+", () => setCount(count + 1)) + ) + ); } } ``` +Save it as `counter.cs` and run it: + ```powershell -dotnet run counter.cs +winapp run counter.cs ``` That is the whole app — one file, no scaffolding, no build output to manage. It is the fastest way to try an idea, reproduce a bug, or paste a complete -runnable example into an issue. +runnable example into an issue. `winapp run` builds the file, synthesizes an +appxmanifest from the `#:property` values, and launches it with package +identity. It needs **winapp 0.7.0 or later**. One package reference is enough, exactly as in the [minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the -Windows App SDK in transitively. The properties below are the project-file -settings that have nowhere else to live, and each fails in a way that does not -name its own cause: +Windows App SDK in transitively. The three properties are the project-file +settings that have nowhere else to live: | Directive | Why it is required | |---|---| | `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | | `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | | `UseWinUI=true` | Brings in the WinUI 3 targets. | -| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | The Windows App SDK needs a concrete architecture; without one the build fails with *"WindowsAppSDKSelfContained requires a supported Windows architecture."* Resolving it from the SDK keeps the file working on x64 and ARM64 alike, rather than pinning it to one. Same line the minimal `.csproj` uses. | -| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | -### Single file, packaged +### Running it with plain `dotnet` -Dropping `WindowsPackageType=None` is also how you opt *in* to package identity — -you just need something to supply the manifest. Add the Windows App SDK build -tools and `dotnet run` hands off automatically: +`winapp run` passes the target architecture through to the build. `dotnet run` +does not, so on its own it stops with *"WindowsAppSDKSelfContained requires a +supported Windows architecture"*. Two more directives make the file +self-sufficient: ```csharp -#:package Microsoft.UI.Reactor@0.1.0-preview.16 -#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true +#:property WindowsPackageType=None #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) ``` ```powershell dotnet run counter.cs -# [WinAppRunSupport] Intercepted 'dotnet run' for packaged app -# Launching packaged application... ``` -The package synthesizes an appxmanifest from the `#:property` values, registers -a loose layout, and launches the app with real package identity — so -`Package.Current` and the `ms-appx:` scheme work from a single file. It bundles -the CLI it needs, so nothing has to be installed globally. - -If you would rather drive that explicitly, or you already have the -[Windows App SDK CLI](https://github.com/microsoft/winappCli) installed, -`winapp run counter.cs` does the same thing without the extra `#:package` line. -Both need **winapp 0.7.0 or later**. +| Directive | Why it is required | +|---|---| +| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | Supplies the architecture the Windows App SDK needs, resolved from the SDK so the same file works on x64 and ARM64 rather than being pinned to one. Same line the minimal `.csproj` uses. | +| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | -So the header is the switch: keep `WindowsPackageType=None` for a plain -unpackaged window, or drop it and add the build tools for a packaged app. +This shape is unpackaged, so there is no package identity: `Package.Current` and +`ms-appx:` URIs are unavailable. To keep those under `dotnet run`, drop +`WindowsPackageType=None` and add +`#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0` instead — its targets +intercept `dotnet run`, synthesize the manifest, and launch the app packaged, +which is the same thing `winapp run` does without the extra reference. ## Contributor setup (one-time) diff --git a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs index a7d79a28c..8994c74ac 100644 --- a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs +++ b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs @@ -189,62 +189,61 @@ private static bool IsParameterDeclaration(string parameter) // app — the snippet mechanism extracts from projects. It stays one contiguous // block rather than a ledgered header plus a snippet-backed body because the // section's whole claim is "this is the entire file"; splitting it would show the - // reader two fragments to reassemble. + // reader two fragments to reassemble. It is also kept verbatim in step with the + // published blog's quick-start, so the two cannot drift. // // What that costs is smaller than it looks. Every construct in the body is // exercised by a compiled snippet on this same page — ReactorApp.Run(string), - // Component/Render, UseState, VStack(spacing, …), TextBlock($"…").FontSize(…), - // .Bold() and .Padding(…) all appear in getting-started/hello-world, and + // Component/Render, UseState, VStack, Heading and HStack(spacing, …) all appear + // in getting-started/hello-world and getting-started/layout-basics, and // UseState + Button(string, Action) in getting-started/usestate-counter. An API // rename that broke this block would redden those first. // - // The genuinely unverified part is the six #: directives, and they are covered + // The genuinely unverified part is the #: directives, and they are covered // separately: the Reactor version is the {{reactorVersion}} token the doc // compiler substitutes, and SingleFileGuideHeaderTests pins the TargetFramework - // to src/Reactor/Reactor.csproj. That last one matters because a stale Windows - // version surfaces as CS0234 "the namespace 'Reactor' does not exist", which - // reads like a missing package rather than a wrong TFM. + // to src/Reactor/Reactor.csproj and asserts each remaining directive sits in the + // block that needs it. That TFM check matters because a stale Windows version + // surfaces as CS0234 "the namespace 'Reactor' does not exist", which reads like + // a missing package rather than a wrong TFM. [""" #:package Microsoft.UI.Reactor@{{reactorVersion}} #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true - #:property WindowsPackageType=None - #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) using Microsoft.UI.Reactor; using Microsoft.UI.Reactor.Core; using static Microsoft.UI.Reactor.Factories; - ReactorApp.Run("Counter"); + ReactorApp.Run("Hello Reactor"); - class Counter : Component + class MyApp : Component { public override Element Render() { var (count, setCount) = UseState(0); - return VStack(12, - TextBlock($"Count: {count}").FontSize(24).Bold(), - Button("Increment", () => setCount(count + 1)) - ).Padding(24); + return VStack( + Heading($"Count: {count}"), + HStack(8, + Button("-", () => setCount(count - 1)), + Button("+", () => setCount(count + 1)) + ) + ); } } """] = - "File-based app: no .csproj by definition, so it cannot be a doc app. Its DSL is covered by the compiled hello-world/usestate-counter snippets on the same page; its TFM by SingleFileGuideHeaderTests.", + "File-based app: no .csproj by definition, so it cannot be a doc app. Matches the published blog's quick-start verbatim. Its DSL is covered by the compiled hello-world/usestate-counter/layout-basics snippets on the same page; its TFM by SingleFileGuideHeaderTests.", - // The packaged variant, shown as a header-only fragment because the point is the - // two-line delta from the block above — add the build tools package, drop - // WindowsPackageType=None — not the component body, which is unchanged. + // The two directives that let the same file run under plain `dotnet` instead of + // `winapp run`. A fragment by design: the point is the delta, not a second copy + // of the app. [""" - #:package Microsoft.UI.Reactor@{{reactorVersion}} - #:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 - #:property OutputType=WinExe - #:property TargetFramework=net10.0-windows10.0.22621.0 - #:property UseWinUI=true + #:property WindowsPackageType=None #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) """] = - "Header-only delta for the packaged variant; same file-based-app constraint, TFM guarded by SingleFileGuideHeaderTests.", + "Two-line delta for running the same file under plain dotnet; placement is guarded by SingleFileGuideHeaderTests.", }, ["source-mapping"] = new(StringComparer.Ordinal) diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs index d86c56c5e..7f4e66305 100644 --- a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -77,100 +77,98 @@ public void DocumentedTargetFramework_MatchesTheFrameworkProject() } /// - /// The unpackaged example must declare WindowsPackageType=None and the packaged one - /// must not — the directive's *placement* is what the section teaches. + /// The primary example carries the full header; the second block is the dotnet run + /// delta and must carry exactly the two directives that delta exists to add. /// /// - /// Asserting the file merely contains the directive somewhere is too weak: the two blocks - /// differ by two lines, so the realistic drift is a copy-paste that moves it between them, - /// not one that deletes it. Moving it would leave a whole-file Contains green while - /// the unpackaged example lost the directive its own table says is mandatory *and* the - /// packaged example silently stopped being packaged. Without the directive an app builds - /// clean and then dies at startup with REGDB_E_CLASSNOTREG, because the WinAppSDK - /// bootstrapper ships in the output but never auto-initializes. + /// The section's whole shape is "here is the file, and here are the two lines you add to run + /// it without winapp". If either directive migrated into the primary block, that block + /// would stop matching the blog and the published quick-start, and the delta would no longer + /// be a delta — while a whole-file Contains check stayed green through the move. So + /// placement is asserted, not just presence. /// [Fact] - public void PackageTypeDirective_SitsInTheUnpackagedExampleOnly() + public void DotnetDeltaDirectives_SitInTheSecondBlockOnly() { var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); var blocks = HeaderBlocks(guide); - // Two header blocks: the unpackaged example, then the packaged delta. If the section is + // Two header blocks: the complete file, then the dotnet-run delta. If the section is // restructured into a different number, this needs rereading rather than silently // passing over whichever blocks happen to remain. Assert.Equal(2, blocks.Count); - Assert.True( - blocks[0].Contains("#:property WindowsPackageType=None", StringComparison.Ordinal), - "The first #: header block is the unpackaged example and must declare " - + "'#:property WindowsPackageType=None'. Without it the app builds clean and then " - + "dies at startup with REGDB_E_CLASSNOTREG."); - - Assert.False( - blocks[1].Contains("#:property WindowsPackageType=None", StringComparison.Ordinal), - "The second #: header block is the packaged example and must NOT declare " - + "'#:property WindowsPackageType=None' — dropping that directive is precisely what " - + "opts the app into package identity."); + foreach (var directive in new[] + { + "#:property WindowsPackageType=None", + "#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)", + }) + { + Assert.True( + blocks[1].Contains(directive, StringComparison.Ordinal), + $"The second #: block is the 'run it with plain dotnet' delta and must declare " + + $"'{directive}'. Without RuntimeIdentifier the build stops with " + + "\"WindowsAppSDKSelfContained requires a supported Windows architecture\"; " + + "without WindowsPackageType=None the app builds clean and then dies at startup " + + "with REGDB_E_CLASSNOTREG."); + + Assert.False( + blocks[0].Contains(directive, StringComparison.Ordinal), + $"The first #: block is the complete single-file app as `winapp run` needs it, " + + $"and must NOT declare '{directive}'. winapp supplies the architecture itself, " + + "and that four-directive header is what the published blog and quick-start " + + "show — adding a line here silently forks them."); + } } /// - /// Every directive the guide's table calls load-bearing must appear in both header blocks. + /// Every directive the primary header's table calls load-bearing must appear in it. /// /// - /// These are asserted together because they share a failure mode: the block is exempt from - /// compilation, so dropping any one of them leaves the whole suite green while a reader's - /// dotnet run fails. Each row of the table is a claim, and each claim is checked. + /// These share a failure mode: the block is exempt from compilation, so dropping any one of + /// them leaves the whole suite green while a reader's run fails. Each row of the table is a + /// claim, and each claim is checked. /// /// OutputType=WinExe — does not fail the build at all; it links the app for the /// console subsystem instead. Measured on this tree: the PE subsystem byte reads 3 (console) /// without it and 2 (Windows GUI) with it, so a console window sits behind the UI. + /// TargetFramework — also checked against the framework project by + /// ; this pins its presence + /// in the primary block specifically. /// UseWinUI=true — brings in the WinUI 3 targets. - /// RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) — supplies the - /// architecture the Windows App SDK requires, and resolves it from the SDK so the file stays - /// portable across x64 and ARM64. Without it the build fails with "WindowsAppSDKSelfContained - /// requires a supported Windows architecture". /// - /// WindowsPackageType is deliberately absent from this list — it is the one directive - /// that must differ between the two blocks, so it is checked separately for placement. /// [Theory] [InlineData("#:property OutputType=WinExe")] + [InlineData("#:property TargetFramework=")] [InlineData("#:property UseWinUI=true")] - [InlineData("#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)")] - public void BothExamples_DeclareTheLoadBearingDirectives(string directive) + public void PrimaryHeader_DeclaresTheLoadBearingDirectives(string directive) { var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); var blocks = HeaderBlocks(guide); Assert.Equal(2, blocks.Count); - - for (var i = 0; i < blocks.Count; i++) - { - Assert.True( - blocks[i].Contains(directive, StringComparison.Ordinal), - $"Header block {i + 1} of {GuideTemplate} is missing '{directive}'. The guide's " - + "directive table documents it as required, and this block is exempt from " - + "compilation, so nothing else would catch its removal."); - } + Assert.True( + blocks[0].Contains(directive, StringComparison.Ordinal), + $"The primary single-file header in {GuideTemplate} is missing '{directive}'. The " + + "guide's directive table documents it as required, and this block is exempt from " + + "compilation, so nothing else would catch its removal."); } /// - /// The packaged example must reference the package that supplies its manifest. + /// The section must still name the package that turns dotnet run into a packaged + /// launch. /// /// - /// Presence only. See the class remarks for why the version is not pinned here. + /// It is prose rather than a third header block, so nothing else would notice it going + /// missing. Presence only — see the class remarks for why the version is not pinned. /// [Fact] - public void PackagedExample_ReferencesTheBuildToolsPackage() + public void Section_NamesTheBuildToolsPackageForPackagedDotnetRun() { var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); - var blocks = HeaderBlocks(guide); - Assert.Equal(2, blocks.Count); - Assert.True( - blocks[1].Contains("Microsoft.Windows.SDK.BuildTools.WinApp", StringComparison.Ordinal), - "The packaged example must reference Microsoft.Windows.SDK.BuildTools.WinApp, whose " - + "targets synthesize the appxmanifest and intercept 'dotnet run'."); + Assert.Contains("Microsoft.Windows.SDK.BuildTools.WinApp", guide, StringComparison.Ordinal); } /// From e7c2894c48e5a9423487931f11c17e1d6fc562fd Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Mon, 28 Sep 2026 14:38:19 -0700 Subject: [PATCH 6/7] Depend on WindowsAppSDK.Runtime; package single-file apps by default Apps that referenced only Microsoft.UI.Reactor were silently built self-contained. Since #822 the package carried Microsoft.WindowsAppSDK.WinUI alone, and without Microsoft.WindowsAppSDK.Runtime the Windows App SDK's Base.targets defaults WindowsAppSDKSelfContained to true, which then demands an architecture: `dotnet run` without -r or -p:Platform failed with "WindowsAppSDKSelfContained requires a supported Windows architecture". Directory.Build.targets now gives every framework-dependent WinUI project, libraries included, WinUI + Runtime, so the published package flows Runtime to consumer apps. WinAppSDKReferenceGuardTests asserts that Reactor resolves Runtime and does not mark it private; restricting Runtime to apps again, or making it private, reddens the guard. With apps framework-dependent again, the single-file header in Getting Started adds Microsoft.Windows.SDK.BuildTools.WinApp, so `dotnet run counter.cs` launches the app packaged with no architecture flag, the same as `winapp run counter.cs`. Both runners obey WindowsPackageType: None runs the app unpackaged, and with neither that nor the BuildTools package, plain `dotnet run` dies at startup with REGDB_E_CLASSNOTREG. Bundling the runtime is documented as the WindowsAppSDKSelfContained=true opt-in, which is what needs -r; -r alone bundles nothing. SKILL.md and reactor-build-and-check teach the same header, with WindowsPackageType=None as the fallback when Developer Mode is off. CONTRIBUTING, spec 022 and the packaging guide describe the new dependency. Corrects earlier commits on this branch, which were measured against the self-contained preview.16: the self-contained default came from #822, not from a Windows App SDK change, and WindowsPackageType=None was a no-op for those apps rather than the cause of REGDB_E_CLASSNOTREG. That crash only applies to framework-dependent apps launched as a plain exe, which Reactor-only apps are again after this change. Measured against a locally packed Reactor with the Runtime dependency: the guide's header runs packaged under both `dotnet run` and `winapp run`, and unpackaged under both with WindowsPackageType=None; an explicit WindowsAppSDKSelfContained=true app still builds and runs with -r win-x64. Reactor.slnx Release: 0 errors. DocPipeline.Tests 479 passed / 0 failed. WinAppSDKReferenceGuard, agent-kit, skill-prose and search-index gates pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- CHANGELOG.md | 16 ++- CONTRIBUTING.md | 2 +- Directory.Build.targets | 27 +++-- SKILL.md | 23 ++-- .../_pipeline/templates/getting-started.md.dt | 50 ++++---- docs/_pipeline/templates/packaging.md.dt | 10 +- docs/guide/getting-started.md | 50 ++++---- docs/guide/packaging.md | 10 +- docs/specs/022-packaging-and-distribution.md | 2 +- .../skills/reactor-build-and-check/SKILL.md | 17 +-- .../InlineSnippetLedgerTests.cs | 11 +- .../SingleFileGuideHeaderTests.cs | 110 ++++++++++-------- .../WinAppSDKReferenceGuardTests.cs | 46 ++++++-- 13 files changed, 214 insertions(+), 160 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e4132f3..5f8a2c3be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,10 +30,11 @@ Conventions for contributors: - **Getting Started documents the single-file path.** A Reactor app does not need a `.csproj`: .NET 10 runs a lone `.cs` file whose `#:package` / `#:property` - header supplies what a project file otherwise would. The guide shows the same - four-directive app the blog quick-start uses, run with `winapp run counter.cs`, - plus the two directives that let the same file run under plain `dotnet run` - (winappCli#794 / #874, shipped in winapp 0.7.0). + header supplies what a project file otherwise would. The header also references + `Microsoft.Windows.SDK.BuildTools.WinApp`, so `dotnet run counter.cs` launches + the app packaged, just like `winapp run counter.cs`; `WindowsPackageType=None` + runs it unpackaged (winappCli#794 / #874, shipped in winapp 0.7.0). The + agent-kit skills use the same header. - **Framework mechanics are searchable in the ReactorGallery index (spec 064, issue #1275).** `find-ui --source reactor` answered "what is control X" but not @@ -121,6 +122,13 @@ Conventions for contributors: ### Fixed +- **Apps that reference only `Microsoft.UI.Reactor` are framework-dependent + again** (regression from #822). The package now depends on + `Microsoft.WindowsAppSDK.Runtime`; without it the Windows App SDK silently + built such apps self-contained, so a plain `dotnet run` failed with + *"WindowsAppSDKSelfContained requires a supported Windows architecture"*. To + keep bundling the Windows App Runtime, set `WindowsAppSDKSelfContained=true`. + - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested `ManipulationDelta` shape rather than Reactor's flat gesture structs, and the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0e8e50e0..47935e61f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope `Directory.Build.props`) - Visual Studio 2022 (17.8+) or VS Code with C# Dev Kit -> **Windows App SDK packages:** The right package is injected centrally by `Directory.Build.targets` based on each project's shape: framework-dependent **libraries** (including the shipped `Microsoft.UI.Reactor`) reference the lean `Microsoft.WindowsAppSDK.WinUI` sub-package; framework-dependent **apps** additionally reference `Microsoft.WindowsAppSDK.Runtime`; **self-contained / MSIX** projects keep the full `Microsoft.WindowsAppSDK` metapackage (it bundles the runtime + DWriteCore redist). The metapackage's AI/ML/Widgets/DWrite slices are otherwise unused. Versions are pinned in `Directory.Build.props` (`WindowsAppSDKWinUIVersion` and `WindowsAppSDKVersion`) — update them there to change the version for every project at once. +> **Windows App SDK packages:** The right package is injected centrally by `Directory.Build.targets` based on each project's shape: framework-dependent projects — **libraries and apps alike**, including the shipped `Microsoft.UI.Reactor` — reference the lean `Microsoft.WindowsAppSDK.WinUI` sub-package plus `Microsoft.WindowsAppSDK.Runtime` (a library's dependencies become its consumers', and without Runtime the SDK silently builds a consumer app self-contained); **self-contained / MSIX** projects keep the full `Microsoft.WindowsAppSDK` metapackage (it bundles the runtime + DWriteCore redist). The metapackage's AI/ML/Widgets/DWrite slices are otherwise unused. Versions are pinned in `Directory.Build.props` (`WindowsAppSDKWinUIVersion` and `WindowsAppSDKVersion`) — update them there to change the version for every project at once. > **Clone to a short path (Windows):** Clone into a short root directory such as > `C:\src\` (for example `C:\src\microsoft-ui-reactor`) rather than a deeply diff --git a/Directory.Build.targets b/Directory.Build.targets index 5ec9801ce..61eb474e0 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -52,17 +52,24 @@ which transitively brings Base + Foundation + InteractiveExperiences + WebView2 (the bootstrapper, deployment manager, MRT Core and app-lifecycle MSBuild targets). The metapackage also aggregates AI / ML / Widgets / DWrite, which the - repo never uses — so framework-dependent projects reference only - Microsoft.WindowsAppSDK.WinUI and let the machine-wide installed - Microsoft.WindowsAppRuntime.2 supply native bits (DWriteCore, etc.) at runtime. + repo never uses — so framework-dependent projects reference + Microsoft.WindowsAppSDK.WinUI + Microsoft.WindowsAppSDK.Runtime rather than the + metapackage, and let the machine-wide installed Microsoft.WindowsAppRuntime.2 + supply native bits (DWriteCore, etc.) at runtime. + + Runtime is what makes an app framework-dependent: it sets + MicrosoftWindowsAppSDKPackageDir, and without it the SDK's + Microsoft.WindowsAppSDK.Base.targets silently defaults WindowsAppSDKSelfContained + to true (it only errors when a project sets that property to something other + than true explicitly). Libraries take it too, even though they don't produce an + app, because a library's dependencies become its consumers' dependencies: when + the published Microsoft.UI.Reactor carried WinUI alone, every app that + referenced only Reactor was silently built self-contained, which also demands a + RuntimeIdentifier. Projects that BUNDLE the native runtime keep the full metapackage, which carries the Runtime redist + DWriteCore: WindowsAppSDKSelfContained=true, or - framework-dependent MSIX (WindowsPackageType=MSIX). A framework-dependent WinUI - *executable* additionally needs Microsoft.WindowsAppSDK.Runtime — the SDK's - Microsoft.WindowsAppSDK.Base.targets rejects a framework-dependent app that - references neither Runtime nor WindowsAppSDKSelfContained. Libraries don't produce - an app, so they take the lean WinUI reference alone. + framework-dependent MSIX (WindowsPackageType=MSIX). Evaluating the conditions here in .targets (a late import) means per-project OutputType / WindowsAppSDKSelfContained / WindowsPackageType values are already @@ -80,14 +87,12 @@ <_ReactorWinAppSDKMetapackage>false <_ReactorWinAppSDKMetapackage Condition="'$(WindowsAppSDKSelfContained)' == 'true' or '$(WindowsPackageType)' == 'MSIX'">true - <_ReactorWinAppSDKFrameworkDependentApp>false - <_ReactorWinAppSDKFrameworkDependentApp Condition="'$(_ReactorWinAppSDKMetapackage)' != 'true' and ('$(OutputType)' == 'Exe' or '$(OutputType)' == 'WinExe')">true - + diff --git a/SKILL.md b/SKILL.md index b237489ae..217fb33e4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -541,11 +541,10 @@ For lightweight demos, skip the `.csproj` entirely. Add a file-level header: ```csharp #:package Microsoft.UI.Reactor@0.0.0-local +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true -#:property WindowsPackageType=None -#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) using Microsoft.UI.Reactor; using static Microsoft.UI.Reactor.Factories; @@ -557,16 +556,20 @@ ReactorApp.Run("Hello", ctx => }); ``` -Run with `dotnet run MyApp.cs`. The `RuntimeIdentifier` line resolves the -architecture from the SDK, so the same file works on x64 and ARM64 without -`-p:Platform` on every invocation; without it the build fails with -*"WindowsAppSDKSelfContained requires a supported Windows architecture."* +Run with `dotnet run MyApp.cs` — no architecture flag. This is the header the +[Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) +documents: `Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` launch +the app packaged, with package identity, which needs Developer Mode. If +Developer Mode is off, or you don't need identity, add +`#:property WindowsPackageType=None` to run it as a plain unpackaged `.exe`. +With neither, the app builds, then dies at startup with `REGDB_E_CLASSNOTREG`. +Either way the app uses the Windows App Runtime installed on the machine. To +bundle it instead, add `#:property WindowsAppSDKSelfContained=true`; that mode +also needs an architecture (`-r win-x64` or `-r win-arm64`), and `-r` alone +bundles nothing. In selfhost the version is `0.0.0-local` — run `mur pack-local` first if the package isn't found. Outside the clone, replace the version with the published -release you depend on. See -[Getting Started](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) -for the packaged variant, which drops `WindowsPackageType=None` and adds -`Microsoft.Windows.SDK.BuildTools.WinApp` to launch with package identity. +release you depend on. > **Always capture `dotnet run` output.** Build errors exit with code 1. > Read compiler output, fix, retry. Don't assume success without checking. diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt index 3c8c2be0e..253cfa5a8 100644 --- a/docs/_pipeline/templates/getting-started.md.dt +++ b/docs/_pipeline/templates/getting-started.md.dt @@ -90,6 +90,7 @@ otherwise would: ```csharp #:package Microsoft.UI.Reactor@{{reactorVersion}} +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true @@ -120,53 +121,46 @@ class MyApp : Component Save it as `counter.cs` and run it: ```powershell -winapp run counter.cs +dotnet run counter.cs ``` That is the whole app — one file, no scaffolding, no build output to manage. It is the fastest way to try an idea, reproduce a bug, or paste a complete -runnable example into an issue. `winapp run` builds the file, synthesizes an -appxmanifest from the `#:property` values, and launches it with package -identity. It needs **winapp 0.7.0 or later**. +runnable example into an issue. Like a scaffolded app, it runs packaged: +`Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` synthesize an +appxmanifest from the `#:property` values, register the app, and launch it with +package identity, which needs **Developer Mode**. If you have the +[winapp CLI](https://github.com/microsoft/winappCli) 0.7.0 or later, +`winapp run counter.cs` does the same. -One package reference is enough, exactly as in the -[minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the -Windows App SDK in transitively. The three properties are the project-file -settings that have nowhere else to live: +`Microsoft.UI.Reactor` brings the Windows App SDK in transitively, exactly as in +the [minimal `.csproj`](index.md#minimal-setup). The rest of the header: | Directive | Why it is required | |---|---| +| `Microsoft.Windows.SDK.BuildTools.WinApp` | Lets `dotnet run` package the app. It carries its own copy of the winapp CLI, so that does not need installing. Without it, plain `dotnet run` cannot package the app, and it dies at startup with `COMException … REGDB_E_CLASSNOTREG` — an error that says nothing about the missing package — unless you run it unpackaged (below). | | `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | | `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | | `UseWinUI=true` | Brings in the WinUI 3 targets. | -### Running it with plain `dotnet` +### Running it unpackaged -`winapp run` passes the target architecture through to the build. `dotnet run` -does not, so on its own it stops with *"WindowsAppSDKSelfContained requires a -supported Windows architecture"*. Two more directives make the file -self-sufficient: +`WindowsPackageType` decides whether the app runs packaged, and `dotnet run` and +`winapp run` both obey it. Declare the app unpackaged to run it as a plain `.exe`: ```csharp #:property WindowsPackageType=None -#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) ``` -```powershell -dotnet run counter.cs -``` +An unpackaged app has no package identity, so `Package.Current` and `ms-appx:` +URIs are unavailable. -| Directive | Why it is required | -|---|---| -| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | Supplies the architecture the Windows App SDK needs, resolved from the SDK so the same file works on x64 and ARM64 rather than being pinned to one. Same line the minimal `.csproj` uses. | -| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | - -This shape is unpackaged, so there is no package identity: `Package.Current` and -`ms-appx:` URIs are unavailable. To keep those under `dotnet run`, drop -`WindowsPackageType=None` and add -`#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0` instead — its targets -intercept `dotnet run`, synthesize the manifest, and launch the app packaged, -which is the same thing `winapp run` does without the extra reference. +Either way the app is framework-dependent: it uses the Windows App Runtime +installed on the machine. To carry it with the app instead, add +`#:property WindowsAppSDKSelfContained=true`. That mode also needs the +architecture named with `-r win-x64` or `-r win-arm64`, because the runtime it +copies is native code built separately for each CPU — `-r` on its own does not +bundle anything. ## Contributor setup (one-time) diff --git a/docs/_pipeline/templates/packaging.md.dt b/docs/_pipeline/templates/packaging.md.dt index 5fac5b182..1c1ec0f8b 100644 --- a/docs/_pipeline/templates/packaging.md.dt +++ b/docs/_pipeline/templates/packaging.md.dt @@ -71,9 +71,10 @@ machines, with ARM64 second for Snapdragon X). The sub-package brings the WinUI 3 SDK — reference assemblies plus the MSBuild build/props/targets — while the native WinUI runtime is supplied by the machine-wide Windows App Runtime install (or bundled into the publish output -when `WindowsAppSDKSelfContained=true`). That sub-package is what a consumer -gets transitively from `Microsoft.UI.Reactor`; the scaffolded template adds the -full +when `WindowsAppSDKSelfContained=true`). That sub-package, together with +`Microsoft.WindowsAppSDK.Runtime` (which keeps the app framework-dependent), is +what a consumer gets transitively from `Microsoft.UI.Reactor`; the scaffolded +template adds the full [`Microsoft.WindowsAppSDK`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK) metapackage on top at scaffold time (see below), because the self-contained shape needs the Runtime redist the metapackage carries. Inside this repo the @@ -295,7 +296,8 @@ frozen when the template shipped. The two references then sit side-by-side in your CSPROJ, so the SDK version stays an obvious knob — edit it there when you need a specific WinUI patch. If you skip the template and reference `Microsoft.UI.Reactor` by hand, the SDK still -arrives transitively as `Microsoft.WindowsAppSDK.WinUI`; add the full +arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus +`Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full metapackage yourself when you want `WindowsAppSDKSelfContained=true` or an MSIX, both of which need the Runtime redist. The repo-internal `WindowsAppSDKVersion` MSBuild property only governs projects under this diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 62ef1d401..6858a45f1 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -78,6 +78,7 @@ otherwise would: ```csharp #:package Microsoft.UI.Reactor@0.1.0-preview.16 +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true @@ -108,53 +109,46 @@ class MyApp : Component Save it as `counter.cs` and run it: ```powershell -winapp run counter.cs +dotnet run counter.cs ``` That is the whole app — one file, no scaffolding, no build output to manage. It is the fastest way to try an idea, reproduce a bug, or paste a complete -runnable example into an issue. `winapp run` builds the file, synthesizes an -appxmanifest from the `#:property` values, and launches it with package -identity. It needs **winapp 0.7.0 or later**. +runnable example into an issue. Like a scaffolded app, it runs packaged: +`Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` synthesize an +appxmanifest from the `#:property` values, register the app, and launch it with +package identity, which needs **Developer Mode**. If you have the +[winapp CLI](https://github.com/microsoft/winappCli) 0.7.0 or later, +`winapp run counter.cs` does the same. -One package reference is enough, exactly as in the -[minimal `.csproj`](index.md#minimal-setup) — `Microsoft.UI.Reactor` brings the -Windows App SDK in transitively. The three properties are the project-file -settings that have nowhere else to live: +`Microsoft.UI.Reactor` brings the Windows App SDK in transitively, exactly as in +the [minimal `.csproj`](index.md#minimal-setup). The rest of the header: | Directive | Why it is required | |---|---| +| `Microsoft.Windows.SDK.BuildTools.WinApp` | Lets `dotnet run` package the app. It carries its own copy of the winapp CLI, so that does not need installing. Without it, plain `dotnet run` cannot package the app, and it dies at startup with `COMException … REGDB_E_CLASSNOTREG` — an error that says nothing about the missing package — unless you run it unpackaged (below). | | `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | | `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | | `UseWinUI=true` | Brings in the WinUI 3 targets. | -### Running it with plain `dotnet` +### Running it unpackaged -`winapp run` passes the target architecture through to the build. `dotnet run` -does not, so on its own it stops with *"WindowsAppSDKSelfContained requires a -supported Windows architecture"*. Two more directives make the file -self-sufficient: +`WindowsPackageType` decides whether the app runs packaged, and `dotnet run` and +`winapp run` both obey it. Declare the app unpackaged to run it as a plain `.exe`: ```csharp #:property WindowsPackageType=None -#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) ``` -```powershell -dotnet run counter.cs -``` +An unpackaged app has no package identity, so `Package.Current` and `ms-appx:` +URIs are unavailable. -| Directive | Why it is required | -|---|---| -| `RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` | Supplies the architecture the Windows App SDK needs, resolved from the SDK so the same file works on x64 and ARM64 rather than being pinned to one. Same line the minimal `.csproj` uses. | -| `WindowsPackageType=None` | Declares the app unpackaged so the WinAppSDK bootstrapper initializes. **Omit it and the app builds cleanly, then dies at startup with `COMException … REGDB_E_CLASSNOTREG`** — the bootstrapper ships in the output but never runs. That error says nothing about the missing directive, so it is worth recognizing. | - -This shape is unpackaged, so there is no package identity: `Package.Current` and -`ms-appx:` URIs are unavailable. To keep those under `dotnet run`, drop -`WindowsPackageType=None` and add -`#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0` instead — its targets -intercept `dotnet run`, synthesize the manifest, and launch the app packaged, -which is the same thing `winapp run` does without the extra reference. +Either way the app is framework-dependent: it uses the Windows App Runtime +installed on the machine. To carry it with the app instead, add +`#:property WindowsAppSDKSelfContained=true`. That mode also needs the +architecture named with `-r win-x64` or `-r win-arm64`, because the runtime it +copies is native code built separately for each CPU — `-r` on its own does not +bundle anything. ## Contributor setup (one-time) diff --git a/docs/guide/packaging.md b/docs/guide/packaging.md index d3d89de3d..45eb54613 100644 --- a/docs/guide/packaging.md +++ b/docs/guide/packaging.md @@ -65,9 +65,10 @@ machines, with ARM64 second for Snapdragon X). The sub-package brings the WinUI 3 SDK — reference assemblies plus the MSBuild build/props/targets — while the native WinUI runtime is supplied by the machine-wide Windows App Runtime install (or bundled into the publish output -when `WindowsAppSDKSelfContained=true`). That sub-package is what a consumer -gets transitively from `Microsoft.UI.Reactor`; the scaffolded template adds the -full +when `WindowsAppSDKSelfContained=true`). That sub-package, together with +`Microsoft.WindowsAppSDK.Runtime` (which keeps the app framework-dependent), is +what a consumer gets transitively from `Microsoft.UI.Reactor`; the scaffolded +template adds the full [`Microsoft.WindowsAppSDK`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK) metapackage on top at scaffold time (see below), because the self-contained shape needs the Runtime redist the metapackage carries. Inside this repo the @@ -313,7 +314,8 @@ frozen when the template shipped. The two references then sit side-by-side in your CSPROJ, so the SDK version stays an obvious knob — edit it there when you need a specific WinUI patch. If you skip the template and reference `Microsoft.UI.Reactor` by hand, the SDK still -arrives transitively as `Microsoft.WindowsAppSDK.WinUI`; add the full +arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus +`Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full metapackage yourself when you want `WindowsAppSDKSelfContained=true` or an MSIX, both of which need the Runtime redist. The repo-internal `WindowsAppSDKVersion` MSBuild property only governs projects under this diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index e6958a8ce..5febb91b5 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -430,7 +430,7 @@ Consumer gets: framework, analyzers, source generator, and WinUI SDK (transitive - **Internal feed ownership.** Which Azure Artifacts organization hosts the P1 feed? Creating one takes ~a day plus approvals. - **Signing prerequisite for P1.** Does the chosen internal feed enforce signed packages? If yes, P1 needs ESRP too, not just P2. - **Package ID.** `Microsoft.UI.Reactor` assumes we stay in the `Microsoft.UI.*` namespace (see spec 018 for the namespace rename). If that namespace decision changes, the package ID follows. -- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent libraries — including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`), so the published package flows only WinUI to consumers rather than the full metapackage (AI/ML/Widgets/DWrite); framework-dependent apps additionally reference `Microsoft.WindowsAppSDK.Runtime`; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a different WinUI version can reference their own `Microsoft.WindowsAppSDK*` version — NuGet unifies it transitively with the library's. +- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent projects — libraries and apps alike, including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`) plus `Microsoft.WindowsAppSDK.Runtime` rather than the full metapackage (AI/ML/Widgets/DWrite), so the published package flows WinUI + Runtime to consumers — without Runtime the SDK silently builds a consumer app self-contained; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a different WinUI version can reference their own `Microsoft.WindowsAppSDK*` version — NuGet unifies it transitively with the library's. - **`mur` install-script trust boundary.** `iwr | iex` from GitHub Releases works for P1 but will concern P3 users. Document the signed-binary fallback (direct download + verify signature) before public launch. ## 14. Implementation Phases diff --git a/plugins/reactor/skills/reactor-build-and-check/SKILL.md b/plugins/reactor/skills/reactor-build-and-check/SKILL.md index 313937172..2e15d8194 100644 --- a/plugins/reactor/skills/reactor-build-and-check/SKILL.md +++ b/plugins/reactor/skills/reactor-build-and-check/SKILL.md @@ -10,14 +10,17 @@ Run after every non-trivial edit. **Read the output** — `dotnet run` exits wit ### Single-file `.cs` (default for new apps) ```powershell -dotnet run App.cs -p:Platform=ARM64 # or -p:Platform=x64 +dotnet run App.cs ``` -Or put `#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)` in -the file header and just `dotnet run App.cs` — the architecture resolves from the -SDK, so the same file works on x64 and ARM64. That is the shape the +The file header needs `#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0`, +which launches the app packaged (needs Developer Mode), or +`#:property WindowsPackageType=None`, which runs it unpackaged. With neither, +the app builds, then dies at startup with `REGDB_E_CLASSNOTREG`. No +`-p:Platform` is needed — only a self-contained app +(`#:property WindowsAppSDKSelfContained=true`) needs an architecture. The [Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) -documents. +documents the packaged header. Single-file builds **do not load analyzers**. You'll catch CS errors but not the Reactor-specific `REACTOR_*` warnings. @@ -142,8 +145,8 @@ Additional flags: | `CS1061` | error | "'X' does not contain a definition for 'Y'" | A type-specific modifier called on an element type that does not declare it — `.Bold()` / `.FontSize()` / `.TextWrapping()` are `TextBlockElement`-only, so `Button("Go").Bold()` fails. This is **not** an ordering problem: modifiers are generic (` where T : Element`) and preserve the concrete type, so `.Margin(16).Bold()` and `.Bold().Margin(16)` compile alike. Check the receiver's element type against `reactor-dsl/references/reactor.api.txt`. | | `CS0117` | error | "'Element' does not contain a definition for X" | You're calling a factory or static member that doesn't exist — confirm the name against `reactor-dsl/references/reactor.api.txt`. (For a missing *instance* modifier see `CS1061` above.) | | `CS1955` | error | "Non-invocable member 'Element.Margin' cannot be used like a method" | Missing `using Microsoft.UI.Reactor;`. The element records expose same-named *properties* (`Element.Margin`, `Element.Padding`, plus `CornerRadius`/`BorderThickness` on derived records), and C# only falls back to extension methods when none is in scope — so an absent import reports the property as non-invocable rather than saying the fluent modifier isn't imported. It reads like the API doesn't exist; it does. Add the `using`. (Distinct from `REACTOR_DYM_001` above, which is a genuine property invoked with parens, e.g. `GridSize.Auto()`.) | -| `MSB4025` | error | "The project file could not be loaded" | Single-file `.cs` build attempted without an architecture on a WinUI project. Add `-p:Platform=ARM64` (or x64), or set it once in the file header with `#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)`. | -| `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`, or add the `RuntimeIdentifier` header line above. | +| `MSB4025` | error | "The project file could not be loaded" | Self-contained (`WindowsAppSDKSelfContained=true`) single-file `.cs` build attempted without an architecture. Add `-p:Platform=ARM64` (or x64), or `-r win-arm64` / `-r win-x64`. | +| `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`. | If a `REACTOR_*` ID isn't in this table, the bundled analyzer DLL has more docs. The descriptions ship in the warnings themselves. diff --git a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs index 8994c74ac..dc65c78de 100644 --- a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs +++ b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs @@ -208,6 +208,7 @@ private static bool IsParameterDeclaration(string parameter) // a missing package rather than a wrong TFM. [""" #:package Microsoft.UI.Reactor@{{reactorVersion}} + #:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true @@ -234,16 +235,14 @@ public override Element Render() } } """] = - "File-based app: no .csproj by definition, so it cannot be a doc app. Matches the published blog's quick-start verbatim. Its DSL is covered by the compiled hello-world/usestate-counter/layout-basics snippets on the same page; its TFM by SingleFileGuideHeaderTests.", + "File-based app: no .csproj by definition, so it cannot be a doc app. The published blog's quick-start app, plus the BuildTools package that lets dotnet run launch it packaged. Its DSL is covered by the compiled hello-world/usestate-counter/layout-basics snippets on the same page; its header by SingleFileGuideHeaderTests.", - // The two directives that let the same file run under plain `dotnet` instead of - // `winapp run`. A fragment by design: the point is the delta, not a second copy - // of the app. + // The one directive that switches the same file to an unpackaged launch. A + // fragment by design: the point is the delta, not a second copy of the app. [""" #:property WindowsPackageType=None - #:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier) """] = - "Two-line delta for running the same file under plain dotnet; placement is guarded by SingleFileGuideHeaderTests.", + "One-line delta that runs the same file unpackaged; placement is guarded by SingleFileGuideHeaderTests.", }, ["source-mapping"] = new(StringComparer.Ordinal) diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs index 7f4e66305..df6afd9ed 100644 --- a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -32,7 +32,7 @@ namespace Microsoft.UI.Reactor.Cli.Docs.Tests; /// net10.0-windows10.0.19041.0 produces against this package. /// /// -/// The Microsoft.Windows.SDK.BuildTools.WinApp pin in the packaged example is deliberately +/// The Microsoft.Windows.SDK.BuildTools.WinApp pin in the header is deliberately /// not version-guarded. WinAppSDKReferenceGuardTests does sweep #:package /// headers, but only for Microsoft.WindowsAppSDK…, and this repo has no central pin for /// the build-tools package to check a doc against — it is a consumer-side tool rather than a @@ -77,48 +77,75 @@ public void DocumentedTargetFramework_MatchesTheFrameworkProject() } /// - /// The primary example carries the full header; the second block is the dotnet run - /// delta and must carry exactly the two directives that delta exists to add. + /// The primary example carries the full header; the second block is the unpackaged delta and + /// must be exactly the one directive that delta exists to add. /// /// - /// The section's whole shape is "here is the file, and here are the two lines you add to run - /// it without winapp". If either directive migrated into the primary block, that block - /// would stop matching the blog and the published quick-start, and the delta would no longer - /// be a delta — while a whole-file Contains check stayed green through the move. So - /// placement is asserted, not just presence. + /// + /// The header runs packaged under both runners — Microsoft.Windows.SDK.BuildTools.WinApp + /// makes dotnet run package the app the way winapp run does — and + /// WindowsPackageType=None is the switch both obey to run it unpackaged instead. For a + /// Reactor-only app it is also what lets that plain exe start: the app is framework-dependent, + /// because the package flows Microsoft.WindowsAppSDK.Runtime (guarded by + /// WinAppSDKReferenceGuardTests.Reactor_library_flows_WinUI_and_Runtime_not_the_metapackage), + /// and the directive turns on the Windows App SDK bootstrapper. No architecture is needed: only + /// a self-contained app needs one, and the section offers that as an opt-in rather than a step. + /// Equality rather than Contains, so a RuntimeIdentifier line creeping back into + /// the delta fails here. + /// + /// + /// Placement matters as much as presence. Both runners obey the directive — measured: each + /// launched the app with no package identity — so in the primary block it would make every + /// launch unpackaged and silently cost the reader the identity that block exists to give them. + /// /// [Fact] - public void DotnetDeltaDirectives_SitInTheSecondBlockOnly() + public void DotnetDeltaDirective_SitsInTheSecondBlockOnly() { var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); var blocks = HeaderBlocks(guide); - // Two header blocks: the complete file, then the dotnet-run delta. If the section is + // Two header blocks: the complete file, then the unpackaged delta. If the section is // restructured into a different number, this needs rereading rather than silently // passing over whichever blocks happen to remain. Assert.Equal(2, blocks.Count); - foreach (var directive in new[] - { - "#:property WindowsPackageType=None", - "#:property RuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)", - }) - { - Assert.True( - blocks[1].Contains(directive, StringComparison.Ordinal), - $"The second #: block is the 'run it with plain dotnet' delta and must declare " - + $"'{directive}'. Without RuntimeIdentifier the build stops with " - + "\"WindowsAppSDKSelfContained requires a supported Windows architecture\"; " - + "without WindowsPackageType=None the app builds clean and then dies at startup " - + "with REGDB_E_CLASSNOTREG."); - - Assert.False( - blocks[0].Contains(directive, StringComparison.Ordinal), - $"The first #: block is the complete single-file app as `winapp run` needs it, " - + $"and must NOT declare '{directive}'. winapp supplies the architecture itself, " - + "and that four-directive header is what the published blog and quick-start " - + "show — adding a line here silently forks them."); - } + const string directive = "#:property WindowsPackageType=None"; + Assert.Equal(directive, blocks[1]); + + Assert.False( + blocks[0].Contains(directive, StringComparison.Ordinal), + $"The first #: block is the complete single-file app, packaged by default, and must NOT " + + $"declare '{directive}': dotnet run and winapp run both obey it and launch the app " + + "unpackaged, so the reader would lose the package identity that block is there to give them."); + } + + /// + /// The primary header is exactly the five directives the page documents. + /// + /// + /// Compared as an order-insensitive set of directive names: the values are pinned by the other + /// tests, and this one is about which directives the reader has to write at all. It is what + /// catches an architecture or packaging directive being added to the file everyone copies — + /// every such directive is either unnecessary or, like WindowsPackageType, changes how + /// the app runs. + /// + [Fact] + public void PrimaryHeader_DeclaresExactlyTheDocumentedDirectives() + { + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + Assert.Equal(2, blocks.Count); + + var names = blocks[0].Split('\n') + .Select(d => Regex.Match(d, @"^#:(?:package\s+(?[^@\s]+)|property\s+(?[^=\s]+))").Groups["name"].Value) + .Order(StringComparer.Ordinal) + .ToArray(); + + Assert.Equal( + new[] { "Microsoft.UI.Reactor", "Microsoft.Windows.SDK.BuildTools.WinApp", "OutputType", "TargetFramework", "UseWinUI" }, + names); } /// @@ -129,6 +156,10 @@ public void DotnetDeltaDirectives_SitInTheSecondBlockOnly() /// them leaves the whole suite green while a reader's run fails. Each row of the table is a /// claim, and each claim is checked. /// + /// Microsoft.Windows.SDK.BuildTools.WinApp — without it plain dotnet run + /// cannot package the app, and it dies at startup with REGDB_E_CLASSNOTREG. Measured: + /// the same header with WindowsPackageType unset, or even set to MSIX, crashes + /// that way under plain dotnet run. /// OutputType=WinExe — does not fail the build at all; it links the app for the /// console subsystem instead. Measured on this tree: the PE subsystem byte reads 3 (console) /// without it and 2 (Windows GUI) with it, so a console window sits behind the UI. @@ -139,6 +170,7 @@ public void DotnetDeltaDirectives_SitInTheSecondBlockOnly() /// /// [Theory] + [InlineData("#:package Microsoft.Windows.SDK.BuildTools.WinApp@")] [InlineData("#:property OutputType=WinExe")] [InlineData("#:property TargetFramework=")] [InlineData("#:property UseWinUI=true")] @@ -155,22 +187,6 @@ public void PrimaryHeader_DeclaresTheLoadBearingDirectives(string directive) + "compilation, so nothing else would catch its removal."); } - /// - /// The section must still name the package that turns dotnet run into a packaged - /// launch. - /// - /// - /// It is prose rather than a third header block, so nothing else would notice it going - /// missing. Presence only — see the class remarks for why the version is not pinned. - /// - [Fact] - public void Section_NamesTheBuildToolsPackageForPackagedDotnetRun() - { - var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); - - Assert.Contains("Microsoft.Windows.SDK.BuildTools.WinApp", guide, StringComparison.Ordinal); - } - /// /// The contiguous runs of #: directives in the guide, in document order. /// diff --git a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs index 2dddf07df..e5ac3968b 100644 --- a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs +++ b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs @@ -14,8 +14,9 @@ namespace Microsoft.UI.Reactor.Tests; /// Guards the Windows App SDK dependency policy (see Directory.Build.targets). /// Repo projects must not reference the full Microsoft.WindowsAppSDK /// metapackage directly: the correct package is injected centrally — -/// Microsoft.WindowsAppSDK.WinUI for framework-dependent libraries, WinUI + -/// Microsoft.WindowsAppSDK.Runtime for framework-dependent apps, and the full +/// Microsoft.WindowsAppSDK.WinUI + Microsoft.WindowsAppSDK.Runtime for +/// framework-dependent projects (libraries included, so Runtime flows to consumer +/// apps), and the full /// metapackage only for self-contained / MSIX projects. A stray direct reference /// bypasses that rule and re-drags the unused AI / ML / Widgets / DWrite slices back /// into the dependency graph. @@ -86,21 +87,31 @@ public void No_repo_project_references_the_WindowsAppSDK_metapackage_directly() offenders.Count == 0, "These projects reference the Microsoft.WindowsAppSDK metapackage directly. " + "Remove the reference — Directory.Build.targets injects " - + "Microsoft.WindowsAppSDK.WinUI (+ .Runtime for apps) or the metapackage " + + "Microsoft.WindowsAppSDK.WinUI + .Runtime or the metapackage " + "(self-contained / MSIX) centrally. Offenders:\n " + string.Join("\n ", offenders.OrderBy(x => x, StringComparer.Ordinal))); } /// /// Outcome-level guard: the shipped Microsoft.UI.Reactor library must - /// resolve the lean WinUI sub-package and must NOT flow the full metapackage to - /// consumers. Reads Reactor's restore graph (present because this test project - /// references Reactor), so it catches a central-rule regression wherever it is - /// introduced — including in Directory.Build.*, which the source scan above - /// does not cover. + /// resolve the lean WinUI sub-package plus Microsoft.WindowsAppSDK.Runtime, + /// and must NOT flow the full metapackage to consumers. Reads Reactor's restore + /// graph (present because this test project references Reactor), so it catches a + /// central-rule regression wherever it is introduced — including in + /// Directory.Build.*, which the source scan above does not cover. /// + /// + /// Runtime is what keeps a consumer app framework-dependent: it sets + /// MicrosoftWindowsAppSDKPackageDir, and an app whose graph lacks it is + /// silently built self-contained by the SDK's Base.targets. That is what + /// happened to every app referencing only Reactor between #822 and this guard, + /// surfacing as "WindowsAppSDKSelfContained requires a supported Windows + /// architecture". Being in Reactor's own graph is not enough, so the edge is also + /// checked for flowing: a PrivateAssets="all" reference would pass the graph + /// check while never reaching the nuspec. + /// [Fact] - public void Reactor_library_resolves_WinUI_subpackage_not_the_metapackage() + public void Reactor_library_flows_WinUI_and_Runtime_not_the_metapackage() { var root = RepoRootFinder.FindRepoRoot(); Assert.NotNull(root); @@ -116,7 +127,24 @@ public void Reactor_library_resolves_WinUI_subpackage_not_the_metapackage() .ToList(); Assert.Contains(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK.WinUI/", StringComparison.Ordinal)); + Assert.Contains(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK.Runtime/", StringComparison.Ordinal)); Assert.DoesNotContain(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK/", StringComparison.Ordinal)); + + foreach (var framework in doc.RootElement.GetProperty("project").GetProperty("frameworks").EnumerateObject()) + { + Assert.True( + framework.Value.GetProperty("dependencies").TryGetProperty("Microsoft.WindowsAppSDK.Runtime", out var runtime), + $"Microsoft.WindowsAppSDK.Runtime is not a direct dependency of Reactor for {framework.Name}, so " + + "nothing guarantees it reaches the nuspec. The Windows App SDK injection in " + + "Directory.Build.targets must give framework-dependent libraries Runtime as well as WinUI."); + + var suppressParent = runtime.TryGetProperty("suppressParent", out var value) ? value.GetString() : null; + Assert.False( + string.Equals(suppressParent, "All", StringComparison.OrdinalIgnoreCase), + $"Microsoft.WindowsAppSDK.Runtime is a private asset of Reactor for {framework.Name} " + + "(suppressParent=All), so it never reaches consumer apps and they fall back to " + + "self-contained. Drop PrivateAssets from that reference."); + } } /// From c0fb1d4068712ed4c96923f83b7175ffbcfd7249 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Tue, 29 Sep 2026 11:32:00 -0700 Subject: [PATCH 7/7] Document bumping WinUI through the full Windows App SDK package Now that Microsoft.UI.Reactor depends on Microsoft.WindowsAppSDK.Runtime, a consumer that references a newer Microsoft.WindowsAppSDK.WinUI on its own gets a build error. NuGet raises WinUI (and the Foundation and InteractiveExperiences packages it brings in) but leaves Runtime at the version Reactor asks for, because neither package depends on the other. Runtime's component-version check then fails the build with "One or more referenced Windows App SDK components are newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime package". This is the Windows App SDK's intended behaviour: Runtime decides the minimum Windows App Runtime the app asks for, so it must not lag the components the app compiles against. The error's own advice points the wrong way for someone upgrading on purpose: it says to change the component references to match Runtime, which undoes the upgrade, and it never mentions the full Microsoft.WindowsAppSDK package, which moves Runtime and the components together. So the fix is now documented where a consumer will look: the packaging guide quotes the error text so it can be searched, the landing page and spec 022 say to bump the full package (spec 022 previously said NuGet would unify a consumer's own WinUI reference), and the CHANGELOG entry and the reactor-build-and-check error table cover it. Reproduced against a locally packed Reactor from this branch: adding Microsoft.WindowsAppSDK.WinUI 2.3.9 fails with the error above; adding Microsoft.WindowsAppSDK 2.5.1 instead builds, with Runtime 2.5.1 and WinUI 2.3.9. DocPipeline.Tests 479 passed / 0 failed. Agent-kit, skill-prose, search-index and WinAppSDKReferenceGuard gates 293 passed / 0 failed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 393937bf-e01f-4a84-a862-ca06ed302d2f --- CHANGELOG.md | 3 +++ docs/_pipeline/templates/index.md.dt | 4 +++- docs/_pipeline/templates/packaging.md.dt | 7 ++++++- docs/guide/README.md | 4 +++- docs/guide/index.md | 4 +++- docs/guide/packaging.md | 7 ++++++- docs/specs/022-packaging-and-distribution.md | 2 +- plugins/reactor/skills/reactor-build-and-check/SKILL.md | 1 + 8 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d05bff9f0..dc92ff7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,9 @@ Conventions for contributors: built such apps self-contained, so a plain `dotnet run` failed with *"WindowsAppSDKSelfContained requires a supported Windows architecture"*. To keep bundling the Windows App Runtime, set `WindowsAppSDKSelfContained=true`. + To move to a newer WinUI, bump the full `Microsoft.WindowsAppSDK` package; + bumping `Microsoft.WindowsAppSDK.WinUI` on its own now fails the Windows App + SDK's version check. - **The Forms guide's "Validation Context" example now works as written** (issue #1262). Clicking **Register** on an empty form submitted successfully diff --git a/docs/_pipeline/templates/index.md.dt b/docs/_pipeline/templates/index.md.dt index 771d6a682..2a13b7538 100644 --- a/docs/_pipeline/templates/index.md.dt +++ b/docs/_pipeline/templates/index.md.dt @@ -214,7 +214,9 @@ project and edit the `.csproj`: One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you want to pin a specific WinUI patch or bundle the runtime with -`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the +`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not +`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's +version check. See [Packaging](packaging.md) for the ``, ARM64, and self-contained shapes the project template uses. Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with diff --git a/docs/_pipeline/templates/packaging.md.dt b/docs/_pipeline/templates/packaging.md.dt index 1c1ec0f8b..ff40bd60b 100644 --- a/docs/_pipeline/templates/packaging.md.dt +++ b/docs/_pipeline/templates/packaging.md.dt @@ -299,7 +299,12 @@ template and reference `Microsoft.UI.Reactor` by hand, the SDK still arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus `Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full metapackage yourself when you want `WindowsAppSDKSelfContained=true` or -an MSIX, both of which need the Runtime redist. The repo-internal +an MSIX, both of which need the Runtime redist. The metapackage is also +how you move to a newer WinUI, because it moves Runtime with it: a newer +`Microsoft.WindowsAppSDK.WinUI` on its own leaves Runtime behind, and the +build fails with *"One or more referenced Windows App SDK components are +newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime +package"*. The repo-internal `WindowsAppSDKVersion` MSBuild property only governs projects under this clone (`Directory.Build.props`); consumer projects pick their version directly. diff --git a/docs/guide/README.md b/docs/guide/README.md index e0bd7f939..17769014d 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -240,7 +240,9 @@ project and edit the `.csproj`: One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you want to pin a specific WinUI patch or bundle the runtime with -`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the +`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not +`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's +version check. See [Packaging](packaging.md) for the ``, ARM64, and self-contained shapes the project template uses. Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with diff --git a/docs/guide/index.md b/docs/guide/index.md index e0bd7f939..17769014d 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -240,7 +240,9 @@ project and edit the `.csproj`: One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you want to pin a specific WinUI patch or bundle the runtime with -`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the +`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not +`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's +version check. See [Packaging](packaging.md) for the ``, ARM64, and self-contained shapes the project template uses. Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with diff --git a/docs/guide/packaging.md b/docs/guide/packaging.md index 45eb54613..8543af81d 100644 --- a/docs/guide/packaging.md +++ b/docs/guide/packaging.md @@ -317,7 +317,12 @@ template and reference `Microsoft.UI.Reactor` by hand, the SDK still arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus `Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full metapackage yourself when you want `WindowsAppSDKSelfContained=true` or -an MSIX, both of which need the Runtime redist. The repo-internal +an MSIX, both of which need the Runtime redist. The metapackage is also +how you move to a newer WinUI, because it moves Runtime with it: a newer +`Microsoft.WindowsAppSDK.WinUI` on its own leaves Runtime behind, and the +build fails with *"One or more referenced Windows App SDK components are +newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime +package"*. The repo-internal `WindowsAppSDKVersion` MSBuild property only governs projects under this clone (`Directory.Build.props`); consumer projects pick their version directly. diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 5febb91b5..625385f9a 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -430,7 +430,7 @@ Consumer gets: framework, analyzers, source generator, and WinUI SDK (transitive - **Internal feed ownership.** Which Azure Artifacts organization hosts the P1 feed? Creating one takes ~a day plus approvals. - **Signing prerequisite for P1.** Does the chosen internal feed enforce signed packages? If yes, P1 needs ESRP too, not just P2. - **Package ID.** `Microsoft.UI.Reactor` assumes we stay in the `Microsoft.UI.*` namespace (see spec 018 for the namespace rename). If that namespace decision changes, the package ID follows. -- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent projects — libraries and apps alike, including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`) plus `Microsoft.WindowsAppSDK.Runtime` rather than the full metapackage (AI/ML/Widgets/DWrite), so the published package flows WinUI + Runtime to consumers — without Runtime the SDK silently builds a consumer app self-contained; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a different WinUI version can reference their own `Microsoft.WindowsAppSDK*` version — NuGet unifies it transitively with the library's. +- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent projects — libraries and apps alike, including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`) plus `Microsoft.WindowsAppSDK.Runtime` rather than the full metapackage (AI/ML/Widgets/DWrite), so the published package flows WinUI + Runtime to consumers — without Runtime the SDK silently builds a consumer app self-contained; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a newer WinUI reference the `Microsoft.WindowsAppSDK` metapackage at that version, which moves Runtime and the components together. Referencing `Microsoft.WindowsAppSDK.WinUI` alone leaves Runtime at the library's version, and Runtime's component-version check fails the build. - **`mur` install-script trust boundary.** `iwr | iex` from GitHub Releases works for P1 but will concern P3 users. Document the signed-binary fallback (direct download + verify signature) before public launch. ## 14. Implementation Phases diff --git a/plugins/reactor/skills/reactor-build-and-check/SKILL.md b/plugins/reactor/skills/reactor-build-and-check/SKILL.md index 2e15d8194..17c05b52c 100644 --- a/plugins/reactor/skills/reactor-build-and-check/SKILL.md +++ b/plugins/reactor/skills/reactor-build-and-check/SKILL.md @@ -147,6 +147,7 @@ Additional flags: | `CS1955` | error | "Non-invocable member 'Element.Margin' cannot be used like a method" | Missing `using Microsoft.UI.Reactor;`. The element records expose same-named *properties* (`Element.Margin`, `Element.Padding`, plus `CornerRadius`/`BorderThickness` on derived records), and C# only falls back to extension methods when none is in scope — so an absent import reports the property as non-invocable rather than saying the fluent modifier isn't imported. It reads like the API doesn't exist; it does. Add the `using`. (Distinct from `REACTOR_DYM_001` above, which is a genuine property invoked with parens, e.g. `GridSize.Auto()`.) | | `MSB4025` | error | "The project file could not be loaded" | Self-contained (`WindowsAppSDKSelfContained=true`) single-file `.cs` build attempted without an architecture. Add `-p:Platform=ARM64` (or x64), or `-r win-arm64` / `-r win-x64`. | | `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`. | +| — | error | "One or more referenced Windows App SDK components are newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime package" | A newer `Microsoft.WindowsAppSDK.WinUI` (or another Windows App SDK component) was referenced on its own, leaving `Microsoft.WindowsAppSDK.Runtime` at Reactor's version. Reference the full `Microsoft.WindowsAppSDK` package at the version you want instead; it moves Runtime with the components. Don't switch to `WindowsAppSDKSelfContained=true` just to silence it. | If a `REACTOR_*` ID isn't in this table, the bundled analyzer DLL has more docs. The descriptions ship in the warnings themselves.