mirror of
https://github.com/ChrisTitusTech/winutil
synced 2026-04-06 06:38:31 +00:00
Compare commits
17 Commits
26.04.02
...
219b93989a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219b93989a | ||
|
|
e4cb061b0e | ||
|
|
d16642bf0e | ||
|
|
1dc1b81439 | ||
|
|
3bc14e9b64 | ||
|
|
8db5e6b461 | ||
|
|
d0012449ad | ||
|
|
18594f6d2d | ||
|
|
50c4398394 | ||
|
|
ed5ec52767 | ||
|
|
319ee4e555 | ||
|
|
2ef7f2deb9 | ||
|
|
cca9bee107 | ||
|
|
669ecd9c64 | ||
|
|
64ea075727 | ||
|
|
773ea3a950 | ||
|
|
410d3c5056 |
@@ -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,40 +103,25 @@ 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"]
|
||||||
@@ -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")
|
||||||
@@ -261,6 +196,7 @@ function Invoke-WinUtilISOModify {
|
|||||||
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
|
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
|
||||||
$sync["WPFWin11ISOStatusLog"].ScrollToEnd()
|
$sync["WPFWin11ISOStatusLog"].ScrollToEnd()
|
||||||
})
|
})
|
||||||
|
Add-Content -Path (Join-Path $workDir "WinUtil_Win11ISO.log") -Value "[$ts] $msg" -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
function SetProgress($label, $pct) {
|
function SetProgress($label, $pct) {
|
||||||
@@ -272,173 +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 300–800 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 4–5 GB; exporting a
|
|
||||||
# single index typically drops it to ~3 GB, saving 1–2 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"
|
||||||
})
|
})
|
||||||
}
|
} 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
|
||||||
@@ -446,13 +320,36 @@ function Invoke-WinUtilISOModify {
|
|||||||
$script.BeginInvoke() | Out-Null
|
$script.BeginInvoke() | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-WinUtilISOCleanAndReset {
|
function Invoke-WinUtilISOCheckExistingWork {
|
||||||
<#
|
if ($sync["Win11ISOContentsDir"] -and (Test-Path $sync["Win11ISOContentsDir"])) { return }
|
||||||
.SYNOPSIS
|
|
||||||
Deletes the temporary working directory created during ISO modification
|
|
||||||
and resets the entire ISO UI back to its initial state (Step 1 only).
|
|
||||||
#>
|
|
||||||
|
|
||||||
|
$existingWorkDir = Get-Item -Path (Join-Path $env:TEMP "WinUtil_Win11ISO*") -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.PSIsContainer } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $existingWorkDir) { return }
|
||||||
|
|
||||||
|
$isoContents = Join-Path $existingWorkDir.FullName "iso_contents"
|
||||||
|
if (-not (Test-Path $isoContents)) { return }
|
||||||
|
|
||||||
|
$sync["Win11ISOWorkDir"] = $existingWorkDir.FullName
|
||||||
|
$sync["Win11ISOContentsDir"] = $isoContents
|
||||||
|
|
||||||
|
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
|
||||||
|
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
|
||||||
|
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
|
||||||
|
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
|
||||||
|
|
||||||
|
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
|
||||||
|
Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)"
|
||||||
|
Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
|
||||||
|
Write-Win11ISOLog "Click 'Clean & Reset' if you want to start over with a new ISO."
|
||||||
|
|
||||||
|
[System.Windows.MessageBox]::Show(
|
||||||
|
"A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nStep 4 (output options) has been restored so you can save the already-modified image.`n`nClick 'Clean & Reset' in Step 4 if you want to start over.",
|
||||||
|
"Existing Work Found", "OK", "Info")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-WinUtilISOCleanAndReset {
|
||||||
$workDir = $sync["Win11ISOWorkDir"]
|
$workDir = $sync["Win11ISOWorkDir"]
|
||||||
|
|
||||||
if ($workDir -and (Test-Path $workDir)) {
|
if ($workDir -and (Test-Path $workDir)) {
|
||||||
@@ -460,17 +357,104 @@ function Invoke-WinUtilISOCleanAndReset {
|
|||||||
"This will delete the temporary working directory:`n`n$workDir`n`nAnd reset the interface back to the start.`n`nContinue?",
|
"This will delete the temporary working directory:`n`n$workDir`n`nAnd reset the interface back to the start.`n`nContinue?",
|
||||||
"Clean & Reset", "YesNo", "Warning")
|
"Clean & Reset", "YesNo", "Warning")
|
||||||
if ($confirm -ne "Yes") { return }
|
if ($confirm -ne "Yes") { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $false
|
||||||
|
|
||||||
|
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
||||||
|
$runspace.ApartmentState = "STA"
|
||||||
|
$runspace.ThreadOptions = "ReuseThread"
|
||||||
|
$runspace.Open()
|
||||||
|
$runspace.SessionStateProxy.SetVariable("sync", $sync)
|
||||||
|
$runspace.SessionStateProxy.SetVariable("workDir", $workDir)
|
||||||
|
|
||||||
|
$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()
|
||||||
|
})
|
||||||
|
Add-Content -Path (Join-Path $workDir "WinUtil_Win11ISO.log") -Value "[$ts] $msg" -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "Deleting temp directory: $workDir"
|
if ($workDir) {
|
||||||
Remove-Item -Path $workDir -Recurse -Force -ErrorAction Stop
|
$mountDir = Join-Path $workDir "wim_mount"
|
||||||
Write-Win11ISOLog "Temp directory deleted."
|
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 {
|
} catch {
|
||||||
Write-Win11ISOLog "WARNING: could not fully delete temp directory: $_"
|
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: $_" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clear all stored ISO state
|
if ($workDir -and (Test-Path $workDir)) {
|
||||||
|
Log "Scanning files to delete in: $workDir"
|
||||||
|
SetProgress "Scanning files..." 5
|
||||||
|
|
||||||
|
$allFiles = @(Get-ChildItem -Path $workDir -File -Recurse -Force -ErrorAction SilentlyContinue)
|
||||||
|
$allDirs = @(Get-ChildItem -Path $workDir -Directory -Recurse -Force -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { $_.FullName.Length } -Descending)
|
||||||
|
$total = $allFiles.Count
|
||||||
|
$deleted = 0
|
||||||
|
|
||||||
|
Log "Found $total files to delete."
|
||||||
|
|
||||||
|
foreach ($f in $allFiles) {
|
||||||
|
try { Remove-Item -Path $f.FullName -Force -ErrorAction Stop } catch { Log "WARNING: could not delete $($f.FullName): $_" }
|
||||||
|
$deleted++
|
||||||
|
if ($deleted % 100 -eq 0 -or $deleted -eq $total) {
|
||||||
|
$pct = [math]::Round(($deleted / [Math]::Max($total, 1)) * 85) + 5
|
||||||
|
SetProgress "Deleting files in $($f.Directory.Name)... ($deleted / $total)" $pct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($d in $allDirs) {
|
||||||
|
try { Remove-Item -Path $d.FullName -Force -ErrorAction SilentlyContinue } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try { Remove-Item -Path $workDir -Recurse -Force -ErrorAction Stop } catch {}
|
||||||
|
|
||||||
|
if (Test-Path $workDir) {
|
||||||
|
Log "WARNING: some items could not be deleted in $workDir"
|
||||||
|
} else {
|
||||||
|
Log "Temp directory deleted successfully."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log "No temp directory found — resetting UI."
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProgress "Resetting UI..." 95
|
||||||
|
Log "Resetting interface..."
|
||||||
|
|
||||||
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
$sync["Win11ISOWorkDir"] = $null
|
$sync["Win11ISOWorkDir"] = $null
|
||||||
$sync["Win11ISOContentsDir"] = $null
|
$sync["Win11ISOContentsDir"] = $null
|
||||||
$sync["Win11ISOImagePath"] = $null
|
$sync["Win11ISOImagePath"] = $null
|
||||||
@@ -479,7 +463,6 @@ function Invoke-WinUtilISOCleanAndReset {
|
|||||||
$sync["Win11ISOImageInfo"] = $null
|
$sync["Win11ISOImageInfo"] = $null
|
||||||
$sync["Win11ISOUSBDisks"] = $null
|
$sync["Win11ISOUSBDisks"] = $null
|
||||||
|
|
||||||
# Reset the UI to the initial state
|
|
||||||
$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"
|
||||||
@@ -488,23 +471,35 @@ function Invoke-WinUtilISOCleanAndReset {
|
|||||||
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
|
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
|
||||||
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
|
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
|
||||||
$sync["WPFWin11ISOSelectSection"].Visibility = "Visible"
|
$sync["WPFWin11ISOSelectSection"].Visibility = "Visible"
|
||||||
$sync["WPFWin11ISOStatusLog"].Text = "Ready. Please select a Windows 11 ISO to begin."
|
|
||||||
$sync["WPFWin11ISOStatusLog"].Height = 140
|
|
||||||
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
|
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
|
||||||
|
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $true
|
||||||
|
|
||||||
|
$sync.progressBarTextBlock.Text = ""
|
||||||
|
$sync.progressBarTextBlock.ToolTip = ""
|
||||||
|
$sync.ProgressBar.Value = 0
|
||||||
|
|
||||||
|
$sync["WPFWin11ISOStatusLog"].Text = "Ready. Please select a Windows 11 ISO to begin."
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
Log "ERROR during Clean & Reset: $_"
|
||||||
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
|
$sync.progressBarTextBlock.Text = ""
|
||||||
|
$sync.progressBarTextBlock.ToolTip = ""
|
||||||
|
$sync.ProgressBar.Value = 0
|
||||||
|
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}) | Out-Null
|
||||||
|
|
||||||
|
$script.BeginInvoke() | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
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 1–3 first.",
|
"No modified ISO content found. Please complete Steps 1-3 first.",
|
||||||
"Not Ready", "OK", "Warning")
|
"Not Ready", "OK", "Warning")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -520,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 |
|
||||||
@@ -535,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
|
||||||
@@ -549,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",
|
||||||
@@ -559,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 " "
|
||||||
@@ -586,230 +596,46 @@ 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: $_"
|
||||||
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
|
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Start-Sleep -Milliseconds 800
|
|
||||||
Set-WinUtilProgressBar -Label "" -Percent 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-WinUtilISORefreshUSBDrives {
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Populates the USB drive ComboBox with all currently attached removable drives.
|
|
||||||
#>
|
|
||||||
$combo = $sync["WPFWin11ISOUSBDriveComboBox"]
|
|
||||||
$combo.Items.Clear()
|
|
||||||
|
|
||||||
$removable = Get-Disk | Where-Object { $_.BusType -eq "USB" } | Sort-Object Number
|
|
||||||
|
|
||||||
if ($removable.Count -eq 0) {
|
|
||||||
$combo.Items.Add("No USB drives detected")
|
|
||||||
$combo.SelectedIndex = 0
|
|
||||||
Write-Win11ISOLog "No USB drives detected."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($disk in $removable) {
|
|
||||||
$sizeGB = [math]::Round($disk.Size / 1GB, 1)
|
|
||||||
$label = "Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] — $($disk.PartitionStyle)"
|
|
||||||
$combo.Items.Add($label)
|
|
||||||
}
|
|
||||||
$combo.SelectedIndex = 0
|
|
||||||
Write-Win11ISOLog "Found $($removable.Count) USB drive(s)."
|
|
||||||
|
|
||||||
# Store disk objects for later use
|
|
||||||
$sync["Win11ISOUSBDisks"] = $removable
|
|
||||||
}
|
|
||||||
|
|
||||||
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"]
|
|
||||||
$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
|
|
||||||
}
|
|
||||||
|
|
||||||
$selectedIndex = $sync["WPFWin11ISOUSBDriveComboBox"].SelectedIndex
|
|
||||||
if ($selectedIndex -lt 0 -or -not $usbDisks -or $selectedIndex -ge $usbDisks.Count) {
|
|
||||||
[System.Windows.MessageBox]::Show(
|
|
||||||
"Please select a USB drive from the dropdown.",
|
|
||||||
"No Drive Selected", "OK", "Warning")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetDisk = $usbDisks[$selectedIndex]
|
|
||||||
$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-Win11ISOLog "USB write cancelled by user."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
|
|
||||||
Write-Win11ISOLog "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()
|
|
||||||
})
|
})
|
||||||
}
|
} finally {
|
||||||
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 {
|
|
||||||
SetProgress "Formatting USB drive..." 10
|
|
||||||
|
|
||||||
# ── Diskpart script: clean, GPT, create ESP + data partitions ──
|
|
||||||
$dpScript = @"
|
|
||||||
select disk $diskNum
|
|
||||||
clean
|
|
||||||
convert gpt
|
|
||||||
create partition efi size=512
|
|
||||||
format quick fs=fat32 label="SYSTEM"
|
|
||||||
assign
|
|
||||||
create partition primary
|
|
||||||
format quick fs=fat32 label="WINPE"
|
|
||||||
assign
|
|
||||||
exit
|
|
||||||
"@
|
|
||||||
$dpFile = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
|
|
||||||
$dpScript | Set-Content -Path $dpFile -Encoding ASCII
|
|
||||||
Log "Running diskpart on Disk $diskNum..."
|
|
||||||
diskpart /s $dpFile | Out-Null
|
|
||||||
Remove-Item $dpFile -Force
|
|
||||||
|
|
||||||
SetProgress "Identifying USB partitions..." 30
|
|
||||||
Start-Sleep -Seconds 3 # let Windows assign drive letters
|
|
||||||
|
|
||||||
# Find newly assigned drive letter for the data partition
|
|
||||||
$usbVol = Get-Partition -DiskNumber $diskNum |
|
|
||||||
Where-Object { $_.Type -eq "Basic" } |
|
|
||||||
Get-Volume |
|
|
||||||
Where-Object { $_.FileSystemLabel -eq "WINPE" } |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
if (-not $usbVol) {
|
|
||||||
throw "Could not locate the formatted USB data partition. Drive letter may not have been assigned automatically."
|
|
||||||
}
|
|
||||||
|
|
||||||
$usbDrive = "$($usbVol.DriveLetter):"
|
|
||||||
Log "USB data partition: $usbDrive"
|
|
||||||
SetProgress "Copying Windows 11 files to USB..." 45
|
|
||||||
|
|
||||||
# ── Copy files (split large install.wim if > 4 GB for FAT32) ──
|
|
||||||
$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..."
|
|
||||||
$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 | Out-Null
|
|
||||||
Log "install.wim split complete."
|
|
||||||
|
|
||||||
# Copy everything else (exclude install.wim)
|
|
||||||
$robocopyArgs = @($contentsDir, $usbDrive, "/E", "/XF", "install.wim", "/NFL", "/NDL", "/NJH", "/NJS")
|
|
||||||
& robocopy @robocopyArgs | Out-Null
|
|
||||||
} else {
|
|
||||||
& robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS | Out-Null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
& robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
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["WPFWin11ISOWriteUSBButton"].IsEnabled = $true
|
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}) | Out-Null
|
}) | Out-Null
|
||||||
|
|||||||
@@ -4,50 +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
|
||||||
Performs the following operations against an already-mounted WIM image:
|
Removes AppX bloatware and OneDrive, optionally injects all drivers exported from
|
||||||
|
the running system into install.wim and boot.wim (controlled by the
|
||||||
|
-InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware
|
||||||
|
bypass, privacy, OOBE, telemetry, update suppression), deletes CEIP/WU
|
||||||
|
scheduled-task definition files, and optionally writes autounattend.xml to the ISO
|
||||||
|
root and removes the support\ folder from the ISO contents directory.
|
||||||
|
|
||||||
1. Removes provisioned AppX bloatware packages via DISM.
|
All setup scripts embedded in the autounattend.xml <Extensions><File> nodes are
|
||||||
2. Removes OneDriveSetup.exe from the system image.
|
written directly into the WIM at their target paths under C:\Windows\Setup\Scripts\
|
||||||
3. Loads offline registry hives (COMPONENTS, DEFAULT, NTUSER, SOFTWARE, SYSTEM)
|
to ensure they survive Windows Setup stripping unrecognised-namespace XML elements
|
||||||
and applies the following tweaks:
|
from the Panther copy of the answer file.
|
||||||
- Bypasses hardware requirement checks (CPU, RAM, SecureBoot, Storage, TPM).
|
|
||||||
- Disables sponsored-app delivery and ContentDeliveryManager features.
|
|
||||||
- Enables local-account OOBE path (BypassNRO).
|
|
||||||
- Writes autounattend.xml to the Sysprep directory inside the WIM and,
|
|
||||||
optionally, to the ISO/USB root so Windows Setup picks it up at boot.
|
|
||||||
- Disables reserved storage.
|
|
||||||
- Disables BitLocker device encryption.
|
|
||||||
- Hides the Chat (Teams) taskbar icon.
|
|
||||||
- Disables OneDrive folder backup (KFM).
|
|
||||||
- Disables telemetry, advertising ID, and input personalization.
|
|
||||||
- Blocks post-install delivery of DevHome, Outlook, and Teams.
|
|
||||||
- Disables Windows Copilot.
|
|
||||||
- Disables Windows Update during OOBE.
|
|
||||||
4. Deletes unwanted scheduled-task XML definition files (CEIP, Appraiser, etc.).
|
|
||||||
5. Removes the support\ folder from the ISO contents directory (if supplied).
|
|
||||||
|
|
||||||
Mounting and dismounting the WIM is the responsibility of the caller
|
Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO).
|
||||||
(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"
|
||||||
@@ -62,24 +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.22
|
Version : 26.03.02
|
||||||
#>
|
#>
|
||||||
param (
|
param (
|
||||||
[Parameter(Mandatory)][string]$ScratchDir,
|
[Parameter(Mandatory)][string]$ScratchDir,
|
||||||
# Root directory of the extracted ISO contents. When supplied, autounattend.xml
|
|
||||||
# is written here so Windows Setup picks it up automatically at boot.
|
|
||||||
[string]$ISOContentsDir = "",
|
[string]$ISOContentsDir = "",
|
||||||
# Autounattend XML content. In compiled winutil.ps1 this comes from the embedded
|
|
||||||
# $WinUtilAutounattendXml here-string; in dev mode it is read from tools\autounattend.xml.
|
|
||||||
[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 {
|
||||||
@@ -100,15 +83,37 @@ function Invoke-WinUtilISOScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ═════════════════════════════════════════════════════════════════════════
|
function Add-DriversToImage {
|
||||||
# 1. Remove provisioned AppX packages
|
param ([string]$MountPath, [string]$DriverDir, [string]$Label = "image", [scriptblock]$Logger)
|
||||||
# ═════════════════════════════════════════════════════════════════════════
|
& dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse 2>&1 |
|
||||||
|
ForEach-Object { & $Logger " dism[$Label]: $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-BootWimInject {
|
||||||
|
param ([string]$BootWimPath, [string]$DriverDir, [scriptblock]$Logger)
|
||||||
|
Set-ItemProperty -Path $BootWimPath -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue
|
||||||
|
$mountDir = Join-Path $env:TEMP "WinUtil_BootMount_$(Get-Random)"
|
||||||
|
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
|
||||||
|
try {
|
||||||
|
& $Logger "Mounting boot.wim (index 2) for driver injection..."
|
||||||
|
Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir -ErrorAction Stop | Out-Null
|
||||||
|
Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger
|
||||||
|
& $Logger "Saving boot.wim..."
|
||||||
|
Dismount-WindowsImage -Path $mountDir -Save -ErrorAction Stop | Out-Null
|
||||||
|
& $Logger "boot.wim driver injection complete."
|
||||||
|
} catch {
|
||||||
|
& $Logger "Warning: boot.wim driver injection failed: $_"
|
||||||
|
try { Dismount-WindowsImage -Path $mountDir -Discard -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||||
|
} finally {
|
||||||
|
Remove-Item -Path $mountDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 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',
|
||||||
@@ -155,25 +160,46 @@ 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 "*$_*" }
|
|
||||||
|
# ── 2. Inject current system drivers (optional) ───────────────────────────
|
||||||
|
if ($InjectCurrentSystemDrivers) {
|
||||||
|
& $Log "Exporting all drivers from running system..."
|
||||||
|
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)"
|
||||||
|
New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null
|
||||||
|
try {
|
||||||
|
Export-WindowsDriver -Online -Destination $driverExportRoot | Out-Null
|
||||||
|
|
||||||
|
& $Log "Injecting current system drivers into install.wim..."
|
||||||
|
Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log
|
||||||
|
& $Log "install.wim driver injection complete."
|
||||||
|
|
||||||
|
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
|
||||||
|
$bootWim = Join-Path $ISOContentsDir "sources\boot.wim"
|
||||||
|
if (Test-Path $bootWim) {
|
||||||
|
& $Log "Injecting current system drivers into boot.wim..."
|
||||||
|
Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log
|
||||||
|
} else {
|
||||||
|
& $Log "Warning: boot.wim not found — skipping boot.wim driver injection."
|
||||||
}
|
}
|
||||||
foreach ($package in $packagesToRemove) {
|
}
|
||||||
& dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$package"
|
} catch {
|
||||||
|
& $Log "Error during driver export/injection: $_"
|
||||||
|
} finally {
|
||||||
|
Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
& $Log "Driver injection skipped."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ═════════════════════════════════════════════════════════════════════════
|
# ── 3. Remove OneDrive ────────────────────────────────────────────────────
|
||||||
# 2. 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 ────────────────────────────────────────────────────
|
||||||
# 3. 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"
|
||||||
@@ -222,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
|
||||||
@@ -304,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 ─────────────────────────────
|
||||||
# 4. 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
|
||||||
@@ -321,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 ─────────────────────────────────────────
|
||||||
# 5. 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
|
||||||
|
|||||||
179
functions/private/Invoke-WinUtilISOUSB.ps1
Normal file
179
functions/private/Invoke-WinUtilISOUSB.ps1
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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
|
||||||
|
Write-Win11ISOLog "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-Win11ISOLog "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
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedIndex = $sync["WPFWin11ISOUSBDriveComboBox"].SelectedIndex
|
||||||
|
if ($selectedIndex -lt 0 -or -not $usbDisks -or $selectedIndex -ge $usbDisks.Count) {
|
||||||
|
[System.Windows.MessageBox]::Show("Please select a USB drive from the dropdown.", "No Drive Selected", "OK", "Warning")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDisk = $usbDisks[$selectedIndex]
|
||||||
|
$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-Win11ISOLog "USB write cancelled by user."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
|
||||||
|
Write-Win11ISOLog "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.progressBarTextBlock.Text = $label
|
||||||
|
$sync.progressBarTextBlock.ToolTip = $label
|
||||||
|
$sync.ProgressBar.Value = [Math]::Max($pct, 5)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FreeDriveLetter {
|
||||||
|
$used = (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue).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
|
||||||
|
$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..."
|
||||||
|
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
|
||||||
|
Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
|
||||||
|
$diskObj = Get-Disk -Number $diskNum -ErrorAction Stop
|
||||||
|
if ($diskObj.PartitionStyle -eq 'RAW') {
|
||||||
|
Initialize-Disk -Number $diskNum -PartitionStyle GPT -ErrorAction Stop
|
||||||
|
Log "Disk $diskNum initialized as GPT."
|
||||||
|
} else {
|
||||||
|
Set-Disk -Number $diskNum -PartitionStyle GPT -ErrorAction Stop
|
||||||
|
Log "Disk $diskNum converted to GPT (was $($diskObj.PartitionStyle))."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Phase 3: Create FAT32 partition via diskpart
|
||||||
|
$volLabel = "W11-" + (Get-Date).ToString('yyMMdd')
|
||||||
|
$dpFile2 = Join-Path $env:TEMP "winutil_diskpart2_$(Get-Random).txt"
|
||||||
|
"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..."
|
||||||
|
diskpart /s $dpFile2 2>&1 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
|
||||||
|
Remove-Item $dpFile2 -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
SetProgress "Assigning drive letters..." 30
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$partitions = Get-Partition -DiskNumber $diskNum -ErrorAction Stop
|
||||||
|
Log "Partitions on Disk $diskNum after format: $($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 WINPE (Basic) partition on Disk $diskNum after format."
|
||||||
|
}
|
||||||
|
|
||||||
|
try { Remove-PartitionAccessPath -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -AccessPath "$($winpePart.DriveLetter):" -ErrorAction SilentlyContinue } catch {}
|
||||||
|
$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}:"
|
||||||
|
if (-not (Test-Path $usbDrive)) { throw "Drive $usbDrive is not accessible after letter assignment." }
|
||||||
|
Log "USB data partition: $usbDrive"
|
||||||
|
SetProgress "Copying Windows 11 files to USB..." 45
|
||||||
|
|
||||||
|
# Copy files (exFAT supports files > 4 GB, no splitting needed)
|
||||||
|
& 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["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
|
$sync.progressBarTextBlock.Text = ""
|
||||||
|
$sync.progressBarTextBlock.ToolTip = ""
|
||||||
|
$sync.ProgressBar.Value = 0
|
||||||
|
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}) | Out-Null
|
||||||
|
|
||||||
|
$script.BeginInvoke() | Out-Null
|
||||||
|
}
|
||||||
@@ -534,6 +534,10 @@ $sync["FontScalingApplyButton"].Add_Click({
|
|||||||
|
|
||||||
# ── Win11ISO Tab button handlers ──────────────────────────────────────────────
|
# ── Win11ISO Tab button handlers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
$sync["WPFTab5BT"].Add_Click({
|
||||||
|
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
|
||||||
|
})
|
||||||
|
|
||||||
$sync["WPFWin11ISOBrowseButton"].Add_Click({
|
$sync["WPFWin11ISOBrowseButton"].Add_Click({
|
||||||
Write-Debug "WPFWin11ISOBrowseButton clicked"
|
Write-Debug "WPFWin11ISOBrowseButton clicked"
|
||||||
Invoke-WinUtilISOBrowse
|
Invoke-WinUtilISOBrowse
|
||||||
|
|||||||
@@ -273,22 +273,43 @@
|
|||||||
<Style TargetType="ComboBox">
|
<Style TargetType="ComboBox">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundColor}" />
|
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundColor}" />
|
||||||
<Setter Property="Background" Value="{DynamicResource ComboBoxBackgroundColor}" />
|
<Setter Property="Background" Value="{DynamicResource ComboBoxBackgroundColor}" />
|
||||||
|
<Setter Property="MinWidth" Value="{DynamicResource ButtonWidth}" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="ComboBox">
|
<ControlTemplate TargetType="ComboBox">
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Border x:Name="OuterBorder"
|
||||||
|
BorderBrush="{DynamicResource BorderColor}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="{DynamicResource ButtonCornerRadius}"
|
||||||
|
Background="{TemplateBinding Background}">
|
||||||
<ToggleButton x:Name="ToggleButton"
|
<ToggleButton x:Name="ToggleButton"
|
||||||
Background="{TemplateBinding Background}"
|
Background="Transparent"
|
||||||
BorderBrush="{TemplateBinding Background}"
|
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
ClickMode="Press">
|
ClickMode="Press">
|
||||||
<TextBlock Text="{TemplateBinding SelectionBoxItem}"
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{TemplateBinding SelectionBoxItem}"
|
||||||
Foreground="{TemplateBinding Foreground}"
|
Foreground="{TemplateBinding Foreground}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2"
|
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
/>
|
Margin="6,3,2,3"/>
|
||||||
|
<Path Grid.Column="1"
|
||||||
|
Data="M 0,0 L 8,0 L 4,5 Z"
|
||||||
|
Fill="{TemplateBinding Foreground}"
|
||||||
|
Width="8" Height="5"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Stretch="Uniform"
|
||||||
|
Margin="4,0,6,0"/>
|
||||||
|
</Grid>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
</Border>
|
||||||
<Popup x:Name="Popup"
|
<Popup x:Name="Popup"
|
||||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||||
Placement="Bottom"
|
Placement="Bottom"
|
||||||
@@ -297,11 +318,11 @@
|
|||||||
PopupAnimation="Slide">
|
PopupAnimation="Slide">
|
||||||
<Border x:Name="DropDownBorder"
|
<Border x:Name="DropDownBorder"
|
||||||
Background="{TemplateBinding Background}"
|
Background="{TemplateBinding Background}"
|
||||||
BorderBrush="{TemplateBinding Foreground}"
|
BorderBrush="{DynamicResource BorderColor}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="4">
|
CornerRadius="4">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<ItemsPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2"/>
|
<ItemsPresenter HorizontalAlignment="Left" VerticalAlignment="Center" Margin="4,2"/>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
</Popup>
|
</Popup>
|
||||||
@@ -1340,21 +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">
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
|
||||||
<Border Grid.Row="0" Name="WPFWin11ISOSelectSection" Style="{StaticResource BorderStyle}">
|
<!-- ─── STEP 1 : Select Windows 11 ISO ─────────────── -->
|
||||||
<Grid Margin="5">
|
<Grid Name="WPFWin11ISOSelectSection" Margin="5" HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -1441,16 +1458,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<!-- ─── STEP 2 : Mount & Verify ISO ──────────────────── -->
|
||||||
<!-- STEP 2 : Mount & Verify ISO -->
|
<Grid Name="WPFWin11ISOMountSection"
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
Margin="5"
|
||||||
<Border Grid.Row="1"
|
Visibility="Collapsed"
|
||||||
Name="WPFWin11ISOMountSection"
|
HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
|
||||||
Style="{StaticResource BorderStyle}"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<Grid Margin="5">
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -1472,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 -->
|
||||||
@@ -1500,22 +1520,17 @@
|
|||||||
FontSize="{DynamicResource FontSize}"
|
FontSize="{DynamicResource FontSize}"
|
||||||
Foreground="{DynamicResource MainForegroundColor}"
|
Foreground="{DynamicResource MainForegroundColor}"
|
||||||
Background="{DynamicResource MainBackgroundColor}"
|
Background="{DynamicResource MainBackgroundColor}"
|
||||||
BorderBrush="{DynamicResource BorderColor}"
|
HorizontalAlignment="Left"
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Margin="0,0,0,0"/>
|
Margin="0,0,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<!-- ─── STEP 3 : Modify install.wim ───────────────────── -->
|
||||||
<!-- STEP 3 : Modify install.wim -->
|
<StackPanel Name="WPFWin11ISOModifySection"
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
Margin="5"
|
||||||
<Border Grid.Row="2"
|
Visibility="Collapsed"
|
||||||
Name="WPFWin11ISOModifySection"
|
HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
|
||||||
Style="{StaticResource BorderStyle}"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<StackPanel Margin="5">
|
|
||||||
<TextBlock FontSize="{DynamicResource FontSize}" FontWeight="Bold"
|
<TextBlock FontSize="{DynamicResource FontSize}" FontWeight="Bold"
|
||||||
Foreground="{DynamicResource MainForegroundColor}" Margin="0,0,0,8">
|
Foreground="{DynamicResource MainForegroundColor}" Margin="0,0,0,8">
|
||||||
Step 3 - Modify install.wim
|
Step 3 - Modify install.wim
|
||||||
@@ -1534,15 +1549,12 @@
|
|||||||
Width="Auto" Padding="12,0"
|
Width="Auto" Padding="12,0"
|
||||||
Height="{DynamicResource ButtonHeight}"/>
|
Height="{DynamicResource ButtonHeight}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<!-- ─── STEP 4 : Output Options ───────────────────────── -->
|
||||||
<!-- STEP 4 : Output Options -->
|
<StackPanel Name="WPFWin11ISOOutputSection"
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
Margin="5"
|
||||||
<Border Grid.Row="3"
|
Visibility="Collapsed"
|
||||||
Name="WPFWin11ISOOutputSection"
|
HorizontalAlignment="Left" MinWidth="{DynamicResource ButtonWidth}">
|
||||||
Style="{StaticResource BorderStyle}">
|
|
||||||
<StackPanel Margin="5">
|
|
||||||
<!-- Header row: title + Clean & Reset button -->
|
<!-- Header row: title + Clean & Reset button -->
|
||||||
<Grid Margin="0,0,0,12">
|
<Grid Margin="0,0,0,12">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -1579,7 +1591,7 @@
|
|||||||
Height="{DynamicResource ButtonHeight}"/>
|
Height="{DynamicResource ButtonHeight}"/>
|
||||||
<Button Grid.Column="2"
|
<Button Grid.Column="2"
|
||||||
Name="WPFWin11ISOChooseUSBButton"
|
Name="WPFWin11ISOChooseUSBButton"
|
||||||
Content="Write Directly to a USB Drive (erases drive)"
|
Content="Write Directly to a USB Drive (ERASES DRIVE)"
|
||||||
Foreground="OrangeRed"
|
Foreground="OrangeRed"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Width="Auto" Padding="12,0"
|
Width="Auto" Padding="12,0"
|
||||||
@@ -1613,7 +1625,7 @@
|
|||||||
Margin="0,0,6,0"/>
|
Margin="0,0,6,0"/>
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
Name="WPFWin11ISORefreshUSBButton"
|
Name="WPFWin11ISORefreshUSBButton"
|
||||||
Content="↻ Refresh"
|
Content="Refresh"
|
||||||
Width="Auto" Padding="8,0"
|
Width="Auto" Padding="8,0"
|
||||||
Height="{DynamicResource ButtonHeight}"/>
|
Height="{DynamicResource ButtonHeight}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -1626,34 +1638,37 @@
|
|||||||
Margin="0,0,0,10"/>
|
Margin="0,0,0,10"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<!-- Status Log (fills remaining height) -->
|
||||||
<!-- Status / Log Output -->
|
<Grid Grid.Row="1" Margin="5">
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<Grid.RowDefinitions>
|
||||||
<Border Grid.Row="4" Style="{StaticResource BorderStyle}">
|
<RowDefinition Height="Auto"/>
|
||||||
<StackPanel>
|
<RowDefinition Height="*"/>
|
||||||
<TextBlock FontSize="{DynamicResource FontSize}" FontWeight="Bold"
|
</Grid.RowDefinitions>
|
||||||
Foreground="{DynamicResource MainForegroundColor}" Margin="0,0,0,6">
|
<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>
|
||||||
</Border>
|
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
Reference in New Issue
Block a user