From 07a4f3e2ede5977135e323dd70c8d4e3b0e0e375 Mon Sep 17 00:00:00 2001 From: Simone Date: Sat, 9 May 2026 00:08:05 +0200 Subject: [PATCH] refactor: remove duplicate root Setup-TemplateVM.ps1 (canonical is template/) --- Setup-TemplateVM.ps1 | 659 ------------------------------------------- 1 file changed, 659 deletions(-) delete mode 100644 Setup-TemplateVM.ps1 diff --git a/Setup-TemplateVM.ps1 b/Setup-TemplateVM.ps1 deleted file mode 100644 index b854b5a..0000000 --- a/Setup-TemplateVM.ps1 +++ /dev/null @@ -1,659 +0,0 @@ -#Requires -RunAsAdministrator -#Requires -Version 5.1 -<# -.SYNOPSIS - Provisions a Windows VM as a CI build template (run INSIDE the VM once). - -.DESCRIPTION - This script is run ONE TIME inside the template VM before taking the - "BaseClean" snapshot. After the snapshot is taken, never modify the VM. - - Installs and configures: - - Windows Updates (all available, via COM API — no external module needed) - - WinRM with basic authentication (for PSSession from host) - - A dedicated local build user account - - Visual Studio Build Tools 2026 (MSBuild, MSVC, .NET desktop targets) - - .NET SDK 10.x - - Firewall rules for WinRM - - NOTE: Git is NOT installed. The host clones the repository and copies - the source tree into the VM via WinRM (Option B isolation model). - - ╔══════════════════════════════════════════════════════════════════════╗ - ║ NETWORK REQUIREMENT DURING SETUP ║ - ║ The VM must have internet access while this script runs so it can ║ - ║ download Windows Updates and tool installers. Configure the VM NIC ║ - ║ to VMnet8 (NAT) before running setup, then switch back to VMnet11 ║ - ║ (Host-Only) BEFORE taking the BaseClean snapshot. ║ - ╚══════════════════════════════════════════════════════════════════════╝ - - After this script completes: - 1. Switch VM NIC from VMnet8 (NAT) → VMnet11 (Host-Only) in VMware - Workstation: VM → Settings → Network Adapter → Custom: VMnet11 - 2. Shut down the VM (Start → Shut down) - 3. In VMware Workstation: VM → Snapshot → Take Snapshot - Name it exactly: BaseClean - 4. Power off the VM and leave it powered off permanently - -.PARAMETER SkipWindowsUpdate - Skip the Windows Update step (useful if updates were already applied). - -.NOTES - Invoke via Prepare-TemplateSetup.ps1 from the HOST (recommended), or - run manually from an elevated PowerShell session INSIDE the VM: - Set-ExecutionPolicy Bypass -Scope Process -Force - .\Setup-TemplateVM.ps1 -#> -[CmdletBinding()] -param( - # Local username for the CI build account inside the VM - [string] $BuildUsername = 'ci_build', - - # Password for the build account. - # IMPORTANT: Change this to a strong password and store it in - # Windows Credential Manager on the HOST as target "BuildVMGuest". - [string] $BuildPassword = 'CIBuild!ChangeMe2026', - - # .NET SDK channel to install (should match the host SDK version) - [string] $DotNetSdkVersion = '10.0', - - # Python version to install (x.y.z — must match an exact release on python.org) - [string] $PythonVersion = '3.13.3', - - # Skip Windows Update (useful when updates already applied or no internet) - [switch] $SkipWindowsUpdate -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Write-Step { - param([string]$Message) - Write-Host "`n=== $Message ===" -ForegroundColor Cyan -} - -# ── Step 0: Windows Update ──────────────────────────────────────────────────── -# NOTE: The Windows Update COM API (Microsoft.Update.Session) raises E_ACCESSDENIED -# when called from a WinRM/network-logon session. We work around this by running -# the actual download+install inside a Scheduled Task (SYSTEM account, full token). -if ($SkipWindowsUpdate) { - Write-Host "[Setup] Skipping Windows Update (SkipWindowsUpdate switch set)." -} -else { - Write-Step "Running Windows Update via Scheduled Task (avoids WinRM token restrictions)" - - # --- worker script that runs as SYSTEM inside the scheduled task --- - $wuWorkerPath = 'C:\CI\wu_worker.ps1' - $wuResultPath = 'C:\CI\wu_result.txt' - $wuRebootPath = 'C:\CI\wu_reboot.txt' - $wuLogPath = 'C:\CI\wu_log.txt' - - $wuWorker = @' -$ErrorActionPreference = 'Stop' -try { - $s = New-Object -ComObject Microsoft.Update.Session - $q = $s.CreateUpdateSearcher() - $found = $q.Search("IsInstalled=0 and Type='Software' and IsHidden=0") - $count = $found.Updates.Count - if ($count -eq 0) { - "0" | Set-Content C:\CI\wu_result.txt - "False" | Set-Content C:\CI\wu_reboot.txt - "No updates needed." | Set-Content C:\CI\wu_log.txt - } else { - "Downloading $count update(s)..." | Set-Content C:\CI\wu_log.txt - $dl = $s.CreateUpdateDownloader() - $dl.Updates = $found.Updates - $dl.Download() | Out-Null - "Installing $count update(s)..." | Add-Content C:\CI\wu_log.txt - $inst = $s.CreateUpdateInstaller() - $inst.Updates = $found.Updates - $r = $inst.Install() - [string]$r.ResultCode | Set-Content C:\CI\wu_result.txt - [string]$r.RebootRequired | Set-Content C:\CI\wu_reboot.txt - "Done. ResultCode=$($r.ResultCode) RebootRequired=$($r.RebootRequired)" | Add-Content C:\CI\wu_log.txt - } -} catch { - "ERROR: $_" | Set-Content C:\CI\wu_log.txt - "99" | Set-Content C:\CI\wu_result.txt - "False" | Set-Content C:\CI\wu_reboot.txt -} -'@ - $wuWorker | Set-Content -Path $wuWorkerPath -Encoding UTF8 - - # Remove any leftover result files from a previous run - Remove-Item $wuResultPath, $wuRebootPath -ErrorAction SilentlyContinue - - # Register scheduled task running as SYSTEM - $taskName = 'CI-WindowsUpdate' - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` - -Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$wuWorkerPath`"" - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest - Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null - - Write-Host "Scheduled task registered. Starting Windows Update..." - Start-ScheduledTask -TaskName $taskName - - # Poll until the task finishes (result file written = task complete) - $timeout = [datetime]::UtcNow.AddMinutes(60) - $dotCount = 0 - while (-not (Test-Path $wuResultPath)) { - if ([datetime]::UtcNow -gt $timeout) { - throw "Windows Update timed out after 60 minutes." - } - Start-Sleep -Seconds 15 - $dotCount++ - if ($dotCount % 4 -eq 0) { Write-Host " ... still running ($(([datetime]::UtcNow - ($timeout.AddMinutes(-60))).Minutes) min elapsed)" } - } - - # Read results - $resultCode = [int](Get-Content $wuResultPath -Raw).Trim() - $rebootNeeded = (Get-Content $wuRebootPath -Raw).Trim() -eq 'True' - $log = Get-Content $wuLogPath -Raw -ErrorAction SilentlyContinue - - Write-Host "Windows Update log:" - Write-Host $log - - # Clean up - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - Remove-Item $wuWorkerPath, $wuResultPath, $wuRebootPath, $wuLogPath -ErrorAction SilentlyContinue - - if ($resultCode -eq 99) { - throw "Windows Update worker script failed. See log above." - } - if ($resultCode -notin @(0, 2, 3)) { - Write-Warning "Windows Update returned unexpected result code: $resultCode" - } - else { - Write-Host "Windows Update completed (ResultCode=$resultCode)." - } - - if ($rebootNeeded) { - Write-Warning "*** REBOOT REQUIRED to finish installing updates. ***" - Write-Warning " Restart the VM, then re-run this script with: -SkipWindowsUpdate" - exit 3010 - } -} - - -# ── Step 1: WinRM ───────────────────────────────────────────────────────────── -Write-Step "Configuring WinRM" - -Enable-PSRemoting -Force -SkipNetworkProfileCheck 3>$null | Out-Null - -# Allow unencrypted (HTTP/5985) — acceptable for an isolated lab network. -# Migrate to HTTPS/5986 with a self-signed cert for hardened environments. -winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null -winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null - -# Increase shell memory and timeout limits for long builds -winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="2048"}' | Out-Null -winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null - -Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>$null -Set-Item WSMan:\localhost\Shell\IdleTimeOut 7200000 -Force 3>$null # 2 hours - -# Ensure WinRM starts automatically -Set-Service -Name WinRM -StartupType Automatic 3>$null -Start-Service WinRM 3>$null - -Write-Host "WinRM configured." - -# ── Step 2: Firewall ────────────────────────────────────────────────────────── -Write-Step "Disabling Windows Firewall (all profiles) — isolated lab VM" - -# Disable all firewall profiles entirely. This VM lives on a Host-Only network -# (VMnet11) with no external exposure, so full disable is acceptable and avoids -# any rule-order or profile-classification issues that would block WinRM/ICMP. -Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False -Write-Host "Windows Firewall disabled on all profiles." - -# ── Step 3: Build user account ──────────────────────────────────────────────── -Write-Step "Creating build user account: $BuildUsername" - -$existingUser = Get-LocalUser -Name $BuildUsername -ErrorAction SilentlyContinue -if ($existingUser) { - Write-Host "User '$BuildUsername' already exists — skipping creation." -} -else { - # Use net user instead of New-LocalUser: the LocalAccounts cmdlets have a known - # bug where SecureString deserialized over WinRM causes InvalidPasswordException. - $netResult = & net user $BuildUsername $BuildPassword /add /y 2>&1 - if ($LASTEXITCODE -ne 0) { - throw "Failed to create user '$BuildUsername': $netResult" - } - Write-Host "User '$BuildUsername' created." -} - -# Always ensure password policy and Administrators membership (idempotent) -& net user $BuildUsername /passwordchg:no | Out-Null -Set-LocalUser -Name $BuildUsername -PasswordNeverExpires $true - -$isMember = & net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername -if (-not $isMember) { - & net localgroup Administrators $BuildUsername /add | Out-Null - Write-Host "User '$BuildUsername' added to Administrators." -} -else { - Write-Host "User '$BuildUsername' already in Administrators." -} - -# ── Step 3b: Disable UAC ────────────────────────────────────────────────────── -Write-Step "Disabling UAC (isolated lab VM)" - -# EnableLUA=0 disables UAC entirely — admin processes get full token without prompt. -# LocalAccountTokenFilterPolicy=1 ensures remote admin connections (WinRM) also -# get an elevated token (avoids the filtered-token issue with runProgramInGuest). -Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' ` - -Name EnableLUA -Value 0 -Type DWord -Force -Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' ` - -Name LocalAccountTokenFilterPolicy -Value 1 -Type DWord -Force -Write-Host "UAC disabled." - -# ── Step 4: CI working directories ─────────────────────────────────────────── -Write-Step "Creating CI working directories" - -foreach ($dir in @('C:\CI\build', 'C:\CI\output', 'C:\CI\scripts')) { - if (-not (Test-Path $dir)) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null - Write-Host "Created: $dir" - } -} - -# ── Step 4b: Explorer settings ─────────────────────────────────────────────── -Write-Step "Configuring Explorer defaults" - -# Open Explorer to 'This PC' (Devices and Drives) instead of Quick Access. -# LaunchTo=1 = This PC, LaunchTo=2 = Quick Access (default) -$explorerAdvKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Set-ItemProperty -Path $explorerAdvKey -Name 'LaunchTo' -Value 1 -Type DWord -Force - -# Also apply to the Default User profile so ci_build and any new user inherits it. -$defaultHive = 'C:\Users\Default\NTUSER.DAT' -$mountName = 'TempDefaultUser' -if (Test-Path $defaultHive) { - reg load "HKU\$mountName" $defaultHive | Out-Null - $defKey = "Registry::HKU\$mountName\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" - if (-not (Test-Path $defKey)) { New-Item -Path $defKey -Force | Out-Null } - Set-ItemProperty -Path $defKey -Name 'LaunchTo' -Value 1 -Type DWord -Force - [gc]::Collect() - reg unload "HKU\$mountName" | Out-Null - Write-Host "Explorer: 'This PC' set for current user and default profile." -} -else { - Write-Host "Explorer: 'This PC' set for current user (default hive not found)." -} - -# ── Step 5: Disable Windows Defender ──────────────────────────────────────── -Write-Step "Disabling Windows Defender real-time protection" - -# Defender scanning significantly slows compilation and NuGet restore. -# On an isolated CI template VM this trade-off is acceptable. -$defSvc = Get-Service -Name WinDefend -ErrorAction SilentlyContinue -if ($defSvc) { - Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue 3>$null - Set-MpPreference -DisableBehaviorMonitoring $true -ErrorAction SilentlyContinue 3>$null - Set-MpPreference -DisableIOAVProtection $true -ErrorAction SilentlyContinue 3>$null - Set-MpPreference -DisableScriptScanning $true -ErrorAction SilentlyContinue 3>$null - # Exclude CI build paths from any remaining scanning - Add-MpPreference -ExclusionPath 'C:\CI' -ErrorAction SilentlyContinue 3>$null - Add-MpPreference -ExclusionPath 'C:\dotnet' -ErrorAction SilentlyContinue 3>$null - Add-MpPreference -ExclusionPath 'C:\Python' -ErrorAction SilentlyContinue 3>$null - Write-Host "Windows Defender real-time protection disabled." -} -else { - Write-Host "Windows Defender service not found — skipping." -} - -# Disable via Group Policy registry key — survives Defender auto-updates and reboots. -# This is the same key that SCCM/GPO uses in enterprise environments. -$defenderKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' -if (-not (Test-Path $defenderKey)) { - New-Item -Path $defenderKey -Force | Out-Null -} -Set-ItemProperty -Path $defenderKey -Name 'DisableAntiSpyware' -Value 1 -Type DWord -Force -Set-ItemProperty -Path $defenderKey -Name 'DisableAntiVirus' -Value 1 -Type DWord -Force -# Also disable via the Windows Security Center policy subkey -$rtKey = "$defenderKey\Real-Time Protection" -if (-not (Test-Path $rtKey)) { - New-Item -Path $rtKey -Force | Out-Null -} -Set-ItemProperty -Path $rtKey -Name 'DisableRealtimeMonitoring' -Value 1 -Type DWord -Force -Set-ItemProperty -Path $rtKey -Name 'DisableBehaviorMonitoring' -Value 1 -Type DWord -Force -Set-ItemProperty -Path $rtKey -Name 'DisableIOAVProtection' -Value 1 -Type DWord -Force -Set-ItemProperty -Path $rtKey -Name 'DisableScriptScanning' -Value 1 -Type DWord -Force -Write-Host "Defender GPO registry keys written (persistent across updates)." - -# ── Step 6: Install .NET SDK ───────────────────────────────────────────────── -Write-Step "Installing .NET SDK $DotNetSdkVersion" - -$dotnetInstalled = Get-Command dotnet -ErrorAction SilentlyContinue -if ($dotnetInstalled) { - $installedVersion = dotnet --version - Write-Host ".NET SDK already installed: $installedVersion" -} -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 - - Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..." - & $dotnetInstallScript ` - -Channel $DotNetSdkVersion ` - -InstallDir 'C:\dotnet' ` - -NoPath - - # Add to system PATH - $currentPath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') - if ($currentPath -notlike '*C:\dotnet*') { - [System.Environment]::SetEnvironmentVariable( - 'PATH', - "$currentPath;C:\dotnet", - 'Machine' - ) - } - $env:PATH = $env:PATH + ';C:\dotnet' - - Write-Host ".NET SDK installed: $(dotnet --version)" -} - -# ── Step 7: Install Python ─────────────────────────────────────────────────── -Write-Step "Installing Python $PythonVersion" - -$pythonExe = 'C:\Python\python.exe' -if (Test-Path $pythonExe) { - Write-Host "Python already installed: $(& $pythonExe --version 2>&1)" -} -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 - - Write-Host "Installing Python $PythonVersion (all users, prepend PATH)..." - $pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @( - '/quiet', - 'InstallAllUsers=1', - 'PrependPath=1', - 'Include_test=0', - 'Include_doc=0', - 'TargetDir=C:\Python' - ) -Wait -PassThru - - Remove-Item $pyInstallerPath -ErrorAction SilentlyContinue - - if ($pyProc -and $pyProc.ExitCode -ne 0) { - throw "Python installation failed (exit $($pyProc.ExitCode))." - } - - # Refresh PATH for subsequent steps - $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + - [System.Environment]::GetEnvironmentVariable('PATH', 'User') - - Write-Host "Python installed: $(python --version 2>&1)" -} - -# ── Step 8: Install Visual Studio Build Tools 2022 ─────────────────────────── -Write-Step "Installing Visual Studio Build Tools 2026" - -$msbuildPath = 'C:\Program Files (x86)\Microsoft Visual Studio\2026\BuildTools\MSBuild\Current\Bin\MSBuild.exe' -if (Test-Path $msbuildPath) { - Write-Host "VS Build Tools 2026 already installed." -} -else { - $vsInstallerUrl = 'https://aka.ms/vs/stable/vs_buildtools.exe' - $vsInstallerPath = 'C:\CI\vs_BuildTools.exe' - $vsResultPath = 'C:\CI\vs_result.txt' - $vsLogPath = 'C:\CI\vs_log.txt' - - Write-Host "Downloading VS Build Tools installer..." - Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy - Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing - - # Remove Zone.Identifier ADS — files downloaded from internet are marked Zone 3 (Internet). - # SYSTEM account cannot execute them without this unblock step. - Unblock-File -Path $vsInstallerPath -ErrorAction SilentlyContinue - - # Verify the download produced a valid Windows PE file (MZ header) - $mzBytes = [System.IO.File]::ReadAllBytes($vsInstallerPath) | Select-Object -First 2 - if (-not ($mzBytes[0] -eq 0x4D -and $mzBytes[1] -eq 0x5A)) { - $fileSize = (Get-Item $vsInstallerPath).Length - throw "VS installer download appears invalid (size=$fileSize, not an EXE). Check the URL: $vsInstallerUrl" - } - Write-Host "Installer verified OK ($((Get-Item $vsInstallerPath).Length) bytes)." - - # VS installer fails under WinRM (network logon token). Run via Scheduled Task as SYSTEM. - $vsWorkerPath = 'C:\CI\vs_worker.ps1' - $vsWorker = @" -# SYSTEM account may have no TEMP set — use C:\Windows\Temp -`$env:TEMP = 'C:\Windows\Temp' -`$env:TMP = 'C:\Windows\Temp' - -# Clean any corrupt VS installer cache left by previous failed attempts -Remove-Item 'C:\ProgramData\Microsoft\VisualStudio\Packages' -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item 'C:\Windows\Temp\vs*' -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item 'C:\Windows\Temp\dd_*' -Force -ErrorAction SilentlyContinue - -`$result = try { - `$proc = Start-Process -FilePath '$vsInstallerPath' -ArgumentList @( - '--quiet','--wait','--norestart','--nocache', - '--installPath "C:\Program Files (x86)\Microsoft Visual Studio\2026\BuildTools"', - '--add','Microsoft.VisualStudio.Workload.VCTools', - '--includeRecommended', - '--add','Microsoft.VisualStudio.Workload.MSBuildTools', - '--add','Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools', - '--add','Microsoft.VisualStudio.Component.NuGet.BuildTools', - '--add','Microsoft.Net.Component.4.8.SDK', - '--add','Microsoft.Net.Component.4.7.2.TargetingPack' - ) -Wait -PassThru - if (`$proc) { [string]`$proc.ExitCode } else { '99' } -} catch { - "99: `$_" -} -if (-not `$result) { `$result = '99' } -`$result | Set-Content '$vsResultPath' -Encoding UTF8 -"@ - $vsWorker | Set-Content -Path $vsWorkerPath -Encoding UTF8 - - Remove-Item $vsResultPath -ErrorAction SilentlyContinue - - $taskName = 'CI-VSBuildTools' - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` - -Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$vsWorkerPath`"" - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest - Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null - - Write-Host "Installing VS Build Tools via Scheduled Task (this may take 15-30 minutes)..." - Start-ScheduledTask -TaskName $taskName - - $timeout = [datetime]::UtcNow.AddMinutes(60) - $dots = 0 - while (-not (Test-Path $vsResultPath)) { - if ([datetime]::UtcNow -gt $timeout) { throw "VS Build Tools installation timed out." } - # If task already finished without writing the result file, the worker itself crashed - $taskInfo = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue - if ($taskInfo -and $taskInfo.State -eq 'Ready') { - $lastResult = (Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue).LastTaskResult - throw "VS worker task exited prematurely (LastTaskResult=0x$('{0:X}' -f $lastResult)). Check C:\CI\vs_worker.ps1 for syntax errors." - } - Start-Sleep -Seconds 20 - $dots++ - if ($dots % 3 -eq 0) { Write-Host " ... still installing ($(([datetime]::UtcNow - ($timeout.AddMinutes(-60))).Minutes) min elapsed)" } - } - - $rawResult = Get-Content $vsResultPath -Raw -ErrorAction SilentlyContinue - $rawResult = if ($rawResult) { $rawResult.Trim() } else { '99' } - # Result file may contain "0", "3010", or "99: " - $vsExit = if ($rawResult -match '^\d+$') { [int]$rawResult } else { 99 } - $vsMsg = if ($vsExit -eq 99) { $rawResult } else { '' } - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - Remove-Item $vsWorkerPath, $vsResultPath, $vsInstallerPath -ErrorAction SilentlyContinue - - if ($vsExit -notin @(0, 3010)) { - throw "VS Build Tools installation failed (exit $vsExit). $vsMsg" - } - Write-Host "VS Build Tools 2026 installed (exit code $vsExit)." -} - -# Add MSBuild to system PATH -$msbuildDir = Split-Path $msbuildPath -Parent -$currentPath = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') -if ($currentPath -notlike "*$msbuildDir*") { - [System.Environment]::SetEnvironmentVariable( - 'PATH', - "$currentPath;$msbuildDir", - 'Machine' - ) - Write-Host "MSBuild added to system PATH." -} - -# ── Step 9: Verify toolchain ─────────────────────────────────────────────── -Write-Step "Verifying toolchain" - -$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + - [System.Environment]::GetEnvironmentVariable('PATH', 'User') - -$allOk = $true - -# Helper: resolve a binary — tries hardcoded path first, then Get-Command (PATH) -function Resolve-Tool { - param([string]$HardcodedPath, [string]$CommandName) - if ($HardcodedPath -and (Test-Path $HardcodedPath -PathType Leaf)) { - return $HardcodedPath - } - $found = Get-Command $CommandName -ErrorAction SilentlyContinue - if ($found) { return $found.Source } - return $null -} - -# dotnet -$dotnetBin = Resolve-Tool '' 'dotnet' -if ($dotnetBin) { - $v = & $dotnetBin --version 2>&1 | Select-Object -First 1 - Write-Host " [OK] dotnet: $v ($dotnetBin)" -} else { - Write-Warning " [FAIL] dotnet not found" - $allOk = $false -} - -# python -$pythonBin = Resolve-Tool 'C:\Python\python.exe' 'python' -if ($pythonBin) { - $v = & $pythonBin --version 2>&1 | Select-Object -First 1 - Write-Host " [OK] python: $v ($pythonBin)" -} else { - Write-Warning " [FAIL] python not found" - $allOk = $false -} - -# msbuild -$msbuildBin = Resolve-Tool $msbuildPath 'msbuild' -if ($msbuildBin) { - $v = & $msbuildBin -version 2>&1 | Select-Object -First 1 - Write-Host " [OK] msbuild: $v ($msbuildBin)" -} else { - Write-Warning " [FAIL] msbuild not found at '$msbuildPath' and not in PATH" - $allOk = $false -} - -if (-not $allOk) { - Write-Warning "Some tools failed verification. Review above and rerun if needed." -} - -# ── Cleanup ─────────────────────────────────────────────────────────────────── -Write-Step "Cleaning up disk" - -# 1. Windows Disk Cleanup (cleanmgr) — run with /sagerun:1 using preset flags -# First register the flags via /sageset:1 in the registry (silent, no UI) -$cleanKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches' -$sageset = 1 -$cleanCategories = @( - 'Active Setup Temp Folders', - 'BranchCache', - 'Downloaded Program Files', - 'Internet Cache Files', - 'Memory Dump Files', - 'Old ChkDsk Files', - 'Previous Installations', - 'Recycle Bin', - 'Service Pack Cleanup', - 'Setup Log Files', - 'System error memory dump files', - 'System error minidump files', - 'Temporary Files', - 'Temporary Setup Files', - 'Thumbnail Cache', - 'Update Cleanup', - 'Upgrade Discarded Files', - 'Windows Defender', - 'Windows Error Reporting Archive Files', - 'Windows Error Reporting Files', - 'Windows Error Reporting Queue Files', - 'Windows Error Reporting Temp Files', - 'Windows ESD installation files', - 'Windows Upgrade Log Files' -) -foreach ($cat in $cleanCategories) { - $keyPath = "$cleanKey\$cat" - if (Test-Path $keyPath) { - Set-ItemProperty -Path $keyPath -Name "StateFlags$('{0:D4}' -f $sageset)" -Value 2 -Type DWord -Force -ErrorAction SilentlyContinue - } -} -Write-Host "Running cleanmgr /sagerun:$sageset (may take a few minutes)..." -$proc = Start-Process -FilePath 'cleanmgr.exe' -ArgumentList "/sagerun:$sageset" -Wait -PassThru -Write-Host "cleanmgr exited (code $($proc.ExitCode))." - -# 2. Clear Windows Update cache -Write-Host "Stopping Windows Update service..." -Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue -Remove-Item 'C:\Windows\SoftwareDistribution\Download\*' -Recurse -Force -ErrorAction SilentlyContinue -Start-Service -Name wuauserv -ErrorAction SilentlyContinue -Write-Host "Windows Update download cache cleared." - -# 3. Clear CBS / component store logs -Remove-Item 'C:\Windows\Logs\CBS\*' -Force -ErrorAction SilentlyContinue - -# 4. Clear TEMP folders -Remove-Item 'C:\Windows\Temp\*' -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue - -# 5. Clear Prefetch -Remove-Item 'C:\Windows\Prefetch\*' -Force -ErrorAction SilentlyContinue - -# 6. Run DISM component store cleanup (removes superseded update components) -Write-Host "Running DISM component store cleanup (StartComponentCleanup)..." -$dism = Start-Process -FilePath 'dism.exe' ` - -ArgumentList '/Online','/Cleanup-Image','/StartComponentCleanup','/ResetBase' ` - -Wait -PassThru -Write-Host "DISM exited (code $($dism.ExitCode))." - -Write-Host "Disk cleanup complete." - -# ── Done ────────────────────────────────────────────────────────────────────── -Write-Host "" -Write-Host "============================================================" -ForegroundColor Green -Write-Host " Template VM setup complete!" -ForegroundColor Green -Write-Host "============================================================" -ForegroundColor Green -Write-Host "" -Write-Host " MANDATORY NEXT STEPS:" -Write-Host "" -Write-Host " 1. SWITCH NIC TO HOST-ONLY (critical before snapshot!):" -Write-Host " VMware Workstation -> VM -> Settings -> Network Adapter" -Write-Host " Change from: VMnet8 (NAT)" -Write-Host " Change to: Custom: VMnet11" -Write-Host "" -Write-Host " 2. Shut down this VM: Start -> Shut down" -Write-Host "" -Write-Host " 3. Take snapshot in VMware Workstation:" -Write-Host " VM -> Snapshot -> Take Snapshot" -Write-Host " Name it EXACTLY: BaseClean" -Write-Host "" -Write-Host " 4. Leave VM powered off permanently." -Write-Host "" -Write-Host " 5. On the HOST - store credentials in Windows Credential Manager:" -Write-Host " New-StoredCredential -Target 'BuildVMGuest' -UserName '$BuildUsername' ``" -Write-Host " -Password '$BuildPassword' -Persist LocalMachine" -Write-Host " (Run in an elevated PowerShell with CredentialManager module)" -Write-Host ""