Compare commits

...

2 Commits

Author SHA1 Message Date
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
3 changed files with 110 additions and 129 deletions

View File

@@ -223,6 +223,8 @@ function Invoke-WinUtilISOModify {
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true
$runspace.SessionStateProxy.SetVariable("sync", $sync)
$runspace.SessionStateProxy.SetVariable("isoPath", $isoPath)
$runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter)
@@ -231,6 +233,7 @@ function Invoke-WinUtilISOModify {
$runspace.SessionStateProxy.SetVariable("selectedWimIndex", $selectedWimIndex)
$runspace.SessionStateProxy.SetVariable("selectedEditionName", $selectedEditionName)
$runspace.SessionStateProxy.SetVariable("autounattendContent", $autounattendContent)
$runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers)
# Serialize functions so they are available inside the runspace
$isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + `
@@ -324,7 +327,7 @@ function Invoke-WinUtilISOModify {
# ── Apply all WinUtil modifications via Invoke-WinUtilISOScript ──
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,

View File

@@ -4,13 +4,22 @@ function Invoke-WinUtilISOScript {
Applies WinUtil modifications to a mounted Windows 11 install.wim image.
.DESCRIPTION
Removes AppX bloatware and OneDrive, injects hardware drivers (NVMe, Precision
Touchpad/HID, and network) exported from the running system, optionally injects
extended Storage & Network drivers from the ChrisTitusTech/storage-lan-drivers
repository (requires git, installed via winget if absent), applies offline registry
tweaks (hardware bypass, privacy, OOBE, telemetry, update suppression), deletes
CEIP/WU scheduled-task definition files, and optionally drops autounattend.xml and
removes the support\ folder from the ISO contents directory.
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.
All setup scripts embedded in the autounattend.xml <Extensions><File> nodes
(Specialize.ps1, DefaultUser.ps1, FirstLogon.ps1, UserOnce.ps1, etc.) are written
directly into the WIM at their target paths under C:\Windows\Setup\Scripts\. This
pre-staging is necessary because Windows Setup strips unrecognised-namespace XML
elements — including the Schneegans <Extensions> block — when copying the answer
file to %WINDIR%\Panther\unattend.xml. Without pre-staging the [scriptblock] that
tries to extract scripts from the Panther copy receives $null, no scripts reach
disk, and both the specialize-pass actions and FirstLogonCommands silently fail.
Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO).
.PARAMETER ScratchDir
@@ -28,6 +37,11 @@ function Invoke-WinUtilISOScript {
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 — no drivers are injected.
.PARAMETER Log
Optional ScriptBlock used for progress/status logging.
Receives a single [string] message argument.
@@ -46,12 +60,13 @@ function Invoke-WinUtilISOScript {
.NOTES
Author : Chris Titus @christitustech
GitHub : https://github.com/ChrisTitusTech
Version : 26.02.25b
Version : 26.03.02
#>
param (
[Parameter(Mandatory)][string]$ScratchDir,
[string]$ISOContentsDir = "",
[string]$AutoUnattendXml = "",
[bool]$InjectCurrentSystemDrivers = $false,
[scriptblock]$Log = { param($m) Write-Output $m }
)
@@ -80,32 +95,6 @@ function Invoke-WinUtilISOScript {
}
}
# Copies driver package folders (one per exported .inf) whose online class
# matches any of the supplied class names into a new temp staging directory.
# Returns the staging directory path — caller must delete it when done.
# Using a staging dir lets DISM inject all drivers in a single /Recurse
# call instead of one DISM process launch per .inf file.
function New-DriverStagingDir {
param ([string]$ExportRoot, [string[]]$Classes)
$stagingDir = Join-Path $env:TEMP "WinUtil_DriverStage_$(Get-Random)"
New-Item -Path $stagingDir -ItemType Directory -Force | Out-Null
Get-WindowsDriver -Online |
Where-Object { $_.ClassName -in $Classes } |
ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_.OriginalFileName) } |
Select-Object -Unique |
ForEach-Object {
Get-ChildItem -Path $ExportRoot -Filter "$_.inf" -Recurse -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DirectoryName -Unique |
ForEach-Object {
# Each exported driver lives in its own sub-folder;
# copy that folder (with its binary files) into staging.
$dest = Join-Path $stagingDir (Split-Path $_ -Leaf)
Copy-Item -Path $_ -Destination $dest -Recurse -Force -ErrorAction SilentlyContinue
}
}
return $stagingDir
}
# Injects all drivers from $DriverDir into a DISM-mounted image in one call.
function Add-DriversToImage {
param (
@@ -207,104 +196,41 @@ function Invoke-WinUtilISOScript {
}
# ═════════════════════════════════════════════════════════════════════════
# 2. Inject hardware drivers (NVMe / Trackpad / Network)
# Injected into BOTH install.wim (OS) AND boot.wim index 2 (Setup).
# Without storage drivers in boot.wim, Windows Setup cannot see the
# target disk on systems with unsupported NVMe / SATA controllers.
# 2. Inject current system drivers (optional)
# Enabled by the "Inject current system drivers" checkbox at the
# Mount & Verify step. Exports ALL drivers from the running system
# and injects them into install.wim AND boot.wim index 2 so Windows
# Setup can see the target disk on systems with unsupported NVMe or
# SATA controllers.
# ═════════════════════════════════════════════════════════════════════════
& $Log "Exporting hardware drivers from running system (NVMe, HID/Trackpad, Network)..."
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
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)"
New-Item -Path $driverExportRoot -ItemType Directory -Force | 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."
try {
Export-WindowsDriver -Online -Destination $driverExportRoot | Out-Null
# Stage matching driver folders then do a single DISM /Recurse call.
# install.wim: SCSIAdapter + HIDClass + Net
$installStage = New-DriverStagingDir -ExportRoot $driverExportRoot -Classes @('SCSIAdapter','HIDClass','Net')
& $Log "Injecting staged drivers into install.wim (single DISM call)..."
Add-DriversToImage -MountPath $ScratchDir -DriverDir $installStage -Label "install" -Logger $Log
Remove-Item -Path $installStage -Recurse -Force -ErrorAction SilentlyContinue
& $Log "install.wim driver injection complete."
# boot.wim: SCSIAdapter + Net only (HID not needed in WinPE)
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$bootWim = Join-Path $ISOContentsDir "sources\boot.wim"
if (Test-Path $bootWim) {
$bootStage = New-DriverStagingDir -ExportRoot $driverExportRoot -Classes @('SCSIAdapter','Net')
& $Log "Injecting staged drivers into boot.wim (single DISM call)..."
Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $bootStage -Logger $Log
Remove-Item -Path $bootStage -Recurse -Force -ErrorAction SilentlyContinue
} 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
}
# ── 2c. Optional: extended Storage & Network drivers from community repo ──
$extDriverChoice = [System.Windows.MessageBox]::Show(
"Would you like to add extended Storage and Network drivers?`n`n" +
"This installs EVERY Storage and Networking device driver " +
"in EXISTANCE into the image. (~1000 drivers)`n`n" +
"No Wireless drivers only Ethernet, use for stubborn systems " +
"with unsupported NVMe or Ethernet controllers.",
"Extended Drivers", "YesNo", "Question")
if ($extDriverChoice -eq 'Yes') {
& $Log "Extended driver injection requested."
# Ensure git is available
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitCmd) {
& $Log "Git not found — installing via winget..."
winget install --id Git.Git -e --source winget `
--accept-package-agreements --accept-source-agreements | Out-Null
# Refresh PATH so git is visible in this session
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('PATH', 'User')
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
}
if (-not $gitCmd) {
& $Log "Warning: git could not be found after install attempt — skipping extended drivers."
} else {
$extRepoDir = Join-Path $env:TEMP "WinUtil_ExtDrivers_$(Get-Random)"
try {
& $Log "Cloning storage-lan-drivers repository..."
& git clone --depth 1 `
"https://github.com/ChrisTitusTech/storage-lan-drivers" `
$extRepoDir 2>&1 | ForEach-Object { & $Log " git: $_" }
if (Test-Path $extRepoDir) {
& $Log "Injecting extended drivers into install.wim (this may take several minutes)..."
Add-DriversToImage -MountPath $ScratchDir -DriverDir $extRepoDir -Label "install" -Logger $Log
& $Log "Extended driver injection into install.wim complete."
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$bootWimExt = Join-Path $ISOContentsDir "sources\boot.wim"
if (Test-Path $bootWimExt) {
& $Log "Injecting extended drivers into boot.wim..."
Invoke-BootWimInject -BootWimPath $bootWimExt -DriverDir $extRepoDir -Logger $Log
} else {
& $Log "Warning: boot.wim not found — skipping extended driver injection into boot.wim."
}
}
# Also inject into boot.wim so Windows Setup can see the target disk.
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: repository clone directory not found — skipping extended drivers."
& $Log "Warning: boot.wim not found — skipping boot.wim driver injection."
}
} catch {
& $Log "Error during extended driver injection: $_"
} finally {
Remove-Item -Path $extRepoDir -Recurse -Force -ErrorAction SilentlyContinue
}
} catch {
& $Log "Error during driver export/injection: $_"
} finally {
Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
}
} else {
& $Log "Extended driver injection skipped."
& $Log "Driver injection skipped."
}
# ═════════════════════════════════════════════════════════════════════════
@@ -366,10 +292,55 @@ function Invoke-WinUtilISOScript {
Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' 'BypassNRO' 'REG_DWORD' '1'
if ($AutoUnattendXml) {
# ── Place autounattend.xml inside the WIM (Sysprep) ──────────────────
$sysprepDest = "$ScratchDir\Windows\System32\Sysprep\autounattend.xml"
Set-Content -Path $sysprepDest -Value $AutoUnattendXml -Encoding UTF8 -Force
& $Log "Written autounattend.xml to Sysprep directory."
# ── Pre-stage embedded setup scripts directly into the WIM ────────────
# The autounattend.xml (Schneegans generator format) embeds all setup
# scripts as <Extensions><File> nodes. The specialize-pass command that
# is supposed to extract them reads C:\Windows\Panther\unattend.xml, but
# Windows Setup strips unrecognised-namespace elements (including the
# entire <Extensions> block) when it copies the answer file to that path.
# As a result [scriptblock]::Create($null) throws, no scripts are written
# to C:\Windows\Setup\Scripts\, Specialize.ps1 and DefaultUser.ps1 never
# run, and FirstLogon.ps1 is absent so FirstLogonCommands silently fails.
#
# Writing the scripts directly into the WIM guarantees they are present
# on the target drive after Windows Setup applies the image, regardless
# of whether the Panther extraction step succeeds.
try {
$xmlDoc = [xml]::new()
$xmlDoc.LoadXml($AutoUnattendXml)
$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) {
# Paths in the XML are absolute Windows paths (e.g. C:\Windows\Setup\Scripts\…).
# Strip the drive-letter prefix so we can root them under $ScratchDir.
$absPath = $fileNode.GetAttribute("path")
$relPath = $absPath -replace '^[A-Za-z]:[/\\]', ''
$destPath = Join-Path $ScratchDir $relPath
$destDir = Split-Path $destPath -Parent
New-Item -Path $destDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
# Match the encoding logic used by the original ExtractScript.
$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 }
}
$bytes = $encoding.GetPreamble() + $encoding.GetBytes($fileNode.InnerText.Trim())
[System.IO.File]::WriteAllBytes($destPath, $bytes)
& $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),

View File

@@ -1494,6 +1494,13 @@
HorizontalAlignment="Left"
Width="Auto" Padding="12,0"
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>
<!-- Verification results panel -->