winapp-maui
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 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 |
microsoft/winappcli · MIT · Revision a1325be26b8d
Be the first to comment
Share what worked or leave a question for the creator.