feat(template): trim build-VM footprint (services, features, Edge, apps)

An ephemeral headless build VM carries a lot of dead weight. Split the trim
by cost so heavy servicing never runs under the live explorer shell (which
crash-loops during CBS churn at first logon):

post-install.win.ps1 (light, runs under explorer at first logon):
- disable waste services: audio, camera, CDP/sync, push (Wpn), telemetry
  (DiagTrack/InventorySvc), search index (WSearch), DPS/WDI, MSDTC, TrkWks,
  Themes, MapsBroker, RetailDemo, PcaSvc, WSAIFabricSvc; BITS -> on-demand
- disable per-user service templates (CDPUserSvc/OneSyncSvc/WpnUserService/...)
  so no per-session clone spawns
- remove in-box apps: FeedbackHub/GetHelp/Getstarted/OfficeHub (appx +
  provisioned), Snip & Sketch, Paint, RDP client (mstsc) — all best-effort

prepare-win (heavy, runs under cmd shell, before the final reboot so the
following DISM cleanup compacts the freed component store):
- Uninstall-WindowsFeature WindowsAdminCenterSetup, XPS-Viewer,
  Wireless-Networking, Windows-Defender (-NoRestart)
- Remove-WindowsCapability AzureArcSetup (payload; auto-start already off)
- uninstall Edge + EdgeWebView2 and disable their leftover updater services
- gated by --skip-trim

Server Manager auto-launch was already disabled (task + reg + GPO policy);
its exe is part of the OS and cannot be uninstalled. Defender was already
runtime-off; removing the feature drops the AV stack entirely.
This commit is contained in:
2026-06-10 00:34:28 +02:00
parent 1fb4f58575
commit 43ad5afd09
2 changed files with 103 additions and 0 deletions
+58
View File
@@ -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