From b1985bc8d1be571fdf70eb104069d0e40d89da30 Mon Sep 17 00:00:00 2001 From: leileizhang Date: Mon, 20 Oct 2025 08:52:49 +0800 Subject: [PATCH] Introduce shared sparse package identity for PowerToys (#42352) ## 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`). ## 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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Co-authored-by: Gordon Lam (SH) --- .github/actions/spell-check/expect.txt | 7 + .pipelines/ESRPSigning_core.json | 4 +- Directory.Packages.props | 2 + PowerToys.sln | 11 + .../CustomAction.cpp | 210 +++++++++ .../CustomAction.def | 2 + installer/PowerToysSetupVNext/Product.wxs | 9 + src/PackageIdentity/AppxManifest.xml | 67 +++ src/PackageIdentity/BuildSparsePackage.cmd | 6 + src/PackageIdentity/BuildSparsePackage.ps1 | 422 ++++++++++++++++++ src/PackageIdentity/Check-ProcessIdentity.ps1 | 43 ++ .../Images/Square150x150Logo.png | Bin 0 -> 68 bytes .../Images/Square44x44Logo.png | Bin 0 -> 68 bytes src/PackageIdentity/Images/StoreLogo.png | Bin 0 -> 68 bytes src/PackageIdentity/PackageIdentity.vcxproj | 120 +++++ .../PackageIdentity.vcxproj.filters | 25 ++ src/PackageIdentity/readme.md | 90 ++++ .../imageresizer/ui/ImageResizerUI.csproj | 18 + .../ui/ImageResizerUI.dev.manifest | 8 + .../ui/ImageResizerUI.prod.manifest | 8 + 20 files changed, 1051 insertions(+), 1 deletion(-) create mode 100644 src/PackageIdentity/AppxManifest.xml create mode 100644 src/PackageIdentity/BuildSparsePackage.cmd create mode 100644 src/PackageIdentity/BuildSparsePackage.ps1 create mode 100644 src/PackageIdentity/Check-ProcessIdentity.ps1 create mode 100644 src/PackageIdentity/Images/Square150x150Logo.png create mode 100644 src/PackageIdentity/Images/Square44x44Logo.png create mode 100644 src/PackageIdentity/Images/StoreLogo.png create mode 100644 src/PackageIdentity/PackageIdentity.vcxproj create mode 100644 src/PackageIdentity/PackageIdentity.vcxproj.filters create mode 100644 src/PackageIdentity/readme.md create mode 100644 src/modules/imageresizer/ui/ImageResizerUI.dev.manifest create mode 100644 src/modules/imageresizer/ui/ImageResizerUI.prod.manifest diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index a96b86ef34..9115249393 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -193,6 +193,7 @@ changecursor CHILDACTIVATE CHILDWINDOW CHOOSEFONT +CIBUILD cidl CIELCh cim @@ -383,6 +384,7 @@ DISPLAYFREQUENCY displayname DISPLAYORIENTATION divyan +djwsxzxb Dlg DLGFRAME DLGMODALFRAME @@ -443,6 +445,7 @@ EDITSHORTCUTS EDITTEXT EFile ekus +eku emojis ENABLEDELAYEDEXPANSION ENABLEDPOPUP @@ -816,6 +819,7 @@ keyvault KILLFOCUS killrunner kmph +ksa kvp Kybd LARGEICON @@ -922,6 +926,7 @@ LWA lwin LZero MAGTRANSFORM +makeappx MAKEINTRESOURCE MAKEINTRESOURCEA MAKEINTRESOURCEW @@ -1254,6 +1259,7 @@ pinvoke pipename PKBDLLHOOKSTRUCT pkgfamily +PKI plib ploc ploca @@ -1695,6 +1701,7 @@ syskeydown SYSKEYUP SYSLIB SYSMENU +systemai SYSTEMAPPS SYSTEMMODAL SYSTEMTIME diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 2a1760d94e..d99fabf0eb 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -235,7 +235,9 @@ "*Microsoft.CmdPal.UI_*.msix", "PowerToys.DSC.dll", - "PowerToys.DSC.exe" + "PowerToys.DSC.exe", + + "PowerToysSparse.msix" ], "SigningInfo": { "Operations": [ diff --git a/Directory.Packages.props b/Directory.Packages.props index 3fe8abe8fb..129abadb00 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,6 +61,8 @@ + + diff --git a/PowerToys.sln b/PowerToys.sln index f8f26cde91..f4f2e1bd0f 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -26,6 +26,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} = {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} {D940E07F-532C-4FF3-883F-790DA014F19A} = {D940E07F-532C-4FF3-883F-790DA014F19A} {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} {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} EndProjectSection @@ -50,6 +51,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{1AFB64 EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common.Lib.UnitTests", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}" 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}" EndProject 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|x64.ActiveCfg = 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.Build.0 = Debug|ARM64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp index 308b304591..0cfc3b1765 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp @@ -594,6 +594,216 @@ LExit: 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(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(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(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(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(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(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(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(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) { SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT); diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def index 931a555953..4bad107f16 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def @@ -33,3 +33,5 @@ EXPORTS CleanPowerRenameRuntimeRegistryCA CleanNewPlusRuntimeRegistryCA SetBundleInstallLocationCA + InstallPackageIdentityMSIXCA + UninstallPackageIdentityMSIXCA diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs index 2505557d77..556fddc7f4 100644 --- a/installer/PowerToysSetupVNext/Product.wxs +++ b/installer/PowerToysSetupVNext/Product.wxs @@ -112,6 +112,7 @@ + @@ -123,6 +124,7 @@ + @@ -144,6 +146,7 @@ + + + + + + PowerToys.SparseApp + PowerToys + Images\StoreLogo.png + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/PackageIdentity/BuildSparsePackage.cmd b/src/PackageIdentity/BuildSparsePackage.cmd new file mode 100644 index 0000000000..71a4a6a77c --- /dev/null +++ b/src/PackageIdentity/BuildSparsePackage.cmd @@ -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% diff --git a/src/PackageIdentity/BuildSparsePackage.ps1 b/src/PackageIdentity/BuildSparsePackage.ps1 new file mode 100644 index 0000000000..1e341c24f5 --- /dev/null +++ b/src/PackageIdentity/BuildSparsePackage.ps1 @@ -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 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 diff --git a/src/PackageIdentity/Check-ProcessIdentity.ps1 b/src/PackageIdentity/Check-ProcessIdentity.ps1 new file mode 100644 index 0000000000..767afe542f --- /dev/null +++ b/src/PackageIdentity/Check-ProcessIdentity.ps1 @@ -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 diff --git a/src/PackageIdentity/Images/Square150x150Logo.png b/src/PackageIdentity/Images/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..01a45755d730717d72605170c1faf3def396b403 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$f8zNWnCcn7)g4Zc Q017jBy85}Sb4q9e0Ein7fdBvi literal 0 HcmV?d00001 diff --git a/src/PackageIdentity/Images/Square44x44Logo.png b/src/PackageIdentity/Images/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..01a45755d730717d72605170c1faf3def396b403 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$f8zNWnCcn7)g4Zc Q017jBy85}Sb4q9e0Ein7fdBvi literal 0 HcmV?d00001 diff --git a/src/PackageIdentity/Images/StoreLogo.png b/src/PackageIdentity/Images/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..01a45755d730717d72605170c1faf3def396b403 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$f8zNWnCcn7)g4Zc Q017jBy85}Sb4q9e0Ein7fdBvi literal 0 HcmV?d00001 diff --git a/src/PackageIdentity/PackageIdentity.vcxproj b/src/PackageIdentity/PackageIdentity.vcxproj new file mode 100644 index 0000000000..b8bd2dc1f2 --- /dev/null +++ b/src/PackageIdentity/PackageIdentity.vcxproj @@ -0,0 +1,120 @@ + + + + + + true + true + + + + + + + -NoSign + + -CIBuild + + + + + + + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + + 15.0 + Win32Proj + {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D} + PackageIdentity + PackageIdentity + false + + + + + + Utility + true + v143 + + + + Utility + false + v143 + true + + + + Utility + true + v143 + + + + Utility + false + v143 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + Images + + + Images + + + Images + + + + + + + + + \ No newline at end of file diff --git a/src/PackageIdentity/PackageIdentity.vcxproj.filters b/src/PackageIdentity/PackageIdentity.vcxproj.filters new file mode 100644 index 0000000000..608c80f2b9 --- /dev/null +++ b/src/PackageIdentity/PackageIdentity.vcxproj.filters @@ -0,0 +1,25 @@ + + + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + png;jpg;jpeg;gif;bmp;ico + + + + + + + + + + Images + + + Images + + + Images + + + \ No newline at end of file diff --git a/src/PackageIdentity/readme.md b/src/PackageIdentity/readme.md new file mode 100644 index 0000000000..2af2bbb26d --- /dev/null +++ b/src/PackageIdentity/readme.md @@ -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}\\`. + +> 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 `` 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 `` element with `packageName="Microsoft.PowerToys.SparseApp"`, `applicationId` matching the ``, and a `publisher` that matches the sparse package. Keep the manifest’s 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_!PowerToys.MyModuleUI`) or activate it via `IApplicationActivationManager::ActivateApplication` using the same AppUserModelID. + + - For locally built packages, resolve the `` 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. diff --git a/src/modules/imageresizer/ui/ImageResizerUI.csproj b/src/modules/imageresizer/ui/ImageResizerUI.csproj index aca9f9d81e..b146db8435 100644 --- a/src/modules/imageresizer/ui/ImageResizerUI.csproj +++ b/src/modules/imageresizer/ui/ImageResizerUI.csproj @@ -24,6 +24,14 @@ Resources\ImageResizer.ico + + ImageResizerUI.dev.manifest + + + + ImageResizerUI.prod.manifest + + PublicResXFileCodeGenerator @@ -55,4 +63,14 @@ Resources.resx + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/imageresizer/ui/ImageResizerUI.dev.manifest b/src/modules/imageresizer/ui/ImageResizerUI.dev.manifest new file mode 100644 index 0000000000..cb91bc2b66 --- /dev/null +++ b/src/modules/imageresizer/ui/ImageResizerUI.dev.manifest @@ -0,0 +1,8 @@ + + + + + diff --git a/src/modules/imageresizer/ui/ImageResizerUI.prod.manifest b/src/modules/imageresizer/ui/ImageResizerUI.prod.manifest new file mode 100644 index 0000000000..bbb50a9ec5 --- /dev/null +++ b/src/modules/imageresizer/ui/ImageResizerUI.prod.manifest @@ -0,0 +1,8 @@ + + + + +