diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1 index 927622aab3..76ae4c92ef 100644 --- a/tools/build/build-installer.ps1 +++ b/tools/build/build-installer.ps1 @@ -27,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. @@ -129,188 +131,290 @@ if ($Clean) { RunMSBuild 'PowerToys.slnx' '/t:Clean' $Platform $Configuration } -# Set up versioning using versionSetting.ps1 if Version is provided -# These files will be modified by versionSetting.ps1 and need to be restored after build -$versionFilesToRestore = @( - "src\Version.props", - "src\CmdPalVersion.props", - "src\modules\powerrename\PowerRenameContextMenu\AppxManifest.xml", - "src\modules\imageresizer\ImageResizerContextMenu\AppxManifest.xml", - "src\modules\FileLocksmith\FileLocksmithContextMenu\AppxManifest.xml", - "src\modules\NewPlus\NewShellExtensionContextMenu\AppxManifest.xml" -) -$versionFileBackups = @{} +# Git Stash Logic to handle workspace cleanup +$stashedChanges = $false +$scriptPathRelative = "tools/build/build-installer.ps1" -# These files are generated by Terminal versioning and need to be cleaned up after build -$cmdpalGeneratedFiles = @( - "src\modules\cmdpal\Directory.Build.props", - "src\modules\cmdpal\Directory.Build.targets", - "src\modules\cmdpal\Microsoft.CmdPal.UI\BuildInfo.xml", - "src\modules\cmdpal\Microsoft.CmdPal.UI\GeneratedPackage.appxmanifest", - "src\modules\cmdpal\ext\ProcessMonitorExtension\BuildInfo.xml", - "src\modules\cmdpal\ext\ProcessMonitorExtension\GeneratedPackage.appxmanifest", - "src\modules\cmdpal\ext\SamplePagesExtension\BuildInfo.xml", - "src\modules\cmdpal\ext\SamplePagesExtension\GeneratedPackage.appxmanifest" -) - -# Backup all version files before any versioning scripts run -Write-Host "[VERSION] Backing up version files before modification..." -foreach ($relPath in $versionFilesToRestore) { - $fullPath = Join-Path $repoRoot $relPath - if (Test-Path $fullPath) { - $versionFileBackups[$relPath] = Get-Content $fullPath -Raw - Write-Host " Backed up: $relPath" - } +# 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 '\\', '/' } -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" +Push-Location $repoRoot +try { + if (git status --porcelain) { + 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 +} + +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 } + } + + Write-Host "[VERSION] Setting up versioning using Microsoft.Windows.Terminal.Versioning..." + + # 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 versionSetting.ps1 at: $versionScript" - exit 1 + Write-Error "Could not find Setup.ps1 in $versionRoot" } -} -Write-Host "[VERSION] Setting up versioning using Microsoft.Windows.Terminal.Versioning..." + # 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 '' -# 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." + $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 } } - $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 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 + # 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" } - 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 -} + if (-not $SkipBuild) { + RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration + RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration + } -if ($Clean) { - Write-Host '[CLEAN] installer (keep *.exe)' + if ($Clean) { + Write-Host '[CLEAN] installer (keep *.exe)' + Push-Location $repoRoot + try { + git clean -xfd -e '*.exe' -- .\installer\ | Out-Null + } finally { + Pop-Location + } + } + + # 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 { - git clean -xfd -e '*.exe' -- .\installer\ | Out-Null + # 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 } } -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 - -# Restore version files if they were backed up -if ($versionFileBackups.Count -gt 0) { - Write-Host "[VERSION] Restoring version files to original state..." - foreach ($relPath in $versionFileBackups.Keys) { - $fullPath = Join-Path $repoRoot $relPath - Set-Content -Path $fullPath -Value $versionFileBackups[$relPath] -NoNewline - Write-Host " Restored: $relPath" - } -} - -# Clean up cmdpal generated files from Terminal versioning -Write-Host "[CLEANUP] Removing cmdpal generated files..." -foreach ($relPath in $cmdpalGeneratedFiles) { - $fullPath = Join-Path $repoRoot $relPath - if (Test-Path $fullPath) { - Remove-Item $fullPath -Force - Write-Host " Removed: $relPath" - } -} - Write-Host '[PIPELINE] Completed'