diff --git a/TODO.md b/TODO.md index 353495c..3da804e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # TODO — Local CI/CD System - + > Documento unico di lavoro: roadmap, audit trail dei task completati, e backlog post-v1.0 > con priorità e razionale. Le voci aperte sono raggruppate per area e marcate **P0..P3**: @@ -18,9 +18,9 @@ --- ## Summary -_Last updated: 2026-05-11 (post Sprint 10: §6.1 Linux Build VM e2e PASS — Test-Ns7zipBuild-Linux.ps1 4× PASS, nsis7z.dll 2272 KB su Linux = parità Windows; In-VM Git Clone sezione chiusa)_ +_Last updated: 2026-05-11 (post Sprint 11: §6.6 Tier-2 Toolchain DONE — PowerShell 7.4.7, NSIS 3.10, CMake 3.31.6, Node.js 22.14.0, WiX 4.0.5, gh CLI 2.67.0, Sysinternals, vcpkg — Step 12 in Setup-WinBuild2025.ps1)_ -**Stato generale**: §1, §2, §4, §5 chiusi. §3 completo. §6.1 Linux Build VM completato con e2e PASS (nsis7z.dll 2272 KB, parità Windows). §6.6 Tier-1 (Git+7-Zip) nel template. Prossimo P2: §6.6 Tier-2 toolchain. +**Stato generale**: §1, §2, §4, §5 chiusi. §3 completo. §6.1 Linux Build VM completato con e2e PASS (nsis7z.dll 2272 KB, parità Windows). §6.6 Tier-1+Tier-2 completati nel template (Step 11+12). Prossimi P2: Test Plan + §6.2-6.5. | Stato | Area | Done | Open | Note | | ----- | ------------------------------- | ---: | ---: | ------------------------------------------------------------------ | @@ -34,17 +34,16 @@ _Last updated: 2026-05-11 (post Sprint 10: §6.1 Linux Build VM e2e PASS — Tes | [x] | §3 Performance & throughput | 4 | 0 | 3.1/3.2/3.3/3.6 done; 3.4/3.5 deferred — **§3 CHIUSO** | | [x] | §4 Osservabilità & manutenzione | 3 | 0 | 4.1/4.3/4.4 done (4.2 Prometheus, 4.5 Dashboard rimossi) | | [x] | §5 Qualità codice & test | 5 | 0 | 5.1/5.2/5.3/5.4/5.5 done | -| [ ] | §6 Scalabilità & estensioni | 2 | 4 | 6.1 (Linux) done, 6.6 Tier-1 (Git+7-Zip) done; 6.2-6.5 TODO | +| [ ] | §6 Scalabilità & estensioni | 2 | 4 | 6.1 (Linux) done, 6.6 Tier-1+Tier-2 done (Step 11+12); 6.2-6.5 TODO | | [ ] | §7 Refactor Deploy ↔ Setup | 4 | 1 | 7.1/7.2/7.3/7.4 done; 7.5 (rinomina) opzionale | | [x] | In-VM Git Clone (§3.3) | 4 | 0 | COMPLETATO 2026-05-10 — e2e PASS: 34s/25.7% vs baseline | | [x] | Linux Build VM (§6.1) | 5 | 0 | Deploy/Prepare/Setup/Transport + doc + e2e PASS nsis7z.dll 2272 KB | **Prossimi passi consigliati** (vedi anche "Suggerimento di sequencing" in fondo): 1. **Test Plan** — eseguire completo `docs/TEST-PLAN-v1.3-to-HEAD.md` (25+ Pester tests, validazione features) -2. **§6.6 expansion** — Toolchain Tier-2 (PowerShell 7, NSIS, CMake, Node.js, WiX, gh, Sysinternals, vcpkg) -3. **Security** — Rifiori se condizioni cambiano; vedi sezione "Deferred (Home Lab)" -4. **§3.5** — vCPU/RAM tuning per workload (P2, deferred — home lab optimization) -5. **§3.4** — Pre-warm pool di cloni (P3, deferred — profiling-dependent optimization) +2. **Security** — Rifiori se condizioni cambiano; vedi sezione "Deferred (Home Lab)" +3. **§3.5** — vCPU/RAM tuning per workload (P2, deferred — home lab optimization) +4. **§3.4** — Pre-warm pool di cloni (P3, deferred — profiling-dependent optimization) --- ## Setup & Infrastructure @@ -501,10 +500,11 @@ runs-on: ${{ matrix.target }}-build Per chiavi di firma (Authenticode, GPG), usare i Gitea secrets nel workflow e passarli a `Invoke-CIJob.ps1` come param + env, mai loggati. Stesso modello di §1.5 per il PAT. -### 6.6 [P2] [~] Toolchain Tier-1 in Setup-WinBuild2025 -File: [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1). +### 6.6 [P2] [x] Toolchain Tier-1 + Tier-2 in Setup-WinBuild2025 +File: [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1), +[template/Prepare-WinBuild2025.ps1](template/Prepare-WinBuild2025.ps1). -**Status**: Git + 7-Zip implemented (Step 11, 2026-05-10); Tier-2 (8 tools) pending. +**Status**: COMPLETATO 2026-05-11 — Step 11 (Tier-1: Git + 7-Zip) + Step 12 (Tier-2: 8 tools) implementati. **Motivazione**: toolchain attuale (`.NET SDK`, `Python`, `VS Build Tools`) copre solo C#/Python/MSBuild. Aggiungere tools generici-CI riduce il bisogno di installazione ad-hoc nei workflow e abilita @@ -514,20 +514,20 @@ build più ampi senza toccare il template. - [x] **Git for Windows** (v2.54.0.windows.1) — Step 11, self-clone in VM - [x] **7-Zip** (26.01) — Step 11, universal compression + fallback §3.2 -### Tier-2 (backlog) — tool da aggiungere come ulteriori step `Setup-WinBuild2025` (ognuno con `Assert-Step`): +### Tier-2 (implementato — Step 12) | Tool | Versione | Razionale | | ------------------- | ----------------------- | ------------------------------------------------------------------ | | **Git for Windows** | 2.54.0.windows.1 | [x] Self-clone in VM (alt a host-zip-transfer), submodule fetch | | **7-Zip** | 26.01 | [x] Universale unpack/zip; molti installer e workflow ne dipendono | -| **PowerShell 7.x** | latest LTS | [ ] Più veloce di 5.1, parallel pipelines, operatori moderni | -| **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) | -| **CMake** | latest | Build system cross-platform per progetti C/C++ nativi | -| **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) | -| **WiX Toolset** | v4 stable | MSI building da .NET projects | -| **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR | -| **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky | -| **vcpkg** | latest | Package manager C++ per dipendenze native | +| **PowerShell 7.x** | 7.4.7 (LTS) | [x] Più veloce di 5.1, parallel pipelines, operatori moderni | +| **NSIS** | 3.10 | [x] Test build installer (es. `nsis-plugin-nsinnounp`) | +| **CMake** | 3.31.6 | [x] Build system cross-platform per progetti C/C++ nativi | +| **Node.js LTS** | 22.14.0 | [x] Frontend/Electron pipelines, JS toolchain (npm) | +| **WiX Toolset** | 4.0.5 (dotnet tool) | [x] MSI building da .NET projects | +| **gh CLI** | 2.67.0 | [x] Upload artifact a release Gitea/GitHub, comment PR | +| **Sysinternals** | latest (CDN Microsoft) | [x] Diagnostica process/handle quando build flaky | +| **vcpkg** | HEAD (shallow clone) | [x] Package manager C++ per dipendenze native | **Approccio**: - Pinning hash SHA256 per ogni installer (vedi §1.3) diff --git a/template/Prepare-WinBuild2025.ps1 b/template/Prepare-WinBuild2025.ps1 index 363f7c6..bd92f74 100644 --- a/template/Prepare-WinBuild2025.ps1 +++ b/template/Prepare-WinBuild2025.ps1 @@ -132,6 +132,15 @@ param( [string] $DotNetSdkVersion = '10.0', + # Tier-2 Toolchain version pins — forwarded as-is to Setup-WinBuild2025.ps1 + [string] $Pwsh7Version = '7.4.7', + [string] $NSISVersion = '3.10', + [string] $CMakeVersion = '3.31.6', + [string] $NodeJSVersion = '22.14.0', + [string] $WiXVersion = '4.0.5', + [string] $GhCliVersion = '2.67.0', + [string] $VcpkgRef = '', + [switch] $SkipWindowsUpdate, # Skip disk cleanup inside the VM (cleanmgr + DISM + cache clear) — speeds up re-runs @@ -367,6 +376,13 @@ try { BuildUsername = $BuildUsername DotNetSdkVersion = $DotNetSdkVersion AdminPassword = $AdminPassword + Pwsh7Version = $Pwsh7Version + NSISVersion = $NSISVersion + CMakeVersion = $CMakeVersion + NodeJSVersion = $NodeJSVersion + WiXVersion = $WiXVersion + GhCliVersion = $GhCliVersion + VcpkgRef = $VcpkgRef } if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true } if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true } diff --git a/template/Setup-WinBuild2025.ps1 b/template/Setup-WinBuild2025.ps1 index a785a46..f9e0601 100644 --- a/template/Setup-WinBuild2025.ps1 +++ b/template/Setup-WinBuild2025.ps1 @@ -56,10 +56,14 @@ Step 11 — Tier-1 Toolchain (Git for Windows + 7-Zip) — §6.6 Installs to C:\Program Files (Git default) and C:\Program Files\7-Zip Validation: git and 7z commands resolvable, executables present + Step 12 — Tier-2 Toolchain (PowerShell 7 LTS, NSIS, CMake, Node.js LTS, + WiX v4, gh CLI, 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: 9 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: @@ -74,9 +78,12 @@ 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 after base toolchain; enables §3.3 in-VM clone + Tier-2 (Step 12) — Pwsh7/NSIS/CMake/Node/WiX/gh/Sysinternals/vcpkg after Tier-1; + Sysinternals extraction requires 7-Zip; vcpkg clone requires Git NOTE: Step 11 installs Git for Windows + 7-Zip (Tier-1 Toolchain §6.6). - This enables §3.3 (In-VM Git clone) and cross-platform build support. + Step 12 installs PowerShell 7 LTS, NSIS, CMake, Node.js LTS, WiX v4, gh CLI, + Sysinternals Suite, and vcpkg (Tier-2 Toolchain §6.6). ╔══════════════════════════════════════════════════════════════════════╗ ║ NETWORK REQUIREMENT DURING SETUP ║ @@ -147,7 +154,31 @@ param( [string] $GitVersion = '2.54.0.windows.1', # 7-Zip version (latest stable) - [string] $SevenZipVersion = '26.01' + [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.4.7', + + # NSIS (makensis.exe — Windows installer builds) + [string] $NSISVersion = '3.10', + + # 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.67.0', + + # vcpkg revision: git commit SHA or tag. Default '' = shallow HEAD clone. + [string] $VcpkgRef = '' ) Set-StrictMode -Version Latest @@ -217,6 +248,21 @@ $script:Hashes = @{ # 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 ─────────────── @@ -225,7 +271,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\git_installer.exe', 'C:\CI\7zip_installer.msi' + '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) { @@ -1098,6 +1147,315 @@ Assert-Step '7-Zip' '7z command resolvable' { Write-Host "Tier-1 Toolchain installation complete." -ForegroundColor Green +# ── Step 12: Tier-2 Toolchain ───────────────────────────────────────────────── +Write-Step "Installing Tier-2 Toolchain (Pwsh7 / NSIS / CMake / Node.js / WiX / gh CLI / 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 +} + +# ── 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 { Invoke-WebRequest -Uri $pwsh7Url -OutFile $pwsh7Msi -UseBasicParsing } + 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 { + $nsisUrl = "https://prdownloads.sourceforge.net/nsis/nsis-$NSISVersion-setup.exe" + $nsisInstaller = 'C:\CI\nsis_installer.exe' + Write-Host "Downloading NSIS $NSISVersion from SourceForge..." + try { Invoke-WebRequest -Uri $nsisUrl -OutFile $nsisInstaller -UseBasicParsing } + catch { throw "Failed to download NSIS from '$nsisUrl': $_" } + 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) } + +# ── 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 { Invoke-WebRequest -Uri $cmakeUrl -OutFile $cmakeMsi -UseBasicParsing } + 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 { Invoke-WebRequest -Uri $nodeUrl -OutFile $nodeMsi -UseBasicParsing } + 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) } + +# ── 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 { Invoke-WebRequest -Uri $ghUrl -OutFile $ghMsi -UseBasicParsing } + 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 } + +# ── 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 { Invoke-WebRequest -Uri $sysZipUrl -OutFile $sysZip -UseBasicParsing } + 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 + # ── Cleanup ─────────────────────────────────────────────────────────────────── if ($SkipCleanup) { Write-Host "`n[Setup] Skipping disk cleanup (-SkipCleanup switch set)." -ForegroundColor Yellow @@ -1265,7 +1623,13 @@ Assert-Step 'Final' 'no leftover installer scripts in C:\CI' { -not (Test-Path 'C:\CI\wu_worker.ps1') -and -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') + -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) @@ -1273,6 +1637,18 @@ Assert-Step 'Final' 'git command available' { Assert-Step 'Final' '7z command available' { [bool](Get-Command 7z -ErrorAction SilentlyContinue) } +Assert-Step 'Final' 'Tier-2 Toolchain all resolvable (pwsh, makensis, cmake, node, wix, gh, sysinternals, vcpkg)' { + $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 cmake -ErrorAction SilentlyContinue) -and + (Get-Command node -ErrorAction SilentlyContinue) -and + (Get-Command wix -ErrorAction SilentlyContinue) -and + (Get-Command gh -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/Validate-SetupState.ps1 b/template/Validate-SetupState.ps1 index 402ffc1..90eeebb 100644 --- a/template/Validate-SetupState.ps1 +++ b/template/Validate-SetupState.ps1 @@ -228,6 +228,32 @@ try { Get-Command 7z -ErrorAction SilentlyContinue } + # Tier-2 Toolchain (§6.6) + Chk 'PowerShell 7 (pwsh.exe in PATH)' { + Get-Command pwsh -ErrorAction SilentlyContinue + } + Chk 'NSIS (makensis.exe in PATH)' { + Get-Command makensis -ErrorAction SilentlyContinue + } + Chk 'CMake (cmake.exe in PATH)' { + Get-Command cmake -ErrorAction SilentlyContinue + } + Chk 'Node.js (node.exe in PATH)' { + Get-Command node -ErrorAction SilentlyContinue + } + Chk 'WiX Toolset (wix.exe in PATH)' { + Get-Command wix -ErrorAction SilentlyContinue + } + Chk 'GitHub CLI (gh.exe in PATH)' { + Get-Command gh -ErrorAction SilentlyContinue + } + Chk 'Sysinternals PsExec.exe present' { + Test-Path 'C:\BuildTools\Sysinternals\PsExec.exe' -PathType Leaf + } + Chk 'vcpkg.exe present' { + Test-Path 'C:\BuildTools\vcpkg\vcpkg.exe' -PathType Leaf + } + # No leftover installer files in C:\CI root Chk 'no leftover installer scripts in C:\CI' { $leftovers = Get-ChildItem 'C:\CI' -File -EA SilentlyContinue | @@ -347,6 +373,14 @@ try { Chk 'MSBuild.exe present' { $msb=Get-Command msbuild -EA SilentlyContinue; $msb -and (Test-Path $msb.Source) } Chk 'Git for Windows (git.exe in PATH)' { Get-Command git -ErrorAction SilentlyContinue } Chk '7-Zip (7z.exe in PATH)' { Get-Command 7z -ErrorAction SilentlyContinue } + Chk 'PowerShell 7 (pwsh.exe in PATH)' { Get-Command pwsh -ErrorAction SilentlyContinue } + Chk 'NSIS (makensis.exe in PATH)' { Get-Command makensis -ErrorAction SilentlyContinue } + Chk 'CMake (cmake.exe in PATH)' { Get-Command cmake -ErrorAction SilentlyContinue } + Chk 'Node.js (node.exe in PATH)' { Get-Command node -ErrorAction SilentlyContinue } + Chk 'WiX Toolset (wix.exe in PATH)' { Get-Command wix -ErrorAction SilentlyContinue } + Chk 'GitHub CLI (gh.exe in PATH)' { Get-Command gh -ErrorAction SilentlyContinue } + Chk 'Sysinternals PsExec.exe present' { Test-Path 'C:\BuildTools\Sysinternals\PsExec.exe' -PathType Leaf } + Chk 'vcpkg.exe present' { Test-Path 'C:\BuildTools\vcpkg\vcpkg.exe' -PathType Leaf } Chk 'no leftover installer scripts in C:\CI' { $l=Get-ChildItem 'C:\CI' -File -EA SilentlyContinue | Where-Object { $_.Name -match 'wu_worker|wu_result|vs_buildtools|dotnet-install' }; $l.Count -eq 0 } return $results.ToArray() } -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion