Refactor Win11 Creator ISO workflow (#4862)

* refactor: simplify Win11 Creator ISO workflow

Keep no-driver ISO generation on the fast copy-only path.

Inject exported drivers with one selected-index WIM mount, one Add-Driver pass, and one commit.

Limit WinPE staging to boot-storage drivers and improve workflow validation coverage.

* refactor: stage Win11 setup script fallback

Copy the prepared setup script payloads into sources/$//Setup/Scripts so setup media does not depend solely on answer-file extension extraction.

Keep the existing autounattend execution path unchanged and cover the fallback files in the Win11 Creator tests.

* refactor: address codex feedback

* refactor: run ISO verification in runspace

Rename the ISO logger to Write-WinUtilISOLog so the shared runspace pool imports it automatically. Run Mount & Verify off the WPF thread with dispatcher-safe UI updates, and add Pester coverage for the runspace and logger contract.

* refactor: address ISO review feedback

Keep the selected ISO stable during background verification. Apply ContentDeliveryManager settings to both the first account and the default profile. Block FAT32 USB creation when install.esd exceeds the supported file size.

* refactor: address ISO setup edge cases

Set BypassNRO before OOBE so local account setup is available during Windows Setup.

Detect registered DISM mounts during driver-injection cleanup so partial mount failures are discarded.

* refactor: address ISO review edge cases

Use the actual FAT32 file limit for install.esd USB checks.

Disable ISO modification while mount verification is running.

Apply unsupported-hardware notice suppression to the first user profile.

* refactor: stage ISO setup safeguards earlier

Set device encryption and reserved-storage registry guards before OOBE.

Enable the OEM configuration-set fallback when setup scripts are staged there.

* Update functions/private/Invoke-WinUtilISOUSB.ps1

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Titus <contact@christitus.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Omar
2026-07-28 14:08:56 -05:00
committed by GitHub
co-authored by Chris Titus coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent fae58d73c4
commit 5ebb24b107
7 changed files with 941 additions and 764 deletions
+324 -85
View File
@@ -37,14 +37,12 @@ Describe "Win11 Creator setup media" {
}
$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"
$script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport"
$script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB"
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
$script:addDriversFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Add-DriversToImage"
$script:answerFileChildElementFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Get-WinUtilISOScriptChildElement"
$script:answerFileConversionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "ConvertTo-WinUtilISOAnswerFile"
$script:editionConfigFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Write-WinUtilISOEditionConfig"
$script:wimMetadataAssertionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Assert-WinUtilISOWimMetadata"
}
It "autounattend template does not force a product key" {
@@ -58,12 +56,35 @@ Describe "Win11 Creator setup media" {
}
}
It "ISO script accepts selected edition setup metadata" {
It "sets every hardware bypass before Windows Setup checks requirements" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($bypass in 'BypassTPMCheck', 'BypassSecureBootCheck', 'BypassRAMCheck', 'BypassCPUCheck', 'BypassStorageCheck') {
$paths | Should -Match ([regex]::Escape($bypass))
}
}
It "sets OOBE-sensitive registry values before OOBE starts" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Deployment"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($valueName in 'BypassNRO', 'PreventDeviceEncryption', 'ShippedWithReserves') {
$paths | Should -Match ([regex]::Escape($valueName))
}
}
It "ISO script accepts selected edition and driver-only WIM servicing metadata" {
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
$content = Get-Content -Path $isoScriptPath -Raw
foreach ($pattern in @(
'\[string\]\$InstallEditionId',
'\[string\]\$InstallImagePath',
'\[int\]\$InstallImageIndex',
'sources\\ei\.cfg',
'PID\.txt'
@@ -85,8 +106,57 @@ Describe "Win11 Creator setup media" {
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
}
It "keeps WIM servicing limited to one driver-only mount and commit" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
"'/Mount-Image'",
"'/Add-Driver'",
"'/Commit'",
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
'install.wim metadata validation passed'
)) {
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
}
foreach ($forbiddenText in @(
'Mount-WindowsImage',
'Dismount-WindowsImage',
'Export-WindowsImage',
'Set-WindowsImage',
'/ResetBase',
'/Cleanup-Image'
)) {
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
}
}
It "stages only boot-storage drivers in WinPE" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
$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'))
}
It "rejects invalid WIM metadata before and after driver injection" {
. ([scriptblock]::Create($script:wimMetadataAssertionFunction))
$valid = @{ Languages = 'en-US'; Installation = 'Client'; Edition = 'Professional'; ProductSuite = 'Terminal Server'; ProductType = 'WinNT' }
$invalidBefore = $valid.Clone()
$invalidBefore.Edition = '<undefined>'
$invalidAfter = $valid.Clone()
$invalidAfter.ProductType = '<undefined>'
{ Assert-WinUtilISOWimMetadata -Before $invalidBefore } | Should -Throw '*already invalid*'
{ Assert-WinUtilISOWimMetadata -Before $valid -After $invalidAfter } | Should -Throw '*validation failed*'
}
It "tracks every background ISO workflow with the shared busy state" {
foreach ($functionText in @(
$script:mountAndVerifyFunction,
$script:modifyFunction,
$script:cleanAndResetFunction,
$script:exportFunction,
@@ -97,14 +167,28 @@ Describe "Win11 Creator setup media" {
}
}
It "mounts the copied image file that was verified from the ISO" {
foreach ($expectedText in @(
'$sourceImageFileName = Split-Path $wimPath -Leaf',
'$localWim = Join-Path $isoContents "sources\$sourceImageFileName"',
'Copied ISO image file not found: sources\$sourceImageFileName'
)) {
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText))
}
It "runs ISO mount and verification outside the UI thread" {
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape("Invoke-WPFRunspace -ParameterList @(,('isoPath', `$isoPath))"))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Invoke-WPFUIThread {'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Write-WinUtilISOLog'))
$script:mountAndVerifyFunction | Should -Not -Match ([regex]::Escape('Write-Win11ISOLog'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $true'))
}
It "blocks oversized install.esd before USB erase confirmation" {
$script:writeUsbFunction | Should -Match ([regex]::Escape('$installEsd = Join-Path $contentsDir "sources\install.esd"'))
$script:writeUsbFunction | Should -Match ([regex]::Escape('$esdSizeBytes -ge 4GB'))
$script:writeUsbFunction | Should -Match 'install\.esd file'
$guardIndex = $script:writeUsbFunction.IndexOf('$installEsd = Join-Path $contentsDir "sources\install.esd"')
$confirmationIndex = $script:writeUsbFunction.IndexOf('Confirm USB Erase')
$guardIndex | Should -BeGreaterThan -1
$confirmationIndex | Should -BeGreaterThan $guardIndex
}
It "maps Windows edition names to setup edition IDs" {
@@ -135,9 +219,7 @@ Describe "Win11 Creator setup media" {
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
}
It "writes ei.cfg and removes stale PID.txt for selected editions" {
. ([scriptblock]::Create($script:editionConfigFunction))
It "writes ei.cfg and removes stale PID.txt for the selected edition" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$sourcesDir = Join-Path $contentRoot "sources"
$logs = [System.Collections.Generic.List[string]]::new()
@@ -146,8 +228,10 @@ Describe "Win11 Creator setup media" {
try {
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8
Set-Content -Path (Join-Path $sourcesDir "ei.cfg") -Value "stale-cfg" -Encoding UTF8
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "Professional" -Logger $logger
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId "Professional" -Log $logger
Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
@@ -160,90 +244,245 @@ Describe "Win11 Creator setup media" {
}
}
It "skips ei.cfg when selected edition ID is unknown" {
. ([scriptblock]::Create($script:editionConfigFunction))
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$logs = [System.Collections.Generic.List[string]]::new()
$logger = { param($message) $logs.Add([string]$message) }
It "stages the complete WinUtil customization script and selected image index" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoAnswerFile_$([guid]::NewGuid())"
$template = Get-Content -Path $script:autoUnattendPath -Raw
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InstallEditionId "Core" -InstallImageIndex 6
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "" -Logger $logger
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot "autounattend.xml") -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
Test-Path (Join-Path $contentRoot "sources\ei.cfg") | Should -BeFalse
($logs -join "|") | Should -Match "selected edition ID is unknown"
$answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $nsMgr).InnerText | Should -Be '6'
$postInstallFile = $answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1"]', $nsMgr)
$postInstallFile | Should -Not -BeNullOrEmpty
$postInstallFile.InnerText | Should -Match 'Remove-AppxProvisionedPackage'
$postInstallFile.InnerText | Should -Match 'DisableWindowsConsumerFeatures'
$postInstallFile.InnerText | Should -Match 'Microsoft Compatibility Appraiser'
$postInstallFile.InnerText | Should -Match 'OneDriveSetup.exe'
$postInstallFile.InnerText | Should -Match 'function Set-WinUtilContentDeliveryManagerValues'
$postInstallFile.InnerText | Should -Match ([regex]::Escape('Set-WinUtilContentDeliveryManagerValues $defaultHive'))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilContentDeliveryManagerValues 'HKCU'"))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV1'"))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV2'"))
foreach ($defaultProfilePath in @(
'$defaultHive\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo',
'$defaultHive\Software\Microsoft\Windows\CurrentVersion\Privacy',
'$defaultHive\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy',
'$defaultHive\Software\Microsoft\Input\TIPC',
'$defaultHive\Software\Microsoft\InputPersonalization',
'$defaultHive\Software\Microsoft\InputPersonalization\TrainedDataStore',
'$defaultHive\Software\Microsoft\Personalization\Settings'
)) {
$postInstallFile.InnerText | Should -Match ([regex]::Escape($defaultProfilePath))
}
$firstLogonFile = $answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\FirstLogon.ps1"]', $nsMgr)
$firstLogonFile.InnerText | Should -Match 'WinUtil-PostInstall.ps1'
$setupScriptsRoot = Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts'
Test-Path (Join-Path $setupScriptsRoot 'Specialize.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'DefaultUser.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'FirstLogon.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'WinUtil-PostInstall.ps1') | Should -BeTrue
Get-Content -Path (Join-Path $setupScriptsRoot 'FirstLogon.ps1') -Raw | Should -Match 'WinUtil-PostInstall.ps1'
Get-Content -Path (Join-Path $setupScriptsRoot 'WinUtil-PostInstall.ps1') -Raw | Should -Match 'Remove-AppxProvisionedPackage'
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseInput($postInstallFile.InnerText, [ref]$tokens, [ref]$errors) | Out-Null
$errors.Count | Should -Be 0
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "converts autounattend setup metadata without preserving placeholder product keys" {
. ([scriptblock]::Create($script:answerFileChildElementFunction))
. ([scriptblock]::Create($script:answerFileConversionFunction))
[xml]$templateXml = Get-Content -Path $script:autoUnattendPath -Raw
$unattendNs = "urn:schemas-microsoft-com:unattend"
$nsMgr = New-Object System.Xml.XmlNamespaceManager($templateXml.NameTable)
$nsMgr.AddNamespace("u", $unattendNs)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$setupComponent = $templateXml.SelectSingleNode('//u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
$userData = $templateXml.CreateElement("UserData", $unattendNs)
$productKey = $templateXml.CreateElement("ProductKey", $unattendNs)
$key = $templateXml.CreateElement("Key", $unattendNs)
$key.InnerText = "00000-00000-00000-00000-00000"
[void]$productKey.AppendChild($key)
[void]$userData.AppendChild($productKey)
[void]$setupComponent.AppendChild($userData)
$originalSetupFileCount = $templateXml.SelectNodes("//sg:File", $nsMgr).Count
[xml]$converted = ConvertTo-WinUtilISOAnswerFile -XmlContent $templateXml.OuterXml -ImageIndex 3
$convertedNsMgr = New-Object System.Xml.XmlNamespaceManager($converted.NameTable)
$convertedNsMgr.AddNamespace("u", $unattendNs)
$convertedNsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$converted.DocumentElement.NamespaceURI | Should -Be $unattendNs
$converted.DocumentElement.GetAttribute("xmlns:wcm") | Should -Be "http://schemas.microsoft.com/WMIConfig/2002/State"
$converted.SelectNodes('//u:component[@name="Microsoft-Windows-Setup"]/u:UserData/u:ProductKey', $convertedNsMgr).Count | Should -Be 0
$converted.SelectSingleNode('//u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $convertedNsMgr).InnerText | Should -Be "3"
$converted.SelectNodes("//sg:File", $convertedNsMgr).Count | Should -Be $originalSetupFileCount
}
It "logs DISM output for driver injection through a mocked command" {
. ([scriptblock]::Create($script:addDriversFunction))
$script:dismArguments = @()
function dism {
param([Parameter(ValueFromRemainingArguments)]$Arguments)
$script:dismArguments = @($Arguments)
"driver one"
"driver two"
}
It "stages storage drivers for WinPE and adds all drivers to one install.wim index" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
Add-DriversToImage -MountPath "C:\Mount" -DriverDir "C:\Drivers" -Label "install" -Logger {
param($message)
$logs.Add([string]$message)
$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%==========================]'
}
}
($script:dismArguments -join "|") | Should -Be "/English|/image:C:\Mount|/Add-Driver|/Driver:C:\Drivers|/Recurse"
($logs -join "|") | Should -Be " dism[install]: driver one| dism[install]: driver two"
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' }
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)
}
$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
@($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 '/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'
[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 'staged 2 boot-storage packages for WinPE'
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
($logs -join '|') | Should -Not -Match '100.0%'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "keeps driver export and boot.wim injection behind the selected branch" {
$content = Get-Content -Path $script:isoScriptPath -Raw
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'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
foreach ($expectedText in @(
'if ($InjectCurrentSystemDrivers)',
'Export-WindowsDriver -Online -Destination $driverExportRoot',
'Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log',
'Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log',
'Warning: boot.wim not found - skipping boot.wim driver injection.',
'Driver injection skipped.'
)) {
$content | Should -Match ([regex]::Escape($expectedText))
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 50
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} 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'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*DISM mount failed*'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "does not add driver setup artifacts when injection is disabled" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNoDrivers_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InjectCurrentSystemDrivers $false -InstallEditionId 'Core'
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
[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
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "enables configuration-set fallback when staging OEM setup scripts" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoFallback_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId 'Core'
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr).InnerText |
Should -Be 'true'
Test-Path (Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts\FirstLogon.ps1') | Should -BeTrue
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}