Files
PowerToys/src/PackageIdentity
leileizhang b1985bc8d1 Introduce shared sparse package identity for PowerToys (#42352)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request adds support for building, installing, and managing a
shared sparse MSIX package to grant package identity to select Win32
components in PowerToys. It introduces a new `PackageIdentity` project,
updates the installer to handle the new MSIX package during
install/uninstall, and provides developer documentation for working with
the sparse package. Additionally, new dependencies and signing rules are
included to support these changes.

**Sparse Package Identity Support**

* Added new `PackageIdentity` project to the solution for building the
sparse MSIX package, and included it in solution/project build
configurations (`PowerToys.sln`).
[[1]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R29)
[[2]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R54-R55)
[[3]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R873-R880)
* Added developer documentation (`sparse-package.md`) and updated
documentation indexes to describe how to build, register, and consume
the sparse MSIX package.
[[1]](diffhunk://#diff-b4e39fb55a49c6de336d5847d75a55dd1d14840578da0ed9130f0130b61b34aaR1-R87)
[[2]](diffhunk://#diff-d0f204e503506a26ef2aa3605a8d64ac353393526fb5dcf48d4287c821f3edbcR31)
[[3]](diffhunk://#diff-430296c8d28f70d8a0164b44d7dfc30ffb1fb32466dad181947f35885b7f28d1R13)

**Installer Enhancements**

* Implemented new custom actions in the installer to install and
uninstall the `PowerToysSparse.msix` package, supporting both per-user
and machine-level scenarios (`CustomAction.cpp`, `CustomAction.def`,
`Product.wxs`).
[[1]](diffhunk://#diff-a7680a20bf0315cff463a95588a100c99d2afc53030f6e947f1f1dcaca5eefd7R597-R806)
[[2]](diffhunk://#diff-79daec0ccfcea63a2f3acb7d811b8b508529921123c754111bbccbea98b2bd74R36-R37)
[[3]](diffhunk://#diff-c12203517db7cde9ad34df9e6611457d1d3c7bc8eb7d58e06739887d3c1034afR115)
[[4]](diffhunk://#diff-c12203517db7cde9ad34df9e6611457d1d3c7bc8eb7d58e06739887d3c1034afR127)
[[5]](diffhunk://#diff-c12203517db7cde9ad34df9e6611457d1d3c7bc8eb7d58e06739887d3c1034afR149)
[[6]](diffhunk://#diff-c12203517db7cde9ad34df9e6611457d1d3c7bc8eb7d58e06739887d3c1034afR205-R210)

**Build and Dependency Updates**

* Added new NuGet package dependencies for Windows App SDK AI and
Runtime to support MSIX and sparse package features
(`Directory.Packages.props`).
* Updated signing pipeline to include the new `PowerToysSparse.msix`
artifact (`.pipelines/ESRPSigning_core.json`).
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
2025-10-20 08:52:49 +08:00
..

PowerToys sparse package identity

This document describes how to build, sign, register, and consume the shared sparse MSIX package that grants package identity to select Win32 components of PowerToys.

Package overview

The sparse package lives under src/PackageIdentity. It produces a payload-free MSIX whose Identity matches Microsoft.PowerToys.SparseApp. The manifest contains one entry per Win32 surface that should run with identity (for example Settings, PowerOCR, Image Resizer).

The MSIX contains only metadata. When the package is registered you must point -ExternalLocation to the output folder that hosts the Win32 binaries (for example x64\Release).

Building the sparse package locally

Two options are available:

  • Build the utility project from Visual Studio: PackageIdentity.vcxproj defines a GenerateSparsePackage target that runs before PrepareForBuild and invokes the helper script automatically.
  • Invoke the helper script directly from PowerShell:
$repoRoot = "C:/git/PowerToys"
pwsh "$repoRoot/src/PackageIdentity/BuildSparsePackage.ps1" -Platform x64 -Configuration Release

Supported switches:

  • -Clean removes previous bin/obj outputs and uninstalls existing installation.
  • -ForceCert regenerates the local dev certificate (.pfx/.cer/.pwd/.thumbprint) under src/PackageIdentity/.user.
  • -NoSign skips signing. The MSIX still builds but must be signed before deployment.
  • -CIBuild (or setting $env:CIBuild = 'true') keeps the manifest publisher intact and skips the local cert substitution.

The script determines the proper makeappx.exe for the host build machine (x64 on typical developer boxes) and creates PowerToysSparse.msix in {repo}\<Platform>\<Configuration>.

After packaging finishes, the helper also emits src/PackageIdentity/.user/PowerToysSparse.publisher.txt. This file mirrors the publisher string Windows will see once the sparse package is registered, which downstream projects can read to stay in sync when generating their own manifests.

Local signing basics

When -NoSign is not used the script generates (or reuses) a development certificate and signs the package via signtool.exe:

  1. Artifacts are stored in src/PackageIdentity/.user/PowerToysSparse.certificate.sample.* (.cer and .thumbprint).

  2. Install the .cer into CurrentUserTrustedPeople (and TrustedRoot, if necessary) so Windows trusts the signature:

    $repoRoot = "C:/git/PowerToys"
    Import-Certificate -FilePath "$repoRoot/src/PackageIdentity/.user/PowerToysSparse.certificate.sample.cer" -CertStoreLocation Cert:\CurrentUser\TrustedPeople
    
  3. The private key stays in the current user's personal certificate store.

Registering or unregistering the package

After PowerToysSparse.msix is generated:

# First time registration
$repoRoot = "C:/git/PowerToys"
$outputRoot = Join-Path $repoRoot "x64/Release"
Add-AppxPackage -Path (Join-Path $outputRoot "PowerToysSparse.msix") -ExternalLocation $outputRoot

# Re-register after manifest tweaks only
Add-AppxPackage -Register (Join-Path $repoRoot "src/PackageIdentity/AppxManifest.xml") -ExternalLocation $outputRoot -ForceApplicationShutdown

# Remove the sparse identity
Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Remove-AppxPackage

-ExternalLocation should match the output folder that contains the Win32 executables declared in the manifest. Re-run registration whenever the manifest or executable layout changes.

CI-specific guidance

  • Pass -CIBuild to BuildSparsePackage.ps1 (or build with msbuild PackageIdentity.vcxproj /p:CIBuild=true). This prevents the script from rewriting the manifest publisher to the local dev certificate subject.
  • The project automatically adds -NoSign only when $(CIBuild) is true. Local Debug and Release builds are signed with the development certificate.
  • Make sure the agent trusts whichever certificate signs the package. If the package remains unsigned (-NoSign) it cannot be installed on test machines until it is signed.

Consuming the identity from other components

  1. Add a new <Application> entry inside src/PackageIdentity/AppxManifest.xml. Use a unique Id (for example PowerToys.MyModuleUI) and set Executable to the Win32 binary relative to the -ExternalLocation root.

  2. Ensure the binary is copied into the platform/configuration output folder (x64\Release, ARM64\Debug, etc.) so the sparse package can locate it.

  3. Embed a sparse identity manifest in the Win32 binary so it binds to the MSIX identity at runtime. The manifest must declare an <msix> element with packageName="Microsoft.PowerToys.SparseApp", applicationId matching the <Application Id>, and a publisher that matches the sparse package. Keep the manifests publisher in sync with src/PackageIdentity/.user/PowerToysSparse.publisher.txt (emitted by BuildSparsePackage.ps1). See src/modules/imageresizer/ui/ImageResizerUI.csproj for an example that points ApplicationManifest to ImageResizerUI.dev.manifest for local builds and switches to ImageResizerUI.prod.manifest when $(CIBuild) is true.

  4. Register or re-register the sparse package so Windows learns about the new application Id.

  5. To launch the Win32 surface with identity, use the shell:AppsFolder activation form (for example: shell:AppsFolder\Microsoft.PowerToys.SparseApp_<PackageFamilyName>!PowerToys.MyModuleUI) or activate it via IApplicationActivationManager::ActivateApplication using the same AppUserModelID.

    • For locally built packages, resolve the <PackageFamilyName> with Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Select-Object -ExpandProperty PackageFamilyName.
    • Store-distributed builds use Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe. Local developer builds created by this script typically use a different family name derived from the dev certificate.
  6. Context menu handlers or other launchers should fall back to the unpackaged executable path for environments where the sparse package is not present.

Troubleshooting tips

  • Program 'makeappx.exe' failed to run: make sure you are running an x64 PowerShell host. The script now chooses the appropriate makeappx automatically; update your repo if the log still points to an ARM64 binary.
  • HRESULT 0x800B0109 (trust failure): install the development certificate into both TrustedPeople and TrustedRoot stores for the current user.
  • Stale registration: remove the package with Remove-AppxPackage and re-run the script with -Clean to rebuild from scratch.