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