Build: Fix build script for a local installer (#44168)

<!-- 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

Not maintained since wix5 upgrade, so make it build locally for an
installer
1. Do elevation when dev cert is not added to root store
2. Set up version to build arg to build, and build cmdpal version same
with CI
3. cmdpal AOT local build
4. Make sure every msix file is signed successfully

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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

Verify the script can build an installer on a new devbox, and cmdpal,
filelocksmith etc can be run without problem
<img width="872" height="275" alt="image"
src="https://github.com/user-attachments/assets/cf4cff0d-0d90-4496-a7f8-50c582d9c340"
/>
<img width="1251" height="555" alt="image"
src="https://github.com/user-attachments/assets/6529c1a8-a532-4dfc-9f74-2c2fd37e28e6"
/>
Output for msix packages:
PackageFullName Version
--------------- -------
Microsoft.PowerToys.SparseApp_0.96.2.0_neutral__8wekyb3d8bbwe 0.96.2.0

Microsoft.PowerToys.FileLocksmithContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0

Microsoft.PowerToys.ImageResizerContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0
Microsoft.PowerToys.NewPlusContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0

Microsoft.PowerToys.PowerRenameContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kai Tao
2025-12-11 13:17:42 +08:00
committed by GitHub
parent d32ea86314
commit f8c5ff8c0c
2 changed files with 328 additions and 57 deletions

View File

@@ -3,14 +3,7 @@
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
.DESCRIPTION
This script automates the end-to-end build and packaging process for PowerToys, including:
- Restoring and building all necessary solutions (CmdPal, BugReportTool, etc.)
- Cleaning up old output
- Signing generated .msix packages
- Building the WiX v5 (VNext) MSI and bootstrapper installers
It is designed to work in local development.
The cert used to sign the packages is generated by
Builds and packages PowerToys (CmdPal and installer) locally. Handles solution build, signing, and WiX installer generation.
.PARAMETER Platform
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'x64'.
@@ -34,10 +27,12 @@ Runs the pipeline for x64 Release.
Runs the pipeline for x64 Release with machine-wide installer.
.NOTES
- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
- Generated MSIX files will be signed using cert-sign-package.ps1.
- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
- First time run need admin permission to trust the certificate.
- This script uses git to manage workspace state:
* Uncommitted changes are stashed before build and popped afterwards.
* Version files and manifests modified during build are reverted.
* Untracked generated files are cleaned up.
- Use the -Clean parameter to clean build outputs (bin/obj) and ignored files.
- The built installer will be placed under: installer/PowerToysSetupVNext/[Platform]/[Configuration]/User[Machine]Setup
relative to the solution root directory.
- To run the full installation in other machines, call "./cert-management.ps1" to export the cert used to sign the packages.
@@ -45,11 +40,29 @@ Runs the pipeline for x64 Release with machine-wide installer.
#>
param (
[string]$Platform = '',
[string]$Platform = 'x64',
[string]$Configuration = 'Release',
[string]$PerUser = 'true'
[string]$PerUser = 'true',
[string]$Version,
[switch]$EnableCmdPalAOT,
[switch]$Clean,
[switch]$SkipBuild,
[switch]$Help
)
if ($Help) {
Write-Host "Usage: .\build-installer.ps1 [-Platform <x64|arm64>] [-Configuration <Release|Debug>] [-PerUser <true|false>] [-Version <0.0.1>] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]"
Write-Host " -Platform Target platform (default: auto-detect or x64)"
Write-Host " -Configuration Build configuration (default: Release)"
Write-Host " -PerUser Build per-user installer (default: true)"
Write-Host " -Version Sets the PowerToys version (default: from src\Version.props)"
Write-Host " -EnableCmdPalAOT Enable AOT compilation for CmdPal (slower build)"
Write-Host " -Clean Clean output directories before building"
Write-Host " -SkipBuild Skip building the main solution and tools (assumes they are already built)"
Write-Host " -Help Show this help message"
exit 0
}
# Ensure helpers are available
. "$PSScriptRoot\build-common.ps1"
@@ -89,62 +102,310 @@ if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx")))
}
Write-Host "PowerToys repository root detected: $repoRoot"
# WiX v5 projects use WixToolset.Sdk via NuGet/MSBuild; no separate WiX installation is required.
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1} PerUser={2}" -f $Platform, $Configuration, $PerUser)
Write-Host ''
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
$buildOutputPath = Join-Path $repoRoot "$Platform\$Configuration"
if (Test-Path $cmdpalOutputPath) {
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
}
$commonArgs = '/p:CIBuild=true'
# No local projects found (or continuing) - build full solution and tools
RestoreThenBuild 'PowerToys.slnx' $commonArgs $Platform $Configuration
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
Select-Object -ExpandProperty FullName
if ($msixFiles.Count) {
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
& (Join-Path $PSScriptRoot "cert-sign-package.ps1") -TargetPaths $msixFiles
}
else {
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
}
# Generate DSC manifest files
Write-Host '[DSC] Generating DSC manifest files...'
$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1'
if (Test-Path $dscScriptPath) {
& $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot
if ($LASTEXITCODE -ne 0) {
Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE"
exit 1
# Clean should be done first before any other steps
if ($Clean) {
if (Test-Path $cmdpalOutputPath) {
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
}
Write-Host '[DSC] DSC manifest files generated successfully'
} else {
Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath"
if (Test-Path $buildOutputPath) {
Write-Host "[CLEAN] Removing previous build output: $buildOutputPath"
Remove-Item $buildOutputPath -Recurse -Force -ErrorAction Ignore
}
Write-Host "[CLEAN] Cleaning all build artifacts (git clean -Xfd)..."
Push-Location $repoRoot
try {
git clean -Xfd | Out-Null
} catch {
Write-Warning "[CLEAN] git clean failed: $_"
} finally {
Pop-Location
}
Write-Host "[CLEAN] Cleaning solution (msbuild /t:Clean)..."
RunMSBuild 'PowerToys.slnx' '/t:Clean' $Platform $Configuration
}
RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration
RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration
# Git Stash Logic to handle workspace cleanup
$stashedChanges = $false
$scriptPathRelative = "tools/build/build-installer.ps1"
# Calculate relative path of this script to exclude it from stash/reset
$currentScriptPath = $MyInvocation.MyCommand.Definition
if ($currentScriptPath.StartsWith($repoRoot)) {
$scriptPathRelative = $currentScriptPath.Substring($repoRoot.Length).TrimStart('\', '/')
$scriptPathRelative = $scriptPathRelative -replace '\\', '/'
}
Write-Host '[CLEAN] installer (keep *.exe)'
Push-Location $repoRoot
try {
git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
$gitStatus = git status --porcelain
if ($gitStatus.Length -gt 0) {
Write-Host "[GIT] Uncommitted changes detected. Stashing (excluding this script)..."
$stashCountBefore = (git stash list).Count
# Exclude the current script from stash so we don't revert it while running
git stash push --include-untracked -m "PowerToys Build Auto-Stash" -- . ":(exclude)$scriptPathRelative"
$stashCountAfter = (git stash list).Count
if ($stashCountAfter -gt $stashCountBefore) {
$stashedChanges = $true
Write-Host "[GIT] Changes stashed."
} else {
Write-Host "[GIT] No changes to stash (likely only this script is modified)."
}
}
} finally {
Pop-Location
}
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
try {
if ($Version) {
Write-Host "[VERSION] Setting PowerToys version to $Version using versionSetting.ps1..."
$versionScript = Join-Path $repoRoot ".pipelines\versionSetting.ps1"
if (Test-Path $versionScript) {
& $versionScript -versionNumber $Version -DevEnvironment 'Local'
if (-not $?) {
Write-Error "versionSetting.ps1 failed"
exit 1
}
} else {
Write-Error "Could not find versionSetting.ps1 at: $versionScript"
exit 1
}
}
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
Write-Host "[VERSION] Setting up versioning using Microsoft.Windows.Terminal.Versioning..."
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
# Check for nuget.exe - download to AppData if not available
$nugetDownloaded = $false
$nugetPath = $null
if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) {
Write-Warning "nuget.exe not found in PATH. Attempting to download..."
$nugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$nugetDir = Join-Path $env:LOCALAPPDATA "PowerToys\BuildTools"
if (-not (Test-Path $nugetDir)) { New-Item -ItemType Directory -Path $nugetDir -Force | Out-Null }
$nugetPath = Join-Path $nugetDir "nuget.exe"
if (-not (Test-Path $nugetPath)) {
try {
Invoke-WebRequest $nugetUrl -OutFile $nugetPath
$nugetDownloaded = $true
} catch {
Write-Error "Failed to download nuget.exe. Please install it manually and add to PATH."
exit 1
}
}
$env:Path += ";$nugetDir"
}
# Install Terminal versioning package to AppData
$versioningDir = Join-Path $env:LOCALAPPDATA "PowerToys\BuildTools\.versioning"
if (-not (Test-Path $versioningDir)) { New-Item -ItemType Directory -Path $versioningDir -Force | Out-Null }
$configFile = Join-Path $repoRoot ".pipelines\release-nuget.config"
# Install the package
# Use -ExcludeVersion to make the path predictable
nuget install Microsoft.Windows.Terminal.Versioning -ConfigFile $configFile -OutputDirectory $versioningDir -ExcludeVersion -NonInteractive
$versionRoot = Join-Path $versioningDir "Microsoft.Windows.Terminal.Versioning"
$setupScript = Join-Path $versionRoot "build\Setup.ps1"
if (Test-Path $setupScript) {
& $setupScript -ProjectDirectory (Join-Path $repoRoot "src\modules\cmdpal") -Verbose
} else {
Write-Error "Could not find Setup.ps1 in $versionRoot"
}
# WiX v5 projects use WixToolset.Sdk via NuGet/MSBuild; no separate WiX installation is required.
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1} PerUser={2}" -f $Platform, $Configuration, $PerUser)
Write-Host ''
$commonArgs = '/p:CIBuild=true /p:IsPipeline=true'
if ($EnableCmdPalAOT) {
$commonArgs += " /p:EnableCmdPalAOT=true"
}
# No local projects found (or continuing) - build full solution and tools
if (-not $SkipBuild) {
RestoreThenBuild 'PowerToys.slnx' $commonArgs $Platform $Configuration
}
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
Select-Object -ExpandProperty FullName
if ($msixFiles.Count) {
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
& (Join-Path $PSScriptRoot "cert-sign-package.ps1") -TargetPaths $msixFiles
}
else {
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
}
# Generate DSC v2 manifests (PowerToys.Settings.DSC.Schema.Generator)
# The csproj PostBuild event is skipped on ARM64, so we run it manually here if needed.
if ($Platform -eq 'arm64') {
Write-Host "[DSC] Manually generating DSC v2 manifests for ARM64..."
# 1. Get Version
$versionPropsPath = Join-Path $repoRoot "src\Version.props"
[xml]$versionProps = Get-Content $versionPropsPath
$ptVersion = $versionProps.Project.PropertyGroup.Version
# Directory.Build.props appends .0 to the version for .csproj files
$ptVersionFull = "$ptVersion.0"
# 2. Build the Generator
$generatorProj = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\PowerToys.Settings.DSC.Schema.Generator.csproj"
RunMSBuild $generatorProj "/t:Build" $Platform $Configuration
# 3. Define paths
# The generator output path is in the project's bin folder
$generatorExe = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\bin\$Platform\$Configuration\PowerToys.Settings.DSC.Schema.Generator.exe"
if (-not (Test-Path $generatorExe)) {
Write-Warning "Could not find generator at expected path: $generatorExe"
Write-Warning "Searching in build output..."
$found = Get-ChildItem -Path (Join-Path $repoRoot "$Platform\$Configuration") -Filter "PowerToys.Settings.DSC.Schema.Generator.exe" -Recurse | Select-Object -First 1
if ($found) {
$generatorExe = $found.FullName
}
}
$settingsLibDll = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\PowerToys.Settings.UI.Lib.dll"
$dscGenDir = Join-Path $repoRoot "src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$ptVersionFull"
if (-not (Test-Path $dscGenDir)) {
New-Item -ItemType Directory -Path $dscGenDir -Force | Out-Null
}
$outPsm1 = Join-Path $dscGenDir "Microsoft.PowerToys.Configure.psm1"
$outPsd1 = Join-Path $dscGenDir "Microsoft.PowerToys.Configure.psd1"
# 4. Run Generator
if (Test-Path $generatorExe) {
Write-Host "[DSC] Executing: $generatorExe"
$generatorDir = Split-Path -Parent $generatorExe
$winUI3AppsDir = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps"
# Copy dependencies from WinUI3Apps to Generator directory to satisfy WinRT/WinUI3 dependencies
# This avoids "Class not registered" errors without polluting the WinUI3Apps directory which is used for packaging.
if (Test-Path $winUI3AppsDir) {
Write-Host "[DSC] Copying dependencies from $winUI3AppsDir to $generatorDir"
Get-ChildItem -Path $winUI3AppsDir -Filter "*.dll" | ForEach-Object {
$destPath = Join-Path $generatorDir $_.Name
if (-not (Test-Path $destPath)) {
Copy-Item -Path $_.FullName -Destination $destPath -Force
}
}
# Also copy resources.pri if it exists, as it might be needed for resource lookup
$priFile = Join-Path $winUI3AppsDir "resources.pri"
if (Test-Path $priFile) {
Copy-Item -Path $priFile -Destination $generatorDir -Force
}
}
Push-Location $generatorDir
try {
# Now we can use the local DLLs
$localSettingsLibDll = Join-Path $generatorDir "PowerToys.Settings.UI.Lib.dll"
if (Test-Path $localSettingsLibDll) {
Write-Host "[DSC] Using local DLL: $localSettingsLibDll"
& $generatorExe $localSettingsLibDll $outPsm1 $outPsd1
} else {
# Fallback (shouldn't happen if copy succeeded or build was correct)
Write-Warning "[DSC] Local DLL not found, falling back to: $settingsLibDll"
& $generatorExe $settingsLibDll $outPsm1 $outPsd1
}
if ($LASTEXITCODE -ne 0) {
Write-Error "DSC v2 generation failed with exit code $LASTEXITCODE"
exit 1
}
} finally {
Pop-Location
}
Write-Host "[DSC] DSC v2 manifests generated successfully."
} else {
Write-Error "Could not find generator executable at $generatorExe"
exit 1
}
}
# Generate DSC manifest files
Write-Host '[DSC] Generating DSC manifest files...'
$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1'
if (Test-Path $dscScriptPath) {
& $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot
if ($LASTEXITCODE -ne 0) {
Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE"
exit 1
}
Write-Host '[DSC] DSC manifest files generated successfully'
} else {
Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath"
}
if (-not $SkipBuild) {
RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration
RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration
}
# Set NUGET_PACKAGES environment variable if not set, to help wixproj find heat.exe
if (-not $env:NUGET_PACKAGES) {
$env:NUGET_PACKAGES = Join-Path $env:USERPROFILE ".nuget\packages"
Write-Host "[ENV] Set NUGET_PACKAGES to $env:NUGET_PACKAGES"
}
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
# Fix: WiX v5 locally puts the MSI in an 'en-us' subfolder, but the Bootstrapper expects it in the root of UserSetup/MachineSetup.
# We move it up one level to match expectations.
$setupType = if ($PerUser -eq 'true') { 'UserSetup' } else { 'MachineSetup' }
$msiParentDir = Join-Path $repoRoot "installer\PowerToysSetupVNext\$Platform\$Configuration\$setupType"
$msiEnUsDir = Join-Path $msiParentDir "en-us"
if (Test-Path $msiEnUsDir) {
Write-Host "[FIX] Moving MSI files from $msiEnUsDir to $msiParentDir"
Get-ChildItem -Path $msiEnUsDir -Filter *.msi | Move-Item -Destination $msiParentDir -Force
}
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
} finally {
# Restore workspace state using Git
Write-Host "[GIT] Cleaning up build artifacts..."
Push-Location $repoRoot
try {
# Revert all changes EXCEPT the script itself
# This cleans up Version.props, AppxManifests, etc.
git checkout HEAD -- . ":(exclude)$scriptPathRelative"
# Remove untracked files (generated manifests, etc.)
# -f: force, -d: remove directories, -q: quiet
git clean -fd -q
if ($stashedChanges) {
Write-Host "[GIT] Restoring stashed changes..."
git stash pop --index
if ($LASTEXITCODE -ne 0) {
Write-Warning "[GIT] 'git stash pop' reported conflicts or errors. Your changes are in the stash list."
}
}
} finally {
Pop-Location
}
}
Write-Host '[PIPELINE] Completed'

View File

@@ -61,8 +61,18 @@ function ImportAndVerifyCertificate {
try {
$null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
} catch {
Write-Warning "Failed to import certificate to $storePath : $_"
return $false
if ($_.Exception.Message -match "Access is denied" -or $_.Exception.InnerException.Message -match "Access is denied") {
Write-Warning "Access denied to $storePath. Attempting to import with admin privileges..."
try {
Start-Process powershell -ArgumentList "-NoProfile", "-Command", "& { Import-Certificate -FilePath '$cerPath' -CertStoreLocation '$storePath' }" -Verb RunAs -Wait
} catch {
Write-Warning "Failed to request admin privileges: $_"
return $false
}
} else {
Write-Warning "Failed to import certificate to $storePath : $_"
return $false
}
}
$imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }