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>
This commit is contained in:
leileizhang
2025-10-20 08:52:49 +08:00
committed by GitHub
parent b3628aa068
commit b1985bc8d1
20 changed files with 1051 additions and 1 deletions

View File

@@ -193,6 +193,7 @@ changecursor
CHILDACTIVATE CHILDACTIVATE
CHILDWINDOW CHILDWINDOW
CHOOSEFONT CHOOSEFONT
CIBUILD
cidl cidl
CIELCh CIELCh
cim cim
@@ -383,6 +384,7 @@ DISPLAYFREQUENCY
displayname displayname
DISPLAYORIENTATION DISPLAYORIENTATION
divyan divyan
djwsxzxb
Dlg Dlg
DLGFRAME DLGFRAME
DLGMODALFRAME DLGMODALFRAME
@@ -443,6 +445,7 @@ EDITSHORTCUTS
EDITTEXT EDITTEXT
EFile EFile
ekus ekus
eku
emojis emojis
ENABLEDELAYEDEXPANSION ENABLEDELAYEDEXPANSION
ENABLEDPOPUP ENABLEDPOPUP
@@ -816,6 +819,7 @@ keyvault
KILLFOCUS KILLFOCUS
killrunner killrunner
kmph kmph
ksa
kvp kvp
Kybd Kybd
LARGEICON LARGEICON
@@ -922,6 +926,7 @@ LWA
lwin lwin
LZero LZero
MAGTRANSFORM MAGTRANSFORM
makeappx
MAKEINTRESOURCE MAKEINTRESOURCE
MAKEINTRESOURCEA MAKEINTRESOURCEA
MAKEINTRESOURCEW MAKEINTRESOURCEW
@@ -1254,6 +1259,7 @@ pinvoke
pipename pipename
PKBDLLHOOKSTRUCT PKBDLLHOOKSTRUCT
pkgfamily pkgfamily
PKI
plib plib
ploc ploc
ploca ploca
@@ -1695,6 +1701,7 @@ syskeydown
SYSKEYUP SYSKEYUP
SYSLIB SYSLIB
SYSMENU SYSMENU
systemai
SYSTEMAPPS SYSTEMAPPS
SYSTEMMODAL SYSTEMMODAL
SYSTEMTIME SYSTEMTIME

View File

