feat(template): cleanup-after-update + ruff fixes

Move disk cleanup out of the Windows Update loop:
- PS scripts: extract Invoke-DiskCleanup function, add -CleanupOnly switch
  for standalone post-update cleanup pass.
- Python command: always pass -SkipCleanup:$true during WU loop; after
  loop exits 0, run a separate -CleanupOnly invocation (skipped if
  --skip-cleanup). Fails fast if cleanup-only returns nonzero.
- Tests: 3 new tests for cleanup-runs-after-loop, skip-cleanup suppresses
  it, and cleanup-failure propagates as ClickException.
- ruff --fix: remove unused shutil import, sort imports, fix UP037/UP035.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 02:41:15 +02:00
parent d5f362b8dc
commit f96b9953c5
6 changed files with 356 additions and 225 deletions
+112 -107
View File
@@ -148,6 +148,11 @@ param(
# Skip disk cleanup step (cleanmgr + DISM + cache clear) — speeds up re-runs during dev/debug
[switch] $SkipCleanup,
# Run disk cleanup only — skip all provisioning steps and jump straight to the
# cleanup section. Intended for use by Prepare-WinBuild2025.ps1 after the Windows
# Update loop completes, so cleanup always runs on the fully-updated system.
[switch] $CleanupOnly,
# Administrator password — used only to write DefaultPassword for autologin.
# Optional: if empty, DefaultPassword registry value is left as-is (Deploy may have set it).
[string] $AdminPassword = '',
@@ -231,6 +236,97 @@ function Assert-Hash {
Write-Host " [OK] SHA256 verified: $Label" -ForegroundColor Green
}
function Invoke-DiskCleanup {
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 WU-adjacent services before cache clear..."
foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') {
Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue
}
$sdDownload = 'C:\Windows\SoftwareDistribution\Download'
Remove-Item $sdDownload -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $sdDownload -Force | Out-Null
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."
# DISM / WaaSMedicSvc may reset WU service StartType — re-enforce Disabled.
foreach ($svc in 'wuauserv', 'UsoSvc') {
Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue
}
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv' `
-Name Start -Value 4 -Type DWord -Force -ErrorAction SilentlyContinue
# Report remaining WU cache size (WaaSMedicSvc restores files — informational only).
$sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -File -Force -ErrorAction SilentlyContinue
$sdBytes = [long]0
if ($sdFiles) { foreach ($f in $sdFiles) { $sdBytes += $f.Length } }
$sdMB = [math]::Round($sdBytes / 1MB, 1)
Write-Host " SoftwareDistribution\Download: $(@($sdFiles).Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)."
Assert-Step 'Cleanup' 'wuauserv StartType Disabled after cache clear' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
}
}
# ── Pinned SHA256 hashes for downloaded installers (§1.3) ─────────────────────
# Fill in the hash values below. Get them by:
# (Invoke-WebRequest '<URL>' -UseBasicParsing -OutFile 'tmp.bin'; (Get-FileHash 'tmp.bin' -Algorithm SHA256).Hash)
@@ -272,6 +368,20 @@ $script:Hashes = @{
# vcpkg: cloned from GitHub via TLS; pin integrity with $VcpkgRef (commit SHA)
}
# ── CleanupOnly early-exit ────────────────────────────────────────────────────
# Called by Prepare-WinBuild2025.ps1 after the Windows Update loop completes.
# Skips all provisioning steps; runs only disk cleanup then exits.
if ($CleanupOnly) {
if ($SkipCleanup) {
Write-Host "[Setup] -CleanupOnly + -SkipCleanup: nothing to do." -ForegroundColor Yellow
exit 0
}
Write-Host "[Setup] CleanupOnly mode — running disk cleanup on fully-updated system." -ForegroundColor Cyan
Invoke-DiskCleanup
Write-Host "[Setup] CleanupOnly complete." -ForegroundColor Green
exit 0
}
# ── Pre-flight: remove temp files from any previous partial run ───────────────
# These files are normally deleted at the end of each step that creates them.
# A previous interrupted run may have left them behind — remove before starting.
@@ -1527,114 +1637,9 @@ Write-Host "Tier-2 Toolchain installation complete." -ForegroundColor Green
# ── Cleanup ───────────────────────────────────────────────────────────────────
if ($SkipCleanup) {
Write-Host "`n[Setup] Skipping disk cleanup (-SkipCleanup switch set)." -ForegroundColor Yellow
} else {
Invoke-DiskCleanup
}
else {
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
# Stop all services that might hold file locks inside SoftwareDistribution:
# wuauserv — Windows Update (already Disabled from Step 6b, stop is idempotent)
# UsoSvc — Update Orchestrator (already Disabled from Step 6b)
# bits — Background Intelligent Transfer Service (downloads WU payloads)
# dosvc — Delivery Optimization (peer-to-peer WU cache)
# TrustedInstaller — Windows Modules Installer (unpacks .cab/.msu update packages)
Write-Host "Stopping WU-adjacent services before cache clear..."
foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') {
Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue
}
# Delete and recreate the Download folder — more reliable than 'Remove-Item *'
# because locked files inside subdirectories silently fail with wildcard delete.
$sdDownload = 'C:\Windows\SoftwareDistribution\Download'
Remove-Item $sdDownload -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $sdDownload -Force | Out-Null
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."
# DISM StartComponentCleanup / WaaSMedicSvc may reset service StartType during cleanup.
# Re-enforce Disabled on both WU services after DISM completes.
foreach ($svc in 'wuauserv', 'UsoSvc') {
Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue
}
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv' `
-Name Start -Value 4 -Type DWord -Force -ErrorAction SilentlyContinue
# Validation — leftover artifacts from this run should be gone
# Note: do NOT assert SoftwareDistribution\Download empty.
# WaaSMedicSvc (Windows Update Medic Service) is tamper-protected — cannot be stopped —
# and actively restores WU files immediately after deletion. Files reappearing here is
# normal and harmless: WU is permanently disabled by Step 6b (service Disabled + GPO keys).
# Report remaining size as informational only.
$sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -File -Force -ErrorAction SilentlyContinue
# Avoid Measure-Object .Sum: under Set-StrictMode -Version Latest, PS 5.1 treats the
# nullable Sum property as absent when the pipe is empty, throwing "property not found".
$sdBytes = [long]0
if ($sdFiles) { foreach ($f in $sdFiles) { $sdBytes += $f.Length } }
$sdMB = [math]::Round($sdBytes / 1MB, 1)
Write-Host " SoftwareDistribution\Download: $(@($sdFiles).Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)."
Assert-Step 'Cleanup' 'wuauserv StartType Disabled after cache clear' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
}
} # end if (-not $SkipCleanup)
# ── Pre-Final re-affirmation ──────────────────────────────────────────────────
# Steps 7-10 (installs, cleanup, DISM) can take hours. During that window background