Production-grade Windows PE builder with PowerShell 7 integration. Features: - PowerShell 7 integrated into WinPE environment - Automatic script launcher for external media - Configurable startup and fallback scripts - Custom console colors - Maximized console window on boot - PowerShell module integration - Windows ADK auto-installation - Defender optimization for faster builds Project Structure: - Build-PE.ps1: Main build orchestrator - config.json: Configuration file - lib/: Build modules (Dependencies, WinPEBuilder, PowerShell7, ModuleManager, DefenderOptimization) - resources/: Runtime scripts (Invoke-ScriptLauncher, Invoke-ScriptMenu, Driver-Manager) - CLAUDE.md: Complete project documentation - README.md: Quick start guide Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
516 lines
17 KiB
PowerShell
516 lines
17 KiB
PowerShell
#Requires -Version 5.1
|
|
#Requires -RunAsAdministrator
|
|
<#
|
|
.SYNOPSIS
|
|
Builds a customized Windows PE image with PowerShell 7 and script launcher.
|
|
|
|
.DESCRIPTION
|
|
Automates the complete process of creating a WinPE image that includes:
|
|
- PowerShell 7 integration
|
|
- Custom PowerShell modules
|
|
- Automatic script launcher on boot
|
|
- Detection of external media with .scripts folder
|
|
|
|
.PARAMETER SkipADKCheck
|
|
Skip Windows ADK installation check (not recommended).
|
|
|
|
.PARAMETER CleanBuild
|
|
Removes existing work directory for a clean build.
|
|
|
|
.PARAMETER DefenderOptimization
|
|
Optimize Windows Defender for faster build performance:
|
|
- None: No changes to Defender (default, safest)
|
|
- DisableRealTime: Temporarily disable real-time protection (fastest, 50-70% improvement, restored after build)
|
|
- AddExclusions: Add build directories to exclusions (balanced, 20-30% improvement, cleaned up after)
|
|
|
|
.EXAMPLE
|
|
.\Build-PE.ps1
|
|
Performs a standard build with all checks
|
|
|
|
.EXAMPLE
|
|
.\Build-PE.ps1 -CleanBuild
|
|
Removes previous build artifacts and starts fresh
|
|
|
|
.EXAMPLE
|
|
.\Build-PE.ps1 -DefenderOptimization DisableRealTime
|
|
Builds with real-time protection temporarily disabled for maximum speed
|
|
|
|
.EXAMPLE
|
|
.\Build-PE.ps1 -DefenderOptimization AddExclusions
|
|
Adds build directories to Defender exclusions during build
|
|
|
|
.NOTES
|
|
Requires Windows 10/11 with administrator privileges
|
|
First run will download and install Windows ADK (~4GB, 20-30 min)
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$SkipADKCheck,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$CleanBuild,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[ValidateSet('None', 'DisableRealTime', 'AddExclusions')]
|
|
[string]$DefenderOptimization = 'None'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
$Script:StartTime = Get-Date
|
|
$Script:LogPath = Join-Path $PSScriptRoot "output\build.log"
|
|
|
|
#region Logging
|
|
|
|
function Write-Log {
|
|
param(
|
|
[string]$Message,
|
|
[string]$Level = "INFO"
|
|
)
|
|
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
$logMessage = "[$timestamp] [$Level] $Message"
|
|
|
|
if (-not (Test-Path (Split-Path $Script:LogPath))) {
|
|
New-Item -ItemType Directory -Path (Split-Path $Script:LogPath) -Force | Out-Null
|
|
}
|
|
|
|
Add-Content -Path $Script:LogPath -Value $logMessage
|
|
}
|
|
|
|
function Write-Status {
|
|
param(
|
|
[string]$Message,
|
|
[string]$Type = "Info"
|
|
)
|
|
|
|
$color = switch ($Type) {
|
|
"Success" { "Green" }
|
|
"Warning" { "Yellow" }
|
|
"Error" { "Red" }
|
|
"Info" { "Cyan" }
|
|
"Progress" { "Magenta" }
|
|
default { "White" }
|
|
}
|
|
|
|
$prefix = switch ($Type) {
|
|
"Success" { "[✓]" }
|
|
"Warning" { "[!]" }
|
|
"Error" { "[✗]" }
|
|
"Info" { "[i]" }
|
|
"Progress" { "[*]" }
|
|
default { "[-]" }
|
|
}
|
|
|
|
Write-Host "$prefix " -ForegroundColor $color -NoNewline
|
|
Write-Host $Message
|
|
|
|
Write-Log -Message $Message -Level $Type.ToUpper()
|
|
}
|
|
|
|
function Write-Header {
|
|
param([string]$Title)
|
|
|
|
$line = "=" * 80
|
|
Write-Host ""
|
|
Write-Host $line -ForegroundColor Cyan
|
|
Write-Host " $Title" -ForegroundColor Yellow
|
|
Write-Host $line -ForegroundColor Cyan
|
|
Write-Host ""
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Initialization
|
|
|
|
Write-Host ""
|
|
Write-Host "================================================================================" -ForegroundColor Cyan
|
|
Write-Host " WinPE Script Launcher Builder" -ForegroundColor Yellow
|
|
Write-Host " Production Build System" -ForegroundColor White
|
|
Write-Host "================================================================================" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
Write-Log "Build started" "INFO"
|
|
Write-Status "Build log: $Script:LogPath" "Info"
|
|
Write-Host ""
|
|
|
|
if (Test-Path $Script:LogPath) {
|
|
Clear-Content $Script:LogPath
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Import Modules
|
|
|
|
Write-Header "Loading Build Modules"
|
|
|
|
$libModules = @(
|
|
"Dependencies.psm1",
|
|
"WinPEBuilder.psm1",
|
|
"PowerShell7.psm1",
|
|
"ModuleManager.psm1",
|
|
"DefenderOptimization.psm1"
|
|
)
|
|
|
|
foreach ($module in $libModules) {
|
|
$modulePath = Join-Path $PSScriptRoot "lib\$module"
|
|
|
|
if (-not (Test-Path $modulePath)) {
|
|
Write-Status "Module not found: $module" "Error"
|
|
throw "Required module missing: $modulePath"
|
|
}
|
|
|
|
try {
|
|
Import-Module $modulePath -Force -ErrorAction Stop
|
|
Write-Status "Loaded: $module" "Success"
|
|
}
|
|
catch {
|
|
Write-Status "Failed to load: $module" "Error"
|
|
throw "Module import failed: $_"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
|
|
#endregion
|
|
|
|
#region Load Configuration
|
|
|
|
Write-Header "Loading Configuration"
|
|
|
|
$configPath = Join-Path $PSScriptRoot "config.json"
|
|
|
|
if (-not (Test-Path $configPath)) {
|
|
Write-Status "Configuration file not found: $configPath" "Error"
|
|
throw "config.json is required"
|
|
}
|
|
|
|
try {
|
|
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
|
Write-Status "Configuration loaded successfully" "Success"
|
|
Write-Status "PowerShell 7 Version: $($config.powershell7Version)" "Info"
|
|
Write-Status "Startup Script: $(if ($config.startupScript) {$config.startupScript} else {'Invoke-ScriptLauncher.ps1'})" "Info"
|
|
Write-Status "Target Script (External): $($config.targetScript)" "Info"
|
|
Write-Status "Fallback Script (Offline): $(if ($config.fallbackScript) {$config.fallbackScript} else {'Invoke-ScriptMenu.ps1'})" "Info"
|
|
Write-Status "Scripts Folder: $($config.scriptsFolder)" "Info"
|
|
Write-Status "Modules: $($config.powershellModules.Count) configured" "Info"
|
|
}
|
|
catch {
|
|
Write-Status "Failed to parse configuration" "Error"
|
|
throw "Configuration error: $_"
|
|
}
|
|
|
|
Write-Host ""
|
|
|
|
#endregion
|
|
|
|
#region Dependency Checks
|
|
|
|
Write-Header "Validating Dependencies"
|
|
|
|
Write-Status "Checking administrator privileges..." "Progress"
|
|
if (-not (Test-AdminPrivileges)) {
|
|
Write-Status "Administrator privileges required" "Error"
|
|
throw "Please run this script as Administrator"
|
|
}
|
|
Write-Status "Running with administrator privileges" "Success"
|
|
|
|
if (-not $SkipADKCheck) {
|
|
Write-Status "Checking Windows ADK installation..." "Progress"
|
|
|
|
if (-not (Test-WindowsADK)) {
|
|
Write-Status "Windows ADK not found" "Warning"
|
|
Write-Host ""
|
|
Write-Host "The Windows Assessment and Deployment Kit (ADK) is required." -ForegroundColor Yellow
|
|
Write-Host "This will download and install:" -ForegroundColor White
|
|
Write-Host " - Windows ADK (~150MB download)" -ForegroundColor Cyan
|
|
Write-Host " - WinPE Add-on (~5GB download)" -ForegroundColor Cyan
|
|
Write-Host " - Installation time: ~20-30 minutes" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
$response = Read-Host "Install Windows ADK now? (Y/N)"
|
|
|
|
if ($response -ne 'Y' -and $response -ne 'y') {
|
|
Write-Status "Build cancelled by user" "Warning"
|
|
exit 0
|
|
}
|
|
|
|
Install-WindowsADK
|
|
Write-Status "Windows ADK installed successfully" "Success"
|
|
}
|
|
else {
|
|
Write-Status "Windows ADK is installed" "Success"
|
|
}
|
|
}
|
|
|
|
Write-Status "Checking PowerShell 7 archive..." "Progress"
|
|
$ps7FileName = "PowerShell-$($config.powershell7Version)-win-x64.zip"
|
|
$ps7Path = Join-Path $PSScriptRoot $ps7FileName
|
|
|
|
if (-not (Test-PowerShell7Archive -Config $config)) {
|
|
Write-Status "PowerShell 7 archive not found" "Warning"
|
|
Write-Status "Downloading PowerShell $($config.powershell7Version)..." "Progress"
|
|
|
|
Get-PowerShell7Archive -Config $config
|
|
|
|
Write-Status "PowerShell 7 downloaded" "Success"
|
|
}
|
|
else {
|
|
Write-Status "PowerShell 7 archive found" "Success"
|
|
}
|
|
|
|
Write-Host ""
|
|
|
|
#endregion
|
|
|
|
#region Clean Build Option
|
|
|
|
if ($CleanBuild) {
|
|
Write-Header "Clean Build"
|
|
|
|
$workDir = Join-Path $PSScriptRoot "work"
|
|
|
|
if (Test-Path $workDir) {
|
|
Write-Status "Removing existing work directory..." "Progress"
|
|
try {
|
|
Remove-Item $workDir -Recurse -Force
|
|
Write-Status "Work directory cleaned" "Success"
|
|
}
|
|
catch {
|
|
Write-Status "Failed to clean work directory" "Warning"
|
|
Write-Status "Error: $($_.Exception.Message)" "Warning"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Build Process
|
|
|
|
# Windows Defender Optimization
|
|
$defenderState = $null
|
|
$defenderExclusionsAdded = $false
|
|
|
|
if ($DefenderOptimization -ne 'None') {
|
|
Write-Header "Windows Defender Optimization"
|
|
|
|
if (-not (Test-DefenderAvailable)) {
|
|
Write-Status "Windows Defender not available on this system" "Warning"
|
|
Write-Status "Continuing without Defender optimization" "Info"
|
|
Write-Host ""
|
|
}
|
|
else {
|
|
try {
|
|
if ($DefenderOptimization -eq 'DisableRealTime') {
|
|
Write-Status "Mode: Temporarily disable real-time protection" "Info"
|
|
Write-Status "Disabling Windows Defender real-time protection..." "Progress"
|
|
|
|
$defenderState = Disable-DefenderRealTimeProtection
|
|
|
|
if ($null -ne $defenderState) {
|
|
Write-Status "Real-time protection disabled for build duration" "Success"
|
|
Write-Status "Will be restored automatically when build completes" "Info"
|
|
Write-Status "Expected performance improvement: 50-70%" "Info"
|
|
}
|
|
}
|
|
elseif ($DefenderOptimization -eq 'AddExclusions') {
|
|
Write-Status "Mode: Add build directories to exclusions" "Info"
|
|
Write-Status "Adding build directories to Defender exclusions..." "Progress"
|
|
|
|
Add-DefenderExclusions -PSScriptRoot $PSScriptRoot
|
|
$defenderExclusionsAdded = $true
|
|
|
|
Write-Status "Exclusions added (will be removed after build)" "Success"
|
|
Write-Status "Expected performance improvement: 20-30%" "Info"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Status "Defender optimization failed: $($_.Exception.Message)" "Warning"
|
|
Write-Status "Continuing without Defender optimization" "Info"
|
|
}
|
|
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
try {
|
|
Write-Header "Building WinPE Image"
|
|
|
|
Write-Status "Step 1/10: Initializing WinPE workspace" "Progress"
|
|
Initialize-WinPEWorkspace
|
|
Write-Status "Workspace initialized" "Success"
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 2/10: Mounting WinPE image" "Progress"
|
|
Mount-WinPEImage
|
|
$mountDir = Get-MountDirectory
|
|
Write-Status "Image mounted at: $mountDir" "Success"
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 3/10: Adding optional components" "Progress"
|
|
$components = @(
|
|
"WinPE-WMI",
|
|
"WinPE-NetFX",
|
|
"WinPE-Scripting",
|
|
"WinPE-PowerShell",
|
|
"WinPE-StorageWMI",
|
|
"WinPE-DismCmdlets"
|
|
)
|
|
|
|
foreach ($component in $components) {
|
|
Add-WinPEOptionalComponent -Name $component
|
|
}
|
|
Write-Status "Optional components installed" "Success"
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 4/10: Setting scratch space" "Progress"
|
|
Set-WinPEScratchSpace -SizeMB 512
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 5/10: Installing PowerShell 7" "Progress"
|
|
Install-PowerShell7ToWinPE -ZipPath $ps7Path -MountDir $mountDir
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 6/10: Copying PowerShell modules" "Progress"
|
|
if ($config.powershellModules -and $config.powershellModules.Count -gt 0) {
|
|
foreach ($moduleName in $config.powershellModules) {
|
|
$modulePath = Get-RepositoryModulePath -ModuleName $moduleName
|
|
|
|
Install-ModuleToWinPE -ModuleName $moduleName -SourcePath $modulePath -MountDir $mountDir
|
|
}
|
|
Write-Status "All modules copied successfully" "Success"
|
|
}
|
|
else {
|
|
Write-Status "No modules configured - skipping" "Info"
|
|
}
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 7/10: Copying launcher scripts and configuration" "Progress"
|
|
|
|
# Copy startup script
|
|
$startupScriptName = if ($config.startupScript) { $config.startupScript } else { "Invoke-ScriptLauncher.ps1" }
|
|
$startupSource = Join-Path $PSScriptRoot "resources\$startupScriptName"
|
|
$startupDest = Join-Path $mountDir "Windows\System32\$startupScriptName"
|
|
|
|
if (Test-Path $startupSource) {
|
|
Copy-Item -Path $startupSource -Destination $startupDest -Force
|
|
Write-Status "Startup script copied: $startupScriptName" "Success"
|
|
}
|
|
else {
|
|
Write-Status "Startup script not found: $startupScriptName" "Error"
|
|
throw "Startup script missing: $startupSource"
|
|
}
|
|
|
|
# Copy fallback script
|
|
$fallbackScriptName = if ($config.fallbackScript) { $config.fallbackScript } else { "Invoke-ScriptMenu.ps1" }
|
|
$fallbackSource = Join-Path $PSScriptRoot "resources\$fallbackScriptName"
|
|
$fallbackDest = Join-Path $mountDir "Windows\System32\$fallbackScriptName"
|
|
|
|
if (Test-Path $fallbackSource) {
|
|
Copy-Item -Path $fallbackSource -Destination $fallbackDest -Force
|
|
Write-Status "Fallback script copied: $fallbackScriptName" "Success"
|
|
}
|
|
else {
|
|
Write-Status "Fallback script not found: $fallbackScriptName" "Error"
|
|
throw "Fallback script missing: $fallbackSource"
|
|
}
|
|
|
|
# Copy configuration file
|
|
$configDest = Join-Path $mountDir "Windows\System32\winpe-config.json"
|
|
Copy-Item -Path $configPath -Destination $configDest -Force
|
|
Write-Status "Configuration copied to image" "Success"
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 8/10: Configuring startup script" "Progress"
|
|
Set-PowerShell7Environment -MountDir $mountDir -LauncherScriptPath $startupScriptName
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 9/10: Unmounting and committing changes" "Progress"
|
|
Dismount-WinPEImage
|
|
Write-Host ""
|
|
|
|
Write-Status "Step 10/10: Creating bootable ISO" "Progress"
|
|
New-BootableMedia -OutputPath (Join-Path $PSScriptRoot "output")
|
|
|
|
Write-Host ""
|
|
Write-Host "================================================================================" -ForegroundColor Green
|
|
Write-Status "BUILD COMPLETED SUCCESSFULLY!" "Success"
|
|
Write-Host "================================================================================" -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
$outputISO = Join-Path $PSScriptRoot "output\WinPE.iso"
|
|
$isoSize = (Get-Item $outputISO).Length / 1MB
|
|
|
|
Write-Status "Output: $outputISO" "Success"
|
|
Write-Status "Size: $([math]::Round($isoSize, 2)) MB" "Info"
|
|
|
|
$elapsed = (Get-Date) - $Script:StartTime
|
|
Write-Status "Build time: $($elapsed.ToString('mm\:ss'))" "Info"
|
|
Write-Host ""
|
|
|
|
Write-Host "Next steps:" -ForegroundColor Yellow
|
|
Write-Host " 1. Test ISO in virtual machine (Hyper-V, VirtualBox, etc.)" -ForegroundColor Cyan
|
|
Write-Host " 2. Create .scripts folder on external media" -ForegroundColor Cyan
|
|
Write-Host " 3. Add your scripts to .scripts folder" -ForegroundColor Cyan
|
|
Write-Host " 4. Boot the WinPE image" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
Write-Log "Build completed successfully" "INFO"
|
|
}
|
|
catch {
|
|
Write-Host ""
|
|
Write-Host "================================================================================" -ForegroundColor Red
|
|
Write-Status "BUILD FAILED!" "Error"
|
|
Write-Host "================================================================================" -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Status "Error: $($_.Exception.Message)" "Error"
|
|
Write-Host ""
|
|
Write-Status "Check log file for details: $Script:LogPath" "Info"
|
|
Write-Host ""
|
|
|
|
Write-Log "Build failed: $($_.Exception.Message)" "ERROR"
|
|
|
|
try {
|
|
Write-Status "Attempting to unmount image..." "Warning"
|
|
Dismount-WinPEImage
|
|
}
|
|
catch {
|
|
Write-Status "Could not unmount image cleanly" "Warning"
|
|
Write-Status "Run 'dism /Cleanup-Mountpoints' to clean up" "Info"
|
|
}
|
|
|
|
exit 1
|
|
}
|
|
finally {
|
|
# Restore Windows Defender settings
|
|
if ($DefenderOptimization -ne 'None') {
|
|
Write-Host ""
|
|
Write-Header "Restoring Windows Defender Settings"
|
|
|
|
try {
|
|
if ($DefenderOptimization -eq 'DisableRealTime' -and $null -ne $defenderState) {
|
|
Write-Status "Restoring Windows Defender real-time protection..." "Progress"
|
|
Restore-DefenderRealTimeProtection -OriginalState $defenderState
|
|
Write-Status "Real-time protection restored" "Success"
|
|
}
|
|
elseif ($DefenderOptimization -eq 'AddExclusions' -and $defenderExclusionsAdded) {
|
|
Write-Status "Removing temporary Defender exclusions..." "Progress"
|
|
Remove-DefenderExclusions -PSScriptRoot $PSScriptRoot
|
|
Write-Status "Exclusions removed" "Success"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Status "Failed to restore Defender settings: $($_.Exception.Message)" "Warning"
|
|
Write-Status "You may need to manually restore Defender settings" "Warning"
|
|
}
|
|
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
#endregion
|