diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index daa6449a66..52f203a9de 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -198,6 +198,7 @@ CLIPBOARDUPDATE CLIPCHILDREN CLIPSIBLINGS closesocket +clp CLSCTX clsids Clusion @@ -1045,6 +1046,7 @@ NOINHERITLAYOUT NOINTERFACE NOINVERT NOLINKINFO +nologo NOMCX NOMINMAX NOMIRRORBITMAP @@ -1277,6 +1279,7 @@ pstm PStr pstream pstrm +pswd PSYSTEM psz ptb @@ -1423,6 +1426,7 @@ searchterm SEARCHUI SECONDARYDISPLAY secpol +securestring SEEMASKINVOKEIDLIST SELCHANGE SENDCHANGE diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 37256bdd68..f15b8a4714 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -79,10 +79,7 @@ - - - - + diff --git a/src/common/utils/package.h b/src/common/utils/package.h index 60bde7ea53..138f3b8e5b 100644 --- a/src/common/utils/package.h +++ b/src/common/utils/package.h @@ -301,6 +301,7 @@ namespace package if (!std::filesystem::exists(directoryPath)) { Logger::error(L"The directory '" + directoryPath + L"' does not exist."); + return {}; } const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase); diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj index 4395e340fa..5bd12f316e 100644 --- a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj +++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj @@ -2,6 +2,7 @@ + 17.0 Win32Proj @@ -49,13 +50,21 @@ - EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + + EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL; + %(PreprocessorDefinitions); + $(CommandPaletteBranding) + + + IS_DEV_BRANDING;%(PreprocessorDefinitions) + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) $(OutDir)$(TargetName)$(TargetExt) + diff --git a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp index 134249f049..be3eb6a3b7 100644 --- a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp +++ b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp @@ -208,7 +208,7 @@ public: try { std::wstring packageName = L"Microsoft.CommandPalette"; -#ifdef _DEBUG +#ifdef IS_DEV_BRANDING packageName = L"Microsoft.CommandPalette.Dev"; #endif if (!package::GetRegisteredPackage(packageName, false).has_value()) @@ -245,12 +245,21 @@ public: errorMessage += e.what(); Logger::error(errorMessage); } - -#if _DEBUG - LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false); + try + { +#ifdef IS_DEV_BRANDING + LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false); #else - LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false); + LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false); #endif + } + catch (std::exception& e) + { + std::string errorMessage{ "Exception thrown while trying to launch CmdPal: " }; + errorMessage += e.what(); + Logger::error(errorMessage); + throw; + } } virtual void disable() diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1 new file mode 100644 index 0000000000..28e6939760 --- /dev/null +++ b/tools/build/build-installer.ps1 @@ -0,0 +1,122 @@ +<# +.SYNOPSIS +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, StylesReportTool, etc.) +- Cleaning up old output +- Signing generated .msix packages +- Building the WiX-based MSI and bootstrapper installers + +It is designed to work in local development. + +.PARAMETER Platform +Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'. + +.PARAMETER Configuration +Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'. + +.EXAMPLE +.\build-installer.ps1 +Runs the installer build pipeline for ARM64 Release (default). + +.EXAMPLE +.\build-installer.ps1 -Platform x64 -Configuration Release +Runs the pipeline for x64 Debug. + +.NOTES +- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment. +- 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. +- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup + relative to the solution root directory. +- The installer can't be run right after the build, I need to copy it to another file before it can be run. +#> + + +param ( + [string]$Platform = 'arm64', + [string]$Configuration = 'Release' +) + +$repoRoot = Resolve-Path "$PSScriptRoot\..\.." +Set-Location $repoRoot + +function RunMSBuild { + param ( + [string]$Solution, + [string]$ExtraArgs + ) + + $base = @( + $Solution + "/p:Platform=`"$Platform`"" + "/p:Configuration=$Configuration" + '/verbosity:normal' + '/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly' + '/nologo' + ) + + $cmd = $base + ($ExtraArgs -split ' ') + Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' ')) + & msbuild.exe @cmd + + if ($LASTEXITCODE -ne 0) { + Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs) + exit $LASTEXITCODE + } + +} + +function RestoreThenBuild { + param ([string]$Solution) + + # 1) restore + RunMSBuild $Solution '/t:restore /p:RestorePackagesConfig=true' + # 2) build ------------------------------------------------- + RunMSBuild $Solution '/m' +} + +Write-Host ("Make sure wix is installed and available") +& "$PSScriptRoot\ensure-wix.ps1" + +Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration) +Write-Host '' + +$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal" + +if (Test-Path $cmdpalOutputPath) { + Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath" + Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore +} + +RestoreThenBuild '.\PowerToys.sln' + +$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 '; ')) + & "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles +} +else { + Write-Warning "[SIGN] No .msix files found in $msixSearchRoot" +} + +RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln' +RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln' + +Write-Host '[CLEAN] installer (keep *.exe)' +git clean -xfd -e '*.exe' -- .\installer\ | Out-Null + +RunMSBuild '.\installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true' + +RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysInstaller /p:PerUser=true' + +RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysBootstrapper /p:PerUser=true' + +Write-Host '[PIPELINE] Completed' \ No newline at end of file diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1 new file mode 100644 index 0000000000..a085a5ca54 --- /dev/null +++ b/tools/build/cert-management.ps1 @@ -0,0 +1,159 @@ +<# +.SYNOPSIS +Ensures a code signing certificate exists and is trusted in all necessary certificate stores. + +.DESCRIPTION +This script provides two functions: + +1. EnsureCertificate: + - Searches for an existing code signing certificate by subject name. + - If not found, creates a new self-signed certificate. + - Exports the certificate and attempts to import it into: + - CurrentUser\TrustedPeople + - CurrentUser\Root + - LocalMachine\Root (admin privileges may be required) + +2. ImportAndVerifyCertificate: + - Imports a `.cer` file into the specified certificate store if not already present. + - Verifies the certificate is successfully imported by checking thumbprint. + +This is useful in build or signing pipelines to ensure a valid and trusted certificate is available before signing MSIX or executable files. + +.PARAMETER certSubject +The subject name of the certificate to search for or create. Default is: +"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" + +.PARAMETER cerPath +(ImportAndVerifyCertificate only) The file path to a `.cer` certificate file to import. + +.PARAMETER storePath +(ImportAndVerifyCertificate only) The destination certificate store path (e.g. Cert:\CurrentUser\Root). + +.EXAMPLE +$cert = EnsureCertificate + +Ensures the default certificate exists and is trusted, and returns the certificate object. + +.EXAMPLE +ImportAndVerifyCertificate -cerPath "$env:TEMP\temp_cert.cer" -storePath "Cert:\CurrentUser\Root" + +Imports a certificate into the CurrentUser Root store and verifies its presence. + +.NOTES +- For full trust, administrative privileges may be needed to import into LocalMachine\Root. +- Certificates are created using RSA and SHA256 and marked as CodeSigningCert. +#> + +function ImportAndVerifyCertificate { + param ( + [string]$cerPath, + [string]$storePath + ) + + $thumbprint = (Get-PfxCertificate -FilePath $cerPath).Thumbprint + + $existingCert = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint } + if ($existingCert) { + Write-Host "Certificate already exists in $storePath" + return $true + } + + try { + $null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop + } catch { + Write-Warning "Failed to import certificate to $storePath : $_" + return $false + } + + $imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint } + if ($imported) { + Write-Host "Certificate successfully imported to $storePath" + return $true + } else { + Write-Warning "Certificate not found in $storePath after import" + return $false + } +} + +function EnsureCertificate { + param ( + [string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" + ) + + $cert = Get-ChildItem -Path Cert:\CurrentUser\My | + Where-Object { $_.Subject -eq $certSubject } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + + if (-not $cert) { + Write-Host "Certificate not found. Creating a new one..." + + $cert = New-SelfSignedCertificate -Subject $certSubject ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyAlgorithm RSA ` + -Type CodeSigningCert ` + -HashAlgorithm SHA256 + + if (-not $cert) { + Write-Error "Failed to create a new certificate." + return $null + } + + Write-Host "New certificate created with thumbprint: $($cert.Thumbprint)" + } + else { + Write-Host "Using existing certificate with thumbprint: $($cert.Thumbprint)" + } + + $cerPath = "$env:TEMP\temp_cert.cer" + [void](Export-Certificate -Cert $cert -FilePath $cerPath -Force) + + if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\TrustedPeople")) { return $null } + if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\Root")) { return $null } + if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\LocalMachine\Root")) { + Write-Warning "Failed to import to LocalMachine\Root (admin may be required)" + return $null + } + + return $cert +} + +function Export-CertificateFiles { + param ( + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, + [string]$CerPath, + [string]$PfxPath, + [securestring]$PfxPassword + ) + + if (-not $Certificate) { + Write-Error "No certificate provided to export." + return + } + + if ($CerPath) { + try { + Export-Certificate -Cert $Certificate -FilePath $CerPath -Force | Out-Null + Write-Host "Exported CER to: $CerPath" + } catch { + Write-Warning "Failed to export CER file: $_" + } + } + + if ($PfxPath -and $PfxPassword) { + try { + Export-PfxCertificate -Cert $Certificate -FilePath $PfxPath -Password $PfxPassword -Force | Out-Null + Write-Host "Exported PFX to: $PfxPath" + } catch { + Write-Warning "Failed to export PFX file: $_" + } + } + + if (-not $CerPath -and -not $PfxPath) { + Write-Warning "No output path specified. Nothing was exported." + } +} + +$cert = EnsureCertificate +$pswd = ConvertTo-SecureString -String "MySecurePassword123!" -AsPlainText -Force +Export-CertificateFiles -Certificate $cert -CerPath "$env:TEMP\cert.cer" -PfxPath "$env:TEMP\cert.pfx" -PfxPassword $pswd diff --git a/tools/build/cert-sign-package.ps1 b/tools/build/cert-sign-package.ps1 new file mode 100644 index 0000000000..8bb57762a5 --- /dev/null +++ b/tools/build/cert-sign-package.ps1 @@ -0,0 +1,29 @@ +param ( + [string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + [string[]]$TargetPaths = "C:\PowerToys\ARM64\Release\WinUI3Apps\CmdPal\AppPackages\Microsoft.CmdPal.UI_0.0.1.0_Test\Microsoft.CmdPal.UI_0.0.1.0_arm64.msix" +) + +. "$PSScriptRoot\cert-management.ps1" +$cert = EnsureCertificate -certSubject $certSubject + +if (-not $cert) { + Write-Error "Failed to prepare certificate." + exit 1 +} + +Write-Host "Certificate ready: $($cert.Thumbprint)" + +if (-not $TargetPaths -or $TargetPaths.Count -eq 0) { + Write-Error "No target files provided to sign." + exit 1 +} + +foreach ($filePath in $TargetPaths) { + if (-not (Test-Path $filePath)) { + Write-Warning "Skipping: File does not exist - $filePath" + continue + } + + Write-Host "Signing: $filePath" + & signtool sign /sha1 $($cert.Thumbprint) /fd SHA256 /t http://timestamp.digicert.com "$filePath" +} \ No newline at end of file diff --git a/tools/build/ensure-wix.ps1 b/tools/build/ensure-wix.ps1 new file mode 100644 index 0000000000..988d382f07 --- /dev/null +++ b/tools/build/ensure-wix.ps1 @@ -0,0 +1,71 @@ +<# +.SYNOPSIS + Ensure WiX Toolset 3.14 (build 3141) is installed and ready to use. + +.DESCRIPTION + - Skips installation if the toolset is already installed (unless -Force is used). + - Otherwise downloads the official installer and binaries, verifies SHA-256, installs silently, + and copies wix.targets into the installation directory. +.PARAMETER Force + Forces reinstallation even if the toolset is already detected. +.PARAMETER InstallDir + The target installation path. Default is 'C:\Program Files (x86)\WiX Toolset v3.14'. +.EXAMPLE + .\EnsureWix.ps1 # Ensure WiX is installed + .\EnsureWix.ps1 -Force # Force reinstall +#> +[CmdletBinding()] +param( + [switch]$Force, + [string]$InstallDir = 'C:\Program Files (x86)\WiX Toolset v3.14' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +# Download URLs and expected SHA-256 hashes +$WixDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe' +$WixBinariesDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip' +$InstallerHashExpected = '6BF6D03D6923D9EF827AE1D943B90B42B8EBB1B0F68EF6D55F868FA34C738A29' +$BinariesHashExpected = '6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31' + +# Check if WiX is already installed +$candlePath = Join-Path $InstallDir 'bin\candle.exe' +if (-not $Force -and (Test-Path $candlePath)) { + Write-Host "WiX Toolset is already installed at `"$InstallDir`". Skipping installation." + return +} + +# Temp file paths +$tmpDir = [IO.Path]::GetTempPath() +$installer = Join-Path $tmpDir 'wix314.exe' +$binariesZip = Join-Path $tmpDir 'wix314-binaries.zip' + +# Download installer and binaries +Write-Host 'Downloading WiX installer...' +Invoke-WebRequest -Uri $WixDownloadUrl -OutFile $installer -UseBasicParsing +Write-Host 'Downloading WiX binaries...' +Invoke-WebRequest -Uri $WixBinariesDownloadUrl -OutFile $binariesZip -UseBasicParsing + +# Verify SHA-256 hashes +Write-Host 'Verifying installer hash...' +if ((Get-FileHash -Algorithm SHA256 $installer).Hash -ne $InstallerHashExpected) { + throw 'wix314.exe SHA256 hash mismatch' +} +Write-Host 'Verifying binaries hash...' +if ((Get-FileHash -Algorithm SHA256 $binariesZip).Hash -ne $BinariesHashExpected) { + throw 'wix314-binaries.zip SHA256 hash mismatch' +} + +# Perform silent installation +Write-Host 'Installing WiX Toolset silently...' +Start-Process -FilePath $installer -ArgumentList '/install','/quiet' -Wait + +# Extract binaries and copy wix.targets +$expandDir = Join-Path $tmpDir 'wix-binaries' +if (Test-Path $expandDir) { Remove-Item $expandDir -Recurse -Force } +Expand-Archive -Path $binariesZip -DestinationPath $expandDir -Force +Copy-Item -Path (Join-Path $expandDir 'wix.targets') ` + -Destination (Join-Path $InstallDir 'wix.targets') -Force + +Write-Host "WiX Toolset has been successfully installed at: $InstallDir"