first commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Enables all Windows features required for WSL 2 on Windows 11 and installs the latest Ubuntu distribution.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script must be run as Administrator. It will:
|
||||||
|
1. Enable the "Microsoft-Windows-Subsystem-Linux" and "VirtualMachinePlatform" optional features.
|
||||||
|
2. Set WSL 2 as the default WSL version.
|
||||||
|
3. Install/update the WSL kernel components.
|
||||||
|
4. Install the latest Ubuntu distribution from the Microsoft Store catalog (via `wsl --install -d Ubuntu`).
|
||||||
|
|
||||||
|
A restart may be required after enabling the Windows features for the first time. If a restart is
|
||||||
|
needed, the script will detect it, prompt you, and re-run automatically after reboot (via a scheduled
|
||||||
|
task) unless you decline.
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Run this script from an elevated PowerShell prompt:
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\Install-WSL2Ubuntu.ps1
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Distribution = "Ubuntu"
|
||||||
|
)
|
||||||
|
|
||||||
|
$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
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Step 1: Enable required Windows optional features ---
|
||||||
|
Write-Step "Enabling 'Windows Subsystem for Linux' feature"
|
||||||
|
$wslFeature = Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart
|
||||||
|
|
||||||
|
Write-Step "Enabling 'Virtual Machine Platform' feature"
|
||||||
|
$vmpFeature = Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart
|
||||||
|
|
||||||
|
$restartNeeded = $wslFeature.RestartNeeded -or $vmpFeature.RestartNeeded
|
||||||
|
|
||||||
|
if ($restartNeeded) {
|
||||||
|
Write-Host "`nA restart is required to finish enabling the WSL 2 prerequisites." -ForegroundColor Yellow
|
||||||
|
$answer = Read-Host "Restart now and continue installation automatically after reboot? (Y/N)"
|
||||||
|
|
||||||
|
if ($answer -match '^[Yy]') {
|
||||||
|
Write-Step "Scheduling this script to resume after reboot"
|
||||||
|
|
||||||
|
$scriptPath = $MyInvocation.MyCommand.Path
|
||||||
|
$taskName = "Resume-WSL2Install"
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||||
|
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Distribution `"$Distribution`""
|
||||||
|
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Highest
|
||||||
|
|
||||||
|
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Force | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Restarting in 10 seconds... (Ctrl+C to cancel)" -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
Restart-Computer -Force
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Please restart your computer manually, then re-run this script to continue." -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# If we get here after a reboot via the scheduled task, clean it up.
|
||||||
|
$existingTask = Get-ScheduledTask -TaskName "Resume-WSL2Install" -ErrorAction SilentlyContinue
|
||||||
|
if ($existingTask) {
|
||||||
|
Unregister-ScheduledTask -TaskName "Resume-WSL2Install" -Confirm:$false
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Step 2: Update WSL and set default version to 2 ---
|
||||||
|
Write-Step "Updating the WSL kernel components"
|
||||||
|
wsl --update
|
||||||
|
|
||||||
|
Write-Step "Setting WSL default version to 2"
|
||||||
|
wsl --set-default-version 2
|
||||||
|
|
||||||
|
# --- Step 3: Install the latest Ubuntu distribution ---
|
||||||
|
Write-Step "Checking for an existing '$Distribution' installation"
|
||||||
|
$installedDistros = (wsl --list --quiet 2>$null) -replace "`0", ""
|
||||||
|
|
||||||
|
if ($installedDistros -match [regex]::Escape($Distribution)) {
|
||||||
|
Write-Host "'$Distribution' is already installed. Skipping installation." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Step "Installing '$Distribution' (latest version from Microsoft Store)"
|
||||||
|
wsl --install -d $Distribution --no-launch
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Done"
|
||||||
|
Write-Host "WSL 2 features are enabled and '$Distribution' has been installed." -ForegroundColor Green
|
||||||
|
Write-Host "Launch it by running: wsl -d $Distribution" -ForegroundColor Green
|
||||||
|
Write-Host "On first launch you'll be asked to create a Unix username and password." -ForegroundColor Green
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Builds a bootable WinPE USB drive that can be used to run New-Win11GeneralizedImage.ps1 -Mode Capture.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script must be run as Administrator on a machine that has the Windows ADK and the
|
||||||
|
"Windows PE add-on for the ADK" installed. It will:
|
||||||
|
1. Locate copype.cmd / MakeWinPEMedia.cmd from the installed ADK.
|
||||||
|
2. Build a WinPE working folder for the specified architecture (copype).
|
||||||
|
3. Optionally inject PowerShell + network/storage support and a custom startup script.
|
||||||
|
4. Copy New-Win11GeneralizedImage.ps1 (and any extra files) onto the WinPE media so it's
|
||||||
|
available once booted.
|
||||||
|
5. Format the target USB drive and copy the bootable WinPE media onto it (MakeWinPEMedia /UFD).
|
||||||
|
|
||||||
|
WARNING: The target USB drive is completely erased. Double-check -UsbDriveLetter before running.
|
||||||
|
|
||||||
|
.PARAMETER UsbDriveLetter
|
||||||
|
The drive letter of the USB flash drive to turn into bootable WinPE media, e.g. "F:". This drive
|
||||||
|
will be wiped.
|
||||||
|
|
||||||
|
.PARAMETER Architecture
|
||||||
|
WinPE architecture to build: amd64 (default), x86, or arm64.
|
||||||
|
|
||||||
|
.PARAMETER WorkingDirectory
|
||||||
|
Folder used to stage the WinPE image files. Defaults to "C:\WinPE_<Architecture>".
|
||||||
|
|
||||||
|
.PARAMETER IncludeCaptureScript
|
||||||
|
Path to New-Win11GeneralizedImage.ps1 (or any other script) to copy onto the WinPE media so it can
|
||||||
|
be run directly after boot. Defaults to New-Win11GeneralizedImage.ps1 next to this script, if present.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\New-WinPEUsbMedia.ps1 -UsbDriveLetter F: -Architecture amd64
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Requires the Windows ADK + WinPE add-on: https://learn.microsoft.com/windows-hardware/get-started/adk-install
|
||||||
|
Run from an elevated "Deployment and Imaging Tools Environment" or a normal elevated PowerShell prompt.
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidatePattern('^[A-Za-z]:$')]
|
||||||
|
[string]$UsbDriveLetter,
|
||||||
|
|
||||||
|
[ValidateSet("amd64", "x86", "arm64")]
|
||||||
|
[string]$Architecture = "amd64",
|
||||||
|
|
||||||
|
[string]$WorkingDirectory,
|
||||||
|
|
||||||
|
[string]$IncludeCaptureScript = (Join-Path -Path $PSScriptRoot -ChildPath "New-Win11GeneralizedImage.ps1")
|
||||||
|
)
|
||||||
|
|
||||||
|
$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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $WorkingDirectory) {
|
||||||
|
$WorkingDirectory = "C:\WinPE_$Architecture"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Locate the ADK deployment tools that ship copype.cmd / MakeWinPEMedia.cmd
|
||||||
|
$adkRoots = @(
|
||||||
|
"${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment",
|
||||||
|
"$env:ProgramFiles\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment"
|
||||||
|
)
|
||||||
|
$dandiScript = $null
|
||||||
|
foreach ($root in $adkRoots) {
|
||||||
|
$candidate = Join-Path -Path (Split-Path -Path $root -Parent) -ChildPath "DandISetEnv.bat"
|
||||||
|
if (Test-Path -Path $candidate) {
|
||||||
|
$dandiScript = $candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $dandiScript) {
|
||||||
|
Write-Error "Could not find the Windows ADK Deployment Tools environment (DandISetEnv.bat). Install the Windows ADK and the 'Windows PE add-on for the ADK' first: https://learn.microsoft.com/windows-hardware/get-started/adk-install"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Using ADK deployment tools environment: $dandiScript"
|
||||||
|
|
||||||
|
# Import the ADK's Deployment Tools environment variables (sets copype/MakeWinPEMedia on PATH) into this session
|
||||||
|
$envVars = cmd /c "`"$dandiScript`" && set"
|
||||||
|
foreach ($line in $envVars) {
|
||||||
|
if ($line -match '^([^=]+)=(.*)$') {
|
||||||
|
[System.Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], "Process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$copype = (Get-Command copype.cmd -ErrorAction SilentlyContinue)
|
||||||
|
$makeWinPEMedia = (Get-Command MakeWinPEMedia.cmd -ErrorAction SilentlyContinue)
|
||||||
|
if (-not $copype -or -not $makeWinPEMedia) {
|
||||||
|
Write-Error "copype.cmd / MakeWinPEMedia.cmd were not found on PATH even after loading the ADK environment."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -Path $WorkingDirectory) {
|
||||||
|
Write-Step "Removing existing working directory $WorkingDirectory"
|
||||||
|
Remove-Item -Path $WorkingDirectory -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Building WinPE working folder ($Architecture) at $WorkingDirectory"
|
||||||
|
& $copype.Source $Architecture $WorkingDirectory
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "copype failed with exit code $LASTEXITCODE."
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeCaptureScript -and (Test-Path -Path $IncludeCaptureScript)) {
|
||||||
|
Write-Step "Copying $IncludeCaptureScript onto the WinPE media"
|
||||||
|
$mediaRoot = Join-Path -Path $WorkingDirectory -ChildPath "media"
|
||||||
|
Copy-Item -Path $IncludeCaptureScript -Destination (Join-Path -Path $mediaRoot -ChildPath (Split-Path -Path $IncludeCaptureScript -Leaf)) -Force
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warning "IncludeCaptureScript '$IncludeCaptureScript' not found, skipping."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nThe target drive $UsbDriveLetter will be ERASED." -ForegroundColor Yellow
|
||||||
|
$confirm = Read-Host "Type YES to continue and format $UsbDriveLetter as bootable WinPE USB media"
|
||||||
|
if ($confirm -ne "YES") {
|
||||||
|
Write-Host "Aborted, no changes made to $UsbDriveLetter." -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Formatting $UsbDriveLetter and copying bootable WinPE media (MakeWinPEMedia /UFD)"
|
||||||
|
& $makeWinPEMedia.Source /UFD $WorkingDirectory $UsbDriveLetter /f
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "MakeWinPEMedia failed with exit code $LASTEXITCODE."
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nWinPE USB media created successfully on $UsbDriveLetter." -ForegroundColor Green
|
||||||
|
Write-Host "Boot the reference machine from this USB drive, then from the WinPE command prompt run:" -ForegroundColor Green
|
||||||
|
Write-Host " powershell -ExecutionPolicy Bypass -File X:\New-Win11GeneralizedImage.ps1 -Mode Capture -SourceDrive D: -DestinationWim E:\Images\Win11Pro.wim" -ForegroundColor DarkGray
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# powershell-scripts
|
||||||
|
|
||||||
|
A collection of standalone PowerShell scripts for Windows setup and deployment tasks. All scripts must be run from an elevated (Administrator) PowerShell prompt.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Install-WSL2Ubuntu.ps1](#install-wsl2ubuntups1) — Enable WSL 2 and install Ubuntu.
|
||||||
|
- [New-Win11GeneralizedImage.ps1](#new-win11generalizedimageps1) — Generalize a Windows 11 Pro reference machine and capture it to a deployment WIM.
|
||||||
|
- [New-WinPEUsbMedia.ps1](#new-winpeusbmediaps1) — Build a bootable WinPE USB drive for use with the capture script.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install-WSL2Ubuntu.ps1
|
||||||
|
|
||||||
|
Enables the Windows features required for WSL 2 and installs the latest Ubuntu distribution.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Enables the `Microsoft-Windows-Subsystem-Linux` and `VirtualMachinePlatform` optional features.
|
||||||
|
2. Sets WSL 2 as the default WSL version.
|
||||||
|
3. Installs/updates the WSL kernel components.
|
||||||
|
4. Installs the latest Ubuntu distribution via `wsl --install -d Ubuntu`.
|
||||||
|
5. If a restart is required after enabling the features, prompts to reboot and automatically resumes the script after sign-in (via a scheduled task).
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `-Distribution` | `Ubuntu` | The WSL distribution name to install. |
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\Install-WSL2Ubuntu.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New-Win11GeneralizedImage.ps1
|
||||||
|
|
||||||
|
Prepares a Windows 11 Pro reference machine and creates a generalized (sysprepped) WIM image for deployment. Operates in two modes.
|
||||||
|
|
||||||
|
### Mode: Sysprep (default)
|
||||||
|
|
||||||
|
Run **on the reference machine** while it's still the running OS.
|
||||||
|
|
||||||
|
1. Optionally removes provisioned AppX packages matching supplied wildcard patterns.
|
||||||
|
2. Runs `DISM /Online /Cleanup-Image /StartComponentCleanup /ResetBase` to shrink the component store.
|
||||||
|
3. Generates a minimal `unattend.xml` (skips OOBE prompts) if one isn't supplied.
|
||||||
|
4. Runs `sysprep.exe /oobe /generalize /shutdown /unattend:<file>`. The machine powers off automatically once done.
|
||||||
|
|
||||||
|
> Windows cannot capture its own running volume — after sysprep shuts the machine down, boot it from WinPE media (see [New-WinPEUsbMedia.ps1](#new-winpeusbmediaps1)) to actually capture the image.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `-Mode` | `Sysprep` | `Sysprep` or `Capture`. |
|
||||||
|
| `-UnattendPath` | (generated) | Path to a custom unattend.xml. |
|
||||||
|
| `-RemoveProvisionedApps` | `@()` | Wildcard patterns of AppX packages to remove, e.g. `"*Xbox*","*Solitaire*"`. |
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```powershell
|
||||||
|
.\New-Win11GeneralizedImage.ps1 -Mode Sysprep -RemoveProvisionedApps "*Xbox*","*Solitaire*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode: Capture
|
||||||
|
|
||||||
|
Run **from WinPE**, after the reference machine has shut down from sysprep, with the generalized Windows volume mounted as an offline drive letter (not the running OS).
|
||||||
|
|
||||||
|
Captures the offline volume into a `.wim` file using `DISM /Capture-Image`. Refuses to run against the currently running OS volume as a safety check.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `-SourceDrive` | *(required)* | Drive letter of the generalized volume, e.g. `D:`. |
|
||||||
|
| `-DestinationWim` | *(required)* | Output path for the `.wim` file, e.g. `E:\Images\Win11Pro.wim`. |
|
||||||
|
| `-ImageName` | `Windows 11 Pro - Deployment` | Friendly name stored in the WIM metadata. |
|
||||||
|
| `-Compress` | `max` | DISM compression level: `fast`, `max`, or `none`. |
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```powershell
|
||||||
|
.\New-Win11GeneralizedImage.ps1 -Mode Capture -SourceDrive D: -DestinationWim "E:\Images\Win11Pro.wim"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New-WinPEUsbMedia.ps1
|
||||||
|
|
||||||
|
Builds a bootable WinPE USB drive that can be used to run `New-Win11GeneralizedImage.ps1 -Mode Capture`.
|
||||||
|
|
||||||
|
**Prerequisites:** the [Windows ADK](https://learn.microsoft.com/windows-hardware/get-started/adk-install) and the "Windows PE add-on for the ADK" must be installed. Both can be installed via winget:
|
||||||
|
```powershell
|
||||||
|
winget install --id Microsoft.WindowsADK
|
||||||
|
winget install --id Microsoft.WindowsADK.WinPEAddon
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Locates `copype.cmd` / `MakeWinPEMedia.cmd` from the installed ADK and loads the Deployment Tools environment.
|
||||||
|
2. Runs `copype` to stage a WinPE working folder for the chosen architecture.
|
||||||
|
3. Copies `New-Win11GeneralizedImage.ps1` onto the WinPE media so it's available after boot.
|
||||||
|
4. Prompts for confirmation, then formats the target USB drive and copies the bootable WinPE media onto it via `MakeWinPEMedia /UFD`.
|
||||||
|
|
||||||
|
> ⚠️ The target USB drive is completely erased. Double-check `-UsbDriveLetter` before confirming.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `-UsbDriveLetter` | *(required)* | Drive letter of the USB flash drive to format, e.g. `F:`. |
|
||||||
|
| `-Architecture` | `amd64` | WinPE architecture: `amd64`, `x86`, or `arm64`. |
|
||||||
|
| `-WorkingDirectory` | `C:\WinPE_<Architecture>` | Folder used to stage the WinPE image files. |
|
||||||
|
| `-IncludeCaptureScript` | `New-Win11GeneralizedImage.ps1` next to this script | Extra script copied onto the WinPE media. |
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```powershell
|
||||||
|
.\New-WinPEUsbMedia.ps1 -UsbDriveLetter F: -Architecture amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typical end-to-end workflow
|
||||||
|
|
||||||
|
1. `New-WinPEUsbMedia.ps1` — build the WinPE capture USB (one-time, on any machine with the ADK installed).
|
||||||
|
2. `New-Win11GeneralizedImage.ps1 -Mode Sysprep` — generalize the reference machine; it shuts down automatically.
|
||||||
|
3. Boot the reference machine from the WinPE USB.
|
||||||
|
4. `New-Win11GeneralizedImage.ps1 -Mode Capture` — capture the generalized volume to a `.wim` file for deployment.
|
||||||
Reference in New Issue
Block a user