diff --git a/WinPEScript b/WinPEScript new file mode 100644 index 0000000..27a1150 --- /dev/null +++ b/WinPEScript @@ -0,0 +1,821 @@ +#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