From e2ccd11f07217b389f1622a69794224412e046e1 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:13:06 -0700 Subject: [PATCH] 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. --- FFUDevelopment/BuildFFUVM.ps1 | 195 +++++++++++++++++- FFUDevelopment/BuildFFUVM_UI.xaml | 7 +- .../FFUUI.Core/FFUUI.Core.Config.psm1 | 2 + .../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 4 + .../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 2 + FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 | 14 ++ 6 files changed, 211 insertions(+), 13 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 362fdce..dae7032 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -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,15 +4583,39 @@ 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 (!(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" + 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' } - if ((Get-ChildItem -Path $PEDriversFolder -Recurse | Measure-Object -Property Length -Sum).Sum -lt 1MB) { - WriteLog "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is empty" - throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is empty" + 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" + } + if ((Get-ChildItem -Path $PEDriversFolder -Recurse | Measure-Object -Property Length -Sum).Sum -lt 1MB) { + WriteLog "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is empty" + throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is empty" + } + WriteLog 'PEDriver validation complete' } - WriteLog 'PEDriver validation complete' } #Validate PPKG folder diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml index 4c13da2..36c70d6 100644 --- a/FFUDevelopment/BuildFFUVM_UI.xaml +++ b/FFUDevelopment/BuildFFUVM_UI.xaml @@ -628,9 +628,10 @@ - - - + + + + diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 index f30422f..9ba53ad 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 @@ -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 diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 index 0ec360a..e9ed276 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 @@ -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) diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 index e599ef5..21771f8 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 @@ -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 diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 index 04c7869..d825e3a 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 @@ -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