Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/skills/winapp-frameworks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Each framework has a detailed guide — refer to the links below rather than try
|-----------|---------------|-------|
| **Electron** | `npm install --save-dev @microsoft/winappcli` | [Electron setup guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/electron/setup.md) |
| **.NET** (WPF, WinForms, Console) | `winget install Microsoft.winappcli` | [.NET guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/dotnet.md) |
| **.NET MAUI** | `winget install Microsoft.winappcli` | [MAUI guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/maui.md) + `winapp-maui` skill |
| **C++** (CMake, MSBuild) | `winget install Microsoft.winappcli` | [C++ guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/cpp.md) |
| **Rust** | `winget install Microsoft.winappcli` | [Rust guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/rust.md) |
| **Flutter** | `winget install Microsoft.winappcli` | [Flutter guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/flutter.md) |
Expand Down Expand Up @@ -77,6 +78,13 @@ winapp run bin\x64\Debug\<tfm>\win-x64\

Replace `<tfm>` with your target framework (e.g., `net10.0-windows10.0.26100.0`), and adjust `x64` to match your target architecture.

### .NET MAUI
MAUI has one important quirk: its checked-in `Platforms/Windows/Package.appxmanifest` is full of `$placeholder$` tokens that **`winapp package` does not resolve**. MAUI's **resizetizer** fills them at build/publish time into a generated manifest:
- Resizetizer manifest: `obj\<Config>\<TFM>\<RID>\resizetizer\m\Package.appxmanifest`
- Fully-resolved output manifest: `bin\<Config>\<TFM>\<RID>\AppxManifest.xml`

Publish the Windows head first, then point `winapp package --manifest` at the **generated** manifest — never the source one. See the dedicated **`winapp-maui`** skill for the full workflow, CI examples, and troubleshooting.

