generalize: assign the data-disk letter every boot, and heal a temp profile

The relocated profile went temporary on the target and stayed that way. The
cause was not the profile: the image ships with ProfilesDirectory set to
D:\Users but with no drive letter for the data disk. generalize strips
MountedDevices, and the specialize pass that re-asserts the letter runs only in
the build VM, never on the target -- so the zvol boots letterless, the first
autologon cannot find D:\Users\sagar (event 1511), and Windows falls back to a
temporary profile, renames the real ProfileList key to .bak, and the fault
sticks on every later logon.

Confirmed by reading the shipped image offline: ProfileList has the SID at
D:\Users\TEMP with a .bak sibling at D:\Users\sagar, MountedDevices carries no
\DosDevices\D:, and the sagar hive on the zvol is intact -- so nothing was
wrong but the letter.

An onstart SYSTEM task now runs the existing (idempotent) data-disk init, whose
diskpart assign writes MountedDevices and so makes D: persistent for every
later boot. Only the first boot is exposed to the race; if it left a .bak, a
small PowerShell heal puts the key back, drops the temp profile, and reboots
once -- after which D: is persistent and the real profile loads. Same onstart /
SYSTEM mechanism the static address already uses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
This commit is contained in:
Git Sagar 2026-09-10 17:43:11 -03:00
parent 6a62a649bd
commit d94c576df2

View file

@ -143,6 +143,49 @@ in
:done
'';
# PowerShell in its own file, so neither cmd quoting nor Nix's '' need to be
# fought. If the account's real profile got backed up to a .bak key (the
# temporary-profile fallback), put it back: drop the temp key, rename .bak to
# the live SID, remove the temp directory, and drop a flag so the caller
# knows to reboot.
healProfileScript = pkgs.writeText "vmix-heal-profile.ps1" ''
$pl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
$bak = Get-ChildItem $pl -ErrorAction SilentlyContinue | Where-Object {
$_.PSChildName -like '*.bak' -and
(Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).ProfileImagePath -eq '${profilesDirectory}\${username}'
} | Select-Object -First 1
if ($bak) {
$sid = $bak.PSChildName -replace '\.bak$'
Remove-Item (Join-Path $pl $sid) -Recurse -Force -ErrorAction SilentlyContinue
Rename-Item $bak.PSPath $sid -ErrorAction SilentlyContinue
Remove-Item '${profilesDirectory}\TEMP' -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path C:\Windows\Temp\vmix-profile-healed -ItemType File -Force | Out-Null
}
'';
# Ensures D: exists on every boot and heals a profile that went temporary.
#
# generalize strips MountedDevices, so the shipped image carries no drive
# letter for the data disk, and the specialize pass that would re-assert it
# runs only in the build, not on the target. The zvol therefore boots
# letterless, the first autologon cannot find its relocated profile at
# ${profilesDirectory}\${username} (event 1511) and falls back to a temporary
# one, backing the real key up as .bak and making the fault stick.
#
# diskpart assign writes MountedDevices, so once this has run once D: is
# persistent for every later boot. Only the first boot is exposed, and if it
# left a .bak the heal puts it back and reboots -- the next boot, D: now
# persistent and ProfileList clean, logs straight into the real profile.
bootDataProfileScript = pkgs.writeText "vmix-data-profile.cmd" ''
@echo off
call C:\vmix-init-data-disk.cmd
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-heal-profile.ps1 > C:\Windows\Temp\vmix-data-profile.log 2>&1
if exist C:\Windows\Temp\vmix-profile-healed (
del /q C:\Windows\Temp\vmix-profile-healed
shutdown /r /t 5 /c "vmix: repaired relocated profile, restarting"
)
'';
folderLocationsXml = lib.optionalString (profilesDirectory != null) ''
<!-- Profiles live on the data disk, so the OS disk stays disposable
and rebuilding it does not take the profile along -->
@ -247,6 +290,13 @@ in
schtasks /create /tn "vmix-static-ip" /tr "C:\vmix-static-ip.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1
''}
${lib.optionalString (dataDisk != null) ''
:: Ensures D: is assigned on every boot -- the image ships without a
:: persisted letter for the data disk -- and heals a profile that went
:: temporary before D: was ready. Onstart / SYSTEM, like the address task.
schtasks /create /tn "vmix-data-profile" /tr "C:\vmix-data-profile.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1
''}
${lib.optionalString (writeFilter != null) ''
:: Install the feature now, but defer configuring it: uwfmgr does not exist
:: until this has been through a reboot, and the swapfile belongs on the
@ -357,7 +407,11 @@ in {
{ source = oobeXml; dest = "/oobe-unattend.xml"; }
{ source = postOobeScript; dest = "/post-oobe.cmd"; }
{ source = masScript; dest = "/MAS_AIO.cmd"; }
] ++ lib.optional (dataDisk != null) { source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }
] ++ lib.optionals (dataDisk != null) [
{ source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }
{ source = healProfileScript; dest = "/vmix-heal-profile.ps1"; }
{ source = bootDataProfileScript; dest = "/vmix-data-profile.cmd"; }
]
++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; }
++ lib.optional (staticIP != null) { source = staticIPScript; dest = "/vmix-static-ip.cmd"; };
# delayOobeRun: sysprep + shutdown — OOBE runs on real hardware