Files
SilverMetal/windows/branding/Apply-Branding.ps1
sysadmin 6124448003
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m47s
fix(first-boot): branding-online parse crash (em-dash/encoding) + bootstrap cleanup task + recovery QR
Found by reading the unencrypted VM disk after run #7:
1. Online branding never ran: Apply-Branding.ps1 had a UTF-8 em-dash in a Write-Warning
   STRING; Windows PowerShell 5.1 (SetupComplete) reads .ps1 as ANSI, mangled it, broke
   the string terminator -> whole script failed to parse -> lock/login/wallpaper branding
   never re-applied. Fix: ASCII-ify the em-dash AND save the branding scripts UTF-8-with-BOM
   so PS5.1 always decodes them correctly (verified parses under PS5.1 + PS7).
2. sm-bootstrap never removed: TearDownAsync used schtasks /tr with an inline -EncodedCommand,
   which silently fails past the ~261-char /tr limit, so the cleanup task was never created
   (confirmed NO_TASK on disk). Fix: Register-ScheduledTask (no length limit).
3. Done step: show a QR code of the BitLocker recovery key (QRCoder) for phone backup, and
   lay key+QR side-by-side so the Restart button no longer overflows below the fold.

Verified: welcome solution builds, 29/29 tests; branding Pester 6/6 unit (offline-integration
needs elevation, runs in CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:41:30 +01:00

161 lines
4.3 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS Apply SilverMetal Windows branding (4 layers), offline (WIM) or online.
.DESCRIPTION
Offline: reg-load the mounted image's SOFTWARE + default NTUSER hives, write
values, stage assets, reg-unload. Online: write live HKLM + default-user hive.
Design: ../docs/superpowers/specs/2026-06-09-first-boot-branding-design.md
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidateSet('Offline','Online')][string]$Mode,
[string]$MountPath, # required for Offline
[string]$Manifest = "$PSScriptRoot\branding.manifest.json",
[string]$AssetsDir = "$PSScriptRoot\assets",
[switch]$PassThru
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\lib\BrandingLayers.ps1"
function Write-Stage { param($m) Write-Host "==> $m" -ForegroundColor Cyan }
if ($Mode -eq 'Offline' -and -not $MountPath) { throw 'Offline mode requires -MountPath.' }
$m = Get-Content $Manifest -Raw | ConvertFrom-Json
# Destination paths (image-relative for offline, live for online).
$winRoot = if ($Mode -eq 'Offline') { $MountPath } else { 'C:' }
$logoDest = Join-Path $winRoot 'Windows\System32\oemlogo.bmp'
$lockDir = Join-Path $winRoot 'Windows\Web\Screen\SilverMetal'
$wallDir = Join-Path $winRoot 'Windows\Web\Wallpaper\SilverMetal'
$themeDest = Join-Path $winRoot 'Windows\Resources\Themes'
$lockLive = 'C:\Windows\Web\Screen\SilverMetal\' + $m.lockScreen.image
$wallLive = 'C:\Windows\Web\Wallpaper\SilverMetal\' + $m.desktop.wallpaper
$logoLive = 'C:\Windows\System32\oemlogo.bmp'
# --- stage assets ---
Write-Stage "Stage branding assets ($Mode)"
New-Item -ItemType Directory -Force $lockDir,$wallDir,$themeDest,(Split-Path $logoDest) | Out-Null
Copy-Item (Join-Path $AssetsDir $m.oem.logo) $logoDest -Force
Copy-Item (Join-Path $AssetsDir $m.lockScreen.image) (Join-Path $lockDir $m.lockScreen.image) -Force
Copy-Item (Join-Path $AssetsDir $m.desktop.wallpaper)(Join-Path $wallDir $m.desktop.wallpaper) -Force
Copy-Item (Join-Path $AssetsDir $m.desktop.theme) (Join-Path $themeDest $m.desktop.theme) -Force
$result = [ordered]@{ OemApplied=$false; LockScreenApplied=$false; DesktopApplied=$false; BitLockerApplied=$false }
function Invoke-WithHive {
param([string]$HivePath,[string]$Name,[scriptblock]$Body)
& reg load "HKLM\$Name" $HivePath | Out-Null
if ($LASTEXITCODE -ne 0) { throw "reg load $Name ($HivePath) failed" }
try { & $Body "Registry::HKEY_LOCAL_MACHINE\$Name" }
finally {
[gc]::Collect(); Start-Sleep -Milliseconds 500
& reg unload "HKLM\$Name" | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Warning "reg unload $Name failed ($LASTEXITCODE) -- hive may be leaked" }
}
}
if ($Mode -eq 'Offline') {
$swHive = Join-Path $MountPath 'Windows\System32\config\SOFTWARE'
$duHive = Join-Path $MountPath 'Users\Default\NTUSER.DAT'
Invoke-WithHive $swHive 'SM_BRAND_SW' {
param($sw)
Write-Stage 'OEM About'; Set-OemInformation -SoftwareRoot $sw -Manifest $m -LogoPath $logoLive; $result.OemApplied=$true
Write-Stage 'Lock screen'; Set-LockScreen -SoftwareRoot $sw -ImagePath $lockLive -Lock:$m.lockScreen.lock; $result.LockScreenApplied=$true
Write-Stage 'BitLocker preboot';Set-BitLockerPreboot -SoftwareRoot $sw -Manifest $m; $result.BitLockerApplied=$true
}
Invoke-WithHive $duHive 'SM_BRAND_DU' {
param($du)
Write-Stage 'Desktop'; Set-DesktopBranding -DefaultUserRoot $du -Manifest $m -WallpaperPath $wallLive; $result.DesktopApplied=$true
}
} else {
Set-OemInformation -SoftwareRoot 'HKLM:\SOFTWARE' -Manifest $m -LogoPath $logoLive; $result.OemApplied=$true
Set-LockScreen -SoftwareRoot 'HKLM:\SOFTWARE' -ImagePath $lockLive -Lock:$m.lockScreen.lock; $result.LockScreenApplied=$true
Set-BitLockerPreboot -SoftwareRoot 'HKLM:\SOFTWARE' -Manifest $m; $result.BitLockerApplied=$true
Invoke-WithHive 'C:\Users\Default\NTUSER.DAT' 'SM_BRAND_DU' {
param($du) Set-DesktopBranding -DefaultUserRoot $du -Manifest $m -WallpaperPath $wallLive; $result.DesktopApplied=$true
}
}
Write-Host 'Branding applied.' -ForegroundColor Green
if ($PassThru) { [pscustomobject]$result }