mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 17:41:14 +10:00
Refactor Win11 Creator ISO workflow (#4862)
* refactor: simplify Win11 Creator ISO workflow Keep no-driver ISO generation on the fast copy-only path. Inject exported drivers with one selected-index WIM mount, one Add-Driver pass, and one commit. Limit WinPE staging to boot-storage drivers and improve workflow validation coverage. * refactor: stage Win11 setup script fallback Copy the prepared setup script payloads into sources/$//Setup/Scripts so setup media does not depend solely on answer-file extension extraction. Keep the existing autounattend execution path unchanged and cover the fallback files in the Win11 Creator tests. * refactor: address codex feedback * refactor: run ISO verification in runspace Rename the ISO logger to Write-WinUtilISOLog so the shared runspace pool imports it automatically. Run Mount & Verify off the WPF thread with dispatcher-safe UI updates, and add Pester coverage for the runspace and logger contract. * refactor: address ISO review feedback Keep the selected ISO stable during background verification. Apply ContentDeliveryManager settings to both the first account and the default profile. Block FAT32 USB creation when install.esd exceeds the supported file size. * refactor: address ISO setup edge cases Set BypassNRO before OOBE so local account setup is available during Windows Setup. Detect registered DISM mounts during driver-injection cleanup so partial mount failures are discarded. * refactor: address ISO review edge cases Use the actual FAT32 file limit for install.esd USB checks. Disable ISO modification while mount verification is running. Apply unsupported-hardware notice suppression to the first user profile. * refactor: stage ISO setup safeguards earlier Set device encryption and reserved-storage registry guards before OOBE. Enable the OEM configuration-set fallback when setup scripts are staged there. * Update functions/private/Invoke-WinUtilISOUSB.ps1 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Chris Titus <contact@christitus.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Chris Titus
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent
fae58d73c4
commit
5ebb24b107
@@ -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.
|
- 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.
|
- 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, 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 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.
|
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function Write-Win11ISOLog {
|
function Write-WinUtilISOLog {
|
||||||
param([string]$Message)
|
param([string]$Message)
|
||||||
$ts = (Get-Date).ToString("HH:mm:ss")
|
$ts = (Get-Date).ToString("HH:mm:ss")
|
||||||
$logLine = "[$ts] $Message"
|
$logLine = "[$ts] $Message"
|
||||||
@@ -35,7 +35,7 @@ function Invoke-WinUtilISOBrowse {
|
|||||||
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
|
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
|
||||||
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
|
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
|
||||||
|
|
||||||
Write-Win11ISOLog "ISO selected: $isoPath ($fileSizeGB GB)"
|
Write-WinUtilISOLog "ISO selected: $isoPath ($fileSizeGB GB)"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-WinUtilISOMountAndVerify {
|
function Invoke-WinUtilISOMountAndVerify {
|
||||||
@@ -46,84 +46,102 @@ function Invoke-WinUtilISOMountAndVerify {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Win11ISOLog "Mounting ISO: $isoPath"
|
Write-WinUtilISOLog "Mounting ISO: $isoPath"
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Mounting ISO..." -Percent 10
|
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 {
|
Invoke-WPFRunspace -ParameterList @(,('isoPath', $isoPath)) -ScriptBlock {
|
||||||
Mount-DiskImage -ImagePath $isoPath
|
param($isoPath)
|
||||||
|
|
||||||
do {
|
try {
|
||||||
Start-Sleep -Milliseconds 500
|
Mount-DiskImage -ImagePath $isoPath
|
||||||
} until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter)
|
|
||||||
|
|
||||||
$driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":"
|
do {
|
||||||
Write-Win11ISOLog "Mounted at drive $driveLetter"
|
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"
|
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Verifying ISO contents..." -Percent 30
|
||||||
$esdPath = Join-Path $driveLetter "sources\install.esd"
|
|
||||||
|
|
||||||
if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) {
|
$wimPath = Join-Path $driveLetter "sources\install.wim"
|
||||||
Dismount-DiskImage -ImagePath $isoPath
|
$esdPath = Join-Path $driveLetter "sources\install.esd"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
$activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath }
|
if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) {
|
||||||
|
Dismount-DiskImage -ImagePath $isoPath
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55
|
Write-WinUtilISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO."
|
||||||
$imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName
|
Invoke-WPFUIThread {
|
||||||
|
[System.Windows.MessageBox]::Show(
|
||||||
if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) {
|
"This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.",
|
||||||
Dismount-DiskImage -ImagePath $isoPath
|
"Invalid ISO", "OK", "Error")
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 }
|
return
|
||||||
}
|
}
|
||||||
})
|
|
||||||
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
|
|
||||||
|
|
||||||
$sync["Win11ISODriveLetter"] = $driveLetter
|
$activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath }
|
||||||
$sync["Win11ISOWimPath"] = $activeWim
|
|
||||||
$sync["Win11ISOImagePath"] = $isoPath
|
|
||||||
$sync["WPFWin11ISOModifySection"].Visibility = "Visible"
|
|
||||||
|
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "ISO verified" -Percent 100
|
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55
|
||||||
Write-Win11ISOLog "ISO verified OK. Editions found: $($imageInfo.Count)"
|
$imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName
|
||||||
} catch {
|
|
||||||
Write-Win11ISOLog "ERROR during mount/verify: $_"
|
if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) {
|
||||||
[System.Windows.MessageBox]::Show(
|
Dismount-DiskImage -ImagePath $isoPath
|
||||||
"An error occurred while mounting or verifying the ISO:`n`n$_",
|
Write-WinUtilISOLog "ERROR: No 'Windows 11' edition found in the image."
|
||||||
"Error", "OK", "Error")
|
Invoke-WPFUIThread {
|
||||||
} finally {
|
[System.Windows.MessageBox]::Show(
|
||||||
Start-Sleep -Milliseconds 800
|
"No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.",
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $false
|
"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
|
$selectedWimIndex = $sync["Win11ISOImageInfo"][0].ImageIndex
|
||||||
}
|
}
|
||||||
$selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" }
|
$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["WPFWin11ISOModifyButton"].IsEnabled = $false
|
||||||
$sync["Win11ISOModifying"] = $true
|
$sync["Win11ISOModifying"] = $true
|
||||||
@@ -170,7 +188,6 @@ function Invoke-WinUtilISOModify {
|
|||||||
$runspace.ThreadOptions = "ReuseThread"
|
$runspace.ThreadOptions = "ReuseThread"
|
||||||
$runspace.Open()
|
$runspace.Open()
|
||||||
$injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true
|
$injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true
|
||||||
|
|
||||||
$runspace.SessionStateProxy.SetVariable("sync", $sync)
|
$runspace.SessionStateProxy.SetVariable("sync", $sync)
|
||||||
$runspace.SessionStateProxy.SetVariable("isoPath", $isoPath)
|
$runspace.SessionStateProxy.SetVariable("isoPath", $isoPath)
|
||||||
$runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter)
|
$runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter)
|
||||||
@@ -182,7 +199,7 @@ function Invoke-WinUtilISOModify {
|
|||||||
$runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers)
|
$runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers)
|
||||||
|
|
||||||
$isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ${function:Invoke-WinUtilISOScript}.ToString() + "`n}"
|
$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("isoScriptFuncDef", $isoScriptFuncDef)
|
||||||
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
|
$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 '<undefined>' } | 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 '<undefined>') {
|
|
||||||
$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 '<undefined>' } | 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 {
|
try {
|
||||||
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
|
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
|
||||||
@@ -359,51 +261,30 @@ function Invoke-WinUtilISOModify {
|
|||||||
|
|
||||||
Log "Creating working directory: $workDir"
|
Log "Creating working directory: $workDir"
|
||||||
$isoContents = Join-Path $workDir "iso_contents"
|
$isoContents = Join-Path $workDir "iso_contents"
|
||||||
$mountDir = Join-Path $workDir "wim_mount"
|
New-Item -ItemType Directory -Path $isoContents -Force
|
||||||
New-Item -ItemType Directory -Path $isoContents, $mountDir -Force
|
|
||||||
SetProgress "Copying ISO contents..." 10
|
SetProgress "Copying ISO contents..." 10
|
||||||
|
|
||||||
Log "Copying ISO contents from $driveLetter to $isoContents..."
|
Log "Copying ISO contents from $driveLetter to $isoContents..."
|
||||||
& robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS
|
& robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS
|
||||||
Log "ISO contents copied."
|
Log "ISO contents copied."
|
||||||
SetProgress "Mounting install.wim..." 25
|
SetProgress "Preparing setup media..." 25
|
||||||
|
|
||||||
$sourceImageFileName = Split-Path $wimPath -Leaf
|
$sourceImageFileName = Split-Path $wimPath -Leaf
|
||||||
$localWim = Join-Path $isoContents "sources\$sourceImageFileName"
|
$localWim = Join-Path $isoContents "sources\$sourceImageFileName"
|
||||||
if (-not (Test-Path $localWim)) {
|
if (-not (Test-Path $localWim)) {
|
||||||
throw "Copied ISO image file not found: sources\$sourceImageFileName"
|
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..."
|
Log "Writing autounattend.xml and edition selection..."
|
||||||
Mount-WindowsImage -ImagePath $localWim -Index $selectedWimIndex -Path $mountDir
|
Invoke-WinUtilISOScript -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallImagePath $localWim -InstallImageIndex $selectedWimIndex -InstallEditionId $selectedEditionId -Log { param($m) Log $m }
|
||||||
SetProgress "Modifying install.wim..." 45
|
|
||||||
$selectedEditionId = Get-WinUtilMountedImageEditionId -MountDir $mountDir -EditionName $selectedEditionName -Logger ${function:Log}
|
|
||||||
|
|
||||||
Log "Applying WinUtil modifications to install.wim..."
|
SetProgress "Preserving install image..." 70
|
||||||
Invoke-WinUtilISOScript -ScratchDir $mountDir -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallEditionId $selectedEditionId -InstallImageIndex 1 -Log { param($m) Log $m }
|
if ($injectDrivers) {
|
||||||
|
Log "Added current-system drivers to $sourceImageFileName index $selectedWimIndex with one mount and commit."
|
||||||
SetProgress "Cleaning up component store (WinSxS)..." 56
|
} else {
|
||||||
Log "Running DISM component store cleanup (/ResetBase)..."
|
Log "Preserved the original $sourceImageFileName without mounting, exporting, or modifying it."
|
||||||
& 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 "Dismounting source ISO..." 80
|
SetProgress "Dismounting source ISO..." 80
|
||||||
Log "Dismounting original ISO..."
|
Log "Dismounting original ISO..."
|
||||||
@@ -421,16 +302,6 @@ function Invoke-WinUtilISOModify {
|
|||||||
} catch {
|
} catch {
|
||||||
Log "ERROR during modification: $_"
|
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 {
|
try {
|
||||||
$mountedISO = Get-DiskImage -ImagePath $isoPath
|
$mountedISO = Get-DiskImage -ImagePath $isoPath
|
||||||
if ($mountedISO -and $mountedISO.Attached) {
|
if ($mountedISO -and $mountedISO.Attached) {
|
||||||
@@ -498,9 +369,9 @@ function Invoke-WinUtilISOCheckExistingWork {
|
|||||||
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
|
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
|
||||||
|
|
||||||
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
|
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
|
||||||
Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)"
|
Write-WinUtilISOLog "Existing working directory found: $($existingWorkDir.FullName)"
|
||||||
Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
|
Write-WinUtilISOLog "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 "Click 'Clean & Reset' if you want to start over with a new ISO."
|
||||||
|
|
||||||
[System.Windows.MessageBox]::Show(
|
[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.",
|
"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) {
|
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 {
|
try {
|
||||||
# First ensure winget is installed and operational
|
# First ensure winget is installed and operational
|
||||||
Install-WinUtilWinget
|
Install-WinUtilWinget
|
||||||
|
|
||||||
$winget = Get-Command winget
|
$winget = Get-Command winget
|
||||||
$result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements
|
$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" |
|
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" |
|
||||||
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
|
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
|
||||||
Select-Object -First 1 -ExpandProperty FullName
|
Select-Object -First 1 -ExpandProperty FullName
|
||||||
} catch {
|
} catch {
|
||||||
Write-Win11ISOLog "winget not available or install failed: $_"
|
Write-WinUtilISOLog "winget not available or install failed: $_"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $oscdimg) {
|
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(
|
[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.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")
|
"oscdimg Not Found", "OK", "Warning")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Write-Win11ISOLog "oscdimg.exe installed successfully."
|
Write-WinUtilISOLog "oscdimg.exe installed successfully."
|
||||||
}
|
}
|
||||||
|
|
||||||
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false
|
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false
|
||||||
@@ -727,7 +598,7 @@ function Invoke-WinUtilISOExport {
|
|||||||
$runspace.SessionStateProxy.SetVariable("outputISO", $outputISO)
|
$runspace.SessionStateProxy.SetVariable("outputISO", $outputISO)
|
||||||
$runspace.SessionStateProxy.SetVariable("oscdimg", $oscdimg)
|
$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)
|
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
|
||||||
|
|
||||||
$script = [Management.Automation.PowerShell]::Create()
|
$script = [Management.Automation.PowerShell]::Create()
|
||||||
@@ -745,13 +616,13 @@ function Invoke-WinUtilISOExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Write-Win11ISOLog "Exporting to ISO: $outputISO"
|
Write-WinUtilISOLog "Exporting to ISO: $outputISO"
|
||||||
SetProgress "Building ISO..." 10
|
SetProgress "Building ISO..." 10
|
||||||
|
|
||||||
$bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`""
|
$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`"")
|
$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 = [System.Diagnostics.ProcessStartInfo]::new()
|
||||||
$psi.FileName = $oscdimg
|
$psi.FileName = $oscdimg
|
||||||
@@ -768,7 +639,7 @@ function Invoke-WinUtilISOExport {
|
|||||||
# Stream stdout line-by-line as oscdimg runs
|
# Stream stdout line-by-line as oscdimg runs
|
||||||
while (-not $proc.StandardOutput.EndOfStream) {
|
while (-not $proc.StandardOutput.EndOfStream) {
|
||||||
$line = $proc.StandardOutput.ReadLine()
|
$line = $proc.StandardOutput.ReadLine()
|
||||||
if ($line.Trim()) { Write-Win11ISOLog $line }
|
if ($line.Trim()) { Write-WinUtilISOLog $line }
|
||||||
}
|
}
|
||||||
|
|
||||||
$proc.WaitForExit()
|
$proc.WaitForExit()
|
||||||
@@ -776,17 +647,17 @@ function Invoke-WinUtilISOExport {
|
|||||||
# Flush any stderr after process exits
|
# Flush any stderr after process exits
|
||||||
$stderr = $proc.StandardError.ReadToEnd()
|
$stderr = $proc.StandardError.ReadToEnd()
|
||||||
foreach ($line in ($stderr -split "`r?`n")) {
|
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) {
|
if ($proc.ExitCode -eq 0) {
|
||||||
SetProgress "ISO exported" 100
|
SetProgress "ISO exported" 100
|
||||||
Write-Win11ISOLog "ISO exported successfully: $outputISO"
|
Write-WinUtilISOLog "ISO exported successfully: $outputISO"
|
||||||
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
[System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info")
|
[System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info")
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Write-Win11ISOLog "oscdimg exited with code $($proc.ExitCode)."
|
Write-WinUtilISOLog "oscdimg exited with code $($proc.ExitCode)."
|
||||||
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
[System.Windows.MessageBox]::Show(
|
[System.Windows.MessageBox]::Show(
|
||||||
"oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.",
|
"oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.",
|
||||||
@@ -794,7 +665,7 @@ function Invoke-WinUtilISOExport {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Write-Win11ISOLog "ERROR during ISO export: $_"
|
Write-WinUtilISOLog "ERROR during ISO export: $_"
|
||||||
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
|
||||||
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
|
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,220 +1,228 @@
|
|||||||
function Invoke-WinUtilISOScript {
|
function Invoke-WinUtilISOScript {
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Applies WinUtil modifications to a mounted Windows 11 install.wim image.
|
Prepares copied Windows setup media without modifying its install image.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Removes AppX bloatware and OneDrive, optionally injects all drivers exported from
|
Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup
|
||||||
the running system into install.wim and boot.wim (controlled by the
|
in the answer file for first logon, writes sources\ei.cfg for the selected
|
||||||
-InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware
|
edition, and optionally adds current-system drivers to one install.wim index.
|
||||||
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 <Extensions><File> 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.
|
|
||||||
|
|
||||||
.PARAMETER ISOContentsDir
|
.PARAMETER ISOContentsDir
|
||||||
Optional. Root directory of the extracted ISO contents. When supplied,
|
Root directory of the copied ISO contents.
|
||||||
autounattend.xml is written here and the support\ folder is removed.
|
|
||||||
|
|
||||||
.PARAMETER AutoUnattendXml
|
.PARAMETER AutoUnattendXml
|
||||||
Optional. Full XML content for autounattend.xml. If empty, the OOBE bypass
|
Full XML content for autounattend.xml.
|
||||||
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.
|
|
||||||
|
|
||||||
.PARAMETER InstallEditionId
|
.PARAMETER InstallEditionId
|
||||||
Optional. Windows edition ID for the selected image, for example Professional
|
Windows setup EditionID for sources\ei.cfg, for example Professional or Core.
|
||||||
or Core. Used to write sources\ei.cfg so setup does not fall back to an
|
|
||||||
embedded firmware product key for a different edition.
|
.PARAMETER InstallImagePath
|
||||||
|
Copied install.wim to service when current-system driver injection is enabled.
|
||||||
|
|
||||||
.PARAMETER InstallImageIndex
|
.PARAMETER InstallImageIndex
|
||||||
Optional. Image index that setup should install from the final install.wim.
|
Selected edition index in install.wim.
|
||||||
Win11 Creator exports the selected edition to a single-image WIM, so this
|
|
||||||
defaults to 1.
|
|
||||||
|
|
||||||
.PARAMETER Log
|
.PARAMETER Log
|
||||||
Optional ScriptBlock for progress/status logging. Receives a single [string] argument.
|
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 (
|
param (
|
||||||
[Parameter(Mandatory)][string]$ScratchDir,
|
[Parameter(Mandatory)][string]$ISOContentsDir,
|
||||||
[string]$ISOContentsDir = "",
|
|
||||||
[string]$AutoUnattendXml = "",
|
[string]$AutoUnattendXml = "",
|
||||||
[bool]$InjectCurrentSystemDrivers = $false,
|
[bool]$InjectCurrentSystemDrivers = $false,
|
||||||
[string]$InstallEditionId = "",
|
[string]$InstallEditionId = "",
|
||||||
|
[string]$InstallImagePath = "",
|
||||||
[int]$InstallImageIndex = 1,
|
[int]$InstallImageIndex = 1,
|
||||||
[scriptblock]$Log = { param($m) Write-Output $m }
|
[scriptblock]$Log = { param($m) Write-Output $m }
|
||||||
)
|
)
|
||||||
function Set-ISOScriptReg {
|
|
||||||
param ([string]$Path, [string]$Name, [string]$Type, [string]$Value)
|
function Add-WinUtilISOStagedDrivers {
|
||||||
try {
|
param (
|
||||||
& reg add $Path /v $Name /t $Type /d $Value /f
|
[Parameter(Mandatory)][string]$ContentRoot,
|
||||||
& $Log "Set registry value: $Path\$Name"
|
[Parameter(Mandatory)][string]$InstallImagePath,
|
||||||
} catch {
|
[Parameter(Mandatory)][int]$InstallImageIndex,
|
||||||
& $Log "Error setting registry value: $_"
|
[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 {
|
function Test-WinUtilISOStorageDriver {
|
||||||
param ([string]$path)
|
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
|
||||||
try {
|
|
||||||
& reg delete $path /f
|
if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') {
|
||||||
& $Log "Removed registry key: $path"
|
return $true
|
||||||
} catch {
|
}
|
||||||
& $Log "Error removing registry key: $_"
|
|
||||||
|
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 {
|
function Invoke-WinUtilISODism {
|
||||||
param ([string]$MountPath, [string]$DriverDir, [string]$Label = "image", [scriptblock]$Logger)
|
param (
|
||||||
& dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse |
|
[Parameter(Mandatory)][string[]]$Arguments,
|
||||||
ForEach-Object { & $Logger " dism[$Label]: $_" }
|
[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 '<undefined>' -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 '<undefined>' -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 {
|
try {
|
||||||
& $Logger "Mounting boot.wim (index 2) for driver injection..."
|
& $Logger "Exporting current system drivers before modifying install.wim..."
|
||||||
Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir
|
$dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
||||||
Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger
|
$dismProcess = Start-Process -FilePath "dism.exe" -ArgumentList "/online /export-driver /destination:`"$driverExportRoot`" /LogPath:`"$dismLog`"" -Wait -NoNewWindow -PassThru
|
||||||
& $Logger "Saving boot.wim..."
|
if ($dismProcess.ExitCode -ne 0) {
|
||||||
Dismount-WindowsImage -Path $mountDir -Save
|
throw "dism.exe driver export failed with exit code $($dismProcess.ExitCode)."
|
||||||
& $Logger "boot.wim driver injection complete."
|
}
|
||||||
} catch {
|
|
||||||
& $Logger "Warning: boot.wim driver injection failed: $_"
|
$driverInfs = @(Get-ChildItem -Path $driverExportRoot -Filter '*.inf' -Recurse -File)
|
||||||
try { Dismount-WindowsImage -Path $mountDir -Discard } catch { & $Logger "Warning: could not discard boot.wim mount: $_" }
|
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 {
|
} finally {
|
||||||
Remove-Item -Path $mountDir -Recurse -Force
|
if ($imageMounted -or (Test-WinUtilISOMountedImage -Path $mountDir)) {
|
||||||
}
|
try {
|
||||||
}
|
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
|
||||||
|
} catch {
|
||||||
function Get-WinUtilISOScriptChildElement {
|
& $Logger "Warning: could not discard the failed install.wim mount: $_"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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 {
|
function Write-WinUtilISOEditionConfig {
|
||||||
@@ -224,10 +232,6 @@ function Invoke-WinUtilISOScript {
|
|||||||
[scriptblock]$Logger
|
[scriptblock]$Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
if (-not (Test-Path $ContentRoot)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourcesDir = Join-Path $ContentRoot "sources"
|
$sourcesDir = Join-Path $ContentRoot "sources"
|
||||||
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
|
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
@@ -256,253 +260,281 @@ Retail
|
|||||||
& $Logger "Written sources\ei.cfg for EditionID '$EditionId'."
|
& $Logger "Written sources\ei.cfg for EditionID '$EditionId'."
|
||||||
}
|
}
|
||||||
|
|
||||||
# -- 1. Remove provisioned AppX packages ----------------------------------
|
function Add-WinUtilISOSetupCustomizations {
|
||||||
& $Log "Removing provisioned AppX packages..."
|
param (
|
||||||
|
[Parameter(Mandatory)][string]$XmlContent,
|
||||||
|
[Parameter(Mandatory)][int]$InstallImageIndex,
|
||||||
|
[scriptblock]$Logger
|
||||||
|
)
|
||||||
|
|
||||||
$packages = & dism /English "/image:$ScratchDir" /Get-ProvisionedAppxPackages |
|
$appxPackages = @(
|
||||||
ForEach-Object { if ($_ -match 'PackageName : (.*)') { $matches[1] } }
|
'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 = @(
|
$appxList = ($appxPackages | ForEach-Object { " '$_'" }) -join "`r`n"
|
||||||
'Clipchamp.Clipchamp',
|
$postInstallScript = @"
|
||||||
'Microsoft.BingNews',
|
`$ErrorActionPreference = 'Continue'
|
||||||
'Microsoft.BingSearch',
|
`$logPath = 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.log'
|
||||||
'Microsoft.BingWeather',
|
Start-Transcript -Path `$logPath -Append -ErrorAction SilentlyContinue
|
||||||
'Microsoft.GetHelp',
|
|
||||||
'Microsoft.MicrosoftOfficeHub',
|
try {
|
||||||
'Microsoft.MicrosoftSolitaireCollection',
|
Write-Host 'WinUtil: Removing provisioned AppX packages...'
|
||||||
'Microsoft.MicrosoftStickyNotes',
|
`$packages = @(
|
||||||
'Microsoft.OutlookForWindows',
|
$appxList
|
||||||
'Microsoft.Paint',
|
|
||||||
'Microsoft.PowerAutomateDesktop',
|
|
||||||
'Microsoft.StartExperiencesApp',
|
|
||||||
'Microsoft.Todos',
|
|
||||||
'Microsoft.Windows.DevHome',
|
|
||||||
'Microsoft.WindowsFeedbackHub',
|
|
||||||
'Microsoft.WindowsSoundRecorder',
|
|
||||||
'Microsoft.ZuneMusic',
|
|
||||||
'MicrosoftCorporationII.QuickAssist',
|
|
||||||
'MSTeams'
|
|
||||||
)
|
)
|
||||||
|
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 "*$_*" } } |
|
function Set-WinUtilRegistryValue([string]`$Path, [string]`$Name, [string]`$Type, [string]`$Value) {
|
||||||
ForEach-Object { & dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$_" }
|
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) {
|
if ($InjectCurrentSystemDrivers) {
|
||||||
& $Log "Exporting all drivers from running system..."
|
Add-WinUtilISOStagedDrivers -ContentRoot $ISOContentsDir -Logger $Log -InstallImagePath $InstallImagePath -InstallImageIndex $InstallImageIndex
|
||||||
$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 <Extensions><File> 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."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Invoke-WinUtilISORefreshUSBDrives {
|
|||||||
$combo.Items.Add("No USB drives detected.")
|
$combo.Items.Add("No USB drives detected.")
|
||||||
$combo.SelectedIndex = 0
|
$combo.SelectedIndex = 0
|
||||||
$sync["Win11ISOUSBDisks"] = @()
|
$sync["Win11ISOUSBDisks"] = @()
|
||||||
Write-Win11ISOLog "No USB drives detected."
|
Write-WinUtilISOLog "No USB drives detected."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ function Invoke-WinUtilISORefreshUSBDrives {
|
|||||||
$combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
|
$combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
|
||||||
}
|
}
|
||||||
$combo.SelectedIndex = 0
|
$combo.SelectedIndex = 0
|
||||||
Write-Win11ISOLog "Found $($removable.Count) USB drive(s)."
|
Write-WinUtilISOLog "Found $($removable.Count) USB drive(s)."
|
||||||
$sync["Win11ISOUSBDisks"] = $removable
|
$sync["Win11ISOUSBDisks"] = $removable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +30,20 @@ function Invoke-WinUtilISOWriteUSB {
|
|||||||
return
|
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"]
|
$combo = $sync["WPFWin11ISOUSBDriveComboBox"]
|
||||||
$selectedIndex = $combo.SelectedIndex
|
$selectedIndex = $combo.SelectedIndex
|
||||||
$selectedItemText = [string]$combo.SelectedItem
|
$selectedItemText = [string]$combo.SelectedItem
|
||||||
@@ -56,13 +70,13 @@ function Invoke-WinUtilISOWriteUSB {
|
|||||||
"Confirm USB Erase", "YesNo", "Warning")
|
"Confirm USB Erase", "YesNo", "Warning")
|
||||||
|
|
||||||
if ($confirm -ne "Yes") {
|
if ($confirm -ne "Yes") {
|
||||||
Write-Win11ISOLog "USB write cancelled by user."
|
Write-WinUtilISOLog "USB write cancelled by user."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
|
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
|
||||||
$sync["Win11ISOProcessRunning"] = $true
|
$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 = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
||||||
$runspace.ApartmentState = "STA"
|
$runspace.ApartmentState = "STA"
|
||||||
|
|||||||
+324
-85
@@ -37,14 +37,12 @@ Describe "Win11 Creator setup media" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify"
|
$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:cleanAndResetFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCleanAndReset"
|
||||||
$script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport"
|
$script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport"
|
||||||
$script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB"
|
$script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB"
|
||||||
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
|
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
|
||||||
$script:addDriversFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Add-DriversToImage"
|
$script:wimMetadataAssertionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Assert-WinUtilISOWimMetadata"
|
||||||
$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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
It "autounattend template does not force a product key" {
|
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"
|
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
|
||||||
$content = Get-Content -Path $isoScriptPath -Raw
|
$content = Get-Content -Path $isoScriptPath -Raw
|
||||||
|
|
||||||
foreach ($pattern in @(
|
foreach ($pattern in @(
|
||||||
'\[string\]\$InstallEditionId',
|
'\[string\]\$InstallEditionId',
|
||||||
|
'\[string\]\$InstallImagePath',
|
||||||
'\[int\]\$InstallImageIndex',
|
'\[int\]\$InstallImageIndex',
|
||||||
'sources\\ei\.cfg',
|
'sources\\ei\.cfg',
|
||||||
'PID\.txt'
|
'PID\.txt'
|
||||||
@@ -85,8 +106,57 @@ Describe "Win11 Creator setup media" {
|
|||||||
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
|
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It "keeps WIM servicing limited to one driver-only mount and commit" {
|
||||||
|
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
|
||||||
|
|
||||||
|
foreach ($expectedText in @(
|
||||||
|
"'/Mount-Image'",
|
||||||
|
"'/Add-Driver'",
|
||||||
|
"'/Commit'",
|
||||||
|
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
|
||||||
|
'install.wim metadata validation passed'
|
||||||
|
)) {
|
||||||
|
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($forbiddenText in @(
|
||||||
|
'Mount-WindowsImage',
|
||||||
|
'Dismount-WindowsImage',
|
||||||
|
'Export-WindowsImage',
|
||||||
|
'Set-WindowsImage',
|
||||||
|
'/ResetBase',
|
||||||
|
'/Cleanup-Image'
|
||||||
|
)) {
|
||||||
|
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "stages only boot-storage drivers in WinPE" {
|
||||||
|
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
|
||||||
|
|
||||||
|
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
|
||||||
|
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
|
||||||
|
$isoScriptContent | Should -Not -Match ([regex]::Escape('sources\$OEM$\$$\Drivers'))
|
||||||
|
$isoScriptContent | Should -Not -Match ([regex]::Escape('WinUtil-InstallDrivers.ps1'))
|
||||||
|
$isoScriptContent | Should -Not -Match ([regex]::Escape('SetupComplete.cmd'))
|
||||||
|
}
|
||||||
|
|
||||||
|
It "rejects invalid WIM metadata before and after driver injection" {
|
||||||
|
. ([scriptblock]::Create($script:wimMetadataAssertionFunction))
|
||||||
|
|
||||||
|
$valid = @{ Languages = 'en-US'; Installation = 'Client'; Edition = 'Professional'; ProductSuite = 'Terminal Server'; ProductType = 'WinNT' }
|
||||||
|
$invalidBefore = $valid.Clone()
|
||||||
|
$invalidBefore.Edition = '<undefined>'
|
||||||
|
$invalidAfter = $valid.Clone()
|
||||||
|
$invalidAfter.ProductType = '<undefined>'
|
||||||
|
|
||||||
|
{ Assert-WinUtilISOWimMetadata -Before $invalidBefore } | Should -Throw '*already invalid*'
|
||||||
|
{ Assert-WinUtilISOWimMetadata -Before $valid -After $invalidAfter } | Should -Throw '*validation failed*'
|
||||||
|
}
|
||||||
|
|
||||||
It "tracks every background ISO workflow with the shared busy state" {
|
It "tracks every background ISO workflow with the shared busy state" {
|
||||||
foreach ($functionText in @(
|
foreach ($functionText in @(
|
||||||
|
$script:mountAndVerifyFunction,
|
||||||
$script:modifyFunction,
|
$script:modifyFunction,
|
||||||
$script:cleanAndResetFunction,
|
$script:cleanAndResetFunction,
|
||||||
$script:exportFunction,
|
$script:exportFunction,
|
||||||
@@ -97,14 +167,28 @@ Describe "Win11 Creator setup media" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
It "mounts the copied image file that was verified from the ISO" {
|
It "runs ISO mount and verification outside the UI thread" {
|
||||||
foreach ($expectedText in @(
|
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape("Invoke-WPFRunspace -ParameterList @(,('isoPath', `$isoPath))"))
|
||||||
'$sourceImageFileName = Split-Path $wimPath -Leaf',
|
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Invoke-WPFUIThread {'))
|
||||||
'$localWim = Join-Path $isoContents "sources\$sourceImageFileName"',
|
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Write-WinUtilISOLog'))
|
||||||
'Copied ISO image file not found: sources\$sourceImageFileName'
|
$script:mountAndVerifyFunction | Should -Not -Match ([regex]::Escape('Write-Win11ISOLog'))
|
||||||
)) {
|
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false'))
|
||||||
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText))
|
$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" {
|
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 ""
|
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
|
||||||
}
|
}
|
||||||
|
|
||||||
It "writes ei.cfg and removes stale PID.txt for selected editions" {
|
It "writes ei.cfg and removes stale PID.txt for the selected edition" {
|
||||||
. ([scriptblock]::Create($script:editionConfigFunction))
|
|
||||||
|
|
||||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
|
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
|
||||||
$sourcesDir = Join-Path $contentRoot "sources"
|
$sourcesDir = Join-Path $contentRoot "sources"
|
||||||
$logs = [System.Collections.Generic.List[string]]::new()
|
$logs = [System.Collections.Generic.List[string]]::new()
|
||||||
@@ -146,8 +228,10 @@ Describe "Win11 Creator setup media" {
|
|||||||
try {
|
try {
|
||||||
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
|
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 "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 "PID.txt") | Should -BeFalse
|
||||||
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
|
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" {
|
It "stages the complete WinUtil customization script and selected image index" {
|
||||||
. ([scriptblock]::Create($script:editionConfigFunction))
|
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoAnswerFile_$([guid]::NewGuid())"
|
||||||
|
$template = Get-Content -Path $script:autoUnattendPath -Raw
|
||||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
|
|
||||||
$logs = [System.Collections.Generic.List[string]]::new()
|
|
||||||
$logger = { param($message) $logs.Add([string]$message) }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
|
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
|
$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'
|
||||||
($logs -join "|") | Should -Match "selected edition ID is unknown"
|
|
||||||
|
$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 {
|
} finally {
|
||||||
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
It "converts autounattend setup metadata without preserving placeholder product keys" {
|
It "stages storage drivers for WinPE and adds all drivers to one install.wim index" {
|
||||||
. ([scriptblock]::Create($script:answerFileChildElementFunction))
|
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
|
||||||
. ([scriptblock]::Create($script:answerFileConversionFunction))
|
$installWim = Join-Path $contentRoot 'sources\install.wim'
|
||||||
|
$template = Get-Content -Path $script:autoUnattendPath -Raw
|
||||||
[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"
|
|
||||||
}
|
|
||||||
|
|
||||||
$logs = [System.Collections.Generic.List[string]]::new()
|
$logs = [System.Collections.Generic.List[string]]::new()
|
||||||
Add-DriversToImage -MountPath "C:\Mount" -DriverDir "C:\Drivers" -Label "install" -Logger {
|
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
|
||||||
param($message)
|
|
||||||
$logs.Add([string]$message)
|
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"
|
Mock Start-Process {
|
||||||
($logs -join "|") | Should -Be " dism[install]: driver one| dism[install]: driver two"
|
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" {
|
It "discards a partially mounted install.wim after mount failure" {
|
||||||
$content = Get-Content -Path $script:isoScriptPath -Raw
|
$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 @(
|
function dism.exe {
|
||||||
'if ($InjectCurrentSystemDrivers)',
|
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
|
||||||
'Export-WindowsDriver -Online -Destination $driverExportRoot',
|
|
||||||
'Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log',
|
$script:dismCalls.Add(($Arguments -join '|'))
|
||||||
'Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log',
|
if ($Arguments -contains '/Get-WimInfo') {
|
||||||
'Warning: boot.wim not found - skipping boot.wim driver injection.',
|
$global:LASTEXITCODE = 0
|
||||||
'Driver injection skipped.'
|
'Languages : en-US'
|
||||||
)) {
|
'Installation : Client'
|
||||||
$content | Should -Match ([regex]::Escape($expectedText))
|
'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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,14 @@
|
|||||||
<Order>3</Order>
|
<Order>3</Order>
|
||||||
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
|
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
|
||||||
</RunSynchronousCommand>
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>4</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassCPUCheck /t REG_DWORD /d 1 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>5</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassStorageCheck /t REG_DWORD /d 1 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
</RunSynchronous>
|
</RunSynchronous>
|
||||||
</component>
|
</component>
|
||||||
</settings>
|
</settings>
|
||||||
@@ -48,6 +56,18 @@
|
|||||||
<Order>5</Order>
|
<Order>5</Order>
|
||||||
<Path>reg.exe unload "HKU\DefaultUser"</Path>
|
<Path>reg.exe unload "HKU\DefaultUser"</Path>
|
||||||
</RunSynchronousCommand>
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>6</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>7</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v PreventDeviceEncryption /t REG_DWORD /d 1 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>8</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v ShippedWithReserves /t REG_DWORD /d 0 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
</RunSynchronous>
|
</RunSynchronous>
|
||||||
</component>
|
</component>
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
+1
-1
@@ -1682,7 +1682,7 @@
|
|||||||
Foreground="{DynamicResource MainForegroundColor}"
|
Foreground="{DynamicResource MainForegroundColor}"
|
||||||
IsChecked="False"
|
IsChecked="False"
|
||||||
Margin="0,8,0,0"
|
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."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Verification results panel -->
|
<!-- Verification results panel -->
|
||||||
|
|||||||
Reference in New Issue
Block a user