From b89fc7340d2100a18e0b0bd7358d5681dcf3ea93 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 11 May 2026 21:01:24 +0200 Subject: [PATCH] fix(template): use absolute \ path before PATH refresh (git not yet in session PATH) --- template/Setup-WinBuild2022.ps1 | 595 +++++++++++++++++++++++++++++++- template/Setup-WinBuild2025.ps1 | 2 +- 2 files changed, 586 insertions(+), 11 deletions(-) diff --git a/template/Setup-WinBuild2022.ps1 b/template/Setup-WinBuild2022.ps1 index 328ca64..089b457 100644 --- a/template/Setup-WinBuild2022.ps1 +++ b/template/Setup-WinBuild2022.ps1 @@ -53,10 +53,17 @@ Validation: MSBuild.exe exists, executes, PATH, VC dir present Step 10 — Toolchain cross-check (dotnet, python, msbuild) Throws on any missing tool + Step 11 — Tier-1 Toolchain (Git, 7-Zip, PowerShell 7 LTS, NSIS, gh CLI) — §6.6 + Installs to C:\Program Files\\ and C:\Program Files (x86)\NSIS + Validation: git, 7z, pwsh, makensis, gh commands resolvable + Step 12 — Tier-2 Toolchain (CMake, Node.js LTS, WiX v4, + Sysinternals Suite, vcpkg) — §6.6 + Installs to C:\Program Files\\ and C:\BuildTools\\ + Validation: each binary resolvable via Get-Command Cleanup — Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM wuauserv already Disabled (Step 6b) — not restarted after cache clear Validation: wuauserv StartType Disabled - Final — Pre-snapshot gate: 7 cross-cutting checks across all steps + Final — Pre-snapshot gate: 11 cross-cutting checks across all steps Throws if any check fails — prevents snapshot of broken state Step ordering rationale: @@ -70,9 +77,13 @@ WU (Step 6) — all prereqs validated; WU runs with Firewall+Defender already off WU disable (Step 6b) — immediately after WU; snapshot captures WU permanently off Toolchain (7-9) — .NET/Python/VS last; WU done, no scan on installer payloads + Tier-1 (Step 11) — Git + 7-Zip + PS7 + NSIS + gh CLI after base toolchain + Tier-2 (Step 12) — CMake/Node/WiX/Sysinternals/vcpkg after Tier-1; + Sysinternals extraction requires 7-Zip; vcpkg clone requires Git - NOTE: Git is NOT installed by default. See §6.6 (Tier-1 Toolchain) in TODO.md - for the planned addition. The host clones and copies source via WinRM until then. + NOTE: Step 11 installs Tier-1 Toolchain §6.6: Git, 7-Zip, PowerShell 7 LTS, NSIS, gh CLI. + Step 12 installs Tier-2 Toolchain §6.6: CMake, Node.js LTS, WiX v4, + Sysinternals Suite, vcpkg. Skippable via -SkipTier2. ╔══════════════════════════════════════════════════════════════════════╗ ║ NETWORK REQUIREMENT DURING SETUP ║ @@ -137,7 +148,42 @@ param( # Administrator password — used only to write DefaultPassword for autologin. # Optional: if empty, DefaultPassword registry value is left as-is (Deploy may have set it). - [string] $AdminPassword = '' + [string] $AdminPassword = '', + + # Git for Windows version (latest stable) + [string] $GitVersion = '2.54.0.windows.1', + + # 7-Zip version (latest stable) + [string] $SevenZipVersion = '26.01', + + # ── Tier-2 Toolchain versions (§6.6) ────────────────────────────────────── + # Update these defaults when upgrading pinned tool versions. + + # PowerShell 7 LTS (pwsh.exe — modern PS for CI scripts + parallel pipelines) + [string] $Pwsh7Version = '7.6.1', + + # NSIS (makensis.exe — Windows installer builds) + [string] $NSISVersion = '3.12', + + # CMake (cmake.exe — C/C++ cross-platform build system) + [string] $CMakeVersion = '3.31.6', + + # Node.js LTS (node.exe + npm — frontend/Electron/JS tool pipelines) + [string] $NodeJSVersion = '22.14.0', + + # WiX Toolset v4 installed via dotnet tool (wix.exe — MSI package building) + [string] $WiXVersion = '4.0.5', + + # GitHub CLI (gh.exe — artifact upload, PR/release/workflow operations) + [string] $GhCliVersion = '2.92.0', + + # vcpkg revision: git commit SHA or tag. Default '' = shallow HEAD clone. + [string] $VcpkgRef = '', + + # Skip Tier-2 Toolchain installation (CMake, Node.js, WiX, Sysinternals, vcpkg). + # Use when rebuilding the template quickly without internet-heavy downloads, or + # when only Tier-1 tools are needed for a stripped-down template variant. + [switch] $SkipTier2 ) Set-StrictMode -Version Latest @@ -199,6 +245,29 @@ $script:Hashes = @{ # vs_buildtools.exe bootstrapper from aka.ms/vs/stable/vs_buildtools.exe # Stable within a VS release cycle — update when VS major version changes. 'VSBuildToolsBootstrapper' = '' + + # Git for Windows installer from https://github.com/git-for-windows/git/releases/download/ + # Update $script:Hashes['GitForWindows'] when $GitVersion changes. + 'GitForWindows' = '2b96e7854f0520f0f6b709c21041d9801b1be44d5e1a0d9fa621b2fbc40f1983' + + # 7-Zip MSI installer from https://www.7-zip.org/download.html + # Update $script:Hashes['SevenZip'] when $SevenZipVersion changes. + 'SevenZip' = '' + + # Tier-2 Toolchain installer hashes — fill in when pinning supply chain (see BEST-PRACTICES.md §1.3) + # PowerShell--win-x64.msi from https://github.com/PowerShell/PowerShell/releases + 'Pwsh7' = '' + # nsis--setup.exe from https://prdownloads.sourceforge.net/nsis/ + 'NSIS' = '' + # cmake--windows-x86_64.msi from https://github.com/Kitware/CMake/releases + 'CMake' = '' + # node-v-x64.msi from https://nodejs.org/dist/v/ + 'NodeJS' = '' + # gh__windows_amd64.msi from https://github.com/cli/cli/releases + 'GhCli' = '' + # WiX: installed via 'dotnet tool install wix' — integrity via NuGet package signing + # Sysinternals: downloaded from Microsoft CDN — no stable hash (updated continuously) + # vcpkg: cloned from GitHub via TLS; pin integrity with $VcpkgRef (commit SHA) } # ── Pre-flight: remove temp files from any previous partial run ─────────────── @@ -207,7 +276,10 @@ $script:Hashes = @{ $knownTempFiles = @( 'C:\CI\wu_worker.ps1', 'C:\CI\wu_result.txt', 'C:\CI\wu_reboot.txt', 'C:\CI\wu_log.txt', 'C:\CI\vs_worker.ps1', 'C:\CI\vs_result.txt', 'C:\CI\vs_BuildTools.exe', - 'C:\CI\python_installer.exe' + 'C:\CI\python_installer.exe', 'C:\CI\git_installer.exe', 'C:\CI\7zip_installer.msi', + # Tier-2 Toolchain installer temp files + 'C:\CI\pwsh_installer.msi', 'C:\CI\nsis_installer.exe', 'C:\CI\cmake_installer.msi', + 'C:\CI\nodejs_installer.msi', 'C:\CI\ghcli_installer.msi', 'C:\CI\sysinternals.zip' ) foreach ($f in $knownTempFiles) { if (Test-Path $f) { @@ -684,8 +756,8 @@ if ($dotnetInstalled) { else { Write-Host "Downloading dotnet-install.ps1..." $dotnetInstallScript = "$env:TEMP\dotnet-install.ps1" - Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' ` - -OutFile $dotnetInstallScript -UseBasicParsing + (New-Object System.Net.WebClient).DownloadFile( + 'https://dot.net/v1/dotnet-install.ps1', $dotnetInstallScript) Assert-Hash -FilePath $dotnetInstallScript -Label 'dotnet-install.ps1' ` -Expected $script:Hashes['DotNetInstallScript'] @@ -733,7 +805,7 @@ else { $pyInstallerUrl = "https://www.python.org/ftp/python/$PythonVersion/python-$PythonVersion-amd64.exe" $pyInstallerPath = 'C:\CI\python_installer.exe' Write-Host "Downloading Python $PythonVersion installer..." - Invoke-WebRequest -Uri $pyInstallerUrl -OutFile $pyInstallerPath -UseBasicParsing + (New-Object System.Net.WebClient).DownloadFile($pyInstallerUrl, $pyInstallerPath) Assert-Hash -FilePath $pyInstallerPath -Label "python-$PythonVersion-amd64.exe" ` -Expected $script:Hashes['Python'] @@ -789,7 +861,7 @@ else { Write-Host "Downloading VS Build Tools installer..." Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy - Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing + (New-Object System.Net.WebClient).DownloadFile($vsInstallerUrl, $vsInstallerPath) Assert-Hash -FilePath $vsInstallerPath -Label 'vs_buildtools.exe' ` -Expected $script:Hashes['VSBuildToolsBootstrapper'] @@ -960,6 +1032,477 @@ if (-not $allOk) { } Write-Host "All toolchain checks passed." -ForegroundColor Green +# ── Step 11: Tier-1 Toolchain (Git + 7-Zip + PS7 + NSIS + gh CLI) ───────────── +# Critical for §3.3 (In-VM Git clone), cross-platform build support, and scripting. +# Installed to C:\Program Files\\ (default) and C:\Program Files (x86)\NSIS. + +Write-Step "Installing Tier-1 Toolchain (Git / 7-Zip / PS7 / NSIS / gh CLI)" + +# Build tools root directory +$buildToolsRoot = 'C:\BuildTools' +if (-not (Test-Path $buildToolsRoot)) { + New-Item -ItemType Directory -Path $buildToolsRoot -Force | Out-Null +} + +# ── Git for Windows ─────────────────────────────────────────────────────────── +$gitDir = 'C:\Program Files\Git' # default install location for Git for Windows +$gitExe = Join-Path $gitDir 'bin\git.exe' + +if (Test-Path $gitExe) { + Write-Host "Git for Windows already installed: $(& $gitExe --version 2>&1)" +} else { + # Extract base version (e.g. '2.54.0' from '2.54.0.windows.1') for filename + $gitVersionBase = $GitVersion -replace '\.windows\.\d+$', '' + $gitUrl = "https://github.com/git-for-windows/git/releases/download/v$GitVersion/Git-$gitVersionBase-64-bit.exe" + $gitInstaller = 'C:\CI\git_installer.exe' + + Write-Host "Downloading Git for Windows $GitVersion..." + try { + (New-Object System.Net.WebClient).DownloadFile($gitUrl, $gitInstaller) + } catch { + throw "Failed to download Git from $gitUrl : $_" + } + + Assert-Hash -FilePath $gitInstaller -Label "Git-$gitVersionBase-64-bit.exe" ` + -Expected $script:Hashes['GitForWindows'] + + Write-Host "Installing Git for Windows (silent, with PATH)..." + $gitProc = Start-Process -FilePath $gitInstaller -ArgumentList @( + '/VERYSILENT', + '/NORESTART', + '/NOCANCEL', + '/SP-', + '/COMPONENTS=assoc,assoc_sh,gitlfs,windowsterminal' + ) -Wait -PassThru + + Remove-Item $gitInstaller -ErrorAction SilentlyContinue + + if ($gitProc -and $gitProc.ExitCode -ne 0) { + throw "Git installation failed (exit $($gitProc.ExitCode))." + } + + Write-Host "Git installed: $(& $gitExe --version 2>&1)" +} + +# Refresh PATH whether freshly installed or already present +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') + +# Validation +Assert-Step 'Git' 'git command resolvable' { + [bool](Get-Command git -ErrorAction SilentlyContinue) +} +Assert-Step 'Git' 'git version matches or compatible' { + $v = (& git --version 2>&1 | Select-Object -First 1).ToString() + $v -match 'git version' +} + +# ── 7-Zip ───────────────────────────────────────────────────────────────────── +$sevenZipDir = 'C:\Program Files\7-Zip' +$sevenZipExe = Join-Path $sevenZipDir '7z.exe' + +if (Test-Path $sevenZipExe) { + Write-Host "7-Zip already installed." +} else { + $sevenZipUrl = "https://www.7-zip.org/a/7z$($SevenZipVersion -replace '\.', '')-x64.msi" + $sevenZipMsi = 'C:\CI\7zip_installer.msi' + + Write-Host "Downloading 7-Zip $SevenZipVersion..." + try { + (New-Object System.Net.WebClient).DownloadFile($sevenZipUrl, $sevenZipMsi) + } catch { + throw "Failed to download 7-Zip from $sevenZipUrl : $_" + } + + Assert-Hash -FilePath $sevenZipMsi -Label "7z$($SevenZipVersion -replace '.', '')-x64.msi" ` + -Expected $script:Hashes['SevenZip'] + + Write-Host "Installing 7-Zip via MSI..." + $msiProc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @( + '/i', $sevenZipMsi, + '/quiet', + '/norestart' + ) -Wait -PassThru + + Remove-Item $sevenZipMsi -ErrorAction SilentlyContinue + + if ($msiProc -and $msiProc.ExitCode -notin @(0, 3010)) { + throw "7-Zip installation failed (exit $($msiProc.ExitCode))." + } + + Write-Host "7-Zip installed." +} + +# 7-Zip MSI does NOT add to system PATH — register permanently in Machine PATH +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$sevenZipDir*") { + [System.Environment]::SetEnvironmentVariable( + 'PATH', "$machinePath;$sevenZipDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') + +# Validation +Assert-Step '7-Zip' '7z.exe exists at expected path' { + Test-Path -Path $sevenZipExe -PathType Leaf +} +Assert-Step '7-Zip' '7z command resolvable' { + [bool](Get-Command 7z -ErrorAction SilentlyContinue) +} + +# ── PowerShell 7 LTS ───────────────────────────────────────────────────────── +$pwsh7Exe = 'C:\Program Files\PowerShell\7\pwsh.exe' +if (Test-Path $pwsh7Exe) { + $v = & $pwsh7Exe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>&1 | Select-Object -First 1 + Write-Host "PowerShell 7 already installed: $v" +} else { + $pwsh7Url = "https://github.com/PowerShell/PowerShell/releases/download/v$Pwsh7Version/PowerShell-$Pwsh7Version-win-x64.msi" + $pwsh7Msi = 'C:\CI\pwsh_installer.msi' + Write-Host "Downloading PowerShell $Pwsh7Version from GitHub releases..." + try { (New-Object System.Net.WebClient).DownloadFile($pwsh7Url, $pwsh7Msi) } + catch { throw "Failed to download PowerShell from '$pwsh7Url': $_" } + Assert-Hash -FilePath $pwsh7Msi -Label "PowerShell-$Pwsh7Version-win-x64.msi" ` + -Expected $script:Hashes['Pwsh7'] + Write-Host "Installing PowerShell $Pwsh7Version (quiet, no PSRemoting, no manifest)..." + $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @( + '/i', $pwsh7Msi, '/quiet', '/norestart', + 'ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=0', + 'ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=0', + 'ENABLE_PSREMOTING=0', + 'REGISTER_MANIFEST=0' + ) -Wait -PassThru + Remove-Item $pwsh7Msi -ErrorAction SilentlyContinue + if ($proc -and $proc.ExitCode -notin @(0, 3010)) { + throw "PowerShell 7 MSI exited $($proc.ExitCode)." + } + Write-Host "PowerShell $Pwsh7Version installed." +} +$pwsh7Dir = 'C:\Program Files\PowerShell\7' +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$pwsh7Dir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$pwsh7Dir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'Pwsh7' 'pwsh.exe present at expected path' { Test-Path $pwsh7Exe -PathType Leaf } +Assert-Step 'Pwsh7' 'pwsh command resolvable via PATH' { [bool](Get-Command pwsh -ErrorAction SilentlyContinue) } +Assert-Step 'Pwsh7' "pwsh major version matches $($Pwsh7Version.Split('.')[0])" { + $v = (& $pwsh7Exe -NoProfile -Command '$PSVersionTable.PSVersion.Major' 2>&1 | Select-Object -First 1).ToString().Trim() + $v -eq $Pwsh7Version.Split('.')[0] +} + +# ── NSIS ───────────────────────────────────────────────────────────────────── +$nsisExe = 'C:\Program Files (x86)\NSIS\makensis.exe' +if (Test-Path $nsisExe) { + Write-Host "NSIS already installed." +} else { + # SourceForge redirect chain: + # Step 1: sf.net/.../download?use_mirror=autoselect → 200 HTML meta-refresh + # The meta-refresh URL is: https://downloads.sourceforge.net/project/nsis/...?ts=TOKEN&... + # That tokenised URL resolves directly to the binary (no further HTML). + # Strategy: fetch the HTML page via WebClient (faster than IWR — no progress-bar overhead), + # HTML-decode the extracted URL (& → &), then download directly. + $nsisPageUrl = "https://sourceforge.net/projects/nsis/files/NSIS%20$($NSISVersion.Split('.')[0])/$NSISVersion/nsis-$NSISVersion-setup.exe/download?use_mirror=autoselect" + $nsisInstaller = 'C:\CI\nsis_installer.exe' + $sfUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + + Write-Host "Resolving NSIS $NSISVersion download URL from SourceForge..." + $wc = New-Object System.Net.WebClient + $wc.Headers.Add('User-Agent', $sfUserAgent) + try { + $sfHtml = $wc.DownloadString($nsisPageUrl) + } catch { throw "Failed to reach SourceForge download page: $_" } + + # Extract tokenised URL from: + # The HTML may encode & as &, so decode before using the URL. + $nsisCdnUrl = $null + if ($sfHtml -match 'content="\d+;\s*url=(https://downloads\.sourceforge\.net/[^"]+)"') { + $nsisCdnUrl = [System.Net.WebUtility]::HtmlDecode($Matches[1]) + } + if (-not $nsisCdnUrl) { + throw "Could not extract tokenised download URL from SourceForge page for NSIS $NSISVersion. " + + "Page structure may have changed." + } + Write-Host "Downloading NSIS $NSISVersion from SourceForge..." + $wc.Headers.Add('User-Agent', $sfUserAgent) # re-set: DownloadString clears headers + try { + $wc.DownloadFile($nsisCdnUrl, $nsisInstaller) + } catch { throw "Failed to download NSIS from SourceForge: $_" } + + # Guard: verify the file is a valid PE executable (MZ magic bytes). + $peBytes = [System.IO.File]::ReadAllBytes($nsisInstaller) + if ($peBytes.Length -lt 2 -or $peBytes[0] -ne 0x4D -or $peBytes[1] -ne 0x5A) { + Remove-Item $nsisInstaller -ErrorAction SilentlyContinue + throw "NSIS download is not a valid PE executable (MZ header missing). " + + "SourceForge may have returned HTML instead of the binary." + } + Assert-Hash -FilePath $nsisInstaller -Label "nsis-$NSISVersion-setup.exe" ` + -Expected $script:Hashes['NSIS'] + Write-Host "Installing NSIS $NSISVersion (silent /S)..." + $proc = Start-Process -FilePath $nsisInstaller -ArgumentList '/S' -Wait -PassThru + Remove-Item $nsisInstaller -ErrorAction SilentlyContinue + if ($proc -and $proc.ExitCode -ne 0) { + throw "NSIS installer exited $($proc.ExitCode)." + } + Write-Host "NSIS $NSISVersion installed." +} +$nsisDir = 'C:\Program Files (x86)\NSIS' +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$nsisDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$nsisDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'NSIS' 'makensis.exe present at expected path' { Test-Path $nsisExe -PathType Leaf } +Assert-Step 'NSIS' 'makensis command resolvable via PATH' { [bool](Get-Command makensis -ErrorAction SilentlyContinue) } + +# ── GitHub CLI (gh) ─────────────────────────────────────────────────────────── +$ghExe = 'C:\Program Files\GitHub CLI\gh.exe' +if (Test-Path $ghExe) { + $ghv = (& $ghExe --version 2>&1 | Select-Object -First 1).ToString() + Write-Host "GitHub CLI already installed: $ghv" +} else { + $ghUrl = "https://github.com/cli/cli/releases/download/v$GhCliVersion/gh_${GhCliVersion}_windows_amd64.msi" + $ghMsi = 'C:\CI\ghcli_installer.msi' + Write-Host "Downloading GitHub CLI $GhCliVersion from GitHub releases..." + try { (New-Object System.Net.WebClient).DownloadFile($ghUrl, $ghMsi) } + catch { throw "Failed to download GitHub CLI from '$ghUrl': $_" } + Assert-Hash -FilePath $ghMsi -Label "gh_${GhCliVersion}_windows_amd64.msi" ` + -Expected $script:Hashes['GhCli'] + Write-Host "Installing GitHub CLI $GhCliVersion..." + $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @( + '/i', $ghMsi, '/quiet', '/norestart' + ) -Wait -PassThru + Remove-Item $ghMsi -ErrorAction SilentlyContinue + if ($proc -and $proc.ExitCode -notin @(0, 3010)) { + throw "GitHub CLI MSI exited $($proc.ExitCode)." + } + Write-Host "GitHub CLI $GhCliVersion installed." +} +# gh CLI MSI may not update Machine PATH — ensure the install dir is present. +$ghDir = 'C:\Program Files\GitHub CLI' +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$ghDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$ghDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'GhCli' 'gh.exe present at expected path' { Test-Path $ghExe -PathType Leaf } +Assert-Step 'GhCli' 'gh command resolvable via PATH' { [bool](Get-Command gh -ErrorAction SilentlyContinue) } +Assert-Step 'GhCli' 'gh --version exits 0' { & gh --version 2>&1 | Out-Null; $LASTEXITCODE -eq 0 } + +Write-Host "Tier-1 Toolchain installation complete." -ForegroundColor Green + +# ── Step 12: Tier-2 Toolchain ───────────────────────────────────────────────── +if ($SkipTier2) { + Write-Host "`n[Setup] Skipping Tier-2 Toolchain (-SkipTier2 switch set)." -ForegroundColor Yellow +} +else { +Write-Step "Installing Tier-2 Toolchain (CMake / Node.js / WiX / Sysinternals / vcpkg)" + +# BuildTools root may already exist from Step 11 (idempotent). +if (-not (Test-Path $buildToolsRoot)) { + New-Item -ItemType Directory -Path $buildToolsRoot -Force | Out-Null +} + + + +# ── CMake ───────────────────────────────────────────────────────────────────── +$cmakeExe = 'C:\Program Files\CMake\bin\cmake.exe' +if (Test-Path $cmakeExe) { + $cv = (cmake --version 2>&1 | Select-Object -First 1).ToString() + Write-Host "CMake already installed: $cv" +} else { + $cmakeUrl = "https://github.com/Kitware/CMake/releases/download/v$CMakeVersion/cmake-$CMakeVersion-windows-x86_64.msi" + $cmakeMsi = 'C:\CI\cmake_installer.msi' + Write-Host "Downloading CMake $CMakeVersion from GitHub releases..." + try { (New-Object System.Net.WebClient).DownloadFile($cmakeUrl, $cmakeMsi) } + catch { throw "Failed to download CMake from '$cmakeUrl': $_" } + Assert-Hash -FilePath $cmakeMsi -Label "cmake-$CMakeVersion-windows-x86_64.msi" ` + -Expected $script:Hashes['CMake'] + Write-Host "Installing CMake $CMakeVersion (ADD_CMAKE_TO_PATH=System)..." + $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @( + '/i', $cmakeMsi, '/quiet', '/norestart', 'ADD_CMAKE_TO_PATH=System' + ) -Wait -PassThru + Remove-Item $cmakeMsi -ErrorAction SilentlyContinue + if ($proc -and $proc.ExitCode -notin @(0, 3010)) { + throw "CMake MSI exited $($proc.ExitCode)." + } + Write-Host "CMake $CMakeVersion installed." +} +$cmakeDir = 'C:\Program Files\CMake\bin' +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$cmakeDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$cmakeDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'CMake' 'cmake.exe present at expected path' { Test-Path $cmakeExe -PathType Leaf } +Assert-Step 'CMake' 'cmake command resolvable via PATH' { [bool](Get-Command cmake -ErrorAction SilentlyContinue) } +Assert-Step 'CMake' "cmake major version matches $($CMakeVersion.Split('.')[0])" { + $v = (cmake --version 2>&1 | Select-Object -First 1).ToString() + $v -match "cmake version $([regex]::Escape($CMakeVersion.Split('.')[0]))\." +} + +# ── Node.js LTS ─────────────────────────────────────────────────────────────── +$nodeExe = 'C:\Program Files\nodejs\node.exe' +if (Test-Path $nodeExe) { + $nv = (node --version 2>&1 | Select-Object -First 1).ToString() + Write-Host "Node.js already installed: $nv" +} else { + $nodeUrl = "https://nodejs.org/dist/v$NodeJSVersion/node-v$NodeJSVersion-x64.msi" + $nodeMsi = 'C:\CI\nodejs_installer.msi' + Write-Host "Downloading Node.js $NodeJSVersion from nodejs.org..." + try { (New-Object System.Net.WebClient).DownloadFile($nodeUrl, $nodeMsi) } + catch { throw "Failed to download Node.js from '$nodeUrl': $_" } + Assert-Hash -FilePath $nodeMsi -Label "node-v$NodeJSVersion-x64.msi" ` + -Expected $script:Hashes['NodeJS'] + Write-Host "Installing Node.js $NodeJSVersion..." + $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @( + '/i', $nodeMsi, '/quiet', '/norestart' + ) -Wait -PassThru + Remove-Item $nodeMsi -ErrorAction SilentlyContinue + if ($proc -and $proc.ExitCode -notin @(0, 3010)) { + throw "Node.js MSI exited $($proc.ExitCode)." + } + Write-Host "Node.js $NodeJSVersion installed." +} +$nodeDir = 'C:\Program Files\nodejs' +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$nodeDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$nodeDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'NodeJS' 'node.exe present at expected path' { Test-Path $nodeExe -PathType Leaf } +Assert-Step 'NodeJS' 'node command resolvable via PATH' { [bool](Get-Command node -ErrorAction SilentlyContinue) } +Assert-Step 'NodeJS' "node.js major version matches $($NodeJSVersion.Split('.')[0])" { + $v = (node --version 2>&1 | Select-Object -First 1).ToString().TrimStart('v') + $v.Split('.')[0] -eq $NodeJSVersion.Split('.')[0] +} + +# ── WiX Toolset v4 (via dotnet tool) ───────────────────────────────────────── +# Requires .NET SDK (installed in Step 7). Installed to a dedicated tool-path on +# Machine PATH so all users (ci_build, SYSTEM) can invoke wix.exe. +$wixToolsDir = 'C:\BuildTools\Wix' +$wixExe = Join-Path $wixToolsDir 'wix.exe' +if (Test-Path $wixExe) { + Write-Host "WiX Toolset already installed." +} else { + if (-not (Test-Path $wixToolsDir)) { + New-Item -ItemType Directory -Path $wixToolsDir -Force | Out-Null + } + $dotnetBin = (Get-Command dotnet -ErrorAction SilentlyContinue) + if (-not $dotnetBin) { throw "dotnet not found in PATH — Step 7 (DotNet SDK) must complete first." } + Write-Host "Installing WiX Toolset $WiXVersion via 'dotnet tool install wix --tool-path $wixToolsDir'..." + $proc = Start-Process -FilePath $dotnetBin.Source -ArgumentList @( + 'tool', 'install', 'wix', + '--version', $WiXVersion, + '--tool-path', $wixToolsDir + ) -Wait -PassThru + if ($proc -and $proc.ExitCode -ne 0) { + throw "WiX dotnet tool install exited $($proc.ExitCode)." + } + Write-Host "WiX Toolset $WiXVersion installed." +} +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$wixToolsDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$wixToolsDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'WiX' 'wix.exe present at expected path' { Test-Path $wixExe -PathType Leaf } +Assert-Step 'WiX' 'wix command resolvable via PATH' { [bool](Get-Command wix -ErrorAction SilentlyContinue) } + +# ── Sysinternals Suite ──────────────────────────────────────────────────────── +# Downloaded as SysinternalsSuite.zip from Microsoft CDN; extracted via 7-Zip. +# 7-Zip ($sevenZipExe) must already be installed (Step 11 Tier-1). +$sysDir = 'C:\BuildTools\Sysinternals' +$sysPsExec = Join-Path $sysDir 'PsExec.exe' +if ((Test-Path $sysDir) -and (Test-Path $sysPsExec)) { + Write-Host "Sysinternals Suite already installed." +} else { + $sysZipUrl = 'https://download.sysinternals.com/files/SysinternalsSuite.zip' + $sysZip = 'C:\CI\sysinternals.zip' + Write-Host "Downloading Sysinternals Suite from Microsoft CDN..." + try { (New-Object System.Net.WebClient).DownloadFile($sysZipUrl, $sysZip) } + catch { throw "Failed to download Sysinternals Suite: $_" } + if (-not (Test-Path $sysDir)) { + New-Item -ItemType Directory -Path $sysDir -Force | Out-Null + } + Write-Host "Extracting Sysinternals to $sysDir via 7-Zip..." + if (-not (Test-Path $sevenZipExe)) { + throw "7-Zip not found at '$sevenZipExe' — Step 11 (Tier-1) must complete first." + } + & $sevenZipExe x $sysZip "-o$sysDir" -y 2>&1 | Write-Host + if ($LASTEXITCODE -ne 0) { throw "Sysinternals extraction failed (7z exit $LASTEXITCODE)." } + Remove-Item $sysZip -ErrorAction SilentlyContinue + Write-Host "Sysinternals Suite extracted." +} +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$sysDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$sysDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'Sysinternals' 'PsExec.exe present in Sysinternals directory' { Test-Path $sysPsExec -PathType Leaf } +Assert-Step 'Sysinternals' 'procexp.exe present in Sysinternals directory' { Test-Path (Join-Path $sysDir 'procexp.exe') -PathType Leaf } +Assert-Step 'Sysinternals' 'Sysinternals directory on Machine PATH' { + [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') -like "*$sysDir*" +} + +# ── vcpkg ───────────────────────────────────────────────────────────────────── +# Cloned from GitHub and bootstrapped. Requires Git (Step 11 Tier-1). +# $VcpkgRef = '' → shallow --depth 1 clone of HEAD (fastest for template provisioning). +# $VcpkgRef = '' → full clone then checkout (supply-chain-pinned builds). +$vcpkgDir = 'C:\BuildTools\vcpkg' +$vcpkgExe = Join-Path $vcpkgDir 'vcpkg.exe' +if (Test-Path $vcpkgExe) { + $vvpkg = (& $vcpkgExe version 2>&1 | Select-Object -First 1).ToString() + Write-Host "vcpkg already installed: $vvpkg" +} else { + $gitBin = (Get-Command git -ErrorAction SilentlyContinue) + if (-not $gitBin) { throw "git not found in PATH — Step 11 (Tier-1) must complete first." } + if (Test-Path $vcpkgDir) { Remove-Item $vcpkgDir -Recurse -Force } + Write-Host "Cloning vcpkg from GitHub..." + $cloneArgs = @('clone', 'https://github.com/microsoft/vcpkg.git', $vcpkgDir) + if ($VcpkgRef -eq '') { + $cloneArgs += '--depth' + $cloneArgs += '1' + } + & $gitBin.Source @cloneArgs 2>&1 | Write-Host + if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." } + if ($VcpkgRef -ne '') { + Write-Host "Checking out vcpkg ref: $VcpkgRef" + & $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host + if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." } + } + Write-Host "Bootstrapping vcpkg (-disableMetrics)..." + $bootstrapBat = Join-Path $vcpkgDir 'bootstrap-vcpkg.bat' + $proc = Start-Process -FilePath 'cmd.exe' ` + -ArgumentList "/c `"$bootstrapBat`" -disableMetrics" ` + -WorkingDirectory $vcpkgDir -Wait -PassThru + if ($proc -and $proc.ExitCode -ne 0) { + throw "vcpkg bootstrap exited $($proc.ExitCode)." + } + Write-Host "vcpkg bootstrapped." +} +$machinePath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') +if ($machinePath -notlike "*$vcpkgDir*") { + [System.Environment]::SetEnvironmentVariable('PATH', "$machinePath;$vcpkgDir", 'Machine') +} +$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') +Assert-Step 'vcpkg' 'vcpkg.exe present at expected path' { Test-Path $vcpkgExe -PathType Leaf } +Assert-Step 'vcpkg' 'vcpkg command resolvable via PATH' { [bool](Get-Command vcpkg -ErrorAction SilentlyContinue) } +Assert-Step 'vcpkg' 'vcpkg version exits 0' { + & vcpkg version 2>&1 | Out-Null + $LASTEXITCODE -eq 0 +} + +Write-Host "Tier-2 Toolchain installation complete." -ForegroundColor Green +} # end if (-not $SkipTier2) + # ── Cleanup ─────────────────────────────────────────────────────────────────── if ($SkipCleanup) { Write-Host "`n[Setup] Skipping disk cleanup (-SkipCleanup switch set)." -ForegroundColor Yellow @@ -1125,7 +1668,39 @@ Assert-Step 'Final' 'no leftover installer scripts in C:\CI' { -not (Test-Path 'C:\CI\python_installer.exe') -and -not (Test-Path 'C:\CI\vs_BuildTools.exe') -and -not (Test-Path 'C:\CI\wu_worker.ps1') -and - -not (Test-Path 'C:\CI\vs_worker.ps1') + -not (Test-Path 'C:\CI\vs_worker.ps1') -and + -not (Test-Path 'C:\CI\git_installer.exe') -and + -not (Test-Path 'C:\CI\7zip_installer.msi') -and + -not (Test-Path 'C:\CI\pwsh_installer.msi') -and + -not (Test-Path 'C:\CI\nsis_installer.exe') -and + -not (Test-Path 'C:\CI\cmake_installer.msi') -and + -not (Test-Path 'C:\CI\nodejs_installer.msi') -and + -not (Test-Path 'C:\CI\ghcli_installer.msi') -and + -not (Test-Path 'C:\CI\sysinternals.zip') +} +Assert-Step 'Final' 'git command available' { + [bool](Get-Command git -ErrorAction SilentlyContinue) +} +Assert-Step 'Final' '7z command available' { + [bool](Get-Command 7z -ErrorAction SilentlyContinue) +} +Assert-Step 'Final' 'Tier-1 extras all resolvable (pwsh, makensis, gh)' { + $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') + (Get-Command pwsh -ErrorAction SilentlyContinue) -and + (Get-Command makensis -ErrorAction SilentlyContinue) -and + (Get-Command gh -ErrorAction SilentlyContinue) +} +if (-not $SkipTier2) { + Assert-Step 'Final' 'Tier-2 Toolchain all resolvable (cmake, node, wix, sysinternals, vcpkg)' { + $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + + [System.Environment]::GetEnvironmentVariable('PATH', 'User') + (Get-Command cmake -ErrorAction SilentlyContinue) -and + (Get-Command node -ErrorAction SilentlyContinue) -and + (Get-Command wix -ErrorAction SilentlyContinue) -and + (Test-Path 'C:\BuildTools\Sysinternals\PsExec.exe' -PathType Leaf) -and + (Test-Path 'C:\BuildTools\vcpkg\vcpkg.exe' -PathType Leaf) + } } Write-Host "All pre-snapshot checks passed." -ForegroundColor Green diff --git a/template/Setup-WinBuild2025.ps1 b/template/Setup-WinBuild2025.ps1 index f985e17..fee4dbe 100644 --- a/template/Setup-WinBuild2025.ps1 +++ b/template/Setup-WinBuild2025.ps1 @@ -1081,7 +1081,7 @@ if (Test-Path $gitExe) { throw "Git installation failed (exit $($gitProc.ExitCode))." } - Write-Host "Git installed: $(git --version 2>&1)" + Write-Host "Git installed: $(& $gitExe --version 2>&1)" } # Refresh PATH whether freshly installed or already present