Files
2026-05-17 08:40:05 -05:00

822 lines
32 KiB
Plaintext

#Requires -RunAsAdministrator
<#
.SYNOPSIS
Build a WinPE x64 image for netboot.xyz (SMB/TFTP delivery)
.DESCRIPTION
Builds a full-featured WinPE x64 image and automatically downloads +
injects the four NIC driver packs that cover ~95% of real hardware:
Intel - All Intel i210 / i219 / i225 / X550 / X710 adapters
Realtek - Onboard GbE and 2.5GbE (RTL8111 / RTL8125 / RTL8168)
Broadcom - Dell/HP server NICs and enterprise workstations
VirtIO - QEMU / KVM / Proxmox VMs (NetKVM, vioscsi, viostor)
Download sources:
Intel - Intel download CDN (page-scraped for current version)
Realtek - Microsoft Windows Update Catalog
Broadcom - Microsoft Windows Update Catalog
VirtIO - GitHub releases API (virtio-win/virtio-win)
All downloads are non-fatal. If one pack fails, the build continues
with whatever succeeded. Failed packs are listed in the summary.
All paths default to G:\
.REQUIREMENTS
- Windows 10/11 or Windows Server 2016+
- Windows ADK https://go.microsoft.com/fwlink/?linkid=2271336
- WinPE add-on https://go.microsoft.com/fwlink/?linkid=2271337
- Run as Administrator
- G:\ must be present with at least 8 GB free (driver packs add ~1.5 GB)
- Internet access to download NIC driver packs
.PARAMETER WorkDir
Temporary build workspace (wiped each run). Default: G:\WinPE_Build
.PARAMETER OutputDir
Final TFTP output directory. Default: G:\WinPE_Output
.PARAMETER ExtraDriversDir
Optional folder of additional .inf drivers to inject on top of the
downloaded packs. Default: G:\WinPE_Drivers (created if missing)
.PARAMETER SkipNICDownload
Skip all NIC pack downloads. Use if you already have packs in ExtraDriversDir
or want to build without internet access.
.PARAMETER Lang
Language code for optional-component language packs. Default: en-us
.PARAMETER ADKRoot
ADK install path override.
.EXAMPLE
.\Build-WinPE.ps1
.EXAMPLE
.\Build-WinPE.ps1 -SkipNICDownload -ExtraDriversDir "G:\MyDrivers"
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$WorkDir = 'G:\WinPE_Build',
[string]$OutputDir = 'G:\WinPE_Output',
[string]$ExtraDriversDir = 'G:\WinPE_Drivers',
[switch]$SkipNICDownload,
[string]$Lang = 'en-us',
[string]$ADKRoot = "${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# ── Console helpers ────────────────────────────────────────────────────────────
function Write-Step { param($m) Write-Host "`n==► $m" -ForegroundColor Cyan }
function Write-Ok { param($m) Write-Host " OK $m" -ForegroundColor Green }
function Write-Warn { param($m) Write-Host " !! $m" -ForegroundColor Yellow }
function Write-Info { param($m) Write-Host " .. $m" -ForegroundColor DarkCyan }
function Write-Fail { param($m) Write-Host "`n !! $m`n" -ForegroundColor Red; exit 1 }
# ── Derived paths ──────────────────────────────────────────────────────────────
$WinPERoot = Join-Path $ADKRoot 'Windows Preinstallation Environment'
$CopyPeCmd = Join-Path $WinPERoot 'copype.cmd'
$OcRoot = Join-Path $WinPERoot 'amd64\WinPE_OCs'
$NICPacksDir = Join-Path $WorkDir 'NICPacks' # downloaded + extracted packs land here
$WimPath = "$WorkDir\media\sources\boot.wim"
$MountPath = "$WorkDir\mount"
# Track which packs succeeded/failed for the final summary
$NICPackStatus = [ordered]@{
Intel = 'Pending'
Realtek = 'Pending'
Broadcom = 'Pending'
VirtIO = 'Pending'
}
# ==============================================================================
# HELPER: Windows Update Catalog driver downloader
#
# The WU Catalog is the most reliable source for Intel / Realtek / Broadcom
# because it is maintained by Microsoft, requires no account, and returns
# genuine signed driver CABs directly from the Windows Update CDN.
#
# Flow:
# 1. Search the catalog with a query string
# 2. Extract update GUIDs from the results HTML
# 3. POST each GUID to DownloadDialog.aspx to get the direct CAB URL
# 4. Download the CAB and extract it with Windows' built-in expand.exe
# ==============================================================================
function Get-WUCatalogDrivers {
param(
[string]$SearchQuery, # query to run on the catalog
[string]$DestDir, # folder to extract the CAB into
[string]$Label # friendly name for log output
)
Write-Info "[$Label] Searching Windows Update Catalog..."
$searchUri = 'https://www.catalog.update.microsoft.com/Search.aspx?q=' +
[Uri]::EscapeUriString($SearchQuery)
$session = $null
$searchHtml = $null
try {
$searchHtml = (Invoke-WebRequest -Uri $searchUri -UseBasicParsing `
-SessionVariable session -TimeoutSec 60 -ErrorAction Stop).Content
} catch {
Write-Warn "[$Label] Catalog search failed: $($_.Exception.Message)"
return $null
}
# Extract update GUIDs from goToDetails('guid') onclick handlers
$guids = [regex]::Matches($searchHtml,
"goToDetails\('([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})'\)") |
ForEach-Object { $_.Groups[1].Value } |
Select-Object -Unique -First 8
if ($guids.Count -eq 0) {
Write-Warn "[$Label] No catalog entries found for: $SearchQuery"
return $null
}
Write-Info "[$Label] Found $($guids.Count) entries, locating driver CAB..."
$postBase = "&updateIDsBlockedForImport=&wsusApiPresent=&contentImport=&sku=&serverName=&ssl=&portNumber=&version="
foreach ($guid in $guids) {
try {
$postBody = "updateIDs=[{`"size`":0,`"languages`":`"`",`"uidInfo`":`"$guid`",`"updateID`":`"$guid`"}]$postBase"
$dlHtml = (Invoke-WebRequest -Uri 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx' `
-Method POST -Body $postBody -WebSession $session `
-UseBasicParsing -TimeoutSec 30 -ErrorAction Stop).Content
# Download URLs appear as string literals in the response JS
$cabUrl = [regex]::Match($dlHtml, "https://[^'`"\s]+\.cab").Value
if (-not $cabUrl) { continue }
Write-Info "[$Label] Downloading CAB from Windows Update CDN..."
$cabFile = Join-Path $NICPacksDir "$Label.cab"
Invoke-WebRequest -Uri $cabUrl -OutFile $cabFile -UseBasicParsing `
-TimeoutSec 300 -ErrorAction Stop
# Extract CAB using Windows' built-in expand.exe
New-Item -ItemType Directory -Path $DestDir -Force | Out-Null
$expandOut = & expand.exe -F:* $cabFile $DestDir 2>&1
if (-not (Get-ChildItem $DestDir -Recurse -Filter '*.inf' -ErrorAction SilentlyContinue)) {
Write-Warn "[$Label] CAB extracted but contains no INF files — trying next entry..."
Remove-Item $DestDir -Recurse -Force -ErrorAction SilentlyContinue
continue
}
$infCount = (Get-ChildItem $DestDir -Recurse -Filter '*.inf').Count
Write-Ok "[$Label] $infCount INF file(s) extracted"
return $DestDir
} catch {
# Non-fatal — try the next GUID
continue
}
}
Write-Warn "[$Label] Could not find a usable driver CAB in any catalog entry."
return $null
}
# ==============================================================================
# DRIVER PACK: Intel
#
# Intel publishes their NIC drivers (PROWinx64) on their download mirror.
# We scrape the product page to get the current version's download URL so the
# script always fetches the latest without hardcoded version numbers.
# Falls back to Windows Update Catalog if the Intel page scrape fails.
# ==============================================================================
function Get-IntelNICDrivers {
param([string]$DestDir)
Write-Step 'Downloading Intel NIC driver pack'
$intelDest = Join-Path $NICPacksDir 'Intel_raw'
$intelExtract = Join-Path $NICPacksDir 'Intel'
# ── Attempt 1: scrape Intel download page for current ZIP URL ─────────────
try {
Write-Info '[Intel] Fetching current version from Intel download center...'
$page = (Invoke-WebRequest -Uri 'https://www.intel.com/content/www/us/en/download/15084/' `
-UseBasicParsing -TimeoutSec 30 -ErrorAction Stop).Content
# Intel embeds download links in the page HTML
# Try ZIP first (driver-only, no installer overhead)
$zipUrl = [regex]::Match($page,
'https://downloadmirror\.intel\.com/\d+/PROWinx64\.zip').Value
# Fall back to EXE if no ZIP found
$exeUrl = [regex]::Match($page,
'https://downloadmirror\.intel\.com/\d+/PROWinx64\.exe').Value
$downloadUrl = if ($zipUrl) { $zipUrl } elseif ($exeUrl) { $exeUrl } else { $null }
if ($downloadUrl) {
$ext = if ($zipUrl) { 'zip' } else { 'exe' }
$rawFile = Join-Path $NICPacksDir "PROWinx64.$ext"
$version = [regex]::Match($downloadUrl, 'downloadmirror\.intel\.com/(\d+)/').Groups[1].Value
Write-Info "[Intel] Version ID: $version — downloading $ext ($([math]::Round((Invoke-WebRequest $downloadUrl -Method Head -UseBasicParsing).Headers.'Content-Length'/1MB)) MB)..."
Invoke-WebRequest -Uri $downloadUrl -OutFile $rawFile -UseBasicParsing -TimeoutSec 600
New-Item -ItemType Directory -Path $intelExtract -Force | Out-Null
if ($ext -eq 'zip') {
Expand-Archive -Path $rawFile -DestinationPath $intelExtract -Force
} else {
# NSIS self-extracting EXE — try silent extract flag
$proc = Start-Process -FilePath $rawFile `
-ArgumentList "/s /f `"$intelExtract`"" -Wait -PassThru
if ($proc.ExitCode -ne 0 -or -not (Get-ChildItem $intelExtract -ErrorAction SilentlyContinue)) {
# Some Intel packages use a different flag
Start-Process -FilePath $rawFile `
-ArgumentList "-extract:`"$intelExtract`"" -Wait
}
}
# Intel puts WinPE-compatible drivers in the NDIS subfolder
$ndisPath = Get-ChildItem $intelExtract -Recurse -Filter 'NDIS' |
Where-Object { $_.PSIsContainer } | Select-Object -First 1
if ($ndisPath) {
Write-Ok "[Intel] NDIS folder found: $($ndisPath.FullName)"
$NICPackStatus['Intel'] = 'OK (Intel CDN)'
return $ndisPath.FullName
} else {
# NDIS folder not found — inject everything extracted
Write-Warn '[Intel] NDIS subfolder not found — will inject full extracted package.'
$NICPackStatus['Intel'] = 'OK (Intel CDN, full package)'
return $intelExtract
}
}
} catch {
Write-Warn "[Intel] Intel CDN attempt failed: $($_.Exception.Message)"
}
# ── Attempt 2: Windows Update Catalog ─────────────────────────────────────
Write-Info '[Intel] Falling back to Windows Update Catalog...'
$result = Get-WUCatalogDrivers `
-SearchQuery 'Intel Network Adapter Driver Windows 10 x64' `
-DestDir $intelExtract `
-Label 'Intel'
if ($result) {
$NICPackStatus['Intel'] = 'OK (WU Catalog)'
return $result
}
$NICPackStatus['Intel'] = 'FAILED'
return $null
}
# ==============================================================================
# DRIVER PACK: Realtek
#
# Realtek's download site requires JavaScript / CAPTCHA, so we go straight to
# the Windows Update Catalog which carries the identical signed drivers.
# Targets: RTL8111 / RTL8168 / RTL8125 (2.5G) / RTL8153 (USB) family.
# ==============================================================================
function Get-RealtekNICDrivers {
Write-Step 'Downloading Realtek NIC driver pack'
$realtekDest = Join-Path $NICPacksDir 'Realtek'
$result = Get-WUCatalogDrivers `
-SearchQuery 'Realtek PCIe GbE Family Controller Windows 10 x64' `
-DestDir $realtekDest `
-Label 'Realtek'
if ($result) {
$NICPackStatus['Realtek'] = 'OK (WU Catalog)'
return $result
}
# Alternate search if primary returns nothing useful
$result = Get-WUCatalogDrivers `
-SearchQuery 'Realtek USB FE Family Controller Windows 10 x64' `
-DestDir $realtekDest `
-Label 'Realtek-USB'
if ($result) {
$NICPackStatus['Realtek'] = 'OK (WU Catalog, USB variant)'
return $result
}
$NICPackStatus['Realtek'] = 'FAILED'
return $null
}
# ==============================================================================
# DRIVER PACK: Broadcom
#
# Covers Broadcom NetXtreme-I/II (BCM5720 / BCM5721 / BCM57XX) found in
# Dell PowerEdge servers, HP ProLiant, and many enterprise workstations.
# ==============================================================================
function Get-BroadcomNICDrivers {
Write-Step 'Downloading Broadcom NIC driver pack'
$broadcomDest = Join-Path $NICPacksDir 'Broadcom'
$result = Get-WUCatalogDrivers `
-SearchQuery 'Broadcom NetXtreme Gigabit Ethernet Windows 10 x64' `
-DestDir $broadcomDest `
-Label 'Broadcom'
if ($result) {
$NICPackStatus['Broadcom'] = 'OK (WU Catalog)'
return $result
}
$result = Get-WUCatalogDrivers `
-SearchQuery 'Broadcom NetXtreme-I Netlink Windows 10 x64' `
-DestDir $broadcomDest `
-Label 'Broadcom-II'
if ($result) {
$NICPackStatus['Broadcom'] = 'OK (WU Catalog, NetXtreme-I)'
return $result
}
$NICPackStatus['Broadcom'] = 'FAILED'
return $null
}
# ==============================================================================
# DRIVER PACK: VirtIO
#
# Fedora's virtio-win project maintains signed VirtIO drivers for Windows.
# We pull the latest ISO from GitHub releases and extract only the folders
# relevant to WinPE networking and storage:
#
# NetKVM - VirtIO NIC (QEMU/KVM/Proxmox)
# vioscsi - VirtIO SCSI controller
# viostor - VirtIO block storage
# vioserial - VirtIO serial (for some boot environments)
#
# The ISO is mounted using Windows' built-in Mount-DiskImage — no 7-zip needed.
# ==============================================================================
function Get-VirtIODrivers {
Write-Step 'Downloading VirtIO driver pack'
$virtioExtract = Join-Path $NICPacksDir 'VirtIO'
$isoPath = Join-Path $NICPacksDir 'virtio-win.iso'
try {
Write-Info '[VirtIO] Querying GitHub for latest virtio-win release...'
$release = Invoke-RestMethod `
-Uri 'https://api.github.com/repos/virtio-win/virtio-win/releases/latest' `
-Headers @{ 'User-Agent' = 'WinPE-Builder' } `
-TimeoutSec 30 -ErrorAction Stop
# Find the standalone ISO asset (not the RPM or virtio-win-gt ISO)
$isoAsset = $release.assets |
Where-Object { $_.name -match '^virtio-win-[\d\.]+\.iso$' } |
Select-Object -First 1
if (-not $isoAsset) {
Write-Warn '[VirtIO] Could not locate ISO asset in latest release.'
$NICPackStatus['VirtIO'] = 'FAILED'
return $null
}
$sizeMB = [math]::Round($isoAsset.size / 1MB)
Write-Info "[VirtIO] Downloading $($isoAsset.name) ($sizeMB MB)..."
Invoke-WebRequest -Uri $isoAsset.browser_download_url -OutFile $isoPath `
-UseBasicParsing -TimeoutSec 600 -ErrorAction Stop
Write-Info '[VirtIO] Mounting ISO...'
$mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru -ErrorAction Stop
$driveLetter = ($mountResult | Get-Volume).DriveLetter
if (-not $driveLetter) {
Write-Warn '[VirtIO] ISO mounted but drive letter not assigned.'
Dismount-DiskImage -ImagePath $isoPath | Out-Null
$NICPackStatus['VirtIO'] = 'FAILED'
return $null
}
New-Item -ItemType Directory -Path $virtioExtract -Force | Out-Null
# Driver subdirectories relevant to WinPE
# w10\amd64 works for both Windows 10 and 11 WinPE
$driverMap = [ordered]@{
'NetKVM\w10\amd64' = 'NetKVM' # VirtIO NIC
'vioscsi\w10\amd64' = 'vioscsi' # VirtIO SCSI
'viostor\w10\amd64' = 'viostor' # VirtIO block storage
'vioserial\w10\amd64' = 'vioserial' # VirtIO serial
}
$copied = 0
foreach ($src in $driverMap.Keys) {
$srcPath = Join-Path "${driveLetter}:\" $src
$destPath = Join-Path $virtioExtract $driverMap[$src]
if (Test-Path $srcPath) {
Copy-Item $srcPath $destPath -Recurse -Force
Write-Ok " [VirtIO] Copied: $src"
$copied++
} else {
Write-Warn " [VirtIO] Not found in ISO: $src"
}
}
Dismount-DiskImage -ImagePath $isoPath | Out-Null
Write-Ok "[VirtIO] $copied driver folder(s) extracted"
if ($copied -gt 0) {
$NICPackStatus['VirtIO'] = "OK (GitHub — $($release.tag_name))"
return $virtioExtract
} else {
$NICPackStatus['VirtIO'] = 'FAILED (no folders extracted from ISO)'
return $null
}
} catch {
# If ISO is still mounted, clean up
if (Test-Path $isoPath) {
Dismount-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue | Out-Null
}
Write-Warn "[VirtIO] Failed: $($_.Exception.Message)"
$NICPackStatus['VirtIO'] = 'FAILED'
return $null
}
}
# ==============================================================================
# STEP 1 — Prerequisites
# ==============================================================================
Write-Step 'Checking prerequisites'
if (-not (Test-Path 'G:\')) {
Write-Fail 'G:\ is not available. Attach or map the drive before running.'
}
Write-Ok 'G:\ is present'
if (-not (Test-Path $WinPERoot)) {
Write-Fail @"
WinPE add-on not found at: $WinPERoot
Install in order:
ADK https://go.microsoft.com/fwlink/?linkid=2271336
WinPE add-on https://go.microsoft.com/fwlink/?linkid=2271337
"@
}
if (-not (Test-Path $CopyPeCmd)) {
Write-Fail "copype.cmd not found: $CopyPeCmd — reinstall the WinPE add-on."
}
if (-not (Test-Path $OcRoot)) {
Write-Fail "WinPE optional components not found: $OcRoot"
}
$gFreeGB = [math]::Round((Get-PSDrive G).Free / 1GB, 1)
if ($gFreeGB -lt 5) {
Write-Warn "G:\ has only ${gFreeGB} GB free — 8 GB recommended with NIC packs."
} else {
Write-Ok "G:\ free space : ${gFreeGB} GB"
}
Write-Ok "ADK root : $ADKRoot"
Write-Ok "WinPE add-on : $WinPERoot"
# ==============================================================================
# STEP 2 — Directories
# ==============================================================================
Write-Step 'Preparing directories on G:\'
if (Test-Path $WorkDir) {
Write-Warn "Cleaning work directory: $WorkDir"
Get-WindowsImage -Mounted -ErrorAction SilentlyContinue |
Where-Object { $_.Path -like "$WorkDir*" } |
ForEach-Object { Dismount-WindowsImage -Path $_.Path -Discard | Out-Null }
Remove-Item $WorkDir -Recurse -Force
}
@($WorkDir, $OutputDir, $ExtraDriversDir, $NICPacksDir) |
ForEach-Object { New-Item -ItemType Directory -Path $_ -Force | Out-Null }
Write-Ok "WorkDir : $WorkDir"
Write-Ok "OutputDir : $OutputDir"
Write-Ok "ExtraDriversDir: $ExtraDriversDir"
Write-Ok "NICPacksDir : $NICPacksDir"
# ==============================================================================
# STEP 3 — Download NIC driver packs
# ==============================================================================
$injectionPaths = [System.Collections.Generic.List[string]]::new()
if ($SkipNICDownload) {
Write-Step 'Skipping NIC pack downloads (-SkipNICDownload)'
$NICPackStatus['Intel'] = $NICPackStatus['Realtek'] = `
$NICPackStatus['Broadcom'] = $NICPackStatus['VirtIO'] = 'Skipped'
} else {
Write-Host "`n Downloading NIC driver packs — failures are non-fatal." -ForegroundColor DarkCyan
Write-Host " Internet access required. This may take several minutes." -ForegroundColor DarkCyan
$intelPath = Get-IntelNICDrivers
$realtekPath = Get-RealtekNICDrivers
$broadcomPath = Get-BroadcomNICDrivers
$virtioPath = Get-VirtIODrivers
foreach ($p in @($intelPath, $realtekPath, $broadcomPath, $virtioPath)) {
if ($p) { $injectionPaths.Add($p) }
}
Write-Step 'NIC pack download results'
foreach ($k in $NICPackStatus.Keys) {
$status = $NICPackStatus[$k]
if ($status -like 'OK*') { Write-Ok "$k : $status" }
elseif ($status -eq 'FAILED'){ Write-Warn "$k : FAILED (see warnings above)" }
else { Write-Info "$k : $status" }
}
}
# Add any extra drivers from ExtraDriversDir
$extraCount = (Get-ChildItem $ExtraDriversDir -Recurse -Filter '*.inf' -ErrorAction SilentlyContinue).Count
if ($extraCount -gt 0) {
Write-Info "ExtraDriversDir: $extraCount additional INF(s) will be injected"
$injectionPaths.Add($ExtraDriversDir)
}
# ==============================================================================
# STEP 4 — copype
# ==============================================================================
Write-Step 'Running copype amd64 — creating WinPE base on G:\'
$cpResult = & cmd.exe /c "`"$CopyPeCmd`" amd64 `"$WorkDir`"" 2>&1
if ($LASTEXITCODE -ne 0) { Write-Fail "copype.cmd failed (exit $LASTEXITCODE):`n$cpResult" }
Write-Ok 'WinPE base structure created'
# ==============================================================================
# STEP 5 — Mount boot.wim
# ==============================================================================
if (-not (Test-Path $WimPath)) { Write-Fail "boot.wim not found: $WimPath" }
New-Item -ItemType Directory -Path $MountPath -Force | Out-Null
Write-Step 'Mounting boot.wim'
Mount-WindowsImage -ImagePath $WimPath -Index 1 -Path $MountPath | Out-Null
Write-Ok "Mounted: $MountPath"
# ==============================================================================
# STEP 6 — Optional packages
# ==============================================================================
function Add-WinPEPackage {
param([string]$Name)
$pkg = Join-Path $OcRoot "$Name.cab"
$langPkg = Join-Path $OcRoot "$Lang\${Name}_${Lang}.cab"
if (-not (Test-Path $pkg)) { Write-Warn "Package not found, skipping: $Name"; return }
try {
Add-WindowsPackage -Path $MountPath -PackagePath $pkg -WarningAction SilentlyContinue | Out-Null
Write-Ok " $Name"
} catch { Write-Warn " $Name — $($_.Exception.Message)" }
if (Test-Path $langPkg) {
try {
Add-WindowsPackage -Path $MountPath -PackagePath $langPkg -WarningAction SilentlyContinue | Out-Null
Write-Ok " $Name [$Lang]"
} catch { Write-Warn " $Name [$Lang] language pack — non-fatal" }
}
}
Write-Step 'Installing optional WinPE packages'
# Core — install dependencies first
Add-WinPEPackage 'WinPE-WMI'
Add-WinPEPackage 'WinPE-NetFX'
Add-WinPEPackage 'WinPE-Scripting'
Add-WinPEPackage 'WinPE-PowerShell' # requires WMI + NetFX + Scripting
# Deployment
Add-WinPEPackage 'WinPE-DismCmdlets' # requires PowerShell
Add-WinPEPackage 'WinPE-StorageWMI' # requires WMI
Add-WinPEPackage 'WinPE-HTA'
Add-WinPEPackage 'WinPE-SecureStartup'
Add-WinPEPackage 'WinPE-WDS-Tools' # skipped gracefully if absent on this ADK
# Disk imaging / recovery
Add-WinPEPackage 'WinPE-EnhancedStorage'
Add-WinPEPackage 'WinPE-FMAPI'
# Diagnostics / networking
Add-WinPEPackage 'WinPE-Dot3Svc'
Add-WinPEPackage 'WinPE-RNDIS'
Write-Ok 'All packages processed'
# ==============================================================================
# STEP 7 — Inject NIC drivers + extra drivers
# ==============================================================================
Write-Step 'Injecting NIC driver packs into WIM'
if ($injectionPaths.Count -eq 0) {
Write-Warn 'No driver paths to inject — WIM will have inbox drivers only.'
} else {
foreach ($driverPath in $injectionPaths) {
$infCount = (Get-ChildItem $driverPath -Recurse -Filter '*.inf' -ErrorAction SilentlyContinue).Count
if ($infCount -eq 0) {
Write-Warn "No INF files found in $driverPath — skipping."
continue
}
Write-Info "Injecting $infCount INF(s) from: $driverPath"
try {
Add-WindowsDriver -Path $MountPath -Driver $driverPath `
-Recurse -ForceUnsigned -ErrorAction Stop | Out-Null
Write-Ok " Injected: $(Split-Path $driverPath -Leaf) ($infCount INFs)"
} catch {
Write-Warn " Injection warning for $driverPath`: $($_.Exception.Message)"
}
}
}
# ==============================================================================
# STEP 8 — startnet.cmd
# ==============================================================================
Write-Step 'Writing startnet.cmd'
@'
@echo off
:: ===========================================================================
:: WinPE startup — edit the SMB block below for your environment
:: ===========================================================================
echo.
echo [WinPE] Initializing hardware and network...
wpeinit
:: Wait for NIC link-up
ping -n 5 127.0.0.1 > nul
echo [WinPE] Network status:
ipconfig
:: ---------------------------------------------------------------------------
:: Map deployment SMB share — uncomment ONE option
::
:: A) Anonymous share
:: net use Z: \\192.168.1.10\DeploymentShare$
::
:: B) Static domain credentials
:: net use Z: \\192.168.1.10\DeploymentShare$ /user:DOMAIN\svcDeploy P@ssw0rd
::
:: C) Prompt for password
:: net use Z: \\192.168.1.10\DeploymentShare$ /user:DOMAIN\svcDeploy *
:: ---------------------------------------------------------------------------
:: net use Z: \\192.168.1.10\DeploymentShare$
:: if errorlevel 1 ( echo [ERROR] Could not map share & pause & goto :shell )
:: -- MDT LiteTouch ----------------------------------------------------------
:: echo [WinPE] Launching LiteTouch...
:: Z:\Scripts\LiteTouch.vbs
:: -- Custom PowerShell script -----------------------------------------------
:: powershell.exe -ExecutionPolicy Bypass -File Z:\Scripts\Deploy.ps1
:shell
echo.
echo [WinPE] Ready. Mapped drives:
net use
echo.
echo Type EXIT to reboot.
cmd.exe /k
'@ | Set-Content -Path "$MountPath\Windows\System32\startnet.cmd" -Encoding ASCII
Write-Ok 'startnet.cmd written'
# ==============================================================================
# STEP 9 — Scratch space
# ==============================================================================
Write-Step 'Setting scratch space to 512 MB'
Set-WindowsImage -Path $MountPath -ScratchSpaceSize 512 | Out-Null
Write-Ok 'Scratch space: 512 MB'
# ==============================================================================
# STEP 10 — Cleanup inside WIM
# ==============================================================================
Write-Step 'Removing logs and temp files from WIM'
@("$MountPath\Windows\Logs\*", "$MountPath\Windows\Temp\*", "$MountPath\Windows\CBS\*") |
ForEach-Object { Remove-Item $_ -Recurse -Force -ErrorAction SilentlyContinue }
Write-Ok 'WIM cleaned'
# ==============================================================================
# STEP 11 — Unmount and commit
# ==============================================================================
Write-Step 'Unmounting WIM and committing'
Dismount-WindowsImage -Path $MountPath -Save | Out-Null
Write-Ok 'WIM committed'
# ==============================================================================
# STEP 12 — Export optimized WIM
# ==============================================================================
Write-Step 'Exporting WIM with maximum compression'
$optimized = "$WorkDir\boot_opt.wim"
Export-WindowsImage -SourceImagePath $WimPath -SourceIndex 1 `
-DestinationImagePath $optimized -CompressionType Maximum | Out-Null
Remove-Item $WimPath -Force
Move-Item $optimized $WimPath
Write-Ok 'WIM optimized'
# ==============================================================================
# STEP 13 — Build TFTP output tree
# ==============================================================================
Write-Step 'Building TFTP output tree on G:\'
$TftpRoot = Join-Path $OutputDir 'tftp'
@("$TftpRoot\Boot", "$TftpRoot\EFI\Boot", "$TftpRoot\EFI\Microsoft\Boot") |
ForEach-Object { New-Item -ItemType Directory -Path $_ -Force | Out-Null }
Copy-Item "$WorkDir\media\bootmgr" "$TftpRoot\bootmgr" -Force
Copy-Item "$WorkDir\media\Boot\BCD" "$TftpRoot\Boot\BCD" -Force
Copy-Item "$WorkDir\media\Boot\boot.sdi" "$TftpRoot\Boot\boot.sdi" -Force
Copy-Item "$WorkDir\media\sources\boot.wim" "$TftpRoot\Boot\boot.wim" -Force
@{
"$WorkDir\media\bootmgr.efi" = "$TftpRoot\bootmgr.efi"
"$WorkDir\media\EFI\Boot\bootx64.efi" = "$TftpRoot\EFI\Boot\bootx64.efi"
"$WorkDir\media\EFI\Microsoft\Boot\BCD" = "$TftpRoot\EFI\Microsoft\Boot\BCD"
}.GetEnumerator() | ForEach-Object {
if (Test-Path $_.Key) { Copy-Item $_.Key $_.Value -Force; Write-Ok " $(Split-Path $_.Key -Leaf)" }
else { Write-Warn " Not found (non-fatal): $(Split-Path $_.Key -Leaf)" }
}
Write-Ok "TFTP tree ready: $TftpRoot"
# ==============================================================================
# STEP 14 — Summary
# ==============================================================================
$wimSizeMB = [math]::Round((Get-Item "$TftpRoot\Boot\boot.wim").Length / 1MB, 0)
Write-Host @"
============================================================
WinPE x64 Build Complete
============================================================
TFTP output : $TftpRoot
boot.wim : ${wimSizeMB} MB
NIC driver pack results:
"@ -ForegroundColor Cyan
foreach ($k in $NICPackStatus.Keys) {
$status = $NICPackStatus[$k]
if ($status -like 'OK*') { Write-Host " OK $k : $status" -ForegroundColor Green }
elseif ($status -eq 'FAILED') { Write-Host " !! $k : FAILED" -ForegroundColor Yellow }
else { Write-Host " -- $k : $status" -ForegroundColor DarkCyan }
}
Write-Host @"
If any pack shows FAILED:
- Check internet connectivity and re-run
- Or download manually and place INF/SYS/CAT files in G:\WinPE_Drivers,
then re-run with -SkipNICDownload to skip re-downloading
Place wimboot in the TFTP root before booting:
https://github.com/ipxe/wimboot/releases/latest/download/wimboot
TFTP layout:
tftp\
├── wimboot
├── bootmgr / bootmgr.efi
├── Boot\ BCD boot.sdi boot.wim (${wimSizeMB} MB)
└── EFI\ Boot\bootx64.efi Microsoft\Boot\BCD
To update startnet.cmd without a full rebuild:
Mount-WindowsImage -ImagePath '$TftpRoot\Boot\boot.wim' -Index 1 -Path G:\WinPE_Mount
notepad G:\WinPE_Mount\Windows\System32\startnet.cmd
Dismount-WindowsImage -Path G:\WinPE_Mount -Save
"@ -ForegroundColor Cyan