--- name: winapp-maui description: Package and sign .NET MAUI Windows apps with winapp, resolving the resizetizer manifest dependency. Use when packaging or signing a .NET MAUI Windows app, building a MAUI MSIX or signed unpackaged build in CI, or fixing 'manifest contains unresolved placeholders ($placeholder$)' errors from winapp package. --- ## When to use Use this skill when: - **Packaging or signing a .NET MAUI Windows app** with winapp (`winapp package` / `winapp sign`) - **`winapp package` fails** with an error like *"manifest contains unresolved placeholders: `$placeholder$`"* - **Deciding which manifest to hand to winapp** for a MAUI Windows head project - **Setting up CI/CD** (GitHub Actions) that builds a MAUI app and produces a signed MSIX and/or signed unpackaged build MAUI is **not** a "run `winapp init`" framework — the Windows head already has a manifest and a build system that generates the real one for you. The only trick is pointing winapp at the **generated** manifest, never the source one. ## The resizetizer dependency (root cause) A .NET MAUI project has a **source** manifest at `Platforms/Windows/Package.appxmanifest` with placeholder tokens that the MAUI build pipeline resolves: ```xml $placeholder$ User Name $placeholder$.png ... ``` These are resolved at **build/publish time** by **`Microsoft.Maui.Resizetizer`** (bundled with the MAUI workload), which reads MSBuild properties (`ApplicationTitle`, `ApplicationId`, `ApplicationDisplayVersion`, the `MauiIcon`/`MauiSplashScreen` items, etc.), generates the app icon/tile/splash assets, and writes a **resolved** manifest into the intermediate output. **Why winapp trips on this:** `winapp package` only auto-resolves its own entry-point tokens — `$targetnametoken$` and `$targetentrypoint$` (via `--executable`). It does **not** understand MAUI's `$placeholder$` tokens. If you point winapp at the raw `Platforms/Windows/Package.appxmanifest`, packaging fails because those placeholders are still literal `$placeholder$` strings. > **Do not replace MAUI's placeholders in `Platforms/Windows/Package.appxmanifest` just to satisfy winapp.** Keep framework-managed tokens in the source manifest and point winapp at the generated manifest. Files under `obj`/`bin` are regenerated on every build, so never edit those generated copies. ## Where the resolved manifest lives After a **Windows-targeted build or publish**, MAUI produces a fully-usable resolved manifest: | Manifest | Path (relative to project) | State | |----------|----------------------------|-------| | **Resizetizer manifest** | `obj\\\\resizetizer\m\Package.appxmanifest` | MAUI `$placeholder$` tokens resolved; `$targetnametoken$`/`$targetentrypoint$` remain (winapp resolves these via `--executable`) | > **Note:** When building with `WindowsPackageType=MSIX` (the default), MAUI also produces `bin\\\\AppxManifest.xml` — a fully resolved manifest. This file is **not produced** in `WindowsPackageType=None` workflows. The resizetizer manifest above works in both cases. Where: - `` = `Debug` or `Release` - `` = the Windows target framework, e.g. `net10.0-windows10.0.19041.0` - `` = `win-x64` or `win-arm64` Both paths are **per-RID** — you must publish each architecture first, then pack that architecture's manifest. ## Usage ### 1. Publish the Windows head first The resolved manifest only exists **after** a Windows publish, so always publish before packing: ```powershell # Self-contained unpackaged publish (no MSIX container) — regenerates the resolved manifest dotnet publish .\MyApp\MyApp.csproj ` -c Release ` -f net10.0-windows10.0.19041.0 ` -r win-x64 ` -p:WindowsPackageType=None ` -p:SelfContained=true ` -p:WindowsAppSDKSelfContained=true ` --output .\publish\win-x64 ``` > Multi-targeted MAUI projects (`net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0`) build the Windows head only when you pass the Windows `-f`/`-r`. The winapp MSBuild targets are inert for non-Windows TFMs. ### 2. Publisher must match the certificate The resolved manifest preserves `Identity.Publisher` from `Platforms\Windows\Package.appxmanifest`. Your signing certificate subject **must equal** that value exactly, or signing fails with a publisher mismatch. To use a different publisher, edit the source manifest's `Identity Publisher="CN=..."` value and publish again before generating the certificate. ```powershell $manifest = ".\MyApp\obj\Release\net10.0-windows10.0.19041.0\win-x64\resizetizer\m\Package.appxmanifest" # Fail fast if the build didn't produce it (usually means you skipped the Windows publish) if (-not (Test-Path $manifest)) { throw "Resolved manifest not found — publish the Windows head first." } # Generate or replace a matching dev cert from the resolved manifest # The default password is 'password' — use the same for --cert-password below winapp cert generate --manifest $manifest --if-exists overwrite ``` ### 3. Package a signed MSIX — point `--manifest` at the resolved manifest ```powershell winapp package .\publish\win-x64 ` --manifest $manifest ` --executable MyApp.exe ` --cert .\devcert.pfx ` --cert-password password ` --output .\artifacts\MyApp-win-x64.msix ``` `--executable MyApp.exe` resolves the remaining `$targetnametoken$`/`$targetentrypoint$` in the resizetizer manifest. > **Always use the explicit `--manifest` path** for `WindowsPackageType=None` workflows — manifest auto-detection from the publish folder does not apply because no `AppxManifest.xml` is generated in that output. ### 4. Sign the unpackaged build For the loose/unpackaged (`WindowsPackageType=None`) build, sign the executables in place: ```powershell winapp sign .\publish\win-x64\MyApp.exe .\devcert.pfx --password password ``` > `winapp sign` uses a **positional** certificate path + `--password`. `winapp package` uses `--cert` / `--cert-password`. Mixing them is a common mistake. ## CI/CD (GitHub Actions) Example for x64 — pack the resolved manifest and sign. Store a self-signed (or CA-issued) PFX as a base64 secret. For arm64, add a second set of publish/sign/pack steps with `-r win-arm64` and the corresponding manifest path. ```yaml - uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Install MAUI Windows workload run: dotnet workload install maui-windows - uses: microsoft/setup-winapp@v1 - name: Restore signing cert shell: pwsh run: | [IO.File]::WriteAllBytes("$env:RUNNER_TEMP\sign.pfx", [Convert]::FromBase64String("${{ secrets.SIGN_PFX_BASE64 }}")) "SIGN_PFX_PATH=$env:RUNNER_TEMP\sign.pfx" | Out-File $env:GITHUB_ENV -Append - name: Publish Windows head (x64, self-contained) run: > dotnet publish .\MyApp\MyApp.csproj -c Release -f net10.0-windows10.0.19041.0 -r win-x64 -p:WindowsPackageType=None -p:SelfContained=true -p:WindowsAppSDKSelfContained=true --output .\publish\win-x64 - name: Sign unpackaged binaries (x64) shell: pwsh env: SIGN_PFX_PASSWORD: ${{ secrets.SIGN_PFX_PASSWORD }} run: | Get-ChildItem .\publish\win-x64 -Filter *.exe | ForEach-Object { winapp sign $_.FullName $env:SIGN_PFX_PATH --password $env:SIGN_PFX_PASSWORD --quiet } - name: Pack signed MSIX (x64) shell: pwsh env: SIGN_PFX_PASSWORD: ${{ secrets.SIGN_PFX_PASSWORD }} run: | $manifest = ".\MyApp\obj\Release\net10.0-windows10.0.19041.0\win-x64\resizetizer\m\Package.appxmanifest" if (-not (Test-Path $manifest)) { throw "Resolved manifest not found: $manifest" } winapp package .\publish\win-x64 --manifest $manifest --executable MyApp.exe ` --cert $env:SIGN_PFX_PATH --cert-password $env:SIGN_PFX_PASSWORD ` --output .\artifacts\MyApp-win-x64.msix --quiet - name: Cleanup signing cert if: always() shell: pwsh run: | if ($env:SIGN_PFX_PATH -and (Test-Path $env:SIGN_PFX_PATH)) { Remove-Item -Path $env:SIGN_PFX_PATH -Force } ``` **Tips:** - Use `-q`/`--quiet` to reduce log noise. - A **self-signed** cert produces a valid signature but does **not** clear SmartScreen reputation for other users — only an OV/EV cert from a trusted CA builds reputation. See `winapp-signing`. - Add `devcert.pfx` and decoded PFX paths to `.gitignore`; never commit certificates. ### End-to-end validation script For a practical repo-level check of the MAUI workflow, run: ```powershell .\scripts\test-samples.ps1 -Samples maui-app ``` This executes `samples\maui-app\test.Tests.ps1`, which creates a MAUI app from scratch, publishes the Windows head, packages with the generated resizetizer manifest, and signs the unpackaged executable. The repository also includes a concrete MAUI sample project under `samples\maui-app\`. ## Tips - The resolved manifest is **regenerated on every Windows build/publish** — treat `obj\...\resizetizer\m\` and `bin\...\\AppxManifest.xml` as build outputs, not something to check in. - If the manifest path doesn't exist, you almost always **forgot to publish the Windows head for that RID** (or targeted a non-Windows TFM). Publish first. - Package **each architecture separately** from its own per-RID publish folder and manifest, or pass both folders to `winapp package` to build an `.msixbundle` (see `winapp-package`). - For MSIX that shouldn't require the user to install the Windows App SDK runtime, add `--self-contained` to `winapp package` (or publish with `-p:WindowsAppSDKSelfContained=true` for unpackaged). - To launch the unpackaged app locally, run the published executable directly (for example, `.\publish\win-x64\MyApp.exe`). `winapp run` requires a manifest in the input directory; since the `WindowsPackageType=None` publish folder does **not** contain one, pass `--manifest --executable ` explicitly if you use `winapp run`. ## Related skills - **Packaging**: `winapp-package` — full `winapp package` reference, bundles, self-contained - **Signing**: `winapp-signing` — certificate generation, trust, timestamping, CA vs self-signed - **Manifest**: `winapp-manifest` — manifest structure and the `$targetnametoken$` placeholder - **Frameworks**: `winapp-frameworks` — other frameworks (Electron, WPF/WinForms, C++, Rust, Flutter, Tauri) - Hitting an error? See `winapp-troubleshoot` for the error → solution table ## Troubleshooting | Error | Cause | Solution | |-------|-------|----------| | "manifest contains unresolved placeholders: `$placeholder$`" | Pointed winapp at the **source** `Platforms/Windows/Package.appxmanifest` | Point `--manifest` at the resolved manifest (`obj\...\resizetizer\m\Package.appxmanifest` or `bin\...\\AppxManifest.xml`) | | "manifest not found" at the resizetizer path | Windows head not published for that RID | Run `dotnet publish -f -r ` **before** packing | | "unresolved `$targetnametoken$` / `$targetentrypoint$`" | Packed the resizetizer manifest without an entry point | Add `--executable MyApp.exe`, or pack the fully-resolved `bin\...\AppxManifest.xml` instead | | "Publisher mismatch" during signing | Cert subject ≠ resolved manifest `Identity.Publisher` | Set `Identity Publisher="CN=..."` in `Platforms\Windows\Package.appxmanifest`, publish again, then run `winapp cert generate --manifest --if-exists overwrite` | | Placeholders reappear after editing the source manifest | Resizetizer overwrites its generated copy each build | Don't hand-edit the source manifest — change the MSBuild properties / `MauiIcon` instead |