Compare commits

..

6 Commits

Author SHA1 Message Date
Chris Titus
219b93989a large drive support and change to Exfat 2026-03-02 21:08:34 -06:00
Chris Titus Tech
e4cb061b0e Fix Scrollviewer in Status Log 2026-03-02 18:14:11 -06:00
Chris Titus Tech
d16642bf0e Fix OSCDIMG output and cleanup comments 2026-03-02 15:57:43 -06:00
Chris Titus Tech
1dc1b81439 improve clean up 2026-03-02 15:30:16 -06:00
Chris Titus Tech
3bc14e9b64 cleanup injection 2026-03-02 13:27:21 -06:00
Chris Titus Tech
8db5e6b461 fix first run probs 2026-03-02 13:19:28 -06:00
5 changed files with 381 additions and 687 deletions

View File

@@ -1,18 +1,12 @@
function Write-Win11ISOLog { function Write-Win11ISOLog {
<#
.SYNOPSIS
Appends a timestamped message to the Win11ISO status log TextBox.
.PARAMETER Message
The message to append.
#>
param([string]$Message) param([string]$Message)
$timestamp = (Get-Date).ToString("HH:mm:ss") $ts = (Get-Date).ToString("HH:mm:ss")
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$current = $sync["WPFWin11ISOStatusLog"].Text $current = $sync["WPFWin11ISOStatusLog"].Text
if ($current -eq "Ready. Please select a Windows 11 ISO to begin.") { if ($current -eq "Ready. Please select a Windows 11 ISO to begin.") {
$sync["WPFWin11ISOStatusLog"].Text = "[$timestamp] $Message" $sync["WPFWin11ISOStatusLog"].Text = "[$ts] $Message"
} else { } else {
$sync["WPFWin11ISOStatusLog"].Text += "`n[$timestamp] $Message" $sync["WPFWin11ISOStatusLog"].Text += "`n[$ts] $Message"
} }
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length $sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
$sync["WPFWin11ISOStatusLog"].ScrollToEnd() $sync["WPFWin11ISOStatusLog"].ScrollToEnd()
@@ -20,11 +14,6 @@ function Write-Win11ISOLog {
} }
function Invoke-WinUtilISOBrowse { function Invoke-WinUtilISOBrowse {
<#
.SYNOPSIS
Opens an OpenFileDialog so the user can choose a Windows 11 ISO file.
Populates WPFWin11ISOPath and reveals the Mount & Verify section (Step 2).
#>
Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Windows.Forms
$dlg = [System.Windows.Forms.OpenFileDialog]::new() $dlg = [System.Windows.Forms.OpenFileDialog]::new()
@@ -35,18 +24,12 @@ function Invoke-WinUtilISOBrowse {
if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return } if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }
$isoPath = $dlg.FileName $isoPath = $dlg.FileName
# ── Basic size sanity-check (a Win11 ISO is typically > 4 GB) ──
$fileSizeGB = [math]::Round((Get-Item $isoPath).Length / 1GB, 2) $fileSizeGB = [math]::Round((Get-Item $isoPath).Length / 1GB, 2)
$sync["WPFWin11ISOPath"].Text = $isoPath $sync["WPFWin11ISOPath"].Text = $isoPath
$sync["WPFWin11ISOFileInfo"].Text = "File size: $fileSizeGB GB" $sync["WPFWin11ISOFileInfo"].Text = "File size: $fileSizeGB GB"
$sync["WPFWin11ISOFileInfo"].Visibility = "Visible" $sync["WPFWin11ISOFileInfo"].Visibility = "Visible"
# Reveal Step 2
$sync["WPFWin11ISOMountSection"].Visibility = "Visible" $sync["WPFWin11ISOMountSection"].Visibility = "Visible"
# Collapse all later steps whenever a new ISO is chosen
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed" $sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed" $sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed" $sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
@@ -55,17 +38,10 @@ function Invoke-WinUtilISOBrowse {
} }
function Invoke-WinUtilISOMountAndVerify { function Invoke-WinUtilISOMountAndVerify {
<#
.SYNOPSIS
Mounts the selected ISO, verifies it is a valid Windows 11 image,
and populates the edition list. Reveals Step 3 on success.
#>
$isoPath = $sync["WPFWin11ISOPath"].Text $isoPath = $sync["WPFWin11ISOPath"].Text
if ([string]::IsNullOrWhiteSpace($isoPath) -or $isoPath -eq "No ISO selected...") { if ([string]::IsNullOrWhiteSpace($isoPath) -or $isoPath -eq "No ISO selected...") {
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show("Please select an ISO file first.", "No ISO Selected", "OK", "Warning")
"Please select an ISO file first.",
"No ISO Selected", "OK", "Warning")
return return
} }
@@ -73,14 +49,12 @@ function Invoke-WinUtilISOMountAndVerify {
Set-WinUtilProgressBar -Label "Mounting ISO..." -Percent 10 Set-WinUtilProgressBar -Label "Mounting ISO..." -Percent 10
try { try {
# Mount the ISO
$diskImage = Mount-DiskImage -ImagePath $isoPath -PassThru -ErrorAction Stop $diskImage = Mount-DiskImage -ImagePath $isoPath -PassThru -ErrorAction Stop
$driveLetter = ($diskImage | Get-Volume).DriveLetter + ":" $driveLetter = ($diskImage | Get-Volume).DriveLetter + ":"
Write-Win11ISOLog "Mounted at drive $driveLetter" Write-Win11ISOLog "Mounted at drive $driveLetter"
Set-WinUtilProgressBar -Label "Verifying ISO contents..." -Percent 30 Set-WinUtilProgressBar -Label "Verifying ISO contents..." -Percent 30
# ── Verify install.wim / install.esd presence ──
$wimPath = Join-Path $driveLetter "sources\install.wim" $wimPath = Join-Path $driveLetter "sources\install.wim"
$esdPath = Join-Path $driveLetter "sources\install.esd" $esdPath = Join-Path $driveLetter "sources\install.esd"
@@ -96,14 +70,10 @@ function Invoke-WinUtilISOMountAndVerify {
$activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath } $activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath }
# ── Read edition / architecture info ──
Set-WinUtilProgressBar -Label "Reading image metadata..." -Percent 55 Set-WinUtilProgressBar -Label "Reading image metadata..." -Percent 55
$imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName $imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName
# ── Verify at least one Win11 edition is present ── if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) {
$isWin11 = $imageInfo | Where-Object { $_.ImageName -match "Windows 11" }
if (-not $isWin11) {
Dismount-DiskImage -ImagePath $isoPath | Out-Null Dismount-DiskImage -ImagePath $isoPath | Out-Null
Write-Win11ISOLog "ERROR: No 'Windows 11' edition found in the image." Write-Win11ISOLog "ERROR: No 'Windows 11' edition found in the image."
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
@@ -113,10 +83,8 @@ function Invoke-WinUtilISOMountAndVerify {
return return
} }
# Store edition info for later index lookup
$sync["Win11ISOImageInfo"] = $imageInfo $sync["Win11ISOImageInfo"] = $imageInfo
# ── Populate UI ──
$sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)" $sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)"
$sync["WPFWin11ISOEditionComboBox"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOEditionComboBox"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOEditionComboBox"].Items.Clear() $sync["WPFWin11ISOEditionComboBox"].Items.Clear()
@@ -124,12 +92,10 @@ function Invoke-WinUtilISOMountAndVerify {
[void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)") [void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)")
} }
if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) { if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) {
# Default to Windows 11 Pro; fall back to first item if not found
$proIndex = -1 $proIndex = -1
for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) { for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) {
if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") { if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") {
$proIndex = $i $proIndex = $i; break
break
} }
} }
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 } $sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 }
@@ -137,42 +103,27 @@ function Invoke-WinUtilISOMountAndVerify {
}) })
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible" $sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
# Store for later steps
$sync["Win11ISODriveLetter"] = $driveLetter $sync["Win11ISODriveLetter"] = $driveLetter
$sync["Win11ISOWimPath"] = $activeWim $sync["Win11ISOWimPath"] = $activeWim
$sync["Win11ISOImagePath"] = $isoPath $sync["Win11ISOImagePath"] = $isoPath
# Reveal Step 3
$sync["WPFWin11ISOModifySection"].Visibility = "Visible" $sync["WPFWin11ISOModifySection"].Visibility = "Visible"
Set-WinUtilProgressBar -Label "ISO verified" -Percent 100 Set-WinUtilProgressBar -Label "ISO verified" -Percent 100
Write-Win11ISOLog "ISO verified OK. Editions found: $($imageInfo.Count)" Write-Win11ISOLog "ISO verified OK. Editions found: $($imageInfo.Count)"
} } catch {
catch {
Write-Win11ISOLog "ERROR during mount/verify: $_" Write-Win11ISOLog "ERROR during mount/verify: $_"
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"An error occurred while mounting or verifying the ISO:`n`n$_", "An error occurred while mounting or verifying the ISO:`n`n$_",
"Error", "OK", "Error") "Error", "OK", "Error")
} } finally {
finally {
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
Set-WinUtilProgressBar -Label "" -Percent 0 Set-WinUtilProgressBar -Label "" -Percent 0
} }
} }
function Invoke-WinUtilISOModify { function Invoke-WinUtilISOModify {
<#
.SYNOPSIS
Extracts ISO contents to a temp working directory, modifies install.wim,
then repackages the image. Reveals Step 4 (output options) on success.
.NOTES
This function runs inside a PowerShell runspace so the UI stays responsive.
Placeholder modification logic is provided; extend as needed.
#>
$isoPath = $sync["Win11ISOImagePath"] $isoPath = $sync["Win11ISOImagePath"]
$driveLetter= $sync["Win11ISODriveLetter"] $driveLetter = $sync["Win11ISODriveLetter"]
$wimPath = $sync["Win11ISOWimPath"] $wimPath = $sync["Win11ISOWimPath"]
if (-not $isoPath) { if (-not $isoPath) {
@@ -182,9 +133,8 @@ function Invoke-WinUtilISOModify {
return return
} }
# ── Resolve selected edition index from the ComboBox ──
$selectedItem = $sync["WPFWin11ISOEditionComboBox"].SelectedItem $selectedItem = $sync["WPFWin11ISOEditionComboBox"].SelectedItem
$selectedWimIndex = 1 # default fallback $selectedWimIndex = 1
if ($selectedItem -and $selectedItem -match '^(\d+):') { if ($selectedItem -and $selectedItem -match '^(\d+):') {
$selectedWimIndex = [int]$Matches[1] $selectedWimIndex = [int]$Matches[1]
} elseif ($sync["Win11ISOImageInfo"]) { } elseif ($sync["Win11ISOImageInfo"]) {
@@ -193,13 +143,10 @@ function Invoke-WinUtilISOModify {
$selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" } $selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" }
Write-Win11ISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)" Write-Win11ISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)"
# Disable the modify button to prevent double-click
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false $sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$existingWorkDir = Get-Item -Path (Join-Path $env:TEMP "WinUtil_Win11ISO*") -ErrorAction SilentlyContinue | $existingWorkDir = Get-Item -Path (Join-Path $env:TEMP "WinUtil_Win11ISO*") -ErrorAction SilentlyContinue |
Where-Object { $_.PSIsContainer } | Where-Object { $_.PSIsContainer } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
$workDir = if ($existingWorkDir) { $workDir = if ($existingWorkDir) {
Write-Win11ISOLog "Reusing existing temp directory: $($existingWorkDir.FullName)" Write-Win11ISOLog "Reusing existing temp directory: $($existingWorkDir.FullName)"
@@ -208,9 +155,6 @@ function Invoke-WinUtilISOModify {
Join-Path $env:TEMP "WinUtil_Win11ISO_$(Get-Date -Format 'yyyyMMdd_HHmmss')" Join-Path $env:TEMP "WinUtil_Win11ISO_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
} }
# ── Resolve autounattend.xml content ──────────────────────────────────────
# Compiled winutil.ps1 sets $WinUtilAutounattendXml before main.ps1 runs.
# In dev/source mode fall back to reading tools\autounattend.xml directly.
$autounattendContent = if ($WinUtilAutounattendXml) { $autounattendContent = if ($WinUtilAutounattendXml) {
$WinUtilAutounattendXml $WinUtilAutounattendXml
} else { } else {
@@ -218,11 +162,12 @@ function Invoke-WinUtilISOModify {
if (Test-Path $toolsXml) { Get-Content $toolsXml -Raw } else { "" } if (Test-Path $toolsXml) { Get-Content $toolsXml -Raw } else { "" }
} }
# ── Run modification in a background runspace ──
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA" $runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread" $runspace.ThreadOptions = "ReuseThread"
$runspace.Open() $runspace.Open()
$injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true
$runspace.SessionStateProxy.SetVariable("sync", $sync) $runspace.SessionStateProxy.SetVariable("sync", $sync)
$runspace.SessionStateProxy.SetVariable("isoPath", $isoPath) $runspace.SessionStateProxy.SetVariable("isoPath", $isoPath)
$runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter) $runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter)
@@ -231,28 +176,18 @@ function Invoke-WinUtilISOModify {
$runspace.SessionStateProxy.SetVariable("selectedWimIndex", $selectedWimIndex) $runspace.SessionStateProxy.SetVariable("selectedWimIndex", $selectedWimIndex)
$runspace.SessionStateProxy.SetVariable("selectedEditionName", $selectedEditionName) $runspace.SessionStateProxy.SetVariable("selectedEditionName", $selectedEditionName)
$runspace.SessionStateProxy.SetVariable("autounattendContent", $autounattendContent) $runspace.SessionStateProxy.SetVariable("autounattendContent", $autounattendContent)
$runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers)
# Serialize functions so they are available inside the runspace $isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ${function:Invoke-WinUtilISOScript}.ToString() + "`n}"
$isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ` $win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}"
${function:Invoke-WinUtilISOScript}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("isoScriptFuncDef", $isoScriptFuncDef) $runspace.SessionStateProxy.SetVariable("isoScriptFuncDef", $isoScriptFuncDef)
$win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + `
${function:Write-Win11ISOLog}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef) $runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
$refreshUSBFuncDef = "function Invoke-WinUtilISORefreshUSBDrives {`n" + `
${function:Invoke-WinUtilISORefreshUSBDrives}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("refreshUSBFuncDef", $refreshUSBFuncDef)
$script = [Management.Automation.PowerShell]::Create() $script = [Management.Automation.PowerShell]::Create()
$script.Runspace = $runspace $script.Runspace = $runspace
$script.AddScript({ $script.AddScript({
# Import helper functions into this runspace
. ([scriptblock]::Create($isoScriptFuncDef)) . ([scriptblock]::Create($isoScriptFuncDef))
. ([scriptblock]::Create($win11ISOLogFuncDef)) . ([scriptblock]::Create($win11ISOLogFuncDef))
. ([scriptblock]::Create($refreshUSBFuncDef))
function Log($msg) { function Log($msg) {
$ts = (Get-Date).ToString("HH:mm:ss") $ts = (Get-Date).ToString("HH:mm:ss")
@@ -273,174 +208,111 @@ function Invoke-WinUtilISOModify {
} }
try { try {
# ── Hide Steps 1-3 while modification is running; expand log to fill screen ──
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed" $sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed" $sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed" $sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$expandedHeight = [Math]::Max(400, $sync["Form"].ActualHeight - 100)
$sync["WPFWin11ISOStatusLog"].Height = $expandedHeight
$sync["Win11ISOLogExpanded"] = $true
# Register the resize handler once so the log tracks window resizes
if (-not $sync["Win11ISOResizeHandlerAdded"]) {
$sync["Form"].add_SizeChanged({
if ($sync["Win11ISOLogExpanded"]) {
$sync["WPFWin11ISOStatusLog"].Height = [Math]::Max(400, $sync["Form"].ActualHeight - 100)
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
$sync["WPFWin11ISOStatusLog"].ScrollToEnd()
}
})
$sync["Win11ISOResizeHandlerAdded"] = $true
}
}) })
# ── 1. Create working directory structure ──
Log "Creating working directory: $workDir" Log "Creating working directory: $workDir"
$isoContents = Join-Path $workDir "iso_contents" $isoContents = Join-Path $workDir "iso_contents"
$mountDir = Join-Path $workDir "wim_mount" $mountDir = Join-Path $workDir "wim_mount"
New-Item -ItemType Directory -Path $isoContents, $mountDir -Force | Out-Null New-Item -ItemType Directory -Path $isoContents, $mountDir -Force | Out-Null
SetProgress "Copying ISO contents..." 10 SetProgress "Copying ISO contents..." 10
# ── 2. Copy all ISO contents to the working directory ──
Log "Copying ISO contents from $driveLetter to $isoContents..." Log "Copying ISO contents from $driveLetter to $isoContents..."
$robocopyArgs = @($driveLetter, $isoContents, "/E", "/NFL", "/NDL", "/NJH", "/NJS") & robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS | Out-Null
& robocopy @robocopyArgs | Out-Null
Log "ISO contents copied." Log "ISO contents copied."
SetProgress "Mounting install.wim..." 25 SetProgress "Mounting install.wim..." 25
# ── 3. Copy install.wim to working dir (it may be read-only on the DVD) ──
$localWim = Join-Path $isoContents "sources\install.wim" $localWim = Join-Path $isoContents "sources\install.wim"
if (-not (Test-Path $localWim)) { if (-not (Test-Path $localWim)) { $localWim = Join-Path $isoContents "sources\install.esd" }
# ESD path
$localWim = Join-Path $isoContents "sources\install.esd"
}
# Ensure the file is writable
Set-ItemProperty -Path $localWim -Name IsReadOnly -Value $false Set-ItemProperty -Path $localWim -Name IsReadOnly -Value $false
# ── 4. Mount the selected edition of install.wim ──
Log "Mounting install.wim (Index ${selectedWimIndex}: $selectedEditionName) at $mountDir..." Log "Mounting install.wim (Index ${selectedWimIndex}: $selectedEditionName) at $mountDir..."
Mount-WindowsImage -ImagePath $localWim -Index $selectedWimIndex -Path $mountDir -ErrorAction Stop | Out-Null Mount-WindowsImage -ImagePath $localWim -Index $selectedWimIndex -Path $mountDir -ErrorAction Stop | Out-Null
SetProgress "Modifying install.wim..." 45 SetProgress "Modifying install.wim..." 45
# ── Apply all WinUtil modifications via Invoke-WinUtilISOScript ──
Log "Applying WinUtil modifications to install.wim..." Log "Applying WinUtil modifications to install.wim..."
Invoke-WinUtilISOScript -ScratchDir $mountDir -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -Log { param($m) Log $m } Invoke-WinUtilISOScript -ScratchDir $mountDir -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -Log { param($m) Log $m }
# ── 4b. DISM component store cleanup ──
# /ResetBase removes all superseded component versions from WinSxS,
# which is the single largest space saving possible (typically 300800 MB).
# This must be done while the image is still mounted.
SetProgress "Cleaning up component store (WinSxS)..." 56 SetProgress "Cleaning up component store (WinSxS)..." 56
Log "Running DISM component store cleanup (/ResetBase)..." Log "Running DISM component store cleanup (/ResetBase)..."
& dism /English "/image:$mountDir" /Cleanup-Image /StartComponentCleanup /ResetBase | ForEach-Object { Log $_ } & dism /English "/image:$mountDir" /Cleanup-Image /StartComponentCleanup /ResetBase | ForEach-Object { Log $_ }
Log "Component store cleanup complete." Log "Component store cleanup complete."
# ── 5. Save and dismount the WIM ──
SetProgress "Saving modified install.wim..." 65 SetProgress "Saving modified install.wim..." 65
Log "Dismounting and saving install.wim. This will take several minutes..." Log "Dismounting and saving install.wim. This will take several minutes..."
Dismount-WindowsImage -Path $mountDir -Save -ErrorAction Stop | Out-Null Dismount-WindowsImage -Path $mountDir -Save -ErrorAction Stop | Out-Null
Log "install.wim saved." Log "install.wim saved."
# ── 5b. Strip unused editions — export only the selected index ──
# A standard multi-edition install.wim can be 45 GB; exporting a
# single index typically drops it to ~3 GB, saving 12 GB in the ISO.
SetProgress "Removing unused editions from install.wim..." 70 SetProgress "Removing unused editions from install.wim..." 70
Log "Exporting edition '$selectedEditionName' (Index $selectedWimIndex) to a single-edition install.wim..." Log "Exporting edition '$selectedEditionName' (Index $selectedWimIndex) to a single-edition install.wim..."
$exportWim = Join-Path $isoContents "sources\install_export.wim" $exportWim = Join-Path $isoContents "sources\install_export.wim"
Export-WindowsImage ` Export-WindowsImage -SourceImagePath $localWim -SourceIndex $selectedWimIndex -DestinationImagePath $exportWim -ErrorAction Stop | Out-Null
-SourceImagePath $localWim `
-SourceIndex $selectedWimIndex `
-DestinationImagePath $exportWim `
-ErrorAction Stop | Out-Null
Remove-Item -Path $localWim -Force Remove-Item -Path $localWim -Force
Rename-Item -Path $exportWim -NewName "install.wim" -Force Rename-Item -Path $exportWim -NewName "install.wim" -Force
# Update local path so later steps (e.g. ISO build) reference the new file
$localWim = Join-Path $isoContents "sources\install.wim" $localWim = Join-Path $isoContents "sources\install.wim"
Log "Unused editions removed. install.wim now contains only '$selectedEditionName'." Log "Unused editions removed. install.wim now contains only '$selectedEditionName'."
SetProgress "Dismounting source ISO..." 80 SetProgress "Dismounting source ISO..." 80
# ── 6. Dismount the original ISO ──
Log "Dismounting original ISO..." Log "Dismounting original ISO..."
Dismount-DiskImage -ImagePath $isoPath | Out-Null Dismount-DiskImage -ImagePath $isoPath | Out-Null
# Store work directory for output steps
$sync["Win11ISOWorkDir"] = $workDir $sync["Win11ISOWorkDir"] = $workDir
$sync["Win11ISOContentsDir"] = $isoContents $sync["Win11ISOContentsDir"] = $isoContents
SetProgress "Modification complete" 100 SetProgress "Modification complete" 100
Log "install.wim modification complete. Choose an output option in Step 4." Log "install.wim modification complete. Choose an output option in Step 4."
# ── Reveal Step 4 on the UI thread ──
# Note: USB drive enumeration (Get-Disk) is intentionally deferred to
# when the user explicitly selects the USB option, to avoid blocking
# the UI thread here.
$sync["WPFWin11ISOOutputSection"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOOutputSection"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible" $sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
$sync["WPFWin11ISOStatusLog"].Height = 300
}) })
} } catch {
catch {
Log "ERROR during modification: $_" Log "ERROR during modification: $_"
# ── Cleanup: dismount WIM if still mounted ──
try { try {
if (Test-Path $mountDir) { if (Test-Path $mountDir) {
$mountedImages = Get-WindowsImage -Mounted -ErrorAction SilentlyContinue | $mountedImages = Get-WindowsImage -Mounted -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $mountDir }
Where-Object { $_.Path -eq $mountDir }
if ($mountedImages) { if ($mountedImages) {
Log "Cleaning up: dismounting install.wim (discarding changes)..." Log "Cleaning up: dismounting install.wim (discarding changes)..."
Dismount-WindowsImage -Path $mountDir -Discard -ErrorAction SilentlyContinue | Out-Null Dismount-WindowsImage -Path $mountDir -Discard -ErrorAction SilentlyContinue | Out-Null
} }
} }
} catch { } catch { Log "Warning: could not dismount install.wim during cleanup: $_" }
Log "Warning: could not dismount install.wim during cleanup: $_"
}
# ── Cleanup: dismount the source ISO ──
try { try {
$mountedISO = Get-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue $mountedISO = Get-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue
if ($mountedISO -and $mountedISO.Attached) { if ($mountedISO -and $mountedISO.Attached) {
Log "Cleaning up: dismounting source ISO..." Log "Cleaning up: dismounting source ISO..."
Dismount-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue | Out-Null Dismount-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue | Out-Null
} }
} catch { } catch { Log "Warning: could not dismount ISO during cleanup: $_" }
Log "Warning: could not dismount ISO during cleanup: $_"
}
# ── Cleanup: remove temp working directory ──
try { try {
if (Test-Path $workDir) { if (Test-Path $workDir) {
Log "Cleaning up: removing temp directory $workDir..." Log "Cleaning up: removing temp directory $workDir..."
Remove-Item -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue
} }
} catch { } catch { Log "Warning: could not remove temp directory during cleanup: $_" }
Log "Warning: could not remove temp directory during cleanup: $_"
}
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"An error occurred during install.wim modification:`n`n$_", "An error occurred during install.wim modification:`n`n$_",
"Modification Error", "OK", "Error") "Modification Error", "OK", "Error")
}) })
} } finally {
finally {
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = "" $sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = "" $sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0 $sync.ProgressBar.Value = 0
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true $sync["WPFWin11ISOModifyButton"].IsEnabled = $true
# ── Only restore steps 1-3 if Step 4 was NOT successfully shown ──
# When modification succeeds, Step 4 is visible and steps 1-3 stay
# hidden until the user clicks Clean & Reset.
if ($sync["WPFWin11ISOOutputSection"].Visibility -ne "Visible") { if ($sync["WPFWin11ISOOutputSection"].Visibility -ne "Visible") {
$sync["WPFWin11ISOSelectSection"].Visibility = "Visible" $sync["WPFWin11ISOSelectSection"].Visibility = "Visible"
$sync["WPFWin11ISOMountSection"].Visibility = "Visible" $sync["WPFWin11ISOMountSection"].Visibility = "Visible"
$sync["WPFWin11ISOModifySection"].Visibility = "Visible" $sync["WPFWin11ISOModifySection"].Visibility = "Visible"
} }
$sync["Win11ISOLogExpanded"] = $false
$sync["WPFWin11ISOStatusLog"].Height = 140
}) })
} }
}) | Out-Null }) | Out-Null
@@ -449,43 +321,24 @@ function Invoke-WinUtilISOModify {
} }
function Invoke-WinUtilISOCheckExistingWork { function Invoke-WinUtilISOCheckExistingWork {
<# if ($sync["Win11ISOContentsDir"] -and (Test-Path $sync["Win11ISOContentsDir"])) { return }
.SYNOPSIS
Called when the Win11ISO tab is opened. Checks for a pre-existing
WinUtil_Win11ISO temp directory and, if found, restores the working-
directory state so the user can proceed directly to Step 4 (output
options) without repeating the modification.
#>
# If state is already loaded (e.g. user just switched tabs mid-session)
# do nothing so we don't overwrite in-progress work.
if ($sync["Win11ISOContentsDir"] -and (Test-Path $sync["Win11ISOContentsDir"])) {
return
}
$existingWorkDir = Get-Item -Path (Join-Path $env:TEMP "WinUtil_Win11ISO*") -ErrorAction SilentlyContinue | $existingWorkDir = Get-Item -Path (Join-Path $env:TEMP "WinUtil_Win11ISO*") -ErrorAction SilentlyContinue |
Where-Object { $_.PSIsContainer } | Where-Object { $_.PSIsContainer } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $existingWorkDir) { return } if (-not $existingWorkDir) { return }
$isoContents = Join-Path $existingWorkDir.FullName "iso_contents" $isoContents = Join-Path $existingWorkDir.FullName "iso_contents"
if (-not (Test-Path $isoContents)) { return } if (-not (Test-Path $isoContents)) { return }
# Restore state
$sync["Win11ISOWorkDir"] = $existingWorkDir.FullName $sync["Win11ISOWorkDir"] = $existingWorkDir.FullName
$sync["Win11ISOContentsDir"] = $isoContents $sync["Win11ISOContentsDir"] = $isoContents
# Show Step 4 and collapse steps 1-3 (modification already happened)
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed" $sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed" $sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed" $sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible" $sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
$sync["WPFWin11ISOStatusLog"].Height = 300
# Notify via the status log
$dirName = $existingWorkDir.Name
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm") $modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)" Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)"
Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4." Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
@@ -497,14 +350,6 @@ function Invoke-WinUtilISOCheckExistingWork {
} }
function Invoke-WinUtilISOCleanAndReset { function Invoke-WinUtilISOCleanAndReset {
<#
.SYNOPSIS
Deletes the temporary working directory created during ISO modification
and resets the entire ISO UI back to its initial state (Step 1 only).
Deletion runs in a background runspace so the UI stays responsive and
progress is reported to the user.
#>
$workDir = $sync["Win11ISOWorkDir"] $workDir = $sync["Win11ISOWorkDir"]
if ($workDir -and (Test-Path $workDir)) { if ($workDir -and (Test-Path $workDir)) {
@@ -514,7 +359,6 @@ function Invoke-WinUtilISOCleanAndReset {
if ($confirm -ne "Yes") { return } if ($confirm -ne "Yes") { return }
} }
# Disable button so it cannot be clicked twice
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $false $sync["WPFWin11ISOCleanResetButton"].IsEnabled = $false
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
@@ -537,6 +381,7 @@ function Invoke-WinUtilISOCleanAndReset {
}) })
Add-Content -Path (Join-Path $workDir "WinUtil_Win11ISO.log") -Value "[$ts] $msg" -ErrorAction SilentlyContinue Add-Content -Path (Join-Path $workDir "WinUtil_Win11ISO.log") -Value "[$ts] $msg" -ErrorAction SilentlyContinue
} }
function SetProgress($label, $pct) { function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label $sync.progressBarTextBlock.Text = $label
@@ -546,43 +391,56 @@ function Invoke-WinUtilISOCleanAndReset {
} }
try { try {
if ($workDir) {
$mountDir = Join-Path $workDir "wim_mount"
try {
$mountedImages = Get-WindowsImage -Mounted -ErrorAction SilentlyContinue |
Where-Object { $_.Path -like "$workDir*" }
if ($mountedImages) {
foreach ($img in $mountedImages) {
Log "Dismounting WIM at: $($img.Path) (discarding changes)..."
SetProgress "Dismounting WIM image..." 3
Dismount-WindowsImage -Path $img.Path -Discard -ErrorAction Stop | Out-Null
Log "WIM dismounted successfully."
}
} elseif (Test-Path $mountDir) {
Log "No mounted WIM reported by Get-WindowsImage, running DISM /Cleanup-Wim as a precaution..."
SetProgress "Running DISM cleanup..." 3
& dism /English /Cleanup-Wim 2>&1 | ForEach-Object { Log $_ }
}
} catch {
Log "Warning: could not dismount WIM cleanly, attempting DISM /Cleanup-Wim fallback: $_"
try { & dism /English /Cleanup-Wim 2>&1 | ForEach-Object { Log $_ } }
catch { Log "Warning: DISM /Cleanup-Wim also failed: $_" }
}
}
if ($workDir -and (Test-Path $workDir)) { if ($workDir -and (Test-Path $workDir)) {
Log "Scanning files to delete in: $workDir" Log "Scanning files to delete in: $workDir"
SetProgress "Scanning files..." 5 SetProgress "Scanning files..." 5
$allItems = @(Get-ChildItem -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue) $allFiles = @(Get-ChildItem -Path $workDir -File -Recurse -Force -ErrorAction SilentlyContinue)
$total = $allItems.Count $allDirs = @(Get-ChildItem -Path $workDir -Directory -Recurse -Force -ErrorAction SilentlyContinue |
Sort-Object { $_.FullName.Length } -Descending)
$total = $allFiles.Count
$deleted = 0 $deleted = 0
Log "Found $total items to delete." Log "Found $total files to delete."
# Delete files first, then directories (deepest first) foreach ($f in $allFiles) {
$files = $allItems | Where-Object { -not $_.PSIsContainer } try { Remove-Item -Path $f.FullName -Force -ErrorAction Stop } catch { Log "WARNING: could not delete $($f.FullName): $_" }
$dirs = $allItems | Where-Object { $_.PSIsContainer } |
Sort-Object { $_.FullName.Length } -Descending
foreach ($f in $files) {
try { Remove-Item -Path $f.FullName -Force -ErrorAction Stop } catch {}
$deleted++ $deleted++
if ($deleted % 100 -eq 0 -or $deleted -eq $files.Count) { if ($deleted % 100 -eq 0 -or $deleted -eq $total) {
$pct = [math]::Round(($deleted / [Math]::Max($total,1)) * 85) + 5 $pct = [math]::Round(($deleted / [Math]::Max($total, 1)) * 85) + 5
SetProgress "Deleting files... ($deleted / $total)" $pct SetProgress "Deleting files in $($f.Directory.Name)... ($deleted / $total)" $pct
Log "Deleting files... $deleted of $total"
} }
} }
foreach ($d in $dirs) { foreach ($d in $allDirs) {
try { Remove-Item -Path $d.FullName -Force -Recurse -ErrorAction Stop } catch {} try { Remove-Item -Path $d.FullName -Force -ErrorAction SilentlyContinue } catch {}
$deleted++
if ($deleted % 50 -eq 0 -or $deleted -eq $total) {
$pct = [math]::Round(($deleted / [Math]::Max($total,1)) * 85) + 5
SetProgress "Removing directories... ($deleted / $total)" $pct
Log "Removing directories... $deleted of $total"
}
} }
# Remove the root work directory itself try { Remove-Item -Path $workDir -Recurse -Force -ErrorAction Stop } catch {}
try { Remove-Item -Path $workDir -Force -Recurse -ErrorAction Stop } catch {}
if (Test-Path $workDir) { if (Test-Path $workDir) {
Log "WARNING: some items could not be deleted in $workDir" Log "WARNING: some items could not be deleted in $workDir"
@@ -596,9 +454,7 @@ function Invoke-WinUtilISOCleanAndReset {
SetProgress "Resetting UI..." 95 SetProgress "Resetting UI..." 95
Log "Resetting interface..." Log "Resetting interface..."
# ── Full UI reset on the dispatcher thread ──────────────────────
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
# Clear stored state
$sync["Win11ISOWorkDir"] = $null $sync["Win11ISOWorkDir"] = $null
$sync["Win11ISOContentsDir"] = $null $sync["Win11ISOContentsDir"] = $null
$sync["Win11ISOImagePath"] = $null $sync["Win11ISOImagePath"] = $null
@@ -607,7 +463,6 @@ function Invoke-WinUtilISOCleanAndReset {
$sync["Win11ISOImageInfo"] = $null $sync["Win11ISOImageInfo"] = $null
$sync["Win11ISOUSBDisks"] = $null $sync["Win11ISOUSBDisks"] = $null
# Reset UI elements
$sync["WPFWin11ISOPath"].Text = "No ISO selected..." $sync["WPFWin11ISOPath"].Text = "No ISO selected..."
$sync["WPFWin11ISOFileInfo"].Visibility = "Collapsed" $sync["WPFWin11ISOFileInfo"].Visibility = "Collapsed"
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed" $sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed"
@@ -623,11 +478,9 @@ function Invoke-WinUtilISOCleanAndReset {
$sync.progressBarTextBlock.ToolTip = "" $sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0 $sync.ProgressBar.Value = 0
$sync["WPFWin11ISOStatusLog"].Height = 140
$sync["WPFWin11ISOStatusLog"].Text = "Ready. Please select a Windows 11 ISO to begin." $sync["WPFWin11ISOStatusLog"].Text = "Ready. Please select a Windows 11 ISO to begin."
}) })
} } catch {
catch {
Log "ERROR during Clean & Reset: $_" Log "ERROR during Clean & Reset: $_"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = "" $sync.progressBarTextBlock.Text = ""
@@ -642,17 +495,11 @@ function Invoke-WinUtilISOCleanAndReset {
} }
function Invoke-WinUtilISOExport { function Invoke-WinUtilISOExport {
<#
.SYNOPSIS
Saves the modified ISO contents as a new bootable ISO file.
Uses oscdimg.exe (part of the Windows ADK) if present; falls back
to a reminder message if not installed.
#>
$contentsDir = $sync["Win11ISOContentsDir"] $contentsDir = $sync["Win11ISOContentsDir"]
if (-not $contentsDir -or -not (Test-Path $contentsDir)) { if (-not $contentsDir -or -not (Test-Path $contentsDir)) {
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"No modified ISO content found. Please complete Steps 13 first.", "No modified ISO content found. Please complete Steps 1-3 first.",
"Not Ready", "OK", "Warning") "Not Ready", "OK", "Warning")
return return
} }
@@ -668,9 +515,6 @@ function Invoke-WinUtilISOExport {
if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return } if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }
$outputISO = $dlg.FileName $outputISO = $dlg.FileName
Write-Win11ISOLog "Exporting to ISO: $outputISO"
Set-WinUtilProgressBar -Label "Building ISO..." -Percent 10
# Locate oscdimg.exe (Windows ADK or winget per-user install) # Locate oscdimg.exe (Windows ADK or winget per-user install)
$oscdimg = Get-ChildItem "C:\Program Files (x86)\Windows Kits" -Recurse -Filter "oscdimg.exe" -ErrorAction SilentlyContinue | $oscdimg = Get-ChildItem "C:\Program Files (x86)\Windows Kits" -Recurse -Filter "oscdimg.exe" -ErrorAction SilentlyContinue |
@@ -683,12 +527,10 @@ function Invoke-WinUtilISOExport {
if (-not $oscdimg) { if (-not $oscdimg) {
Write-Win11ISOLog "oscdimg.exe not found. Attempting to install via winget..." Write-Win11ISOLog "oscdimg.exe not found. Attempting to install via winget..."
Set-WinUtilProgressBar -Label "Installing oscdimg..." -Percent 5
try { try {
$winget = Get-Command winget -ErrorAction Stop $winget = Get-Command winget -ErrorAction Stop
$result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements 2>&1 $result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements 2>&1
Write-Win11ISOLog "winget output: $result" Write-Win11ISOLog "winget output: $result"
# Re-scan after install
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" -ErrorAction SilentlyContinue | $oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } | Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
Select-Object -First 1 -ExpandProperty FullName Select-Object -First 1 -ExpandProperty FullName
@@ -697,7 +539,6 @@ function Invoke-WinUtilISOExport {
} }
if (-not $oscdimg) { if (-not $oscdimg) {
Set-WinUtilProgressBar -Label "" -Percent 0
Write-Win11ISOLog "oscdimg.exe still not found after install attempt." Write-Win11ISOLog "oscdimg.exe still not found after install attempt."
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install", "oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install",
@@ -707,21 +548,42 @@ function Invoke-WinUtilISOExport {
Write-Win11ISOLog "oscdimg.exe installed successfully." Write-Win11ISOLog "oscdimg.exe installed successfully."
} }
# Build boot parameters (BIOS + UEFI dual-boot) $sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false
$bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`""
$oscdimgArgs = @( $runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
"-m", # ignore source path max size $runspace.ApartmentState = "STA"
"-o", # optimise storage $runspace.ThreadOptions = "ReuseThread"
"-u2", # UDF 2.01 $runspace.Open()
"-udfver102", $runspace.SessionStateProxy.SetVariable("sync", $sync)
"-bootdata:$bootData", $runspace.SessionStateProxy.SetVariable("contentsDir", $contentsDir)
"-l`"CTOS_MODIFIED`"", $runspace.SessionStateProxy.SetVariable("outputISO", $outputISO)
"`"$contentsDir`"", $runspace.SessionStateProxy.SetVariable("oscdimg", $oscdimg)
"`"$outputISO`""
) $win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
$script = [Management.Automation.PowerShell]::Create()
$script.Runspace = $runspace
$script.AddScript({
. ([scriptblock]::Create($win11ISOLogFuncDef))
function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label
$sync.progressBarTextBlock.ToolTip = $label
$sync.ProgressBar.Value = [Math]::Max($pct, 5)
})
}
try { try {
Write-Win11ISOLog "Exporting to ISO: $outputISO"
SetProgress "Building ISO..." 10
$bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`""
$oscdimgArgs = @("-m", "-o", "-u2", "-udfver102", "-bootdata:$bootData", "-l`"CTOS_MODIFIED`"", "`"$contentsDir`"", "`"$outputISO`"")
Write-Win11ISOLog "Running oscdimg..." Write-Win11ISOLog "Running oscdimg..."
$psi = [System.Diagnostics.ProcessStartInfo]::new() $psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = $oscdimg $psi.FileName = $oscdimg
$psi.Arguments = $oscdimgArgs -join " " $psi.Arguments = $oscdimgArgs -join " "
@@ -734,39 +596,49 @@ function Invoke-WinUtilISOExport {
$proc.StartInfo = $psi $proc.StartInfo = $psi
$proc.Start() | Out-Null $proc.Start() | Out-Null
# Stream stdout and stderr line-by-line to the status log # Stream stdout line-by-line as oscdimg runs
$stdoutTask = $proc.StandardOutput.ReadToEndAsync() while (-not $proc.StandardOutput.EndOfStream) {
$stderrTask = $proc.StandardError.ReadToEndAsync() $line = $proc.StandardOutput.ReadLine()
$proc.WaitForExit()
[System.Threading.Tasks.Task]::WaitAll($stdoutTask, $stderrTask)
foreach ($line in ($stdoutTask.Result -split "`r?`n")) {
if ($line.Trim()) { Write-Win11ISOLog $line } if ($line.Trim()) { Write-Win11ISOLog $line }
} }
foreach ($line in ($stderrTask.Result -split "`r?`n")) {
$proc.WaitForExit()
# Flush any stderr after process exits
$stderr = $proc.StandardError.ReadToEnd()
foreach ($line in ($stderr -split "`r?`n")) {
if ($line.Trim()) { Write-Win11ISOLog "[stderr]$line" } if ($line.Trim()) { Write-Win11ISOLog "[stderr]$line" }
} }
if ($proc.ExitCode -eq 0) { if ($proc.ExitCode -eq 0) {
Set-WinUtilProgressBar -Label "ISO exported" -Percent 100 SetProgress "ISO exported" 100
Write-Win11ISOLog "ISO exported successfully: $outputISO" Write-Win11ISOLog "ISO exported successfully: $outputISO"
[System.Windows.MessageBox]::Show( $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
"ISO exported successfully!`n`n$outputISO", [System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info")
"Export Complete", "OK", "Info") })
} else { } else {
Write-Win11ISOLog "oscdimg exited with code $($proc.ExitCode)." Write-Win11ISOLog "oscdimg exited with code $($proc.ExitCode)."
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.", "oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.",
"Export Error", "OK", "Error") "Export Error", "OK", "Error")
})
} }
} } catch {
catch {
Write-Win11ISOLog "ERROR during ISO export: $_" Write-Win11ISOLog "ERROR during ISO export: $_"
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_","Error","OK","Error") $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
} [System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
finally { })
} finally {
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
Set-WinUtilProgressBar -Label "" -Percent 0 $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $true
})
} }
} }) | Out-Null
$script.BeginInvoke() | Out-Null
}

