feat: Add option to dynamically build PE drivers

Introduces a new feature, `UseDriversAsPEDrivers`, that allows WinPE drivers to be sourced directly from the main driver repository.

When enabled, the script scans all available drivers, parses their INF files, and copies only the essential driver types (e.g., storage, mouse, keyboard, touchpad, system devices) needed for WinPE. This eliminates the need to maintain a separate, manually curated `PEDrivers` folder.

The UI is updated with a new checkbox that becomes visible when "Copy PE Drivers" is selected, making this a sub-option. Parameter validation is also adjusted to support this new workflow.
This commit is contained in:
rbalsleyMSFT
2025-09-11 12:13:06 -07:00
parent f3316a017b
commit e2ccd11f07
6 changed files with 211 additions and 13 deletions
+178 -3
View File
@@ -52,7 +52,7 @@ When set to $true, will copy the $FFUDevelopmentPath\Autopilot folder to the Dep
When set to $true, will copy the drivers from the $FFUDevelopmentPath\Drivers folder to the Drivers folder on the deploy partition of the USB drive. Default is $false.
.PARAMETER CopyPEDrivers
When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false.
When set to $true, enables adding WinPE drivers. By default copies drivers from $FFUDevelopmentPath\PEDrivers to the WinPE deployment media unless -UseDriversAsPEDrivers is also $true.
.PARAMETER CopyPPKG
When set to $true, will copy the provisioning package from the $FFUDevelopmentPath\PPKG folder to the Deployment partition of the USB drive. Default is $false.
@@ -132,7 +132,7 @@ When set to $true, will optimize the FFU file. Default is $true.
.PARAMETER OptionalFeatures
Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP).
.PARAMETER orchestrationPath
.PARAMETER OrchestrationPath
Path to the orchestration folder containing scripts that run inside the VM. Default is $FFUDevelopmentPath\Apps\Orchestration.
.PARAMETER PEDriversFolder
@@ -186,6 +186,9 @@ When set to $true, will download and install the latest OneDrive and install it
.PARAMETER UpdatePreviewCU
When set to $true, will download and install the latest Preview cumulative update. Default is $false.
.PARAMETER UseDriversAsPEDrivers
When set to $true (and -CopyPEDrivers is also $true), bypasses the contents of $FFUDevelopmentPath\PEDrivers and instead builds the WinPE driver set dynamically from the $DriversFolder path, copying only the required WinPE drivers. Has no effect if -CopyPEDrivers is not specified. Default is $false.
.PARAMETER UserAppListPath
Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json.
@@ -378,6 +381,7 @@ param(
[bool]$CompressDownloadedDriversToWim = $false,
[bool]$CopyDrivers,
[bool]$CopyPEDrivers,
[bool]$UseDriversAsPEDrivers,
[bool]$RemoveFFU,
[bool]$UpdateLatestCU,
[bool]$UpdatePreviewCU,
@@ -553,6 +557,26 @@ class VhdxCacheItem {
[VhdxCacheUpdateItem[]]$IncludedUpdates = @()
}
#Support for ini reading
$definition = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
string lpAppName,
string lpKeyName,
string lpDefault,
System.Text.StringBuilder lpReturnedString,
uint nSize,
string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern uint GetPrivateProfileSection(
string lpAppName,
byte[] lpReturnedString,
uint nSize,
string lpFileName);
'@
Add-Type -MemberDefinition $definition -Namespace Win32 -Name Kernel32 -PassThru
#Check if Hyper-V feature is installed (requires only checks the module)
$osInfo = Get-WmiObject -Class Win32_OperatingSystem
$isServer = $osInfo.Caption -match 'server'
@@ -2625,6 +2649,108 @@ Function Set-CaptureFFU {
}
}
function Get-PrivateProfileString {
param (
[Parameter()]
[string]$FileName,
[Parameter()]
[string]$SectionName,
[Parameter()]
[string]$KeyName
)
$sbuilder = [System.Text.StringBuilder]::new(1024)
[void][Win32.Kernel32]::GetPrivateProfileString($SectionName, $KeyName, "", $sbuilder, $sbuilder.Capacity, $FileName)
return $sbuilder.ToString()
}
function Get-PrivateProfileSection {
param (
[Parameter()]
[string]$FileName,
[Parameter()]
[string]$SectionName
)
$buffer = [byte[]]::new(16384)
[void][Win32.Kernel32]::GetPrivateProfileSection($SectionName, $buffer, $buffer.Length, $FileName)
$keyValues = [System.Text.Encoding]::Unicode.GetString($buffer).TrimEnd("`0").Split("`0")
$hashTable = @{}
foreach ($keyValue in $keyValues) {
if (![string]::IsNullOrEmpty($keyValue)) {
$parts = $keyValue -split "="
$hashTable[$parts[0]] = $parts[1]
}
}
return $hashTable
}
function Copy-Drivers {
param (
[Parameter()]
[string]$Path,
[Parameter()]
[string]$Output
)
# Find more information about device classes here:
# https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors
# For now, included are system devices, scsi and raid controllers, keyboards, mice and HID devices for touch support
# 4D36E97D-E325-11CE-BFC1-08002BE10318 = System devices
# 4D36E97B-E325-11CE-BFC1-08002BE10318 = SCSI, RAID, and NVMe Controllers
# 4d36e96b-e325-11ce-bfc1-08002be10318 = Keyboards
# 4d36e96f-e325-11ce-bfc1-08002be10318 = Mice and other pointing devices
# 745a17a0-74d3-11d0-b6fe-00a0c90f57da = Human Interface Devices
$filterGUIDs = @("{4D36E97D-E325-11CE-BFC1-08002BE10318}", "{4D36E97B-E325-11CE-BFC1-08002BE10318}", "{4d36e96b-e325-11ce-bfc1-08002be10318}", "{4d36e96f-e325-11ce-bfc1-08002be10318}", "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}")
$exclusionList = "wdmaudio.inf|Sound|Machine Learning|Camera|Firmware"
$pathLength = $Path.Length
$infFiles = Get-ChildItem -Path $Path -Recurse -Filter "*.inf"
for ($i = 0; $i -lt $infFiles.Count; $i++) {
$infFullName = $infFiles[$i].FullName
$infPath = Split-Path -Path $infFullName
$childPath = $infPath.Substring($pathLength)
$targetPath = Join-Path -Path $Output -ChildPath $childPath
if ((Get-PrivateProfileString -FileName $infFullName -SectionName "version" -KeyName "ClassGUID") -in $filterGUIDs) {
#Avoid drivers that reference keywords from the exclusion list to keep the total size small
if (((Get-Content -Path $infFullName) -match $exclusionList).Length -eq 0) {
$providerName = (Get-PrivateProfileString -FileName $infFullName -SectionName "Version" -KeyName "Provider").Trim("%")
WriteLog "Copying PE drivers for $providerName"
WriteLog "Driver inf is: $infFullName"
[void](New-Item -Path $targetPath -ItemType Directory -Force)
Copy-Item -Path $infFullName -Destination $targetPath -Force
$CatalogFileName = Get-PrivateProfileString -FileName $infFullName -SectionName "version" -KeyName "Catalogfile"
Copy-Item -Path "$infPath\$CatalogFileName" -Destination $targetPath -Force
$sourceDiskFiles = Get-PrivateProfileSection -FileName $infFullName -SectionName "SourceDisksFiles"
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
if (!$sourceDiskFiles[$sourceDiskFile].Contains(",")) {
Copy-Item -Path "$infPath\$sourceDiskFile" -Destination $targetPath -Force
} else {
$subdir = ($sourceDiskFiles[$sourceDiskFile] -split ",")[1]
[void](New-Item -Path "$targetPath\$subdir" -ItemType Directory -Force)
Copy-Item -Path "$infPath\$subdir\$sourceDiskFile" -Destination "$targetPath\$subdir" -Force
}
}
#Arch specific files override the files specified in the universal section
$sourceDiskFiles = Get-PrivateProfileSection -FileName $infFullName -SectionName "SourceDisksFiles.$WindowsArch"
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
if (!$sourceDiskFiles[$sourceDiskFile].Contains(",")) {
Copy-Item -Path "$infPath\$sourceDiskFile" -Destination $targetPath -Force
} else {
$subdir = ($sourceDiskFiles[$sourceDiskFile] -split ",")[1]
[void](New-Item -Path "$targetPath\$subdir" -ItemType Directory -Force)
Copy-Item -Path "$infPath\$subdir\$sourceDiskFile" -Destination "$targetPath\$subdir" -Force
}
}
}
}
}
}
function New-PEMedia {
param (
[Parameter()]
@@ -2702,9 +2828,34 @@ function New-PEMedia {
WriteLog 'Copy complete'
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
if ($CopyPEDrivers) {
if ($UseDriversAsPEDrivers) {
WriteLog "UseDriversAsPEDrivers is set. Building WinPE driver set from Drivers folder (bypassing PEDrivers folder contents)."
if (Test-Path -Path $PEDriversFolder) {
try {
Remove-Item -Path (Join-Path $PEDriversFolder '*') -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
WriteLog "Warning: Failed clearing existing PEDriversFolder contents: $($_.Exception.Message)"
}
}
else {
try {
New-Item -Path $PEDriversFolder -ItemType Directory -Force | Out-Null
}
catch {
WriteLog "Error: Failed to create PEDriversFolder at $PEDriversFolder - continuing may fail when adding drivers."
}
}
WriteLog "Copying required WinPE drivers from Drivers folder"
Copy-Drivers -Path $DriversFolder -Output $PEDriversFolder
}
else {
WriteLog "Copying PE drivers from PEDrivers folder"
}
WriteLog "Adding drivers to WinPE media"
try {
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver "$PEDriversFolder" -Recurse -ErrorAction SilentlyContinue | Out-null
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver $PEDriversFolder -Recurse -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-null
}
catch {
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
@@ -4432,6 +4583,29 @@ if ($CopyPEDrivers) {
WriteLog "Driver folder path $PEDriversFolder contains spaces. Please remove spaces from the path and try again."
throw "Driver folder path $PEDriversFolder contains spaces. Please remove spaces from the path and try again."
}
if ($UseDriversAsPEDrivers) {
# When using Drivers as PE drivers, skip strict PEDrivers folder existence/content checks.
$driverSourceAvailable = $false
if ($DriversJsonPath -and (Test-Path -Path $DriversJsonPath)) {
$driverSourceAvailable = $true
WriteLog "Drivers JSON path is set to $DriversJsonPath; drivers will be downloaded for WinPE."
}
elseif ($Make -and $Model) {
$driverSourceAvailable = $true
WriteLog "Make/Model ($Make / $Model) specified; drivers will be downloaded for WinPE."
}
elseif ((Test-Path -Path $DriversFolder) -and ((Get-ChildItem -Path $DriversFolder -Recurse | Measure-Object -Property Length -Sum).Sum -ge 1MB)) {
$driverSourceAvailable = $true
WriteLog "Drivers folder contains existing content; will reuse for WinPE."
}
if (-not $driverSourceAvailable) {
WriteLog "-UseDriversAsPEDrivers is set, but no driver sources are available (Drivers folder missing/empty and no download instructions)."
throw "-UseDriversAsPEDrivers is set, but no driver sources are available (Drivers folder missing/empty and no download instructions)."
}
WriteLog "UseDriversAsPEDrivers is set. Skipping PEDrivers folder existence/content checks; drivers will be sourced from Drivers folder (or downloaded)."
WriteLog 'PEDriver validation complete'
}
else {
if (!(Test-Path -Path $PEDriversFolder)) {
WriteLog "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is missing"
throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is missing"
@@ -4442,6 +4616,7 @@ if ($CopyPEDrivers) {
}
WriteLog 'PEDriver validation complete'
}
}
#Validate PPKG folder
if ($CopyPPKG) {
+4 -3
View File
@@ -628,9 +628,10 @@
<CheckBox x:Name="chkCompressDriversToWIM" Content="Compress Driver Model Folder to WIM" Margin="0,0,5,0" ToolTip="When set to $true, will compress each downloaded driver model folder into a separate WIM file within the Drivers folder. This is useful with Copy Drivers to USB drive."/>
</StackPanel>
<!-- Row 12: Copy PE Drivers Checkbox -->
<StackPanel Grid.Row="12" Orientation="Horizontal" Margin="5">
<CheckBox x:Name="chkCopyPEDrivers" Content="Copy PE Drivers" Margin="0,0,5,0" ToolTip="When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false."/>
<!-- Row 12: PE Driver Options (UseDriversAsPEDrivers is a dependent sub-option) -->
<StackPanel Grid.Row="12" Margin="5">
<CheckBox x:Name="chkCopyPEDrivers" Content="Copy PE Drivers" Margin="0,0,0,5" ToolTip="When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false."/>
<CheckBox x:Name="chkUseDriversAsPEDrivers" Content="Use Drivers Folder as PE Drivers Source" Margin="25,0,0,0" Visibility="Collapsed" ToolTip="When set to $true (and Copy PE Drivers is also checked), bypasses the PE Drivers Folder path and instead scans the Drivers folder to gather only required WinPE drivers. Hidden unless Copy PE Drivers is checked."/>
</StackPanel>
</Grid>
</ScrollViewer>
@@ -34,6 +34,7 @@ function Get-UIConfig {
CopyDrivers = $State.Controls.chkCopyDrivers.IsChecked
CopyOfficeConfigXML = $State.Controls.chkCopyOfficeConfigXML.IsChecked
CopyPEDrivers = $State.Controls.chkCopyPEDrivers.IsChecked
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
@@ -460,6 +461,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtPEDriversFolder' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'PEDriversFolder' -State $State
Set-UIValue -ControlName 'txtDriversJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DriversJsonPath' -State $State
Set-UIValue -ControlName 'chkCopyPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPEDrivers' -State $State
Set-UIValue -ControlName 'chkUseDriversAsPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'UseDriversAsPEDrivers' -State $State
Set-UIValue -ControlName 'chkCompressDriversToWIM' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompressDownloadedDriversToWim' -State $State
# Updates tab
@@ -808,6 +808,10 @@ function Register-EventHandlers {
$State.Controls.chkCopyDrivers.Add_Unchecked($driverCheckboxHandler)
$State.Controls.chkCompressDriversToWIM.Add_Checked($driverCheckboxHandler)
$State.Controls.chkCompressDriversToWIM.Add_Unchecked($driverCheckboxHandler)
$State.Controls.chkCopyPEDrivers.Add_Checked($driverCheckboxHandler)
$State.Controls.chkCopyPEDrivers.Add_Unchecked($driverCheckboxHandler)
$State.Controls.chkUseDriversAsPEDrivers.Add_Checked($driverCheckboxHandler)
$State.Controls.chkUseDriversAsPEDrivers.Add_Unchecked($driverCheckboxHandler)
$State.Controls.btnBrowseDriversFolder.Add_Click({
param($eventSource, $routedEventArgs)
@@ -143,6 +143,7 @@ function Initialize-UIControls {
$State.Controls.txtDriversFolder = $window.FindName('txtDriversFolder')
$State.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder')
$State.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers')
$State.Controls.chkUseDriversAsPEDrivers = $window.FindName('chkUseDriversAsPEDrivers')
$State.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU')
$State.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet')
$State.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender')
@@ -310,6 +311,7 @@ function Initialize-UIDefaults {
$State.Controls.chkInstallDrivers.IsChecked = $State.Defaults.generalDefaults.InstallDrivers
$State.Controls.chkCopyDrivers.IsChecked = $State.Defaults.generalDefaults.CopyDrivers
$State.Controls.chkCopyPEDrivers.IsChecked = $State.Defaults.generalDefaults.CopyPEDrivers
$State.Controls.chkUseDriversAsPEDrivers.IsChecked = $State.Defaults.generalDefaults.UseDriversAsPEDrivers
$State.Controls.chkCompressDriversToWIM.IsChecked = $State.Defaults.generalDefaults.CompressDownloadedDriversToWim
# Drivers tab UI logic
+14
View File
@@ -175,6 +175,7 @@ function Get-GeneralDefaults {
InstallDrivers = $false
CopyDrivers = $false
CopyPEDrivers = $false
UseDriversAsPEDrivers = $false
UpdateADK = $true
CompressDownloadedDriversToWim = $false
}
@@ -292,11 +293,14 @@ function Update-DriverCheckboxStates {
$installDriversChk = $State.Controls.chkInstallDrivers
$copyDriversChk = $State.Controls.chkCopyDrivers
$compressWimChk = $State.Controls.chkCompressDriversToWIM
$copyPEDriversChk = $State.Controls.chkCopyPEDrivers
$useDriversAsPeChk = $State.Controls.chkUseDriversAsPEDrivers
# Default to enabled, then apply disabling rules
$installDriversChk.IsEnabled = $true
$copyDriversChk.IsEnabled = $true
$compressWimChk.IsEnabled = $true
$copyPEDriversChk.IsEnabled = $true
if ($installDriversChk.IsChecked) {
$copyDriversChk.IsEnabled = $false
@@ -310,6 +314,16 @@ function Update-DriverCheckboxStates {
if ($compressWimChk.IsChecked) {
$installDriversChk.IsEnabled = $false
}
# Sub-option visibility logic: only show UseDriversAsPEDrivers when CopyPEDrivers is checked
if ($copyPEDriversChk.IsChecked) {
$useDriversAsPeChk.Visibility = 'Visible'
}
else {
# Parent unchecked: hide and clear sub-option
$useDriversAsPeChk.IsChecked = $false
$useDriversAsPeChk.Visibility = 'Collapsed'
}
}
# Function to manage the visibility of Office UI panels