If you can't run winapp yourself (no shell, or the command is denied), give the user the exact winapp command(s) for their project instead of only describing the steps.
When to use
Use this skill when:
- Packaging or signing a .NET MAUI Windows app with winapp (
winapp package/winapp sign) winapp packagefails 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:
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
...
<uap:VisualElements DisplayName="$placeholder$" ... Square150x150Logo="$placeholder$.png" Square44x44Logo="$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.appxmanifestjust to satisfy winapp. Keep framework-managed tokens in the source manifest and point winapp at the generated manifest. Files underobj/binare 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\<Config>\<TFM>\<RID>\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 producesbin\<Config>\<TFM>\<RID>\AppxManifest.xml— a fully resolved manifest. This file is not produced inWindowsPackageType=Noneworkflows. The resizetizer manifest above works in both cases.
Where:
<Config>=DebugorRelease<TFM>= the Windows target framework, e.g.net10.0-windows10.0.19041.0<RID>=win-x64orwin-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:
# 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.
$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
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
--manifestpath forWindowsPackageType=Noneworkflows — manifest auto-detection from the publish folder does not apply because noAppxManifest.xmlis generated in that output.
4. Sign the unpackaged build
For the loose/unpackaged (WindowsPackageType=None) build, sign the executables in place:
winapp sign .\publish\win-x64\MyApp.exe .\devcert.pfx --password password
winapp signuses a positional certificate path +--password.winapp packageuses--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.
- 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/--quietto 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.pfxand decoded PFX paths to.gitignore; never commit certificates.
End-to-end validation script
For a practical repo-level check of the MAUI workflow, run:
.\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\andbin\...\<RID>\AppxManifest.xmlas 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 packageto build an.msixbundle(seewinapp-package). - For MSIX that shouldn't require the user to install the Windows App SDK runtime, add
--self-containedtowinapp package(or publish with-p:WindowsAppSDKSelfContained=truefor unpackaged). - To launch the unpackaged app locally, run the published executable directly (for example,
.\publish\win-x64\MyApp.exe).winapp runrequires a manifest in the input directory; since theWindowsPackageType=Nonepublish folder does not contain one, pass--manifest <resolved-manifest> --executable <exe>explicitly if you usewinapp run.
Related skills
- Packaging:
winapp-package— fullwinapp packagereference, 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-troubleshootfor 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 |
Set Identity Publisher="CN=..." in Platforms\Windows\Package.appxmanifest, publish again, then run winapp cert generate --manifest <resolved-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 |