<# .SYNOPSIS Prepares a Windows 11 Pro reference machine and creates a generalized (sysprepped) WIM image for deployment. .DESCRIPTION This script must be run as Administrator on the reference machine. It operates in two modes: -Mode Sysprep (default) 1. Optionally removes provisioned AppX packages that should not ship in the image. 2. Runs Disk Cleanup / component store cleanup (DISM /StartComponentCleanup) to shrink the image. 3. Generates an unattend.xml (if one is not supplied) that skips OOBE prompts and re-seals the OS. 4. Runs `sysprep.exe /oobe /generalize /shutdown /unattend:`, which shuts the machine down when done. After the machine shuts down, boot it from a WinPE / Windows PE media (e.g. from a WDS, MDT, or a USB WinPE stick) to actually capture the disk, since Windows cannot capture its own running volume. -Mode Capture Captures the generalized OS volume into a .wim file using DISM. This must be run from WinPE (or another OS) where the sysprepped Windows partition is NOT the running OS - i.e. boot the reference machine from WinPE media after sysprep has shut it down, then run this script (or copy it onto the WinPE media) with -Mode Capture. .PARAMETER Mode 'Sysprep' to generalize the reference machine, or 'Capture' to capture the offline volume to a WIM. .PARAMETER UnattendPath Path to an unattend.xml to pass to sysprep. If omitted in Sysprep mode, a minimal one is generated. .PARAMETER RemoveProvisionedApps In Sysprep mode, an optional list of AppX package name patterns (wildcards allowed) to remove before sysprep, e.g. "*Xbox*","*Solitaire*". Defaults to none (nothing removed) unless specified. .PARAMETER SourceDrive In Capture mode, the drive letter of the generalized Windows volume to capture (e.g. "D:"). Must not be the currently running OS volume. .PARAMETER DestinationWim In Capture mode, the full path of the output .wim file, e.g. "D:\Images\Win11Pro.wim". .PARAMETER ImageName In Capture mode, the friendly name stored in the WIM image metadata. Defaults to "Windows 11 Pro - Deployment". .PARAMETER Compress In Capture mode, DISM compression level: "fast", "max", or "none". Defaults to "max". .EXAMPLE # On the reference machine, generalize and shut down: .\New-Win11GeneralizedImage.ps1 -Mode Sysprep -RemoveProvisionedApps "*Xbox*","*Solitaire*" .EXAMPLE # Booted into WinPE with the sysprepped volume mounted as D: and a USB drive as E:: .\New-Win11GeneralizedImage.ps1 -Mode Capture -SourceDrive D: -DestinationWim "E:\Images\Win11Pro.wim" .NOTES Run this script from an elevated PowerShell prompt: powershell -ExecutionPolicy Bypass -File .\New-Win11GeneralizedImage.ps1 -Mode Sysprep #> [CmdletBinding()] param( [ValidateSet("Sysprep", "Capture")] [string]$Mode = "Sysprep", [string]$UnattendPath, [string[]]$RemoveProvisionedApps = @(), [string]$SourceDrive, [string]$DestinationWim, [string]$ImageName = "Windows 11 Pro - Deployment", [ValidateSet("fast", "max", "none")] [string]$Compress = "max" ) $ErrorActionPreference = "Stop" function Test-IsAdmin { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Write-Step { param([string]$Message) Write-Host "`n==> $Message" -ForegroundColor Cyan } if (-not (Test-IsAdmin)) { Write-Error "This script must be run as Administrator. Right-click PowerShell and choose 'Run as Administrator', then re-run this script." exit 1 } function New-DefaultUnattendXml { param([string]$Path) $xml = @' true true true true Work 3 true true 1 '@ Set-Content -Path $Path -Value $xml -Encoding UTF8 } function Invoke-Sysprep { Write-Step "Preparing reference machine for generalization" if ($RemoveProvisionedApps.Count -gt 0) { Write-Step "Removing provisioned AppX packages matching: $($RemoveProvisionedApps -join ', ')" $provisioned = Get-AppxProvisionedPackage -Online foreach ($pattern in $RemoveProvisionedApps) { $matches = $provisioned | Where-Object { $_.DisplayName -like $pattern } foreach ($pkg in $matches) { Write-Host " Removing $($pkg.DisplayName)" -ForegroundColor DarkGray try { Remove-AppxProvisionedPackage -Online -PackageName $pkg.PackageName -ErrorAction Stop | Out-Null } catch { Write-Warning " Failed to remove $($pkg.DisplayName): $($_.Exception.Message)" } } } } Write-Step "Cleaning up the component store (this can take several minutes)" Dism.exe /Online /Cleanup-Image /StartComponentCleanup /ResetBase | Out-Null if (-not $UnattendPath) { $UnattendPath = Join-Path -Path $env:TEMP -ChildPath "unattend-generalize.xml" Write-Step "No -UnattendPath supplied, generating a default unattend file at $UnattendPath" New-DefaultUnattendXml -Path $UnattendPath } elseif (-not (Test-Path -Path $UnattendPath)) { Write-Error "The specified -UnattendPath '$UnattendPath' does not exist." exit 1 } $sysprepExe = Join-Path -Path $env:WINDIR -ChildPath "System32\Sysprep\sysprep.exe" if (-not (Test-Path -Path $sysprepExe)) { Write-Error "sysprep.exe was not found at '$sysprepExe'." exit 1 } Write-Step "Running sysprep /oobe /generalize /shutdown (the machine will power off when finished)" Write-Host "Unattend file: $UnattendPath" -ForegroundColor DarkGray & $sysprepExe /oobe /generalize /shutdown /unattend:$UnattendPath Write-Host "`nSysprep started. Windows will shut down automatically once generalization completes." -ForegroundColor Yellow Write-Host "Boot this machine from WinPE media next, then run this script with -Mode Capture to create the WIM." -ForegroundColor Yellow } function Invoke-Capture { if (-not $SourceDrive) { Write-Error "-SourceDrive is required in Capture mode (e.g. -SourceDrive D:)." exit 1 } if (-not $DestinationWim) { Write-Error "-DestinationWim is required in Capture mode (e.g. -DestinationWim E:\Images\Win11Pro.wim)." exit 1 } $normalizedSource = $SourceDrive.TrimEnd('\') if (-not ($normalizedSource -match '^[A-Za-z]:$')) { Write-Error "-SourceDrive must be a drive letter like 'D:'." exit 1 } $runningSystemDrive = $env:SystemDrive if ($normalizedSource -ieq $runningSystemDrive) { Write-Error "The source drive '$normalizedSource' appears to be the currently running OS volume. Capture must be performed from WinPE with the target volume offline, not from within the running OS." exit 1 } if (-not (Test-Path -Path "$normalizedSource\Windows")) { Write-Error "No Windows installation found at '$normalizedSource\Windows'. Verify -SourceDrive points to the generalized volume." exit 1 } $destDir = Split-Path -Path $DestinationWim -Parent if ($destDir -and -not (Test-Path -Path $destDir)) { Write-Step "Creating destination folder $destDir" New-Item -Path $destDir -ItemType Directory -Force | Out-Null } if (Test-Path -Path $DestinationWim) { Write-Warning "Destination file '$DestinationWim' already exists and will be overwritten." Remove-Item -Path $DestinationWim -Force } Write-Step "Capturing '$normalizedSource\' to '$DestinationWim' (this can take a long time)" Dism.exe /Capture-Image ` /ImageFile:"$DestinationWim" ` /CaptureDir:"$normalizedSource\" ` /Name:"$ImageName" ` /Compress:$Compress ` /CheckIntegrity if ($LASTEXITCODE -ne 0) { Write-Error "DISM capture failed with exit code $LASTEXITCODE." exit $LASTEXITCODE } Write-Host "`nImage captured successfully: $DestinationWim" -ForegroundColor Green } switch ($Mode) { "Sysprep" { Invoke-Sysprep } "Capture" { Invoke-Capture } }