feat(template): §6.6 Tier-2 toolchain PS7/NSIS/CMake/Node.js/WiX/gh CLI/Sysinternals/vcpkg

Step 12 in Setup-WinBuild2025.ps1 installs all 8 Tier-2 tools:
  - PowerShell 7.4.7 LTS (pwsh.exe)
  - NSIS 3.10 (makensis.exe)
  - CMake 3.31.6 (cmake.exe)
  - Node.js 22.14.0 LTS (node.exe + npm)
  - WiX 4.0.5 via dotnet tool (wix.exe, C:\BuildTools\Wix\)
  - GitHub CLI 2.67.0 (gh.exe)
  - Sysinternals Suite (C:\BuildTools\Sysinternals\, extracted via 7-Zip)
  - vcpkg HEAD shallow clone + bootstrap (C:\BuildTools\vcpkg\)

All tools added to Machine PATH; idempotent (binary-presence check).
Hash entries in \ ready for SHA256 pinning.
knownTempFiles extended; Final gate Assert-Step added for all 8 binaries.

Prepare-WinBuild2025.ps1: 7 new params + pass-through to Setup.
Validate-SetupState.ps1: Tier-2 checks in both main + re-run scriptblocks.
TODO.md: §6.6 [~] -> [x], Sprint 11 summary.
This commit is contained in:
Simone
2026-05-11 20:04:50 +02:00
parent f948f23f0f
commit 85e45c6674
4 changed files with 451 additions and 25 deletions
+20 -20
View File
@@ -1,6 +1,6 @@
# TODO — Local CI/CD System # TODO — Local CI/CD System
<!-- Last updated: 2026-05-11 — Post Sprint 10: §6.1 Linux Build VM e2e PASS (nsis7z.dll 2272 KB su Linux = parità Windows); In-VM Git Clone sezione chiusa (§3.3 COMPLETATO) --> <!-- Last updated: 2026-05-11 — Sprint 11: §6.6 Tier-2 Toolchain DONE (Pwsh7/NSIS/CMake/Node.js/WiX/gh CLI/Sysinternals/vcpkg — Step 12 in Setup-WinBuild2025) -->
> Documento unico di lavoro: roadmap, audit trail dei task completati, e backlog post-v1.0 > 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**: > con priorità e razionale. Le voci aperte sono raggruppate per area e marcate **P0..P3**:
@@ -18,9 +18,9 @@
--- ---
## Summary ## 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 | | 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] | §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] | §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 | | [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 | | [ ] | §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] | 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 | | [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): **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) 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) 2. **Security**Rifiori se condizioni cambiano; vedi sezione "Deferred (Home Lab)"
3. **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.5** — vCPU/RAM tuning per workload (P2, deferred — home lab optimization) 4. **§3.4** — Pre-warm pool di cloni (P3, deferred — profiling-dependent optimization)
5. **§3.4** — Pre-warm pool di cloni (P3, deferred — profiling-dependent optimization)
--- ---
## Setup & Infrastructure ## 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 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. `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 ### 6.6 [P2] [x] Toolchain Tier-1 + Tier-2 in Setup-WinBuild2025
File: [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1). 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. **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 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] **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 - [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 | | Tool | Versione | Razionale |
| ------------------- | ----------------------- | ------------------------------------------------------------------ | | ------------------- | ----------------------- | ------------------------------------------------------------------ |
| **Git for Windows** | 2.54.0.windows.1 | [x] Self-clone in VM (alt a host-zip-transfer), submodule fetch | | **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 | | **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 | | **PowerShell 7.x** | 7.4.7 (LTS) | [x] Più veloce di 5.1, parallel pipelines, operatori moderni |
| **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) | | **NSIS** | 3.10 | [x] Test build installer (es. `nsis-plugin-nsinnounp`) |
| **CMake** | latest | Build system cross-platform per progetti C/C++ nativi | | **CMake** | 3.31.6 | [x] Build system cross-platform per progetti C/C++ nativi |
| **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) | | **Node.js LTS** | 22.14.0 | [x] Frontend/Electron pipelines, JS toolchain (npm) |
| **WiX Toolset** | v4 stable | MSI building da .NET projects | | **WiX Toolset** | 4.0.5 (dotnet tool) | [x] MSI building da .NET projects |
| **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR | | **gh CLI** | 2.67.0 | [x] Upload artifact a release Gitea/GitHub, comment PR |
| **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky | | **Sysinternals** | latest (CDN Microsoft) | [x] Diagnostica process/handle quando build flaky |
| **vcpkg** | latest | Package manager C++ per dipendenze native | | **vcpkg** | HEAD (shallow clone) | [x] Package manager C++ per dipendenze native |
**Approccio**: **Approccio**:
- Pinning hash SHA256 per ogni installer (vedi §1.3) - Pinning hash SHA256 per ogni installer (vedi §1.3)
+16
View File
@@ -132,6 +132,15 @@ param(
[string] $DotNetSdkVersion = '10.0', [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, [switch] $SkipWindowsUpdate,
# Skip disk cleanup inside the VM (cleanmgr + DISM + cache clear) — speeds up re-runs # Skip disk cleanup inside the VM (cleanmgr + DISM + cache clear) — speeds up re-runs
@@ -367,6 +376,13 @@ try {
BuildUsername = $BuildUsername BuildUsername = $BuildUsername
DotNetSdkVersion = $DotNetSdkVersion DotNetSdkVersion = $DotNetSdkVersion
AdminPassword = $AdminPassword AdminPassword = $AdminPassword
Pwsh7Version = $Pwsh7Version
NSISVersion = $NSISVersion
CMakeVersion = $CMakeVersion
NodeJSVersion = $NodeJSVersion
WiXVersion = $WiXVersion
GhCliVersion = $GhCliVersion
VcpkgRef = $VcpkgRef
} }
if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true } if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true }
if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true } if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true }
+381 -5
View File
@@ -56,10 +56,14 @@
Step 11 Tier-1 Toolchain (Git for Windows + 7-Zip) §6.6 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 Installs to C:\Program Files (Git default) and C:\Program Files\7-Zip
Validation: git and 7z commands resolvable, executables present 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\<tool>\ and C:\BuildTools\<tool>\
Validation: each binary resolvable via Get-Command
Cleanup Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM Cleanup Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM
wuauserv already Disabled (Step 6b) not restarted after cache clear wuauserv already Disabled (Step 6b) not restarted after cache clear
Validation: wuauserv StartType Disabled 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 Throws if any check fails prevents snapshot of broken state
Step ordering rationale: Step ordering rationale:
@@ -74,9 +78,12 @@
WU disable (Step 6b) immediately after WU; snapshot captures WU permanently 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 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-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). 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 NETWORK REQUIREMENT DURING SETUP
@@ -147,7 +154,31 @@ param(
[string] $GitVersion = '2.54.0.windows.1', [string] $GitVersion = '2.54.0.windows.1',
# 7-Zip version (latest stable) # 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 Set-StrictMode -Version Latest
@@ -217,6 +248,21 @@ $script:Hashes = @{
# 7-Zip MSI installer from https://www.7-zip.org/download.html # 7-Zip MSI installer from https://www.7-zip.org/download.html
# Update $script:Hashes['SevenZip'] when $SevenZipVersion changes. # Update $script:Hashes['SevenZip'] when $SevenZipVersion changes.
'SevenZip' = '' 'SevenZip' = ''
# Tier-2 Toolchain installer hashes — fill in when pinning supply chain (see BEST-PRACTICES.md §1.3)
# PowerShell-<ver>-win-x64.msi from https://github.com/PowerShell/PowerShell/releases
'Pwsh7' = ''
# nsis-<ver>-setup.exe from https://prdownloads.sourceforge.net/nsis/
'NSIS' = ''
# cmake-<ver>-windows-x86_64.msi from https://github.com/Kitware/CMake/releases
'CMake' = ''
# node-v<ver>-x64.msi from https://nodejs.org/dist/v<ver>/
'NodeJS' = ''
# gh_<ver>_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 ─────────────── # ── Pre-flight: remove temp files from any previous partial run ───────────────
@@ -225,7 +271,10 @@ $script:Hashes = @{
$knownTempFiles = @( $knownTempFiles = @(
'C:\CI\wu_worker.ps1', 'C:\CI\wu_result.txt', 'C:\CI\wu_reboot.txt', 'C:\CI\wu_log.txt', '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\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) { foreach ($f in $knownTempFiles) {
if (Test-Path $f) { if (Test-Path $f) {
@@ -1098,6 +1147,315 @@ Assert-Step '7-Zip' '7z command resolvable' {
Write-Host "Tier-1 Toolchain installation complete." -ForegroundColor Green 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 = '<sha/tag>' → 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 ─────────────────────────────────────────────────────────────────── # ── Cleanup ───────────────────────────────────────────────────────────────────
if ($SkipCleanup) { if ($SkipCleanup) {
Write-Host "`n[Setup] Skipping disk cleanup (-SkipCleanup switch set)." -ForegroundColor Yellow 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\wu_worker.ps1') -and
-not (Test-Path 'C:\CI\vs_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\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' { Assert-Step 'Final' 'git command available' {
[bool](Get-Command git -ErrorAction SilentlyContinue) [bool](Get-Command git -ErrorAction SilentlyContinue)
@@ -1273,6 +1637,18 @@ Assert-Step 'Final' 'git command available' {
Assert-Step 'Final' '7z command available' { Assert-Step 'Final' '7z command available' {
[bool](Get-Command 7z -ErrorAction SilentlyContinue) [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 Write-Host "All pre-snapshot checks passed." -ForegroundColor Green
+34
View File
@@ -228,6 +228,32 @@ try {
Get-Command 7z -ErrorAction SilentlyContinue 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 # No leftover installer files in C:\CI root
Chk 'no leftover installer scripts in C:\CI' { Chk 'no leftover installer scripts in C:\CI' {
$leftovers = Get-ChildItem 'C:\CI' -File -EA SilentlyContinue | $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 '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 '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 '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 } 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() return $results.ToArray()
} -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion } -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion