Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
For a working end-to-end example (WPF app + Inno Setup installer), see the sparse-app sample.
A standard desktop executable — built with dotnet build, MSBuild, CMake, or any other toolchain — has no package identity. Without identity, it cannot use many modern Windows APIs (toast notifications, background tasks, share targets, startup tasks, the app data APIs, and more).
Sparse packaging grants identity to an app without moving its binaries into an MSIX. You ship a tiny identity-only .msix (just a manifest) and register it alongside your normally-installed app using an external location. Your .exe stays exactly where your installer puts it. This is the production counterpart to winapp create-debug-identity, which is for developer-time debugging only.
This guide covers the three CLI steps that map to the first three steps of the official Grant identity to non-packaged apps workflow:
| Step | Command | Result |
|---|---|---|
| 1. Create the identity manifest | winapp init --exe <exe> --sparse |
sparse/appxmanifest.xml + sparse/Assets/ |
| 2. Build & sign the identity package | winapp pack <appxmanifest.xml> --cert <pfx> |
<PackageName>.identity.msix |
| 3. Embed identity into the app | winapp embed-identity <exe> |
<msix> element in the exe's fusion manifest |
Steps 4–5 of the docs (register / unregister the package) are your installer's responsibility — see Installer integration.
When to use sparse packaging
- You already have a mature installer (Inno Setup, WiX, NSIS, MSI) and don't want to switch to MSIX for distribution, but you need identity-gated Windows APIs.
- Your app must install to a path or with a layout that MSIX doesn't allow.
- You want a minimal, additive change: keep your existing install flow and add one
.msixregistration step.
If you're starting fresh and can distribute as MSIX, a full packaged app (winapp init + winapp pack <folder>) is simpler.
Prerequisites
- Windows 10, version 2004 (build 19041) or later. Sparse packages rely on
uap10:AllowExternalContent, which requires 19041+. - winapp CLI — install via winget (or update if already installed):
winget install Microsoft.WinApp --source winget - A code-signing certificate trusted on the target machine. For local testing, generate a development certificate with
winapp cert generateand trust it. Production packages must be signed with a certificate whose subject matches the manifestPublisher.
Walkthrough
The examples below assume a built executable at ./bin/Release/net8.0-windows/MyApp.exe.
Step 1 — Create the sparse identity manifest
winapp init --exe ./bin/Release/net8.0-windows/MyApp.exe --sparse
This infers the package name, publisher, version, and description from the exe (via its file version info) and prompts you to accept or override them. Add --use-defaults (or --no-prompt) to skip the prompts in CI, and --name / --publisher to override specific values:
winapp init --exe ./bin/Release/net8.0-windows/MyApp.exe --sparse --use-defaults `
--name "Contoso.MyApp" --publisher "CN=Contoso"
It writes the following to a dedicated sparse/ folder in the current directory by default (override with --output-dir):
appxmanifest.xml— a sparse manifest with<uap10:AllowExternalContent>true</uap10:AllowExternalContent>(an element under<Properties>),ProcessorArchitecture="neutral", awin32Appapplication, and the exe name filled intoExecutable.Assets/— placeholder visual assets (extracted from the exe's icon when possible).
Why a
sparse/folder and not next to the exe? The manifest andAssets/are build-time inputs consumed bywinapp packandwinapp embed-identity— nothing reads them from beside the exe at runtime (runtime identity comes from the<msix>element embedded in the exe plus the registered package's external location, and the manifest references the exe by name, so its location is independent of where the exe lives). Writing them into a dedicated, source-controlled folder keeps them out of a build-output directory (likebin/) that a clean/rebuild would wipe, and keeps the folder free of binaries so the next steps stay clean.winapp packandwinapp embed-identitylook insparse/automatically, so you rarely need to name the path.
Note: The sparse init flow deliberately skips all SDK/package installation — identity-only packages have no SDK dependencies.
If an appxmanifest.xml already exists in the target directory, init stops rather than overwriting it (and its Assets/). Re-run with --force to regenerate it.
Make sure the Publisher in the generated manifest matches the certificate you'll sign with. Edit appxmanifest.xml if needed, or pass --publisher when generating.
Step 2 — Build and sign the identity package
Point winapp pack at the sparse manifest (a file, not a folder):
winapp pack ./sparse/appxmanifest.xml --cert ./devcert.pfx
Because the manifest declares AllowExternalContent, winapp pack builds an identity-only .msix containing just the manifest — no binaries, no assets. The output defaults to <PackageName>.identity.msix in the current directory; use --output to change it. Signing happens only when you pass --cert (or --generate-cert).
Step 3 — Embed identity into your app
Embed the <msix> element so Windows connects the running exe to the identity package:
# EXE mode — modify the built binary in place (uses mt.exe)
winapp embed-identity ./bin/Release/net8.0-windows/MyApp.exe
Or maintain the side-by-side manifest as a checked-in file and rebuild:
# XML mode — update an external SxS manifest, then rebuild your app
winapp embed-identity ./app.manifest
In XML mode the <msix> element is inserted into (or replaced in) the target manifest. Reference that manifest from your project (for .NET, set <ApplicationManifest>app.manifest</ApplicationManifest>) and rebuild so the element is embedded in the exe.
Both modes read identity from a sparse appxmanifest.xml. When you omit --manifest, winapp looks in a sparse/ folder (where winapp init --exe --sparse writes it by default) beside the target first, then in the current directory, then falls back to beside the target and the current directory; pass --manifest to point elsewhere.
Note: EXE mode rewrites the binary with
mt.exe, which invalidates any existing Authenticode signature. Re-sign the exe (e.g.winapp sign ./MyApp.exe <cert.pfx>) before distributing it.
Step 4 — Register (for local testing)
The manifest's logos are resolved from the external location at runtime, not from the
identity-only .msix. Step 1 wrote them under ./sparse/Assets, so copy them next to your exe
(the external location) before registering — otherwise Windows registers a layout missing every
logo the manifest references:
# Copy the generated assets into the external location (beside your exe)
Copy-Item ./sparse/Assets -Destination .\bin\Release\net8.0-windows\Assets -Recurse -Force
Then register the identity package against that folder (the external location):
Add-AppxPackage -Path .\MyApp.identity.msix `
-ExternalLocation (Resolve-Path .\bin\Release\net8.0-windows)
Launch the app and confirm identity is present — for example, Windows.ApplicationModel.Package.Current.Id.FamilyName should return your package family name instead of throwing.
To clean up:
Remove-AppxPackage <full-package-name>
Asset handling
The sparse .msix is identity-only. The visual assets referenced by the manifest (Assets\StoreLogo.png, tiles, etc.) are resolved from the external content location at runtime — i.e., from your app's install directory — not from inside the .msix.
This means you must deploy the Assets/ folder alongside your application (same layout the manifest expects, relative to the external location).
Step 2 packs the manifest file directly (winapp pack ./sparse/appxmanifest.xml), which builds the identity-only .msix from just that manifest — sibling files are ignored, so it never includes your assets or binaries. (If you instead point winapp pack at a folder whose manifest declares AllowExternalContent, it warns about any assets or binaries it finds, since for a sparse package those belong at the external location, not inside the .msix.)
Installer integration
Registration and unregistration are the installer's job. The pattern is the same across installer tools:
- Install: copy your app binaries, the
Assets/folder, and the.msixto the install directory, then runAdd-AppxPackage -Path "<install-dir>\MyApp.identity.msix" -ExternalLocation "<install-dir>". - Uninstall: run
Remove-AppxPackage <full-package-name>before deleting files.
Security: the install directory is resolved at install time and may contain characters (e.g. a single quote) that break out of a PowerShell string literal. Always escape or validate the path before interpolating it into a
-Commandstring — the WiX and NSIS snippets below assume a trusted install path, while the Inno Setup example demonstrates safe escaping. Prefer passing paths as arguments to a-Filescript over inline-Commandinterpolation.
Inno Setup
Build the PowerShell arguments in a [Code] function so the runtime install path is escaped
for the single-quoted PowerShell literal (an install directory containing a ' must not be able
to inject script):
[Files]
Source: "dist\*"; DestDir: "{app}"; Flags: recursesubdirs
Source: "MyApp.identity.msix"; DestDir: "{app}"
[Run]
Filename: "powershell.exe"; Parameters: "{code:RegisterParams}"; Flags: runhidden
[UninstallRun]
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Get-AppxPackage -Name 'MyApp' | Remove-AppxPackage"""; \
Flags: runhidden
[Code]
function EscapePSLiteral(const Value: string): string;
var S: string;
begin
S := Value; StringChange(S, '''', ''''''); Result := S;
end;
function RegisterParams(Param: string): string;
var AppDir: string;
begin
AppDir := ExpandConstant('{app}');
{ -ErrorAction Stop + try/catch make a registration failure terminating, so powershell.exe
exits nonzero and the AfterInstall callback (see the full sample) can abort with rollback. }
Result := '-NoProfile -ExecutionPolicy Bypass -Command "try { Add-AppxPackage -Path ''' +
EscapePSLiteral(AppDir + '\MyApp.identity.msix') +
''' -ExternalLocation ''' + EscapePSLiteral(AppDir) + ''' -ErrorAction Stop } catch { Write-Error $_; exit 1 }"';
end;
See the sparse-app sample for a complete, working setup.iss.
The WiX and NSIS examples below invoke a small register-sparse.ps1 via -File so the install path is passed as a parameter (PowerShell binds it as data) instead of being interpolated into a -Command string. This avoids script injection through a crafted install directory (e.g. a folder name containing a quote or $(...)):
# register-sparse.ps1 — ship this alongside your installer
param(
[Parameter(Mandatory)] [string] $MsixPath,
[Parameter(Mandatory)] [string] $ExternalLocation,
[Parameter(Mandatory)] [string] $PackageName
)
$ErrorActionPreference = 'Stop'
try {
# Add-AppxPackage emits NON-terminating errors by default, so a failure would otherwise leave
# the process exit code at 0 and let the installer complete without identity. Try the add
# directly first: a fresh install or a version-bumped upgrade registers/updates in place
# without touching any existing registration. -ErrorAction Stop + the outer trap make a real
# failure terminating so the installer (WiX Return="check" / NSIS) sees it.
try {
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
} catch {
# Only ONE failure is safe to resolve by unregister+retry: the exact same version is already
# registered (HRESULT 0x80073CFB, ERROR_PACKAGE_ALREADY_EXISTS — "already installed,
# reinstallation blocked"), which Add-AppxPackage rejects. Re-throw everything else
# (untrusted/corrupt .msix, unsupported OS, ...) so a bad new package can NEVER unregister a
# working prior registration and strip the installed app of the identity it already had.
if ($_.Exception.HResult -ne 0x80073CFB) { throw }
Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
}
} catch {
Write-Error $_
exit 1
}
WiX (v3)
Register per user (Impersonate="yes"), because Add-AppxPackage registers the package for the account that runs it. A deferred action with Impersonate="no" runs as LocalSystem, which does not grant identity to the installing user (and is commonly rejected). For a per-machine MSI, run the registration impersonated so it applies to the invoking user.
A deferred custom action can't read INSTALLFOLDER directly (deferred actions run in a context without access to properties), and simply declaring the action doesn't run it. So marshal the paths in through CustomActionData — an immediate type-51 action whose Property name equals the deferred action's Id — and schedule both after InstallFiles:
<!-- Immediate: stash the command line (with the resolved paths) into the deferred action's
CustomActionData. Windows Installer copies the value of the property named the same as a
deferred action into that action's CustomActionData. -->
<CustomAction Id="SetRegisterSparseCmd" Property="RegisterSparse" Execute="immediate"
Value="powershell.exe -NoProfile -ExecutionPolicy Bypass -File "[INSTALLFOLDER]register-sparse.ps1" -MsixPath "[INSTALLFOLDER]MyApp.identity.msix" -ExternalLocation "[INSTALLFOLDER]" -PackageName "MyPackageIdentityName"" />
<!-- Deferred + impersonated: CAQuietExec reads its command line from CustomActionData when run
deferred, so it registers the package for the invoking user. Return="check" fails the
install if registration fails. -->
<CustomAction Id="RegisterSparse" BinaryKey="WixCA" DllEntry="CAQuietExec"
Execute="deferred" Impersonate="yes" Return="check" />
<InstallExecuteSequence>
<Custom Action="SetRegisterSparseCmd" After="InstallFiles">NOT Installed</Custom>
<Custom Action="RegisterSparse" After="SetRegisterSparseCmd">NOT Installed</Custom>
</InstallExecuteSequence>
CAQuietExec ships in the WiX util extension (WixUtilExtension); reference it so the WixCA binary is available.
A single impersonated action registers identity only for the user running the installer. To provision every user of a per-machine install, register on first launch (per-user) instead, or use a provisioning mechanism such as
Add-AppxProvisionedPackage.
NSIS
Section
# Capture the PowerShell exit code and abort if registration failed. register-sparse.ps1 exits
# nonzero on failure (it sets $ErrorActionPreference='Stop' and traps), so without this check the
# installer would complete even though the app has no identity.
ExecWait 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\register-sparse.ps1" -MsixPath "$INSTDIR\MyApp.identity.msix" -ExternalLocation "$INSTDIR" -PackageName "MyPackageIdentityName"' $0
IntCmp $0 0 +2
Abort "Registering the sparse identity package failed (exit code $0). The app requires package identity."
SectionEnd
Troubleshooting
Package.Current throws / "no package identity" at runtime
- The identity package isn't registered, or the exe's fusion manifest is missing the
<msix>element. Re-runwinapp embed-identity(and rebuild if using XML mode), then re-register withAdd-AppxPackage -ExternalLocation. - The
<msix packageName>/publisher/applicationIdin the exe must exactly match the registered package's identity.
Assets/logos don't appear
- Ensure the
Assets/folder is deployed at the external location with the same relative paths the manifest expects. Assets are resolved from the external location, not the.msix.
Add-AppxPackage fails with a signing / trust error
- The
.msixmust be signed by a certificate that is trusted on the machine and whose subject matches the manifestPublisher. For local testing, generate and trust a dev certificate withwinapp cert generate, and make sure the manifestPublishermatches it.
MakeAppx: "Application with RuntimeBehavior value 'win32App' must not declare EntryPoint"
- A sparse
win32Appapplication must not declareEntryPoint. Manifests generated bywinapp init --sparseare already correct; remove anyEntryPointattribute if you hand-edited the manifest.
"Input is a file but not a sparse manifest"
winapp pack <file>only accepts a manifest that declares<uap10:AllowExternalContent>true</uap10:AllowExternalContent>. Generate one withwinapp init --exe <exe> --sparse, or pass an input folder to build a full MSIX.
See also
Windows developer