mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-10-08 11:39:10 +11:00
Compare commits
13
Commits
ad81e0a7af
...
26.09.29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9419b2803e | ||
|
|
92d5a08d49 | ||
|
|
a3a7c500d4 | ||
|
|
40666e1d31 | ||
|
|
4d4e219562 | ||
|
|
fc03af421b | ||
|
|
8939ecf4b4 | ||
|
|
735b59b82f | ||
|
|
d0d39d6478 | ||
|
|
6f1266eb86 | ||
|
|
48885d7c1e | ||
|
|
abcbc23144 | ||
|
|
311fde6f31 |
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 78 KiB |
@@ -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.
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ This page is generated from [`functions/public/Invoke-WPFPanelAutologin.ps1`](ht
|
||||
|
||||
```powershell title="functions/public/Invoke-WPFPanelAutologin.ps1"
|
||||
function Invoke-WPFPanelAutologin {
|
||||
Invoke-WebRequest -Uri https://live.sysinternals.com/Autologon.exe -OutFile "$winutildir\autologin.exe"
|
||||
Start-Process -FilePath "$winutildir\autologin.exe" -ArgumentList /accepteula
|
||||
$autologonPath = Join-Path $sync.winutildir "autologin.exe"
|
||||
Invoke-WebRequest -Uri https://live.sysinternals.com/Autologon.exe -OutFile $autologonPath
|
||||
Start-Process -FilePath $autologonPath -ArgumentList /accepteula
|
||||
}
|
||||
```
|
||||
|
||||
@@ -21,11 +21,54 @@ function Invoke-WPFSystemRepair {
|
||||
3. DISM - Repair a corrupted Windows operating system image
|
||||
#>
|
||||
|
||||
Start-Process cmd.exe -ArgumentList "/c chkdsk /scan /perf" -NoNewWindow -Wait
|
||||
Start-Process cmd.exe -ArgumentList "/c sfc /scannow" -NoNewWindow -Wait
|
||||
Start-Process cmd.exe -ArgumentList "/c dism /online /cleanup-image /restorehealth" -NoNewWindow -Wait
|
||||
# SuccessCodes maps the non-zero exits a step treats as success to what they mean. The codes
|
||||
# are per step because the same number means different things: 1 and 2 are ordinary chkdsk
|
||||
# outcomes, while 1 from sfc is a failure, and 3010 is a repaired image from DISM only.
|
||||
$steps = @(
|
||||
@{
|
||||
Label = "Checking the disk for errors"
|
||||
Arguments = "/c chkdsk /scan /perf"
|
||||
# 3 is left out: the disk could not be checked, or has errors an online scan cannot
|
||||
# fix, and the steps after this one are not worth running on a disk in that state.
|
||||
SuccessCodes = @{
|
||||
1 = "errors were found and fixed"
|
||||
2 = "cleanup was performed, or was skipped because /f was not given"
|
||||
}
|
||||
},
|
||||
@{
|
||||
Label = "Scanning protected system files"
|
||||
Arguments = "/c sfc /scannow"
|
||||
SuccessCodes = @{}
|
||||
},
|
||||
@{
|
||||
Label = "Repairing the Windows image"
|
||||
Arguments = "/c dism /online /cleanup-image /restorehealth"
|
||||
SuccessCodes = @{
|
||||
3010 = "a restart is needed for the repair to take effect"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Write-Host "==> Finished System Repair"
|
||||
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
|
||||
$completed = 0
|
||||
foreach ($step in $steps) {
|
||||
Step-WinUtilJob -Status "$($step.Label) ($($completed + 1)/$($steps.Count))" -Percent ([int](($completed / $steps.Count) * 100))
|
||||
Write-WinUtilLog -Component "SystemRepair" -Message $step.Label
|
||||
# Start-Process does not throw on a nonzero exit, so without this a failed chkdsk, sfc
|
||||
# or dism run would still be reported as a completed repair
|
||||
$process = Start-Process cmd.exe -ArgumentList $step.Arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
if ($step.SuccessCodes.ContainsKey($exitCode)) {
|
||||
# Start-WinUtilJob records WarningRecord output in both the session log and the
|
||||
# job result, so accepted nonzero outcomes cannot finish with a green checkmark.
|
||||
Write-Warning "$($step.Label) finished: $($step.SuccessCodes[$exitCode])."
|
||||
} else {
|
||||
throw "$($step.Label) failed with exit code $exitCode."
|
||||
}
|
||||
}
|
||||
|
||||
$completed++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,8 +25,5 @@ function Invoke-WPFFixesNTPPool {
|
||||
Restart-Service w32time
|
||||
w32tm /resync
|
||||
|
||||
Write-Host "================================="
|
||||
Write-Host "-- NTP Configuration Complete ---"
|
||||
Write-Host "================================="
|
||||
}
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@ function Invoke-WPFFixesUpdate {
|
||||
param($Aggressive = $false)
|
||||
|
||||
Write-Progress -Id 0 -Activity "Repairing Windows Update" -PercentComplete 0
|
||||
Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
|
||||
Step-WinUtilJob -State "Indeterminate"
|
||||
Write-Host "Starting Windows Update Repair..."
|
||||
# Wait for the first progress bar to show, otherwise the second one won't show
|
||||
Start-Sleep -Milliseconds 200
|
||||
@@ -203,24 +203,14 @@ function Invoke-WPFFixesUpdate {
|
||||
try {
|
||||
(New-Object -ComObject Microsoft.Update.AutoUpdate).DetectNow()
|
||||
} catch {
|
||||
Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
|
||||
Write-WinUtilLog -Level "ERROR" -Component "Updates" -Message "Failed to create Windows Update COM object: $_"
|
||||
Write-Warning "Failed to create Windows Update COM object: $_"
|
||||
}
|
||||
Start-Process -NoNewWindow -FilePath "wuauclt" -ArgumentList "/resetauthorization", "/detectnow"
|
||||
Write-Progress -Id 10 -ParentId 0 -Activity "Forcing discovery" -Status "Completed" -PercentComplete 100
|
||||
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Completed" -PercentComplete 100
|
||||
|
||||
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
|
||||
|
||||
$ButtonType = [System.Windows.MessageBoxButton]::OK
|
||||
$MessageboxTitle = "Reset Windows Update "
|
||||
$Messageboxbody = ("Stock settings loaded.`n Please reboot your computer")
|
||||
$MessageIcon = [System.Windows.MessageBoxImage]::Information
|
||||
|
||||
[System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
|
||||
Write-Host "==============================================="
|
||||
Write-Host "-- Reset All Windows Update Settings to Stock -"
|
||||
Write-Host "==============================================="
|
||||
Show-WinUtilMessage -Message "Stock settings loaded.`n Please reboot your computer" -Title "Reset Windows Update" -Button "OK" -Icon "Information" | Out-Null
|
||||
|
||||
# Remove the progress bars
|
||||
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Completed
|
||||
|
||||
@@ -18,18 +18,8 @@ function Invoke-WPFFixesWinget {
|
||||
.DESCRIPTION
|
||||
BravoNorris for the fantastic idea of a button to reinstall WinGet
|
||||
#>
|
||||
# Install Choco if not already present
|
||||
try {
|
||||
Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
|
||||
Write-Host "==> Starting WinGet Repair"
|
||||
Install-WinUtilWinget
|
||||
} catch {
|
||||
Write-Error "Failed to install WinGet: $_"
|
||||
Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
|
||||
} finally {
|
||||
Write-Host "==> Finished WinGet Repair"
|
||||
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
|
||||
}
|
||||
|
||||
Step-WinUtilJob -Status "Repairing WinGet" -State "Indeterminate"
|
||||
Install-WinUtilWinget -Force
|
||||
}
|
||||
```
|
||||
|
||||
+61
-8
@@ -9,19 +9,72 @@ This page is generated from [`functions/private/Invoke-WinUtilInstallPSProfile.p
|
||||
:::
|
||||
|
||||
```powershell title="functions/private/Invoke-WinUtilInstallPSProfile.ps1"
|
||||
function Get-WinUtilPowerShell7Path {
|
||||
$command = Get-Command pwsh -CommandType Application -ErrorAction SilentlyContinue
|
||||
if ($command) { return $command.Source }
|
||||
|
||||
foreach ($candidate in @(
|
||||
"$env:ProgramFiles\PowerShell\7\pwsh.exe",
|
||||
"$env:LOCALAPPDATA\Microsoft\WindowsApps\pwsh.exe")) {
|
||||
if (Test-Path -LiteralPath $candidate) { return $candidate }
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-WinUtilInstallPSProfile {
|
||||
if (-not (Get-Command wt)) {
|
||||
Write-Host "Windows Terminal not found. Installing..."
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs the CTT PowerShell profile
|
||||
|
||||
.DESCRIPTION
|
||||
The profile targets PowerShell 7, so its setup script has to run under pwsh rather than
|
||||
the runspace this job is on. It runs as a child process with its output captured, so the
|
||||
job log records what happened instead of it scrolling past in a terminal nobody kept.
|
||||
#>
|
||||
|
||||
$pwshPath = Get-WinUtilPowerShell7Path
|
||||
if (-not $pwshPath) {
|
||||
Step-WinUtilJob -Status "Installing PowerShell 7" -State "Indeterminate"
|
||||
Write-WinUtilLog -Component "Feature" -Message "PowerShell 7 not found, installing it first."
|
||||
|
||||
Install-WinUtilWinget
|
||||
winget install Microsoft.WindowsTerminal --source winget --silent
|
||||
Install-WinUtilProgramWinget -Action Install -Programs @("Microsoft.PowerShell") | Out-Null
|
||||
|
||||
# WinGet updates the persisted PATH, not this already-running process. Resolve the
|
||||
# standard install locations as well as the current PATH before deciding it failed.
|
||||
$pwshPath = Get-WinUtilPowerShell7Path
|
||||
if (-not $pwshPath) {
|
||||
throw "PowerShell 7 could not be installed, so the profile cannot be set up."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Get-Command pwsh)) {
|
||||
Write-Host "PowerShell 7 not found. Installing..."
|
||||
Install-WinUtilWinget
|
||||
winget install Microsoft.PowerShell --source winget --installer-type wix --silent
|
||||
Step-WinUtilJob -Status "Running the profile setup" -State "Indeterminate"
|
||||
|
||||
$setupUrl = "https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1"
|
||||
# Stop in the child, so a setup failure is a nonzero exit rather than a logged error and a
|
||||
# exit code of zero
|
||||
$output = & $pwshPath -NoProfile -NonInteractive -Command "`$ErrorActionPreference = 'Stop'; irm '$setupUrl' | iex" 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
$failures = 0
|
||||
foreach ($line in @($output)) {
|
||||
if ($line -is [System.Management.Automation.ErrorRecord]) {
|
||||
$failures++
|
||||
Write-WinUtilErrorRecord -ErrorRecord $line -Component "Feature" -Context "PowerShell profile setup"
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
Write-WinUtilLog -Component "Feature" -Message ([string]$line).Trim()
|
||||
}
|
||||
}
|
||||
|
||||
wt new-tab pwsh -NoExit -Command "irm https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1 | iex"
|
||||
if ($exitCode -ne 0) {
|
||||
throw "The profile setup script exited with code $exitCode."
|
||||
}
|
||||
|
||||
if ($failures -gt 0) {
|
||||
throw "The profile setup script reported $failures error(s); see the log."
|
||||
}
|
||||
|
||||
Write-WinUtilLog -Component "Feature" -Message "CTT PowerShell profile installed. Open a new PowerShell 7 session to use it."
|
||||
}
|
||||
```
|
||||
|
||||
+31
-5
@@ -10,13 +10,39 @@ This page is generated from [`functions/private/Invoke-WinUtilUninstallPSProfile
|
||||
|
||||
```powershell title="functions/private/Invoke-WinUtilUninstallPSProfile.ps1"
|
||||
function Invoke-WinUtilUninstallPSProfile {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restores the PowerShell 7 profile the CTT profile replaced
|
||||
|
||||
if (Test-Path ($Profile + ".bak")) {
|
||||
Move-Item -Path ($Profile + ".bak") -Destination $Profile
|
||||
} else {
|
||||
Remove-Item -Path $Profile
|
||||
.DESCRIPTION
|
||||
The profile path has to come from pwsh itself. $PROFILE inside this job is the worker's
|
||||
own Windows PowerShell profile, which is not the file the install wrote.
|
||||
#>
|
||||
|
||||
$pwshPath = Get-WinUtilPowerShell7Path
|
||||
if (-not $pwshPath) {
|
||||
throw "PowerShell 7 is not installed, so there is no CTT profile to remove."
|
||||
}
|
||||
|
||||
Write-Host "Successfully uninstalled CTT PowerShell Profile." -ForegroundColor Green
|
||||
$profilePath = (& $pwshPath -NoProfile -NonInteractive -Command '$PROFILE' | Select-Object -First 1)
|
||||
if ([string]::IsNullOrWhiteSpace($profilePath)) {
|
||||
throw "Could not determine the PowerShell 7 profile path."
|
||||
}
|
||||
$profilePath = $profilePath.Trim()
|
||||
$backupPath = "$profilePath.bak"
|
||||
|
||||
if (Test-Path $backupPath) {
|
||||
Move-Item -Path $backupPath -Destination $profilePath -Force
|
||||
Write-WinUtilLog -Component "Feature" -Message "Restored the profile that was in place before: $profilePath"
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path $profilePath) {
|
||||
Remove-Item -Path $profilePath -Force
|
||||
Write-WinUtilLog -Component "Feature" -Message "Removed the CTT PowerShell profile: $profilePath"
|
||||
return
|
||||
}
|
||||
|
||||
Write-WinUtilLog -Level "WARN" -Component "Feature" -Message "No PowerShell 7 profile found at $profilePath, nothing to remove."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -13,17 +13,10 @@ function Invoke-WPFSSHServer {
|
||||
<#
|
||||
|
||||
.SYNOPSIS
|
||||
Invokes the OpenSSH Server install in a runspace
|
||||
Installs and starts the OpenSSH Server
|
||||
|
||||
#>
|
||||
|
||||
Invoke-WPFRunspace -ScriptBlock {
|
||||
|
||||
Invoke-WinUtilSSHServer
|
||||
|
||||
Write-Host "======================================="
|
||||
Write-Host "-- OpenSSH Server installed! ---"
|
||||
Write-Host "======================================="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
:::
|
||||
|
||||
```json title="config/tweaks.json"
|
||||
"WPFTweaksDisableLockscreen": {
|
||||
"WPFToggleDisableLockscreen": {
|
||||
"Content": "Lock Screen - Disable",
|
||||
"Description": "Skips the lock screen entirely and goes directly to the sign-in screen on boot and wake.",
|
||||
"category": "Customize Preferences",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Microsoft Outlook New Version"
|
||||
description: "This will ensures the classic Outlook application is used."
|
||||
description: "This will ensure the new Outlook application is used."
|
||||
editUrl: false
|
||||
---
|
||||
|
||||
@@ -11,7 +11,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
```json title="config/tweaks.json"
|
||||
"WPFToggleNewOutlook": {
|
||||
"Content": "Microsoft Outlook New Version",
|
||||
"Description": "This will ensures the classic Outlook application is used.",
|
||||
"Description": "This will ensure the new Outlook application is used.",
|
||||
"category": "Customize Preferences",
|
||||
"panel": "2",
|
||||
"Type": "Toggle",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Activity History - Disable"
|
||||
description: "Erases recent docs, clipboard, and run history."
|
||||
description: "Stops Windows from publishing or uploading user activities while preserving clipboard history."
|
||||
editUrl: false
|
||||
---
|
||||
|
||||
@@ -11,14 +11,14 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
```json title="config/tweaks.json"
|
||||
"WPFTweaksActivity": {
|
||||
"Content": "Activity History - Disable",
|
||||
"Description": "Erases recent docs, clipboard, and run history.",
|
||||
"Description": "Stops Windows from publishing or uploading user activities while preserving clipboard history.",
|
||||
"category": "Essential Tweaks",
|
||||
"panel": "1",
|
||||
"registry": [
|
||||
{
|
||||
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\System",
|
||||
"Name": "EnableActivityFeed",
|
||||
"Value": "0",
|
||||
"Value": "1",
|
||||
"Type": "DWord",
|
||||
"OriginalValue": "<RemoveEntry>"
|
||||
},
|
||||
|
||||
@@ -16,8 +16,10 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
"panel": "1",
|
||||
"InvokeScript": [
|
||||
"
|
||||
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force
|
||||
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force
|
||||
# A temp folder always holds files something has open, including this run's own, and
|
||||
# the job layer counts a logged error as a failed step
|
||||
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
"category": "Essential Tweaks",
|
||||
"panel": "1",
|
||||
"InvokeScript": [
|
||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny Everyone:F"
|
||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny *S-1-1-0:F"
|
||||
],
|
||||
"UndoScript": [
|
||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant Everyone:F"
|
||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant *S-1-1-0:F"
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "End Task With Right Click - Enable"
|
||||
description: "Enables option to end task when right clicking a program in the taskbar."
|
||||
description: "Enables option to end task when right-clicking a program in the taskbar."
|
||||
editUrl: false
|
||||
---
|
||||
|
||||
@@ -11,7 +11,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
```json title="config/tweaks.json"
|
||||
"WPFTweaksEndTaskOnTaskbar": {
|
||||
"Content": "End Task With Right Click - Enable",
|
||||
"Description": "Enables option to end task when right clicking a program in the taskbar.",
|
||||
"Description": "Enables option to end task when right-clicking a program in the taskbar.",
|
||||
"category": "Essential Tweaks",
|
||||
"panel": "1",
|
||||
"registry": [
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "Logitech Download Assistant Auto-Install - Disable"
|
||||
description: "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it."
|
||||
editUrl: false
|
||||
---
|
||||
|
||||
:::note
|
||||
This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitusTech/winutil/blob/main/config/tweaks.json). Do not edit this page directly.
|
||||
:::
|
||||
|
||||
```json title="config/tweaks.json"
|
||||
"WPFTweaksLogiBlock": {
|
||||
"Content": "Logitech Download Assistant Auto-Install - Disable",
|
||||
"Description": "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it.",
|
||||
"category": "z__Advanced Tweaks - CAUTION",
|
||||
"panel": "1",
|
||||
"InvokeScript": [
|
||||
"
|
||||
Stop-Process -Name \"logi_download_assistant\" -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
|
||||
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
|
||||
|
||||
if (Test-Path $LogiPath) {
|
||||
Remove-Item $LogiPath\\* -Recurse -Force
|
||||
} else {
|
||||
New-Item -Path $LogiPath -ItemType Directory
|
||||
}
|
||||
|
||||
icacls $LogiPath /deny \"*S-1-1-0:(W)\"
|
||||
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to deny write access on $LogiPath (exit code $LASTEXITCODE)\" }
|
||||
"
|
||||
],
|
||||
"UndoScript": [
|
||||
"
|
||||
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
|
||||
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
|
||||
|
||||
if (Test-Path $LogiPath) {
|
||||
icacls $LogiPath /remove:d \"*S-1-1-0\"
|
||||
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to remove the write-deny rule on $LogiPath (exit code $LASTEXITCODE)\" }
|
||||
}
|
||||
"
|
||||
],
|
||||
}
|
||||
```
|
||||
+8
-40
@@ -10,53 +10,21 @@ This page is generated from [`functions/public/Invoke-WPFOOSU.ps1`](https://gith
|
||||
|
||||
```powershell title="functions/public/Invoke-WPFOOSU.ps1"
|
||||
function Invoke-WPFOOSU {
|
||||
if ($sync.ProcessRunning) {
|
||||
Show-WinUtilMessage -Message "Another process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
|
||||
return
|
||||
}
|
||||
Start-WinUtilJob -Name "OOSU" -Description "Downloading O&O ShutUp10++" -Parameters @{
|
||||
DownloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
|
||||
} -ScriptBlock {
|
||||
param($DownloadPath)
|
||||
|
||||
$downloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
|
||||
$sync.ProcessRunning = $true
|
||||
|
||||
Invoke-WPFRunspace -ParameterList @(,("downloadPath", $downloadPath)) -ScriptBlock {
|
||||
param($downloadPath)
|
||||
|
||||
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
|
||||
|
||||
try {
|
||||
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ (0%)" -Percent 0
|
||||
}
|
||||
|
||||
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $downloadPath -ProgressCallback {
|
||||
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $DownloadPath -ProgressCallback {
|
||||
param($percent)
|
||||
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
|
||||
}
|
||||
Step-WinUtilJob -Status "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
|
||||
}
|
||||
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Launching O&O ShutUp10++" -Percent 100
|
||||
}
|
||||
Start-Process -FilePath $downloadPath
|
||||
|
||||
Step-WinUtilJob -Status "Launching O&O ShutUp10++" -Percent 100
|
||||
Start-Process -FilePath $DownloadPath
|
||||
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ launched" -Percent 100
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-WinUtilLog -Level "ERROR" -Component "OOSU" -Message "O&O ShutUp10++ download failed: $($_.Exception.Message)"
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ download failed" -Percent 100
|
||||
}
|
||||
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
|
||||
}
|
||||
finally {
|
||||
$sync.ProcessRunning = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+2
-2
@@ -40,12 +40,12 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
New-Item -Path $RazerPath -ItemType Directory
|
||||
}
|
||||
|
||||
icacls $RazerPath /deny \"Everyone:(W)\"
|
||||
icacls $RazerPath /deny \"*S-1-1-0:(W)\"
|
||||
"
|
||||
],
|
||||
"UndoScript": [
|
||||
"
|
||||
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d Everyone
|
||||
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d *S-1-1-0
|
||||
"
|
||||
],
|
||||
}
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
"InvokeScript": [
|
||||
"
|
||||
# Deny permission to remove OneDrive folder
|
||||
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
|
||||
icacls $Env:OneDrive /deny \"*S-1-5-32-544:(D,DC)\"
|
||||
|
||||
Write-Host \"Uninstalling OneDrive...\"
|
||||
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
|
||||
@@ -31,7 +31,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
Remove-Item \"$Env:ProgramData\\Microsoft OneDrive\" -Recurse -Force
|
||||
|
||||
# Grant back permission to access OneDrive folder
|
||||
icacls $Env:OneDrive /grant \"Administrators:(D,DC)\"
|
||||
icacls $Env:OneDrive /grant \"*S-1-5-32-544:(D,DC)\"
|
||||
|
||||
if (-not (Get-ChildItem -Path $Env:OneDrive)) {
|
||||
Remove-Item -Path $Env:OneDrive -Recurse
|
||||
|
||||
+1
-1
@@ -14,6 +14,6 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
||||
"category": "z__Advanced Tweaks - CAUTION",
|
||||
"panel": "1",
|
||||
"Type": "Combobox",
|
||||
"ComboItems": "Default DHCP Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult Mullvad Mullvad_Ads_Trackers Mullvad_Ads_Trackers_Malware Mullvad_Ads_Trackers_Malware_Social Mullvad_Ads_Trackers_Malware_Adult_Gambling Mullvad_Ads_Trackers_Malware_Adult_Gambling_Social",
|
||||
"ComboItems": "Default DHCP Fastest Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -230,7 +230,7 @@ Yes, WinUtil works on Windows Server editions, though some features may not be a
|
||||
|
||||
### Does WinUtil work with Windows LTSC?
|
||||
|
||||
Yes, WinUtil works with Windows 10/11 LTSC editions. Some applications may not be available depending on your configuration.
|
||||
WinUtil currently targets Windows 11. Windows 10 LTSC is not supported, consistent with the Windows 10 support policy above. Some features and applications may vary by Windows 11 edition.
|
||||
|
||||
### Can I use WinUtil in a corporate/enterprise environment?
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Use the quick-selection buttons at the top of the Tweaks tab to speed up setup:
|
||||
* **Minimal**: Selects a smaller, lower-impact set of common tweaks.
|
||||
* **Advanced**: Selects a focused set of safer advanced tweaks. This preset intentionally skips restore point creation and cleanup tasks to avoid a long runtime.
|
||||
* **Clear**: Clears all currently selected tweaks.
|
||||
* **Get Installed Tweaks**: Best-effort detection for tweaks already applied to your system.
|
||||
* **Select Installed Tweaks**: Best-effort detection for tweaks already applied to your system.
|
||||
|
||||
### Run Tweaks
|
||||
* **Open the Tweaks tab**: Navigate to the **Tweaks** tab in the application.
|
||||
|
||||
@@ -27,7 +27,7 @@ Changing modes adjusts system-wide Windows Update behavior. After switching mode
|
||||
- **Feature updates**: Delayed by **365 days** to reduce the chance of disruption from major Windows changes.
|
||||
- **Quality updates**: Delayed by **4 days** to allow time for early issues to surface while still keeping the system protected.
|
||||
- **Drivers**: Excluded from Windows quality updates.
|
||||
- **Restarts**: Scheduled updates do not automatically restart Windows while a user is signed in. A restart explicitly scheduled by a user still takes precedence.
|
||||
- **Installation**: Updates download automatically and notify you when they are ready to install. This setting does not control restarts after installation.
|
||||
- **Availability**: Update deferral policies apply to Windows Pro, Enterprise, and Education editions.
|
||||
- **Why use it**: This mode offers the best balance between security and stability, which is why it is the recommended option for most PCs.
|
||||
|
||||
|
||||
@@ -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,12 +138,13 @@ 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` |
|
||||
| DISM error with an explanation in parentheses, e.g. "exit code 112 (Disk is full)" | WinUtil now explains common DISM failures in plain language. Free up disk space for a "Disk is full" error, or run WinUtil as Administrator for an "Access denied" error. If the code has no explanation, search the number on Microsoft's [DISM error code reference](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes) |
|
||||
| "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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@ function Get-WinUtilTweaksStateReport {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Groups every config/tweaks.json entry's live applied state by category, reusing the same
|
||||
detection Invoke-WPFGetInstalled uses to check the "Get Installed Tweaks" checkboxes.
|
||||
detection Invoke-WPFGetInstalled uses to check the "Select Installed Tweaks" checkboxes.
|
||||
#>
|
||||
|
||||
$categoryFieldNames = [ordered]@{
|
||||
|
||||
@@ -356,7 +356,8 @@ function Invoke-WinUtilISOModify {
|
||||
Invoke-WPFUIThread -ScriptBlock {
|
||||
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
|
||||
}
|
||||
Set-WinUtilISOStep -Step "Working" -Label "Modifying install.wim"
|
||||
$workingLabel = if ($InjectDrivers) { "Modifying install.wim" } else { "Preparing setup media" }
|
||||
Set-WinUtilISOStep -Step "Working" -Label $workingLabel
|
||||
|
||||
$modified = $false
|
||||
try {
|
||||
@@ -401,7 +402,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."
|
||||
|
||||
@@ -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 {
|
||||
@@ -206,6 +185,21 @@ function Invoke-WinUtilISOScript {
|
||||
return @($survivingFolders)
|
||||
}
|
||||
|
||||
$knownExitCode = @{
|
||||
112 = "Disk is full"
|
||||
5 = "Access denied"
|
||||
2 = "File not found"
|
||||
3 = "Path not found"
|
||||
87 = "Invalid parameter"
|
||||
1168 = "Element not found"
|
||||
1392 = "File or directory is corrupted"
|
||||
32 = "File in use / sharing violation"
|
||||
21 = "Device not ready"
|
||||
1460 = "Operation timed out"
|
||||
1223 = "Operation cancelled by user"
|
||||
50 = "Request not supported"
|
||||
}
|
||||
|
||||
function Invoke-WinUtilISODism {
|
||||
param (
|
||||
[Parameter(Mandatory)][string[]]$Arguments,
|
||||
@@ -220,8 +214,12 @@ function Invoke-WinUtilISOScript {
|
||||
& $Logger " dism[$Operation]: $line"
|
||||
}
|
||||
}
|
||||
if ($knownExitCode.ContainsKey($exitCode)) {
|
||||
throw "DISM $Operation failed with exit code $exitCode ($($knownExitCode[$exitCode]))."
|
||||
} else {
|
||||
throw "DISM $Operation failed with exit code $exitCode."
|
||||
}
|
||||
}
|
||||
if ($Operation -ne 'metadata') {
|
||||
& $Logger "DISM $Operation completed."
|
||||
}
|
||||
@@ -267,6 +265,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 +372,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 +381,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 +411,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) })
|
||||
})
|
||||
$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.'
|
||||
}
|
||||
}
|
||||
|
||||
& $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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
$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 +448,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
|
||||
@@ -532,7 +557,6 @@ $appxList
|
||||
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'
|
||||
|
||||
@@ -23,6 +23,9 @@ function Invoke-WinUtilTweaks {
|
||||
|
||||
$action = if ($undo) { "Undo" } else { "Apply" }
|
||||
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak: $CheckBox"
|
||||
# The counter lives in this runspace, so an error a concurrent job logs from its own
|
||||
# runspace cannot be charged to a toggle flipped on the UI thread
|
||||
$errorsBefore = [int]$global:WinUtilJobErrorCount
|
||||
|
||||
if ($undo) {
|
||||
$Values = @{
|
||||
@@ -81,5 +84,10 @@ function Invoke-WinUtilTweaks {
|
||||
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
|
||||
}
|
||||
}
|
||||
$errorCount = [int]$global:WinUtilJobErrorCount - $errorsBefore
|
||||
if ($errorCount -gt 0) {
|
||||
Write-WinUtilLog -Level "WARN" -Component "Tweaks" -Message "$action tweak finished with $errorCount error(s): $CheckBox"
|
||||
} else {
|
||||
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ function Write-WinUtilLog {
|
||||
$null = $sync.LoggedErrors.Add("[$Component] $Message")
|
||||
}
|
||||
|
||||
if ($Level -eq "ERROR" -and -not $Detail -and $global:WinUtilIsJobWorker) {
|
||||
# Global scope is per runspace, so this counter only ever sees errors logged by the
|
||||
# runspace that owns it: a job worker reads its own, and a tweak on the UI thread reads the
|
||||
# UI thread's
|
||||
if ($Level -eq "ERROR" -and -not $Detail) {
|
||||
$global:WinUtilJobErrorCount++
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ function Invoke-WPFUpdatessecurity {
|
||||
1. Disables driver offering through Windows Update
|
||||
2. Defers feature updates for 365 days
|
||||
3. Defers quality updates for 4 days
|
||||
4. Prevents automatic restarts while a user is signed in
|
||||
4. Configures automatic updates to notify when downloaded updates are ready to install
|
||||
|
||||
#>
|
||||
|
||||
@@ -67,13 +67,16 @@ function Invoke-WPFUpdatessecurity {
|
||||
Remove-ItemProperty -Path $legacySettingsPath -Name $legacyValue -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Preventing automatic restarts while users are signed in..."
|
||||
Write-WinUtilLog -Component "Updates" -Message "Configuring scheduled automatic updates without restarting while users are signed in."
|
||||
Write-Host "Configuring automatic updates to download and notify before installation..."
|
||||
Write-WinUtilLog -Component "Updates" -Message "Configuring automatic updates to download and notify before installation."
|
||||
|
||||
New-Item -Path $automaticUpdatePolicyPath -Force
|
||||
# NoAutoRebootWithLoggedOnUsers only applies when automatic updates use option 4.
|
||||
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUOptions" -Type DWord -Value 4
|
||||
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
|
||||
|
||||
# Remove the previous scheduled-install reboot policy when switching to download-and-notify.
|
||||
Remove-ItemProperty -Path $automaticUpdatePolicyPath -Name "NoAutoRebootWithLoggedOnUsers" -ErrorAction SilentlyContinue
|
||||
|
||||
# AUOptions 3 downloads updates and notifies before installation; it does not control restarts.
|
||||
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUOptions" -Type DWord -Value 3
|
||||
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUPowerManagement" -Type DWord -Value 0
|
||||
|
||||
Write-WinUtilLog -Component "Updates" -Message "Recommended Windows Update settings workflow completed."
|
||||
|
||||
@@ -131,7 +131,7 @@ Describe "Write-WinUtilLog" {
|
||||
Should -Invoke -CommandName Write-Warning -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "counts only headline errors written by the active job worker" {
|
||||
It "counts headline errors in the logging runspace whether or not it is a job worker" {
|
||||
$script:sync = [hashtable]::Synchronized(@{
|
||||
winutildir = $script:testRoot
|
||||
LoggedErrors = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
||||
@@ -142,9 +142,9 @@ Describe "Write-WinUtilLog" {
|
||||
Write-WinUtilLog -Level "ERROR" -Component "Test" -Message "job error"
|
||||
Write-WinUtilLog -Level "ERROR" -Detail -Component "Test" -Message "error detail"
|
||||
$global:WinUtilIsJobWorker = $false
|
||||
Write-WinUtilLog -Level "ERROR" -Component "UI" -Message "unrelated error"
|
||||
Write-WinUtilLog -Level "ERROR" -Component "UI" -Message "toggle error"
|
||||
|
||||
$global:WinUtilJobErrorCount | Should -Be 1
|
||||
$global:WinUtilJobErrorCount | Should -Be 2
|
||||
$script:sync.LoggedErrors.Count | Should -Be 2
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,87 @@ Describe "Invoke-WinUtilTweaks" {
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-WinUtilTweaks completion status" {
|
||||
BeforeAll {
|
||||
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilScript.ps1")
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
$script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "winutil-tweaks-$([guid]::NewGuid())"
|
||||
$script:logPath = Join-Path $script:testRoot "logs\winutil_2026-09-15_12-00-00.log"
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
logPath = $script:logPath
|
||||
configs = @{
|
||||
tweaks = [pscustomobject]@{
|
||||
WPFTweaksFailing = [pscustomobject]@{
|
||||
InvokeScript = @("throw 'simulated icacls failure'")
|
||||
UndoScript = @("throw 'simulated icacls undo failure'")
|
||||
}
|
||||
WPFTweaksClean = [pscustomobject]@{
|
||||
InvokeScript = @("Write-Output 'apply tweak'")
|
||||
}
|
||||
# A job worker logging an error from its own runspace lands in the shared
|
||||
# list without passing through this runspace's logger
|
||||
WPFTweaksDuringJob = [pscustomobject]@{
|
||||
InvokeScript = @("`$null = `$sync.LoggedErrors.Add('[Job] error from a concurrent job'); Write-Output 'apply tweak'")
|
||||
}
|
||||
}
|
||||
}
|
||||
# Seeded with an earlier error: only errors logged during this tweak may count
|
||||
LoggedErrors = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new(@("[UI] earlier unrelated failure")))
|
||||
})
|
||||
# Toggle switches run the tweak on the UI thread, outside any job worker. The runspace
|
||||
# counter starts non-zero: only errors logged during this tweak may count
|
||||
Remove-Variable -Name WinUtilIsJobWorker -Scope Global -ErrorAction SilentlyContinue
|
||||
$global:WinUtilJobErrorCount = 3
|
||||
|
||||
Mock Write-Host { }
|
||||
Mock Write-Warning { }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name WinUtilJobErrorCount -Scope Global -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "warns instead of reporting completion when a tweak step logged an error" {
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksFailing"
|
||||
|
||||
$log = Get-Content -Path $script:logPath -Raw
|
||||
$log | Should -Match "\[ERROR\] \[Script\] Runtime exception while running script for WPFTweaksFailing"
|
||||
$log | Should -Match "\[WARN\] \[Tweaks\] Apply tweak finished with 1 error\(s\): WPFTweaksFailing"
|
||||
$log | Should -Not -Match "tweak completed: WPFTweaksFailing"
|
||||
}
|
||||
|
||||
It "warns when an undo step logged an error" {
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksFailing" -undo $true
|
||||
|
||||
$log = Get-Content -Path $script:logPath -Raw
|
||||
$log | Should -Match "\[ERROR\] \[Script\] Runtime exception while running script for WPFTweaksFailing"
|
||||
$log | Should -Match "\[WARN\] \[Tweaks\] Undo tweak finished with 1 error\(s\): WPFTweaksFailing"
|
||||
$log | Should -Not -Match "tweak completed: WPFTweaksFailing"
|
||||
}
|
||||
|
||||
It "reports completion when every tweak step succeeded" {
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksClean"
|
||||
|
||||
$log = Get-Content -Path $script:logPath -Raw
|
||||
$log | Should -Match "\[INFO\] \[Tweaks\] Apply tweak completed: WPFTweaksClean"
|
||||
$log | Should -Not -Match "\[WARN\]"
|
||||
$log | Should -Not -Match "\[ERROR\]"
|
||||
}
|
||||
|
||||
It "ignores an error another runspace logged while the tweak ran" {
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksDuringJob"
|
||||
|
||||
$log = Get-Content -Path $script:logPath -Raw
|
||||
$log | Should -Match "\[INFO\] \[Tweaks\] Apply tweak completed: WPFTweaksDuringJob"
|
||||
$log | Should -Not -Match "tweak finished with"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-WPFtweaksbutton" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
|
||||
@@ -333,7 +333,7 @@ Describe "Invoke-WPFUpdatessecurity" {
|
||||
}
|
||||
}
|
||||
|
||||
It "sets recommended update deferral and auto-reboot policy values" {
|
||||
It "sets recommended update deferral and installation notification policy values" {
|
||||
Invoke-WPFUpdatessecurity
|
||||
|
||||
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
@@ -360,17 +360,15 @@ Describe "Invoke-WPFUpdatessecurity" {
|
||||
$Type -eq "DWord" -and
|
||||
$Value -eq 4
|
||||
}
|
||||
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
|
||||
$Name -eq "NoAutoRebootWithLoggedOnUsers"
|
||||
}
|
||||
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
|
||||
$Name -eq "AUOptions" -and
|
||||
$Type -eq "DWord" -and
|
||||
$Value -eq 4
|
||||
}
|
||||
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
|
||||
$Name -eq "NoAutoRebootWithLoggedOnUsers" -and
|
||||
$Type -eq "DWord" -and
|
||||
$Value -eq 1
|
||||
$Value -eq 3
|
||||
}
|
||||
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
|
||||
|
||||
+284
-19
@@ -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
|
||||
@@ -867,6 +950,109 @@ Describe "Win11 Creator setup media" {
|
||||
}
|
||||
}
|
||||
|
||||
It "reports a friendly explanation when DISM fails with a known exit code" {
|
||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailureKnownCode_$([guid]::NewGuid())"
|
||||
$installWim = Join-Path $contentRoot 'sources\install.wim'
|
||||
$template = Get-Content -Path $script:autoUnattendPath -Raw
|
||||
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
function dism.exe {
|
||||
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
|
||||
|
||||
$script:dismCalls.Add(($Arguments -join '|'))
|
||||
if ($Arguments -contains '/Get-WimInfo') {
|
||||
$global:LASTEXITCODE = 0
|
||||
'Languages : en-US'
|
||||
'Installation : Client'
|
||||
'Edition : Professional'
|
||||
'ProductSuite : Terminal Server'
|
||||
'ProductType : WinNT'
|
||||
} elseif ($Arguments -contains '/Mount-Image') {
|
||||
$global:LASTEXITCODE = 112
|
||||
'Mount failed'
|
||||
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
|
||||
$global:LASTEXITCODE = 0
|
||||
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
|
||||
} elseif ($Arguments -contains '/Export-Driver') {
|
||||
$global:LASTEXITCODE = 0
|
||||
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
|
||||
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
|
||||
)
|
||||
} else {
|
||||
$global:LASTEXITCODE = 0
|
||||
}
|
||||
}
|
||||
|
||||
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 '*112*Disk is full*'
|
||||
|
||||
@($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 "falls back to the plain exit code for an unmapped DISM failure" {
|
||||
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailureUnknownCode_$([guid]::NewGuid())"
|
||||
$installWim = Join-Path $contentRoot 'sources\install.wim'
|
||||
$template = Get-Content -Path $script:autoUnattendPath -Raw
|
||||
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
function dism.exe {
|
||||
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
|
||||
|
||||
$script:dismCalls.Add(($Arguments -join '|'))
|
||||
if ($Arguments -contains '/Get-WimInfo') {
|
||||
$global:LASTEXITCODE = 0
|
||||
'Languages : en-US'
|
||||
'Installation : Client'
|
||||
'Edition : Professional'
|
||||
'ProductSuite : Terminal Server'
|
||||
'ProductType : WinNT'
|
||||
} elseif ($Arguments -contains '/Mount-Image') {
|
||||
$global:LASTEXITCODE = 999
|
||||
'Mount failed'
|
||||
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
|
||||
$global:LASTEXITCODE = 0
|
||||
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
|
||||
} elseif ($Arguments -contains '/Export-Driver') {
|
||||
$global:LASTEXITCODE = 0
|
||||
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
|
||||
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
|
||||
)
|
||||
} else {
|
||||
$global:LASTEXITCODE = 0
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
|
||||
Set-Content -Path $installWim -Value 'mock-wim'
|
||||
. $script:isoScriptPath
|
||||
|
||||
$thrown = $null
|
||||
try {
|
||||
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional'
|
||||
} catch {
|
||||
$thrown = $_.Exception.Message
|
||||
}
|
||||
$thrown | Should -Be 'DISM mount failed with exit code 999.'
|
||||
|
||||
@($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())"
|
||||
|
||||
@@ -934,4 +1120,83 @@ Describe "Win11 Creator setup media" {
|
||||
$exportRunIndex | Should -BeGreaterThan $exportDialogIndex
|
||||
$script:exportFunction | Should -Match ([regex]::Escape('return'))
|
||||
}
|
||||
|
||||
Context "FirstLogon update service restoration" {
|
||||
BeforeAll {
|
||||
[xml]$unattend = Get-Content -LiteralPath $script:autoUnattendPath -Raw
|
||||
$firstLogon = $unattend.SelectSingleNode("//*[local-name()='File' and @path='C:\Windows\Setup\Scripts\FirstLogon.ps1']").InnerText
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseInput($firstLogon, [ref]$tokens, [ref]$parseErrors)
|
||||
if ($parseErrors.Count) { throw 'FirstLogon script failed to parse.' }
|
||||
$blocks = @($ast.FindAll({
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -and
|
||||
$node.ScriptBlock.Find({
|
||||
param($command)
|
||||
$command -is [System.Management.Automation.Language.CommandAst] -and
|
||||
$command.GetCommandName() -eq 'Set-Service'
|
||||
}, $false)
|
||||
}, $true))
|
||||
if ($blocks.Count -ne 1) { throw 'Expected exactly one service restoration block.' }
|
||||
# Never execute the surrounding FirstLogon cleanup, downloads, or installer.
|
||||
$commands = $blocks[0].ScriptBlock.FindAll({
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.CommandAst]
|
||||
}, $true)
|
||||
foreach ($command in $commands) {
|
||||
if ($command.GetCommandName() -notin @('reg.exe', 'Set-Service', 'Set-ItemProperty')) {
|
||||
throw "Unexpected command in restoration block: $($command.Extent.Text)"
|
||||
}
|
||||
}
|
||||
$script:restoreServices = $blocks[0].ScriptBlock.GetScriptBlock()
|
||||
function reg.exe { param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments) }
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
Mock reg.exe { }
|
||||
Mock Set-Service { }
|
||||
Mock Set-ItemProperty { }
|
||||
}
|
||||
|
||||
It "restores ordinary services and writes the protected Medic startup value directly" {
|
||||
& $script:restoreServices
|
||||
|
||||
Should -Invoke Set-Service -Times 3 -Exactly
|
||||
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'BITS' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' }
|
||||
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'wuauserv' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' }
|
||||
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'UsoSvc' -and $StartupType -eq 'Automatic' -and $ErrorAction -eq 'Continue' }
|
||||
Should -Invoke Set-Service -Times 0 -Exactly -ParameterFilter { $Name -eq 'WaaSMedicSvc' }
|
||||
Should -Invoke Set-ItemProperty -Times 1 -Exactly
|
||||
Should -Invoke Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -and
|
||||
$Name -eq 'Start' -and $Value -eq 3 -and $Type -eq 'DWord' -and $ErrorAction -eq 'Continue'
|
||||
}
|
||||
}
|
||||
|
||||
It "keeps failures observable while attempting the remaining restoration work" {
|
||||
$script:restorationCalls = [System.Collections.Generic.List[string]]::new()
|
||||
Mock Set-Service {
|
||||
param($Name, $ErrorAction)
|
||||
$script:restorationCalls.Add($Name)
|
||||
if ($script:restorationCalls.Count -eq 1) {
|
||||
Write-Error 'simulated service restoration failure' -ErrorAction $ErrorAction
|
||||
}
|
||||
}
|
||||
Mock Set-ItemProperty {
|
||||
param($ErrorAction)
|
||||
$script:restorationCalls.Add('Medic registry')
|
||||
Write-Error 'simulated Medic registry failure' -ErrorAction $ErrorAction
|
||||
}
|
||||
|
||||
$output = @(& $script:restoreServices 2>&1)
|
||||
$failures = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
|
||||
$failures.Count | Should -Be 2
|
||||
$failures[0].Exception.Message | Should -Be 'simulated service restoration failure'
|
||||
$failures[1].Exception.Message | Should -Be 'simulated Medic registry failure'
|
||||
$script:restorationCalls.Count | Should -Be 4
|
||||
@($script:restorationCalls | Select-Object -First 3 | Sort-Object) | Should -Be @('BITS', 'UsoSvc', 'wuauserv')
|
||||
$script:restorationCalls[3] | Should -Be 'Medic registry'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +453,11 @@ $scripts = @(
|
||||
reg.exe delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" /v DODownloadMode /f;
|
||||
reg.exe add "HKLM\Software\Policies\Microsoft\Windows\OneDrive" /v DisableFileSyncNGSC /t REG_DWORD /d 0 /f;
|
||||
reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\GameDVR" /v AppCaptureEnabled /t REG_DWORD /d 0 /f;
|
||||
$services = @{ BITS = 'Manual'; wuauserv = 'Manual'; UsoSvc = 'Automatic'; WaaSMedicSvc = 'Manual' };
|
||||
$services = @{ BITS = 'Manual'; wuauserv = 'Manual'; UsoSvc = 'Automatic' };
|
||||
foreach ($name in $services.Keys) {
|
||||
Set-Service -Name $name -StartupType $services[$name] -ErrorAction SilentlyContinue;
|
||||
Set-Service -Name $name -StartupType $services[$name] -ErrorAction Continue;
|
||||
}
|
||||
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -Name 'Start' -Value 3 -Type DWord -ErrorAction Continue;
|
||||
};
|
||||
{
|
||||
reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Education" /f;
|
||||
|
||||
+8
-8
@@ -1423,7 +1423,7 @@
|
||||
<Button Name="WPFminimal" Content=" Minimal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFGetInstalledTweaks" Content=" Select Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
@@ -1503,7 +1503,7 @@
|
||||
<TextBlock Text="- Defers feature updates for 365 days" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
|
||||
<TextBlock Text="- Defers quality updates for 4 days" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
|
||||
<TextBlock Text="- Excludes drivers from quality updates" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
|
||||
<TextBlock Text="- Prevents automatic restarts while a user is signed in" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
|
||||
<TextBlock Text="- Notifies when downloaded updates are ready to install" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
|
||||
<TextBlock Text="Available on Windows Pro, Enterprise, and Education editions."
|
||||
FontSize="11"
|
||||
FontStyle="Italic"
|
||||
@@ -1966,7 +1966,7 @@
|
||||
Foreground="{DynamicResource MainForegroundColor}"
|
||||
IsChecked="False"
|
||||
Cursor="Hand"
|
||||
ToolTip="Stages boot-storage drivers for Setup and adds all exported drivers to the selected install.wim edition in one DISM pass."/>
|
||||
ToolTip="Injects boot-storage drivers for Setup and adds exported drivers to the selected install.wim edition individually, skipping incompatible packages."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -2215,7 +2215,7 @@
|
||||
<Label Content="Selections:" FontSize="{DynamicResource FontSize}" VerticalAlignment="Center" Margin="2"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,2,0,0">
|
||||
<Button Name="WPFDefaultAppxSelection" Content=" Default " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFGetInstalledAppx" Content=" Get Installed " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFGetInstalledAppx" Content=" Select Installed " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFSelectAllAppx" Content=" Select All " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
<Button Name="WPFClearAppxSelection" Content=" Clear Selection " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||
</StackPanel>
|
||||
@@ -2225,13 +2225,13 @@
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2" Style="{StaticResource BorderStyle}" Margin="5,15,5,5">
|
||||
<StackPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<WrapPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<TextBlock Padding="10" TextWrapping="Wrap" Foreground="{DynamicResource MainForegroundColor}">
|
||||
Note: Select the Windows AppX packages you wish to install or remove.
|
||||
<LineBreak/>Install Selected registers a local manifest when available, then falls back to the Microsoft Store.
|
||||
<LineBreak/>Remove Selected removes packages for the current user and all new user profiles.
|
||||
<LineBreak/>'Install Selected' registers a local manifest for the current user when available, then falls back to the Microsoft Store.
|
||||
<LineBreak/>'Remove Selected' removes packages for all users and stops new profiles from getting them by default.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
Reference in New Issue
Block a user