View File

@@ -4,34 +4,38 @@ function Invoke-WinUtilISOScript {
Applies WinUtil modifications to a mounted Windows 11 install.wim image. Applies WinUtil modifications to a mounted Windows 11 install.wim image.
.DESCRIPTION .DESCRIPTION
Removes AppX bloatware and OneDrive, injects hardware drivers (NVMe, Precision Removes AppX bloatware and OneDrive, optionally injects all drivers exported from
Touchpad/HID, and network) exported from the running system, optionally injects the running system into install.wim and boot.wim (controlled by the
extended Storage & Network drivers from the ChrisTitusTech/storage-lan-drivers -InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware
repository (requires git, installed via winget if absent), applies offline registry bypass, privacy, OOBE, telemetry, update suppression), deletes CEIP/WU
tweaks (hardware bypass, privacy, OOBE, telemetry, update suppression), deletes scheduled-task definition files, and optionally writes autounattend.xml to the ISO
CEIP/WU scheduled-task definition files, and optionally drops autounattend.xml and root and removes the support\ folder from the ISO contents directory.
removes the support\ folder from the ISO contents directory.
All setup scripts embedded in the autounattend.xml <Extensions><File> nodes are
written directly into the WIM at their target paths under C:\Windows\Setup\Scripts\
to ensure they survive Windows Setup stripping unrecognised-namespace XML elements
from the Panther copy of the answer file.
Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO). Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO).
.PARAMETER ScratchDir .PARAMETER ScratchDir
Mandatory. Full path to the directory where the Windows image is currently mounted. Mandatory. Full path to the directory where the Windows image is currently mounted.
Example: C:\Users\USERNAME\AppData\Local\Temp\WinUtil_Win11ISO_20260222\wim_mount
.PARAMETER ISOContentsDir .PARAMETER ISOContentsDir
Optional. Root directory of the extracted ISO contents. Optional. Root directory of the extracted ISO contents. When supplied,
When supplied, autounattend.xml is also written here so Windows Setup picks it autounattend.xml is written here and the support\ folder is removed.
up automatically at boot, and the support\ folder is deleted from that location.
.PARAMETER AutoUnattendXml .PARAMETER AutoUnattendXml
Optional. Full XML content for autounattend.xml. Optional. Full XML content for autounattend.xml. If empty, the OOBE bypass
In compiled winutil.ps1 this is the embedded $WinUtilAutounattendXml here-string; file is skipped and a warning is logged.
in dev mode it is read from tools\autounattend.xml.
If empty, the OOBE bypass file is skipped and a warning is logged. .PARAMETER InjectCurrentSystemDrivers
Optional. When $true, exports all drivers from the running system and injects
them into install.wim and boot.wim index 2 (Windows Setup PE).
Defaults to $false.
.PARAMETER Log .PARAMETER Log
Optional ScriptBlock used for progress/status logging. Optional ScriptBlock for progress/status logging. Receives a single [string] argument.
Receives a single [string] message argument.
Defaults to { param($m) Write-Output $m } when not supplied.
.EXAMPLE .EXAMPLE
Invoke-WinUtilISOScript -ScratchDir "C:\Temp\wim_mount" Invoke-WinUtilISOScript -ScratchDir "C:\Temp\wim_mount"
@@ -46,20 +50,19 @@ function Invoke-WinUtilISOScript {
.NOTES .NOTES
Author : Chris Titus @christitustech Author : Chris Titus @christitustech
GitHub : https://github.com/ChrisTitusTech GitHub : https://github.com/ChrisTitusTech
Version : 26.02.25b Version : 26.03.02
#> #>
param ( param (
[Parameter(Mandatory)][string]$ScratchDir, [Parameter(Mandatory)][string]$ScratchDir,
[string]$ISOContentsDir = "", [string]$ISOContentsDir = "",
[string]$AutoUnattendXml = "", [string]$AutoUnattendXml = "",
[bool]$InjectCurrentSystemDrivers = $false,
[scriptblock]$Log = { param($m) Write-Output $m } [scriptblock]$Log = { param($m) Write-Output $m }
) )
# ── Resolve admin group name (for takeown / icacls) ──────────────────────
$adminSID = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') $adminSID = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
$adminGroup = $adminSID.Translate([System.Security.Principal.NTAccount]) $adminGroup = $adminSID.Translate([System.Security.Principal.NTAccount])
# ── Local helpers ─────────────────────────────────────────────────────────
function Set-ISOScriptReg { function Set-ISOScriptReg {
param ([string]$path, [string]$name, [string]$type, [string]$value) param ([string]$path, [string]$name, [string]$type, [string]$value)
try { try {
@@ -80,56 +83,19 @@ function Invoke-WinUtilISOScript {
} }
} }
# Copies driver package folders (one per exported .inf) whose online class
# matches any of the supplied class names into a new temp staging directory.
# Returns the staging directory path — caller must delete it when done.
# Using a staging dir lets DISM inject all drivers in a single /Recurse
# call instead of one DISM process launch per .inf file.
function New-DriverStagingDir {
param ([string]$ExportRoot, [string[]]$Classes)
$stagingDir = Join-Path $env:TEMP "WinUtil_DriverStage_$(Get-Random)"
New-Item -Path $stagingDir -ItemType Directory -Force | Out-Null
Get-WindowsDriver -Online |
Where-Object { $_.ClassName -in $Classes } |
ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_.OriginalFileName) } |
Select-Object -Unique |
ForEach-Object {
Get-ChildItem -Path $ExportRoot -Filter "$_.inf" -Recurse -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DirectoryName -Unique |
ForEach-Object {
# Each exported driver lives in its own sub-folder;
# copy that folder (with its binary files) into staging.
$dest = Join-Path $stagingDir (Split-Path $_ -Leaf)
Copy-Item -Path $_ -Destination $dest -Recurse -Force -ErrorAction SilentlyContinue
}
}
return $stagingDir
}
# Injects all drivers from $DriverDir into a DISM-mounted image in one call.
function Add-DriversToImage { function Add-DriversToImage {
param ( param ([string]$MountPath, [string]$DriverDir, [string]$Label = "image", [scriptblock]$Logger)
[string]$MountPath,
[string]$DriverDir,
[string]$Label = "image",
[scriptblock]$Logger
)
& dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse 2>&1 | & dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse 2>&1 |
ForEach-Object { & $Logger " dism[$Label]: $_" } ForEach-Object { & $Logger " dism[$Label]: $_" }
} }
# Mounts boot.wim index 2, injects all drivers from $DriverDir, saves, dismounts.
function Invoke-BootWimInject { function Invoke-BootWimInject {
param ( param ([string]$BootWimPath, [string]$DriverDir, [scriptblock]$Logger)
[string]$BootWimPath,
[string]$DriverDir,
[scriptblock]$Logger
)
Set-ItemProperty -Path $BootWimPath -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue Set-ItemProperty -Path $BootWimPath -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue
$mountDir = Join-Path $env:TEMP "WinUtil_BootMount_$(Get-Random)" $mountDir = Join-Path $env:TEMP "WinUtil_BootMount_$(Get-Random)"
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
try { try {
& $Logger "Mounting boot.wim (index 2 — Windows Setup) for driver injection..." & $Logger "Mounting boot.wim (index 2) for driver injection..."
Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir -ErrorAction Stop | Out-Null Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir -ErrorAction Stop | Out-Null
Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger
& $Logger "Saving boot.wim..." & $Logger "Saving boot.wim..."
@@ -143,15 +109,11 @@ function Invoke-WinUtilISOScript {
} }
} }
# ═════════════════════════════════════════════════════════════════════════ # ── 1. Remove provisioned AppX packages ──────────────────────────────────
# 1. Remove provisioned AppX packages
# ═════════════════════════════════════════════════════════════════════════
& $Log "Removing provisioned AppX packages..." & $Log "Removing provisioned AppX packages..."
$packages = & dism /English "/image:$ScratchDir" /Get-ProvisionedAppxPackages | $packages = & dism /English "/image:$ScratchDir" /Get-ProvisionedAppxPackages |
ForEach-Object { ForEach-Object { if ($_ -match 'PackageName : (.*)') { $matches[1] } }
if ($_ -match 'PackageName : (.*)') { $matches[1] }
}
$packagePrefixes = @( $packagePrefixes = @(
'AppUp.IntelManagementandSecurityStatus', 'AppUp.IntelManagementandSecurityStatus',
@@ -198,44 +160,26 @@ function Invoke-WinUtilISOScript {
'MicrosoftTeams' 'MicrosoftTeams'
) )
$packagesToRemove = $packages | Where-Object { $packages | Where-Object { $pkg = $_; $packagePrefixes | Where-Object { $pkg -like "*$_*" } } |
$pkg = $_ ForEach-Object { & dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$_" }
$packagePrefixes | Where-Object { $pkg -like "*$_*" }
}
foreach ($package in $packagesToRemove) {
& dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$package"
}
# ═════════════════════════════════════════════════════════════════════════
# 2. Inject hardware drivers (NVMe / Trackpad / Network)
# Injected into BOTH install.wim (OS) AND boot.wim index 2 (Setup).
# Without storage drivers in boot.wim, Windows Setup cannot see the
# target disk on systems with unsupported NVMe / SATA controllers.
# ═════════════════════════════════════════════════════════════════════════
& $Log "Exporting hardware drivers from running system (NVMe, HID/Trackpad, Network)..."
# ── 2. Inject current system drivers (optional) ───────────────────────────
if ($InjectCurrentSystemDrivers) {
& $Log "Exporting all drivers from running system..."
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)" $driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)"
New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null
try { try {
Export-WindowsDriver -Online -Destination $driverExportRoot | Out-Null Export-WindowsDriver -Online -Destination $driverExportRoot | Out-Null
# Stage matching driver folders then do a single DISM /Recurse call. & $Log "Injecting current system drivers into install.wim..."
# install.wim: SCSIAdapter + HIDClass + Net Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log
$installStage = New-DriverStagingDir -ExportRoot $driverExportRoot -Classes @('SCSIAdapter','HIDClass','Net')
& $Log "Injecting staged drivers into install.wim (single DISM call)..."
Add-DriversToImage -MountPath $ScratchDir -DriverDir $installStage -Label "install" -Logger $Log
Remove-Item -Path $installStage -Recurse -Force -ErrorAction SilentlyContinue
& $Log "install.wim driver injection complete." & $Log "install.wim driver injection complete."
# boot.wim: SCSIAdapter + Net only (HID not needed in WinPE)
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$bootWim = Join-Path $ISOContentsDir "sources\boot.wim" $bootWim = Join-Path $ISOContentsDir "sources\boot.wim"
if (Test-Path $bootWim) { if (Test-Path $bootWim) {
$bootStage = New-DriverStagingDir -ExportRoot $driverExportRoot -Classes @('SCSIAdapter','Net') & $Log "Injecting current system drivers into boot.wim..."
& $Log "Injecting staged drivers into boot.wim (single DISM call)..." Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log
Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $bootStage -Logger $Log
Remove-Item -Path $bootStage -Recurse -Force -ErrorAction SilentlyContinue
} else { } else {
& $Log "Warning: boot.wim not found — skipping boot.wim driver injection." & $Log "Warning: boot.wim not found — skipping boot.wim driver injection."
} }
@@ -245,79 +189,17 @@ function Invoke-WinUtilISOScript {
} finally { } finally {
Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
} }
# ── 2c. Optional: extended Storage & Network drivers from community repo ──
$extDriverChoice = [System.Windows.MessageBox]::Show(
"Would you like to add extended Storage and Network drivers?`n`n" +
"This installs EVERY Storage and Networking device driver " +
"in EXISTANCE into the image. (~1000 drivers)`n`n" +
"No Wireless drivers only Ethernet, use for stubborn systems " +
"with unsupported NVMe or Ethernet controllers.",
"Extended Drivers", "YesNo", "Question")
if ($extDriverChoice -eq 'Yes') {
& $Log "Extended driver injection requested."
# Ensure git is available
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitCmd) {
& $Log "Git not found — installing via winget..."
winget install --id Git.Git -e --source winget `
--accept-package-agreements --accept-source-agreements | Out-Null
# Refresh PATH so git is visible in this session
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('PATH', 'User')
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
}
if (-not $gitCmd) {
& $Log "Warning: git could not be found after install attempt — skipping extended drivers."
} else { } else {
$extRepoDir = Join-Path $env:TEMP "WinUtil_ExtDrivers_$(Get-Random)" & $Log "Driver injection skipped."
try {
& $Log "Cloning storage-lan-drivers repository..."
& git clone --depth 1 `
"https://github.com/ChrisTitusTech/storage-lan-drivers" `
$extRepoDir 2>&1 | ForEach-Object { & $Log " git: $_" }
if (Test-Path $extRepoDir) {
& $Log "Injecting extended drivers into install.wim (this may take several minutes)..."
Add-DriversToImage -MountPath $ScratchDir -DriverDir $extRepoDir -Label "install" -Logger $Log
& $Log "Extended driver injection into install.wim complete."
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$bootWimExt = Join-Path $ISOContentsDir "sources\boot.wim"
if (Test-Path $bootWimExt) {
& $Log "Injecting extended drivers into boot.wim..."
Invoke-BootWimInject -BootWimPath $bootWimExt -DriverDir $extRepoDir -Logger $Log
} else {
& $Log "Warning: boot.wim not found — skipping extended driver injection into boot.wim."
}
}
} else {
& $Log "Warning: repository clone directory not found — skipping extended drivers."
}
} catch {
& $Log "Error during extended driver injection: $_"
} finally {
Remove-Item -Path $extRepoDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
} else {
& $Log "Extended driver injection skipped."
} }
# ═════════════════════════════════════════════════════════════════════════ # ── 3. Remove OneDrive ────────────────────────────────────────────────────
# 3. Remove OneDrive
# ═════════════════════════════════════════════════════════════════════════
& $Log "Removing OneDrive..." & $Log "Removing OneDrive..."
& takeown /f "$ScratchDir\Windows\System32\OneDriveSetup.exe" | Out-Null & takeown /f "$ScratchDir\Windows\System32\OneDriveSetup.exe" | Out-Null
& icacls "$ScratchDir\Windows\System32\OneDriveSetup.exe" /grant "$($adminGroup.Value):(F)" /T /C | Out-Null & icacls "$ScratchDir\Windows\System32\OneDriveSetup.exe" /grant "$($adminGroup.Value):(F)" /T /C | Out-Null
Remove-Item -Path "$ScratchDir\Windows\System32\OneDriveSetup.exe" -Force -ErrorAction SilentlyContinue Remove-Item -Path "$ScratchDir\Windows\System32\OneDriveSetup.exe" -Force -ErrorAction SilentlyContinue
# ═════════════════════════════════════════════════════════════════════════ # ── 4. Registry tweaks ────────────────────────────────────────────────────
# 4. Registry tweaks
# ═════════════════════════════════════════════════════════════════════════
& $Log "Loading offline registry hives..." & $Log "Loading offline registry hives..."
reg load HKLM\zCOMPONENTS "$ScratchDir\Windows\System32\config\COMPONENTS" reg load HKLM\zCOMPONENTS "$ScratchDir\Windows\System32\config\COMPONENTS"
reg load HKLM\zDEFAULT "$ScratchDir\Windows\System32\config\default" reg load HKLM\zDEFAULT "$ScratchDir\Windows\System32\config\default"
@@ -366,14 +248,37 @@ function Invoke-WinUtilISOScript {
Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' 'BypassNRO' 'REG_DWORD' '1' Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' 'BypassNRO' 'REG_DWORD' '1'
if ($AutoUnattendXml) { if ($AutoUnattendXml) {
# ── Place autounattend.xml inside the WIM (Sysprep) ────────────────── try {
$sysprepDest = "$ScratchDir\Windows\System32\Sysprep\autounattend.xml" $xmlDoc = [xml]::new()
Set-Content -Path $sysprepDest -Value $AutoUnattendXml -Encoding UTF8 -Force $xmlDoc.LoadXml($AutoUnattendXml)
& $Log "Written autounattend.xml to Sysprep directory."
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$fileNodes = $xmlDoc.SelectNodes("//sg:File", $nsMgr)
if ($fileNodes -and $fileNodes.Count -gt 0) {
foreach ($fileNode in $fileNodes) {
$absPath = $fileNode.GetAttribute("path")
$relPath = $absPath -replace '^[A-Za-z]:[/\\]', ''
$destPath = Join-Path $ScratchDir $relPath
New-Item -Path (Split-Path $destPath -Parent) -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
$ext = [IO.Path]::GetExtension($destPath).ToLower()
$encoding = switch ($ext) {
{ $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8 }
{ $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true) }
default { [System.Text.Encoding]::Default }
}
[System.IO.File]::WriteAllBytes($destPath, ($encoding.GetPreamble() + $encoding.GetBytes($fileNode.InnerText.Trim())))
& $Log "Pre-staged setup script: $relPath"
}
} else {
& $Log "Warning: no <Extensions><File> nodes found in autounattend.xml — setup scripts not pre-staged."
}
} catch {
& $Log "Warning: could not pre-stage setup scripts from autounattend.xml: $_"
}
# ── Place autounattend.xml at the ISO / USB root ──────────────────────
# Windows Setup reads this file first (before booting into the OS),
# which is what drives the local-account / OOBE bypass at install time.
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$isoDest = Join-Path $ISOContentsDir "autounattend.xml" $isoDest = Join-Path $ISOContentsDir "autounattend.xml"
Set-Content -Path $isoDest -Value $AutoUnattendXml -Encoding UTF8 -Force Set-Content -Path $isoDest -Value $AutoUnattendXml -Encoding UTF8 -Force
@@ -448,12 +353,9 @@ function Invoke-WinUtilISOScript {
reg unload HKLM\zSOFTWARE reg unload HKLM\zSOFTWARE
reg unload HKLM\zSYSTEM reg unload HKLM\zSYSTEM
# ═════════════════════════════════════════════════════════════════════════ # ── 5. Delete scheduled task definition files ─────────────────────────────
# 5. Delete scheduled task definition files
# ═════════════════════════════════════════════════════════════════════════
& $Log "Deleting scheduled task definition files..." & $Log "Deleting scheduled task definition files..."
$tasksPath = "$ScratchDir\Windows\System32\Tasks" $tasksPath = "$ScratchDir\Windows\System32\Tasks"
Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" -Force -ErrorAction SilentlyContinue Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" -Force -ErrorAction SilentlyContinue
Remove-Item "$tasksPath\Microsoft\Windows\Customer Experience Improvement Program" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "$tasksPath\Microsoft\Windows\Customer Experience Improvement Program" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\ProgramDataUpdater" -Force -ErrorAction SilentlyContinue Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\ProgramDataUpdater" -Force -ErrorAction SilentlyContinue
@@ -465,12 +367,9 @@ function Invoke-WinUtilISOScript {
Remove-Item "$tasksPath\Microsoft\Windows\WaaSMedic" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "$tasksPath\Microsoft\Windows\WaaSMedic" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$tasksPath\Microsoft\Windows\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "$tasksPath\Microsoft\Windows\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$tasksPath\Microsoft\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "$tasksPath\Microsoft\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue
& $Log "Scheduled task files deleted." & $Log "Scheduled task files deleted."
# ═════════════════════════════════════════════════════════════════════════ # ── 6. Remove ISO support folder ─────────────────────────────────────────
# 6. Remove ISO support folder (fresh-install only; not needed)
# ═════════════════════════════════════════════════════════════════════════
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
& $Log "Removing ISO support\ folder..." & $Log "Removing ISO support\ folder..."
Remove-Item -Path (Join-Path $ISOContentsDir "support") -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path (Join-Path $ISOContentsDir "support") -Recurse -Force -ErrorAction SilentlyContinue

View File

@@ -1,13 +1,9 @@
function Invoke-WinUtilISORefreshUSBDrives { function Invoke-WinUtilISORefreshUSBDrives {
<#
.SYNOPSIS
Populates the USB drive ComboBox with all currently attached removable drives.
#>
$combo = $sync["WPFWin11ISOUSBDriveComboBox"] $combo = $sync["WPFWin11ISOUSBDriveComboBox"]
$combo.Items.Clear()
$removable = Get-Disk | Where-Object { $_.BusType -eq "USB" } | Sort-Object Number $removable = Get-Disk | Where-Object { $_.BusType -eq "USB" } | Sort-Object Number
$combo.Items.Clear()
if ($removable.Count -eq 0) { if ($removable.Count -eq 0) {
$combo.Items.Add("No USB drives detected") $combo.Items.Add("No USB drives detected")
$combo.SelectedIndex = 0 $combo.SelectedIndex = 0
@@ -17,37 +13,25 @@ function Invoke-WinUtilISORefreshUSBDrives {
foreach ($disk in $removable) { foreach ($disk in $removable) {
$sizeGB = [math]::Round($disk.Size / 1GB, 1) $sizeGB = [math]::Round($disk.Size / 1GB, 1)
$label = "Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)" $combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
$combo.Items.Add($label)
} }
$combo.SelectedIndex = 0 $combo.SelectedIndex = 0
Write-Win11ISOLog "Found $($removable.Count) USB drive(s)." Write-Win11ISOLog "Found $($removable.Count) USB drive(s)."
# Store disk objects for later use
$sync["Win11ISOUSBDisks"] = $removable $sync["Win11ISOUSBDisks"] = $removable
} }
function Invoke-WinUtilISOWriteUSB { function Invoke-WinUtilISOWriteUSB {
<#
.SYNOPSIS
Erases the selected USB drive and writes the modified Windows 11 ISO
content as a bootable installation drive (using DISM / robocopy approach).
#>
$contentsDir = $sync["Win11ISOContentsDir"] $contentsDir = $sync["Win11ISOContentsDir"]
$usbDisks = $sync["Win11ISOUSBDisks"] $usbDisks = $sync["Win11ISOUSBDisks"]
if (-not $contentsDir -or -not (Test-Path $contentsDir)) { if (-not $contentsDir -or -not (Test-Path $contentsDir)) {
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show("No modified ISO content found. Please complete Steps 1-3 first.", "Not Ready", "OK", "Warning")
"No modified ISO content found. Please complete Steps 1-3 first.",
"Not Ready", "OK", "Warning")
return return
} }
$selectedIndex = $sync["WPFWin11ISOUSBDriveComboBox"].SelectedIndex $selectedIndex = $sync["WPFWin11ISOUSBDriveComboBox"].SelectedIndex
if ($selectedIndex -lt 0 -or -not $usbDisks -or $selectedIndex -ge $usbDisks.Count) { if ($selectedIndex -lt 0 -or -not $usbDisks -or $selectedIndex -ge $usbDisks.Count) {
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show("Please select a USB drive from the dropdown.", "No Drive Selected", "OK", "Warning")
"Please select a USB drive from the dropdown.",
"No Drive Selected", "OK", "Warning")
return return
} }
@@ -87,6 +71,7 @@ function Invoke-WinUtilISOWriteUSB {
$sync["WPFWin11ISOStatusLog"].ScrollToEnd() $sync["WPFWin11ISOStatusLog"].ScrollToEnd()
}) })
} }
function SetProgress($label, $pct) { function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label $sync.progressBarTextBlock.Text = $label
@@ -95,38 +80,25 @@ function Invoke-WinUtilISOWriteUSB {
}) })
} }
try {
SetProgress "Formatting USB drive..." 10
# ── Helper: find a free drive letter (D-Z) ──────────────────────────
function Get-FreeDriveLetter { function Get-FreeDriveLetter {
$used = (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue).Name $used = (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue).Name
foreach ($c in [char[]](68..90)) { # D..Z foreach ($c in [char[]](68..90)) {
if ($used -notcontains [string]$c) { return $c } if ($used -notcontains [string]$c) { return $c }
} }
return $null return $null
} }
# ── Phase 1: Clean the disk via diskpart ──────────────────────────── try {
# Only run "clean" here. "convert gpt" in diskpart requires the disk to SetProgress "Formatting USB drive..." 10
# already be MBR; after a clean the disk is RAW, so convert gpt fails on
# many systems. We use Initialize-Disk (which accepts RAW disks) instead.
$dpScript1 = @"
select disk $diskNum
clean
exit
"@
$dpFile1 = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
$dpScript1 | Set-Content -Path $dpFile1 -Encoding ASCII
Log "Running diskpart clean on Disk $diskNum..."
$dpOut1 = diskpart /s $dpFile1 2>&1
Remove-Item $dpFile1 -Force -ErrorAction SilentlyContinue
$dpOut1 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
# ── Phase 2: Initialize as GPT via PowerShell ──────────────────────── # Phase 1: Clean disk via diskpart
# After "clean", Windows may still see the disk as initialized (stale $dpFile1 = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
# metadata). Initialize-Disk only accepts RAW disks; Set-Disk handles "select disk $diskNum`nclean`nexit" | Set-Content -Path $dpFile1 -Encoding ASCII
# already-initialized (MBR/GPT) disks with no partitions. Try both. Log "Running diskpart clean on Disk $diskNum..."
diskpart /s $dpFile1 2>&1 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
Remove-Item $dpFile1 -Force -ErrorAction SilentlyContinue
# Phase 2: Initialize as GPT
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
Update-Disk -Number $diskNum -ErrorAction SilentlyContinue Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
$diskObj = Get-Disk -Number $diskNum -ErrorAction Stop $diskObj = Get-Disk -Number $diskNum -ErrorAction Stop
@@ -138,32 +110,19 @@ exit
Log "Disk $diskNum converted to GPT (was $($diskObj.PartitionStyle))." Log "Disk $diskNum converted to GPT (was $($diskObj.PartitionStyle))."
} }
# ── Phase 3: Create partitions via diskpart ────────────────────────── # Phase 3: Create FAT32 partition via diskpart
# "create partition efi" is not supported on removable media.
# A single FAT32 primary partition is all that is needed for a UEFI-
# bootable Windows install USB - the firmware locates \EFI\Boot\bootx64.efi
# on any FAT32 volume regardless of GPT partition type.
# FAT32 label limit is 11 chars; "W11-yyMMdd" = 10, fits without trimming.
$volLabel = "W11-" + (Get-Date).ToString('yyMMdd') $volLabel = "W11-" + (Get-Date).ToString('yyMMdd')
$dpScript2 = @"
select disk $diskNum
create partition primary
format quick fs=fat32 label="$volLabel"
exit
"@
$dpFile2 = Join-Path $env:TEMP "winutil_diskpart2_$(Get-Random).txt" $dpFile2 = Join-Path $env:TEMP "winutil_diskpart2_$(Get-Random).txt"
$dpScript2 | Set-Content -Path $dpFile2 -Encoding ASCII "select disk $diskNum`ncreate partition primary`nformat quick fs=exfat label=`"$volLabel`"`nexit" |
Set-Content -Path $dpFile2 -Encoding ASCII
Log "Creating partitions on Disk $diskNum..." Log "Creating partitions on Disk $diskNum..."
$dpOut2 = diskpart /s $dpFile2 2>&1 diskpart /s $dpFile2 2>&1 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
Remove-Item $dpFile2 -Force -ErrorAction SilentlyContinue Remove-Item $dpFile2 -Force -ErrorAction SilentlyContinue
$dpOut2 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
SetProgress "Assigning drive letters..." 30 SetProgress "Assigning drive letters..." 30
Start-Sleep -Seconds 3 # allow Windows to settle after partition creation Start-Sleep -Seconds 3
Update-Disk -Number $diskNum -ErrorAction SilentlyContinue Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
# ── Explicitly assign drive letters via PowerShell ───────────────────
# This is reliable regardless of registry state, unlike diskpart assign.
$partitions = Get-Partition -DiskNumber $diskNum -ErrorAction Stop $partitions = Get-Partition -DiskNumber $diskNum -ErrorAction Stop
Log "Partitions on Disk $diskNum after format: $($partitions.Count)" Log "Partitions on Disk $diskNum after format: $($partitions.Count)"
foreach ($p in $partitions) { foreach ($p in $partitions) {
@@ -171,12 +130,10 @@ exit
} }
$winpePart = $partitions | Where-Object { $_.Type -eq "Basic" } | Select-Object -Last 1 $winpePart = $partitions | Where-Object { $_.Type -eq "Basic" } | Select-Object -Last 1
if (-not $winpePart) { if (-not $winpePart) {
throw "Could not find the WINPE (Basic) partition on Disk $diskNum after format." throw "Could not find the WINPE (Basic) partition on Disk $diskNum after format."
} }
# Remove stale letter first (noops if none), then assign a fresh one
try { Remove-PartitionAccessPath -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -AccessPath "$($winpePart.DriveLetter):" -ErrorAction SilentlyContinue } catch {} try { Remove-PartitionAccessPath -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -AccessPath "$($winpePart.DriveLetter):" -ErrorAction SilentlyContinue } catch {}
$usbLetter = Get-FreeDriveLetter $usbLetter = Get-FreeDriveLetter
if (-not $usbLetter) { throw "No free drive letters (D-Z) available to assign to the USB data partition." } if (-not $usbLetter) { throw "No free drive letters (D-Z) available to assign to the USB data partition." }
@@ -185,40 +142,15 @@ exit
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
$usbDrive = "${usbLetter}:" $usbDrive = "${usbLetter}:"
if (-not (Test-Path $usbDrive)) { if (-not (Test-Path $usbDrive)) { throw "Drive $usbDrive is not accessible after letter assignment." }
throw "Drive $usbDrive is not accessible after letter assignment."
}
Log "USB data partition: $usbDrive" Log "USB data partition: $usbDrive"
SetProgress "Copying Windows 11 files to USB..." 45 SetProgress "Copying Windows 11 files to USB..." 45
# ── Copy files (split large install.wim if > 4 GB for FAT32) ── # Copy files (exFAT supports files > 4 GB, no splitting needed)
$installWim = Join-Path $contentsDir "sources\install.wim"
if (Test-Path $installWim) {
$wimSizeMB = [math]::Round((Get-Item $installWim).Length / 1MB)
if ($wimSizeMB -gt 3800) {
# FAT32 limit split with DISM
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 | Out-Null
Split-WindowsImage -ImagePath $installWim `
-SplitImagePath $splitDest `
-FileSize 3800 -CheckIntegrity
Log "install.wim split complete."
# Copy everything else (exclude install.wim)
Log "Copying remaining files to USB..."
$robocopyArgs = @($contentsDir, $usbDrive, "/E", "/XF", "install.wim", "/NFL", "/NDL", "/NJH", "/NJS")
& robocopy @robocopyArgs
} else {
& robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS & robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS
}
} else {
& robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS
}
SetProgress "Finalising USB drive..." 90 SetProgress "Finalising USB drive..." 90
Log "Files copied to USB." Log "Files copied to USB."
SetProgress "USB write complete" 100 SetProgress "USB write complete" 100
Log "USB drive is ready for use." Log "USB drive is ready for use."
@@ -227,16 +159,12 @@ exit
"USB drive created successfully!`n`nYou can now boot from this drive to install Windows 11.", "USB drive created successfully!`n`nYou can now boot from this drive to install Windows 11.",
"USB Ready", "OK", "Info") "USB Ready", "OK", "Info")
}) })
} } catch {
catch {
Log "ERROR during USB write: $_" Log "ERROR during USB write: $_"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show("USB write failed:`n`n$_", "USB Write Error", "OK", "Error")
"USB write failed:`n`n$_",
"USB Write Error", "OK", "Error")
}) })
} } finally {
finally {
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = "" $sync.progressBarTextBlock.Text = ""

View File

@@ -535,10 +535,7 @@ $sync["FontScalingApplyButton"].Add_Click({
# ── Win11ISO Tab button handlers ────────────────────────────────────────────── # ── Win11ISO Tab button handlers ──────────────────────────────────────────────
$sync["WPFTab5BT"].Add_Click({ $sync["WPFTab5BT"].Add_Click({
$sync["Form"].Dispatcher.BeginInvoke( $sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
[System.Windows.Threading.DispatcherPriority]::Background,
[action]{ Invoke-WinUtilISOCheckExistingWork }
) | Out-Null
}) })
$sync["WPFWin11ISOBrowseButton"].Add_Click({ $sync["WPFWin11ISOBrowseButton"].Add_Click({

View File

@@ -278,7 +278,6 @@
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="ComboBox"> <ControlTemplate TargetType="ComboBox">
<Grid> <Grid>
<!-- Outer border gives the combo a visible box -->
<Border x:Name="OuterBorder" <Border x:Name="OuterBorder"
BorderBrush="{DynamicResource BorderColor}" BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1" BorderThickness="1"
@@ -289,7 +288,6 @@
BorderThickness="0" BorderThickness="0"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"> ClickMode="Press">
<!-- Text + arrow laid out in a two-column Grid -->
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -301,7 +299,6 @@
Background="Transparent" Background="Transparent"
HorizontalAlignment="Left" VerticalAlignment="Center" HorizontalAlignment="Left" VerticalAlignment="Center"
Margin="6,3,2,3"/> Margin="6,3,2,3"/>
<!-- Scalable vector chevron -->
<Path Grid.Column="1" <Path Grid.Column="1"
Data="M 0,0 L 8,0 L 4,5 Z" Data="M 0,0 L 8,0 L 4,5 Z"
Fill="{TemplateBinding Foreground}" Fill="{TemplateBinding Foreground}"
@@ -1364,20 +1361,17 @@
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem Header="Win11ISO" Visibility="Collapsed" Name="WPFTab5"> <TabItem Header="Win11ISO" Visibility="Collapsed" Name="WPFTab5">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="{DynamicResource TabContentMargin}"> <Grid Name="Win11ISOPanel" Margin="{DynamicResource TabContentMargin}" Background="Transparent">
<Grid Background="Transparent" Name="Win11ISOPanel">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Step 1: Select ISO --> <RowDefinition Height="Auto"/> <!-- Steps 1-4 -->
<RowDefinition Height="Auto"/> <!-- Step 2: Mount & Verify --> <RowDefinition Height="*"/> <!-- Log / Status -->
<RowDefinition Height="Auto"/> <!-- Step 3: Modify install.wim -->
<RowDefinition Height="Auto"/> <!-- Step 4: Output Options -->
<RowDefinition Height="Auto"/> <!-- Log / Status -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- ═══════════════════════════════════════════════════════════ --> <!-- Steps 1-4 -->
<!-- STEP 1 : Select Windows 11 ISO --> <StackPanel Grid.Row="0">
<!-- ═══════════════════════════════════════════════════════════ -->
<Grid Grid.Row="0" Name="WPFWin11ISOSelectSection" Margin="5" HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}"> <!-- ─── STEP 1 : Select Windows 11 ISO ─────────────── -->
<Grid Name="WPFWin11ISOSelectSection" Margin="5" HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -1465,11 +1459,8 @@
</Border> </Border>
</Grid> </Grid>
<!-- ═══════════════════════════════════════════════════════════ --> <!-- ─── STEP 2 : Mount & Verify ISO ──────────────────── -->
<!-- STEP 2 : Mount & Verify ISO --> <Grid Name="WPFWin11ISOMountSection"
<!-- ═══════════════════════════════════════════════════════════ -->
<Grid Grid.Row="1"
Name="WPFWin11ISOMountSection"
Margin="5" Margin="5"
Visibility="Collapsed" Visibility="Collapsed"
HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}"> HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
@@ -1494,6 +1485,13 @@
HorizontalAlignment="Left" HorizontalAlignment="Left"
Width="Auto" Padding="12,0" Width="Auto" Padding="12,0"
Height="{DynamicResource ButtonHeight}"/> Height="{DynamicResource ButtonHeight}"/>
<CheckBox Name="WPFWin11ISOInjectDrivers"
Content="Inject current system drivers"
FontSize="{DynamicResource FontSize}"
Foreground="{DynamicResource MainForegroundColor}"
IsChecked="False"
Margin="0,8,0,0"
ToolTip="Exports all drivers from this machine and injects them into install.wim and boot.wim. Recommended for systems with unsupported NVMe or network controllers."/>
</StackPanel> </StackPanel>
<!-- Verification results panel --> <!-- Verification results panel -->
@@ -1528,11 +1526,8 @@
</Border> </Border>
</Grid> </Grid>
<!-- ═══════════════════════════════════════════════════════════ --> <!-- ─── STEP 3 : Modify install.wim ───────────────────── -->
<!-- STEP 3 : Modify install.wim --> <StackPanel Name="WPFWin11ISOModifySection"
<!-- ═══════════════════════════════════════════════════════════ -->
<StackPanel Grid.Row="2"
Name="WPFWin11ISOModifySection"
Margin="5" Margin="5"
Visibility="Collapsed" Visibility="Collapsed"
HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}"> HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
@@ -1555,11 +1550,8 @@
Height="{DynamicResource ButtonHeight}"/> Height="{DynamicResource ButtonHeight}"/>
</StackPanel> </StackPanel>
<!-- ═══════════════════════════════════════════════════════════ --> <!-- ─── STEP 4 : Output Options ───────────────────────── -->
<!-- STEP 4 : Output Options --> <StackPanel Name="WPFWin11ISOOutputSection"
<!-- ═══════════════════════════════════════════════════════════ -->
<StackPanel Grid.Row="3"
Name="WPFWin11ISOOutputSection"
Margin="5" Margin="5"
Visibility="Collapsed" Visibility="Collapsed"
HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}"> HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
@@ -1646,31 +1638,37 @@
Margin="0,0,0,10"/> Margin="0,0,0,10"/>
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel>
</StackPanel> </StackPanel>
<!-- ═══════════════════════════════════════════════════════════ --> <!-- Status Log (fills remaining height) -->
<!-- Status / Log Output --> <Grid Grid.Row="1" Margin="5">
<!-- ═══════════════════════════════════════════════════════════ --> <Grid.RowDefinitions>
<StackPanel Grid.Row="4" Margin="5"> <RowDefinition Height="Auto"/>
<TextBlock FontSize="{DynamicResource FontSize}" FontWeight="Bold" <RowDefinition Height="*"/>
Foreground="{DynamicResource MainForegroundColor}" Margin="0,0,0,6"> </Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontSize="{DynamicResource FontSize}" FontWeight="Bold"
Foreground="{DynamicResource MainForegroundColor}"
Margin="0,0,0,4">
Status Log Status Log
</TextBlock> </TextBlock>
<TextBox Name="WPFWin11ISOStatusLog" <TextBox Grid.Row="1"
Name="WPFWin11ISOStatusLog"
IsReadOnly="True" IsReadOnly="True"
TextWrapping="Wrap" TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Visible"
Height="140" Padding="6" VerticalAlignment="Stretch"
Padding="6"
Background="{DynamicResource MainBackgroundColor}" Background="{DynamicResource MainBackgroundColor}"
Foreground="{DynamicResource MainForegroundColor}" Foreground="{DynamicResource MainForegroundColor}"
BorderBrush="{DynamicResource BorderColor}" BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1" BorderThickness="1"
Text="Ready. Please select a Windows 11 ISO to begin."/> Text="Ready. Please select a Windows 11 ISO to begin."/>
</StackPanel> </Grid>
</Grid> </Grid>
</ScrollViewer>
</TabItem> </TabItem>
</TabControl> </TabControl>
</Grid> </Grid>