#Requires -Modules Hyper-V, Storage #Requires -PSEdition Desktop #Requires -RunAsAdministrator <# .SYNOPSIS A PowerShell script to create a Windows 10/11 FFU file. .DESCRIPTION This script creates a Windows 10/11 FFU and USB drive to help quickly get a Windows device reimaged. FFU can be customized with drivers, apps, and additional settings. .PARAMETER ISOPath Path to the Windows 10/11 ISO file. .PARAMETER WindowsSKU Edition of Windows 10/11 to be installed, e.g., accepted values are: 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N' .PARAMETER FFUDevelopmentPath Path to the FFU development folder (default is C:\FFUDevelopment). .PARAMETER InstallApps When set to $true, the script will create an Apps.iso file from the $FFUDevelopmentPath\Apps folder. It will also create a VM, mount the Apps.ISO, install the Apps, sysprep, and capture the VM. When set to $False, the FFU is created from a VHDX file. No VM is created. .PARAMETER InstallOffice Install Microsoft Office if set to $true. The script will download the latest ODT and Office files in the $FFUDevelopmentPath\Apps\Office folder and install Office in the FFU via VM .PARAMETER InstallDrivers Install device drivers from the specified $FFUDevelopmentPath\Drivers folder if set to $true. Download the drivers and put them in the Drivers folder. The script will recurse the drivers folder and add the drivers to the FFU. .PARAMETER Memory Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Use 4GB if necesary. .PARAMETER Disksize Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk. .PARAMETER Processors Number of virtual processors for the virtual machine. Recommended to use at least 4. .PARAMETER VMSwitchName Name of the Hyper-V virtual switch. If $InstallApps is set to $true, this must be set. This is required to capture the FFU from the VM. The default is *external*, but you will likely need to change this. .PARAMETER VMLocation Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. .PARAMETER FFUPrefix Prefix for the generated FFU file. Default is _FFU .PARAMETER FFUCaptureLocation Path to the folder where the captured FFU will be stored. Default is $FFUDevelopmentPath\FFU .PARAMETER ShareName Name of the shared folder for FFU capture. The default is FFUCaptureShare. This share will be created with rights for the user account. When finished, the share will be removed. .PARAMETER Username Username for accessing the shared folder. The default is ffu_user. The script will auto create the account and password. When finished, it will remove the account. .PARAMETER VMHostIPAddress IP address of the Hyper-V host for FFU capture. If $InstallApps is set to $true, this parameter must be configured. You must manually configure this. The script will not auto detect your IP (depending on your network adapters, it may not find the correct IP). .PARAMETER CreateCaptureMedia When set to $true, this will create WinPE capture media for use when $InstallApps is set to $true. This capture media will be automatically attached to the VM and the boot order will be changed to automate the capture of the FFU. .PARAMETER CreateDeploymentMedia When set to $true, this will create WinPE deployment media for use when deploying to a physical device. .PARAMETER OptionalFeatures Provide a semi-colon separated list of Windows optional features you want to include in the FFU (e.g. netfx3;TFTP) .PARAMETER ProductKey Product key for the Windows 10/11 edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. .PARAMETER BuildUSBDrive When set to $true, will partition and format a USB drive and copy the captured FFU to the drive. If you'd like to customize the drive to add drivers, provisioning packages, name prefix, etc. You'll need to do that afterward. .PARAMETER WindowsRelease Integer value of 10 or 11. This is used to identify which release of Windows to download. Default is 11. .PARAMETER WindowsVersion String value of the Windows version to download. This is used to identify which version of Windows to download. Default is 23h2. .PARAMETER WindowsArch String value of x86 or x64. This is used to identify which architecture of Windows to download. Default is x64. .PARAMETER WindowsLang String value in language-region format (e.g. en-us). This is used to identify which language of media to download. Default is en-us. .PARAMETER MediaType String value of either business or consumer. This is used to identify which media type to download. Default is consumer. .PARAMETER LogicalSectorBytes unit32 value of 512 or 4096. Not recommended to change from 512. Might be useful for 4kn drives, but needs more testing. Default is 512. .PARAMETER Optimize When set to $true, will optimize the FFU file. Default is $true. .PARAMETER CopyDrivers 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. .PARAMETER RemoveFFU When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. .PARAMETER UpdateLatestCU When set to $true, will download and install the latest cumulative update for Windows 10/11. Default is $false. .PARAMETER UpdatePreviewCU When set to $true, will download and install the latest Preview cumulative update for Windows 10/11. Default is $false. .PARAMETER UpdateLatestNet When set to $true, will download and install the latest .NET Framework for Windows 10/11. Default is $false. .PARAMETER UpdateLatestDefender When set to $true, will download and install the latest Windows Defender definitions and Defender platform update. Default is $false. .PARAMETER UpdateLatestMSRT When set to $true, will download and install the latest Windows Malicious Software Removal Tool. Default is $false .PARAMETER UpdateEdge When set to $true, will download and install the latest Microsoft Edge for Windows 10/11. Default is $false. .PARAMETER UpdateOneDrive When set to $true, will download and install the latest OneDrive for Windows 10/11 and install it as a per machine installation instead of per user. Default is $false. .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. .PARAMETER CopyUnattend When set to $true, will copy the $FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Default is $false. .PARAMETER CopyAutopilot When set to $true, will copy the $FFUDevelopmentPath\Autopilot folder to the Deployment partition of the USB drive. Default is $false. .PARAMETER CompactOS When set to $true, will compact the OS when building the FFU. Default is $true. .PARAMETER CleanupCaptureISO When set to $true, will remove the WinPE capture ISO after the FFU has been captured. Default is $true. .PARAMETER CleanupDeployISO When set to $true, will remove the WinPE deployment ISO after the FFU has been captured. Default is $true. .PARAMETER CleanupAppsISO When set to $true, will remove the Apps ISO after the FFU has been captured. Default is $true. .PARAMETER DriversFolder Path to the drivers folder. Default is $FFUDevelopmentPath\Drivers. .PARAMETER CleanupDrivers When set to $true, will remove the drivers folder after the FFU has been captured. Default is $true. .PARAMETER UserAgent User agent string to use when downloading files. .PARAMETER Headers Headers to use when downloading files. .PARAMETER AllowExternalHardDiskMedia When set to $true, will allow the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is not defined. .PARAMETER PromptExternalHardDiskMedia When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. .PARAMETER Make Make of the device to download drivers. Accepted values are: 'Microsoft', 'Dell', 'HP', 'Lenovo' .PARAMETER Model Model of the device to download drivers. This is required if Make is set. .PARAMETER AppsScriptVariables When passed a hashtable, the script will alter the $FFUDevelopmentPath\Apps\InstallAppsandSysprep.cmd file to set variables with the hashtable keys as variable names and the hashtable values their content. .PARAMETER CustomFFUNameTemplate Sets a custom FFU output name with placeholders. Allowed placeholders are: {Name}, {DisplayVersion}, {SKU}, {BuildDate}, {yyyy}, {MM}, {dd}, {H}, {hh}, {mm}, {tt} .EXAMPLE Command line for most people who want to download the latest Windows 11 Pro x64 media in English (US) with the latest Windows Cumulative Update, .NET Framework, Defender platform and definition updates, Edge, OneDrive, and Office/M365 Apps. It will also copy drivers to the FFU. This can take about 40 minutes to create the FFU due to the time it takes to download and install the updates. .\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -UpdateLatestCU $true -UpdateLatestNet $true -UpdateLatestDefender $true -UpdateEdge $true -UpdateOneDrive $true -verbose Command line for most people who want to create an FFU with Office and drivers and have downloaded their own ISO. This assumes you have copied this script and associated files to the C:\FFUDevelopment folder. If you need to use another drive or folder, change the -FFUDevelopment parameter (e.g. -FFUDevelopment 'D:\FFUDevelopment') .\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose Command line for those who just want a FFU with no drivers, apps, or Office and have downloaded their own ISO. .\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $false -InstallOffice $false -InstallDrivers $false -CreateCaptureMedia $false -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose Command line for those who just want a FFU with Apps and drivers, no Office and have downloaded their own ISO. .\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $false -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose Command line for those who want to download the latest Windows 11 Pro x64 media in English (US) and install the latest version of Office and drivers. .\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose Command line for those who want to download the latest Windows 11 Pro x64 media in French (CA) and install the latest version of Office and drivers. .\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -WindowsRelease 11 -WindowsArch 'x64' -WindowsLang 'fr-ca' -MediaType 'consumer' -verbose Command line for those who want to download the latest Windows 11 Pro x64 media in English (US) and install the latest version of Office and drivers. .\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose .NOTES Additional notes about your script. .LINK https://github.com/rbalsleyMSFT/FFU #> [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [ValidateScript({ Test-Path $_ })] [string]$ISOPath, [ValidateSet('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)')] [string]$WindowsSKU = 'Pro', [ValidateScript({ Test-Path $_ })] [string]$FFUDevelopmentPath = $PSScriptRoot, [bool]$InstallApps, [hashtable]$AppsScriptVariables, [bool]$InstallOffice, [ValidateSet('Microsoft', 'Dell', 'HP', 'Lenovo')] [string]$Make, [string]$Model, # [Parameter(Mandatory = $false)] # [ValidateScript({ # if ($Make) { # return $true # } # if ($_ -and (!(Test-Path -Path '.\Drivers') -or ((Get-ChildItem -Path '.\Drivers' -Recurse | Measure-Object -Property Length -Sum).Sum -lt 1MB))) { # throw 'InstallDrivers is set to $true, but either the Drivers folder is missing or empty' # } # return $true # })] [bool]$InstallDrivers, [uint64]$Memory = 4GB, [uint64]$Disksize = 30GB, [int]$Processors = 4, [string]$VMSwitchName, [string]$VMLocation, [string]$FFUPrefix = '_FFU', [string]$FFUCaptureLocation, [string]$ShareName = "FFUCaptureShare", [string]$Username = "ffu_user", [string]$CustomFFUNameTemplate, [Parameter(Mandatory = $false)] [string]$VMHostIPAddress, [bool]$CreateCaptureMedia = $true, [bool]$CreateDeploymentMedia, [ValidateScript({ $allowedFeatures = @("Windows-Defender-Default-Definitions", "Printing-PrintToPDFServices-Features", "Printing-XPSServices-Features", "TelnetClient", "TFTP", "TIFFIFilter", "LegacyComponents", "DirectPlay", "MSRDC-Infrastructure", "Windows-Identity-Foundation", "MicrosoftWindowsPowerShellV2Root", "MicrosoftWindowsPowerShellV2", "SimpleTCP", "NetFx4-AdvSrvs", "NetFx4Extended-ASPNET45", "WCF-Services45", "WCF-HTTP-Activation45", "WCF-TCP-Activation45", "WCF-Pipe-Activation45", "WCF-MSMQ-Activation45", "WCF-TCP-PortSharing45", "IIS-WebServerRole", "IIS-WebServer", "IIS-CommonHttpFeatures", "IIS-HttpErrors", "IIS-HttpRedirect", "IIS-ApplicationDevelopment", "IIS-Security", "IIS-RequestFiltering", "IIS-NetFxExtensibility", "IIS-NetFxExtensibility45", "IIS-HealthAndDiagnostics", "IIS-HttpLogging", "IIS-LoggingLibraries", "IIS-RequestMonitor", "IIS-HttpTracing", "IIS-URLAuthorization", "IIS-IPSecurity", "IIS-Performance", "IIS-HttpCompressionDynamic", "IIS-WebServerManagementTools", "IIS-ManagementScriptingTools", "IIS-IIS6ManagementCompatibility", "IIS-Metabase", "WAS-WindowsActivationService", "WAS-ProcessModel", "WAS-NetFxEnvironment", "WAS-ConfigurationAPI", "IIS-HostableWebCore", "WCF-HTTP-Activation", "WCF-NonHTTP-Activation", "IIS-StaticContent", "IIS-DefaultDocument", "IIS-DirectoryBrowsing", "IIS-WebDAV", "IIS-WebSockets", "IIS-ApplicationInit", "IIS-ISAPIFilter", "IIS-ISAPIExtensions", "IIS-ASPNET", "IIS-ASPNET45", "IIS-ASP", "IIS-CGI", "IIS-ServerSideIncludes", "IIS-CustomLogging", "IIS-BasicAuthentication", "IIS-HttpCompressionStatic", "IIS-ManagementConsole", "IIS-ManagementService", "IIS-WMICompatibility", "IIS-LegacyScripts", "IIS-LegacySnapIn", "IIS-FTPServer", "IIS-FTPSvc", "IIS-FTPExtensibility", "MSMQ-Container", "MSMQ-DCOMProxy", "MSMQ-Server", "MSMQ-ADIntegration", "MSMQ-HTTP", "MSMQ-Multicast", "MSMQ-Triggers", "IIS-CertProvider", "IIS-WindowsAuthentication", "IIS-DigestAuthentication", "IIS-ClientCertificateMappingAuthentication", "IIS-IISCertificateMappingAuthentication", "IIS-ODBCLogging", "NetFx3", "SMB1Protocol-Deprecation", "MediaPlayback", "WindowsMediaPlayer", "Client-DeviceLockdown", "Client-EmbeddedShellLauncher", "Client-EmbeddedBootExp", "Client-EmbeddedLogon", "Client-KeyboardFilter", "Client-UnifiedWriteFilter", "HostGuardian", "MultiPoint-Connector", "MultiPoint-Connector-Services", "MultiPoint-Tools" , "AppServerClient", "SearchEngine-Client-Package", "WorkFolders-Client", "Printing-Foundation-Features", "Printing-Foundation-InternetPrinting-Client", "Printing-Foundation-LPDPrintService", "Printing-Foundation-LPRPortMonitor", "HypervisorPlatform", "VirtualMachinePlatform", "Microsoft-Windows-Subsystem-Linux", "Client-ProjFS", "Containers-DisposableClientVM", 'Containers-DisposableClientVM', 'Microsoft-Hyper-V-All', 'Microsoft-Hyper-V', 'Microsoft-Hyper-V-Tools-All', 'Microsoft-Hyper-V-Management-PowerShell', 'Microsoft-Hyper-V-Hypervisor', 'Microsoft-Hyper-V-Services', 'Microsoft-Hyper-V-Management-Clients', 'DataCenterBridging', 'DirectoryServices-ADAM-Client', 'Windows-Defender-ApplicationGuard', 'ServicesForNFS-ClientOnly', 'ClientForNFS-Infrastructure', 'NFS-Administration', 'Containers', 'Containers-HNS', 'Containers-SDN', 'SMB1Protocol', 'SMB1Protocol-Client', 'SMB1Protocol-Server', 'SmbDirect') $inputFeatures = $_ -split ';' foreach ($feature in $inputFeatures) { if (-not ($allowedFeatures -contains $feature)) { throw "Invalid optional feature '$feature'. Allowed values: $($allowedFeatures -join ', ')" } } return $true })] [string]$OptionalFeatures, [string]$ProductKey, [bool]$BuildUSBDrive, [Parameter(Mandatory = $false)] [ValidateSet(10, 11, 2016, 2019, 2022, 2025)] [int]$WindowsRelease = 11, [Parameter(Mandatory = $false)] [string]$WindowsVersion = '24h2', [Parameter(Mandatory = $false)] [ValidateSet('x86', 'x64', 'arm64')] [string]$WindowsArch = 'x64', [ValidateScript({ $allowedLang = @('ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr', 'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee', 'fi-fi', 'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it', 'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv', 'nb-no', 'nl-nl', 'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk', 'sl-si', 'sr-latn-rs', 'sv-se', 'th-th', 'tr-tr', 'uk-ua', 'zh-cn', 'zh-tw') if ($allowedLang -contains $_) { $true } else { throw "Invalid WindowsLang value. Allowed values: $($allowedLang -join ', ')" } return $true })] [Parameter(Mandatory = $false)] [string]$WindowsLang = 'en-us', [Parameter(Mandatory = $false)] [ValidateSet('consumer', 'business')] [string]$MediaType = 'consumer', [ValidateSet(512, 4096)] [uint32]$LogicalSectorSizeBytes = 512, [bool]$Optimize = $true, [Parameter(Mandatory = $false)] [ValidateScript({ if ($Make) { return $true } if ($_ -and (!(Test-Path -Path '.\Drivers') -or ((Get-ChildItem -Path '.\Drivers' -Recurse | Measure-Object -Property Length -Sum).Sum -lt 1MB))) { throw 'CopyDrivers is set to $true, but either the Drivers folder is missing or empty' } return $true })] [bool]$CopyDrivers, [bool]$CopyPEDrivers, [bool]$RemoveFFU, [bool]$UpdateLatestCU, [bool]$UpdatePreviewCU, [bool]$UpdateLatestNet, [bool]$UpdateLatestDefender, [bool]$UpdateLatestMSRT, [bool]$UpdateEdge, [bool]$UpdateOneDrive, [bool]$AllowVHDXCaching, [bool]$CopyPPKG, [bool]$CopyUnattend, [bool]$CopyAutopilot, [bool]$CompactOS = $true, [bool]$CleanupCaptureISO = $true, [bool]$CleanupDeployISO = $true, [bool]$CleanupAppsISO = $true, [string]$DriversFolder, [bool]$CleanupDrivers = $true, [string]$UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0', #Microsoft sites will intermittently fail on downloads. These headers are to help with that. $Headers = @{ "Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" "Accept-Encoding" = "gzip, deflate, br, zstd" "Accept-Language" = "en-US,en;q=0.9" "Priority" = "u=0, i" "Sec-Ch-Ua" = "`"Microsoft Edge`";v=`"125`", `"Chromium`";v=`"125`", `"Not.A/Brand`";v=`"24`"" "Sec-Ch-Ua-Mobile" = "?0" "Sec-Ch-Ua-Platform" = "`"Windows`"" "Sec-Fetch-Dest" = "document" "Sec-Fetch-Mode" = "navigate" "Sec-Fetch-Site" = "none" "Sec-Fetch-User" = "?1" "Upgrade-Insecure-Requests" = "1" }, [bool]$AllowExternalHardDiskMedia, [bool]$PromptExternalHardDiskMedia = $true ) $version = '2410.1' #Class definition for vhdx cache class VhdxCacheUpdateItem { [string]$Name VhdxCacheUpdateItem([string]$Name) { $this.Name = $Name } } class VhdxCacheItem { [string]$VhdxFileName = "" [string]$WindowsSKU = "" [string]$WindowsRelease = "" [string]$WindowsVersion = "" [string]$OptionalFeatures = "" [VhdxCacheUpdateItem[]]$IncludedUpdates = @() } #Check if Hyper-V feature is installed (requires only checks the module) $osInfo = Get-WmiObject -Class Win32_OperatingSystem $isServer = $osInfo.Caption -match 'server' if ($isServer) { $hyperVFeature = Get-WindowsFeature -Name Hyper-V if ($hyperVFeature.InstallState -ne "Installed") { Write-Host "Hyper-V feature is not installed. Please install it before running this script." exit } } else { $hyperVFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All if ($hyperVFeature.State -ne "Enabled") { Write-Host "Hyper-V feature is not enabled. Please enable it before running this script." exit } } # Set default values for variables that depend on other parameters if (-not $AppsISO) { $AppsISO = "$FFUDevelopmentPath\Apps.iso" } if (-not $AppsPath) { $AppsPath = "$FFUDevelopmentPath\Apps" } if (-not $DeployISO) { $DeployISO = "$FFUDevelopmentPath\WinPE_FFU_Deploy_$WindowsArch.iso" } if (-not $CaptureISO) { $CaptureISO = "$FFUDevelopmentPath\WinPE_FFU_Capture_$WindowsArch.iso" } if (-not $OfficePath) { $OfficePath = "$AppsPath\Office" } if (-not $rand) { $rand = Get-Random } if (-not $VMLocation) { $VMLocation = "$FFUDevelopmentPath\VM" } if (-not $VMName) { $VMName = "$FFUPrefix-$rand" } if (-not $VMPath) { $VMPath = "$VMLocation\$VMName" } if (-not $VHDXPath) { $VHDXPath = "$VMPath\$VMName.vhdx" } if (-not $FFUCaptureLocation) { $FFUCaptureLocation = "$FFUDevelopmentPath\FFU" } if (-not $LogFile) { $LogFile = "$FFUDevelopmentPath\FFUDevelopment.log" } if (-not $KBPath) { $KBPath = "$FFUDevelopmentPath\KB" } if (-not $DefenderPath) { $DefenderPath = "$AppsPath\Defender" } if (-not $MSRTPath) { $MSRTPath = "$AppsPath\MSRT" } if (-not $OneDrivePath) { $OneDrivePath = "$AppsPath\OneDrive" } if (-not $EdgePath) { $EdgePath = "$AppsPath\Edge" } if (-not $DriversFolder) { $DriversFolder = "$FFUDevelopmentPath\Drivers" } if (-not $PPKGFolder) { $PPKGFolder = "$FFUDevelopmentPath\PPKG" } if (-not $UnattendFolder) { $UnattendFolder = "$FFUDevelopmentPath\Unattend" } if (-not $AutopilotFolder) { $AutopilotFolder = "$FFUDevelopmentPath\Autopilot" } if (-not $PEDriversFolder) { $PEDriversFolder = "$FFUDevelopmentPath\PEDrivers" } if (-not $VHDXCacheFolder) { $VHDXCacheFolder = "$FFUDevelopmentPath\VHDXCache" } if (-not $installationType) { $installationType = if ($WindowsRelease.ToString().Length -eq 2) { 'Client' } else { 'Server' } } if ($installationType -eq 'Server'){ #Map $WindowsRelease to $WindowsVersion for Windows Server switch ($WindowsRelease) { 2016 { $WindowsVersion = '1607' } 2019 { $WindowsVersion = '1809' } 2022 { $WindowsVersion = '21H2' } 2025 { $WindowsVersion = '24H2' } } } #FUNCTIONS function WriteLog($LogText) { Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText" -Force -ErrorAction SilentlyContinue Write-Verbose $LogText } function LogVariableValues { $excludedVariables = @( 'PSBoundParameters', 'PSScriptRoot', 'PSCommandPath', 'MyInvocation', '?', 'ConsoleFileName', 'ExecutionContext', 'false', 'HOME', 'Host', 'hyperVFeature', 'input', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount', 'MaximumVariableCount', 'null', 'PID', 'PSCmdlet', 'PSCulture', 'PSUICulture', 'PSVersionTable', 'ShellId', 'true' ) $allVariables = Get-Variable -Scope Script | Where-Object { $_.Name -notin $excludedVariables } Writelog "Script version: $version" WriteLog 'Logging variables' foreach ($variable in $allVariables) { $variableName = $variable.Name $variableValue = $variable.Value if ($null -ne $variableValue) { WriteLog "[VAR]$variableName`: $variableValue" } else { WriteLog "[VAR]Variable $variableName not found or not set" } } WriteLog 'End logging variables' } function Get-ChildProcesses($parentId) { $result = @() $children = Get-CimInstance Win32_Process -Filter "ParentProcessId = $parentId" foreach ($child in $children) { $result += $child $result += Get-ChildProcesses $child.ProcessId } return $result } function Invoke-Process { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$FilePath, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ArgumentList, [Parameter()] [ValidateNotNullOrEmpty()] [bool]$Wait = $true ) $ErrorActionPreference = 'Stop' try { $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)" $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)" $startProcessParams = @{ FilePath = $FilePath ArgumentList = $ArgumentList RedirectStandardError = $stdErrTempFile RedirectStandardOutput = $stdOutTempFile Wait = $($Wait); PassThru = $true; NoNewWindow = $true; } if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) { $cmd = Start-Process @startProcessParams $cmdOutput = Get-Content -Path $stdOutTempFile -Raw $cmdError = Get-Content -Path $stdErrTempFile -Raw if ($cmd.ExitCode -ne 0 -and $wait -eq $true) { if ($cmdError) { throw $cmdError.Trim() } if ($cmdOutput) { throw $cmdOutput.Trim() } } else { if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) { WriteLog $cmdOutput } } } } catch { #$PSCmdlet.ThrowTerminatingError($_) WriteLog $_ # Write-Host "Script failed - $Logfile for more info" throw $_ } finally { Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore } return $cmd } function Test-Url { param ( [Parameter(Mandatory = $true)] [string]$Url ) try { # Create a web request and check the response $request = [System.Net.WebRequest]::Create($Url) $request.Method = 'HEAD' $response = $request.GetResponse() return $true } catch { return $false } } # Function to download a file using BITS with retry and error handling function Start-BitsTransferWithRetry { param ( [Parameter(Mandatory = $true)] [string]$Source, [Parameter(Mandatory = $true)] [string]$Destination, [int]$Retries = 3 ) $attempt = 0 while ($attempt -lt $Retries) { try { $OriginalVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue' Start-BitsTransfer -Source $Source -Destination $Destination -ErrorAction Stop $ProgressPreference = 'Continue' $VerbosePreference = $OriginalVerbosePreference return } catch { $attempt++ WriteLog "Attempt $attempt of $Retries failed to download $Source. Retrying..." Start-Sleep -Seconds 5 } } WriteLog "Failed to download $Source after $Retries attempts." return $false } function Get-MicrosoftDrivers { param ( [string]$Make, [string]$Model, [int]$WindowsRelease ) $url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120" # Download the webpage content WriteLog "Getting Surface driver information from $url" $OriginalVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent $VerbosePreference = $OriginalVerbosePreference WriteLog "Complete" # Parse the HTML content WriteLog "Parsing web content for models and download links" $html = $webContent.Content $document = New-Object -ComObject "HTMLFILE" $document.IHTMLDocument2_write($html) $document.Close() $models = @() # Select all divs with class 'selectable-content-options__option-content' $divs = $document.getElementsByTagName("div") | Where-Object { $_.className -eq "selectable-content-options__option-content" -or $_.className -eq "selectable-content-options__option-content ocHidden" } foreach ($div in $divs) { $tables = $div.getElementsByTagName("table") foreach ($table in $tables) { $rows = $table.getElementsByTagName("tr") foreach ($row in $rows) { $cells = $row.getElementsByTagName("td") if ($cells.length -ge 2) { $modelName = $cells[0].innerText.Trim() $linkElement = $cells[1].getElementsByTagName("a") if ($linkElement.length -gt 0) { $modelLink = $linkElement[0].href $models += [PSCustomObject]@{ Model = $modelName; Link = $modelLink } } else { # Handle cases with instructions instead of links $modelLink = $cells[1].innerText.Trim() $models += [PSCustomObject]@{ Model = $modelName; Link = $modelLink } } } } } } WriteLog "Parsing complete" # Validate the model $selectedModel = $models | Where-Object { $_.Model -eq $Model } if ($null -eq $selectedModel) { if ($VerbosePreference -ne 'Continue') { Write-Host "The model '$Model' was not found in the list of available models." Write-Host "Please run the script with the -Verbose switch to see the list of available models." } WriteLog "The model '$Model' was not found in the list of available models." WriteLog "Please select a model from the list below by number:" for ($i = 0; $i -lt $models.Count; $i++) { if ($VerbosePreference -ne 'Continue') { Write-Host "$($i + 1). $($models[$i].Model)" } WriteLog "$($i + 1). $($models[$i].Model)" } do { $selection = Read-Host "Enter the number of the model you want to select" WriteLog "User selected model number: $selection" if ($selection -match '^\d+$' -and [int]$selection -ge 1 -and [int]$selection -le $models.Count) { $selectedModel = $models[$selection - 1] } else { if ($VerbosePreference -ne 'Continue') { Write-Host "Invalid selection. Please try again." } WriteLog "Invalid selection. Please try again." } } while ($null -eq $selectedModel) } $Model = $selectedModel.Model WriteLog "Model: $Model" WriteLog "Download Page: $($selectedModel.Link)" # Follow the link to the download page and parse the script tag WriteLog "Getting download page content" $OriginalVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $downloadPageContent = Invoke-WebRequest -Uri $selectedModel.Link -UseBasicParsing -Headers $Headers -UserAgent $UserAgent $VerbosePreference = $OriginalVerbosePreference WriteLog "Complete" WriteLog "Parsing download page for file" $scriptPattern = '' $scriptMatch = [regex]::Match($downloadPageContent.Content, $scriptPattern) if ($scriptMatch.Success) { $scriptContent = $scriptMatch.Groups[1].Value # Extract the download file information from the script tag $downloadFilePattern = '"name":"(.*?)",.*?"url":"(.*?)"' $downloadFileMatches = [regex]::Matches($scriptContent, $downloadFilePattern) $downloadLink = $null foreach ($downloadFile in $downloadFileMatches) { $fileName = $downloadFile.Groups[1].Value $fileUrl = $downloadFile.Groups[2].Value if ($fileName -match "Win$WindowsRelease") { $downloadLink = $fileUrl break } } if ($downloadLink) { WriteLog "Download Link for Windows ${WindowsRelease}: $downloadLink" # Create directory structure if (-not (Test-Path -Path $DriversFolder)) { WriteLog "Creating Drivers folder: $DriversFolder" New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null WriteLog "Drivers folder created" } $surfaceDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make $modelPath = Join-Path -Path $surfaceDriversPath -ChildPath $Model if (-Not (Test-Path -Path $modelPath)) { WriteLog "Creating model folder: $modelPath" New-Item -Path $modelPath -ItemType Directory | Out-Null WriteLog "Complete" } # Download the file $filePath = Join-Path -Path $surfaceDriversPath -ChildPath ($fileName) WriteLog "Downloading $Model driver file to $filePath" Start-BitsTransferWithRetry -Source $downloadLink -Destination $filePath WriteLog "Download complete" # Determine file extension $fileExtension = [System.IO.Path]::GetExtension($filePath).ToLower() if ($fileExtension -eq ".msi") { # Extract the MSI file using an administrative install WriteLog "Extracting MSI file to $modelPath" $arguments = "/a `"$($filePath)`" /qn TARGETDIR=`"$($modelPath)`"" Invoke-Process -FilePath "msiexec.exe" -ArgumentList $arguments | Out-Null WriteLog "Extraction complete" } elseif ($fileExtension -eq ".zip") { # Extract the ZIP file WriteLog "Extracting ZIP file to $modelPath" $ProgressPreference = 'SilentlyContinue' Expand-Archive -Path $filePath -DestinationPath $modelPath -Force $ProgressPreference = 'Continue' WriteLog "Extraction complete" } else { WriteLog "Unsupported file type: $fileExtension" } # Remove the downloaded file WriteLog "Removing $filePath" Remove-Item -Path $filePath -Force WriteLog "Complete" } else { WriteLog "No download link found for Windows $WindowsRelease." } } else { WriteLog "Failed to parse the download page for the MSI file." } } function Get-HPDrivers { [CmdletBinding()] param ( [Parameter()] [string]$Make, [Parameter()] [string]$Model, [Parameter()] [ValidateSet("x64", "x86", "ARM64")] [string]$WindowsArch, [Parameter()] [ValidateSet(10, 11)] [int]$WindowsRelease, [Parameter()] [string]$WindowsVersion ) # Download and extract the PlatformList.cab $PlatformListUrl = 'https://hpia.hpcloud.hp.com/ref/platformList.cab' $DriversFolder = "$DriversFolder\$Make" $PlatformListCab = "$DriversFolder\platformList.cab" $PlatformListXml = "$DriversFolder\PlatformList.xml" if (-not (Test-Path -Path $DriversFolder)) { WriteLog "Creating Drivers folder: $DriversFolder" New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null WriteLog "Drivers folder created" } WriteLog "Downloading $PlatformListUrl to $PlatformListCab" Start-BitsTransferWithRetry -Source $PlatformListUrl -Destination $PlatformListCab WriteLog "Download complete" WriteLog "Expanding $PlatformListCab to $PlatformListXml" Invoke-Process -FilePath expand.exe -ArgumentList "$PlatformListCab $PlatformListXml" | Out-Null WriteLog "Expansion complete" # Parse the PlatformList.xml to find the SystemID based on the ProductName [xml]$PlatformListContent = Get-Content -Path $PlatformListXml $ProductNodes = $PlatformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match $Model } # Create a list of unique ProductName entries $ProductNames = @() foreach ($node in $ProductNodes) { foreach ($productName in $node.ProductName) { if ($productName.'#text' -match $Model) { $ProductNames += [PSCustomObject]@{ ProductName = $productName.'#text' SystemID = $node.SystemID OSReleaseID = $node.OS.OSReleaseIdFileName -replace 'H', 'h' IsWindows11 = $node.OS.IsWindows11 -contains 'true' } } } } if ($ProductNames.Count -gt 1) { Write-Output "More than one model found matching '$Model':" WriteLog "More than one model found matching '$Model':" $ProductNames | ForEach-Object -Begin { $i = 1 } -Process { if ($VerbosePreference -ne 'Continue') { Write-Output "$i. $($_.ProductName)" } WriteLog "$i. $($_.ProductName)" $i++ } $selection = Read-Host "Please select the number corresponding to the correct model" WriteLog "User selected model number: $selection" if ($selection -match '^\d+$' -and [int]$selection -le $ProductNames.Count) { $SelectedProduct = $ProductNames[[int]$selection - 1] $ProductName = $SelectedProduct.ProductName WriteLog "Selected model: $ProductName" $SystemID = $SelectedProduct.SystemID WriteLog "SystemID: $SystemID" $ValidOSReleaseIDs = $SelectedProduct.OSReleaseID WriteLog "Valid OSReleaseIDs: $ValidOSReleaseIDs" $IsWindows11 = $SelectedProduct.IsWindows11 WriteLog "IsWindows11 supported: $IsWindows11" } else { WriteLog "Invalid selection. Exiting." if ($VerbosePreference -ne 'Continue') { Write-Host "Invalid selection. Exiting." } exit } } elseif ($ProductNames.Count -eq 1) { $SelectedProduct = $ProductNames[0] $ProductName = $SelectedProduct.ProductName WriteLog "Selected model: $ProductName" $SystemID = $SelectedProduct.SystemID WriteLog "SystemID: $SystemID" $ValidOSReleaseIDs = $SelectedProduct.OSReleaseID WriteLog "OSReleaseID: $ValidOSReleaseIDs" $IsWindows11 = $SelectedProduct.IsWindows11 WriteLog "IsWindows11: $IsWindows11" } else { WriteLog "No models found matching '$Model'. Exiting." if ($VerbosePreference -ne 'Continue') { Write-Host "No models found matching '$Model'. Exiting." } exit } if (-not $SystemID) { WriteLog "SystemID not found for model: $Model Exiting." if ($VerbosePreference -ne 'Continue') { Write-Host "SystemID not found for model: $Model Exiting." } exit } # Validate if WindowsRelease is 11 and there is no IsWindows11 element set to true if ($WindowsRelease -eq 11 -and -not $IsWindows11) { WriteLog "WindowsRelease is set to 11, but no drivers are available for this Windows release. Please set the -WindowsRelease parameter to 10, or provide your own drivers to the FFUDevelopment\Drivers folder." Write-Output "WindowsRelease is set to 11, but no drivers are available for this Windows release. Please set the -WindowsRelease parameter to 10, or provide your own drivers to the FFUDevelopment\Drivers folder." exit } # Validate WindowsVersion against OSReleaseID $OSReleaseIDs = $ValidOSReleaseIDs -split ' ' $MatchingReleaseID = $OSReleaseIDs | Where-Object { $_ -eq "$WindowsVersion" } if (-not $MatchingReleaseID) { Write-Output "The specified WindowsVersion value '$WindowsVersion' is not valid for the selected model. Please select a valid OSReleaseID:" $OSReleaseIDs | ForEach-Object -Begin { $i = 1 } -Process { Write-Output "$i. $_" $i++ } $selection = Read-Host "Please select the number corresponding to the correct OSReleaseID" WriteLog "User selected OSReleaseID number: $selection" if ($selection -match '^\d+$' -and [int]$selection -le $OSReleaseIDs.Count) { $WindowsVersion = $OSReleaseIDs[[int]$selection - 1] WriteLog "Selected OSReleaseID: $WindowsVersion" } else { WriteLog "Invalid selection. Exiting." exit } } # Modify WindowsArch for URL $Arch = $WindowsArch -replace "^x", "" # Construct the URL to download the driver XML cab for the model $ModelRelease = $SystemID + "_$Arch" + "_$WindowsRelease" + ".0.$WindowsVersion" $DriverCabUrl = "https://hpia.hpcloud.hp.com/ref/$SystemID/$ModelRelease.cab" $DriverCabFile = "$DriversFolder\$ModelRelease.cab" $DriverXmlFile = "$DriversFolder\$ModelRelease.xml" if (-not (Test-Url -Url $DriverCabUrl)) { WriteLog "HP Driver cab URL is not accessible: $DriverCabUrl Exiting" if ($VerbosePreference -ne 'Continue') { Write-Host "HP Driver cab URL is not accessible: $DriverCabUrl Exiting" } exit } # Download and extract the driver XML cab Writelog "Downloading HP Driver cab from $DriverCabUrl to $DriverCabFile" Start-BitsTransferWithRetry -Source $DriverCabUrl -Destination $DriverCabFile WriteLog "Expanding HP Driver cab to $DriverXmlFile" Invoke-Process -FilePath expand.exe -ArgumentList "$DriverCabFile $DriverXmlFile" | Out-Null # Parse the extracted XML file to download individual drivers [xml]$DriverXmlContent = Get-Content -Path $DriverXmlFile $baseUrl = "https://ftp.hp.com/pub/softpaq/sp" WriteLog "Downloading drivers for $ProductName" foreach ($update in $DriverXmlContent.ImagePal.Solutions.UpdateInfo) { if ($update.Category -notmatch '^Driver') { continue } $Name = $update.Name # Fix the name for drivers that contain illegal characters for folder name purposes $Name = $Name -replace '[\\\/\:\*\?\"\<\>\|]', '_' WriteLog "Downloading driver: $Name" $Category = $update.Category $Category = $Category -replace '[\\\/\:\*\?\"\<\>\|]', '_' $Version = $update.Version $Version = $Version -replace '[\\\/\:\*\?\"\<\>\|]', '_' $DriverUrl = "https://$($update.URL)" WriteLog "Driver URL: $DriverUrl" $DriverFileName = [System.IO.Path]::GetFileName($DriverUrl) $downloadFolder = "$DriversFolder\$ProductName\$Category" $DriverFilePath = Join-Path -Path $downloadFolder -ChildPath $DriverFileName if (Test-Path -Path $DriverFilePath) { WriteLog "Driver already downloaded: $DriverFilePath, skipping" continue } if (-not (Test-Path -Path $downloadFolder)) { WriteLog "Creating download folder: $downloadFolder" New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null WriteLog "Download folder created" } # Download the driver with retry WriteLog "Downloading driver to: $DriverFilePath" Start-BitsTransferWithRetry -Source $DriverUrl -Destination $DriverFilePath WriteLog 'Driver downloaded' # Make folder for extraction $extractFolder = "$downloadFolder\$Name\$Version\" + $DriverFileName.TrimEnd('.exe') Writelog "Creating extraction folder: $extractFolder" New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null WriteLog 'Extraction folder created' # Extract the driver $arguments = "/s /e /f `"$extractFolder`"" WriteLog "Extracting driver" Invoke-Process -FilePath $DriverFilePath -ArgumentList $arguments | Out-Null WriteLog "Driver extracted to: $extractFolder" # Delete the .exe driver file after extraction Remove-Item -Path $DriverFilePath -Force WriteLog "Driver installation file deleted: $DriverFilePath" } # Clean up the downloaded cab and xml files Remove-Item -Path $DriverCabFile, $DriverXmlFile, $PlatformListCab, $PlatformListXml -Force WriteLog "Driver cab and xml files deleted" } function Get-LenovoDrivers { param ( [Parameter()] [string]$Model, [Parameter()] [ValidateSet("x64", "x86", "ARM64")] [string]$WindowsArch, [Parameter()] [ValidateSet(10, 11)] [int]$WindowsRelease ) function Get-LenovoPSREF { param ( [string]$ModelName ) $url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$ModelName" WriteLog "Querying Lenovo PSREF API for model: $ModelName" $OriginalVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent $VerbosePreference = $OriginalVerbosePreference WriteLog "Complete" $jsonResponse = $response.Content | ConvertFrom-Json $products = @() foreach ($item in $jsonResponse.data) { $productName = $item.ProductName $machineTypes = $item.MachineType -split " / " foreach ($machineType in $machineTypes) { if ($machineType -eq $ModelName) { WriteLog "Model name entered is a matching machine type" $products = @() $products += [pscustomobject]@{ ProductName = $productName MachineType = $machineType } WriteLog "Product Name: $productName Machine Type: $machineType" return $products } $products += [pscustomobject]@{ ProductName = $productName MachineType = $machineType } } } return ,$products } # Parse the Lenovo PSREF page for the model $machineTypes = Get-LenovoPSREF -ModelName $Model if ($machineTypes.ProductName.Count -eq 0) { WriteLog "No machine types found for model: $Model" WriteLog "Enter a valid model or machine type in the -model parameter" exit } elseif ($machineTypes.ProductName.Count -eq 1) { $machineType = $machineTypes[0].MachineType $model = $machineTypes[0].ProductName } else { if ($VerbosePreference -ne 'Continue'){ Write-Output "Multiple machine types found for model: $Model" } WriteLog "Multiple machine types found for model: $Model" for ($i = 0; $i -lt $machineTypes.ProductName.Count; $i++) { if ($VerbosePreference -ne 'Continue'){ Write-Output "$($i + 1). $($machineTypes[$i].ProductName) ($($machineTypes[$i].MachineType))" } WriteLog "$($i + 1). $($machineTypes[$i].ProductName) ($($machineTypes[$i].MachineType))" } $selection = Read-Host "Enter the number of the model you want to select" $machineType = $machineTypes[$selection - 1].MachineType WriteLog "Selected machine type: $machineType" $model = $machineTypes[$selection - 1].ProductName WriteLog "Selected model: $model" } # Construct the catalog URL based on Windows release and machine type $ModelRelease = $machineType + "_Win" + $WindowsRelease $CatalogUrl = "https://download.lenovo.com/catalog/$ModelRelease.xml" WriteLog "Lenovo Driver catalog URL: $CatalogUrl" if (-not (Test-Url -Url $catalogUrl)) { Write-Error "Lenovo Driver catalog URL is not accessible: $catalogUrl" WriteLog "Lenovo Driver catalog URL is not accessible: $catalogUrl" exit } # Create the folder structure for the Lenovo drivers $driversFolder = "$DriversFolder\$Make" if (-not (Test-Path -Path $DriversFolder)) { WriteLog "Creating Drivers folder: $DriversFolder" New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null WriteLog "Drivers folder created" } # Download and parse the Lenovo catalog XML $LenovoCatalogXML = "$DriversFolder\$ModelRelease.xml" WriteLog "Downloading $catalogUrl to $LenovoCatalogXML" Start-BitsTransferWithRetry -Source $catalogUrl -Destination $LenovoCatalogXML WriteLog "Download Complete" $xmlContent = [xml](Get-Content -Path $LenovoCatalogXML) WriteLog "Parsing Lenovo catalog XML" # Process each package in the catalog foreach ($package in $xmlContent.packages.package) { $packageUrl = $package.location $category = $package.category #If category starts with BIOS, skip the package if ($category -like 'BIOS*') { continue } #If category name is 'Motherboard Devices Backplanes core chipset onboard video PCIe switches', truncate to 'Motherboard Devices' to shorten path if ($category -eq 'Motherboard Devices Backplanes core chipset onboard video PCIe switches') { $category = 'Motherboard Devices' } $packageName = [System.IO.Path]::GetFileName($packageUrl) #Remove the filename from the $packageURL $baseURL = $packageUrl -replace $packageName, "" # Download the package XML $packageXMLPath = "$DriversFolder\$packageName" WriteLog "Downloading $category package XML $packageUrl to $packageXMLPath" If ((Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath) -eq $false) { Write-Output "Failed to download $category package XML: $packageXMLPath" WriteLog "Failed to download $category package XML: $packageXMLPath" continue } # Load the package XML content $packageXmlContent = [xml](Get-Content -Path $packageXMLPath) $packageType = $packageXmlContent.Package.PackageType.type $packageTitle = $packageXmlContent.Package.title.InnerText # Fix the name for drivers that contain illegal characters for folder name purposes $packageTitle = $packageTitle -replace '[\\\/\:\*\?\"\<\>\|]', '_' # If ' - ' is in the package title, truncate the title to the first part of the string. $packageTitle = $packageTitle -replace ' - .*', '' #Check if packagetype = 2. If packagetype is not 2, skip the package. $packageType is a System.Xml.XmlElement. #This filters out Firmware, BIOS, and other non-INF drivers if ($packageType -ne 2) { Remove-Item -Path $packageXMLPath -Force continue } # Extract the driver file name and the extract command $driverFileName = $packageXmlContent.Package.Files.Installer.File.Name $extractCommand = $packageXmlContent.Package.ExtractCommand #if extract command is empty/missing, skip the package if (!($extractCommand)) { Remove-Item -Path $packageXMLPath -Force continue } # Create the download URL and folder structure $driverUrl = $baseUrl + $driverFileName $downloadFolder = "$DriversFolder\$Model\$Category\$packageTitle" $driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driverFileName # Check if file has already been downloaded if (Test-Path -Path $driverFilePath) { Write-Output "Driver already downloaded: $driverFilePath skipping" WriteLog "Driver already downloaded: $driverFilePath skipping" continue } if (-not (Test-Path -Path $downloadFolder)) { WriteLog "Creating download folder: $downloadFolder" New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null WriteLog "Download folder created" } # Download the driver with retry WriteLog "Downloading driver: $driverUrl to $driverFilePath" Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath WriteLog "Driver downloaded" # Make folder for extraction $extractFolder = $downloadFolder + "\" + $driverFileName.TrimEnd($driverFileName[-4..-1]) WriteLog "Creating extract folder: $extractFolder" New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null WriteLog "Extract folder created" # Modify the extract command $modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`"" # Extract the driver # Start-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait -NoNewWindow WriteLog "Extracting driver: $driverFilePath to $extractFolder" Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand | Out-Null WriteLog "Driver extracted" # Delete the .exe driver file after extraction WriteLog "Deleting driver installation file: $driverFilePath" Remove-Item -Path $driverFilePath -Force WriteLog "Driver installation file deleted: $driverFilePath" # Delete the package XML file after extraction WriteLog "Deleting package XML file: $packageXMLPath" Remove-Item -Path $packageXMLPath -Force WriteLog "Package XML file deleted" } #Delete the catalog XML file after processing WriteLog "Deleting catalog XML file: $LenovoCatalogXML" Remove-Item -Path $LenovoCatalogXML -Force WriteLog "Catalog XML file deleted" } function Get-DellDrivers { param ( [Parameter(Mandatory = $true)] [string]$Model, [Parameter(Mandatory = $true)] [ValidateSet("x64", "x86", "ARM64")] [string]$WindowsArch, [Parameter(Mandatory = $true)] [int]$WindowsRelease ) if (-not (Test-Path -Path $DriversFolder)) { WriteLog "Creating Drivers folder: $DriversFolder" New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null WriteLog "Drivers folder created" } $DriversFolder = "$DriversFolder\$Make" WriteLog "Creating Dell Drivers folder: $DriversFolder" New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null WriteLog "Dell Drivers folder created" #CatalogPC.cab is the catalog for Windows client PCs, Catalog.cab is the catalog for Windows Server if ($WindowsRelease -le 11) { $catalogUrl = "http://downloads.dell.com/catalog/CatalogPC.cab" $DellCabFile = "$DriversFolder\CatalogPC.cab" $DellCatalogXML = "$DriversFolder\CatalogPC.XML" } else { $catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab" $DellCabFile = "$DriversFolder\Catalog.cab" $DellCatalogXML = "$DriversFolder\Catalog.xml" } if (-not (Test-Url -Url $catalogUrl)) { WriteLog "Dell Catalog cab URL is not accessible: $catalogUrl Exiting" if ($VerbosePreference -ne 'Continue') { Write-Host "Dell Catalog cab URL is not accessible: $catalogUrl Exiting" } exit } WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $DellCabFile" Start-BitsTransferWithRetry -Source $catalogUrl -Destination $DellCabFile WriteLog "Dell Catalog cab file downloaded" WriteLog "Extracting Dell Catalog cab file to $DellCatalogXML" Invoke-Process -FilePath Expand.exe -ArgumentList "$DellCabFile $DellCatalogXML" | Out-Null WriteLog "Dell Catalog cab file extracted" $xmlContent = [xml](Get-Content -Path $DellCatalogXML) $baseLocation = "https://" + $xmlContent.manifest.baseLocation + "/" $latestDrivers = @{} $softwareComponents = $xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq "DRVR" } foreach ($component in $softwareComponents) { $models = $component.SupportedSystems.Brand.Model foreach ($item in $models) { if ($item.Display.'#cdata-section' -match $Model) { if ($WindowsRelease -le 11) { $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { $_.osArch -eq $WindowsArch } } elseif ($WindowsRelease -eq 2016) { $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W14") } } elseif ($WindowsRelease -eq 2019) { $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W19") } } elseif ($WindowsRelease -eq 2022) { $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W22") } } elseif ($WindowsRelease -eq 2025) { $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W25") } } else { $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W22") } } if ($validOS) { $driverPath = $component.path $downloadUrl = $baseLocation + $driverPath $driverFileName = [System.IO.Path]::GetFileName($driverPath) $name = $component.Name.Display.'#cdata-section' $name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' $name = $name -replace '[\,]', '-' $category = $component.Category.Display.'#cdata-section' $category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_' $version = [version]$component.vendorVersion $namePrefix = ($name -split '-')[0] # Use hash table to store the latest driver for each category to prevent downloading older driver versions if ($latestDrivers[$category]) { if ($latestDrivers[$category][$namePrefix]) { if ($latestDrivers[$category][$namePrefix].Version -lt $version) { $latestDrivers[$category][$namePrefix] = [PSCustomObject]@{ Name = $name; DownloadUrl = $downloadUrl; DriverFileName = $driverFileName; Version = $version; Category = $category } } } else { $latestDrivers[$category][$namePrefix] = [PSCustomObject]@{ Name = $name; DownloadUrl = $downloadUrl; DriverFileName = $driverFileName; Version = $version; Category = $category } } } else { $latestDrivers[$category] = @{} $latestDrivers[$category][$namePrefix] = [PSCustomObject]@{ Name = $name; DownloadUrl = $downloadUrl; DriverFileName = $driverFileName; Version = $version; Category = $category } } } } } } foreach ($category in $latestDrivers.Keys) { foreach ($driver in $latestDrivers[$category].Values) { $downloadFolder = "$DriversFolder\$Model\$($driver.Category)" $driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName if (Test-Path -Path $driverFilePath) { WriteLog "Driver already downloaded: $driverFilePath skipping" continue } WriteLog "Downloading driver: $($driver.Name)" if (-not (Test-Path -Path $downloadFolder)) { WriteLog "Creating download folder: $downloadFolder" New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null WriteLog "Download folder created" } WriteLog "Downloading driver: $($driver.DownloadUrl) to $driverFilePath" try{ Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath WriteLog "Driver downloaded" }catch{ WriteLog "Failed to download driver: $($driver.DownloadUrl) to $driverFilePath" continue } $extractFolder = $downloadFolder + "\" + $driver.DriverFileName.TrimEnd($driver.DriverFileName[-4..-1]) # WriteLog "Creating extraction folder: $extractFolder" # New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null # WriteLog "Extraction folder created" # $arguments = "/s /e=`"$extractFolder`"" $arguments = "/s /drivers=`"$extractFolder`"" WriteLog "Extracting driver: $driverFilePath $arguments" try { #If Category is Chipset, must add -wait $false to the Invoke-Process command line to prevent the script from hanging on the Intel chipset driver which leaves a Window open if ($driver.Category -eq "Chipset") { $process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false #Wait 5 seconds to allow for the extraction process to finish Start-Sleep -Seconds 5 $childProcesses = Get-ChildProcesses $process.Id # Find and stop the last created child process if ($childProcesses) { $latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1 Stop-Process -Id $latestProcess.ProcessId -Force # Sleep 1 second to let process finish exiting so its installer can be removed Start-Sleep -Seconds 1 } #If Category is Network and $isServer is $false, must add -wait $false to the Invoke-Process command line to prevent the script from hanging on the Intel network driver which leaves a Window open } elseif ($driver.Category -eq "Network" -and $isServer -eq $false) { $process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false #Sometimes the network drivers will extract on client OS, wait 5 seconds and check if the process is still running Start-Sleep -Seconds 5 if ($process.HasExited -eq $false) { $childProcesses = Get-ChildProcesses $process.Id # Find and stop the last created child process if ($childProcesses) { $latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1 Stop-Process -Id $latestProcess.ProcessId -Force #Move on to the next driver and skip this one - it won't extract on a client OS even with /s /e switches continue } } } else { Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null } # If $extractFolder is empty, try alternative extraction method if (!(Get-ChildItem -Path $extractFolder -Recurse | Where-Object { -not $_.PSIsContainer })) { WriteLog 'Extraction with /drivers= switch failed. Removing folder and retrying with /s /e switches' Remove-Item -Path $extractFolder -Force -Recurse -ErrorAction SilentlyContinue $arguments = "/s /e=`"$extractFolder`"" WriteLog "Extracting driver: $driverFilePath $arguments" Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null } } catch { WriteLog 'Extraction with /drivers= switch failed. Retrying with /s /e switches' $arguments = "/s /e=`"$extractFolder`"" WriteLog "Extracting driver: $driverFilePath $arguments" Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null } WriteLog "Driver extracted" WriteLog "Deleting driver file: $driverFilePath" Remove-Item -Path $driverFilePath -Force WriteLog "Driver file deleted" } } } function Get-ADKURL { param ( [ValidateSet("Windows ADK", "WinPE add-on")] [string]$ADKOption ) # Define base pattern for URL scraping $basePattern = '