Private/Initialize-Win32ToolkitVhdxLayout.ps1
|
function Initialize-Win32ToolkitVhdxLayout { <# .SYNOPSIS Mounts a VHDX and lays down a UEFI/GPT layout (ESP FAT32 + MSR + Windows NTFS) via diskpart. .DESCRIPTION HOST-ONLY (runs diskpart). Mounts the VHDX, derives the disk number FROM the VHDX and asserts it is file-backed (Assert-Win32ToolkitVhdxDisk — the data-loss guard) BEFORE any `clean`, then partitions per the MS UEFI layout. Drive letters for the ESP and Windows partitions are chosen from the letters actually FREE on this host (Windows 365 / corporate machines commonly already use S:/W:, which broke a hard-coded assignment). Leaves the VHDX mounted (the caller applies the image and then dismounts). Returns the disk number + the ESP and Windows drive letters. See knowledge-base/designs/hyperv-golden-image-build.md (§2.2 Step 5). .PARAMETER VhdxPath Path to the VHDX to partition (created by the caller with New-VHD). .PARAMETER EspLetter Preferred ESP drive letter; used only if free, else an unused letter is chosen automatically. .PARAMETER WindowsLetter Preferred Windows drive letter; used only if free, else an unused letter is chosen automatically. .OUTPUTS PSCustomObject with: DiskNumber, EspDrive (e.g. 'S:'), WindowsDrive (e.g. 'W:'). #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$VhdxPath, [string]$EspLetter, [string]$WindowsLetter ) Mount-VHD -Path $VhdxPath -ErrorAction Stop | Out-Null $disk = Get-DiskImage -ImagePath $VhdxPath | Get-Disk $diskNumber = Assert-Win32ToolkitVhdxDisk -Disk $disk # throws unless 'File Backed Virtual' try { # Letters currently in use on the host (mounted volumes + mapped/network drives). $used = @([System.IO.DriveInfo]::GetDrives() | ForEach-Object { $_.Name.Substring(0, 1).ToUpper() }) # Free letters, Z down to D (skip A/B/C). $free = [System.Collections.Generic.List[string]]::new() foreach ($code in 90..68) { $ch = [string][char]$code; if ($ch -notin $used) { $free.Add($ch) } } if ($free.Count -lt 2) { throw 'Fewer than two free drive letters are available for the VHDX build.' } $esp = if ($EspLetter -and ($EspLetter.ToUpper() -notin $used)) { $EspLetter.ToUpper() } else { $free[0] } $win = if ($WindowsLetter -and ($WindowsLetter.ToUpper() -notin $used) -and ($WindowsLetter.ToUpper() -ne $esp)) { $WindowsLetter.ToUpper() } else { @($free | Where-Object { $_ -ne $esp })[0] } $script = @" select disk $diskNumber clean convert gpt create partition efi size=260 format quick fs=fat32 label=System assign letter=$esp create partition msr size=16 create partition primary format quick fs=ntfs label=Windows assign letter=$win exit "@ $scriptFile = Join-Path ([System.IO.Path]::GetTempPath()) ("w32vhdx_" + [guid]::NewGuid().ToString('N').Substring(0, 8) + '.txt') try { Set-Content -LiteralPath $scriptFile -Value $script -Encoding ASCII $out = & diskpart /s $scriptFile 2>&1 if ($LASTEXITCODE -ne 0) { throw "diskpart failed (exit $LASTEXITCODE): $out" } } finally { Remove-Item -LiteralPath $scriptFile -ErrorAction SilentlyContinue } [pscustomobject]@{ DiskNumber = $diskNumber EspDrive = "${esp}:" WindowsDrive = "${win}:" } } catch { # Release the mount so a re-run (or -Force rebuild) isn't blocked by a half-partitioned VHDX. Dismount-VHD -Path $VhdxPath -ErrorAction SilentlyContinue throw } } |