Create WinPEScript
This commit is contained in:
+821
@@ -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
|
||||
Reference in New Issue
Block a user