diff --git a/src/ci_orchestrator/commands/template.py b/src/ci_orchestrator/commands/template.py index d4741d2..8047f2d 100644 --- a/src/ci_orchestrator/commands/template.py +++ b/src/ci_orchestrator/commands/template.py @@ -523,6 +523,8 @@ def _run_with_heartbeat( @click.option("--skip-windows-update", is_flag=True, help="Pass -SkipWindowsUpdate to toolchain script.") @click.option("--skip-tier2", is_flag=True, help="Pass -SkipTier2 to toolchain script.") @click.option("--skip-cleanup", is_flag=True, help="Pass -SkipCleanup to toolchain script.") +@click.option("--skip-trim", is_flag=True, + help="Skip removing unused features/roles/Edge before the final reboot.") @click.option("--winrm-timeout", default=300, show_default=True, help="Seconds to wait for WinRM.") @click.option("--auth", default="basic", show_default=True, type=click.Choice(["basic", "ntlm", "negotiate"]), @@ -558,6 +560,7 @@ def template_prepare_win( skip_windows_update: bool, skip_tier2: bool, skip_cleanup: bool, + skip_trim: bool, winrm_timeout: int, auth: str, store_credential: bool, @@ -832,6 +835,61 @@ Register-ScheduledTask -TaskName 'CI-StaticIp' ` f"[prepare-win] Validation script error:\n{val_result.stderr}", err=True ) + # Heavy trim BEFORE disk cleanup: remove roles/capabilities/Edge that a + # build VM never uses. This is CBS/installer-heavy, so it runs here under + # the cmd shell (never under explorer at first logon — that is what the + # post-install script deliberately leaves out). -NoRestart defers the + # single reboot to the final one below; the DISM cleanup that follows + # then compacts the component store the removals leave behind. + if not skip_trim: + click.echo("[prepare-win] Trimming unused features / Edge (heavy) ...") + trim_invoke = ( + "$ErrorActionPreference = 'Continue'\n" + # Server roles/optional features no build needs. + "foreach ($f in 'WindowsAdminCenterSetup','XPS-Viewer'," + "'Wireless-Networking','Windows-Defender') {\n" + " $wf = Get-WindowsFeature -Name $f -ErrorAction SilentlyContinue\n" + " if ($wf -and $wf.Installed) {\n" + " Uninstall-WindowsFeature -Name $f -NoRestart " + "-ErrorAction SilentlyContinue | Out-Null\n" + " \"trimmed feature: $f\"\n" + " }\n" + "}\n" + # Azure Arc Setup capability (the auto-start was already disabled + # in post-install; this removes the payload entirely). + "Get-WindowsCapability -Online -ErrorAction SilentlyContinue |\n" + " Where-Object { $_.Name -like 'AzureArcSetup*' -and " + "$_.State -eq 'Installed' } |\n" + " ForEach-Object { Remove-WindowsCapability -Online -Name $_.Name " + "-ErrorAction SilentlyContinue | Out-Null; \"trimmed capability: $($_.Name)\" }\n" + # Edge + EdgeWebView2 (and their background updater services/tasks). + "foreach ($e in @(\n" + " @{ Glob='C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\*\\Installer\\setup.exe';" + " Args=@('--uninstall','--msedge','--channel=stable','--system-level','--force-uninstall') },\n" + " @{ Glob='C:\\Program Files (x86)\\Microsoft\\EdgeWebView\\Application\\*\\Installer\\setup.exe';" + " Args=@('--uninstall','--msedgewebview','--system-level','--force-uninstall') }\n" + ")) {\n" + " $setup = Get-ChildItem -Path $e.Glob -ErrorAction SilentlyContinue |\n" + " Sort-Object FullName -Descending | Select-Object -First 1\n" + " if ($setup) {\n" + " & $setup.FullName @($e.Args) 2>&1 | Out-Null\n" + " \"trimmed: $($setup.FullName)\"\n" + " }\n" + "}\n" + # Edge auto-update services left behind if the uninstall is partial. + "foreach ($s in 'edgeupdate','edgeupdatem','MicrosoftEdgeElevationService') {\n" + " if (Get-Service -Name $s -ErrorAction SilentlyContinue) {\n" + " sc.exe stop $s 2>&1 | Out-Null; sc.exe config $s start= disabled 2>&1 | Out-Null\n" + " }\n" + "}\n" + '"CI_EXITCODE:0"\n' + ) + trim_result = _run_with_heartbeat(transport, trim_invoke, check=False) + display = _strip_exit_marker(trim_result.stdout).strip() + if display: + click.echo(display) + click.echo("[prepare-win] Feature / Edge trim complete.") + # Disk cleanup BEFORE the final reboot. Heavy servicing ops (DISM # StartComponentCleanup, WU finalization) destabilise the running shell # session — explorer crash-loops until the next reboot — so all such work diff --git a/src/ci_orchestrator/winimage/assets/post-install.win.ps1.tmpl b/src/ci_orchestrator/winimage/assets/post-install.win.ps1.tmpl index 97196f3..86de5c1 100644 --- a/src/ci_orchestrator/winimage/assets/post-install.win.ps1.tmpl +++ b/src/ci_orchestrator/winimage/assets/post-install.win.ps1.tmpl @@ -258,6 +258,51 @@ reg add 'HKLM\SOFTWARE\Microsoft\AzureArc' /v OnboardingState /t REG_DWORD /d 4 reg delete 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' /v AzureArcSetup /f 2>&1 | Out-Null L 'Azure Arc auto-start disabled' +# ── Trim waste services (light: no CBS, safe under explorer) ────────────── +# An ephemeral headless build VM needs no audio/camera/sync/push/telemetry/ +# search-index/DTC/link-tracking. All reversible via Set-Service. Heavy feature +# and Edge removal is done in prepare-win (under the cmd shell) to avoid +# destabilising the live explorer shell at first logon. +foreach ($svc in @( + 'Audiosrv','AudioEndpointBuilder','WpnService','camsvc','CDPSvc','DPS', + 'TrkWks','MSDTC','WSAIFabricSvc','InventorySvc','DiagTrack','Themes','WSearch', + 'MapsBroker','RetailDemo','PcaSvc','WdiServiceHost','WdiSystemHost' +)) { + if (Get-Service -Name $svc -ErrorAction SilentlyContinue) { + sc.exe stop $svc 2>&1 | Out-Null + sc.exe config $svc start= disabled 2>&1 | Out-Null + } +} +# Per-user template services (the WpnUserService_xxxx / CDPUserSvc_xxxx clones +# spawn from these templates) — disable the template so no per-session clone starts. +foreach ($base in @( + 'CDPUserSvc','OneSyncSvc','WpnUserService','cbdhsvc', + 'PimIndexMaintenanceSvc','UnistoreSvc','UserDataSvc','MessagingService' +)) { + $k = "HKLM\SYSTEM\CurrentControlSet\Services\$base" + reg query $k 1>$null 2>$null + if ($LASTEXITCODE -eq 0) { reg add $k /v Start /t REG_DWORD /d 4 /f 2>&1 | Out-Null } +} +# BITS is needed on demand by installers/WU; just stop it auto-running. +sc.exe config BITS start= demand 2>&1 | Out-Null +L 'Waste services disabled (audio/camera/sync/push/telemetry/search/DTC); BITS on-demand' + +# ── Strip in-box apps that the build VM never uses (light, per-app stubs) ── +# Server 2025 Desktop may or may not ship these; each is best-effort. +foreach ($pkgLike in 'Microsoft.WindowsFeedbackHub','Microsoft.GetHelp','Microsoft.Getstarted','Microsoft.MicrosoftOfficeHub') { + Get-AppxPackage -AllUsers | Where-Object { $_.Name -like "$pkgLike*" } | + Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue + Get-AppxProvisionedPackage -Online | + Where-Object { $_.DisplayName -like $pkgLike } | + ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue } | Out-Null +} +# Optional-feature "apps" exposing a /uninstall stub (Snip & Sketch, Paint, RDP client). +foreach ($app in 'SnippingTool','mspaint','mstsc') { + $exe = Join-Path $env:WINDIR "System32\$app.exe" + if (Test-Path $exe) { & $exe /uninstall 2>&1 | Out-Null } +} +L 'In-box apps trimmed (FeedbackHub/GetHelp/Getstarted/OfficeHub, Snip/Paint/mstsc)' + # ── Explorer UX: show file extensions + open to This PC ────────────────── reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v HideFileExt /t REG_DWORD /d 0 /f | Out-Null reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v LaunchTo /t REG_DWORD /d 1 /f | Out-Null