@@ -235,7 +235,9 @@
"*Microsoft.CmdPal.UI_*.msix", "*Microsoft.CmdPal.UI_*.msix",
"PowerToys.DSC.dll", "PowerToys.DSC.dll",
"PowerToys.DSC.exe" "PowerToys.DSC.exe",
"PowerToysSparse.msix"
], ],
"SigningInfo": { "SigningInfo": {
"Operations": [ "Operations": [

View File

@@ -61,6 +61,8 @@
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" /> <PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" /> <PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" /> <PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" /> <PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" /> <PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" /> <PackageVersion Include="ModernWpfUI" Version="0.9.4" />

View File

@@ -26,6 +26,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner
{D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} = {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} = {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}
{D940E07F-532C-4FF3-883F-790DA014F19A} = {D940E07F-532C-4FF3-883F-790DA014F19A} {D940E07F-532C-4FF3-883F-790DA014F19A} = {D940E07F-532C-4FF3-883F-790DA014F19A}
{DA425894-6E13-404F-8DCB-78584EC0557A} = {DA425894-6E13-404F-8DCB-78584EC0557A} {DA425894-6E13-404F-8DCB-78584EC0557A} = {DA425894-6E13-404F-8DCB-78584EC0557A}
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D} = {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}
{E364F67B-BB12-4E91-B639-355866EBCD8B} = {E364F67B-BB12-4E91-B639-355866EBCD8B} {E364F67B-BB12-4E91-B639-355866EBCD8B} = {E364F67B-BB12-4E91-B639-355866EBCD8B}
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}
EndProjectSection EndProjectSection
@@ -50,6 +51,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{1AFB64
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common.Lib.UnitTests", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common.Lib.UnitTests", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PackageIdentity", "src\PackageIdentity\PackageIdentity.vcxproj", "{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "powerrename", "powerrename", "{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "powerrename", "powerrename", "{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}"
@@ -867,6 +870,14 @@ Global
{1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|ARM64.Build.0 = Release|ARM64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|ARM64.Build.0 = Release|ARM64
{1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.ActiveCfg = Release|x64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.ActiveCfg = Release|x64
{1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.Build.0 = Release|x64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.Build.0 = Release|x64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|ARM64.Build.0 = Debug|ARM64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|x64.ActiveCfg = Debug|x64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|x64.Build.0 = Debug|x64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|ARM64.ActiveCfg = Release|ARM64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|ARM64.Build.0 = Release|ARM64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|x64.ActiveCfg = Release|x64
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|x64.Build.0 = Release|x64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.ActiveCfg = Debug|ARM64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.ActiveCfg = Debug|ARM64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.Build.0 = Debug|ARM64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.Build.0 = Debug|ARM64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64

View File

@@ -594,6 +594,216 @@ LExit:
return WcaFinalize(er); return WcaFinalize(er);
} }
UINT __stdcall InstallPackageIdentityMSIXCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
LPWSTR customActionData = nullptr;
std::wstring installFolderPath;
std::wstring installScope;
std::wstring msixPath;
std::wstring data;
size_t delimiterPos;
bool isMachineLevel = false;
hr = WcaInitialize(hInstall, "InstallPackageIdentityMSIXCA");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"CustomActionData", &customActionData);
ExitOnFailure(hr, "Failed to get CustomActionData property");
// Parse CustomActionData: "[INSTALLFOLDER];[InstallScope]"
data = customActionData;
delimiterPos = data.find(L';');
installFolderPath = data.substr(0, delimiterPos);
installScope = data.substr(delimiterPos + 1);
// Check if this is a machine-level installation
if (installScope == L"perMachine")
{
isMachineLevel = true;
}
Logger::info(L"Installing PackageIdentity MSIX - perUser: {}", !isMachineLevel);
// Construct path to PackageIdentity MSIX
msixPath = installFolderPath;
msixPath += L"PowerToysSparse.msix";
if (std::filesystem::exists(msixPath))
{
using namespace winrt::Windows::Management::Deployment;
using namespace winrt::Windows::Foundation;
try
{
std::wstring externalLocation = installFolderPath; // External content location (PowerToys install folder)
Uri externalUri{ externalLocation }; // External location URI for sparse package content
Uri packageUri{ msixPath }; // The MSIX file URI
PackageManager packageManager;
if (isMachineLevel)
{
// Machine-level installation
StagePackageOptions stageOptions;
stageOptions.ExternalLocationUri(externalUri);
auto stageResult = packageManager.StagePackageByUriAsync(packageUri, stageOptions).get();
uint32_t stageErrorCode = static_cast<uint32_t>(stageResult.ExtendedErrorCode());
if (stageErrorCode == 0)
{
std::wstring packageFamilyName = L"Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe";
try
{
auto provisionResult = packageManager.ProvisionPackageForAllUsersAsync(packageFamilyName).get();
uint32_t provisionErrorCode = static_cast<uint32_t>(provisionResult.ExtendedErrorCode());
if (provisionErrorCode != 0)
{
Logger::error(L"Machine-level provisioning failed: 0x{:08X}", provisionErrorCode);
}
}
catch (const winrt::hresult_error& ex)
{
Logger::error(L"Provisioning exception: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
}
}
else
{
Logger::error(L"Package staging failed: 0x{:08X}", stageErrorCode);
}
}
else
{
AddPackageOptions addOptions;
addOptions.ExternalLocationUri(externalUri);
auto addResult = packageManager.AddPackageByUriAsync(packageUri, addOptions).get();
if (!addResult.IsRegistered())
{
uint32_t errorCode = static_cast<uint32_t>(addResult.ExtendedErrorCode());
Logger::error(L"Per-user installation failed: 0x{:08X}", errorCode);
}
}
}
catch (const std::exception& ex)
{
Logger::error(L"PackageIdentity MSIX installation failed - Exception: {}",
winrt::to_hstring(ex.what()).c_str());
}
}
else
{
Logger::error(L"PackageIdentity MSIX not found: " + msixPath);
}
LExit:
ReleaseStr(customActionData);
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
UINT __stdcall UninstallPackageIdentityMSIXCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
using namespace winrt::Windows::Management::Deployment;
using namespace winrt::Windows::Foundation;
LPWSTR installScope = nullptr;
bool isMachineLevel = false;
PackageManager pm;
hr = WcaInitialize(hInstall, "UninstallPackageIdentityMSIXCA");
ExitOnFailure(hr, "Failed to initialize");
// Check if this was a machine-level installation
hr = WcaGetProperty(L"InstallScope", &installScope);
if (SUCCEEDED(hr) && installScope && wcscmp(installScope, L"perMachine") == 0)
{
isMachineLevel = true;
}
Logger::info(L"Uninstalling PackageIdentity MSIX - perUser: {}", !isMachineLevel);
try
{
std::wstring packageFamilyName = L"Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe";
if (isMachineLevel)
{
// Machine-level uninstallation: deprovision + remove for all users
// First deprovision the package
try
{
auto deprovisionResult = pm.DeprovisionPackageForAllUsersAsync(packageFamilyName).get();
if (deprovisionResult.IsRegistered())
{
Logger::warn(L"Machine-level deprovisioning completed with warnings");
}
}
catch (const winrt::hresult_error& ex)
{
Logger::warn(L"Machine-level deprovisioning failed: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
}
// Then remove packages for all users
auto packages = pm.FindPackagesForUserWithPackageTypes({}, packageFamilyName, PackageTypes::Main);
for (const auto& package : packages)
{
try
{
auto machineResult = pm.RemovePackageAsync(package.Id().FullName(), RemovalOptions::RemoveForAllUsers).get();
if (machineResult.IsRegistered())
{
uint32_t errorCode = static_cast<uint32_t>(machineResult.ExtendedErrorCode());
Logger::error(L"Machine-level removal failed: 0x{:08X} - {}", errorCode, machineResult.ErrorText());
}
}
catch (const winrt::hresult_error& ex)
{
Logger::error(L"Machine-level removal exception: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
}
}
}
else
{
// Per-user uninstallation: standard removal
auto packages = pm.FindPackagesForUserWithPackageTypes({}, packageFamilyName, PackageTypes::Main);
for (const auto& package : packages)
{
auto userResult = pm.RemovePackageAsync(package.Id().FullName()).get();
if (userResult.IsRegistered())
{
uint32_t errorCode = static_cast<uint32_t>(userResult.ExtendedErrorCode());
Logger::error(L"Per-user removal failed: 0x{:08X} - {}", errorCode, userResult.ErrorText());
}
}
}
}
catch (const std::exception& ex)
{
std::string errorMsg = "Failed to uninstall PackageIdentity MSIX: " + std::string(ex.what());
Logger::error(errorMsg);
// Don't fail the entire uninstallation if PackageIdentity fails
Logger::warn(L"Continuing uninstallation despite PackageIdentity MSIX error");
}
LExit:
ReleaseStr(installScope);
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
UINT __stdcall RemoveWindowsServiceByName(std::wstring serviceName) UINT __stdcall RemoveWindowsServiceByName(std::wstring serviceName)
{ {
SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT); SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

View File

@@ -33,3 +33,5 @@ EXPORTS
CleanPowerRenameRuntimeRegistryCA CleanPowerRenameRuntimeRegistryCA
CleanNewPlusRuntimeRegistryCA CleanNewPlusRuntimeRegistryCA
SetBundleInstallLocationCA SetBundleInstallLocationCA
InstallPackageIdentityMSIXCA
UninstallPackageIdentityMSIXCA

View File

@@ -112,6 +112,7 @@
<Custom Action="SetUninstallCommandNotFoundParam" Before="UninstallCommandNotFound" /> <Custom Action="SetUninstallCommandNotFoundParam" Before="UninstallCommandNotFound" />
<Custom Action="SetUpgradeCommandNotFoundParam" Before="UpgradeCommandNotFound" /> <Custom Action="SetUpgradeCommandNotFoundParam" Before="UpgradeCommandNotFound" />
<Custom Action="SetApplyModulesRegistryChangeSetsParam" Before="ApplyModulesRegistryChangeSets" /> <Custom Action="SetApplyModulesRegistryChangeSetsParam" Before="ApplyModulesRegistryChangeSets" />
<Custom Action="SetInstallPackageIdentityMSIXParam" Before="InstallPackageIdentityMSIX" />
<?if $(var.PerUser) = "true" ?> <?if $(var.PerUser) = "true" ?>
<Custom Action="SetInstallDSCModuleParam" Before="InstallDSCModule" /> <Custom Action="SetInstallDSCModuleParam" Before="InstallDSCModule" />
@@ -123,6 +124,7 @@
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" /> <Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" /> <Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" /> <Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" /> <Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" /> <Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
<!-- TODO: Use to activate embedded MSIX --> <!-- TODO: Use to activate embedded MSIX -->
@@ -144,6 +146,7 @@
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" /> <Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" /> <Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" /> <Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" /> <Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<!-- TODO: Use to activate embedded MSIX --> <!-- TODO: Use to activate embedded MSIX -->
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize"> <!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
@@ -199,6 +202,12 @@
<CustomAction Id="UninstallEmbeddedMSIXTask" Return="ignore" Impersonate="yes" DllEntry="UninstallEmbeddedMSIXCA" BinaryRef="PTCustomActions" /> <CustomAction Id="UninstallEmbeddedMSIXTask" Return="ignore" Impersonate="yes" DllEntry="UninstallEmbeddedMSIXCA" BinaryRef="PTCustomActions" />
<CustomAction Id="SetInstallPackageIdentityMSIXParam" Property="InstallPackageIdentityMSIX" Value="[INSTALLFOLDER];[InstallScope]" />
<CustomAction Id="InstallPackageIdentityMSIX" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallPackageIdentityMSIXCA" BinaryRef="PTCustomActions" />
<CustomAction Id="UninstallPackageIdentityMSIX" Return="ignore" Impersonate="yes" DllEntry="UninstallPackageIdentityMSIXCA" BinaryRef="PTCustomActions" />
<CustomAction Id="InstallDSCModule" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallDSCModuleCA" BinaryRef="PTCustomActions" /> <CustomAction Id="InstallDSCModule" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallDSCModuleCA" BinaryRef="PTCustomActions" />
<CustomAction Id="UninstallDSCModule" Return="ignore" Impersonate="yes" DllEntry="UninstallDSCModuleCA" BinaryRef="PTCustomActions" /> <CustomAction Id="UninstallDSCModule" Return="ignore" Impersonate="yes" DllEntry="UninstallDSCModuleCA" BinaryRef="PTCustomActions" />

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Sparse package manifest (moved to PackageIdentity folder for cleaner organization).
Based on Windows AI Foundry WPF sparse sample with PowerOCR customizations. -->
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
<Identity
Name="Microsoft.PowerToys.SparseApp"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
Version="0.0.1.0" />
<Properties>
<DisplayName>PowerToys.SparseApp</DisplayName>
<PublisherDisplayName>PowerToys</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
<uap10:AllowExternalContent>true</uap10:AllowExternalContent>
</Properties>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.26226.0" />
</Dependencies>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<systemai:Capability Name="systemAIModels"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities>
<Applications>
<Application Id="PowerToys.OCR" Executable="PowerToys.PowerOCR.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
<uap:VisualElements
DisplayName="PowerToys.OCR"
Description="PowerToys OCR Module"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.SettingsUI" Executable="PowerToys.Settings.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
<uap:VisualElements
DisplayName="PowerToys.SettingsUI"
Description="PowerToys Settings UI"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
<uap:VisualElements
DisplayName="PowerToys.ImageResizer"
Description="PowerToys Image Resizer UI"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
</uap:VisualElements>
</Application>
</Applications>
</Package>

View File

@@ -0,0 +1,6 @@
@echo off
REM Wrapper to invoke PowerToys sparse package build script.
REM Pass through all arguments (e.g. Platform=arm64 Configuration=Debug -Clean)
powershell -ExecutionPolicy Bypass -NoLogo -NoProfile -File "%~dp0\BuildSparsePackage.ps1" %*
exit /b %ERRORLEVEL%

View File

@@ -0,0 +1,422 @@
#Requires -Version 5.1
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[ValidateSet("arm64", "x64")]
[string]$Platform = "x64",
[Parameter(Mandatory=$false)]
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Release",
[switch]$Clean,
[switch]$ForceCert,
[switch]$NoSign,
[switch]$CIBuild
)
# PowerToys sparse packaging helper.
# Generates a sparse MSIX (no payload) that grants package identity to selected Win32 components.
# Multiple applications (PowerOCR, Settings UI, etc.) can share this single sparse identity.
$ErrorActionPreference = 'Stop'
$isCIBuild = $false
if ($CIBuild.IsPresent) {
$isCIBuild = $true
} elseif ($env:CIBuild) {
$isCIBuild = $env:CIBuild -ieq 'true'
}
$currentPublisherHint = $script:Config.CertSubject
# Configuration constants - centralized management
$script:Config = @{
IdentityName = "Microsoft.PowerToys.SparseApp"
SparseMsixName = "PowerToysSparse.msix"
CertPrefix = "PowerToysSparse"
CertSubject = 'CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US'
CertValidMonths = 12
}
#region Helper Functions
function Find-WindowsSDKTool {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ToolName,
[Parameter(Mandatory=$false)]
[string]$Architecture = "x64"
)
# Simple fallback: check common Windows SDK locations
$commonPaths = @(
"${env:ProgramFiles}\Windows Kits\10\bin\*\$Architecture\$ToolName",
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\$Architecture\$ToolName",
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x86\$ToolName" # SignTool fallback
)
foreach ($pattern in $commonPaths) {
$found = Get-ChildItem $pattern -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($found) {
Write-BuildLog "Found $ToolName at: $($found.FullName)" -Level Info
return $found.FullName
}
}
throw "$ToolName not found. Please ensure Windows SDK is installed."
}
function Test-CertificateValidity {
param([string]$ThumbprintFile)
if (-not (Test-Path $ThumbprintFile)) { return $false }
try {
$thumb = (Get-Content $ThumbprintFile -Raw).Trim()
if (-not $thumb) { return $false }
$cert = Get-Item "cert:\CurrentUser\My\$thumb" -ErrorAction Stop
return $cert.HasPrivateKey -and $cert.NotAfter -gt (Get-Date)
} catch {
return $false
}
}
function Write-BuildLog {
param([string]$Message, [string]$Level = "Info")
$colors = @{ Error = "Red"; Warning = "Yellow"; Success = "Green"; Info = "Cyan" }
$color = if ($colors.ContainsKey($Level)) { $colors[$Level] } else { "White" }
Write-Host "[$(Get-Date -f 'HH:mm:ss')] $Message" -ForegroundColor $color
}
function Stop-FileProcesses {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$FilePath
)
# This function is kept for compatibility but simplified since
# the staging directory approach resolves the file lock issues
Write-Verbose "File process check for: $FilePath"
}
#endregion
# Environment diagnostics for troubleshooting
Write-BuildLog "Starting PackageIdentity build process..." -Level Info
Write-BuildLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
try {
$execPolicy = Get-ExecutionPolicy
Write-BuildLog "Execution Policy: $execPolicy" -Level Info
} catch {
Write-BuildLog "Execution Policy: Unable to determine (MSBuild environment)" -Level Info
}
Write-BuildLog "Current User: $env:USERNAME" -Level Info
Write-BuildLog "Build Platform: $Platform, Configuration: $Configuration" -Level Info
# Check for Visual Studio environment
if ($env:VSINSTALLDIR) {
Write-BuildLog "Running in Visual Studio environment: $env:VSINSTALLDIR" -Level Info
}
# Ensure certificate provider is available
try {
# Force load certificate provider for MSBuild environment
if (-not (Get-PSProvider -PSProvider Certificate -ErrorAction SilentlyContinue)) {
Write-BuildLog "Loading certificate provider..." -Level Warning
Import-Module Microsoft.PowerShell.Security -Force
}
if (-not (Test-Path 'Cert:\CurrentUser')) {
Write-BuildLog "Certificate drive not available, attempting to initialize..." -Level Warning
Import-Module PKI -ErrorAction SilentlyContinue
# Try to access the certificate store to force initialization
Get-ChildItem "Cert:\CurrentUser\My" -ErrorAction SilentlyContinue | Out-Null
}
} catch {
Write-BuildLog ("Note: Certificate provider setup may need manual configuration: {0}" -f $_) -Level Warning
}
# Project root folder (now set to current script folder for local builds)
$ProjectRoot = $PSScriptRoot
$UserFolder = Join-Path $ProjectRoot '.user'
if (-not (Test-Path $UserFolder)) { New-Item -ItemType Directory -Path $UserFolder | Out-Null }
# Certificate file paths using configuration
$prefix = $script:Config.CertPrefix
$CertThumbFile, $CertCerFile = @('.thumbprint', '.cer') |
ForEach-Object { Join-Path $UserFolder "$prefix.certificate.sample$_" }
# Clean option: remove bin/obj and uninstall existing sparse package if present
if ($Clean) {
Write-BuildLog "Cleaning build artifacts..." -Level Info
'bin','obj' | ForEach-Object {
$target = Join-Path $ProjectRoot $_
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
}
Write-BuildLog "Attempting to remove existing sparse package (best effort)" -Level Info
try { Get-AppxPackage -Name $script:Config.IdentityName | Remove-AppxPackage } catch {}
}
# Force certificate regeneration if requested
if ($ForceCert -and (Test-Path $UserFolder)) {
Write-BuildLog "ForceCert specified: removing existing certificate artifacts..." -Level Warning
Remove-Item $UserFolder -Recurse -Force
New-Item -ItemType Directory -Path $UserFolder | Out-Null
}
# Ensure dev cert (development only; not for production use) - skip if NoSign specified
$needNewCert = -not $NoSign -and (-not (Test-Path $CertThumbFile) -or $ForceCert -or -not (Test-CertificateValidity -ThumbprintFile $CertThumbFile))
if ($needNewCert) {
Write-BuildLog "Generating development certificate (prefix=$($script:Config.CertPrefix))..." -Level Info
# Clear stale files in the certificate cache
if (Test-Path $UserFolder) {
Get-ChildItem -Path $UserFolder | ForEach-Object {
if ($_.PSIsContainer) {
Remove-Item $_.FullName -Recurse -Force
} else {
Remove-Item $_.FullName -Force
}
}
}
if (-not (Test-Path $UserFolder)) {
New-Item -ItemType Directory -Path $UserFolder | Out-Null
}
$now = Get-Date
$expiration = $now.AddMonths($script:Config.CertValidMonths)
# Subject MUST match <Identity Publisher="..."> inside AppxManifest.xml
$friendlyName = "PowerToys Dev Sparse Cert Create=$now"
$keyFriendly = "PowerToys Dev Sparse Key Create=$now"
$certStore = 'cert:\CurrentUser\My'
$ekuOid = '2.5.29.37'
$ekuValue = '1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13'
$eku = "$ekuOid={text}$ekuValue"
$cert = New-SelfSignedCertificate -CertStoreLocation $certStore `
-NotAfter $expiration `
-Subject $script:Config.CertSubject `
-FriendlyName $friendlyName `
-KeyFriendlyName $keyFriendly `
-KeyDescription $keyFriendly `
-TextExtension $eku
# Export certificate files
Set-Content -Path $CertThumbFile -Value $cert.Thumbprint -Force
Export-Certificate -Cert $cert -FilePath $CertCerFile -Force | Out-Null
}
# Determine output directory - using PowerToys standard structure
# Navigate to PowerToys root (two levels up from src/PackageIdentity)
$PowerToysRoot = Split-Path (Split-Path $ProjectRoot -Parent) -Parent
$outDir = Join-Path $PowerToysRoot "$Platform\$Configuration"
if (-not (Test-Path $outDir)) {
Write-BuildLog "Creating output directory: $outDir" -Level Info
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# PackageIdentity folder (this script location) containing the sparse manifest and assets
$sparseDir = $PSScriptRoot
$manifestPath = Join-Path $sparseDir 'AppxManifest.xml'
if (-not (Test-Path $manifestPath)) { throw "Missing AppxManifest.xml in PackageIdentity folder: $manifestPath" }
$versionPropsPath = Join-Path $PowerToysRoot 'src\Version.props'
$targetManifestVersion = $null
$versionCandidate = $null
if (Test-Path $versionPropsPath) {
try {
[xml]$propsXml = Get-Content -Path $versionPropsPath -Raw
$versionCandidate = $propsXml.Project.PropertyGroup.Version
} catch {
Write-BuildLog ("Unable to read version from {0}: {1}" -f $versionPropsPath, $_) -Level Warning
}
} else {
Write-BuildLog "Version.props not found at $versionPropsPath; manifest version will remain unchanged." -Level Warning
}
if ($versionCandidate) {
$targetManifestVersion = $versionCandidate.Trim()
if (($targetManifestVersion -split '\.').Count -lt 4) {
$targetManifestVersion = "$targetManifestVersion.0"
}
Write-BuildLog "Using sparse package version from Version.props: $targetManifestVersion" -Level Info
} else {
Write-BuildLog "No version value provided; manifest version will remain unchanged." -Level Info
}
# Find MakeAppx.exe from Windows SDK
try {
$hostSdkArchitecture = if ([System.Environment]::Is64BitProcess) { 'x64' } else { 'x86' }
$makeAppxPath = Find-WindowsSDKTool -ToolName "makeappx.exe" -Architecture $hostSdkArchitecture
} catch {
Write-Error "MakeAppx.exe not found. Please ensure Windows SDK is installed."
exit 1
}
# Pack sparse MSIX from PackageIdentity folder
$msixPath = Join-Path $outDir $script:Config.SparseMsixName
# Clean up existing MSIX file
if (Test-Path $msixPath) {
Write-BuildLog "Removing existing MSIX file..." -Level Info
try {
Remove-Item $msixPath -Force -ErrorAction Stop
Write-BuildLog "Successfully removed existing MSIX file" -Level Success
} catch {
Write-BuildLog ("Warning: Could not remove existing MSIX file: {0}" -f $_) -Level Warning
}
}
# Create a clean staging directory to avoid file lock issues
$stagingDir = Join-Path $outDir "staging"
if (Test-Path $stagingDir) {
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
try {
Write-BuildLog "Creating clean staging directory for packaging..." -Level Info
# Copy only essential files to staging directory to avoid file locks
$essentialFiles = @(
"AppxManifest.xml"
"Images\*"
)
foreach ($filePattern in $essentialFiles) {
$sourcePath = Join-Path $sparseDir $filePattern
$relativePath = $filePattern
if ($filePattern.Contains('\')) {
$targetDir = Join-Path $stagingDir (Split-Path $relativePath -Parent)
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
}
if ($filePattern.EndsWith('\*')) {
# Copy directory contents
$sourceDir = $sourcePath.TrimEnd('\*')
$targetDir = Join-Path $stagingDir (Split-Path $relativePath.TrimEnd('\*') -Parent)
if (Test-Path $sourceDir) {
Copy-Item -Path "$sourceDir\*" -Destination $targetDir -Force -ErrorAction SilentlyContinue
}
} else {
# Copy single file
$targetPath = Join-Path $stagingDir $relativePath
if (Test-Path $sourcePath) {
Copy-Item -Path $sourcePath -Destination $targetPath -Force -ErrorAction SilentlyContinue
}
}
}
# Ensure publisher matches the dev certificate for local builds
$manifestStagingPath = Join-Path $stagingDir 'AppxManifest.xml'
$shouldUseDevPublisher = -not $isCIBuild
if (Test-Path $manifestStagingPath) {
try {
[xml]$manifestXml = Get-Content -Path $manifestStagingPath -Raw
$identityNode = $manifestXml.Package.Identity
$manifestChanged = $false
if ($identityNode) {
$currentPublisherHint = $identityNode.Publisher
}
if ($identityNode) {
if ($targetManifestVersion -and $identityNode.Version -ne $targetManifestVersion) {
Write-BuildLog "Updating manifest version to $targetManifestVersion" -Level Info
$identityNode.SetAttribute('Version', $targetManifestVersion)
$manifestChanged = $true
}
if ($shouldUseDevPublisher -and $identityNode.Publisher -ne $script:Config.CertSubject) {
Write-BuildLog "Updating manifest publisher for local build" -Level Warning
$identityNode.SetAttribute('Publisher', $script:Config.CertSubject)
$manifestChanged = $true
}
$currentPublisherHint = $identityNode.Publisher
}
if ($manifestChanged) {
$manifestXml.Save($manifestStagingPath)
}
} catch {
Write-BuildLog ("Unable to adjust manifest metadata: {0}" -f $_) -Level Warning
}
}
Write-BuildLog "Staging directory prepared with essential files only" -Level Success
# Pack MSIX using staging directory
Write-BuildLog "Packing sparse MSIX ($($script:Config.SparseMsixName)) from staging -> $msixPath" -Level Info
& $makeAppxPath pack /d $stagingDir /p $msixPath /nv /o
if ($LASTEXITCODE -eq 0 -and (Test-Path $msixPath)) {
Write-BuildLog "MSIX packaging completed successfully" -Level Success
} else {
Write-BuildLog "MakeAppx failed with exit code $LASTEXITCODE" -Level Error
exit 1
}
} finally {
# Clean up staging directory
if (Test-Path $stagingDir) {
try {
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
Write-BuildLog "Cleaned up staging directory" -Level Info
} catch {
Write-BuildLog ("Warning: Could not clean up staging directory: {0}" -f $_) -Level Warning
}
}
}
# Sign package (skip if NoSign specified for CI scenarios)
if ($NoSign) {
Write-BuildLog "Skipping signing (NoSign specified for CI build)" -Level Warning
} else {
# Use certificate thumbprint for signing (safer, no password)
$certThumbprint = (Get-Content -Path $CertThumbFile -Raw).Trim()
try {
$signToolPath = Find-WindowsSDKTool -ToolName "signtool.exe"
} catch {
Write-Error "SignTool.exe not found. Please ensure Windows SDK is installed."
exit 1
}
Write-BuildLog "Signing sparse MSIX using cert thumbprint $certThumbprint..." -Level Info
& $signToolPath sign /fd SHA256 /sha1 $certThumbprint $msixPath
if ($LASTEXITCODE -ne 0) {
Write-Warning "SignTool failed (exit $LASTEXITCODE). Ensure the certificate is in CurrentUser\\My and try -ForceCert if needed."
exit $LASTEXITCODE
}
}
$publisherHintFile = Join-Path $UserFolder "$($script:Config.CertPrefix).publisher.txt"
try {
Set-Content -Path $publisherHintFile -Value $currentPublisherHint -Force -NoNewline
} catch {
Write-BuildLog ("Unable to write publisher hint: {0}" -f $_) -Level Warning
}
Write-BuildLog "`nPackage created: $msixPath" -Level Success
if ($NoSign) {
Write-BuildLog "UNSIGNED package created for CI build. Sign before deployment." -Level Warning
} else {
Write-BuildLog "Install the dev certificate (once): $CertCerFile" -Level Info
Write-BuildLog "Identity Name: $($script:Config.IdentityName)" -Level Info
}
Write-BuildLog "Register sparse package:" -Level Info
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$outDir`"" -Level Warning
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$outDir`" -ForceApplicationShutdown" -Level Warning

View File

@@ -0,0 +1,43 @@
<#
.SYNOPSIS
Determine whether a given process (by PID) runs with an MSIX/UWP package identity.
.DESCRIPTION
Calls the Windows API GetPackageFullName to check if the target process executes under an MSIX/Sparse App/UWP package identity.
Returns the package full name when identity is present, or "No package identity" otherwise.
.PARAMETER ProcessId
The process ID to inspect.
.EXAMPLE
.\Check-ProcessIdentity.ps1 -pid 12345
#>
param(
[Parameter(Mandatory=$true)]
[int]$ProcessId
)
Add-Type -TypeDefinition @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class P {
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr OpenProcess(uint a, bool b, int p);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool CloseHandle(IntPtr h);
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern int GetPackageFullName(IntPtr h, ref int l, StringBuilder b);
public static string G(int pid) {
IntPtr h = OpenProcess(0x1000, false, pid);
if (h == IntPtr.Zero) return "Failed to open process";
int len = 0;
GetPackageFullName(h, ref len, null);
if (len == 0) { CloseHandle(h); return "No package identity"; }
var sb = new StringBuilder(len);
int r = GetPackageFullName(h, ref len, sb);
CloseHandle(h);
return r == 0 ? sb.ToString() : "Error:" + r;
}
}
'@
$result = [P]::G($ProcessId)
Write-Output $result

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- CI Build Configuration -->
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ForceCIPackaging>true</ForceCIPackaging>
<NoSignCI>true</NoSignCI>
</PropertyGroup>
<!-- Target to generate sparse MSIX package -->
<Target Name="GenerateSparsePackage" BeforeTargets="PrepareForBuild">
<!-- Use NoSign only for CI builds to avoid certificate issues on hosted agents -->
<PropertyGroup>
<NoSignParam Condition="'$(NoSignCI)' == 'true'">-NoSign</NoSignParam>
<NoSignParam Condition="'$(NoSignCI)' != 'true'"></NoSignParam>
<CIBuildParam Condition="'$(CIBuild)' == 'true'">-CIBuild</CIBuildParam>
<CIBuildParam Condition="'$(CIBuild)' != 'true'"></CIBuildParam>
</PropertyGroup>
<Exec Command="powershell -NonInteractive -ExecutionPolicy Bypass -File &quot;$(MSBuildThisFileDirectory)BuildSparsePackage.ps1&quot; -Platform $(Platform) -Configuration $(Configuration) $(NoSignParam) $(CIBuildParam)"
ContinueOnError="false"
WorkingDirectory="$(MSBuildThisFileDirectory)" />
</Target>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}</ProjectGuid>
<RootNamespace>PackageIdentity</RootNamespace>
<ProjectName>PackageIdentity</ProjectName>
<UseFastUpToDateCheck>false</UseFastUpToDateCheck>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Utility</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Utility</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Utility</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Utility</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\Solution.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemGroup>
<None Include="AppxManifest.xml" />
<None Include="BuildSparsePackage.ps1" />
<None Include="BuildSparsePackage.cmd" />
<None Include="Check-ProcessIdentity.ps1" />
</ItemGroup>
<ItemGroup>
<Image Include="Images\Square150x150Logo.png">
<Filter>Images</Filter>
</Image>
<Image Include="Images\Square44x44Logo.png">
<Filter>Images</Filter>
</Image>
<Image Include="Images\StoreLogo.png">
<Filter>Images</Filter>
</Image>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Images">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>png;jpg;jpeg;gif;bmp;ico</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<None Include="AppxManifest.xml" />
<None Include="BuildSparsePackage.ps1" />
<None Include="BuildSparsePackage.cmd" />
</ItemGroup>
<ItemGroup>
<Image Include="Images\Square150x150Logo.png">
<Filter>Images</Filter>
</Image>
<Image Include="Images\Square44x44Logo.png">
<Filter>Images</Filter>
</Image>
<Image Include="Images\StoreLogo.png">
<Filter>Images</Filter>
</Image>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,90 @@
# 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:
```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 `CurrentUser``TrustedPeople` (and `TrustedRoot`, if necessary) so Windows trusts the signature:
```powershell
$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:
```powershell
# 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.

View File

@@ -24,6 +24,14 @@
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon> <ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx"> <EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
@@ -55,4 +63,14 @@
<DependentUpon>Resources.resx</DependentUpon> <DependentUpon>Resources.resx</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<!-- Ensure Resources directory and ImageResizer.png are available for dependent projects -->
<Target Name="CopyResourcesToSharedLocation" AfterTargets="Build">
<ItemGroup>
<ResourceFiles Include="$(MSBuildProjectDirectory)\Resources\ImageResizer.png" />
</ItemGroup>
<MakeDir Directories="$(OutputPath)Resources" Condition="!Exists('$(OutputPath)Resources')" />
<Copy SourceFiles="@(ResourceFiles)" DestinationFolder="$(OutputPath)Resources" SkipUnchangedFiles="true" />
</Target>
</Project> </Project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
packageName="Microsoft.PowerToys.SparseApp"
applicationId="PowerToys.ImageResizerUI" />
</assembly>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
packageName="Microsoft.PowerToys.SparseApp"
applicationId="PowerToys.ImageResizerUI" />
</assembly>