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