From abcbc231441a8e6f695ef0a3657d8787757aa342 Mon Sep 17 00:00:00 2001 From: Omar <90123670+mewclouds@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:35:50 -0400 Subject: [PATCH] fix: inject Setup storage into boot.wim (#5102) $WinpeDriver$ matched INF names, so RST companions reached Setup. Add SCSIAdapter and HDC packages to boot.wim index 2 with DISM. --- AGENTS.md | 2 +- .../docs/code-reference/architecture.mdx | 6 +- docs/src/content/docs/guides/win11creator.mdx | 2 +- functions/private/Invoke-WinUtilISO.ps1 | 2 +- functions/private/Invoke-WinUtilISOScript.ps1 | 220 +++++++++--------- pester/win11creator.Tests.ps1 | 121 ++++++++-- 6 files changed, 221 insertions(+), 132 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f3e9530f..9aa011b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,7 +183,7 @@ When the user corrects an agent approach, add or tighten one concrete rule here - Have each Pester file load the assemblies and dot-source the functions it needs; several passed only because an earlier file in alphabetical order happened to load them. - 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 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 Win11 Creator driver injection, inject storage controllers (`SCSIAdapter` / `HDC`) into `boot.wim` index 2 with DISM, not `$WinpeDriver$`. Keep `install.wim` on its own per-package `/Add-Driver` retry loop. For each image, 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. - Public pull-request diffs may be sent to configured external review services without a separate privacy approval; do not block the review loop on upload authorization for this public repository. diff --git a/docs/src/content/docs/code-reference/architecture.mdx b/docs/src/content/docs/code-reference/architecture.mdx index 91e6d01a..7a0ab81b 100644 --- a/docs/src/content/docs/code-reference/architecture.mdx +++ b/docs/src/content/docs/code-reference/architecture.mdx @@ -259,9 +259,9 @@ The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**: ### Driver Injection Feature -**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into both: -- `install.wim` (main OS image) -- `boot.wim` index 2 (Windows Setup PE environment) +**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into: +- `install.wim` (main OS image), excluding stale duplicates +- `boot.wim` index 2 (Windows Setup PE environment), storage controllers only (`SCSIAdapter` / `HDC`) **Use Case**: Enables offline installation on systems with missing drivers. diff --git a/docs/src/content/docs/guides/win11creator.mdx b/docs/src/content/docs/guides/win11creator.mdx index 55ddc710..1cf5e611 100644 --- a/docs/src/content/docs/guides/win11creator.mdx +++ b/docs/src/content/docs/guides/win11creator.mdx @@ -67,7 +67,7 @@ Then click **Run Windows ISO Modification and Creator** to start the customizati - **Disable Copilot and search box suggestions** **Optional: Driver Injection** -- 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. Stale duplicate packages are excluded before injection. 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. +- If enabled, WinUtil exports the drivers from your current system, injects boot-storage drivers into `boot.wim` (Windows Setup, index 2), and injects eligible packages into the selected `install.wim` image. Stale duplicate packages are excluded before injection. 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. diff --git a/functions/private/Invoke-WinUtilISO.ps1 b/functions/private/Invoke-WinUtilISO.ps1 index 3bb7bb7e..87e0fc99 100644 --- a/functions/private/Invoke-WinUtilISO.ps1 +++ b/functions/private/Invoke-WinUtilISO.ps1 @@ -401,7 +401,7 @@ function Invoke-WinUtilISOModify { if ($driversInjected.Value) { Step-WinUtilJob -Status "Finalizing install image..." -Percent 70 - Write-WinUtilISOLog "Added current-system drivers to $sourceImageFileName index $SelectedWimIndex with one mount and commit." + Write-WinUtilISOLog "Added current-system drivers to $sourceImageFileName index $SelectedWimIndex." } elseif ($InjectDrivers) { Step-WinUtilJob -Status "Preserving install image..." -Percent 70 Write-WinUtilISOLog "No current-system drivers were injected into $sourceImageFileName index $SelectedWimIndex; install.wim was left unchanged. Review the warning log entries for details." diff --git a/functions/private/Invoke-WinUtilISOScript.ps1 b/functions/private/Invoke-WinUtilISOScript.ps1 index 2b046d05..a813083a 100644 --- a/functions/private/Invoke-WinUtilISOScript.ps1 +++ b/functions/private/Invoke-WinUtilISOScript.ps1 @@ -6,7 +6,8 @@ function Invoke-WinUtilISOScript { .DESCRIPTION Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup in the answer file for first logon, writes sources\ei.cfg for the selected - edition, and optionally adds current-system drivers to one install.wim index. + edition, and optionally adds current-system drivers to boot.wim index 2 and + one install.wim index. .PARAMETER ISOContentsDir Root directory of the copied ISO contents. @@ -51,31 +52,9 @@ function Invoke-WinUtilISOScript { ) $DriversInjected.Value = $false - function Copy-WinUtilISODriverFolder { - param ( - [Parameter(Mandatory)][string]$Source, - [Parameter(Mandatory)][string]$Destination - ) - - $folderName = Split-Path $Source -Leaf - $targetPath = Join-Path $Destination $folderName - $suffix = 1 - while (Test-Path -LiteralPath $targetPath) { - $targetPath = Join-Path $Destination "${folderName}_$suffix" - $suffix++ - } - - Copy-Item -LiteralPath $Source -Destination $targetPath -Recurse -Force -ErrorAction Stop - return $targetPath - } - function Test-WinUtilISOStorageDriver { param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile) - if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') { - return $true - } - try { return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*(SCSIAdapter|HDC)\s*(?:;.*)?$' } catch { @@ -267,6 +246,93 @@ function Invoke-WinUtilISOScript { return @(& dism.exe /English /Get-MountedImageInfo 2>$null) -match [regex]::Escape($Path) } + function Get-WinUtilISODriverFolderName { + param ([Parameter(Mandatory)][string]$DriverFolder) + + if ($DriverFolder.StartsWith($driverExportRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + return $DriverFolder.Substring($driverExportRoot.Length).TrimStart('\') + } + return $DriverFolder + } + + function Get-WinUtilISORootDriverFolders { + param ([Parameter(Mandatory)][AllowEmptyCollection()][string[]]$DriverFolders) + + return @($DriverFolders | Where-Object { + $candidate = $_ + -not ($DriverFolders | Where-Object { $candidate.StartsWith("$_\", [System.StringComparison]::OrdinalIgnoreCase) }) + }) + } + + function Add-WinUtilISODriversToImage { + param ( + [Parameter(Mandatory)][string]$ImagePath, + [Parameter(Mandatory)][int]$ImageIndex, + [Parameter(Mandatory)][string]$MountDir, + [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$DriverFolders, + [Parameter(Mandatory)][string]$ImageLabel, + [Parameter(Mandatory)][ref]$ImageMounted + ) + + if ($DriverFolders.Count -eq 0) { + & $Logger "No driver packages to add to ${ImageLabel}." + return 0 + } + + Set-ItemProperty -LiteralPath $ImagePath -Name IsReadOnly -Value $false + New-Item -Path $MountDir -ItemType Directory -Force | Out-Null + + $remainingDriverFolders = @($DriverFolders) + while ($remainingDriverFolders.Count -gt 0) { + & $Logger "Mounting $ImageLabel index $ImageIndex for driver injection..." + Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$ImagePath", "/Index:$ImageIndex", "/MountDir:$MountDir") -Operation 'mount' | Out-Null + $ImageMounted.Value = $true + + $failedDriverFolder = $null + $driverName = $null + foreach ($driverFolder in $remainingDriverFolders) { + $driverName = Get-WinUtilISODriverFolderName -DriverFolder $driverFolder + 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 $ImageLabel mount before continuing without '$driverName'." + try { + Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$MountDir", '/Discard') -Operation 'discard' | Out-Null + $ImageMounted.Value = $false + } catch { + throw "Failed to discard the potentially partial $ImageLabel mount after driver package '$driverName' failed: $_" + } + + $remainingDriverFolders = @($remainingDriverFolders | Where-Object { $_ -ne $failedDriverFolder }) + } + + $addedCount = $remainingDriverFolders.Count + if ($addedCount -eq 0) { + if ($ImageLabel -eq 'install.wim') { + & $Logger "Warning: none of the $($DriverFolders.Count) exported driver packages could be added; continuing with an unmodified install.wim." + } else { + & $Logger "Warning: none of the $($DriverFolders.Count) driver packages could be added to ${ImageLabel}." + } + return 0 + } + + & $Logger "Added $addedCount of $($DriverFolders.Count) driver packages to ${ImageLabel}." + & $Logger "Committing the driver-only $ImageLabel change..." + Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$MountDir", '/Commit') -Operation 'commit' | Out-Null + $ImageMounted.Value = $false + return $addedCount + } + if ([IO.Path]::GetExtension($InstallImagePath) -ne '.wim') { throw 'Current-system driver injection requires install.wim; install.esd cannot be serviced in place.' } @@ -287,7 +353,7 @@ function Invoke-WinUtilISOScript { $imageMounted = $false try { - & $Logger "Exporting current system drivers before modifying install.wim..." + & $Logger "Exporting current system drivers before WIM driver injection..." $dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" Invoke-WinUtilISODism -Arguments @('/English', '/Online', '/Export-Driver', "/Destination:$driverExportRoot", "/LogPath:$dismLog") -Operation 'export-driver' | Out-Null @@ -296,32 +362,6 @@ function Invoke-WinUtilISOScript { throw 'DISM exported no driver INF files.' } $driverFolders = @($driverInfs | Group-Object { $_.Directory.FullName }) - $winpeDriverDir = Join-Path $ContentRoot '$WinpeDriver$' - $storageCount = 0 - $copyFailures = 0 - - foreach ($driverFolderGroup in $driverFolders) { - $driverFolder = [string]$driverFolderGroup.Name - $storageInfs = @($driverFolderGroup.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ }) - if ($storageInfs.Count -eq 0) { - continue - } - - try { - New-Item -Path $winpeDriverDir -ItemType Directory -Force | Out-Null - $winpeTarget = Copy-WinUtilISODriverFolder -Source $driverFolder -Destination $winpeDriverDir - $storageCount++ - & $Logger "Staged boot-storage package '$driverFolder' for WinPE as '$winpeTarget'." - } catch { - $copyFailures++ - & $Logger "Warning: failed to stage boot-storage package '$driverFolder': $_" - } - } - - if ($copyFailures -gt 0) { - throw "Failed to stage $copyFailures boot-storage driver package folders." - } - $stagedDriverFolders = @(Select-WinUtilISOStagedDriverPackages -DriverFolderGroups $driverFolders -Logger $Logger) $metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex Assert-WinUtilISOWimMetadata -Before $metadataBefore @@ -352,67 +392,33 @@ function Invoke-WinUtilISOScript { } } - & $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedDriverFolderGroups.Count) excluded)." + & $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($($excludedDriverFolderGroups.Count) excluded)." - Set-ItemProperty -LiteralPath $InstallImagePath -Name IsReadOnly -Value $false - New-Item -Path $mountDir -ItemType Directory -Force | Out-Null + # Storage for Setup comes from the same surviving set as install.wim, so stale + # duplicates never reach boot.wim. + $storageFolders = @( + $driverFolders | + Where-Object { $_.Name -in $stagedDriverFolders } | + Where-Object { @($_.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ }).Count -gt 0 } | + ForEach-Object { [string]$_.Name } + ) + $storageRootFolders = @(Get-WinUtilISORootDriverFolders -DriverFolders $storageFolders) + $rootPackageFolders = @(Get-WinUtilISORootDriverFolders -DriverFolders $stagedDriverFolders) + $imageMountedRef = [ref]$imageMounted - # 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 "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 - - $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 - } + $bootWim = Join-Path $ContentRoot 'sources\boot.wim' + if ($storageRootFolders.Count -gt 0) { + if (Test-Path -LiteralPath $bootWim) { + & $Logger "Adding $($storageRootFolders.Count) root storage driver packages to boot.wim." + $null = Add-WinUtilISODriversToImage -ImagePath $bootWim -ImageIndex 2 -MountDir $mountDir -DriverFolders $storageRootFolders -ImageLabel 'boot.wim' -ImageMounted $imageMountedRef + } else { + & $Logger 'Warning: boot.wim was not found; Windows Setup will not have injected storage drivers.' } - - 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 - + & $Logger "Adding $($rootPackageFolders.Count) root driver packages to install.wim." + $addedCount = Add-WinUtilISODriversToImage -ImagePath $InstallImagePath -ImageIndex $InstallImageIndex -MountDir $mountDir -DriverFolders $rootPackageFolders -ImageLabel 'install.wim' -ImageMounted $imageMountedRef + if ($addedCount -gt 0) { $metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter & $Logger 'Driver injection complete; install.wim metadata validation passed.' @@ -423,7 +429,7 @@ function Invoke-WinUtilISOScript { try { Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null } catch { - & $Logger "Warning: could not discard the failed install.wim mount: $_" + & $Logger "Warning: could not discard the failed WIM mount: $_" } } Remove-Item -LiteralPath $mountDir -Recurse -Force -ErrorAction SilentlyContinue diff --git a/pester/win11creator.Tests.ps1 b/pester/win11creator.Tests.ps1 index e100949a..350aa9a3 100644 --- a/pester/win11creator.Tests.ps1 +++ b/pester/win11creator.Tests.ps1 @@ -214,7 +214,7 @@ Describe "Win11 Creator setup media" { $script:modifyFunction | Should -Match 'if \(\$m -like "Warning:\*"\)[\s\S]*Write-WinUtilISOLog -Level "WARN" -Message \$m -SkipSessionLog[\s\S]*Write-Warning \$m' } - It "keeps WIM servicing limited to one driver-only mount and commit" { + It "keeps WIM servicing limited to DISM Add-Driver without image export or cleanup" { $isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw foreach ($expectedText in @( @@ -222,7 +222,8 @@ Describe "Win11 Creator setup media" { "'/Add-Driver'", "'/Commit'", "`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'", - 'install.wim metadata validation passed' + 'install.wim metadata validation passed', + "Join-Path `$ContentRoot 'sources\boot.wim'" )) { $isoScriptContent | Should -Match ([regex]::Escape($expectedText)) } @@ -233,17 +234,19 @@ Describe "Win11 Creator setup media" { 'Export-WindowsImage', 'Set-WindowsImage', '/ResetBase', - '/Cleanup-Image' + '/Cleanup-Image', + '$WinpeDriver$' )) { $isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText)) } } - It "stages only boot-storage drivers in WinPE" { + It "injects only SCSIAdapter or HDC storage drivers into boot.wim" { $isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw - $isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'")) $isoScriptContent | Should -Match 'SCSIAdapter\|HDC' + $isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot 'sources\boot.wim'")) + $isoScriptContent | Should -Not -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'")) $isoScriptContent | Should -Not -Match ([regex]::Escape('sources\$OEM$\$$\Drivers')) $isoScriptContent | Should -Not -Match ([regex]::Escape('WinUtil-InstallDrivers.ps1')) $isoScriptContent | Should -Not -Match ([regex]::Escape('SetupComplete.cmd')) @@ -435,7 +438,7 @@ Describe "Win11 Creator setup media" { - It "stages storage drivers for WinPE and adds all drivers to one install.wim index" { + It "adds eligible drivers to one install.wim index and does not create `$WinpeDriver$" { $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())" $installWim = Join-Path $contentRoot 'sources\install.wim' $template = Get-Content -Path $script:autoUnattendPath -Raw @@ -443,7 +446,7 @@ Describe "Win11 Creator setup media" { New-WinUtilDriverExportHarness -Fixtures @( @{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' }, - @{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }, + @{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'SCSIAdapter' }, @{ 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' }, @@ -465,12 +468,8 @@ Describe "Win11 Creator setup media" { $logs.Add([string]$message) } - $winpeDriverRoot = Join-Path $contentRoot '$WinpeDriver$' - @(Get-ChildItem -Path $winpeDriverRoot -Directory).Count | Should -Be 2 - Test-Path (Join-Path $winpeDriverRoot 'system_pkg\chipset.inf') | Should -BeFalse - Test-Path (Join-Path $winpeDriverRoot 'storage_pkg\iaStorAC.inf') | Should -BeTrue - Test-Path (Join-Path $winpeDriverRoot 'scsi_pkg\controller.inf') | Should -BeTrue - Test-Path (Join-Path $winpeDriverRoot 'net_pkg\network.inf') | Should -BeFalse + Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse + ($logs -join '|') | Should -Match 'Warning: boot.wim was not found' @($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1 @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 9 @@ -488,7 +487,7 @@ Describe "Win11 Creator setup media" { $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 10 of 11 driver packages \(2 staged for WinPE, 1 excluded\)' + ($logs -join '|') | Should -Match 'Exported 10 of 11 driver packages \(1 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' \(DriverVer 6/1/2024,10\.0\.26100\.9168\)" ($logs -join '|') | Should -Match 'install.wim metadata validation passed' ($logs -join '|') | Should -Match 'DISM mount completed.' @@ -505,6 +504,91 @@ Describe "Win11 Creator setup media" { } } + It "injects SCSIAdapter storage drivers into boot.wim index 2 and not `$WinpeDriver`$" { + $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWim_$([guid]::NewGuid())" + $installWim = Join-Path $contentRoot 'sources\install.wim' + $bootWim = Join-Path $contentRoot 'sources\boot.wim' + $template = Get-Content -Path $script:autoUnattendPath -Raw + $logs = [System.Collections.Generic.List[string]]::new() + + New-WinUtilDriverExportHarness -Fixtures @( + @{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' }, + @{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' }, + @{ Path = 'hdc_pkg'; Name = 'ide.inf'; Class = 'HDC' }, + @{ Path = 'name_only_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }, + @{ Path = 'iastorhsacomponent.inf_amd64_1b2a068a8496b6a2'; Name = 'iaStorHsaComponent.inf'; Class = 'SoftwareComponent' }, + @{ Path = 'iastorhsa_ext.inf_amd64_ba71359697f80d4e'; Name = 'iaStorHsa_Ext.inf'; Class = 'Extension' } + ) + + try { + New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null + Set-Content -Path $installWim -Value 'mock-wim' + Set-Content -Path $bootWim -Value 'mock-boot' + . $script:isoScriptPath + Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log { + param($message) + $logs.Add([string]$message) + } + + Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse + @($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' -and $_ -match '/Index:2' }).Count | Should -Be 1 + @($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' }).Count | Should -Be 1 + @($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 2 + + $bootMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' } | Select-Object -First 1 + $installMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' } | Select-Object -First 1 + $bootMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $bootMountCall) + $installMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $installMountCall) + $bootAdds = @($script:dismCalls[$bootMountIndex..($installMountIndex - 1)] | Where-Object { $_ -match '/Add-Driver' }) + $bootAdds.Count | Should -Be 2 + ($bootAdds -join "`n") | Should -Match ([regex]::Escape('scsi_pkg')) + ($bootAdds -join "`n") | Should -Match ([regex]::Escape('hdc_pkg')) + ($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('net_pkg')) + ($bootAdds -join "`n") | Should -Not -Match 'iastorhsa' + ($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('name_only_pkg')) + + @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 8 + ($logs -join '|') | Should -Match 'Added 2 of 2 driver packages to boot.wim' + ($logs -join '|') | Should -Match 'Added 6 of 6 driver packages to install.wim' + } finally { + Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue + Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It "injects only the newest duplicate storage package into boot.wim" { + $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWimDedup_$([guid]::NewGuid())" + $installWim = Join-Path $contentRoot 'sources\install.wim' + $bootWim = Join-Path $contentRoot 'sources\boot.wim' + $template = Get-Content -Path $script:autoUnattendPath -Raw + $logs = [System.Collections.Generic.List[string]]::new() + + New-WinUtilDriverExportHarness -Fixtures @( + @{ Path = 'iastorvd.inf_amd64_11111111aaaaaaaa'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '1/1/2023,20.2.1.1016' }, + @{ Path = 'iastorvd.inf_amd64_22222222bbbbbbbb'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '6/1/2024,20.2.8.1028' } + ) + + try { + New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null + Set-Content -Path $installWim -Value 'mock-wim' + Set-Content -Path $bootWim -Value 'mock-boot' + . $script:isoScriptPath + Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log { + param($message) + $logs.Add([string]$message) + } + + $bootAdds = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }) + @($bootAdds | Where-Object { $_ -match '22222222bbbbbbbb' }).Count | Should -Be 2 + @($bootAdds | Where-Object { $_ -match '11111111aaaaaaaa' }).Count | Should -Be 0 + ($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to boot.wim' + ($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to install.wim' + } finally { + Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue + Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + It "injects Class=Extension driver packages like any other export" { $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoExtensionInject_$([guid]::NewGuid())" $installWim = Join-Path $contentRoot 'sources\install.wim' @@ -528,7 +612,7 @@ Describe "Win11 Creator setup media" { } @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3 - ($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 staged for WinPE, 0 excluded\)' + ($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 excluded\)' $driversInjected.Value | Should -BeTrue $script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ext_pkg_lower') @@ -628,7 +712,7 @@ Describe "Win11 Creator setup media" { } @($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 -Match 'Exported 2 of 2 driver packages \(0 excluded\)' ($logs -join '|') | Should -Not -Match 'Excluding stale duplicate driver package' $script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_11111111aaaaaaaa') @@ -669,7 +753,7 @@ Describe "Win11 Creator setup media" { } @($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 'Exported 3 of 7 driver packages \(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'" ($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*sample\.inf_amd64_11111111aaaaaaaa' \(DriverVer unknown\) superseded by '.*sample\.inf_amd64_22222222bbbbbbbb' \(DriverVer 3/1/2024,1\.2\.3\.4\)" @@ -753,8 +837,7 @@ Describe "Win11 Creator setup media" { ($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 + Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse $driversInjected.Value | Should -BeFalse } finally { Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue