Compare commits

...
3 Commits
Author SHA1 Message Date
Omar 48885d7c1e docs(win11creator): correct architecture and guide (#5106)
* docs(win11creator): correct architecture and guide

Update documentation to match actual Win11 Creator implementation:

- Clarify customizations run at first logon via autounattend.xml rather
  than offline WIM modification
- Correct removed bloat AppX count from 40+ to 19 packages
- Update USB partition layout to single FAT32 partition with WIM split
- Clarify OneDrive uninstall timing during first logon
- Document missing helpers including USB functions and oscdimg helpers
- Document edition pinning, $OEM$ fallback scripts, and WIM metadata validation
- Update logging location and remove offline registry tweak wording

* docs(win11creator): clarify timing and media limits
2026-09-28 13:08:11 -05:00
Omar abcbc23144 fix: inject Setup storage into boot.wim (#5102)
$WinpeDriver$ matched INF names, so RST companions reached Setup. Add SCSIAdapter and HDC packages to boot.wim index 2 with DISM.
2026-09-28 12:35:50 -05:00
dependabot[bot] 311fde6f31 chore(deps): bump astral-sh/setup-uv from 10.1.0 to 10.2.0 (#5120)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.1.0 to 10.2.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v10.1.0...v10.2.0)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 12:31:43 -05:00
7 changed files with 287 additions and 166 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: Install uv and Python
uses: astral-sh/setup-uv@v10.1.0
uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12.0"
python-version: "3.13"
+1 -1
View File
@@ -183,7 +183,7 @@ When the user corrects an agent approach, add or tighten one concrete rule here
- Have each Pester file load the assemblies and dot-source the functions it needs; several passed only because an earlier file in alphabetical order happened to load them.
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
- For Win11 Creator driver injection, keep offline WIM servicing to one mount and one commit: add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
- For Win11 Creator driver injection, inject storage controllers (`SCSIAdapter` / `HDC`) into `boot.wim` index 2 with DISM, not `$WinpeDriver$`. Keep `install.wim` on its own per-package `/Add-Driver` retry loop. For each image, add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
- 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.
- Public pull-request diffs may be sent to configured external review services without a separate privacy approval; do not block the review loop on upload authorization for this public repository.
@@ -135,21 +135,31 @@ The **Win11 Creator** is a specialized subsystem within Winutil that creates cus
### Win11 Creator Components
**Core Functions** (`functions/private/`):
- `Invoke-WinUtilISO.ps1`: Main orchestrator containing all Win11 Creator functions
- `Invoke-WinUtilISO.ps1`: Main orchestrator containing ISO workflow and helper functions
- `Invoke-WinUtilISOBrowse`: ISO file selection dialog
- `Invoke-WinUtilISOMountAndVerify`: Validates and mounts ISO, verifies it is an official Windows 11 ISO
- `Invoke-WinUtilISOModify`: Launches modification in background runspace
- `Invoke-WinUtilISOExport`: Handles ISO and USB export
- `Invoke-WinUtilISOExport`: Builds bootable ISO via `oscdimg.exe`
- `Invoke-WinUtilISOCheckExistingWork`: Recovers incomplete work sessions
- `Invoke-WinUtilISOCleanAndReset`: Cleans up temp directories and resets UI
- `Write-WinUtilISOLog`: Posts log messages to UI status box and session log
- `Set-WinUtilISOStep`: Controls wizard navigation state
- `Get-WinUtilEditionIdFromName`: Maps edition display names to setup edition IDs
- `Invoke-WinUtilRobocopy`: Copies files with exit code verification
- `Find-WinUtilOscdimg` / `Get-WinUtilOscdimgPath`: Locates or installs `oscdimg.exe`
- `Invoke-WinUtilISOScript.ps1`: Applies modifications to mounted install.wim
- Removes provisioned AppX packages (40+ bloatware apps)
- Injects drivers (optional) from the current system
- Removes OneDrive setup files
- Applies offline registry tweaks (hardware bypass, privacy, telemetry, OOBE)
- Deletes telemetry scheduled task definitions
- Pre-stages setup scripts from autounattend.xml
- `Invoke-WinUtilISOUSB.ps1`: USB drive detection and formatting
- `Invoke-WinUtilISORefreshUSBDrives`: Enumerates USB drives
- `Get-WinUtilFreeDriveLetter`: Finds available drive letters
- `Invoke-WinUtilISOWriteUSB`: Formats USB drive as GPT/FAT32, splits WIM if needed, and copies files
- `Invoke-WinUtilISOScript.ps1`: Prepares setup media and customizations
- Stages AppX removal (19 bloatware packages) into `autounattend.xml` for first logon
- Applies 50+ registry tweaks during Windows Setup and first logon; removes scheduled tasks at first logon
- Triggers OneDrive uninstall during first logon
- Stages setup script fallbacks to `sources\$OEM$\$$\Setup\Scripts\`
- Pins selected edition in `autounattend.xml` and writes `sources\ei.cfg`
- Injects eligible current system drivers into WIM images if enabled (WIM mount is only used for driver servicing)
### Win11 Creator Data Flow
@@ -169,18 +179,19 @@ User optionally enables the Driver Injection checkbox
↓
Invoke-WinUtilISOModify (runs in background runspace)
├─ Create work directory: ~WinUtil_Win11ISO_[timestamp]
├─ Copy ISO contents to disk (~5-6 GB)
├─ Mount install.wim at selected edition/index
├─ Copy ISO contents to disk via robocopy (~5-6 GB)
├─ Invoke-WinUtilISOScript:
│ ├─ Remove 40+ bloat AppX packages
│ ├─ Export and inject drivers (if enabled)
│ ├─ Remove OneDrive setup
│ ├─ Load offline registry hives
│ ├─ Apply 50+ registry tweaks (hardware bypass, privacy, telemetry, OOBE, etc.)
│ ├─ Delete telemetry scheduled task files
│ ├─ Pre-stage setup scripts from autounattend.xml to C:\Windows\Setup\Scripts\
│ └─ Unload registry hives
├─ Dismount and save the modified install.wim (~10+ minutes, slowest step)
│ ├─ Generate autounattend.xml with Windows PE and specialize safeguards
│ ├─ Add first-logon script (19 AppX removals, registry tweaks, OneDrive uninstall)
│ ├─ Pin selected edition index in autounattend.xml (/IMAGE/INDEX)
│ ├─ Stage setup script fallbacks to sources\$OEM$\$$\Setup\Scripts\
│ ├─ Write sources\ei.cfg and remove stale sources\PID.txt
│ └─ If driver injection enabled:
│ ├─ Require install.wim; install.esd cannot accept driver injection
│ ├─ Export current system drivers via DISM
│ ├─ Exclude stale duplicate packages
│ ├─ Inject storage drivers into boot.wim index 2 and eligible drivers into install.wim
│ └─ Validate WIM metadata before and after injection
├─ Dismount source ISO
└─ Report completion, enable export options
↓
@@ -191,12 +202,15 @@ Invoke-WinUtilISOExport (user chooses output)
│
└─ Option 2: Write to USB
├─ Format USB as GPT
├─ Create 512 MB EFI partition
├─ Copy modified ISO contents
├─ Create single FAT32 partition (capped at 32 GB)
├─ Split install.wim into .swm files if > 3.8 GB
├─ Reject install.esd files of 4 GB or more
├─ Copy files via robocopy
└─ Output: Bootable USB (minimum 8 GB)
↓
Invoke-WinUtilISOCleanAndReset (optional)
└─ Delete temp working directory (~10-15 GB)
├─ Dismount any open WIM mounts with discard
├─ Delete temp working directory (~10-15 GB)
└─ Reset UI to initial state
```
@@ -208,20 +222,30 @@ Invoke-WinUtilISOCleanAndReset (optional)
- Checks image metadata for "Windows 11" string
- Rejects custom, modified, or non-Windows 11 ISOs
**WIM Metadata Validation**:
- During driver injection, `Assert-WinUtilISOWimMetadata` validates critical fields (`Languages`, `Installation`, `Edition`, `ProductSuite`, `ProductType`) before and after WIM servicing
- Driver injection stops before export if the required metadata is missing or changes
- Without driver injection, WinUtil preserves the original installation image and skips WIM metadata validation
**Edition Pinning & Setup Fallback**:
- Writes `sources\ei.cfg` and removes `sources\PID.txt` so setup does not use mismatched OEM product keys
- Pins the selected edition index in `autounattend.xml` (`/IMAGE/INDEX`)
- Stages fallback setup scripts under `sources\$OEM$\$$\Setup\Scripts\` with `UseConfigurationSet` enabled
**Work Session Recovery**:
- Auto-detects incomplete work from previous sessions
- Allows resuming the export step without re-running selection and modification
- Prevents redundant modifications
**Modification Safety**:
- All registry changes are documented in a script (reversible)
- Windows PE and specialize set hardware bypass and setup safeguards; `WinUtil-PostInstall.ps1` applies additional registry changes at first logon
- Original ISO never modified; only working copy
- Logged to `WinUtil_Win11ISO.log` for debugging
- DISM handles image dismount with automatic cleanup on error
- Logged to the WinUtil session log and the live UI status panel
- DISM handles image dismount with automatic cleanup and discard on error
### Win11 Creator Registry Tweaks
The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
The `Invoke-WinUtilISOScript` function applies **50+ registry tweaks** during setup and first logon:
**Hardware Bypass**:
- TPM 2.0 check bypass
@@ -259,9 +283,9 @@ The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
### Driver Injection Feature
**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into both:
- `install.wim` (main OS image)
- `boot.wim` index 2 (Windows Setup PE environment)
**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into:
- `install.wim` (main OS image), excluding stale duplicates
- `boot.wim` index 2 (Windows Setup PE environment), storage controllers only (`SCSIAdapter` / `HDC`)
**Use Case**: Enables offline installation on systems with missing drivers.
+15 -7
View File
@@ -45,8 +45,8 @@ Once the ISO is verified, WinUtil moves to this step and shows the mounted drive
Then click **Run Windows ISO Modification and Creator** to start the customization process. WinUtil will:
**App & Component Removal:**
- **Remove 40+ bloat apps** — Clipchamp, Teams, Copilot, Dev Home, new Outlook, Bing apps, Solitaire, and more
- **Delete OneDrive setup** from the image
- **Remove 19 bloat apps** — Clipchamp, Teams, Dev Home, new Outlook, Bing apps, Solitaire, and more
- **Uninstall OneDrive** during first logon
**System Customization:**
- **Bypass hardware checks** — removes TPM, Secure Boot, CPU, RAM, and storage requirement enforcement so the ISO installs on unsupported hardware
@@ -67,9 +67,13 @@ Then click **Run Windows ISO Modification and Creator** to start the customizati
- **Disable Copilot and search box suggestions**
**Optional: Driver Injection**
- If enabled, WinUtil exports the drivers from your current system, stages boot-storage drivers for Windows Setup, and injects eligible packages into the selected `install.wim` image. Stale duplicate packages are excluded before injection. Each remaining package is added separately, so one incompatible package does not stop the others. If a package fails, WinUtil discards the partial mount and retries the remaining packages against the original image. Check the live log for warnings: if every package fails, WinUtil keeps the original `install.wim` and still produces the ISO.
- If enabled, WinUtil exports the drivers from your current system, injects boot-storage drivers into `boot.wim` (Windows Setup, index 2), and injects eligible packages into the selected `install.wim` image. Stale duplicate packages are excluded before injection. Each remaining package is added separately, so one incompatible package does not stop the others. If a package fails, WinUtil discards the partial mount and retries the remaining packages against the original image. Check the live log for warnings: if every package fails, WinUtil keeps the original `install.wim` and still produces the ISO.
A live log shows progress as each step completes. This stage usually takes **10–30 minutes** depending on disk speed. The WIM dismount near the end is the slowest part, so do not close WinUtil while it is running.
:::note
Driver injection requires `sources\install.wim`. If the ISO uses `sources\install.esd`, leave this option off.
:::
A live log shows progress as each step completes. Without driver injection, WinUtil writes the setup changes in a few seconds after the ISO copy. With driver injection, this stage usually takes **10–20 minutes**, depending on hardware. Keep WinUtil open until the log reports completion.
:::note
The resulting ISO is close to the size of the source ISO. WinUtil does not remove the unused editions from `install.wim`; it selects your edition through `sources\ei.cfg` and `autounattend.xml` so Windows Setup installs the right one.
@@ -95,7 +99,11 @@ Once the modification is complete, choose how to save your image:
1. Click **Write Directly to a USB Drive**.
2. Select your USB drive from the dropdown (click **Refresh** if it doesn't appear).
3. Click **Erase & Write to USB** and confirm the warning — **all data on the drive will be permanently erased**.
4. WinUtil formats the drive as GPT with a 512 MB EFI partition and copies the modified Windows files.
4. WinUtil formats the drive as GPT with a single FAT32 partition (capped at 32 GB, splitting `install.wim` into `.swm` files if larger than 3.8 GB) and copies the modified Windows files.
:::note
WinUtil cannot split `install.esd`. If that file is 4 GB or larger, save an ISO instead of writing a FAT32 USB drive.
:::
:::danger
Double-check you have selected the correct drive before confirming. This operation cannot be undone.
@@ -130,10 +138,10 @@ When you install Windows 11 from your modified ISO:
| Problem | Fix |
|---------|-----|
| "install.wim not found" | Not a valid Windows 11 ISO — download a fresh one from Microsoft |
| "install.wim / install.esd was not found" | The ISO has no Windows installation image — download a fresh official ISO from Microsoft |
| "oscdimg.exe not found" | Run `winget install -e --id Microsoft.OSCDIMG` then retry |
| USB drive not showing up | Plug it in, wait a few seconds, then click **Refresh** |
| Modification seems stuck | The WIM dismount step is slow — wait at least 10 minutes before assuming it's frozen |
| Driver injection seems stuck | WIM servicing can pause at a mount or commit. Allow 10–20 minutes depending on hardware, and check the live log before closing WinUtil |
| "Access Denied" error | Make sure WinUtil is running as Administrator |
| Driver injection warning | Review the live log for the named package. Other compatible packages continue; if none succeed, the ISO is created with the original `install.wim` |
| "Setup has failed to validate the product key" | Recreate the ISO with the latest WinUtil. The creator now removes stale `PID.txt`, writes `sources\ei.cfg`, and pins the selected image in `autounattend.xml` so setup does not use an embedded OEM key for a different edition |
+1 -1
View File
@@ -401,7 +401,7 @@ function Invoke-WinUtilISOModify {
if ($driversInjected.Value) {
Step-WinUtilJob -Status "Finalizing install image..." -Percent 70
Write-WinUtilISOLog "Added current-system drivers to $sourceImageFileName index $SelectedWimIndex with one mount and commit."
Write-WinUtilISOLog "Added current-system drivers to $sourceImageFileName index $SelectedWimIndex."
} elseif ($InjectDrivers) {
Step-WinUtilJob -Status "Preserving install image..." -Percent 70
Write-WinUtilISOLog "No current-system drivers were injected into $sourceImageFileName index $SelectedWimIndex; install.wim was left unchanged. Review the warning log entries for details."
+113 -107
View File
@@ -6,7 +6,8 @@ function Invoke-WinUtilISOScript {
.DESCRIPTION
Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup
in the answer file for first logon, writes sources\ei.cfg for the selected
edition, and optionally adds current-system drivers to one install.wim index.
edition, and optionally adds current-system drivers to boot.wim index 2 and
one install.wim index.
.PARAMETER ISOContentsDir
Root directory of the copied ISO contents.
@@ -51,31 +52,9 @@ function Invoke-WinUtilISOScript {
)
$DriversInjected.Value = $false
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 Test-WinUtilISOStorageDriver {
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') {
return $true
}
try {
return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*(SCSIAdapter|HDC)\s*(?:;.*)?$'
} catch {
@@ -267,6 +246,93 @@ function Invoke-WinUtilISOScript {
return @(& dism.exe /English /Get-MountedImageInfo 2>$null) -match [regex]::Escape($Path)
}
function Get-WinUtilISODriverFolderName {
param ([Parameter(Mandatory)][string]$DriverFolder)
if ($DriverFolder.StartsWith($driverExportRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
return $DriverFolder.Substring($driverExportRoot.Length).TrimStart('\')
}
return $DriverFolder
}
function Get-WinUtilISORootDriverFolders {
param ([Parameter(Mandatory)][AllowEmptyCollection()][string[]]$DriverFolders)
return @($DriverFolders | Where-Object {
$candidate = $_
-not ($DriverFolders | Where-Object { $candidate.StartsWith("$_\", [System.StringComparison]::OrdinalIgnoreCase) })
})
}
function Add-WinUtilISODriversToImage {
param (
[Parameter(Mandatory)][string]$ImagePath,
[Parameter(Mandatory)][int]$ImageIndex,
[Parameter(Mandatory)][string]$MountDir,
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$DriverFolders,
[Parameter(Mandatory)][string]$ImageLabel,
[Parameter(Mandatory)][ref]$ImageMounted
)
if ($DriverFolders.Count -eq 0) {
& $Logger "No driver packages to add to ${ImageLabel}."
return 0
}
Set-ItemProperty -LiteralPath $ImagePath -Name IsReadOnly -Value $false
New-Item -Path $MountDir -ItemType Directory -Force | Out-Null
$remainingDriverFolders = @($DriverFolders)
while ($remainingDriverFolders.Count -gt 0) {
& $Logger "Mounting $ImageLabel index $ImageIndex for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$ImagePath", "/Index:$ImageIndex", "/MountDir:$MountDir") -Operation 'mount' | Out-Null
$ImageMounted.Value = $true
$failedDriverFolder = $null
$driverName = $null
foreach ($driverFolder in $remainingDriverFolders) {
$driverName = Get-WinUtilISODriverFolderName -DriverFolder $driverFolder
try {
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$MountDir", '/Add-Driver', "/Driver:$driverFolder", '/Recurse') -Operation "add-driver:$driverName" | Out-Null
} catch {
& $Logger "Warning: failed to add driver package '$driverName': $_"
$failedDriverFolder = $driverFolder
break
}
}
if (-not $failedDriverFolder) {
break
}
& $Logger "Discarding the potentially partial $ImageLabel mount before continuing without '$driverName'."
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$MountDir", '/Discard') -Operation 'discard' | Out-Null
$ImageMounted.Value = $false
} catch {
throw "Failed to discard the potentially partial $ImageLabel mount after driver package '$driverName' failed: $_"
}
$remainingDriverFolders = @($remainingDriverFolders | Where-Object { $_ -ne $failedDriverFolder })
}
$addedCount = $remainingDriverFolders.Count
if ($addedCount -eq 0) {
if ($ImageLabel -eq 'install.wim') {
& $Logger "Warning: none of the $($DriverFolders.Count) exported driver packages could be added; continuing with an unmodified install.wim."
} else {
& $Logger "Warning: none of the $($DriverFolders.Count) driver packages could be added to ${ImageLabel}."
}
return 0
}
& $Logger "Added $addedCount of $($DriverFolders.Count) driver packages to ${ImageLabel}."
& $Logger "Committing the driver-only $ImageLabel change..."
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$MountDir", '/Commit') -Operation 'commit' | Out-Null
$ImageMounted.Value = $false
return $addedCount
}
if ([IO.Path]::GetExtension($InstallImagePath) -ne '.wim') {
throw 'Current-system driver injection requires install.wim; install.esd cannot be serviced in place.'
}
@@ -287,7 +353,7 @@ function Invoke-WinUtilISOScript {
$imageMounted = $false
try {
& $Logger "Exporting current system drivers before modifying install.wim..."
& $Logger "Exporting current system drivers before WIM driver injection..."
$dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
Invoke-WinUtilISODism -Arguments @('/English', '/Online', '/Export-Driver', "/Destination:$driverExportRoot", "/LogPath:$dismLog") -Operation 'export-driver' | Out-Null
@@ -296,32 +362,6 @@ function Invoke-WinUtilISOScript {
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."
}
$stagedDriverFolders = @(Select-WinUtilISOStagedDriverPackages -DriverFolderGroups $driverFolders -Logger $Logger)
$metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore
@@ -352,67 +392,33 @@ function Invoke-WinUtilISOScript {
}
}
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedDriverFolderGroups.Count) excluded)."
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($($excludedDriverFolderGroups.Count) excluded)."
Set-ItemProperty -LiteralPath $InstallImagePath -Name IsReadOnly -Value $false
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
# Storage for Setup comes from the same surviving set as install.wim, so stale
# duplicates never reach boot.wim.
$storageFolders = @(
$driverFolders |
Where-Object { $_.Name -in $stagedDriverFolders } |
Where-Object { @($_.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ }).Count -gt 0 } |
ForEach-Object { [string]$_.Name }
)
$storageRootFolders = @(Get-WinUtilISORootDriverFolders -DriverFolders $storageFolders)
$rootPackageFolders = @(Get-WinUtilISORootDriverFolders -DriverFolders $stagedDriverFolders)
$imageMountedRef = [ref]$imageMounted
# Add each package separately so one bad driver cannot fail the rest. Because
# /Recurse covers descendants, only the highest surviving folder in each tree
# needs its own DISM call.
$rootPackageFolders = @($stagedDriverFolders | Where-Object {
$candidate = $_
-not ($stagedDriverFolders | Where-Object { $candidate.StartsWith("$_\", [System.StringComparison]::OrdinalIgnoreCase) })
})
& $Logger "Adding $($rootPackageFolders.Count) root driver packages to install.wim."
$remainingDriverFolders = @($rootPackageFolders)
while ($remainingDriverFolders.Count -gt 0) {
& $Logger "Mounting install.wim index $InstallImageIndex for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$InstallImagePath", "/Index:$InstallImageIndex", "/MountDir:$mountDir") -Operation 'mount' | Out-Null
$imageMounted = $true
$failedDriverFolder = $null
foreach ($driverFolder in $remainingDriverFolders) {
$driverName = $driverFolder
if ($driverFolder.StartsWith($driverExportRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
$driverName = $driverFolder.Substring($driverExportRoot.Length).TrimStart('\')
}
try {
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$mountDir", '/Add-Driver', "/Driver:$driverFolder", '/Recurse') -Operation "add-driver:$driverName" | Out-Null
} catch {
& $Logger "Warning: failed to add driver package '$driverName': $_"
$failedDriverFolder = $driverFolder
break
}
$bootWim = Join-Path $ContentRoot 'sources\boot.wim'
if ($storageRootFolders.Count -gt 0) {
if (Test-Path -LiteralPath $bootWim) {
& $Logger "Adding $($storageRootFolders.Count) root storage driver packages to boot.wim."
$null = Add-WinUtilISODriversToImage -ImagePath $bootWim -ImageIndex 2 -MountDir $mountDir -DriverFolders $storageRootFolders -ImageLabel 'boot.wim' -ImageMounted $imageMountedRef
} else {
& $Logger 'Warning: boot.wim was not found; Windows Setup will not have injected storage drivers.'
}
if (-not $failedDriverFolder) {
break
}
& $Logger "Discarding the potentially partial install.wim mount before continuing without '$driverName'."
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
$imageMounted = $false
} catch {
throw "Failed to discard the potentially partial install.wim mount after driver package '$driverName' failed: $_"
}
$remainingDriverFolders = @($remainingDriverFolders | Where-Object { $_ -ne $failedDriverFolder })
}
$addedCount = $remainingDriverFolders.Count
if ($addedCount -eq 0) {
# Boot-storage drivers staged for WinPE remain available to Windows Setup.
& $Logger "Warning: none of the $($rootPackageFolders.Count) exported driver packages could be added; continuing with an unmodified install.wim."
} else {
& $Logger "Added $addedCount of $($rootPackageFolders.Count) driver packages to install.wim."
& $Logger 'Committing the driver-only install.wim change...'
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
$imageMounted = $false
& $Logger "Adding $($rootPackageFolders.Count) root driver packages to install.wim."
$addedCount = Add-WinUtilISODriversToImage -ImagePath $InstallImagePath -ImageIndex $InstallImageIndex -MountDir $mountDir -DriverFolders $rootPackageFolders -ImageLabel 'install.wim' -ImageMounted $imageMountedRef
if ($addedCount -gt 0) {
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
@@ -423,7 +429,7 @@ function Invoke-WinUtilISOScript {
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
} catch {
& $Logger "Warning: could not discard the failed install.wim mount: $_"
& $Logger "Warning: could not discard the failed WIM mount: $_"
}
}
Remove-Item -LiteralPath $mountDir -Recurse -Force -ErrorAction SilentlyContinue
+102 -19
View File
@@ -214,7 +214,7 @@ Describe "Win11 Creator setup media" {
$script:modifyFunction | Should -Match 'if \(\$m -like "Warning:\*"\)[\s\S]*Write-WinUtilISOLog -Level "WARN" -Message \$m -SkipSessionLog[\s\S]*Write-Warning \$m'
}
It "keeps WIM servicing limited to one driver-only mount and commit" {
It "keeps WIM servicing limited to DISM Add-Driver without image export or cleanup" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
@@ -222,7 +222,8 @@ Describe "Win11 Creator setup media" {
"'/Add-Driver'",
"'/Commit'",
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
'install.wim metadata validation passed'
'install.wim metadata validation passed',
"Join-Path `$ContentRoot 'sources\boot.wim'"
)) {
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
}
@@ -233,17 +234,19 @@ Describe "Win11 Creator setup media" {
'Export-WindowsImage',
'Set-WindowsImage',
'/ResetBase',
'/Cleanup-Image'
'/Cleanup-Image',
'$WinpeDriver$'
)) {
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
}
}
It "stages only boot-storage drivers in WinPE" {
It "injects only SCSIAdapter or HDC storage drivers into boot.wim" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot 'sources\boot.wim'"))
$isoScriptContent | Should -Not -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$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'))
@@ -435,7 +438,7 @@ Describe "Win11 Creator setup media" {
It "stages storage drivers for WinPE and adds all drivers to one install.wim index" {
It "adds eligible drivers to one install.wim index and does not create `$WinpeDriver$" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
@@ -443,7 +446,7 @@ Describe "Win11 Creator setup media" {
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'SCSIAdapter' },
@{ 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' },
@@ -465,12 +468,8 @@ Describe "Win11 Creator setup media" {
$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
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
($logs -join '|') | Should -Match 'Warning: boot.wim was not found'
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 9
@@ -488,7 +487,7 @@ Describe "Win11 Creator setup media" {
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'Exported 10 of 11 driver packages \(2 staged for WinPE, 1 excluded\)'
($logs -join '|') | Should -Match 'Exported 10 of 11 driver packages \(1 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e' \(DriverVer 6/1/2024,10\.0\.26100\.9168\)"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
@@ -505,6 +504,91 @@ Describe "Win11 Creator setup media" {
}
}
It "injects SCSIAdapter storage drivers into boot.wim index 2 and not `$WinpeDriver`$" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWim_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$bootWim = Join-Path $contentRoot 'sources\boot.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'hdc_pkg'; Name = 'ide.inf'; Class = 'HDC' },
@{ Path = 'name_only_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'iastorhsacomponent.inf_amd64_1b2a068a8496b6a2'; Name = 'iaStorHsaComponent.inf'; Class = 'SoftwareComponent' },
@{ Path = 'iastorhsa_ext.inf_amd64_ba71359697f80d4e'; Name = 'iaStorHsa_Ext.inf'; Class = 'Extension' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
Set-Content -Path $bootWim -Value 'mock-boot'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' -and $_ -match '/Index:2' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 2
$bootMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' } | Select-Object -First 1
$installMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' } | Select-Object -First 1
$bootMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $bootMountCall)
$installMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $installMountCall)
$bootAdds = @($script:dismCalls[$bootMountIndex..($installMountIndex - 1)] | Where-Object { $_ -match '/Add-Driver' })
$bootAdds.Count | Should -Be 2
($bootAdds -join "`n") | Should -Match ([regex]::Escape('scsi_pkg'))
($bootAdds -join "`n") | Should -Match ([regex]::Escape('hdc_pkg'))
($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('net_pkg'))
($bootAdds -join "`n") | Should -Not -Match 'iastorhsa'
($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('name_only_pkg'))
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 8
($logs -join '|') | Should -Match 'Added 2 of 2 driver packages to boot.wim'
($logs -join '|') | Should -Match 'Added 6 of 6 driver packages to install.wim'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects only the newest duplicate storage package into boot.wim" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWimDedup_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$bootWim = Join-Path $contentRoot 'sources\boot.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'iastorvd.inf_amd64_11111111aaaaaaaa'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '1/1/2023,20.2.1.1016' },
@{ Path = 'iastorvd.inf_amd64_22222222bbbbbbbb'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '6/1/2024,20.2.8.1028' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
Set-Content -Path $bootWim -Value 'mock-boot'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
$bootAdds = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' })
@($bootAdds | Where-Object { $_ -match '22222222bbbbbbbb' }).Count | Should -Be 2
@($bootAdds | Where-Object { $_ -match '11111111aaaaaaaa' }).Count | Should -Be 0
($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to boot.wim'
($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to install.wim'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects Class=Extension driver packages like any other export" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoExtensionInject_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
@@ -528,7 +612,7 @@ Describe "Win11 Creator setup media" {
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 staged for WinPE, 0 excluded\)'
($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 excluded\)'
$driversInjected.Value | Should -BeTrue
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ext_pkg_lower')
@@ -628,7 +712,7 @@ Describe "Win11 Creator setup media" {
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 2
($logs -join '|') | Should -Match 'Exported 2 of 2 driver packages \(0 staged for WinPE, 0 excluded\)'
($logs -join '|') | Should -Match 'Exported 2 of 2 driver packages \(0 excluded\)'
($logs -join '|') | Should -Not -Match 'Excluding stale duplicate driver package'
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_11111111aaaaaaaa')
@@ -669,7 +753,7 @@ Describe "Win11 Creator setup media" {
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(0 staged for WinPE, 4 excluded\)'
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(4 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_6688e7b66f8d9fb5' \(DriverVer 1/1/2024,10\.0\.26100\.8972\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*sample\.inf_amd64_11111111aaaaaaaa' \(DriverVer unknown\) superseded by '.*sample\.inf_amd64_22222222bbbbbbbb' \(DriverVer 3/1/2024,1\.2\.3\.4\)"
@@ -753,8 +837,7 @@ Describe "Win11 Creator setup media" {
($logs -join '|') | Should -Match "none of the $script:expectedRootPackages exported driver packages could be added"
($logs -join '|') | Should -Not -Match "Added 0 of $script:expectedRootPackages"
# WinPE staging is independent of WIM servicing, so it must survive the failure.
@(Get-ChildItem -Path (Join-Path $contentRoot '$WinpeDriver$') -Directory).Count | Should -Be 2
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
$driversInjected.Value | Should -BeFalse
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue