Files
winutil/functions/private/Invoke-WinUtilISOUSB.ps1
OmarGitHubChris Tituscoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
5ebb24b107 Refactor Win11 Creator ISO workflow (#4862)
* refactor: simplify Win11 Creator ISO workflow

Keep no-driver ISO generation on the fast copy-only path.

Inject exported drivers with one selected-index WIM mount, one Add-Driver pass, and one commit.

Limit WinPE staging to boot-storage drivers and improve workflow validation coverage.

* refactor: stage Win11 setup script fallback

Copy the prepared setup script payloads into sources/$//Setup/Scripts so setup media does not depend solely on answer-file extension extraction.

Keep the existing autounattend execution path unchanged and cover the fallback files in the Win11 Creator tests.

* refactor: address codex feedback

* refactor: run ISO verification in runspace

Rename the ISO logger to Write-WinUtilISOLog so the shared runspace pool imports it automatically. Run Mount & Verify off the WPF thread with dispatcher-safe UI updates, and add Pester coverage for the runspace and logger contract.

* refactor: address ISO review feedback

Keep the selected ISO stable during background verification. Apply ContentDeliveryManager settings to both the first account and the default profile. Block FAT32 USB creation when install.esd exceeds the supported file size.

* refactor: address ISO setup edge cases

Set BypassNRO before OOBE so local account setup is available during Windows Setup.

Detect registered DISM mounts during driver-injection cleanup so partial mount failures are discarded.

* refactor: address ISO review edge cases

Use the actual FAT32 file limit for install.esd USB checks.

Disable ISO modification while mount verification is running.

Apply unsupported-hardware notice suppression to the first user profile.

* refactor: stage ISO setup safeguards earlier

Set device encryption and reserved-storage registry guards before OOBE.

Enable the OEM configuration-set fallback when setup scripts are staged there.

* Update functions/private/Invoke-WinUtilISOUSB.ps1

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Titus <contact@christitus.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-28 14:08:56 -05:00

287 lines
14 KiB
PowerShell

function Invoke-WinUtilISORefreshUSBDrives {
$combo = $sync["WPFWin11ISOUSBDriveComboBox"]
$removable = @(Get-Disk | Where-Object { $_.BusType -eq "USB" } | Sort-Object Number)
$combo.Items.Clear()
if ($removable.Count -eq 0) {
$combo.Items.Add("No USB drives detected.")
$combo.SelectedIndex = 0
$sync["Win11ISOUSBDisks"] = @()
Write-WinUtilISOLog "No USB drives detected."
return
}
foreach ($disk in $removable) {
$sizeGB = [math]::Round($disk.Size / 1GB, 1)
$combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
}
$combo.SelectedIndex = 0
Write-WinUtilISOLog "Found $($removable.Count) USB drive(s)."
$sync["Win11ISOUSBDisks"] = $removable
}
function Invoke-WinUtilISOWriteUSB {
$contentsDir = $sync["Win11ISOContentsDir"]
$usbDisks = $sync["Win11ISOUSBDisks"]
if (-not $contentsDir -or -not (Test-Path $contentsDir)) {
[System.Windows.MessageBox]::Show("No modified ISO content found. Please complete Steps 1-3 first.", "Not Ready", "OK", "Warning")
return
}
$installWim = Join-Path $contentsDir "sources\install.wim"
$installEsd = Join-Path $contentsDir "sources\install.esd"
if (Test-Path $installEsd) {
$installEsdFile = Get-Item $installEsd
$esdSizeBytes = $installEsdFile.Length
$esdSizeMB = [math]::Ceiling($esdSizeBytes / 1MB)
if ($esdSizeBytes -ge 4GB) {
[System.Windows.MessageBox]::Show(
"This ISO uses an install.esd file that is $esdSizeMB MB. WinUtil's FAT32 USB format cannot store files larger than 4 GB.`n`nExport an ISO instead or use media with install.wim.",
"USB Creation Not Supported", "OK", "Warning")
return
}
}
$combo = $sync["WPFWin11ISOUSBDriveComboBox"]
$selectedIndex = $combo.SelectedIndex
$selectedItemText = [string]$combo.SelectedItem
$usbDisks = @($usbDisks)
$targetDisk = $null
if ($selectedIndex -ge 0 -and $selectedIndex -lt $usbDisks.Count) {
$targetDisk = $usbDisks[$selectedIndex]
} elseif ($selectedItemText -match 'Disk\s+(\d+):') {
$selectedDiskNum = [int]$matches[1]
$targetDisk = $usbDisks | Where-Object { $_.Number -eq $selectedDiskNum } | Select-Object -First 1
}
if (-not $targetDisk) {
[System.Windows.MessageBox]::Show("Please select a USB drive from the dropdown.", "No Drive Selected", "OK", "Warning")
return
}
$diskNum = $targetDisk.Number
$sizeGB = [math]::Round($targetDisk.Size / 1GB, 1)
$confirm = [System.Windows.MessageBox]::Show(
"ALL data on Disk $diskNum ($($targetDisk.FriendlyName), $sizeGB GB) will be PERMANENTLY ERASED.`n`nAre you sure you want to continue?",
"Confirm USB Erase", "YesNo", "Warning")
if ($confirm -ne "Yes") {
Write-WinUtilISOLog "USB write cancelled by user."
return
}
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
$sync["Win11ISOProcessRunning"] = $true
Write-WinUtilISOLog "Starting USB write to Disk $diskNum..."
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$runspace.SessionStateProxy.SetVariable("sync", $sync)
$runspace.SessionStateProxy.SetVariable("diskNum", $diskNum)
$runspace.SessionStateProxy.SetVariable("contentsDir", $contentsDir)
$script = [Management.Automation.PowerShell]::Create()
$script.Runspace = $runspace
$script.AddScript({
function Log($msg) {
$ts = (Get-Date).ToString("HH:mm:ss")
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOStatusLog"].Text += "`n[$ts] $msg"
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
$sync["WPFWin11ISOStatusLog"].ScrollToEnd()
})
}
function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync["WPFTweaksProgressBar"].Visibility = "Visible"
$sync["WPFTweaksProgressLabel"].Text = $label
$sync["WPFTweaksProgressLabel"].ToolTip = $label
$sync["WPFTweaksProgressValue"].Value = [Math]::Max($pct, 5)
})
}
function Get-FreeDriveLetter {
$used = (Get-PSDrive -PSProvider FileSystem).Name
foreach ($c in [char[]](68..90)) {
if ($used -notcontains [string]$c) { return $c }
}
return $null
}
try {
SetProgress "Formatting USB drive..." 10
# Phase 1: Clean disk via diskpart (retry once if the drive is not yet ready)
$dpFile1 = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
"select disk $diskNum`nclean`nexit" | Set-Content -Path $dpFile1 -Encoding ASCII
Log "Running diskpart clean on Disk $diskNum..."
$dpCleanOut = diskpart /s $dpFile1
$dpCleanOut | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
Remove-Item $dpFile1 -Force
if (($dpCleanOut -join ' ') -match 'device is not ready') {
Log "Disk $diskNum was not ready; waiting 5 seconds and retrying clean..."
Start-Sleep -Seconds 5
Update-Disk -Number $diskNum
$dpFile1b = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
"select disk $diskNum`nclean`nexit" | Set-Content -Path $dpFile1b -Encoding ASCII
diskpart /s $dpFile1b | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
Remove-Item $dpFile1b -Force
}
# Phase 2: Initialize as GPT
Start-Sleep -Seconds 2
Update-Disk -Number $diskNum
$diskObj = Get-Disk -Number $diskNum
if ($diskObj.PartitionStyle -eq 'RAW') {
Initialize-Disk -Number $diskNum -PartitionStyle GPT
Log "Disk $diskNum initialized as GPT."
} else {
Set-Disk -Number $diskNum -PartitionStyle GPT
Log "Disk $diskNum converted to GPT (was $($diskObj.PartitionStyle))."
}
# Phase 3: Create FAT32 partition via diskpart, then format with Format-Volume
# (diskpart's 'format' command can fail with "no volume selected" on fresh/never-formatted drives)
$volLabel = "W11-" + (Get-Date).ToString('yyMMdd')
$dpFile2 = Join-Path $env:TEMP "winutil_diskpart2_$(Get-Random).txt"
$maxFat32PartitionMB = 32768
$diskSizeMB = [int][Math]::Floor((Get-Disk -Number $diskNum).Size / 1MB)
$createPartitionCommand = "create partition primary"
if ($diskSizeMB -gt $maxFat32PartitionMB) {
$createPartitionCommand = "create partition primary size=$maxFat32PartitionMB"
Log "Disk $diskNum is $diskSizeMB MB; creating FAT32 partition capped at $maxFat32PartitionMB MB (32 GB)."
}
@(
"select disk $diskNum"
$createPartitionCommand
"exit"
) | Set-Content -Path $dpFile2 -Encoding ASCII
Log "Creating partitions on Disk $diskNum..."
diskpart /s $dpFile2 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
Remove-Item $dpFile2 -Force
SetProgress "Formatting USB partition..." 25
Start-Sleep -Seconds 3
Update-Disk -Number $diskNum
$partitions = Get-Partition -DiskNumber $diskNum
Log "Partitions on Disk $diskNum after creation: $($partitions.Count)"
foreach ($p in $partitions) {
Log " Partition $($p.PartitionNumber) Type=$($p.Type) Letter=$($p.DriveLetter) Size=$([math]::Round($p.Size/1MB))MB"
}
$winpePart = $partitions | Where-Object { $_.Type -eq "Basic" } | Select-Object -Last 1
if (-not $winpePart) {
throw "Could not find the Basic partition on Disk $diskNum after creation."
}
# Format using Format-Volume (reliable on fresh drives; diskpart format fails
# with 'no volume selected' when the partition has never been formatted before)
Log "Formatting Partition $($winpePart.PartitionNumber) as FAT32 (label: $volLabel)..."
Get-Partition -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber |
Format-Volume -FileSystem FAT32 -NewFileSystemLabel $volLabel -Force -Confirm:$false
Log "Partition $($winpePart.PartitionNumber) formatted as FAT32."
SetProgress "Assigning drive letters..." 30
Start-Sleep -Seconds 2
Update-Disk -Number $diskNum
try { Remove-PartitionAccessPath -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -AccessPath "$($winpePart.DriveLetter):" } catch { Log "Warning: could not remove existing partition access path: $_" }
$usbLetter = Get-FreeDriveLetter
if (-not $usbLetter) { throw "No free drive letters (D-Z) available to assign to the USB data partition." }
Set-Partition -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -NewDriveLetter $usbLetter
Log "Assigned drive letter $usbLetter to WINPE partition (Partition $($winpePart.PartitionNumber))."
Start-Sleep -Seconds 2
$usbDrive = "${usbLetter}:"
$retries = 0
while (-not (Test-Path $usbDrive) -and $retries -lt 6) {
$retries++
Log "Waiting for $usbDrive to become accessible (attempt $retries/6)..."
Start-Sleep -Seconds 2
}
if (-not (Test-Path $usbDrive)) { throw "Drive $usbDrive is not accessible after letter assignment." }
Log "USB data partition: $usbDrive"
$contentSizeBytes = (Get-ChildItem -LiteralPath $contentsDir -File -Recurse -Force | Measure-Object -Property Length -Sum).Sum
if (-not $contentSizeBytes) { $contentSizeBytes = 0 }
$usbVolume = Get-Volume -DriveLetter $usbLetter
$partitionCapacityBytes = [int64]$usbVolume.Size
$partitionFreeBytes = [int64]$usbVolume.SizeRemaining
$contentSizeGB = [math]::Round($contentSizeBytes / 1GB, 2)
$partitionCapacityGB = [math]::Round($partitionCapacityBytes / 1GB, 2)
$partitionFreeGB = [math]::Round($partitionFreeBytes / 1GB, 2)
Log "Source content size: $contentSizeGB GB. USB partition capacity: $partitionCapacityGB GB, free: $partitionFreeGB GB."
if ($contentSizeBytes -gt $partitionCapacityBytes) {
throw "ISO content ($contentSizeGB GB) is larger than the USB partition capacity ($partitionCapacityGB GB). Use a larger USB drive or reduce image size."
}
if ($contentSizeBytes -gt $partitionFreeBytes) {
throw "Insufficient free space on USB partition. Required: $contentSizeGB GB, available: $partitionFreeGB GB."
}
SetProgress "Copying Windows 11 files to USB..." 45
# Copy files; split install.wim if > 4 GB (FAT32 limit)
$installWim = Join-Path $contentsDir "sources\install.wim"
if (Test-Path $installWim) {
$wimSizeMB = [math]::Round((Get-Item $installWim).Length / 1MB)
if ($wimSizeMB -gt 3800) {
Log "install.wim is $wimSizeMB MB - splitting for FAT32 compatibility... This will take several minutes."
$splitDest = Join-Path $usbDrive "sources\install.swm"
New-Item -ItemType Directory -Path (Split-Path $splitDest) -Force
Split-WindowsImage -ImagePath $installWim -SplitImagePath $splitDest -FileSize 3800 -CheckIntegrity
Log "install.wim split complete."
Log "Copying remaining files to USB..."
& robocopy $contentsDir $usbDrive /E /XF install.wim /NFL /NDL /NJH /NJS
} else {
& robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS
}
} else {
& robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS
}
SetProgress "Finalising USB drive..." 90
Log "Files copied to USB."
SetProgress "USB write complete" 100
Log "USB drive is ready for use."
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show(
"USB drive created successfully!`n`nYou can now boot from this drive to install Windows 11.",
"USB Ready", "OK", "Info")
})
} catch {
Log "ERROR during USB write: $_"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show("USB write failed:`n`n$_", "USB Write Error", "OK", "Error")
})
} finally {
Start-Sleep -Milliseconds 800
$sync["Win11ISOProcessRunning"] = $false
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync["WPFTweaksProgressBar"].Visibility = "Collapsed"
$sync["WPFTweaksProgressLabel"].Text = ""
$sync["WPFTweaksProgressLabel"].ToolTip = ""
$sync["WPFTweaksProgressValue"].Value = 0
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $true
})
}
})
$script.BeginInvoke()
}