### C++ (CMake, MSBuild)
C++ projects use winapp primarily for SDK projections (CppWinRT headers) and packaging:
- `winapp init --setup-sdks stable` downloads Windows SDK + App SDK and generates CppWinRT headers
Expand Down Expand Up @@ -124,4 +132,5 @@ For full debugging scenarios and IDE setup, see the [Debugging Guide](https://gi
- **Signing**: `winapp-signing` — certificate generation and management
- **Packaging**: `winapp-package` — creating MSIX installers from build output
- **Identity**: `winapp-identity` — enabling package identity for Windows APIs during development
- **MAUI**: `winapp-maui` — packaging/signing .NET MAUI Windows apps and resolving the resizetizer manifest
- Not sure which command to use? See `winapp-troubleshoot` for a command selection flowchart
208 changes: 208 additions & 0 deletions .claude/skills/winapp-maui/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
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.
version: 0.4.1
---
## 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` that is full of MAUI-specific `$placeholder$` tokens:

```xml
<Identity Name="$placeholder$" Publisher="$placeholder$" Version="$placeholder$" />
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>$placeholder$</PublisherDisplayName>
<Logo>$placeholder$</Logo>
</Properties>
...
<uap:VisualElements DisplayName="$placeholder$" ... Square150x150Logo="$placeholder$" Square44x44Logo="$placeholder$">
```

These are resolved at **build/publish time** by **`Microsoft.Maui.Resizetizer`** (bundled with the MAUI workload), which reads MSBuild properties (`ApplicationTitle`, `ApplicationId`, `ApplicationDisplayVersion`, `ApplicationPublisher`, 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.

> **Never edit `Platforms/Windows/Package.appxmanifest` to hard-code values.** The resizetizer overwrites the generated copy on every build, and hand-editing the source breaks the MAUI tooling contract. The fix is to point winapp at the generated manifest instead.

## Where the resolved manifest lives

After a **Windows-targeted build or publish**, MAUI produces two fully-usable resolved manifests:

| Manifest | Path (relative to project) | State |
|----------|----------------------------|-------|
| **Resizetizer manifest (reliable for `WindowsPackageType=None`)** | `obj\<Config>\<TFM>\<RID>\resizetizer\m\Package.appxmanifest` | MAUI `$placeholder$` tokens resolved; `$targetnametoken$`/`$targetentrypoint$` remain (winapp resolves these via `--executable`) |
| **Publish-output manifest (MSIX-oriented builds)** | `bin\<Config>\<TFM>\<RID>\AppxManifest.xml` | Fully resolved when produced by your build; may be absent in `WindowsPackageType=None` workflows |

Where:
- `<Config>` = `Debug` or `Release`
- `<TFM>` = the Windows target framework, e.g. `net10.0-windows10.0.19041.0`
- `<RID>` = `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. Package a signed MSIX — point `--manifest` at the resolved manifest

```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."
}

winapp package .\publish\win-x64 `
--manifest $manifest `
--executable MyApp.exe `
--cert .\devcert.pfx `
--cert-password $env:SIGN_PFX_PASSWORD `
--output .\artifacts\MyApp-win-x64.msix
```

`--executable MyApp.exe` resolves the remaining `$targetnametoken$`/`$targetentrypoint$` in the resizetizer manifest.

> For MAUI `WindowsPackageType=None` pipelines, do **not** rely on manifest auto-detection from the publish folder. The explicit `obj\...\resizetizer\m\Package.appxmanifest` + `--executable` path above is the reliable flow.

### 3. 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 $env:SIGN_PFX_PASSWORD
```

> `winapp sign` uses a **positional** certificate path + `--password`. `winapp package` uses `--cert` / `--cert-password`. Mixing them is a common mistake.

### 4. Publisher must match the certificate

The resolved manifest's `Identity.Publisher` comes from MSBuild (`$(ApplicationPublisher)`, defaulting to something like `CN=User Name`). Your signing certificate subject **must equal** that value exactly, or signing fails with a publisher mismatch.

```powershell
# Read the publisher the resizetizer actually wrote, then generate a matching dev cert
winapp cert generate --manifest $manifest
```

Set `<ApplicationPublisher>CN=Your Company</ApplicationPublisher>` (or the `ApplicationPublisher` MSBuild property) in the `.csproj` to control it, then regenerate the cert to match.

## CI/CD (GitHub Actions)

Build each architecture, then pack its resolved manifest. Store a self-signed (or CA-issued) PFX as a base64 secret.

```yaml
- 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
}
```

Repeat the publish + sign + pack steps for `win-arm64` (swap `-r win-arm64` and the `win-arm64` manifest path).

**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\...\<RID>\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).
- `winapp run`/`dotnet run` on a MAUI Windows head can auto-detect the output manifest, so debugging usually needs no extra flags — the manual `--manifest` matters mainly for explicit `winapp package` in CI.

## 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\...\<RID>\AppxManifest.xml`) |
| "manifest not found" at the resizetizer path | Windows head not published for that RID | Run `dotnet publish -f <windows-tfm> -r <rid>` **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` | `winapp cert generate --manifest <resolved-manifest>`, or set `$(ApplicationPublisher)` in the `.csproj` and regenerate the cert |
| 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 |
2 changes: 2 additions & 0 deletions .claude/skills/winapp-troubleshoot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Use this skill when:
| "Failed to add package identity" | Stale debug identity or untrusted cert | `Get-AppxPackage *yourapp* \| Remove-AppxPackage` to clean up, then `winapp cert install` and retry |
| "Certificate file already exists" | `devcert.pfx` already present | Use `winapp cert generate --if-exists overwrite` or `--if-exists skip` |
| "Manifest already exists" | `Package.appxmanifest` already present | Use `winapp manifest generate --if-exists overwrite` or edit manifest directly |
| "manifest contains unresolved placeholders: `$placeholder$`" (.NET MAUI) | Passed winapp the source `Platforms/Windows/Package.appxmanifest` | Point `--manifest` at the resizetizer-resolved manifest (`obj\...\resizetizer\m\Package.appxmanifest`) — see `winapp-maui` |
| `run` / `create-debug-identity` registration error `0x800704EC` | Developer Mode is disabled | Enable it in **Settings → Privacy & security → For developers**, or `Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' -Name AllowDevelopmentWithoutDevLicense -Value 1`, then retry |
| `run` / `create-debug-identity` registration error `0x80073CFB` | Package already registered with a conflicting identity | Run `winapp unregister` (or `winapp unregister --force` if the package was registered from a different project tree), then retry |

