Add support for sparse packaging workflows - #607
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds first-class support for the production sparse packaging workflow (identity-only MSIX) to the winapp CLI, implementing steps 1–3 of the Microsoft docs flow. It complements the existing developer-time create-debug-identity helper with a supported path to produce a signed, identity-only .msix for distribution, and to embed that identity into an app's side-by-side manifest.
Changes:
- Adds
winapp init --exe <exe> --sparse(generate a sparse identity manifest + placeholder assets, inferring defaults from the exe'sFileVersionInfo, skipping SDK install), makeswinapp packsparse-aware (auto-detectsAllowExternalContent, accepts a manifest file directly, stages a manifest-only package), and introduces the newwinapp embed-identity <exe|xml>command. - Corrects the sparse template per the MS docs schema (
win32Appruntime behavior,MinVersion 10.0.19041.0,ProcessorArchitecture="neutral"), and surfaces the new command/options through the npm SDK and the VS Code extension. - Adds a WPF
samples/sparse-app/(with Inno Setup installer + Pester test wired into CI), a newdocs/guides/sparse.md, and regenerates docs/schema/skill fragments.
Reviewed changes
Copilot reviewed 50 out of 54 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs |
New CreateSparseIdentityPackageAsync, EmbedIdentityAsync, XML-manifest embedding, and sparse output-path resolution |
src/winapp-CLI/WinApp.Cli/Services/MsixService.cs |
Sparse detection/warning helpers and template runtime-behavior handling (minor indentation regression flagged) |
src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs |
Routes manifest-file input with AllowExternalContent to the sparse packaging path |
src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs |
Adds --exe/--sparse options and sparse init flow with validation |
src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs |
New embed-identity command (EXE/XML modes, non-sparse rejection) |
src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.sparse.xml |
Template corrected to MS docs schema (win32App, MinVersion, neutral arch) |
src/winapp-npm/src/winapp-commands.ts |
npm SDK bindings for embed-identity and new init sparse options |
src/winapp-VSC/src/extension.ts, src/winapp-VSC/package.json |
Registers the winapp.embedIdentity VS Code command |
.github/plugin/agents/winapp.agent.md, .claude/agents/winapp.md |
Agent docs updated for embed-identity (regression: winapp run heading dropped) |
samples/sparse-app/* |
New WPF sample, Inno Setup installer, and Pester test |
docs/*, docs/cli-schema.json |
New sparse guide plus regenerated usage/schema/skill docs |
src/winapp-CLI/WinApp.Cli.Tests/SparsePackagingTests.cs, FakeMsixService.cs |
Tests for init inference/validation, pack routing, and embed-identity modes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Build Metrics ReportBinary Sizes
Test Results✅ 4218 passed, 5 skipped out of 4223 tests in 878.9s (+51 tests, +219.2s vs. baseline) Test Coverage✅ 93% line coverage, 87.2% branch coverage · CLI Startup Time51ms median (x64, Try This BuildInstalls the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing. & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 607Switching between builds often?Put the tool on your PATH once: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPathThen this build is just: winapp-pr 607Run Updated 2026-08-04 19:47:48 UTC · commit |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Nikola Metulev (nmetulev)
left a comment
There was a problem hiding this comment.
Review: sparse packaging workflow
I reviewed this branch across security, correctness, CLI UX, alternative solutions, test coverage, docs/samples, and packaging, then empirically validated the findings by running the real winapp init --sparse -> pack -> embed-identity workflow against three genuine executables: a .NET exe, the actual electron.exe, and a Rust cargo exe.
Bottom line: the feature works end-to-end on all three frameworks — identity embeds correctly, Electron's existing manifest (Common-Controls, trustInfo, dpiAware, supportedOS) is preserved, and quoted publisher DNs (CN="GitHub, Inc.") are XML-escaped. The items below are worth addressing; details are in the inline comments.
Scope note
Some issues live in EmbedMsixIdentityToExeAsync, which pre-exists on main (previously only used internally for debug-identity embedding). This PR doesn't introduce those bugs, but the new embed-identity command routes user-supplied exes into that method for the first time, which materially widens their blast radius. They're flagged at the new call site (MsixService.Identity.cs:181) and labeled accordingly.
Findings
| ID | Sev | Area | Where | Issue | Repro |
|---|---|---|---|---|---|
| H1 | High | security | extension.ts:560/567 (sink :66) | PowerShell injection: file-picker path interpolated unescaped into terminal.sendText |
executed arbitrary code |
| M1 | Med | correctness | Identity.cs (pre-existing, new call :181) | Fixed-name temp manifests deleted from target dir -> silent user-file loss | deleted planted files |
| M2 | Med | cli-ux | EmbedIdentityCommand.cs:51 | Auto-detect prefers Package.appxmanifest over sparse appxmanifest.xml; fails when both present |
exit 1 |
| M3 | Med | docs | sparse.md:68 | --cert ./dev.pfx but cert generate writes devcert.pfx |
verified |
| M8 | Med | correctness | Identity.cs (pre-existing, new call :181) | Re-embedding a changed identity hard-fails with cryptic mt.exe c1010001 | exit 1 |
| L2 | Low | correctness | Identity.cs:655 (pre-existing) | Stray ; in generated fusion manifest (mt.exe strips it -> cosmetic) |
fires, stripped |
Also worth a look (outside the diff hunks, so no inline anchor)
- Test coverage: the new
embed-identityEXE path, the identity-only.msixcontents (CreateSparseIdentityPackageAsync), andinit --sparsewithout--exedon't appear to be asserted inSparsePackagingTests.cs. An idempotency/rerun test would have caught M8. - README: the PR links the new sample in the samples table (
:259) but not the guide in the "Additional guides" section (:130). - File size: this change grows
MsixService.Identity.csto ~1,209 lines (was 850) — over the repo's ~1,000-line guideline; a partial-class split would help.
Reviewed with the pr-review skill + a GPT cross-check, then validated empirically on dotnet/electron/rust repro apps.
# Conflicts: # docs/npm-usage.md # src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs # src/winapp-VSC/package.json # src/winapp-VSC/src/extension.ts
- embed-identity auto-detect now prefers a sparse manifest so a full Package.appxmanifest alongside appxmanifest.xml is no longer picked and rejected. - Manifest embedding writes temp files under the system temp directory with unique names instead of fixed names beside the exe, avoiding silent deletion of user files. - Strip any existing <msix> from the extracted manifest before the mt.exe merge so re-branding an exe is idempotent instead of failing c1010001. - Remove stray semicolon in the generated assemblyIdentity fusion snippet. - Fix cert filename in sparse guide (devcert.pfx, matching cert generate). - Add regression tests for temp-file safety and <msix> stripping. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 55 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
samples/sparse-app/installer/setup.iss:26
- The installer script lives under
installer/, and Inno resolves relative[Files]sources from the script directory by default. Consequently this path resolves asinstaller/bin/...; compiling the documentedinstaller/setup.isscurrently fails on line 42 with “No files found matching ...\installer\bin\...”. Set the source root to the sample's parent directory so bothPublishDirandMyMsixNameresolve to the artifacts produced by the README commands.
#define PublishDir "bin\Release\net10.0-windows10.0.19041.0\win-x64\publish"
Copilot review: - embed-identity: warn users to re-sign the exe after mt.exe rewrites it (invalidates any existing Authenticode signature). - sparse-app setup.iss: resolve relative [Files] sources from the sample dir (SourceDir=..) so PublishDir and the identity .msix resolve correctly. - sparse-app setup.iss: unregister by exact Identity Name (Get-AppxPackage -Name SparseAppSample) instead of a SparseAppSample* wildcard. - setup skill fragment: document the sparse identity init workflow and outputs; regenerate the plugin/claude skill mirrors. Code quality (CodeQL generic-catch + LINQ/using): - EmbedIdentityCommand: use LINQ over a static readonly candidate array; narrow IsSparseManifest catch; rethrow OperationCanceledException before generic catch. - PackageCommand: narrow IsSparseManifestAsync catch; rethrow cancellation. - InitCommand: rethrow cancellation before the sparse-init generic catch. - ManifestService: using var for the extracted Icon/Bitmap; narrow the FileVersionInfo and cleanup catches; rethrow cancellation in logo extraction. The temp-file data-loss re-flag was already fixed in b18bad3 (temp manifests live under Path.GetTempPath()); no code change needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
- init: reject the sparse-only options (--exe/--name/--publisher/--output-dir) when --sparse is absent, so scripts fail instead of silently discarding input. - ManifestService: reject inferred versions whose components exceed 65535 (MSIX Identity/@Version is 16-bit) so inference falls back to a packable default; add regression cases to NormalizeManifestVersion tests. - embed-identity: document the actual manifest search order (target dir first, then current directory) in the --manifest description and the sparse guide, instead of the inaccurate "./appxmanifest.xml" default. - sparse guide: note that EXE mode invalidates the Authenticode signature. - sparse-app sample: copy Assets/ to build/publish output so the external content location has the logos the manifest references. - sparse-app app.manifest: fix the invalid XML comment (removed the literal "--manifest", which XML comments cannot contain). - Regenerate docs/cli-schema.json, the identity skill, and the npm winapp-commands.ts binding to match (schema version pinned at 0.5.1). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 56 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (3)
src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs:148
- This sparse branch returns before the normal init path, so the positional
base-directoryand existing options such as--config-dir,--config-only,--setup-sdks,--ignore-config, and--no-gitignoreare silently ignored. For example,winapp init ./identity --exe ./app.exe --sparsesucceeds but writes beside the exe instead of./identity. Reject incompatible arguments with an actionable error, or explicitly map the positional directory to sparse output semantics.
src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs:27 - The help text says the default is
./appxmanifest.xml, but the implementation first searches beside the target, then the current directory, and also considersPackage.appxmanifest. This can select a different identity than the documented default. Describe the actual precedence here so--help, CLI schema, npm docs, and generated skills remain accurate after regeneration.
samples/sparse-app/sparse-app.csproj:18 - The installer copies only the publish directory, but this project does not mark the checked-in
Assets/files for output or publish copying. A normaldotnet publishtherefore omits the external assets referenced byappxmanifest.xml, so the production installer cannot deploy the external-content layout it claims to provide. Add copy metadata forAssets/**and assert the published assets exist in the sample test.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 70 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (5)
src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:128
- This validation still accepts empty required attributes.
ParseAppxManifestAsynconly checks whetherIdentity/@Name,Identity/@Publisher, andApplication/@Idare present, so values such asName=""pass; because packing uses MakeAppx/nv, the command can then report success while producing an undeployable identity package. Explicitly reject null/empty/whitespace values before packaging.
samples/sparse-app/installer/setup.iss:124 - The fallback unregisters the currently working package for every
Add-AppxPackagefailure, not just a same-version conflict. Trust, access, malformed-package, or transient deployment failures therefore remove the old registration and then usually fail again, leaving an upgrade rollback with no working package identity. Only unregister for the specific conflict that requires replacement, or preserve and restore the prior registration on failure.
src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs:186 - Rejecting
--executableleaves no way to package sparse manifests produced by the existingwinapp manifest generate --template sparseflow: that generator intentionally leavesExecutable="$targetnametoken$.exe"(seeManifestCommandTests.cs:103), and the new manifest-file path copies it unchanged while using MakeAppx/nv. The command can therefore report success for an identity package whose executable is the literal placeholder. Resolve the token from--executable, or fail validation when a manifest-file input still contains it.
docs/guides/sparse.md:59 - This describes
Assets/as a build-time input consumed bypack/embed-identity, but neither command consumes it: manifest-file packing intentionally stages only the manifest, and embedding reads only identity metadata. The assets are deployment inputs that must be copied into the registered external location, as the guide correctly explains later. Calling them build-time inputs here can lead users to omit them from the installer.
> **Why a `sparse/` folder and not next to the exe?** The manifest and `Assets/` are **build-time inputs** consumed by `winapp pack` and `winapp 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 (like `bin/`) that a clean/rebuild would wipe, and keeps the folder free of binaries so the next steps stay clean. `winapp pack` and `winapp embed-identity` look in `sparse/` automatically, so you rarely need to name the path.
docs/guides/sparse.md:211
- This recommended installer script removes the existing package on any exception from the first add. Failures unrelated to a same-version conflict (for example trust, access, or a corrupt new package) then destroy a working registration before retrying the same failing operation. Restrict replacement to the relevant deployment conflict or restore the old registration if the retry fails.
try {
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
} catch {
Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
}
Two CI failures were both caused by the sparse-packaging additions:
- sparse-app job: ISCC failed to compile setup.iss ('BEGIN' expected on
line 130). A Pascal { } block comment contained the text '{app}', and
Inno's brace comments do not nest, so the '}' in '{app}' closed the
comment early and the trailing prose was parsed as code. Reworded the
comment to avoid the constant braces.
- build-and-package job: the MS Learn docs validation (port-mslearn-docs.ps1)
failed because docs/guides/sparse.md had no '<!-- description: -->' marker
distinct from its H1 (a hard error), and the ported page was not present in
the generated toc.yml. Added the description marker and registered
guides/sparse.md in the port script's title map and TOC tree.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 71 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:128
- Manifest-file packaging bypasses the sparse normalization used for folder inputs and then invokes MakeAppx with
/nv.ParseAppxManifestAsynconly proves that identity/application fields exist, so a sparse manifest withEntryPoint, legacyRuntimeBehavior="packagedClassicApp", or another deployment-invalid sparse combination can still produce a “successful” MSIX thatAdd-AppxPackagerejects. Apply the same sparse rewrite/semantic validation as the folder path before staging, and add a regression case for an invalid hand-authored sparse manifest.
samples/sparse-app/installer/setup.iss:123 - This catch removes the existing registration after every
Add-AppxPackagefailure, not only the same-version conflict described above. A bad signature, missing trust, locked file, or transient deployment error therefore deletes a working prior identity; if the retry fails, installer file rollback cannot restore that registration. Only unregister after positively identifying the expected same-version conflict, or fail while preserving the existing package.
samples/sparse-app/sparse-app.csproj:22 - Use the repository’s established recursive asset glob (
Assets\**\*), as documented byIDotNetService.cs:126and asserted inDotNetServiceTests.cs:1087. The bare trailing**deviates from the generated project pattern and can fail to enumerate the asset files that sparse registration expects in publish output.
src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs:297 - The linked issue specifies that sparse init defaults to the executable’s directory, but this implementation defaults to
./sparseunder the current directory. The new docs consistently describe./sparse, so this appears intentional; update the PR/issue acceptance text or restore the specified default so the public contract is unambiguous.
samples/sparse-app/installer/setup.iss:89 - An exact
Get-AppxPackage -Namematch identifies only the package Identity Name, not the publisher/package family. If another package family uses the same Name, this pipeline unregisters it too, contradicting the comment that unrelated packages are protected. Filter by the expected Publisher/PackageFamilyName and remove one exact PackageFullName.
docs/guides/sparse.md:212 - The reusable installer script has the same destructive fallback as the sample: any add failure first removes the currently working registration, even when the failure is unrelated to a same-version conflict. Preserve the existing package unless the caught deployment error is specifically the conflict this retry is intended to resolve.
try {
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
} catch {
Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
}
Resolve conflicts in test-samples.yml (union of sparse-app and winui-app/ winui-solution/winui-unpackaged-app sample lists) and the auto-generated agent files (keep embed-identity section plus main's updated run command description). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 72 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
samples/sparse-app/installer/setup.iss:123
- The fallback unregisters the currently working package after any
Add-AppxPackagefailure, not only a same-version conflict. A bad/untrusted replacement package, locked files, or a transient deployment error therefore removes the existing registration; when the retry fails, Inno rolls back files but does not restore that registration, leaving the previously installed app without identity. Only remove on the specific conflict this fallback is intended to handle, and preserve/restore the prior registration if the retry fails.
The register-sparse.ps1 docs helper and the sparse-app setup.iss inline registration unregistered the existing package on ANY first Add-AppxPackage failure, then retried. An untrusted or corrupt new .msix (or an unsupported OS) would trip the catch, remove a working prior registration, then fail the retry too — leaving the installed app with no identity. Gate the unregister+retry on HRESULT 0x80073CFB (ERROR_PACKAGE_ALREADY_EXISTS, the same-version-already-registered conflict Add-AppxPackage rejects) and re-throw every other failure so a bad package can never strip existing identity. Verified the parsing of 0x80073CFB is identical on Windows PowerShell 5.1 and PowerShell 7, and validated both control-flow paths (conflict -> remove+retry; untrusted -> abort without removing) with mocked cmdlets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 72 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
samples/sparse-app/installer/setup.iss:144
RaiseExceptionfrom this[Files]AfterInstallcallback does not fail Setup. I compiled and ran the installer with an untrusted package certificate:RegisterParamsreturned exit code 1, but Inno logged the exception as a suppressed “Expression error,” continued creating shortcuts/uninstall metadata, exited 0, and left the app installed with noSparseAppSamplepackage registered. This turns a registration failure into an apparently successful identity-less install. Move registration to an Inno execution path whose failure propagates to Setup, and explicitly clean up copied files/registration on failure rather than relying on automatic rollback.
The direct-file sparse pack path staged the input manifest with File.Copy, bypassing the sparse corrections folder packing applies. A manifest from an older template or hand-edit could ship with RuntimeBehavior=packagedClassicApp, a missing ProcessorArchitecture, or a MinVersion below the 10.0.19041.0 that AllowExternalContent requires. Add MsixService.NormalizeSparseIdentityManifest, applied when staging the manifest: forces win32App/mediumIL for an .exe app, removes EntryPoint, defaults a missing ProcessorArchitecture to neutral, and raises any TargetDeviceFamily MinVersion below 10.0.19041.0. Corrections are surfaced as status messages. No-op for a freshly generated (correct) manifest. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Folder inputs route sparse manifests through UpdateAppxManifestContentAsync, but that rewrite did not raise TargetDeviceFamily/@MinVersion. A folder created from an older sparse template kept 10.0.18362.0; because packing uses MakeAppx /nv, the command could still produce and sign an MSIX deployment rejects, since AllowExternalContent requires 10.0.19041.0. Extract the MinVersion flooring from NormalizeSparseIdentityManifest into a shared RaiseSparseTargetDeviceFamilyMinVersion helper and apply it in the folder-packing sparse block, so both the manifest-file and folder paths enforce the same 10.0.19041.0 floor. Corrections are surfaced as status messages. Add a folder-path regression test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 72 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:242
- This does not actually prevent an invalid identity package from being produced.
ParseAppxManifestAsynconly checks thatIdentity.Name,Identity.Publisher, andApplication.Idattributes exist; empty values, a missingIdentity.Version, and missing required package sections such asDependencies/TargetDeviceFamilystill pass. Because packaging deliberately uses MakeAppx/nv, such a manifest can be packed and signed successfully but then fail duringAdd-AppxPackage. Please validate the complete deployment-required sparse manifest structure (and non-empty/schema-valid attribute values) before packaging.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 72 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/winapp-npm/src/cli-args.ts:94
- This does not match the native parser for repeated occurrences. The built CLI resolves
--sparse=false --sparsetofalse(the explicit value wins over the bare occurrence), while this returnstrue; two valued occurrences are rejected by native parsing rather than using the last value. In the npm wrapper, that mismatch selects the sparse fast path and can reject--add-js-bindingseven though nativeinitis in normal mode. Track explicit values separately and bypass wrapper hooks for combinations the native parser will reject.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 71 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
docs/guides/sparse.md:215
- The documented retry removes the existing working registration before the replacement is known to succeed. If the second
Add-AppxPackagefails, the installer receives an error but the old app has already lost package identity. Avoid unregistering for a same-version reinstall, or preserve and restore the prior registration when the retry fails.
Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:205
- A missing or malformed
MinVersionbypasses this normalization becauseVersion.TryParsereturns false. Since sparse packages are packed with MakeAppx/nv, the command can then report success (and even sign the output) for a package that deployment rejects. Treat an unparseable value like a below-floor value, or fail validation explicitly.
| 'Get-AppxPackage -Name ''' + EscapePSLiteral('{#MyPackageName}') + ''' | Remove-AppxPackage -ErrorAction SilentlyContinue; ' + | ||
| 'Add-AppxPackage -Path ''' + MsixPath + ''' -ExternalLocation ''' + EscapePSLiteral(AppDir) + ''' -ErrorAction Stop } ' + |
Add sparse packaging support to the winapp CLI
fixes #286
Summary
Adds first-class support for the production sparse packaging workflow described in the
MS docs: Grant package identity to non-packaged apps.
Previously, the only sparse-related capability was
create-debug-identity— a developer-timehelper that requires Developer Mode and registers a raw manifest. There was no supported path to
produce a signed, identity-only
.msixfor distribution. This PR covers steps 1–3 of the MSdocs workflow (manifest → signed
.msix→ embed identity into the exe). Steps 4–5(register/unregister) remain the installer's responsibility.
The sparse packaging flow
A sparse package is an identity-only MSIX: it contains just a manifest (no binaries, no
bundled assets) and grants Windows package identity to an app that installs normally to the file
system. Identity unlocks modern Windows APIs (notifications, background tasks, share target,
startup tasks,
Package.Current, etc.) for otherwise unpackaged Win32/WPF/WinForms apps. Assetsand binaries are resolved from an external content location at runtime, registered via
Add-AppxPackage -ExternalLocation.The three CLI steps map directly to the MS docs:
winapp init --exe <exe> --sparseFileVersionInfowinapp pack <manifest> --cert <pfx>.msix(infers sparse fromAllowExternalContent)winapp embed-identity <exe|xml><msix>identity element into the app's SxS/fusion manifestAdd-AppxPackage -ExternalLocation/Remove-AppxPackage— not the CLI's jobWhat this adds
1.
winapp init --exe <exe> --sparseappxmanifest.xmlfrom the template, plus placeholder assets inAssets/.FileVersionInfo, with sensible fallbacks.--use-defaults/--no-promptfor CI.--exerequires--sparse, with a clear error otherwise..msix.2.
winapp pack— sparse-awareAllowExternalContent="true"in the manifest.appxmanifest.xmldirectly (no folder required): stages a manifest-only directory and packs it.--cert/--generate-cert).3.
winapp embed-identity <exe|xml>(new command)<msix>element into the exe's RT_MANIFEST viamt.exe.<msix>element in an external SxS manifest file.--manifest(default./appxmanifest.xml) and rejects non-sparse manifests with a clear error, since identity embedding only applies to external-location packages.4. Sparse template & code corrections (per MS docs schema)
RuntimeBehavior="packagedClassicApp"→"win32App"(correct for plain Win32, not Desktop Bridge).MinVersionraised to10.0.19041.0(required forAllowExternalContent/uap10).ProcessorArchitecture="neutral"to<Identity>(identity-only packages carry no binaries).Documentation
docs/guides/sparse.md— overview, prerequisites, end-to-end walkthrough, asset handling, installer integration (NSIS/WiX/Inno), and troubleshooting.docs/usage.md,docs/npm-usage.md, README, CLI schema, and the Copilot/Claude skill fragments (identity,package,setup).Sample:
samples/sparse-app/A minimal WPF sample demonstrating the full flow end-to-end, including an Inno Setup
installer (
installer/setup.iss) that installs the app, deploys the.msix, and registers thesparse package on install (and unregisters on uninstall).
MainWindowqueriesPackage.Currentand displays the package family name (or "No package identity" when unregistered).Includes a Pester
test.Tests.ps1and is wired into thetest-samplesCI matrix.What the CLI does not do
Add-AppxPackage -ExternalLocation).create-debug-identity— kept as-is for the debug workflow.Testing
SparsePackagingTests.cs(+426 lines) covering init inference/validation, sparse packrouting (manifest-vs-folder, warnings),
embed-identityEXE/XML modes, non-sparse rejection,and output-path resolution (including dotted-directory and
.msixbundleedge cases).FakeMsixServiceextended for command-level routing tests.dotnet test ... --filter "FullyQualifiedName~SparsePackaging").