Inject Win11 Creator drivers per package instead of one batch (#5048)

* fix(iso): inject drivers per package

Add each root package folder separately so one bad driver cannot fail the rest. The batch form is roughly four times faster. That cost is accepted for one deterministic code path

* Preserve retained nested driver packages

* Discard partial driver injection attempts

* Remove excluded nested driver INFs

---------

Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
Omar
2026-09-02 12:09:18 -05:00
committed by GitHub
co-authored by Chris Titus
parent cc5e31460b
commit df858c4cd7
5 changed files with 390 additions and 77 deletions
+1 -1
View File
@@ -177,6 +177,6 @@ When the user corrects an agent approach, add or tighten one concrete rule here
- Keep UI helpers such as `Invoke-WPFUIThread` and `Set-WinUtilTweaksProgressIndicator` safe to call without a window; the `-Preset` and `-Config` paths run the workflows before the form is created and before PresentationCore is loaded.
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export.
- For Win11 Creator driver injection, keep offline WIM servicing to one mount and one commit: add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
@@ -70,7 +70,7 @@ Click **Run Windows ISO Modification and Creator** to start the customization pr
- **Disable Copilot and search box suggestions**
**Optional: Driver Injection**
- If enabled, it injects all drivers from your current system into the install.wim and boot.wim — useful for offline installations on machines with missing drivers. This is an optional checkbox in Step 3.
- If enabled, WinUtil exports the drivers from your current system, stages boot-storage drivers for Windows Setup, and injects eligible packages into the selected `install.wim` image. Extension-class packages and stale duplicates are excluded. Each remaining package is added separately, so one incompatible package does not stop the others. If a package fails, WinUtil discards the partial mount and retries the remaining packages against the original image. Check the live log for warnings: if every package fails, WinUtil keeps the original `install.wim` and still produces the ISO.
A live log shows progress as each step completes. This stage usually takes **10–30 minutes** depending on disk speed. The WIM dismount near the end is the slowest part, so do not close WinUtil while it is running.
@@ -138,6 +138,7 @@ When you install Windows 11 from your modified ISO:
| USB drive not showing up | Plug it in, wait a few seconds, then click **Refresh** |
| Modification seems stuck | The WIM dismount step is slow — wait at least 10 minutes before assuming it's frozen |
| "Access Denied" error | Make sure WinUtil is running as Administrator |
| Driver injection warning | Review the live log for the named package. Other compatible packages continue; if none succeed, the ISO is created with the original `install.wim` |
| "Setup has failed to validate the product key" | Recreate the ISO with the latest WinUtil. The creator now removes stale `PID.txt`, writes `sources\ei.cfg`, and pins the selected image in `autounattend.xml` so setup does not use an embedded OEM key for a different edition |
---
+4 -2
View File
@@ -280,12 +280,14 @@ function Invoke-WinUtilISOModify {
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallImagePath $localWim -InstallImageIndex $selectedWimIndex -InstallEditionId $selectedEditionId -Log { param($m) Log $m } -DriversInjected $driversInjected
SetProgress "Preserving install image..." 70
if ($driversInjected.Value) {
SetProgress "Finalizing install image..." 70
Log "Added current-system drivers to $sourceImageFileName index $selectedWimIndex with one mount and commit."
} elseif ($injectDrivers) {
Log "No current-system drivers needed injection into $sourceImageFileName index $selectedWimIndex; install.wim was left unchanged."
SetProgress "Preserving install image..." 70
Log "No current-system drivers were injected into $sourceImageFileName index $selectedWimIndex; install.wim was left unchanged. Review the warning log entries for details."
} else {
SetProgress "Preserving install image..." 70
Log "Preserved the original $sourceImageFileName without mounting, exporting, or modifying it."
}
+88 -23
View File
@@ -28,7 +28,7 @@ function Invoke-WinUtilISOScript {
.PARAMETER DriversInjected
Optional [ref] set to $true only if driver injection actually mounted and committed
install.wim; stays $false if injection was skipped (disabled, or nothing survived filtering).
install.wim; stays $false if injection was skipped or no package was added successfully.
#>
param (
[Parameter(Mandatory)][string]$ISOContentsDir,
@@ -300,17 +300,18 @@ function Invoke-WinUtilISOScript {
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss')_$(([guid]::NewGuid()).ToString('N').Substring(0, 8))"
$mountDir = Join-Path (Split-Path -Path $ContentRoot -Parent) 'wim_mount'
New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null
# %TEMP% can be an 8.3 alias, but Get-ChildItem below reports long paths, so the
# exported folders would not share this prefix unless it is expanded first.
$driverExportRoot = (Get-Item -LiteralPath $driverExportRoot).FullName
$imageMounted = $false
try {
& $Logger "Exporting current system drivers before modifying install.wim..."
$dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$dismProcess = Start-Process -FilePath "dism.exe" -ArgumentList "/online /export-driver /destination:`"$driverExportRoot`" /LogPath:`"$dismLog`"" -Wait -NoNewWindow -PassThru
if ($dismProcess.ExitCode -ne 0) {
throw "dism.exe driver export failed with exit code $($dismProcess.ExitCode)."
}
Invoke-WinUtilISODism -Arguments @('/English', '/Online', '/Export-Driver', "/Destination:$driverExportRoot", "/LogPath:$dismLog") -Operation 'export-driver' | Out-Null
$driverInfs = @(Get-ChildItem -Path $driverExportRoot -Filter '*.inf' -Recurse -File)
$driverInfs = @(Get-ChildItem -LiteralPath $driverExportRoot -Filter '*.inf' -Recurse -File)
if ($driverInfs.Count -eq 0) {
throw 'DISM exported no driver INF files.'
}
@@ -350,8 +351,25 @@ function Invoke-WinUtilISOScript {
# isn't a failure: leave install.wim untouched and continue building the ISO.
& $Logger 'No drivers found to inject: every exported package was excluded (Extension class or stale duplicate). Skipping driver injection; install.wim is unchanged.'
} else {
$excludedFolders = @($driverFolders.Name | Where-Object { $_ -notin $stagedDriverFolders })
foreach ($excludedFolder in $excludedFolders) {
$excludedDriverFolderGroups = @($driverFolders | Where-Object { $_.Name -notin $stagedDriverFolders })
foreach ($excludedDriverFolderGroup in $excludedDriverFolderGroups) {
$excludedFolder = [string]$excludedDriverFolderGroup.Name
$hasRetainedDescendant = [bool]@($stagedDriverFolders | Where-Object {
$_.StartsWith("$excludedFolder\", [System.StringComparison]::OrdinalIgnoreCase)
}).Count
if ($hasRetainedDescendant) {
try {
foreach ($excludedInf in $excludedDriverFolderGroup.Group) {
Remove-Item -LiteralPath $excludedInf.FullName -Force -ErrorAction Stop
}
} catch {
throw "Failed to remove excluded driver INF files from package '$excludedFolder' before injection: $_"
}
& $Logger "Keeping excluded driver package directory '$excludedFolder' because it contains a retained nested package, after removing its excluded INF files."
continue
}
try {
Remove-Item -LiteralPath $excludedFolder -Recurse -Force -ErrorAction Stop
} catch {
@@ -359,25 +377,72 @@ function Invoke-WinUtilISOScript {
}
}
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedFolders.Count) excluded)."
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedDriverFolderGroups.Count) excluded)."
Set-ItemProperty -LiteralPath $InstallImagePath -Name IsReadOnly -Value $false
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
& $Logger "Mounting install.wim index $InstallImageIndex once for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$InstallImagePath", "/Index:$InstallImageIndex", "/MountDir:$mountDir") -Operation 'mount' | Out-Null
$imageMounted = $true
& $Logger "Adding all exported drivers to the selected Windows image in one DISM operation..."
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$mountDir", '/Add-Driver', "/Driver:$driverExportRoot", '/Recurse') -Operation 'add-driver' | Out-Null
# Add each package separately so one bad driver cannot fail the rest. Because
# /Recurse covers descendants, only the highest surviving folder in each tree
# needs its own DISM call.
$rootPackageFolders = @($stagedDriverFolders | Where-Object {
$candidate = $_
-not ($stagedDriverFolders | Where-Object { $candidate.StartsWith("$_\", [System.StringComparison]::OrdinalIgnoreCase) })
})
& $Logger 'Committing the driver-only install.wim change...'
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
$imageMounted = $false
& $Logger "Adding $($rootPackageFolders.Count) root driver packages to install.wim."
$remainingDriverFolders = @($rootPackageFolders)
while ($remainingDriverFolders.Count -gt 0) {
& $Logger "Mounting install.wim index $InstallImageIndex for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$InstallImagePath", "/Index:$InstallImageIndex", "/MountDir:$mountDir") -Operation 'mount' | Out-Null
$imageMounted = $true
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
$DriversInjected.Value = $true
$failedDriverFolder = $null
foreach ($driverFolder in $remainingDriverFolders) {
$driverName = $driverFolder
if ($driverFolder.StartsWith($driverExportRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
$driverName = $driverFolder.Substring($driverExportRoot.Length).TrimStart('\')
}
try {
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$mountDir", '/Add-Driver', "/Driver:$driverFolder", '/Recurse') -Operation "add-driver:$driverName" | Out-Null
} catch {
& $Logger "Warning: failed to add driver package '$driverName': $_"
$failedDriverFolder = $driverFolder
break
}
}
if (-not $failedDriverFolder) {
break
}
& $Logger "Discarding the potentially partial install.wim mount before continuing without '$driverName'."
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
$imageMounted = $false
} catch {
throw "Failed to discard the potentially partial install.wim mount after driver package '$driverName' failed: $_"
}
$remainingDriverFolders = @($remainingDriverFolders | Where-Object { $_ -ne $failedDriverFolder })
}
$addedCount = $remainingDriverFolders.Count
if ($addedCount -eq 0) {
# Boot-storage drivers staged for WinPE remain available to Windows Setup.
& $Logger "Warning: none of the $($rootPackageFolders.Count) exported driver packages could be added; continuing with an unmodified install.wim."
} else {
& $Logger "Added $addedCount of $($rootPackageFolders.Count) driver packages to install.wim."
& $Logger 'Committing the driver-only install.wim change...'
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
$imageMounted = $false
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
$DriversInjected.Value = $true
}
}
} finally {
if ($imageMounted -or (Test-WinUtilISOMountedImage -Path $mountDir)) {
@@ -387,8 +452,8 @@ function Invoke-WinUtilISOScript {
& $Logger "Warning: could not discard the failed install.wim mount: $_"
}
}
Remove-Item -Path $mountDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $mountDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
+295 -50
View File
@@ -36,12 +36,77 @@ Describe "Win11 Creator setup media" {
return $functionAst.Extent.Text
}
# Root package folders the fixture below yields, once nested_pkg\x64 is folded into
# its parent. Written out rather than derived, so a broken dedup fails the assertions
# instead of quietly moving the expectation with it.
$script:expectedRootPackages = 8
$script:resilienceDriverFixtures = @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'group_b\duplicate'; Name = 'extension.inf'; Class = 'Media' },
@{ Path = 'nested_pkg'; Name = 'main.inf'; Class = 'Net' },
@{ Path = 'nested_pkg\x64'; Name = 'extra.inf'; Class = 'Net' },
@{ Path = 'net_pkg_v2'; Name = 'network2.inf'; Class = 'Net' }
)
# Stands in for `dism /Online /Export-Driver`, which the ISO script now invokes
# through Invoke-WinUtilISODism rather than Start-Process. The two `duplicate` leaf
# names cover per-package failure logging, which must stay unambiguous; `nested_pkg`
# covers a package holding INFs at its root and in a subfolder; and `net_pkg_v2` is a
# sibling whose name starts with `net_pkg`, which the dedup must not swallow.
function Export-WinUtilTestDriverPackage {
param (
[Parameter(Mandatory)][string[]]$Arguments,
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fixtures
)
$destination = @($Arguments | Where-Object { $_ -like '/Destination:*' })
if ($destination.Count -ne 1) {
throw "Expected exactly one /Destination argument in the mocked DISM export: $($Arguments -join ' ')"
}
$exportRoot = $destination[0].Substring('/Destination:'.Length)
$script:driverExportRoot = $exportRoot
# .NET file APIs rather than the cmdlets: Set-Content's -Encoding comes from the
# FileSystem provider, and a wildcard character in $exportRoot stops the provider
# resolving, so the parameter disappears.
foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
[void][System.IO.Directory]::CreateDirectory($fixturePath)
[System.IO.File]::WriteAllText(
(Join-Path $fixturePath $fixture.Name),
(@(
'[Version]'
"Class=$($fixture.Class)"
if ($fixture.Provider) { "Provider=$($fixture.Provider)" }
if ($fixture.DriverVer) {
$versionKeyword = if ($fixture.VersionKeyword) { $fixture.VersionKeyword } else { 'DriverVer' }
"$versionKeyword=$($fixture.DriverVer)"
}
) -join "`r`n"),
[System.Text.Encoding]::ASCII)
}
}
function New-WinUtilDriverExportHarness {
param ([Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fixtures)
param (
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fixtures,
[string]$FailedDriverPath,
[switch]$FailAllAddDrivers,
[switch]$FailDiscard
)
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
$script:driverExportRoot = $null
$script:driverExportFixtures = $Fixtures
$script:failedDriverPath = $FailedDriverPath
$script:failAllAddDrivers = $FailAllAddDrivers
$script:failDiscard = $FailDiscard
$script:exportedInfsAtAddDriver = $null
Set-Item -Path function:global:dism.exe -Value {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
@@ -56,44 +121,23 @@ Describe "Win11 Creator setup media" {
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
'[==========================100.0%==========================]'
} elseif ($Arguments -contains '/Export-Driver') {
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures $script:driverExportFixtures
} elseif ($Arguments -contains '/Add-Driver') {
# Snapshot what's still on disk right as DISM would /Recurse over it: this is the
# only point excluded folders are provably gone, since the SUT wipes the whole
# export root in its own cleanup once Invoke-WinUtilISOScript returns.
$script:exportRootAtAddDriver = @(Get-ChildItem -Path $script:driverExportRoot -Directory -Recurse -ErrorAction SilentlyContinue | ForEach-Object FullName)
$script:exportRootAtAddDriver = @(Get-ChildItem -LiteralPath $script:driverExportRoot -Directory -Recurse -ErrorAction SilentlyContinue | ForEach-Object FullName)
$script:exportedInfsAtAddDriver = @(Get-ChildItem -LiteralPath $script:driverExportRoot -Filter '*.inf' -File -Recurse -ErrorAction SilentlyContinue | ForEach-Object FullName)
if ($script:failAllAddDrivers -or ($script:failedDriverPath -and @($Arguments | Where-Object { $_ -like "*$($script:failedDriverPath)" }).Count -gt 0)) {
$global:LASTEXITCODE = 13
'Error: 13'
}
} elseif ($Arguments -contains '/Discard' -and $script:failDiscard) {
$global:LASTEXITCODE = 50
'Discard failed'
}
}
Mock Start-Process {
param($FilePath, $ArgumentList)
if ($FilePath -ne 'dism.exe') {
throw "Unexpected process in driver export mock: $FilePath"
}
$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
if (-not $destinationMatch.Success) {
throw "Unable to find the mocked DISM export destination in: $ArgumentList"
}
$exportRoot = $destinationMatch.Groups[1].Value
$script:driverExportRoot = $exportRoot
foreach ($fixture in $script:driverExportFixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
$infContent = "[Version]`r`nClass=$($fixture.Class)"
if ($fixture.Provider) {
$infContent += "`r`nProvider=$($fixture.Provider)"
}
if ($fixture.DriverVer) {
$versionKeyword = if ($fixture.VersionKeyword) { $fixture.VersionKeyword } else { 'DriverVer' }
$infContent += "`r`n$versionKeyword=$($fixture.DriverVer)"
}
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value $infContent -Encoding ASCII
}
return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }
}
$script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify"
@@ -389,14 +433,18 @@ Describe "Win11 Creator setup media" {
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'hdx_asusext_apot_g5-tse.inf_amd64_aabbccddeeff0011'; Name = 'hdx_asusext_apot_g5-tse.inf'; Class = 'Extension' },
@{ Path = 'ntprint.inf_x86_7426e1b60aa62272'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2023,10.0.26100.8875' },
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' }
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' },
@{ Path = 'nested_pkg'; Name = 'main.inf'; Class = 'Net' },
@{ Path = 'nested_pkg\x64'; Name = 'extra.inf'; Class = 'Net' },
@{ Path = 'net_pkg_v2'; Name = 'network2.inf'; Class = 'Net' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
@@ -409,16 +457,22 @@ Describe "Win11 Creator setup media" {
Test-Path (Join-Path $winpeDriverRoot 'net_pkg\network.inf') | Should -BeFalse
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 2
($script:dismCalls -join "`n") | Should -Not -Match '/Cleanup-Image|/Export-Image'
($logs -join '|') | Should -Match "Added $script:expectedRootPackages of $script:expectedRootPackages driver packages"
# nested_pkg holds INFs at its root and under x64. The root add already recurses
# into x64, so the subfolder must not get an /Add-Driver call of its own.
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' -and $_ -match ([regex]::Escape('nested_pkg')) }).Count | Should -Be 1
($script:dismCalls -join "`n") | Should -Not -Match ([regex]::Escape('nested_pkg\x64'))
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'Exported 6 of 8 driver packages \(2 staged for WinPE, 2 excluded\)'
($logs -join '|') | Should -Match 'Exported 9 of 11 driver packages \(2 staged for WinPE, 2 excluded\)'
($logs -join '|') | Should -Match "Excluding extension-class driver package '.*hdx_asusext_apot_g5-tse.*'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e' \(DriverVer 6/1/2024,10\.0\.26100\.9168\)"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
@@ -474,6 +528,73 @@ Describe "Win11 Creator setup media" {
}
}
It "preserves a retained nested package when its parent package is excluded" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNestedRetained_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'parent_pkg'; Name = 'extension.inf'; Class = 'Extension' },
@{ Path = 'parent_pkg\retained_child'; Name = 'network.inf'; Class = 'Net' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
$addDriverCalls = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' })
$addDriverCalls.Count | Should -Be 1
$addDriverCalls[0] | Should -Match ([regex]::Escape('parent_pkg\retained_child'))
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'parent_pkg')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'parent_pkg\retained_child')
$script:exportedInfsAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'parent_pkg\extension.inf')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'parent_pkg\retained_child\network.inf')
($logs -join '|') | Should -Match "Keeping excluded driver package directory '.*parent_pkg' because it contains a retained nested package"
$driversInjected.Value | Should -BeTrue
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "removes an excluded nested INF before recursively adding its retained ancestor" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNestedExcluded_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'root_pkg'; Name = 'root.inf'; Class = 'Net' },
@{ Path = 'root_pkg\excluded_child'; Name = 'extension.inf'; Class = 'Extension' },
@{ Path = 'root_pkg\excluded_child\retained_grandchild'; Name = 'storage.inf'; Class = 'SCSIAdapter' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional'
$addDriverCalls = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' })
$addDriverCalls.Count | Should -Be 1
$addDriverCalls[0] | Should -Match ([regex]::Escape('/Driver:' + (Join-Path $script:driverExportRoot 'root_pkg')))
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\excluded_child')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\excluded_child\retained_grandchild')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\root.inf')
$script:exportedInfsAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'root_pkg\excluded_child\extension.inf')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\excluded_child\retained_grandchild\storage.inf')
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "validates WIM metadata and reports no injection when every package is excluded" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoAllExcluded_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
@@ -526,7 +647,7 @@ Describe "Win11 Creator setup media" {
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 2
($logs -join '|') | Should -Match 'Exported 2 of 2 driver packages \(0 staged for WinPE, 0 excluded\)'
($logs -join '|') | Should -Not -Match 'Excluding stale duplicate driver package'
@@ -567,7 +688,7 @@ Describe "Win11 Creator setup media" {
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(0 staged for WinPE, 4 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_6688e7b66f8d9fb5' \(DriverVer 1/1/2024,10\.0\.26100\.8972\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
@@ -587,6 +708,136 @@ Describe "Win11 Creator setup media" {
}
}
It "discards partial changes and commits the remaining drivers when one package fails to add" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverPartial_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures -FailedDriverPath 'group_a\duplicate'
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 2
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
$lastMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' } | Select-Object -Last 1
$lastMountIndex = [Array]::LastIndexOf($script:dismCalls.ToArray(), $lastMountCall)
$finalMountCalls = @($script:dismCalls[($lastMountIndex + 1)..($script:dismCalls.Count - 1)])
@($finalMountCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be ($script:expectedRootPackages - 1)
($finalMountCalls -join "`n") | Should -Not -Match ([regex]::Escape('group_a\duplicate'))
($logs -join '|') | Should -Match "Added $($script:expectedRootPackages - 1) of $script:expectedRootPackages driver packages"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
# group_a and group_b share a leaf name, so the warning must name the failing
# package by its subpath; a leaf-only name would read 'duplicate' and be ambiguous.
($logs -join '|') | Should -Match ([regex]::Escape("failed to add driver package 'group_a\duplicate'"))
($logs -join '|') | Should -Match ([regex]::Escape('add-driver:group_b\duplicate completed'))
$driversInjected.Value | Should -BeTrue
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "discards the mount and keeps going when every driver package fails to add" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverAllFail_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures -FailAllAddDrivers
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$true
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 0
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 1
($logs -join '|') | Should -Match "none of the $script:expectedRootPackages exported driver packages could be added"
($logs -join '|') | Should -Not -Match "Added 0 of $script:expectedRootPackages"
# WinPE staging is independent of WIM servicing, so it must survive the failure.
@(Get-ChildItem -Path (Join-Path $contentRoot '$WinpeDriver$') -Directory).Count | Should -Be 2
$driversInjected.Value | Should -BeFalse
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "finds exported drivers when the temp path contains wildcard characters" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverGlob_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures
# A Windows account named like "John [Work]" puts wildcard characters in %TEMP%.
# Get-ChildItem -Path would glob them, find nothing, and abort the whole ISO run.
$originalTemp = $env:TEMP
$bracketTemp = Join-Path ([IO.Path]::GetTempPath()) "WinUtil [Glob] $([guid]::NewGuid())"
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
New-Item -Path $bracketTemp -ItemType Directory -Force | Out-Null
$env:TEMP = $bracketTemp
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
($logs -join '|') | Should -Match "Added $script:expectedRootPackages of $script:expectedRootPackages driver packages"
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
($logs -join '|') | Should -Match ([regex]::Escape('add-driver:nested_pkg completed'))
} finally {
$env:TEMP = $originalTemp
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $bracketTemp -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "stops when a potentially partial mount cannot be discarded after a package fails" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverDiscardFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures -FailAllAddDrivers -FailDiscard
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*Failed to discard the potentially partial install.wim mount after driver package*'
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 2
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 0
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "discards a partially mounted install.wim after mount failure" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
@@ -610,22 +861,16 @@ Describe "Win11 Creator setup media" {
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} elseif ($Arguments -contains '/Export-Driver') {
$global:LASTEXITCODE = 0
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
)
} else {
$global:LASTEXITCODE = 0
}
}
Mock Start-Process {
param($FilePath, $ArgumentList)
$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
$exportRoot = $destinationMatch.Groups[1].Value
$fixturePath = Join-Path $exportRoot 'storage_pkg'
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath 'iaStorAC.inf') -Value "[Version]`r`nClass=System" -Encoding ASCII
return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'