first commit
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
<#
|
||||
.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:<file>`, 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 = @'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<NetworkLocation>Work</NetworkLocation>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
<SkipMachineOOBE>true</SkipMachineOOBE>
|
||||
<SkipUserOOBE>true</SkipUserOOBE>
|
||||
</OOBE>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="generalize">
|
||||
<component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<SkipRearm>1</SkipRearm>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
'@
|
||||
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user