mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-10-08 11:39:10 +11:00
Filter unserviceable and stale drivers before Win11 Creator injection (#5016)
* fix(win11-creator): filter unserviceable and stale drivers before injection Add-Driver aborts the whole batch when one exported package is bad, which broke ISO creation for anyone with an Extension-class or stale duplicate driver in their store (#4971, #4982). Exclude both before the single Add-Driver call instead of guessing per-vendor. * fix(win11-creator): address driver filtering review feedback Accept date-only DriverVer entries instead of treating them as unknown. Use the full folder path as the dedup fallback key so two unrelated packages can't collide on a shared leaf name. Discard the logger's own output inside the selector so an emitting -Log callback can't inflate the survivor count. Skip driver injection instead of failing the whole ISO build when nothing is left to inject. Also extracts the repeated DISM mock setup in the driver tests into one shared harness, and asserts that excluded packages are actually deleted from the export root before Add-Driver runs, not just logged. * fix: address remaining code review comments
This commit is contained in:
@@ -277,11 +277,14 @@ function Invoke-WinUtilISOModify {
|
||||
$selectedEditionId = Get-WinUtilEditionIdFromName -EditionName $selectedEditionName
|
||||
|
||||
Log "Writing autounattend.xml and edition selection..."
|
||||
Invoke-WinUtilISOScript -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallImagePath $localWim -InstallImageIndex $selectedWimIndex -InstallEditionId $selectedEditionId -Log { param($m) Log $m }
|
||||
$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 ($injectDrivers) {
|
||||
if ($driversInjected.Value) {
|
||||
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."
|
||||
} else {
|
||||
Log "Preserved the original $sourceImageFileName without mounting, exporting, or modifying it."
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ function Invoke-WinUtilISOScript {
|
||||
|
||||
.PARAMETER Log
|
||||
Optional ScriptBlock for progress/status logging. Receives a single [string] argument.
|
||||
|
||||
.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).
|
||||
#>
|
||||
param (
|
||||
[Parameter(Mandatory)][string]$ISOContentsDir,
|
||||
@@ -33,7 +37,8 @@ function Invoke-WinUtilISOScript {
|
||||
[string]$InstallEditionId = "",
|
||||
[string]$InstallImagePath = "",
|
||||
[int]$InstallImageIndex = 1,
|
||||
[scriptblock]$Log = { param($m) Write-Output $m }
|
||||
[scriptblock]$Log = { param($m) Write-Output $m },
|
||||
[ref]$DriversInjected = [ref]$false
|
||||
)
|
||||
|
||||
function Add-WinUtilISOStagedDrivers {
|
||||
@@ -41,8 +46,10 @@ function Invoke-WinUtilISOScript {
|
||||
[Parameter(Mandatory)][string]$ContentRoot,
|
||||
[Parameter(Mandatory)][string]$InstallImagePath,
|
||||
[Parameter(Mandatory)][int]$InstallImageIndex,
|
||||
[scriptblock]$Logger
|
||||
[scriptblock]$Logger,
|
||||
[ref]$DriversInjected = [ref]$false
|
||||
)
|
||||
$DriversInjected.Value = $false
|
||||
|
||||
function Copy-WinUtilISODriverFolder {
|
||||
param (
|
||||
@@ -77,6 +84,148 @@ function Invoke-WinUtilISOScript {
|
||||
}
|
||||
}
|
||||
|
||||
function Test-WinUtilISODriverExtensionClass {
|
||||
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
|
||||
|
||||
try {
|
||||
return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*"?Extension"?\s*(?:;.*)?$'
|
||||
} catch {
|
||||
$null = & $Logger "Warning: could not classify driver '$($InfFile.FullName)': $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-WinUtilISODriverPackageVersion {
|
||||
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
|
||||
|
||||
try {
|
||||
$infText = Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop
|
||||
} catch {
|
||||
$null = & $Logger "Warning: could not read '$($InfFile.FullName)' to determine its driver version: $_"
|
||||
return $null
|
||||
}
|
||||
|
||||
# The version component of DriverVer is optional per the INF spec (date-only entries
|
||||
# are valid); treat a missing version as 0.0 so date-only entries still rank correctly
|
||||
# instead of being discarded as unparseable.
|
||||
$match = [regex]::Match($infText, '(?im)^\s*DriverVer\s*=\s*(?<date>\d{1,2}/\d{1,2}/\d{4})\s*(?:,\s*(?<version>\d+(?:\.\d+){0,3}))?\s*(?:;.*)?$')
|
||||
if (-not $match.Success) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$date = [datetime]::ParseExact($match.Groups['date'].Value, 'M/d/yyyy', [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
$versionText = if ($match.Groups['version'].Success) { $match.Groups['version'].Value } else { '0' }
|
||||
if (($versionText.Split('.')).Count -lt 2) {
|
||||
$versionText = "$versionText.0"
|
||||
}
|
||||
$version = [version]$versionText
|
||||
} catch {
|
||||
$null = & $Logger "Warning: could not parse DriverVer '$($match.Value.Trim())' in '$($InfFile.FullName)': $_"
|
||||
return $null
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Date = $date
|
||||
Version = $version
|
||||
Raw = if ($match.Groups['version'].Success) { "$($match.Groups['date'].Value),$($match.Groups['version'].Value)" } else { $match.Groups['date'].Value }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-WinUtilISODriverProvider {
|
||||
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
|
||||
|
||||
try {
|
||||
$infText = Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop
|
||||
} catch {
|
||||
$null = & $Logger "Warning: could not read '$($InfFile.FullName)' to determine its provider: $_"
|
||||
return ''
|
||||
}
|
||||
|
||||
$match = [regex]::Match($infText, '(?im)^\s*Provider\s*=\s*(?<provider>.+?)\s*(?:;.*)?$')
|
||||
if (-not $match.Success) {
|
||||
return ''
|
||||
}
|
||||
return $match.Groups['provider'].Value.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Select-WinUtilISOStagedDriverPackages {
|
||||
param (
|
||||
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$DriverFolderGroups,
|
||||
[scriptblock]$Logger
|
||||
)
|
||||
|
||||
$survivingFolders = [System.Collections.Generic.List[string]]::new()
|
||||
$dedupGroups = @{}
|
||||
|
||||
foreach ($driverFolderGroup in $DriverFolderGroups) {
|
||||
$driverFolder = [string]$driverFolderGroup.Name
|
||||
$isExtension = [bool]@($driverFolderGroup.Group | Where-Object { Test-WinUtilISODriverExtensionClass -InfFile $_ }).Count
|
||||
|
||||
if ($isExtension) {
|
||||
# $null = discards $Logger's own output; this function's return value is captured
|
||||
# by the caller, and an emitting logger (e.g. this function's own default) would
|
||||
# otherwise leak into the surviving-folder list.
|
||||
$null = & $Logger "Excluding extension-class driver package '$driverFolder' from Add-Driver (Class=Extension is not a serviceable hardware driver)."
|
||||
continue
|
||||
}
|
||||
|
||||
# DISM names exported package folders <infname>_<arch>_<hash>; grouping on infname+arch
|
||||
# (dropping the hash) is what lets us recognize two exports of the same driver. When a
|
||||
# folder doesn't match that pattern, fall back to the full path rather than the leaf name:
|
||||
# two unrelated folders at different depths (e.g. group_a\duplicate and group_b\duplicate)
|
||||
# can share a leaf name, and the full path is guaranteed unique per group.
|
||||
$leafName = Split-Path -Path $driverFolder -Leaf
|
||||
$dedupKey = $driverFolder
|
||||
$nameMatch = [regex]::Match($leafName, '(?i)^(?<infname>.+)_(?<arch>x86|amd64|arm64|arm|wow)_[0-9a-f]{16}$')
|
||||
if ($nameMatch.Success) {
|
||||
$provider = Get-WinUtilISODriverProvider -InfFile $driverFolderGroup.Group[0]
|
||||
$dedupKey = "$($nameMatch.Groups['infname'].Value.ToLowerInvariant())_$($nameMatch.Groups['arch'].Value.ToLowerInvariant())_$provider"
|
||||
}
|
||||
|
||||
if (-not $dedupGroups.ContainsKey($dedupKey)) {
|
||||
$dedupGroups[$dedupKey] = [System.Collections.Generic.List[object]]::new()
|
||||
}
|
||||
$dedupGroups[$dedupKey].Add($driverFolderGroup)
|
||||
}
|
||||
|
||||
foreach ($dedupKey in $dedupGroups.Keys) {
|
||||
$candidates = $dedupGroups[$dedupKey]
|
||||
if ($candidates.Count -eq 1) {
|
||||
$survivingFolders.Add([string]$candidates[0].Name)
|
||||
continue
|
||||
}
|
||||
|
||||
$ranked = @($candidates | ForEach-Object {
|
||||
$primaryVersion = ($_.Group | ForEach-Object { Get-WinUtilISODriverPackageVersion -InfFile $_ } | Where-Object { $_ }) |
|
||||
Sort-Object -Property Date, Version -Descending | Select-Object -First 1
|
||||
[pscustomobject]@{ Folder = [string]$_.Name; Version = $primaryVersion }
|
||||
})
|
||||
|
||||
$withVersion = @($ranked | Where-Object { $_.Version })
|
||||
if ($withVersion.Count -eq 0) {
|
||||
$null = & $Logger "Warning: could not determine DriverVer for any duplicate of '$dedupKey'; keeping all $($ranked.Count) package(s) rather than guessing."
|
||||
foreach ($candidate in $ranked) {
|
||||
$survivingFolders.Add($candidate.Folder)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$kept = $withVersion | Sort-Object -Property @{ Expression = { $_.Version.Date } }, @{ Expression = { $_.Version.Version } } -Descending | Select-Object -First 1
|
||||
$survivingFolders.Add($kept.Folder)
|
||||
|
||||
foreach ($candidate in $ranked) {
|
||||
if ($candidate.Folder -eq $kept.Folder) {
|
||||
continue
|
||||
}
|
||||
$droppedVersion = if ($candidate.Version) { $candidate.Version.Raw } else { 'unknown' }
|
||||
$null = & $Logger "Excluding stale duplicate driver package '$($candidate.Folder)' (DriverVer $droppedVersion) superseded by '$($kept.Folder)' (DriverVer $($kept.Version.Raw))."
|
||||
}
|
||||
}
|
||||
|
||||
return @($survivingFolders)
|
||||
}
|
||||
|
||||
function Invoke-WinUtilISODism {
|
||||
param (
|
||||
[Parameter(Mandatory)][string[]]$Arguments,
|
||||
@@ -192,26 +341,44 @@ function Invoke-WinUtilISOScript {
|
||||
throw "Failed to stage $copyFailures boot-storage driver package folders."
|
||||
}
|
||||
|
||||
& $Logger "Exported $($driverInfs.Count) driver INF files across $($driverFolders.Count) package folders; staged $storageCount boot-storage packages for WinPE."
|
||||
$stagedDriverFolders = @(Select-WinUtilISOStagedDriverPackages -DriverFolderGroups $driverFolders -Logger $Logger)
|
||||
$metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
|
||||
Assert-WinUtilISOWimMetadata -Before $metadataBefore
|
||||
|
||||
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
|
||||
if ($stagedDriverFolders.Count -eq 0) {
|
||||
# Nothing safe to inject (e.g. every exported package was an Extension-class add-on)
|
||||
# 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) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $excludedFolder -Recurse -Force -ErrorAction Stop
|
||||
} catch {
|
||||
throw "Failed to remove excluded driver package '$excludedFolder' before injection: $_"
|
||||
}
|
||||
}
|
||||
|
||||
& $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
|
||||
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedFolders.Count) excluded)."
|
||||
|
||||
& $Logger 'Committing the driver-only install.wim change...'
|
||||
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
|
||||
$imageMounted = $false
|
||||
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
|
||||
|
||||
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
|
||||
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
|
||||
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
|
||||
& $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
|
||||
|
||||
& $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)) {
|
||||
try {
|
||||
@@ -535,6 +702,6 @@ $appxList
|
||||
Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log
|
||||
|
||||
if ($InjectCurrentSystemDrivers) {
|
||||
Add-WinUtilISOStagedDrivers -ContentRoot $ISOContentsDir -Logger $Log -InstallImagePath $InstallImagePath -InstallImageIndex $InstallImageIndex
|
||||
Add-WinUtilISOStagedDrivers -ContentRoot $ISOContentsDir -Logger $Log -InstallImagePath $InstallImagePath -InstallImageIndex $InstallImageIndex -DriversInjected $DriversInjected
|
||||
}
|
||||
}
|
||||
|
||||
+230
-48
@@ -36,6 +36,66 @@ Describe "Win11 Creator setup media" {
|
||||
return $functionAst.Extent.Text
|
||||
}
|
||||
|
||||
function New-WinUtilDriverExportHarness {
|
||||
param ([Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fixtures)
|
||||
|
||||
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
|
||||
$script:driverExportRoot = $null
|
||||
$script:driverExportFixtures = $Fixtures
|
||||
|
||||
Set-Item -Path function:global:dism.exe -Value {
|
||||
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
|
||||
|
||||
$script:dismCalls.Add(($Arguments -join '|'))
|
||||
$global:LASTEXITCODE = 0
|
||||
if ($Arguments -contains '/Get-WimInfo') {
|
||||
'Languages : en-US'
|
||||
'Installation : Client'
|
||||
'Edition : Professional'
|
||||
'ProductSuite : Terminal Server'
|
||||
'ProductType : WinNT'
|
||||
} elseif ($Arguments -contains '/Mount-Image') {
|
||||
'[==========================100.0%==========================]'
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
$script:mountAndVerifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOMountAndVerify"
|
||||
$script:cleanAndResetFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCleanAndReset"
|
||||
@@ -320,54 +380,17 @@ Describe "Win11 Creator setup media" {
|
||||
$installWim = Join-Path $contentRoot 'sources\install.wim'
|
||||
$template = Get-Content -Path $script:autoUnattendPath -Raw
|
||||
$logs = [System.Collections.Generic.List[string]]::new()
|
||||
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
function dism.exe {
|
||||
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
|
||||
|
||||
$script:dismCalls.Add(($Arguments -join '|'))
|
||||
$global:LASTEXITCODE = 0
|
||||
if ($Arguments -contains '/Get-WimInfo') {
|
||||
'Languages : en-US'
|
||||
'Installation : Client'
|
||||
'Edition : Professional'
|
||||
'ProductSuite : Terminal Server'
|
||||
'ProductType : WinNT'
|
||||
} elseif ($Arguments -contains '/Mount-Image') {
|
||||
'[==========================100.0%==========================]'
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
$fixtures = @(
|
||||
@{ 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 = 'Extension' }
|
||||
)
|
||||
|
||||
foreach ($fixture in $fixtures) {
|
||||
$fixturePath = Join-Path $exportRoot $fixture.Path
|
||||
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
|
||||
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value "[Version]`r`nClass=$($fixture.Class)" -Encoding ASCII
|
||||
}
|
||||
|
||||
return [pscustomobject]@{ ExitCode = 0 }
|
||||
} -ParameterFilter { $FilePath -eq 'dism.exe' }
|
||||
New-WinUtilDriverExportHarness -Fixtures @(
|
||||
@{ 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 = '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' }
|
||||
)
|
||||
|
||||
try {
|
||||
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
|
||||
@@ -395,10 +418,169 @@ 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 'staged 2 boot-storage packages for WinPE'
|
||||
($logs -join '|') | Should -Match 'Exported 6 of 8 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'
|
||||
($logs -join '|') | Should -Match 'DISM mount completed.'
|
||||
($logs -join '|') | Should -Not -Match '100.0%'
|
||||
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'hdx_asusext_apot_g5-tse.inf_amd64_aabbccddeeff0011')
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_7426e1b60aa62272')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_58e7118cdecb935e')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'system_pkg')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'group_a\duplicate')
|
||||
} finally {
|
||||
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It "excludes Class=Extension driver packages from Add-Driver regardless of vendor or case" {
|
||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoExtensionExclude_$([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 = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
|
||||
@{ Path = 'ext_pkg_lower'; Name = 'lowercase_extension.inf'; Class = 'extension' },
|
||||
@{ Path = 'ext_pkg_quoted'; Name = 'quoted_extension.inf'; Class = '"Extension"' }
|
||||
)
|
||||
|
||||
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 '/Add-Driver' }).Count | Should -Be 1
|
||||
($logs -join '|') | Should -Match 'Exported 1 of 3 driver packages \(0 staged for WinPE, 2 excluded\)'
|
||||
($logs -join '|') | Should -Match "Excluding extension-class driver package '.*ext_pkg_lower'"
|
||||
($logs -join '|') | Should -Match "Excluding extension-class driver package '.*ext_pkg_quoted'"
|
||||
($logs -join '|') | Should -Not -Match "Excluding extension-class driver package '.*net_pkg'"
|
||||
$driversInjected.Value | Should -BeTrue
|
||||
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ext_pkg_lower')
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ext_pkg_quoted')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'net_pkg')
|
||||
} 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'
|
||||
$template = Get-Content -Path $script:autoUnattendPath -Raw
|
||||
$logs = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
New-WinUtilDriverExportHarness -Fixtures @(
|
||||
@{ Path = 'ext_pkg_a'; Name = 'a.inf'; Class = 'Extension' },
|
||||
@{ Path = 'ext_pkg_b'; Name = 'b.inf'; Class = 'Extension' }
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
$driversInjected.Value | Should -BeFalse
|
||||
($logs -join '|') | Should -Match 'No drivers found to inject: every exported package was excluded'
|
||||
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 1
|
||||
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 0
|
||||
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 0
|
||||
} finally {
|
||||
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It "keeps packages from different providers even when the INF name and architecture match" {
|
||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoProviderCollision_$([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 = 'device.inf_amd64_11111111aaaaaaaa'; Name = 'device.inf'; Class = 'Net'; Provider = 'Contoso'; DriverVer = '1/1/2023,1.0.0.0' },
|
||||
@{ Path = 'device.inf_amd64_22222222bbbbbbbb'; Name = 'device.inf'; Class = 'Net'; Provider = 'Fabrikam'; DriverVer = '1/1/2024,2.0.0.0' }
|
||||
)
|
||||
|
||||
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 {
|
||||
param($message)
|
||||
$logs.Add([string]$message)
|
||||
}
|
||||
|
||||
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
|
||||
($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'
|
||||
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_11111111aaaaaaaa')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_22222222bbbbbbbb')
|
||||
} finally {
|
||||
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It "drops stale duplicate driver versions and keeps only the highest DriverVer" {
|
||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoStaleDedup_$([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 @(
|
||||
# Three-way duplicate mirroring the real ntprint.inf report: only the newest DriverVer should survive.
|
||||
@{ Path = 'ntprint.inf_x86_7426e1b60aa62272'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2023,10.0.26100.8875' },
|
||||
@{ Path = 'ntprint.inf_x86_6688e7b66f8d9fb5'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2024,10.0.26100.8972' },
|
||||
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' },
|
||||
# A duplicate pair where one package is missing DriverVer entirely: the parseable one must win.
|
||||
@{ Path = 'sample.inf_amd64_11111111aaaaaaaa'; Name = 'sample.inf'; Class = 'Net' },
|
||||
@{ Path = 'sample.inf_amd64_22222222bbbbbbbb'; Name = 'sample.inf'; Class = 'Net'; DriverVer = '3/1/2024,1.2.3.4' },
|
||||
# A duplicate pair keyed entirely on case-insensitive DriverVer parsing: the uppercase
|
||||
# DRIVERVER on the newer package must still be read and win the comparison.
|
||||
@{ Path = 'caps.inf_amd64_33333333cccccccc'; Name = 'caps.inf'; Class = 'Net'; DriverVer = '1/1/2020,1.0.0.0'; VersionKeyword = 'driverver' },
|
||||
@{ Path = 'caps.inf_amd64_44444444dddddddd'; Name = 'caps.inf'; Class = 'Net'; DriverVer = '1/1/2021,2.0.0.0'; VersionKeyword = 'DRIVERVER' }
|
||||
)
|
||||
|
||||
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 {
|
||||
param($message)
|
||||
$logs.Add([string]$message)
|
||||
}
|
||||
|
||||
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
|
||||
($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'"
|
||||
($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\)"
|
||||
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*caps\.inf_amd64_33333333cccccccc' \(DriverVer 1/1/2020,1\.0\.0\.0\) superseded by '.*caps\.inf_amd64_44444444dddddddd' \(DriverVer 1/1/2021,2\.0\.0\.0\)"
|
||||
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_7426e1b60aa62272')
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_6688e7b66f8d9fb5')
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'sample.inf_amd64_11111111aaaaaaaa')
|
||||
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'caps.inf_amd64_33333333cccccccc')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_58e7118cdecb935e')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'sample.inf_amd64_22222222bbbbbbbb')
|
||||
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'caps.inf_amd64_44444444dddddddd')
|
||||
} finally {
|
||||
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Reference in New Issue
Block a user