Expand Down Expand Up @@ -127,6 +128,7 @@ For full details, see the [Debugging Guide](https://github.com/microsoft/WinAppC
- **Packaging**: `winapp-package` — creating MSIX installers
- **Identity**: `winapp-identity` — enabling package identity for Windows APIs
- **Frameworks**: `winapp-frameworks` — framework-specific guidance (Electron, .NET, C++, Rust, Flutter, Tauri)
- **MAUI**: `winapp-maui` — packaging/signing .NET MAUI Windows apps and resolving the resizetizer manifest


## Command Reference
Expand Down
9 changes: 9 additions & 0 deletions .github/plugin/skills/winapp-cli/frameworks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Each framework has a detailed guide — refer to the links below rather than try
|-----------|---------------|-------|
| **Electron** | `npm install --save-dev @microsoft/winappcli` | [Electron setup guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/electron/setup.md) |
| **.NET** (WPF, WinForms, Console) | `winget install Microsoft.winappcli` | [.NET guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/dotnet.md) |
| **.NET MAUI** | `winget install Microsoft.winappcli` | [MAUI guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/maui.md) + `winapp-maui` skill |
| **C++** (CMake, MSBuild) | `winget install Microsoft.winappcli` | [C++ guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/cpp.md) |
| **Rust** | `winget install Microsoft.winappcli` | [Rust guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/rust.md) |
| **Flutter** | `winget install Microsoft.winappcli` | [Flutter guide](https://github.com/microsoft/WinAppCli/blob/main/docs/guides/flutter.md) |
Expand Down Expand Up @@ -77,6 +78,13 @@ winapp run bin\x64\Debug\<tfm>\win-x64\

Replace `<tfm>` with your target framework (e.g., `net10.0-windows10.0.26100.0`), and adjust `x64` to match your target architecture.

### .NET MAUI
MAUI has one important quirk: its checked-in `Platforms/Windows/Package.appxmanifest` is full of `$placeholder$` tokens that **`winapp package` does not resolve**. MAUI's **resizetizer** fills them at build/publish time into a generated manifest:
- Resizetizer manifest: `obj\<Config>\<TFM>\<RID>\resizetizer\m\Package.appxmanifest`
- Fully-resolved output manifest: `bin\<Config>\<TFM>\<RID>\AppxManifest.xml`

Publish the Windows head first, then point `winapp package --manifest` at the **generated** manifest — never the source one. See the dedicated **`winapp-maui`** skill for the full workflow, CI examples, and troubleshooting.

### C++ (CMake, MSBuild)
C++ projects use winapp primarily for SDK projections (CppWinRT headers) and packaging:
- `winapp init --setup-sdks stable` downloads Windows SDK + App SDK and generates CppWinRT headers
Expand Down Expand Up @@ -124,4 +132,5 @@ For full debugging scenarios and IDE setup, see the [Debugging Guide](https://gi
- **Signing**: `winapp-signing` — certificate generation and management
- **Packaging**: `winapp-package` — creating MSIX installers from build output
- **Identity**: `winapp-identity` — enabling package identity for Windows APIs during development
- **MAUI**: `winapp-maui` — packaging/signing .NET MAUI Windows apps and resolving the resizetizer manifest
- Not sure which command to use? See `winapp-troubleshoot` for a command selection flowchart
Loading
Loading