Compare commits

...

17 Commits

Author SHA1 Message Date
Chris Titus
219b93989a large drive support and change to Exfat 2026-03-02 21:08:34 -06:00
Chris Titus Tech
e4cb061b0e Fix Scrollviewer in Status Log 2026-03-02 18:14:11 -06:00
Chris Titus Tech
d16642bf0e Fix OSCDIMG output and cleanup comments 2026-03-02 15:57:43 -06:00
Chris Titus Tech
1dc1b81439 improve clean up 2026-03-02 15:30:16 -06:00
Chris Titus Tech
3bc14e9b64 cleanup injection 2026-03-02 13:27:21 -06:00
Chris Titus Tech
8db5e6b461 fix first run probs 2026-03-02 13:19:28 -06:00
Chris Titus
d0012449ad fix single driver install issues 2026-02-25 16:59:44 -06:00
Chris Titus
18594f6d2d inject to boot.wim for install 2026-02-25 16:43:49 -06:00
Chris Titus
50c4398394 create log file on iso generation 2026-02-25 16:14:03 -06:00
Chris Titus
ed5ec52767 fix syntax error 2026-02-25 16:08:18 -06:00
Chris Titus
319ee4e555 fix verbage 2026-02-25 16:02:16 -06:00
Chris Titus
2ef7f2deb9 initial driver support 2026-02-25 15:16:51 -06:00
Chris Titus
cca9bee107 Add minimal driver injection 2026-02-25 13:48:54 -06:00
Chris Titus
669ecd9c64 expand ui and fix clean and reset 2026-02-25 11:44:02 -06:00
Chris Titus
64ea075727 Cleanup and Verbose output for copy 2026-02-25 11:27:20 -06:00
Chris Titus
773ea3a950 fix full button width 2026-02-25 11:05:45 -06:00
Chris Titus
410d3c5056 initial usb fixes 2026-02-25 10:44:33 -06:00
6 changed files with 669 additions and 602 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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) ───────────────────────────
foreach ($package in $packagesToRemove) { if ($InjectCurrentSystemDrivers) {
& dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$package" & $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."
}
}
} 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
@@ -272,8 +321,8 @@ function Invoke-WinUtilISOScript {
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate'
& $Log "Disabling Copilot..." & $Log "Disabling Copilot..."
Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1' Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1'
Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Edge' 'HubsSidebarEnabled' 'REG_DWORD' '0' Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Edge' 'HubsSidebarEnabled' 'REG_DWORD' '0'
Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' 'DisableSearchBoxSuggestions' 'REG_DWORD' '1' Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' 'DisableSearchBoxSuggestions' 'REG_DWORD' '1'
& $Log "Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..." & $Log "Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..."
@@ -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

View 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
}

View File

@@ -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

View File

@@ -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>
<ToggleButton x:Name="ToggleButton" <Border x:Name="OuterBorder"
Background="{TemplateBinding Background}" BorderBrush="{DynamicResource BorderColor}"
BorderBrush="{TemplateBinding Background}" BorderThickness="1"
BorderThickness="0" CornerRadius="{DynamicResource ButtonCornerRadius}"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Background="{TemplateBinding Background}">
ClickMode="Press"> <ToggleButton x:Name="ToggleButton"
<TextBlock Text="{TemplateBinding SelectionBoxItem}" Background="Transparent"
Foreground="{TemplateBinding Foreground}" BorderThickness="0"
Background="Transparent" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" ClickMode="Press">
/> <Grid>
</ToggleButton> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{TemplateBinding SelectionBoxItem}"
Foreground="{TemplateBinding Foreground}"
Background="Transparent"
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>
</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"/> <!-- Steps 1-4 -->
<RowDefinition Height="Auto"/> <!-- Step 1: Select ISO --> <RowDefinition Height="*"/> <!-- Log / Status -->
<RowDefinition Height="Auto"/> <!-- Step 2: Mount & Verify --> </Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Step 3: Modify install.wim -->
<RowDefinition Height="Auto"/> <!-- Step 4: Output Options -->
<RowDefinition Height="Auto"/> <!-- Log / Status -->
</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>
</Border>
<!-- ═══════════════════════════════════════════════════════════ --> </StackPanel>
<!-- Status / Log Output -->
<!-- ═══════════════════════════════════════════════════════════ -->
<Border Grid.Row="4" Style="{StaticResource BorderStyle}">
<StackPanel>
<TextBlock FontSize="{DynamicResource FontSize}" FontWeight="Bold"
Foreground="{DynamicResource MainForegroundColor}" Margin="0,0,0,6">
Status Log
</TextBlock>
<TextBox Name="WPFWin11ISOStatusLog"
IsReadOnly="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
Height="140" Padding="6"
Background="{DynamicResource MainBackgroundColor}"
Foreground="{DynamicResource MainForegroundColor}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
Text="Ready. Please select a Windows 11 ISO to begin."/>
</StackPanel>
</Border>
<!-- Status Log (fills remaining height) -->
<Grid Grid.Row="1" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontSize="{DynamicResource FontSize}" FontWeight="Bold"
Foreground="{DynamicResource MainForegroundColor}"
Margin="0,0,0,4">
Status Log
</TextBlock>
<TextBox Grid.Row="1"
Name="WPFWin11ISOStatusLog"
IsReadOnly="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Visible"
VerticalAlignment="Stretch"
Padding="6"
Background="{DynamicResource MainBackgroundColor}"
Foreground="{DynamicResource MainForegroundColor}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
Text="Ready. Please select a Windows 11 ISO to begin."/>
</Grid> </Grid>
</ScrollViewer>
</Grid>
</TabItem> </TabItem>
</TabControl> </TabControl>
</Grid> </Grid>