diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1 index 65497aeeaa..257f70e6b6 100644 --- a/tools/build/build-installer.ps1 +++ b/tools/build/build-installer.ps1 @@ -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 ] [-Configuration ] [-PerUser ] [-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' diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1 index f92146f730..bc7c758f70 100644 --- a/tools/build/cert-management.ps1 +++ b/tools/build/cert-management.ps1 @@ -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 }