Files
assets/install-scripts/Build-OSDCloudSurface-x64.ps1
2026-09-18 20:27:52 +02:00

258 lines
12 KiB
PowerShell

<#
.SYNOPSIS
Builds (and maintains) an OSDCloud x64 deployment USB for Surface devices on a WinRE base.
.DESCRIPTION
Run as Administrator on an x64 Windows 11 machine, preferably an Intel Surface:
New-OSDCloudTemplate -WinRE uses the WinRE of the machine it runs on, and the WinRE
of a Surface already contains Microsoft's own Surface drivers.
The script, in order:
1. Checks prerequisites (x64, admin, ADK + WinPE add-on, OSD module, WinRE enabled)
2. Creates the WinRE template once (skipped if it already exists)
3. Creates the workspace once (skipped if it already exists)
4. Runs Edit-OSDCloudWinPE with all driver folders, the Wi-Fi profile, WirelessConnect
and a fixed Start-OSDCloud start line (edition, language, activation)
5. Verifies boot.wim (WLAN stack present, no VHF HidMini, key drivers present)
6. Updates the USB stick if an OSDCloudUSB volume is present
7. Optionally downloads the Windows image to the stick (-UpdateOS)
.PARAMETER DriverRoot
Folder with one subfolder per model containing .inf drivers for WinPE. All subfolders are
included. Example: C:\Drivers\IntelWiFi24, C:\Drivers\SL5-WinPE, C:\Drivers\SL6-WinPE.
.PARAMETER WifiProfile
Path to the exported Wi-Fi profile (netsh wlan export profile name=<SSID> key=clear).
.PARAMETER ZTI
Adds -ZTI to Start-OSDCloud: no confirmation, the disk is wiped immediately.
.PARAMETER UpdateOS
Download/refresh the Windows image on the stick (Update-OSDCloudUSB -OSName ...).
.PARAMETER SkipArchCheck
Allow running on an ARM64 host, only when the x64 template already exists. Editing an
existing x64 wim works on any architecture; only creating the WinRE template does not.
.PARAMETER IncludePnPDump
Adds a StartNet script to the wim that logs PnP device status to the stick on every boot
(OSDCloud\Logs\PnP-*.txt). Useful for troubleshooting, costs a few seconds of boot time.
.EXAMPLE
# First run, on an Intel Surface:
.\Build-OSDCloudSurface-x64.ps1 -UpdateOS
.EXAMPLE
# Monthly maintenance (new OSD module, refreshed image), on any machine:
Update-Module OSD -Force
.\Build-OSDCloudSurface-x64.ps1 -UpdateOS -SkipArchCheck
.NOTES
Deliberately NOT used: -CloudDriver Surface. That pack contains surface_hid_mini.inf 0.9.60.0,
the VHF variant of the Surface HidMini driver, which does not start in WinPE and blocks the
keyboard on the Surface Laptop series. Surface drivers come from the MSI folders in DriverRoot.
Per-model WinPE driver folder lists:
https://learn.microsoft.com/surface/enable-surface-keyboard-for-windows-pe-deployment
#>
[CmdletBinding()]
param (
[string] $TemplateName = 'SurfaceX64',
[string] $WorkspacePath = 'C:\OSDCloud\SurfaceX64',
[string] $DriverRoot = 'C:\Drivers',
[string] $WifiProfile = 'C:\Wifi\WiFiProfile.xml',
[string] $OSEdition = 'Enterprise',
[string] $OSLanguage = 'en-us',
[string] $OSActivation = 'Volume',
[switch] $ZTI,
[switch] $UpdateOS,
[switch] $IncludePnPDump,
[switch] $SkipArchCheck
)
$ErrorActionPreference = 'Stop'
function Step($t) { Write-Host "`n=== $t ===" -ForegroundColor Cyan }
function Ok($t) { Write-Host " [OK] $t" -ForegroundColor Green }
function Warn($t) { Write-Host " [!!] $t" -ForegroundColor Yellow }
function Fail($t) { Write-Host " [XX] $t" -ForegroundColor Red; exit 1 }
# ------------------------------------------------------------------
Step '1. Prerequisites'
# ------------------------------------------------------------------
if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') {
if ($SkipArchCheck) {
Warn "$($env:PROCESSOR_ARCHITECTURE) host: can only edit an existing x64 template, cannot create a new -WinRE template."
} else {
Fail "This is a $($env:PROCESSOR_ARCHITECTURE) machine. The WinRE template must be created once on x64 Windows. If the template already exists, use -SkipArchCheck."
}
} else { Ok 'x64 host' }
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators')
if (-not $isAdmin) { Fail 'Run PowerShell as Administrator.' }
Ok 'Administrator'
$TemplatePath = "$env:ProgramData\OSDCloud\Templates\$TemplateName"
$TemplateExists = Test-Path "$TemplatePath\Media\sources\boot.wim"
if (-not $TemplateExists) {
if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') { Fail 'No existing x64 template and this is not an x64 host. Create the template on x64 Windows first.' }
$re = reagentc /info 2>&1 | Out-String
if ($re -notmatch 'Enabled') { Fail 'WinRE is not Enabled on this machine (reagentc /info). A -WinRE template cannot be created.' }
Ok 'WinRE enabled on this machine'
}
$adk = "${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit"
if (-not (Test-Path "$adk\Deployment Tools")) {
Warn 'ADK not found, installing via winget...'
winget install Microsoft.WindowsADK --silent --accept-package-agreements --accept-source-agreements | Out-Null
}
if (-not (Test-Path "$adk\Windows Preinstallation Environment\amd64")) {
Warn 'WinPE add-on not found, installing via winget...'
winget install Microsoft.ADKPEAddon --silent --accept-package-agreements --accept-source-agreements | Out-Null
}
if (-not (Test-Path "$adk\Windows Preinstallation Environment\amd64")) { Fail 'ADK / WinPE add-on still missing.' }
Ok 'ADK + WinPE add-on'
if (-not (Get-Module OSD -ListAvailable)) {
Warn 'OSD module not found, installing...'
Install-Module OSD -Force -Scope AllUsers
}
Import-Module OSD -Force
Ok "OSD module $((Get-Module OSD).Version)"
if (-not (Test-Path $WifiProfile)) { Fail "Wi-Fi profile not found: $WifiProfile" }
$wifiXml = Get-Content $WifiProfile -Raw
if ($wifiXml -notmatch '<protected>false</protected>') { Fail 'Wi-Fi profile was exported encrypted. Re-export as Administrator with key=clear.' }
Ok "Wi-Fi profile: $WifiProfile"
$DriverPaths = @(Get-ChildItem $DriverRoot -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
if ($DriverPaths.Count -eq 0) { Fail "No driver folders found under $DriverRoot" }
foreach ($p in $DriverPaths) {
$n = (Get-ChildItem $p -Recurse -Filter *.inf).Count
if ($n -eq 0) { Warn "$p contains no .inf files" } else { Ok "$p ($n inf)" }
}
# Start-OSDCloud OSName: pick the 25H2 x64 name this OSD version knows
$valid = (Get-Command Start-OSDCloud).Parameters['OSName'].Attributes |
Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } |
Select-Object -ExpandProperty ValidValues
$OSName = $valid | Where-Object { $_ -match '^Windows 11 25H2' -and $_ -notmatch 'ARM' } | Select-Object -First 1
if (-not $OSName) { Fail "No 'Windows 11 25H2' entry in the Start-OSDCloud OSName list. Update the OSD module." }
Ok "OSName for Start-OSDCloud: '$OSName'"
# ------------------------------------------------------------------
Step "2. Template '$TemplateName' (WinRE)"
# ------------------------------------------------------------------
if ($TemplateExists) {
Ok 'Template already exists, skipping'
} else {
Write-Host ' This takes 10 to 20 minutes...'
New-OSDCloudTemplate -Name $TemplateName -WinRE
}
Set-OSDCloudTemplate -Name $TemplateName | Out-Null
# ------------------------------------------------------------------
Step "3. Workspace $WorkspacePath"
# ------------------------------------------------------------------
if (Test-Path "$WorkspacePath\Media\sources\boot.wim") {
Ok 'Workspace already exists, skipping'
} else {
New-OSDCloudWorkspace -WorkspacePath $WorkspacePath
}
Set-OSDCloudWorkspace -WorkspacePath $WorkspacePath | Out-Null
# Optional: PnP dump script in Config\Scripts\StartNet (ends up in the wim and on the stick)
$startNet = "$WorkspacePath\Config\Scripts\StartNet"
$dump = "$startNet\PnPDump.ps1"
if ($IncludePnPDump) {
New-Item $startNet -ItemType Directory -Force | Out-Null
@'
$usb = (Get-Volume | Where-Object FileSystemLabel -eq 'OSDCloudUSB').DriveLetter
if (-not $usb) { return }
$log = "${usb}:\OSDCloud\Logs\PnP-$(Get-Date -Format yyyyMMdd-HHmmss).txt"
New-Item (Split-Path $log) -ItemType Directory -Force | Out-Null
"=== DEVICES WITH PROBLEMS ===" | Set-Content $log
Get-CimInstance Win32_PnPEntity | Where-Object Status -ne 'OK' |
Select-Object Status, ConfigManagerErrorCode, Name, DeviceID |
Format-Table -AutoSize | Out-String -Width 250 | Add-Content $log
"=== NET ADAPTERS ===" | Add-Content $log
Get-NetAdapter | Format-List Name, InterfaceDescription, Status, MacAddress, DriverVersion | Out-String | Add-Content $log
"=== WLAN ===" | Add-Content $log
netsh wlan show drivers 2>&1 | Add-Content $log
netsh wlan show interfaces 2>&1 | Add-Content $log
'@ | Set-Content $dump -Encoding UTF8
Ok 'PnPDump.ps1 added to StartNet'
} elseif (Test-Path $dump) {
Remove-Item $dump -Force
Ok 'PnPDump.ps1 removed from StartNet'
}
# ------------------------------------------------------------------
Step '4. Edit-OSDCloudWinPE'
# ------------------------------------------------------------------
$startArgs = "-OSName `"$OSName`" -OSEdition $OSEdition -OSLanguage $OSLanguage -OSActivation $OSActivation"
if ($ZTI) { $startArgs += ' -ZTI'; Warn 'ZTI enabled: no confirmation, disk is wiped immediately' }
Write-Host " Start-OSDCloud $startArgs"
Edit-OSDCloudWinPE `
-DriverPath $DriverPaths `
-CloudDriver USB `
-WifiProfile $WifiProfile `
-WirelessConnect `
-StartOSDCloud $startArgs
# ------------------------------------------------------------------
Step '5. Verify boot.wim'
# ------------------------------------------------------------------
$Wim = "$WorkspacePath\Media\sources\boot.wim"
$Mnt = "$env:TEMP\OSDCloudCheck"
New-Item $Mnt -ItemType Directory -Force | Out-Null
try {
Mount-WindowsImage -ImagePath $Wim -Index 1 -Path $Mnt -ReadOnly | Out-Null
if (Test-Path "$Mnt\Windows\System32\wlanapi.dll") { Ok 'WLAN stack present (WinRE base)' }
else { Warn 'NO wlanapi.dll: this is not a WinRE base, Wi-Fi will not work' }
$drv = Get-WindowsDriver -Path $Mnt
if ($drv | Where-Object OriginalFileName -match 'surface_hid_mini') {
Warn 'surface_hid_mini.inf (VHF variant) is in the wim. Remove it or the Surface keyboard will not work.'
} else { Ok 'No VHF HidMini in the wim' }
foreach ($k in 'surfacehidminidriver','surfaceserialhub','surfaceintegration','netwtw08','ialpss2_uart2') {
$hit = $drv | Where-Object OriginalFileName -match $k | Select-Object -First 1
if ($hit) { Ok "$k ($($hit.Version))" } else { Warn "$k not found" }
}
if (Test-Path "$Mnt\OSDCloud\Config\Scripts\WiFiProfile.xml") { Ok 'WiFiProfile.xml in the wim' } else { Warn 'WiFiProfile.xml missing' }
if (Test-Path "$Mnt\Windows\System32\WirelessConnect.exe") { Ok 'WirelessConnect.exe in the wim' } else { Warn 'WirelessConnect.exe missing' }
$sn = Get-Content "$Mnt\Windows\System32\startnet.cmd" -Raw
if ($sn -match 'Start-OSDCloud ') { Ok 'startnet.cmd launches Start-OSDCloud with fixed parameters' } else { Warn 'startnet.cmd has no Start-OSDCloud line' }
}
finally {
Dismount-WindowsImage -Path $Mnt -Discard | Out-Null
}
Write-Host (" boot.wim: {0:N0} MB" -f ((Get-Item $Wim).Length / 1MB))
# ------------------------------------------------------------------
Step '6. USB stick'
# ------------------------------------------------------------------
$usb = Get-Volume | Where-Object FileSystemLabel -eq 'OSDCloudUSB'
if ($usb) {
Update-OSDCloudUSB
Ok "Stick updated ($($usb.DriveLetter):)"
if ($UpdateOS) {
Step '7. Windows image on the stick'
$shortName = ($OSName -replace ' x64','').Trim()
Update-OSDCloudUSB -OSName $shortName -OSLanguage $OSLanguage -OSActivation $OSActivation
# remove old ESDs, keep the newest
Get-ChildItem "$($usb.DriveLetter):\OSDCloud\OS" -Recurse -Filter *.esd -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -Skip 1 | Remove-Item -Force
Ok 'Image updated, old ESD files removed'
}
Write-Host (" Free on stick: {0:N1} GB" -f ((Get-Volume -DriveLetter $usb.DriveLetter).SizeRemaining / 1GB))
} else {
Warn 'No stick with label OSDCloudUSB found. First time: run New-OSDCloudUSB, then run this script again.'
}
Write-Host "`nDone. Test on an Intel Surface without a cable: keyboard, Wi-Fi, local image, confirmation prompt." -ForegroundColor Green