mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 09:31:15 +10:00
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:
co-authored by
Chris Titus
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent
fae58d73c4
commit
5ebb24b107
+324
-85
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user