diff --git a/AGENTS.md b/AGENTS.md index 9cc2594f..1f25165c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,5 +189,6 @@ When the user corrects an agent approach, add or tighten one concrete rule here - When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic. - Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity. - For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media. +- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. - For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives. - For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP. diff --git a/functions/private/Invoke-WinUtilISO.ps1 b/functions/private/Invoke-WinUtilISO.ps1 index 53dd31d8..af9f6f3d 100644 --- a/functions/private/Invoke-WinUtilISO.ps1 +++ b/functions/private/Invoke-WinUtilISO.ps1 @@ -1,4 +1,4 @@ -function Write-Win11ISOLog { +function Write-WinUtilISOLog { param([string]$Message) $ts = (Get-Date).ToString("HH:mm:ss") $logLine = "[$ts] $Message" @@ -35,7 +35,7 @@ function Invoke-WinUtilISOBrowse { $sync["WPFWin11ISOModifySection"].Visibility = "Collapsed" $sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed" - Write-Win11ISOLog "ISO selected: $isoPath ($fileSizeGB GB)" + Write-WinUtilISOLog "ISO selected: $isoPath ($fileSizeGB GB)" } function Invoke-WinUtilISOMountAndVerify { @@ -46,84 +46,102 @@ function Invoke-WinUtilISOMountAndVerify { return } - Write-Win11ISOLog "Mounting ISO: $isoPath" + Write-WinUtilISOLog "Mounting ISO: $isoPath" Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Mounting ISO..." -Percent 10 + $sync["WPFWin11ISOBrowseButton"].IsEnabled = $false + $sync["WPFWin11ISOMountButton"].IsEnabled = $false + $sync["WPFWin11ISOModifyButton"].IsEnabled = $false + $sync["Win11ISOProcessRunning"] = $true - try { - Mount-DiskImage -ImagePath $isoPath + Invoke-WPFRunspace -ParameterList @(,('isoPath', $isoPath)) -ScriptBlock { + param($isoPath) - do { - Start-Sleep -Milliseconds 500 - } until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter) + try { + Mount-DiskImage -ImagePath $isoPath - $driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":" - Write-Win11ISOLog "Mounted at drive $driveLetter" + do { + Start-Sleep -Milliseconds 500 + } until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter) - Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Verifying ISO contents..." -Percent 30 + $driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":" + Write-WinUtilISOLog "Mounted at drive $driveLetter" - $wimPath = Join-Path $driveLetter "sources\install.wim" - $esdPath = Join-Path $driveLetter "sources\install.esd" + Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Verifying ISO contents..." -Percent 30 - if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) { - Dismount-DiskImage -ImagePath $isoPath - Write-Win11ISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO." - [System.Windows.MessageBox]::Show( - "This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.", - "Invalid ISO", "OK", "Error") - Set-WinUtilTweaksProgressIndicator -Visible $false - return - } + $wimPath = Join-Path $driveLetter "sources\install.wim" + $esdPath = Join-Path $driveLetter "sources\install.esd" - $activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath } - - Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55 - $imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName - - if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) { - Dismount-DiskImage -ImagePath $isoPath - Write-Win11ISOLog "ERROR: No 'Windows 11' edition found in the image." - [System.Windows.MessageBox]::Show( - "No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.", - "Not a Windows 11 ISO", "OK", "Error") - Set-WinUtilTweaksProgressIndicator -Visible $false - return - } - - $sync["Win11ISOImageInfo"] = $imageInfo - - $sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)" - $sync["WPFWin11ISOEditionComboBox"].Dispatcher.Invoke([action]{ - $sync["WPFWin11ISOEditionComboBox"].Items.Clear() - foreach ($img in $imageInfo) { - [void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)") - } - if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) { - $proIndex = -1 - for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) { - if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") { - $proIndex = $i; break - } + if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) { + Dismount-DiskImage -ImagePath $isoPath + Write-WinUtilISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO." + Invoke-WPFUIThread { + [System.Windows.MessageBox]::Show( + "This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.", + "Invalid ISO", "OK", "Error") } - $sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 } + return } - }) - $sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible" - $sync["Win11ISODriveLetter"] = $driveLetter - $sync["Win11ISOWimPath"] = $activeWim - $sync["Win11ISOImagePath"] = $isoPath - $sync["WPFWin11ISOModifySection"].Visibility = "Visible" + $activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath } - Set-WinUtilTweaksProgressIndicator -Visible $true -Label "ISO verified" -Percent 100 - Write-Win11ISOLog "ISO verified OK. Editions found: $($imageInfo.Count)" - } catch { - Write-Win11ISOLog "ERROR during mount/verify: $_" - [System.Windows.MessageBox]::Show( - "An error occurred while mounting or verifying the ISO:`n`n$_", - "Error", "OK", "Error") - } finally { - Start-Sleep -Milliseconds 800 - Set-WinUtilTweaksProgressIndicator -Visible $false + Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55 + $imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName + + if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) { + Dismount-DiskImage -ImagePath $isoPath + Write-WinUtilISOLog "ERROR: No 'Windows 11' edition found in the image." + Invoke-WPFUIThread { + [System.Windows.MessageBox]::Show( + "No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.", + "Not a Windows 11 ISO", "OK", "Error") + } + return + } + + $sync["Win11ISOImageInfo"] = $imageInfo + $sync["Win11ISODriveLetter"] = $driveLetter + $sync["Win11ISOWimPath"] = $activeWim + $sync["Win11ISOImagePath"] = $isoPath + + Invoke-WPFUIThread { + $sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)" + $sync["WPFWin11ISOEditionComboBox"].Items.Clear() + foreach ($img in $imageInfo) { + [void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)") + } + if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) { + $proIndex = -1 + for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) { + if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") { + $proIndex = $i; break + } + } + $sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 } + } + $sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible" + $sync["WPFWin11ISOModifySection"].Visibility = "Visible" + $sync["WPFWin11ISOModifyButton"].IsEnabled = $true + } + + Set-WinUtilTweaksProgressIndicator -Visible $true -Label "ISO verified" -Percent 100 + Write-WinUtilISOLog "ISO verified OK. Editions found: $($imageInfo.Count)" + } catch { + $errorMessage = $_ + Write-WinUtilISOLog "ERROR during mount/verify: $errorMessage" + Invoke-WPFUIThread { + [System.Windows.MessageBox]::Show( + "An error occurred while mounting or verifying the ISO:`n`n$errorMessage", + "Error", "OK", "Error") + } + } finally { + Start-Sleep -Milliseconds 800 + Set-WinUtilTweaksProgressIndicator -Visible $false + Invoke-WPFUIThread { + $sync["WPFWin11ISOBrowseButton"].IsEnabled = $true + $sync["WPFWin11ISOMountButton"].IsEnabled = $true + $sync["Win11ISOProcessRunning"] = $false + } + } } } @@ -147,7 +165,7 @@ function Invoke-WinUtilISOModify { $selectedWimIndex = $sync["Win11ISOImageInfo"][0].ImageIndex } $selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" } - Write-Win11ISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)" + Write-WinUtilISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)" $sync["WPFWin11ISOModifyButton"].IsEnabled = $false $sync["Win11ISOModifying"] = $true @@ -170,7 +188,6 @@ function Invoke-WinUtilISOModify { $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true - $runspace.SessionStateProxy.SetVariable("sync", $sync) $runspace.SessionStateProxy.SetVariable("isoPath", $isoPath) $runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter) @@ -182,7 +199,7 @@ function Invoke-WinUtilISOModify { $runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers) $isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ${function:Invoke-WinUtilISOScript}.ToString() + "`n}" - $win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}" + $win11ISOLogFuncDef = "function Write-WinUtilISOLog {`n" + ${function:Write-WinUtilISOLog}.ToString() + "`n}" $runspace.SessionStateProxy.SetVariable("isoScriptFuncDef", $isoScriptFuncDef) $runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef) @@ -235,121 +252,6 @@ function Invoke-WinUtilISOModify { } } - function Get-WinUtilMountedImageEditionId { - param( - [Parameter(Mandatory)][string]$MountDir, - [string]$EditionName, - [scriptblock]$Logger - ) - - try { - $dismOutput = & dism /English "/Image:$MountDir" /Get-CurrentEdition 2>&1 - foreach ($line in $dismOutput) { - if ($line -match '^\s*Current Edition\s*:\s*(.+?)\s*$') { - $editionId = $Matches[1].Trim() - if ($editionId) { - if ($Logger) { $null = $Logger.Invoke("Detected mounted image EditionID: $editionId") } - return $editionId - } - } - } - } catch { - if ($Logger) { $null = $Logger.Invoke("Warning: could not detect mounted image EditionID with DISM: $_") } - } - - $fallbackEditionId = Get-WinUtilEditionIdFromName -EditionName $EditionName - if ($fallbackEditionId -and $Logger) { - $null = $Logger.Invoke("Using fallback EditionID '$fallbackEditionId' from selected edition name.") - } - return $fallbackEditionId - } - - function Get-DismImageInfoMap { - param( - [Parameter(Mandatory)][string]$ImagePath, - [int]$Index = 1 - ) - - $map = @{} - $lines = & dism /English "/Get-ImageInfo" "/ImageFile:$ImagePath" "/Index:$Index" - foreach ($line in $lines) { - if ($line -match '^\s*([^:]+?)\s*:\s*(.*)$') { - $key = $Matches[1].Trim() - $val = $Matches[2].Trim() - if (-not $map.ContainsKey($key)) { - $map[$key] = $val - } - } - } - return $map - } - - function Invoke-WinUtilWimMetadataHydration { - param( - [Parameter(Mandatory)][string]$ImagePath, - [Parameter(Mandatory)][string]$EditionName, - [scriptblock]$Logger - ) - - $metadataLogger = $Logger - - function LogMeta([string]$Message) { - if ($metadataLogger) { - $null = $metadataLogger.Invoke($Message) - } - } - - $before = Get-DismImageInfoMap -ImagePath $ImagePath -Index 1 - $undefinedBefore = @($before.GetEnumerator() | Where-Object { $_.Value -eq '' } | ForEach-Object { $_.Key }) - - if ($undefinedBefore.Count -eq 0) { - LogMeta "Metadata check: no undefined DISM fields detected." - return - } - - LogMeta "Metadata check: undefined DISM fields detected: $($undefinedBefore -join ', ')" - LogMeta "Attempting best-effort metadata hydration for install.wim..." - - $setImage = Get-Command Set-WindowsImage -ErrorAction SilentlyContinue - if (-not $setImage) { - LogMeta "Set-WindowsImage is unavailable on this host; cannot write additional WIM metadata fields." - return - } - - $targetName = if ($EditionName -and $EditionName -ne 'Unknown') { $EditionName } else { $before['Name'] } - if (-not $targetName) { $targetName = 'Windows 11' } - - $targetDescription = if ($before['Description'] -and $before['Description'] -ne '') { - $before['Description'] - } else { - $targetName - } - - $setArgs = @{ - ImagePath = $ImagePath - Index = 1 - Name = $targetName - Description = $targetDescription - ErrorAction = 'Stop' - } - - try { - Set-WindowsImage @setArgs | Out-Null - LogMeta "Applied Set-WindowsImage metadata updates (Name/Description)." - } catch { - LogMeta "Warning: Set-WindowsImage metadata update failed: $_" - } - - $after = Get-DismImageInfoMap -ImagePath $ImagePath -Index 1 - $undefinedAfter = @($after.GetEnumerator() | Where-Object { $_.Value -eq '' } | ForEach-Object { $_.Key }) - if ($undefinedAfter.Count -eq 0) { - LogMeta "Metadata hydration complete: no undefined DISM fields remain." - } else { - LogMeta "Metadata hydration complete. Remaining undefined DISM fields: $($undefinedAfter -join ', ')" - LogMeta "Note: some DISM metadata fields are read-only and come from Microsoft image internals." - } - } - try { $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed" @@ -359,51 +261,30 @@ function Invoke-WinUtilISOModify { Log "Creating working directory: $workDir" $isoContents = Join-Path $workDir "iso_contents" - $mountDir = Join-Path $workDir "wim_mount" - New-Item -ItemType Directory -Path $isoContents, $mountDir -Force + New-Item -ItemType Directory -Path $isoContents -Force SetProgress "Copying ISO contents..." 10 Log "Copying ISO contents from $driveLetter to $isoContents..." & robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS Log "ISO contents copied." - SetProgress "Mounting install.wim..." 25 + SetProgress "Preparing setup media..." 25 $sourceImageFileName = Split-Path $wimPath -Leaf $localWim = Join-Path $isoContents "sources\$sourceImageFileName" if (-not (Test-Path $localWim)) { throw "Copied ISO image file not found: sources\$sourceImageFileName" } - Set-ItemProperty -Path $localWim -Name IsReadOnly -Value $false + $selectedEditionId = Get-WinUtilEditionIdFromName -EditionName $selectedEditionName - Log "Mounting install.wim (Index ${selectedWimIndex}: $selectedEditionName) at $mountDir..." - Mount-WindowsImage -ImagePath $localWim -Index $selectedWimIndex -Path $mountDir - SetProgress "Modifying install.wim..." 45 - $selectedEditionId = Get-WinUtilMountedImageEditionId -MountDir $mountDir -EditionName $selectedEditionName -Logger ${function:Log} + 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 } - Log "Applying WinUtil modifications to install.wim..." - Invoke-WinUtilISOScript -ScratchDir $mountDir -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallEditionId $selectedEditionId -InstallImageIndex 1 -Log { param($m) Log $m } - - SetProgress "Cleaning up component store (WinSxS)..." 56 - Log "Running DISM component store cleanup (/ResetBase)..." - & dism /English "/image:$mountDir" /Cleanup-Image /StartComponentCleanup /ResetBase | ForEach-Object { Log $_ } - Log "Component store cleanup complete." - - SetProgress "Saving modified install.wim..." 65 - Log "Dismounting and saving install.wim. This will take several minutes..." - Dismount-WindowsImage -Path $mountDir -Save - Log "install.wim saved." - - SetProgress "Removing unused editions from install.wim..." 70 - Log "Exporting edition '$selectedEditionName' (Index $selectedWimIndex) to a single-edition install.wim..." - $exportWim = Join-Path $isoContents "sources\install_export.wim" - Export-WindowsImage -SourceImagePath $localWim -SourceIndex $selectedWimIndex -DestinationImagePath $exportWim - Remove-Item -Path $localWim -Force - Rename-Item -Path $exportWim -NewName "install.wim" -Force - $localWim = Join-Path $isoContents "sources\install.wim" - Log "Unused editions removed. install.wim now contains only '$selectedEditionName'." - - SetProgress "Hydrating WIM metadata..." 76 - Invoke-WinUtilWimMetadataHydration -ImagePath $localWim -EditionName $selectedEditionName -Logger ${function:Log} + SetProgress "Preserving install image..." 70 + if ($injectDrivers) { + Log "Added current-system drivers to $sourceImageFileName index $selectedWimIndex with one mount and commit." + } else { + Log "Preserved the original $sourceImageFileName without mounting, exporting, or modifying it." + } SetProgress "Dismounting source ISO..." 80 Log "Dismounting original ISO..." @@ -421,16 +302,6 @@ function Invoke-WinUtilISOModify { } catch { Log "ERROR during modification: $_" - try { - if (Test-Path $mountDir) { - $mountedImages = Get-WindowsImage -Mounted | Where-Object { $_.Path -eq $mountDir } - if ($mountedImages) { - Log "Cleaning up: dismounting install.wim (discarding changes)..." - Dismount-WindowsImage -Path $mountDir -Discard - } - } - } catch { Log "Warning: could not dismount install.wim during cleanup: $_" } - try { $mountedISO = Get-DiskImage -ImagePath $isoPath if ($mountedISO -and $mountedISO.Attached) { @@ -498,9 +369,9 @@ function Invoke-WinUtilISOCheckExistingWork { $sync["WPFWin11ISOOutputSection"].Visibility = "Visible" $modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm") - Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)" - Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4." - Write-Win11ISOLog "Click 'Clean & Reset' if you want to start over with a new ISO." + Write-WinUtilISOLog "Existing working directory found: $($existingWorkDir.FullName)" + Write-WinUtilISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4." + Write-WinUtilISOLog "Click 'Clean & Reset' if you want to start over with a new ISO." [System.Windows.MessageBox]::Show( "A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nStep 4 (output options) has been restored so you can save the already-modified image.`n`nClick 'Clean & Reset' in Step 4 if you want to start over.", @@ -690,29 +561,29 @@ function Invoke-WinUtilISOExport { } if (-not $oscdimg) { - Write-Win11ISOLog "oscdimg.exe not found. Attempting to install via winget..." + Write-WinUtilISOLog "oscdimg.exe not found. Attempting to install via winget..." try { # First ensure winget is installed and operational Install-WinUtilWinget $winget = Get-Command winget $result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements - Write-Win11ISOLog "winget output: $result" + Write-WinUtilISOLog "winget output: $result" $oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" | Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } | Select-Object -First 1 -ExpandProperty FullName } catch { - Write-Win11ISOLog "winget not available or install failed: $_" + Write-WinUtilISOLog "winget not available or install failed: $_" } if (-not $oscdimg) { - Write-Win11ISOLog "oscdimg.exe still not found after install attempt." + Write-WinUtilISOLog "oscdimg.exe still not found after install attempt." [System.Windows.MessageBox]::Show( "oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install", "oscdimg Not Found", "OK", "Warning") return } - Write-Win11ISOLog "oscdimg.exe installed successfully." + Write-WinUtilISOLog "oscdimg.exe installed successfully." } $sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false @@ -727,7 +598,7 @@ function Invoke-WinUtilISOExport { $runspace.SessionStateProxy.SetVariable("outputISO", $outputISO) $runspace.SessionStateProxy.SetVariable("oscdimg", $oscdimg) - $win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}" + $win11ISOLogFuncDef = "function Write-WinUtilISOLog {`n" + ${function:Write-WinUtilISOLog}.ToString() + "`n}" $runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef) $script = [Management.Automation.PowerShell]::Create() @@ -745,13 +616,13 @@ function Invoke-WinUtilISOExport { } try { - Write-Win11ISOLog "Exporting to ISO: $outputISO" + Write-WinUtilISOLog "Exporting to ISO: $outputISO" SetProgress "Building ISO..." 10 $bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`"" $oscdimgArgs = @("-m", "-o", "-u2", "-udfver102", "-bootdata:$bootData", "-l`"CTOS_MODIFIED`"", "`"$contentsDir`"", "`"$outputISO`"") - Write-Win11ISOLog "Running oscdimg..." + Write-WinUtilISOLog "Running oscdimg..." $psi = [System.Diagnostics.ProcessStartInfo]::new() $psi.FileName = $oscdimg @@ -768,7 +639,7 @@ function Invoke-WinUtilISOExport { # Stream stdout line-by-line as oscdimg runs while (-not $proc.StandardOutput.EndOfStream) { $line = $proc.StandardOutput.ReadLine() - if ($line.Trim()) { Write-Win11ISOLog $line } + if ($line.Trim()) { Write-WinUtilISOLog $line } } $proc.WaitForExit() @@ -776,17 +647,17 @@ function Invoke-WinUtilISOExport { # Flush any stderr after process exits $stderr = $proc.StandardError.ReadToEnd() foreach ($line in ($stderr -split "`r?`n")) { - if ($line.Trim()) { Write-Win11ISOLog "[stderr]$line" } + if ($line.Trim()) { Write-WinUtilISOLog "[stderr]$line" } } if ($proc.ExitCode -eq 0) { SetProgress "ISO exported" 100 - Write-Win11ISOLog "ISO exported successfully: $outputISO" + Write-WinUtilISOLog "ISO exported successfully: $outputISO" $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ [System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info") }) } else { - Write-Win11ISOLog "oscdimg exited with code $($proc.ExitCode)." + Write-WinUtilISOLog "oscdimg exited with code $($proc.ExitCode)." $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ [System.Windows.MessageBox]::Show( "oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.", @@ -794,7 +665,7 @@ function Invoke-WinUtilISOExport { }) } } catch { - Write-Win11ISOLog "ERROR during ISO export: $_" + Write-WinUtilISOLog "ERROR during ISO export: $_" $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ [System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error") }) diff --git a/functions/private/Invoke-WinUtilISOScript.ps1 b/functions/private/Invoke-WinUtilISOScript.ps1 index fb2bb205..cc26b5bc 100644 --- a/functions/private/Invoke-WinUtilISOScript.ps1 +++ b/functions/private/Invoke-WinUtilISOScript.ps1 @@ -1,220 +1,228 @@ function Invoke-WinUtilISOScript { <# .SYNOPSIS - Applies WinUtil modifications to a mounted Windows 11 install.wim image. + Prepares copied Windows setup media without modifying its install image. .DESCRIPTION - Removes AppX bloatware and OneDrive, optionally injects all drivers exported from - the running system into install.wim and boot.wim (controlled by the - -InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware - bypass, privacy, OOBE, telemetry, update suppression), deletes CEIP/WU - scheduled-task definition files, and optionally writes autounattend.xml to the ISO - root and removes the support\ folder from the ISO contents directory. - - All setup scripts embedded in the autounattend.xml nodes are - written directly into the WIM at their target paths under C:\Windows\Setup\Scripts\ - to ensure they survive Windows Setup stripping unrecognised-namespace XML elements - from the Panther copy of the answer file. - - Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO). - - .PARAMETER ScratchDir - Mandatory. Full path to the directory where the Windows image is currently mounted. + Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup + in the answer file for first logon, writes sources\ei.cfg for the selected + edition, and optionally adds current-system drivers to one install.wim index. .PARAMETER ISOContentsDir - Optional. Root directory of the extracted ISO contents. When supplied, - autounattend.xml is written here and the support\ folder is removed. + Root directory of the copied ISO contents. .PARAMETER AutoUnattendXml - Optional. Full XML content for autounattend.xml. If empty, the OOBE bypass - file is skipped and a warning is logged. - - .PARAMETER InjectCurrentSystemDrivers - Optional. When $true, exports all drivers from the running system and injects - them into install.wim and boot.wim index 2 (Windows Setup PE). - Defaults to $false. + Full XML content for autounattend.xml. .PARAMETER InstallEditionId - Optional. Windows edition ID for the selected image, for example Professional - or Core. Used to write sources\ei.cfg so setup does not fall back to an - embedded firmware product key for a different edition. + Windows setup EditionID for sources\ei.cfg, for example Professional or Core. + + .PARAMETER InstallImagePath + Copied install.wim to service when current-system driver injection is enabled. .PARAMETER InstallImageIndex - Optional. Image index that setup should install from the final install.wim. - Win11 Creator exports the selected edition to a single-image WIM, so this - defaults to 1. + Selected edition index in install.wim. .PARAMETER Log Optional ScriptBlock for progress/status logging. Receives a single [string] argument. - - .EXAMPLE - Invoke-WinUtilISOScript -ScratchDir "C:\Temp\wim_mount" - - .EXAMPLE - Invoke-WinUtilISOScript ` - -ScratchDir $mountDir ` - -ISOContentsDir $isoRoot ` - -AutoUnattendXml (Get-Content .\tools\autounattend.xml -Raw) ` - -Log { param($m) Write-Host $m } - - .NOTES - Author : Chris Titus @christitustech - GitHub : https://github.com/ChrisTitusTech #> param ( - [Parameter(Mandatory)][string]$ScratchDir, - [string]$ISOContentsDir = "", + [Parameter(Mandatory)][string]$ISOContentsDir, [string]$AutoUnattendXml = "", [bool]$InjectCurrentSystemDrivers = $false, [string]$InstallEditionId = "", + [string]$InstallImagePath = "", [int]$InstallImageIndex = 1, [scriptblock]$Log = { param($m) Write-Output $m } ) - function Set-ISOScriptReg { - param ([string]$Path, [string]$Name, [string]$Type, [string]$Value) - try { - & reg add $Path /v $Name /t $Type /d $Value /f - & $Log "Set registry value: $Path\$Name" - } catch { - & $Log "Error setting registry value: $_" + + function Add-WinUtilISOStagedDrivers { + param ( + [Parameter(Mandatory)][string]$ContentRoot, + [Parameter(Mandatory)][string]$InstallImagePath, + [Parameter(Mandatory)][int]$InstallImageIndex, + [scriptblock]$Logger + ) + + function Copy-WinUtilISODriverFolder { + param ( + [Parameter(Mandatory)][string]$Source, + [Parameter(Mandatory)][string]$Destination + ) + + $folderName = Split-Path $Source -Leaf + $targetPath = Join-Path $Destination $folderName + $suffix = 1 + while (Test-Path -LiteralPath $targetPath) { + $targetPath = Join-Path $Destination "${folderName}_$suffix" + $suffix++ + } + + Copy-Item -LiteralPath $Source -Destination $targetPath -Recurse -Force -ErrorAction Stop + return $targetPath } - } - function Remove-ISOScriptReg { - param ([string]$path) - try { - & reg delete $path /f - & $Log "Removed registry key: $path" - } catch { - & $Log "Error removing registry key: $_" + function Test-WinUtilISOStorageDriver { + param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile) + + if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') { + return $true + } + + try { + return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*(SCSIAdapter|HDC)\s*(?:;.*)?$' + } catch { + & $Logger "Warning: could not classify storage driver '$($InfFile.FullName)': $_" + return $false + } } - } - function Add-DriversToImage { - param ([string]$MountPath, [string]$DriverDir, [string]$Label = "image", [scriptblock]$Logger) - & dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse | - ForEach-Object { & $Logger " dism[$Label]: $_" } - } + function Invoke-WinUtilISODism { + param ( + [Parameter(Mandatory)][string[]]$Arguments, + [Parameter(Mandatory)][string]$Operation + ) + + $output = @(& dism.exe @Arguments 2>&1) + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + foreach ($line in @($output | Select-Object -Last 20)) { + if (-not [string]::IsNullOrWhiteSpace([string]$line)) { + & $Logger " dism[$Operation]: $line" + } + } + throw "DISM $Operation failed with exit code $exitCode." + } + if ($Operation -ne 'metadata') { + & $Logger "DISM $Operation completed." + } + return $output + } + + function Get-WinUtilISOWimMetadata { + param ([Parameter(Mandatory)][string]$ImagePath, [Parameter(Mandatory)][int]$Index) + + $metadata = @{} + $output = Invoke-WinUtilISODism -Arguments @('/English', '/Get-WimInfo', "/WimFile:$ImagePath", "/Index:$Index") -Operation 'metadata' + foreach ($line in $output) { + if ([string]$line -match '^\s*([^:]+?)\s*:\s*(.*?)\s*$') { + $metadata[$Matches[1].Trim()] = $Matches[2].Trim() + } + } + return $metadata + } + + function Assert-WinUtilISOWimMetadata { + param ( + [Parameter(Mandatory)][hashtable]$Before, + [hashtable]$After + ) + + foreach ($key in 'Languages', 'Installation', 'Edition', 'ProductSuite', 'ProductType') { + $beforeValue = [string]$Before[$key] + if ($beforeValue -eq '' -or ($key -in 'Installation', 'Edition', 'ProductType' -and [string]::IsNullOrWhiteSpace($beforeValue))) { + throw "install.wim metadata is already invalid: $key is undefined. Driver injection was not attempted." + } + if ($After) { + $afterValue = [string]$After[$key] + if ($afterValue -eq '' -or ($beforeValue -and $afterValue -ne $beforeValue)) { + throw "install.wim metadata validation failed after driver injection: $key changed from '$beforeValue' to '$afterValue'." + } + } + } + } + + function Test-WinUtilISOMountedImage { + param ([Parameter(Mandatory)][string]$Path) + + return @(& dism.exe /English /Get-MountedImageInfo 2>$null) -match [regex]::Escape($Path) + } + + if ([IO.Path]::GetExtension($InstallImagePath) -ne '.wim') { + throw 'Current-system driver injection requires install.wim; install.esd cannot be serviced in place.' + } + if (-not (Test-Path -LiteralPath $InstallImagePath)) { + throw "install.wim was not found: $InstallImagePath" + } + if ($InstallImageIndex -lt 1) { + throw 'Current-system driver injection requires a valid install.wim image index.' + } + + $driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss')_$(([guid]::NewGuid()).ToString('N').Substring(0, 8))" + $mountDir = Join-Path (Split-Path -Path $ContentRoot -Parent) 'wim_mount' + New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null + $imageMounted = $false - function Invoke-BootWimInject { - param ([string]$BootWimPath, [string]$DriverDir, [scriptblock]$Logger) - Set-ItemProperty -Path $BootWimPath -Name IsReadOnly -Value $false - $mountDir = Join-Path $env:TEMP "WinUtil_BootMount_$(Get-Random)" - New-Item -Path $mountDir -ItemType Directory -Force try { - & $Logger "Mounting boot.wim (index 2) for driver injection..." - Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir - Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger - & $Logger "Saving boot.wim..." - Dismount-WindowsImage -Path $mountDir -Save - & $Logger "boot.wim driver injection complete." - } catch { - & $Logger "Warning: boot.wim driver injection failed: $_" - try { Dismount-WindowsImage -Path $mountDir -Discard } catch { & $Logger "Warning: could not discard boot.wim mount: $_" } + & $Logger "Exporting current system drivers before modifying install.wim..." + $dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" + $dismProcess = Start-Process -FilePath "dism.exe" -ArgumentList "/online /export-driver /destination:`"$driverExportRoot`" /LogPath:`"$dismLog`"" -Wait -NoNewWindow -PassThru + if ($dismProcess.ExitCode -ne 0) { + throw "dism.exe driver export failed with exit code $($dismProcess.ExitCode)." + } + + $driverInfs = @(Get-ChildItem -Path $driverExportRoot -Filter '*.inf' -Recurse -File) + if ($driverInfs.Count -eq 0) { + throw 'DISM exported no driver INF files.' + } + $driverFolders = @($driverInfs | Group-Object { $_.Directory.FullName }) + $winpeDriverDir = Join-Path $ContentRoot '$WinpeDriver$' + $storageCount = 0 + $copyFailures = 0 + + foreach ($driverFolderGroup in $driverFolders) { + $driverFolder = [string]$driverFolderGroup.Name + $storageInfs = @($driverFolderGroup.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ }) + if ($storageInfs.Count -eq 0) { + continue + } + + try { + New-Item -Path $winpeDriverDir -ItemType Directory -Force | Out-Null + $winpeTarget = Copy-WinUtilISODriverFolder -Source $driverFolder -Destination $winpeDriverDir + $storageCount++ + & $Logger "Staged boot-storage package '$driverFolder' for WinPE as '$winpeTarget'." + } catch { + $copyFailures++ + & $Logger "Warning: failed to stage boot-storage package '$driverFolder': $_" + } + } + + if ($copyFailures -gt 0) { + throw "Failed to stage $copyFailures boot-storage driver package folders." + } + + & $Logger "Exported $($driverInfs.Count) driver INF files across $($driverFolders.Count) package folders; staged $storageCount boot-storage packages for WinPE." + $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 + + & $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.' } finally { - Remove-Item -Path $mountDir -Recurse -Force - } - } - - function Get-WinUtilISOScriptChildElement { - param ( - [Parameter(Mandatory)][System.Xml.XmlElement]$Parent, - [Parameter(Mandatory)][string]$Name, - [Parameter(Mandatory)][string]$NamespaceUri - ) - - foreach ($childNode in $Parent.ChildNodes) { - if ($childNode.NodeType -eq [System.Xml.XmlNodeType]::Element -and - $childNode.LocalName -eq $Name -and - $childNode.NamespaceURI -eq $NamespaceUri) { - return [System.Xml.XmlElement]$childNode + if ($imageMounted -or (Test-WinUtilISOMountedImage -Path $mountDir)) { + try { + Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null + } catch { + & $Logger "Warning: could not discard the failed install.wim mount: $_" + } } + Remove-Item -Path $mountDir -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue } - - $childElement = $Parent.OwnerDocument.CreateElement($Name, $NamespaceUri) - [void]$Parent.AppendChild($childElement) - return $childElement - } - - function ConvertTo-WinUtilISOAnswerFile { - param ( - [Parameter(Mandatory)][string]$XmlContent, - [int]$ImageIndex = 1 - ) - - if ($ImageIndex -lt 1) { $ImageIndex = 1 } - - $unattendNs = "urn:schemas-microsoft-com:unattend" - $wcmNs = "http://schemas.microsoft.com/WMIConfig/2002/State" - - $xmlDoc = [xml]::new() - $xmlDoc.PreserveWhitespace = $true - $xmlDoc.LoadXml($XmlContent) - - if ($xmlDoc.DocumentElement.NamespaceURI -ne $unattendNs) { - throw "Unexpected autounattend.xml namespace: $($xmlDoc.DocumentElement.NamespaceURI)" - } - - if (-not $xmlDoc.DocumentElement.HasAttribute("xmlns:wcm")) { - $xmlDoc.DocumentElement.SetAttribute("wcm", "http://www.w3.org/2000/xmlns/", $wcmNs) - } - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $nsMgr.AddNamespace("u", $unattendNs) - - $windowsPESettings = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]', $nsMgr) - if (-not $windowsPESettings) { - $windowsPESettings = $xmlDoc.CreateElement("settings", $unattendNs) - $windowsPESettings.SetAttribute("pass", "windowsPE") - [void]$xmlDoc.DocumentElement.PrependChild($windowsPESettings) - } - - $setupComponent = $windowsPESettings.SelectSingleNode('u:component[@name="Microsoft-Windows-Setup"]', $nsMgr) - if (-not $setupComponent) { - $setupComponent = $xmlDoc.CreateElement("component", $unattendNs) - $setupComponent.SetAttribute("name", "Microsoft-Windows-Setup") - $setupComponent.SetAttribute("processorArchitecture", "amd64") - $setupComponent.SetAttribute("publicKeyToken", "31bf3856ad364e35") - $setupComponent.SetAttribute("language", "neutral") - $setupComponent.SetAttribute("versionScope", "nonSxS") - [void]$windowsPESettings.AppendChild($setupComponent) - } - - $productKeyNodes = @($setupComponent.SelectNodes("u:UserData/u:ProductKey", $nsMgr)) - foreach ($productKeyNode in $productKeyNodes) { - $keyNode = $productKeyNode.SelectSingleNode("u:Key", $nsMgr) - $keyValue = if ($keyNode) { $keyNode.InnerText.Trim() } else { "" } - - if ([string]::IsNullOrWhiteSpace($keyValue) -or $keyValue -eq "00000-00000-00000-00000-00000") { - [void]$productKeyNode.ParentNode.RemoveChild($productKeyNode) - } - } - - $imageInstall = Get-WinUtilISOScriptChildElement -Parent $setupComponent -Name "ImageInstall" -NamespaceUri $unattendNs - $osImage = Get-WinUtilISOScriptChildElement -Parent $imageInstall -Name "OSImage" -NamespaceUri $unattendNs - $installFrom = Get-WinUtilISOScriptChildElement -Parent $osImage -Name "InstallFrom" -NamespaceUri $unattendNs - - $existingMetadataNodes = @($installFrom.SelectNodes("u:MetaData", $nsMgr)) - foreach ($metadataNode in $existingMetadataNodes) { - [void]$installFrom.RemoveChild($metadataNode) - } - - $metadata = $xmlDoc.CreateElement("MetaData", $unattendNs) - $actionAttribute = $xmlDoc.CreateAttribute("wcm", "action", $wcmNs) - $actionAttribute.Value = "add" - [void]$metadata.Attributes.Append($actionAttribute) - - $keyElement = $xmlDoc.CreateElement("Key", $unattendNs) - $keyElement.InnerText = "/IMAGE/INDEX" - [void]$metadata.AppendChild($keyElement) - - $valueElement = $xmlDoc.CreateElement("Value", $unattendNs) - $valueElement.InnerText = [string]$ImageIndex - [void]$metadata.AppendChild($valueElement) - - [void]$installFrom.AppendChild($metadata) - - return $xmlDoc.OuterXml } function Write-WinUtilISOEditionConfig { @@ -224,10 +232,6 @@ function Invoke-WinUtilISOScript { [scriptblock]$Logger ) - if (-not (Test-Path $ContentRoot)) { - return - } - $sourcesDir = Join-Path $ContentRoot "sources" New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null @@ -256,253 +260,281 @@ Retail & $Logger "Written sources\ei.cfg for EditionID '$EditionId'." } - # -- 1. Remove provisioned AppX packages ---------------------------------- - & $Log "Removing provisioned AppX packages..." + function Add-WinUtilISOSetupCustomizations { + param ( + [Parameter(Mandatory)][string]$XmlContent, + [Parameter(Mandatory)][int]$InstallImageIndex, + [scriptblock]$Logger + ) - $packages = & dism /English "/image:$ScratchDir" /Get-ProvisionedAppxPackages | - ForEach-Object { if ($_ -match 'PackageName : (.*)') { $matches[1] } } + $appxPackages = @( + 'Clipchamp.Clipchamp', 'Microsoft.BingNews', 'Microsoft.BingSearch', + 'Microsoft.BingWeather', 'Microsoft.GetHelp', 'Microsoft.MicrosoftOfficeHub', + 'Microsoft.MicrosoftSolitaireCollection', 'Microsoft.MicrosoftStickyNotes', + 'Microsoft.OutlookForWindows', 'Microsoft.Paint', 'Microsoft.PowerAutomateDesktop', + 'Microsoft.StartExperiencesApp', 'Microsoft.Todos', 'Microsoft.Windows.DevHome', + 'Microsoft.WindowsFeedbackHub', 'Microsoft.WindowsSoundRecorder', + 'Microsoft.ZuneMusic', 'MicrosoftCorporationII.QuickAssist', 'MSTeams' + ) - $packagePrefixes = @( - 'Clipchamp.Clipchamp', - 'Microsoft.BingNews', - 'Microsoft.BingSearch', - 'Microsoft.BingWeather', - 'Microsoft.GetHelp', - 'Microsoft.MicrosoftOfficeHub', - 'Microsoft.MicrosoftSolitaireCollection', - 'Microsoft.MicrosoftStickyNotes', - 'Microsoft.OutlookForWindows', - 'Microsoft.Paint', - 'Microsoft.PowerAutomateDesktop', - 'Microsoft.StartExperiencesApp', - 'Microsoft.Todos', - 'Microsoft.Windows.DevHome', - 'Microsoft.WindowsFeedbackHub', - 'Microsoft.WindowsSoundRecorder', - 'Microsoft.ZuneMusic', - 'MicrosoftCorporationII.QuickAssist', - 'MSTeams' + $appxList = ($appxPackages | ForEach-Object { " '$_'" }) -join "`r`n" + $postInstallScript = @" +`$ErrorActionPreference = 'Continue' +`$logPath = 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.log' +Start-Transcript -Path `$logPath -Append -ErrorAction SilentlyContinue + +try { + Write-Host 'WinUtil: Removing provisioned AppX packages...' + `$packages = @( +$appxList ) + foreach (`$package in `$packages) { + Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | + Where-Object { `$_.DisplayName -like "*`$package*" } | + ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName `$_.PackageName -ErrorAction SilentlyContinue | Out-Null } + Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue | + Where-Object { `$_.Name -like "*`$package*" } | + ForEach-Object { Remove-AppxPackage -AllUsers -Package `$_.PackageFullName -ErrorAction SilentlyContinue | Out-Null } + } - $packages | Where-Object { $pkg = $_; $packagePrefixes | Where-Object { $pkg -like "*$_*" } } | - ForEach-Object { & dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$_" } + function Set-WinUtilRegistryValue([string]`$Path, [string]`$Name, [string]`$Type, [string]`$Value) { + reg.exe add `$Path /v `$Name /t `$Type /d `$Value /f 2>&1 | Out-Null + } + + function Set-WinUtilContentDeliveryManagerValues([string]`$HiveRoot) { + `$contentDeliveryManager = "`$HiveRoot\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" + Set-WinUtilRegistryValue `$contentDeliveryManager 'OemPreInstalledAppsEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'PreInstalledAppsEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SilentInstalledAppsEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'ContentDeliveryAllowed' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'FeatureManagementEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'PreInstalledAppsEverEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SoftLandingEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContentEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-310093Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338388Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338389Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338393Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-353694Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-353696Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue `$contentDeliveryManager 'SystemPaneSuggestionsEnabled' 'REG_DWORD' '0' + reg.exe delete "`$contentDeliveryManager\Subscriptions" /f 2>&1 | Out-Null + reg.exe delete "`$contentDeliveryManager\SuggestedApps" /f 2>&1 | Out-Null + } + + Write-Host 'WinUtil: Applying registry tweaks...' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' 'ShippedWithReserves' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Control\BitLocker' 'PreventDeviceEncryption' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Chat' 'ChatIcon' 'REG_DWORD' '3' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive' 'DisableFileSyncNGSC' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection' 'AllowTelemetry' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\dmwappushservice' 'Start' 'REG_DWORD' '4' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Edge' 'HubsSidebarEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer' 'DisableSearchBoxSuggestions' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Teams' 'DisableInstallation' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Mail' 'PreventRun' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableWindowsConsumerFeatures' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableConsumerAccountStateContent' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableCloudOptimizedContent' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start' 'ConfigureStartPins' 'REG_SZ' '{"pinnedList": [{}]}' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' 'BypassNRO' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassCPUCheck' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassRAMCheck' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassSecureBootCheck' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassStorageCheck' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassTPMCheck' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\MoSetup' 'AllowUpgradesWithUnsupportedTPMOrCPU' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\PushToInstall' 'DisablePushToInstall' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\MRT' 'DontOfferThroughWUAU' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' 'workCompleted' 'REG_DWORD' '1' + reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' /f 2>&1 | Out-Null + reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' /f 2>&1 | Out-Null + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'NoAutoUpdate' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'AUOptions' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'UseWUServer' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'DisableWindowsUpdateAccess' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUServer' 'REG_SZ' 'http://localhost:8080' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUStatusServer' 'REG_SZ' 'http://localhost:8080' + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' 'workCompleted' 'REG_DWORD' '1' + reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' /f 2>&1 | Out-Null + Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' 'DODownloadMode' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\BITS' 'Start' 'REG_DWORD' '4' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\wuauserv' 'Start' 'REG_DWORD' '4' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\UsoSvc' 'Start' 'REG_DWORD' '4' + Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' 'Start' 'REG_DWORD' '4' + + `$defaultHive = 'HKU\WinUtilDefault' + reg.exe load `$defaultHive 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null + if (`$LASTEXITCODE -eq 0) { + Set-WinUtilRegistryValue "`$defaultHive\Control Panel\UnsupportedHardwareNotificationCache" 'SV1' 'REG_DWORD' '0' + Set-WinUtilRegistryValue "`$defaultHive\Control Panel\UnsupportedHardwareNotificationCache" 'SV2' 'REG_DWORD' '0' + Set-WinUtilContentDeliveryManagerValues `$defaultHive + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo" 'Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Windows\CurrentVersion\Privacy" 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy" 'HasAccepted' 'REG_DWORD' '0' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Input\TIPC" 'Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization" 'RestrictImplicitInkCollection' 'REG_DWORD' '1' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization" 'RestrictImplicitTextCollection' 'REG_DWORD' '1' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization\TrainedDataStore" 'HarvestContacts' 'REG_DWORD' '0' + Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Personalization\Settings" 'AcceptedPrivacyPolicy' 'REG_DWORD' '0' + reg.exe unload `$defaultHive 2>&1 | Out-Null + } + + Set-WinUtilContentDeliveryManagerValues 'HKCU' + Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV1' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV2' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' 'TaskbarMn' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' 'Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Windows\CurrentVersion\Privacy' 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' 'HasAccepted' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Input\TIPC' 'Enabled' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization' 'RestrictImplicitInkCollection' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization' 'RestrictImplicitTextCollection' 'REG_DWORD' '1' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization\TrainedDataStore' 'HarvestContacts' 'REG_DWORD' '0' + Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Personalization\Settings' 'AcceptedPrivacyPolicy' 'REG_DWORD' '0' + + Write-Host 'WinUtil: Removing scheduled task definitions...' + `$taskPaths = @( + 'C:\Windows\System32\Tasks\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser', + 'C:\Windows\System32\Tasks\Microsoft\Windows\Customer Experience Improvement Program', + 'C:\Windows\System32\Tasks\Microsoft\Windows\Application Experience\ProgramDataUpdater', + 'C:\Windows\System32\Tasks\Microsoft\Windows\Chkdsk\Proxy', + 'C:\Windows\System32\Tasks\Microsoft\Windows\Windows Error Reporting\QueueReporting', + 'C:\Windows\System32\Tasks\Microsoft\Windows\InstallService', + 'C:\Windows\System32\Tasks\Microsoft\Windows\UpdateOrchestrator', + 'C:\Windows\System32\Tasks\Microsoft\Windows\UpdateAssistant', + 'C:\Windows\System32\Tasks\Microsoft\Windows\WaaSMedic', + 'C:\Windows\System32\Tasks\Microsoft\Windows\WindowsUpdate', + 'C:\Windows\System32\Tasks\Microsoft\WindowsUpdate' + ) + foreach (`$taskPath in `$taskPaths) { Remove-Item -LiteralPath `$taskPath -Recurse -Force -ErrorAction SilentlyContinue } + + Start-Process -FilePath 'C:\Windows\System32\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait -ErrorAction SilentlyContinue + Write-Host 'WinUtil: Post-install customization complete.' +} finally { + Stop-Transcript -ErrorAction SilentlyContinue +} +"@ + + $xmlDoc = [xml]::new() + $xmlDoc.PreserveWhitespace = $true + $xmlDoc.LoadXml($XmlContent) + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend') + $nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/') + + $setupComponent = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr) + $extensions = $xmlDoc.SelectSingleNode('//sg:Extensions', $nsMgr) + $firstLogonFile = $xmlDoc.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\FirstLogon.ps1"]', $nsMgr) + if (-not $setupComponent -or -not $extensions -or -not $firstLogonFile) { + throw 'autounattend.xml is missing a required Windows Setup, Extensions, or FirstLogon.ps1 node.' + } + + $imageInstall = $setupComponent.SelectSingleNode('u:ImageInstall', $nsMgr) + if (-not $imageInstall) { + $imageInstall = $xmlDoc.CreateElement('ImageInstall', $setupComponent.NamespaceURI) + [void]$setupComponent.AppendChild($imageInstall) + } + $osImage = $imageInstall.SelectSingleNode('u:OSImage', $nsMgr) + if (-not $osImage) { + $osImage = $xmlDoc.CreateElement('OSImage', $setupComponent.NamespaceURI) + [void]$imageInstall.AppendChild($osImage) + } + $installFrom = $osImage.SelectSingleNode('u:InstallFrom', $nsMgr) + if (-not $installFrom) { + $installFrom = $xmlDoc.CreateElement('InstallFrom', $setupComponent.NamespaceURI) + [void]$osImage.AppendChild($installFrom) + } + foreach ($existingMetadata in @($installFrom.SelectNodes('u:MetaData', $nsMgr))) { + [void]$installFrom.RemoveChild($existingMetadata) + } + $metadata = $xmlDoc.CreateElement('MetaData', $setupComponent.NamespaceURI) + $action = $xmlDoc.CreateAttribute('wcm', 'action', 'http://schemas.microsoft.com/WMIConfig/2002/State') + $action.Value = 'add' + [void]$metadata.Attributes.Append($action) + $key = $xmlDoc.CreateElement('Key', $setupComponent.NamespaceURI) + $key.InnerText = '/IMAGE/INDEX' + [void]$metadata.AppendChild($key) + $value = $xmlDoc.CreateElement('Value', $setupComponent.NamespaceURI) + $value.InnerText = [string]$InstallImageIndex + [void]$metadata.AppendChild($value) + [void]$installFrom.AppendChild($metadata) + + $postInstallFile = $xmlDoc.CreateElement('File', $extensions.NamespaceURI) + $postInstallFile.SetAttribute('path', 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1') + $postInstallFile.InnerText = $postInstallScript + [void]$extensions.AppendChild($postInstallFile) + + $firstLogonFile.InnerText = "& 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1';`r`n`r`n$($firstLogonFile.InnerText.Trim())" + + $null = & $Logger 'Added WinUtil post-install AppX, registry, and scheduled-task customizations to autounattend.xml.' + return $xmlDoc.OuterXml + } + + function Add-WinUtilISOSetupScriptFallback { + param ( + [Parameter(Mandatory)][string]$ContentRoot, + [Parameter(Mandatory)][string]$XmlContent, + [scriptblock]$Logger + ) + + $xmlDoc = [xml]::new() + $xmlDoc.PreserveWhitespace = $true + $xmlDoc.LoadXml($XmlContent) + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend') + $nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/') + + $setupScriptsRoot = Join-Path $ContentRoot 'sources\$OEM$\$$\Setup\Scripts' + $stagedCount = 0 + foreach ($file in $xmlDoc.SelectNodes('//sg:File', $nsMgr)) { + $path = $file.GetAttribute('path') + if (-not $path.StartsWith('C:\Windows\Setup\Scripts\', [System.StringComparison]::OrdinalIgnoreCase)) { + continue + } + + $relativePath = $path.Substring('C:\Windows\Setup\Scripts\'.Length) + $targetPath = Join-Path $setupScriptsRoot $relativePath + New-Item -Path (Split-Path $targetPath -Parent) -ItemType Directory -Force | Out-Null + + $encoding = switch ([System.IO.Path]::GetExtension($targetPath)) { + { $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; break } + { $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true); break } + default { [System.Text.Encoding]::Default } + } + $bytes = $encoding.GetPreamble() + $encoding.GetBytes($file.InnerText.Trim()) + [System.IO.File]::WriteAllBytes($targetPath, $bytes) + $stagedCount++ + } + + $useConfigurationSet = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr) + if ($useConfigurationSet) { + $useConfigurationSet.InnerText = 'true' + [System.IO.File]::WriteAllText((Join-Path $ContentRoot 'autounattend.xml'), $xmlDoc.OuterXml, [System.Text.UTF8Encoding]::new($false)) + } + & $Logger "Staged $stagedCount WinUtil setup script fallback files at '$setupScriptsRoot'." + } + + if (-not (Test-Path $ISOContentsDir)) { + throw "ISO contents directory does not exist: $ISOContentsDir" + } + + if ([string]::IsNullOrWhiteSpace($AutoUnattendXml)) { + throw "autounattend.xml content is required to prepare setup media." + } + + $preparedAutoUnattendXml = Add-WinUtilISOSetupCustomizations -XmlContent $AutoUnattendXml -InstallImageIndex $InstallImageIndex -Logger $Log + $unattendPath = Join-Path $ISOContentsDir "autounattend.xml" + [System.IO.File]::WriteAllText($unattendPath, $preparedAutoUnattendXml, [System.Text.UTF8Encoding]::new($false)) + & $Log "Written autounattend.xml with WinUtil setup customizations to ISO root ($unattendPath)." + Add-WinUtilISOSetupScriptFallback -ContentRoot $ISOContentsDir -XmlContent $preparedAutoUnattendXml -Logger $Log + + Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log - # -- 2. Inject current system drivers (optional) --------------------------- if ($InjectCurrentSystemDrivers) { - & $Log "Exporting all drivers from running system..." - $driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)" - New-Item -Path $driverExportRoot -ItemType Directory -Force - try { - Export-WindowsDriver -Online -Destination $driverExportRoot - - & $Log "Injecting current system drivers into install.wim..." - Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log - & $Log "install.wim driver injection complete." - - if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { - $bootWim = Join-Path $ISOContentsDir "sources\boot.wim" - if (Test-Path $bootWim) { - & $Log "Injecting current system drivers into boot.wim..." - Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log - } else { - & $Log "Warning: boot.wim not found - skipping boot.wim driver injection." - } - } - } catch { - & $Log "Error during driver export/injection: $_" - } finally { - Remove-Item -Path $driverExportRoot -Recurse -Force - } - } else { - & $Log "Driver injection skipped." - } - - # -- 3. Registry tweaks ---------------------------------------------------- - & $Log "Loading offline registry hives..." - reg load HKLM\zCOMPONENTS "$ScratchDir\Windows\System32\config\COMPONENTS" - reg load HKLM\zDEFAULT "$ScratchDir\Windows\System32\config\default" - reg load HKLM\zNTUSER "$ScratchDir\Users\Default\ntuser.dat" - reg load HKLM\zSOFTWARE "$ScratchDir\Windows\System32\config\SOFTWARE" - reg load HKLM\zSYSTEM "$ScratchDir\Windows\System32\config\SYSTEM" - - & $Log "Bypassing system requirements..." - Set-ISOScriptReg -Path 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV1' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV2' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV1' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV2' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassCPUCheck' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassRAMCheck' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassSecureBootCheck' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassStorageCheck' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassTPMCheck' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\MoSetup' -Name 'AllowUpgradesWithUnsupportedTPMOrCPU' -Type 'REG_DWORD' -Value '1' - - & $Log "Disabling sponsored apps..." - Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'OemPreInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'PreInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SilentInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableWindowsConsumerFeatures' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'ContentDeliveryAllowed' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\PolicyManager\current\device\Start' -Name 'ConfigureStartPins' -Type 'REG_SZ' -Value '{"pinnedList": [{}]}' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'FeatureManagementEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'PreInstalledAppsEverEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SoftLandingEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContentEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-310093Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338388Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338389Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338393Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-353694Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-353696Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SystemPaneSuggestionsEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\PushToInstall' -Name 'DisablePushToInstall' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\MRT' -Name 'DontOfferThroughWUAU' -Type 'REG_DWORD' -Value '1' - Remove-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\Subscriptions' - Remove-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\SuggestedApps' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableConsumerAccountStateContent' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableCloudOptimizedContent' -Type 'REG_DWORD' -Value '1' - - & $Log "Enabling local accounts on OOBE..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' -Name 'BypassNRO' -Type 'REG_DWORD' -Value '1' - - if ($AutoUnattendXml) { - $preparedAutoUnattendXml = $AutoUnattendXml - try { - $preparedAutoUnattendXml = ConvertTo-WinUtilISOAnswerFile -XmlContent $AutoUnattendXml -ImageIndex $InstallImageIndex - & $Log "Prepared autounattend.xml to install image index $InstallImageIndex without forcing a product key." - } catch { - & $Log "Warning: could not prepare autounattend.xml image selection: $_" - } - - try { - $xmlDoc = [xml]::new() - $xmlDoc.LoadXml($preparedAutoUnattendXml) - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/") - - $fileNodes = $xmlDoc.SelectNodes("//sg:File", $nsMgr) - if ($fileNodes -and $fileNodes.Count -gt 0) { - foreach ($fileNode in $fileNodes) { - $absPath = $fileNode.GetAttribute("path") - $relPath = $absPath -replace '^[A-Za-z]:[/\\]', '' - $destPath = Join-Path $ScratchDir $relPath - New-Item -Path (Split-Path $destPath -Parent) -ItemType Directory -Force - - $ext = [IO.Path]::GetExtension($destPath).ToLower() - $encoding = switch ($ext) { - { $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8 } - { $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true) } - default { [System.Text.Encoding]::Default } - } - [System.IO.File]::WriteAllBytes($destPath, ($encoding.GetPreamble() + $encoding.GetBytes($fileNode.InnerText.Trim()))) - & $Log "Pre-staged setup script: $relPath" - } - } else { - & $Log "Warning: no nodes found in autounattend.xml - setup scripts not pre-staged." - } - } catch { - & $Log "Warning: could not pre-stage setup scripts from autounattend.xml: $_" - } - - if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { - $isoDest = Join-Path $ISOContentsDir "autounattend.xml" - Set-Content -Path $isoDest -Value $preparedAutoUnattendXml -Encoding UTF8 -Force - & $Log "Written autounattend.xml to ISO root ($isoDest)." - } - } else { - & $Log "Warning: autounattend.xml content is empty - skipping OOBE bypass file." - } - - if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { - Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log - } - - & $Log "Disabling reserved storage..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' -Name 'ShippedWithReserves' -Type 'REG_DWORD' -Value '0' - - & $Log "Disabling BitLocker device encryption..." - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Control\BitLocker' -Name 'PreventDeviceEncryption' -Type 'REG_DWORD' -Value '1' - - & $Log "Disabling Chat icon..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Chat' -Name 'ChatIcon' -Type 'REG_DWORD' -Value '3' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name 'TaskbarMn' -Type 'REG_DWORD' -Value '0' - - & $Log "Disabling OneDrive folder backup..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\OneDrive' -Name 'DisableFileSyncNGSC' -Type 'REG_DWORD' -Value '1' - - & $Log "Disabling telemetry..." - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' -Name 'Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\Privacy' -Name 'TailoredExperiencesWithDiagnosticDataEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' -Name 'HasAccepted' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Input\TIPC' -Name 'Enabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' -Name 'RestrictImplicitInkCollection' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' -Name 'RestrictImplicitTextCollection' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization\TrainedDataStore' -Name 'HarvestContacts' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Personalization\Settings' -Name 'AcceptedPrivacyPolicy' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\DataCollection' -Name 'AllowTelemetry' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\dmwappushservice' -Name 'Start' -Type 'REG_DWORD' -Value '4' - - & $Log "Preventing installation of DevHome and Outlook..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1' - Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' - Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' - - & $Log "Disabling Copilot..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' -Name 'TurnOffWindowsCopilot' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Edge' -Name 'HubsSidebarEnabled' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' -Name 'DisableSearchBoxSuggestions' -Type 'REG_DWORD' -Value '1' - - & $Log "Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'NoAutoUpdate' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'AUOptions' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'UseWUServer' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'DisableWindowsUpdateAccess' -Type 'REG_DWORD' -Value '1' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'WUServer' -Type 'REG_SZ' -Value 'http://localhost:8080' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'WUStatusServer' -Type 'REG_SZ' -Value 'http://localhost:8080' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1' - Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' -Name 'DODownloadMode' -Type 'REG_DWORD' -Value '0' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\BITS' -Name 'Start' -Type 'REG_DWORD' -Value '4' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\wuauserv' -Name 'Start' -Type 'REG_DWORD' -Value '4' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\UsoSvc' -Name 'Start' -Type 'REG_DWORD' -Value '4' - Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\WaaSMedicSvc' -Name 'Start' -Type 'REG_DWORD' -Value '4' - - & $Log "Preventing installation of Teams..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Teams' -Name 'DisableInstallation' -Type 'REG_DWORD' -Value '1' - - & $Log "Preventing installation of new Outlook..." - Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Mail' -Name 'PreventRun' -Type 'REG_DWORD' -Value '1' - - & $Log "Unloading offline registry hives..." - reg unload HKLM\zCOMPONENTS - reg unload HKLM\zDEFAULT - reg unload HKLM\zNTUSER - reg unload HKLM\zSOFTWARE - reg unload HKLM\zSYSTEM - - # -- 4. Delete scheduled task definition files ----------------------------- - & $Log "Deleting scheduled task definition files..." - $tasksPath = "$ScratchDir\Windows\System32\Tasks" - Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" -Force - Remove-Item "$tasksPath\Microsoft\Windows\Customer Experience Improvement Program" -Recurse -Force - Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\ProgramDataUpdater" -Force - Remove-Item "$tasksPath\Microsoft\Windows\Chkdsk\Proxy" -Force - Remove-Item "$tasksPath\Microsoft\Windows\Windows Error Reporting\QueueReporting" -Force - Remove-Item "$tasksPath\Microsoft\Windows\InstallService" -Recurse -Force - Remove-Item "$tasksPath\Microsoft\Windows\UpdateOrchestrator" -Recurse -Force - Remove-Item "$tasksPath\Microsoft\Windows\UpdateAssistant" -Recurse -Force - Remove-Item "$tasksPath\Microsoft\Windows\WaaSMedic" -Recurse -Force - Remove-Item "$tasksPath\Microsoft\Windows\WindowsUpdate" -Recurse -Force - Remove-Item "$tasksPath\Microsoft\WindowsUpdate" -Recurse -Force - & $Log "Scheduled task files deleted." - - # -- 5. Remove ISO support folder ----------------------------------------- - if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) { - & $Log "Removing ISO support\ folder..." - Remove-Item -Path (Join-Path $ISOContentsDir "support") -Recurse -Force - & $Log "ISO support\ folder removed." + Add-WinUtilISOStagedDrivers -ContentRoot $ISOContentsDir -Logger $Log -InstallImagePath $InstallImagePath -InstallImageIndex $InstallImageIndex } } diff --git a/functions/private/Invoke-WinUtilISOUSB.ps1 b/functions/private/Invoke-WinUtilISOUSB.ps1 index 3c9dee06..3d042eb5 100644 --- a/functions/private/Invoke-WinUtilISOUSB.ps1 +++ b/functions/private/Invoke-WinUtilISOUSB.ps1 @@ -8,7 +8,7 @@ function Invoke-WinUtilISORefreshUSBDrives { $combo.Items.Add("No USB drives detected.") $combo.SelectedIndex = 0 $sync["Win11ISOUSBDisks"] = @() - Write-Win11ISOLog "No USB drives detected." + Write-WinUtilISOLog "No USB drives detected." return } @@ -17,7 +17,7 @@ function Invoke-WinUtilISORefreshUSBDrives { $combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)") } $combo.SelectedIndex = 0 - Write-Win11ISOLog "Found $($removable.Count) USB drive(s)." + Write-WinUtilISOLog "Found $($removable.Count) USB drive(s)." $sync["Win11ISOUSBDisks"] = $removable } @@ -30,6 +30,20 @@ function Invoke-WinUtilISOWriteUSB { return } + $installWim = Join-Path $contentsDir "sources\install.wim" + $installEsd = Join-Path $contentsDir "sources\install.esd" + if (Test-Path $installEsd) { + $installEsdFile = Get-Item $installEsd + $esdSizeBytes = $installEsdFile.Length + $esdSizeMB = [math]::Ceiling($esdSizeBytes / 1MB) + if ($esdSizeBytes -ge 4GB) { + [System.Windows.MessageBox]::Show( + "This ISO uses an install.esd file that is $esdSizeMB MB. WinUtil's FAT32 USB format cannot store files larger than 4 GB.`n`nExport an ISO instead or use media with install.wim.", + "USB Creation Not Supported", "OK", "Warning") + return + } + } + $combo = $sync["WPFWin11ISOUSBDriveComboBox"] $selectedIndex = $combo.SelectedIndex $selectedItemText = [string]$combo.SelectedItem @@ -56,13 +70,13 @@ function Invoke-WinUtilISOWriteUSB { "Confirm USB Erase", "YesNo", "Warning") if ($confirm -ne "Yes") { - Write-Win11ISOLog "USB write cancelled by user." + Write-WinUtilISOLog "USB write cancelled by user." return } $sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false $sync["Win11ISOProcessRunning"] = $true - Write-Win11ISOLog "Starting USB write to Disk $diskNum..." + Write-WinUtilISOLog "Starting USB write to Disk $diskNum..." $runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $runspace.ApartmentState = "STA" diff --git a/pester/win11creator.Tests.ps1 b/pester/win11creator.Tests.ps1 index 326b66ed..bd18bc9b 100644 --- a/pester/win11creator.Tests.ps1 +++ b/pester/win11creator.Tests.ps1 @@ -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 = '' + $invalidAfter = $valid.Clone() + $invalidAfter.ProductType = '' + + { 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 } } diff --git a/tools/autounattend.xml b/tools/autounattend.xml index f92861f4..d237be10 100644 --- a/tools/autounattend.xml +++ b/tools/autounattend.xml @@ -21,6 +21,14 @@ 3 reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f + + 4 + reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassCPUCheck /t REG_DWORD /d 1 /f + + + 5 + reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassStorageCheck /t REG_DWORD /d 1 /f + @@ -48,6 +56,18 @@ 5 reg.exe unload "HKU\DefaultUser" + + 6 + reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f + + + 7 + reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v PreventDeviceEncryption /t REG_DWORD /d 1 /f + + + 8 + reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v ShippedWithReserves /t REG_DWORD /d 0 /f + diff --git a/xaml/inputXML.xaml b/xaml/inputXML.xaml index 90d589e0..273ae5a2 100644 --- a/xaml/inputXML.xaml +++ b/xaml/inputXML.xaml @@ -1682,7 +1682,7 @@ Foreground="{DynamicResource MainForegroundColor}" IsChecked="False" Margin="0,8,0,0" - ToolTip="Exports all drivers from this machine and injects them into install.wim and boot.wim. Recommended for systems with unsupported NVMe or network controllers."/> + ToolTip="Stages boot-storage drivers for Setup and adds all exported drivers to the selected install.wim edition in one DISM pass."/>