From 21ebbdf9c9383cc8ec6318a293d5d50d24e957ae Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Mon, 17 Jun 2024 12:05:56 -0700
Subject: [PATCH 01/17] initial commit
---
FFUDevelopment/BuildFFUVM.ps1 | 996 ++++++++++++++++++++++++++++++++--
1 file changed, 955 insertions(+), 41 deletions(-)
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index dc4a6af..56ca111 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -184,13 +184,18 @@ param(
[string]$FFUDevelopmentPath = $PSScriptRoot,
[bool]$InstallApps,
[bool]$InstallOffice,
+ [string]$Make,
+ [string]$Model,
[Parameter(Mandatory = $false)]
[ValidateScript({
- 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'
- }
+ 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,
@@ -262,6 +267,9 @@ param(
[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'
}
@@ -281,9 +289,12 @@ param(
[bool]$CompactOS = $true,
[bool]$CleanupCaptureISO = $true,
[bool]$CleanupDeployISO = $true,
- [bool]$CleanupAppsISO = $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'
)
-$version = '2405.1'
+$version = '2406.1'
#Check if Hyper-V feature is installed (requires only checks the module)
$osInfo = Get-WmiObject -Class Win32_OperatingSystem
@@ -321,6 +332,7 @@ if (-not $KBPath) { $KBPath = "$FFUDevelopmentPath\KB" }
if (-not $DefenderPath) { $DefenderPath = "$AppsPath\Defender" }
if (-not $OneDrivePath) { $OneDrivePath = "$AppsPath\OneDrive" }
if (-not $EdgePath) { $EdgePath = "$AppsPath\Edge" }
+if (-not $DriversFolder) { $DriversFolder = "$FFUDevelopmentPath\Drivers" }
#FUNCTIONS
function WriteLog($LogText) {
@@ -433,6 +445,811 @@ function Invoke-Process {
}
}
+
+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 (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
+ WriteLog "Complete"
+
+ # Parse the content of the relevant nested divs
+ WriteLog "Parsing web content for models and download links"
+ $html = $webContent.Content
+ $nestedDivPattern = '
(.*?)
'
+ $nestedDivMatches = [regex]::Matches($html, $nestedDivPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
+
+ $models = @()
+ $modelPattern = '(.*?)
\s*\s*\s* \s*window.__DLCDetails__={(.*?)}'
+ $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
+ 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"
+ 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"
+
+ # 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
+ 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
+ )
+
+ # Parse the Lenovo PSREF search page for machine types
+ function Get-LenovoPSREF {
+ param (
+ [string]$ModelName
+ )
+
+ $url = "https://psref.lenovo.com/search"
+ WriteLog "Getting Lenovo PSREF page for model: $ModelName"
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ $webContent = Invoke-WebRequest -Uri $url
+ $VerbosePreference = $OriginalVerbosePreference
+ WriteLog "Complete"
+
+ # Access the parsed HTML
+ WriteLog "Parsing content"
+ $parsedHtml = $webContent.ParsedHtml
+
+ # Select the nodes you are interested in
+ $productNameNodes = $parsedHtml.getElementsByTagName("li") | Where-Object { $_.className -contains "productname_li" }
+ $products = @()
+ # Iterate through the nodes
+ foreach ($product in $productNameNodes) {
+ $productA = $product.getElementsByTagName("a") | Where-Object { $_.tagName -eq "a" }
+ $innertext = @($productA.innertext) # Ensure innertext is treated as an array
+ $productName = $innertext[0]
+
+ #if $productname contains 'Chromebook', skip the product
+ if ($productName -like '*Chromebook*') {
+ continue
+ }
+ $machineTypes = $innertext[1..($innertext.Count - 1)]
+
+ if ($innertext -match $ModelName) {
+ 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"
+ continue
+ }
+ $products += [pscustomobject]@{
+ ProductName = $productName
+ MachineType = $machineType
+ }
+ }
+ }
+ }
+
+ return ,$products
+ }
+
+ # Parse the Lenovo PSREF page for the model
+ $machineTypes = Get-LenovoPSREF -ModelName $Model
+ if ($machineTypes.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.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.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
+ 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
+ )
+
+ $catalogUrl = "http://downloads.dell.com/catalog/CatalogPC.cab"
+ 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
+ }
+
+ 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"
+
+ $DellCabFile = "$DriversFolder\CatalogPC.cab"
+ WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $DellCabFile"
+ Start-BitsTransferWithRetry -Source $catalogUrl -Destination $DellCabFile
+ WriteLog "Dell Catalog cab file downloaded"
+
+ $DellCatalogXML = "$DriversFolder\CatalogPC.XML"
+ WriteLog "Extracting Dell Catalog cab file to $DellCatalogXML"
+ Invoke-Process -FilePath Expand.exe -ArgumentList "$DellCabFile $DellCatalogXML"
+ 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) {
+ $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { $_.osArch -eq $WindowsArch }
+ 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'
+ $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) {
+ Write-Output "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`""
+ WriteLog "Extracting driver: $driverFilePath to $extractFolder"
+ Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments
+ 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")]
@@ -450,7 +1267,10 @@ function Get-ADKURL {
try {
# Retrieve content of Microsoft documentation page
- $ADKWebPage = Invoke-RestMethod "https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install"
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ $ADKWebPage = Invoke-RestMethod "https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install" -Headers (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
# Extract download URL based on specified pattern
$ADKMatch = [regex]::Match($ADKWebPage, $ADKUrlPattern)
@@ -469,7 +1289,10 @@ function Get-ADKURL {
}
# Retrieve headers of the FWlink URL
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
$FWLinkRequest = Invoke-WebRequest -Uri $ADKFWLink -Method Head -MaximumRedirection 0 -ErrorAction SilentlyContinue
+ $VerbosePreference = $OriginalVerbosePreference
if ($FWLinkRequest.StatusCode -ne 302) {
WriteLog "Failed to retrieve ADK download URL. Unexpected status code: $($FWLinkRequest.StatusCode)"
@@ -514,7 +1337,7 @@ function Install-ADK {
$installerLocation = Join-Path $env:TEMP $installer
WriteLog "Downloading $ADKOption from $ADKUrl to $installerLocation"
- Start-BitsTransfer -Source $ADKUrl -Destination $installerLocation -ErrorAction Stop
+ Start-BitsTransferWithRetry -Source $ADKUrl -Destination $installerLocation -ErrorAction Stop
WriteLog "$ADKOption downloaded to $installerLocation"
WriteLog "Installing $ADKOption with $feature enabled"
@@ -606,7 +1429,7 @@ function Confirm-ADKVersionIsLatest {
$installedADKVersion = $adkRegKey.GetValue("DisplayVersion")
# Retrieve content of Microsoft documentation page
- $adkWebPage = Invoke-RestMethod "https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install"
+ $adkWebPage = Invoke-RestMethod "https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install" -Headers (Get-Headers) -UserAgent $UserAgent
# Specify regex pattern for ADK version
$adkVersionPattern = 'ADK\s+(\d+(\.\d+)+)'
# Check for regex pattern match
@@ -720,7 +1543,10 @@ function Get-WindowsESD {
# Download cab file
WriteLog "Downloading Cab file"
$cabFilePath = Join-Path $PSScriptRoot "tempCabFile.cab"
- Invoke-WebRequest -Uri $cabFileUrl -OutFile $cabFilePath
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ Invoke-WebRequest -Uri $cabFileUrl -OutFile $cabFilePath -Headers (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
WriteLog "Download succeeded"
# Extract XML from cab file
@@ -744,7 +1570,10 @@ function Get-WindowsESD {
#Required to fix slow downloads
$ProgressPreference = 'SilentlyContinue'
WriteLog "Downloading $($file.filePath) to $esdFIlePath"
- Invoke-WebRequest -Uri $file.FilePath -OutFile $esdFilePath
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ Invoke-WebRequest -Uri $file.FilePath -OutFile $esdFilePath -Headers (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
WriteLog "Download succeeded"
#Set back to show progress
$ProgressPreference = 'Continue'
@@ -758,9 +1587,28 @@ function Get-WindowsESD {
}
}
+#Microsoft webpages are intermittently returning 404 errors when downloading ODT and Surface Drivers.
+function Get-Headers{
+ $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"
+ }
+}
+
function Get-ODTURL {
- [String]$MSWebPage = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
+ # [String]$MSWebPage = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
+ [String]$MSWebPage = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117' -Headers (Get-Headers) -UserAgent $UserAgent
$MSWebPage | ForEach-Object {
if ($_ -match 'url=(https://.*officedeploymenttool.*\.exe)') {
@@ -774,11 +1622,13 @@ function Get-Office {
$ODTUrl = Get-ODTURL
$ODTInstallFile = "$env:TEMP\odtsetup.exe"
WriteLog "Downloading Office Deployment Toolkit from $ODTUrl to $ODTInstallFile"
- Invoke-WebRequest -Uri $ODTUrl -OutFile $ODTInstallFile
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ Invoke-WebRequest -Uri $ODTUrl -OutFile $ODTInstallFile -Headers (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
# Extract ODT
WriteLog "Extracting ODT to $OfficePath"
- # Start-Process -FilePath $ODTInstallFile -ArgumentList "/extract:$OfficePath /quiet" -Wait
Invoke-Process $ODTInstallFile "/extract:$OfficePath /quiet"
# Run setup.exe with config.xml and modify xml file to download to $OfficePath
@@ -787,7 +1637,6 @@ function Get-Office {
$xmlContent.Configuration.Add.SourcePath = $OfficePath
$xmlContent.Save($ConfigXml)
WriteLog "Downloading M365 Apps/Office to $OfficePath"
- # Start-Process -FilePath "$OfficePath\setup.exe" -ArgumentList "/download $ConfigXml" -Wait
Invoke-Process $OfficePath\setup.exe "/download $ConfigXml"
WriteLog "Cleaning up ODT default config files and checking InstallAppsandSysprep.cmd file for proper command line"
@@ -813,7 +1662,10 @@ function Get-KBLink {
[Parameter(Mandatory)]
[string]$Name
)
- $results = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=$Name"
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ $results = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=$Name" -Headers (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
$kbids = $results.InputFields |
Where-Object { $_.type -eq 'Button' -and $_.Value -eq 'Download' } |
Select-Object -ExpandProperty ID
@@ -840,10 +1692,13 @@ function Get-KBLink {
Write-Verbose -Message "Downloading information for $guid"
$post = @{ size = 0; updateID = $guid; uidInfo = $guid } | ConvertTo-Json -Compress
$body = @{ updateIDs = "[$post]" }
- $links = Invoke-WebRequest -Uri 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx' -Method Post -Body $body |
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ $links = Invoke-WebRequest -Uri 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx' -Method Post -Body $body -Headers (Get-Headers) -UserAgent $UserAgent |
Select-Object -ExpandProperty Content |
Select-String -AllMatches -Pattern "http[s]?://[^']*\.microsoft\.com/[^']*|http[s]?://[^']*\.windowsupdate\.com/[^']*" |
Select-Object -Unique
+ $VerbosePreference = $OriginalVerbosePreference
foreach ($link in $links) {
$link.matches.value
@@ -870,7 +1725,10 @@ function Get-LatestWindowsKB {
}
# Use Invoke-WebRequest to fetch the content of the page
- $response = Invoke-WebRequest -Uri $updateHistoryUrl
+ $OriginalVerbosePreference = $VerbosePreference
+ $VerbosePreference = 'SilentlyContinue'
+ $response = Invoke-WebRequest -Uri $updateHistoryUrl -Headers (Get-Headers) -UserAgent $UserAgent
+ $VerbosePreference = $OriginalVerbosePreference
# Use a regular expression to find the KB article number
$kbArticleRegex = 'KB\d+'
@@ -897,7 +1755,7 @@ function Save-KB {
if ($WindowsArch -is [array]) {
#Some file names include either x64 or amd64
if ($link -match $WindowsArch[0] -or $link -match $WindowsArch[1]) {
- Start-BitsTransfer -Source $link -Destination $Path
+ Start-BitsTransferWithRetry -Source $link -Destination $Path
$fileName = ($link -split '/')[-1]
break
}
@@ -919,27 +1777,27 @@ function Save-KB {
#Make sure we're getting the correct architecture for the Security Health Setup update
if ($WindowsArch -eq 'x64'){
if ($link -match 'securityhealthsetup_e1'){
- Start-BitsTransfer -Source $link -Destination $Path
+ Start-BitsTransferWithRetry -Source $link -Destination $Path
$fileName = ($link -split '/')[-1]
break
}
}
elseif ($WindowsArch -eq 'arm64'){
if ($link -match 'securityhealthsetup_25'){
- Start-BitsTransfer -Source $link -Destination $Path
+ Start-BitsTransferWithRetry -Source $link -Destination $Path
$fileName = ($link -split '/')[-1]
break
}
}
continue
}
- Start-BitsTransfer -Source $link -Destination $Path
+ Start-BitsTransferWithRetry -Source $link -Destination $Path
$fileName = ($link -split '/')[-1]
}
}
else {
if ($link -match $WindowsArch) {
- Start-BitsTransfer -Source $link -Destination $Path
+ Start-BitsTransferWithRetry -Source $link -Destination $Path
$fileName = ($link -split '/')[-1]
break
}
@@ -1447,7 +2305,7 @@ function Optimize-FFUCaptureDrive {
WriteLog 'Mounting VHDX for volume optimization'
Mount-VHD -Path $VhdxPath
WriteLog 'Defragmenting Windows partition...'
- Optimize-Volume -DriveLetter W -Defrag -NormalPriority -Verbose
+ Optimize-Volume -DriveLetter W -Defrag -NormalPriority -Verbose
WriteLog 'Performing slab consolidation on Windows partition...'
Optimize-Volume -DriveLetter W -SlabConsolidate -NormalPriority -Verbose
WriteLog 'Dismounting VHDX'
@@ -1533,7 +2391,7 @@ function New-FFU {
WriteLog 'Mounting complete'
WriteLog 'Adding drivers - This will take a few minutes, please be patient'
try {
- Add-WindowsDriver -Path "$FFUDevelopmentPath\Mount" -Driver "$FFUDevelopmentPath\Drivers" -Recurse -ErrorAction SilentlyContinue | Out-null
+ Add-WindowsDriver -Path "$FFUDevelopmentPath\Mount" -Driver "$DriversFolder" -Recurse -ErrorAction SilentlyContinue | Out-null
}
catch {
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
@@ -1775,7 +2633,12 @@ Function New-DeploymentUSB {
#Copy drivers using robocopy due to potential size
if ($CopyDrivers) {
WriteLog "Copying drivers to $DeployPartitionDriveLetter\Drivers"
- robocopy "$FFUDevelopmentPath\Drivers" "$DeployPartitionDriveLetter\Drivers" /E /R:5 /W:5 /J
+ if ($Make){
+ robocopy "$DriversFolder\$Make" "$DeployPartitionDriveLetter\Drivers" /E /R:5 /W:5 /J
+ }else{
+ robocopy "$DriversFolder" "$DeployPartitionDriveLetter\Drivers" /E /R:5 /W:5 /J
+ }
+
}
#Copy Unattend folder in the FFU folder to the USB drive. Can use copy-item as it's a small folder
if ($CopyUnattend) {
@@ -2016,7 +2879,46 @@ if (($InstallApps -eq $false) -and (($UpdateLatestDefender -eq $true) -or ($Upda
}
#Get script variable values
-LogVariableValues
+LogVariableValues
+
+#Check if environment is dirty
+If (Test-Path -Path "$FFUDevelopmentPath\dirty.txt") {
+ Get-FFUEnvironment
+}
+WriteLog 'Creating dirty.txt file'
+New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null
+
+#Get drivers first since user could be prompted for additional info
+if (($make) -and ($model)){
+ try {
+ if ($Make -eq 'HP'){
+ WriteLog 'Getting HP drivers'
+ Get-HPDrivers -Make $Make -Model $Model -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease -WindowsVersion $WindowsVersion
+ WriteLog 'Getting HP drivers completed successfully'
+ }
+ if ($make -eq 'Microsoft'){
+ WriteLog 'Getting Microsoft drivers'
+ Get-MicrosoftDrivers -Make $Make -Model $Model -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease
+ WriteLog 'Getting Microsoft drivers completed successfully'
+ }
+ if ($make -eq 'Lenovo'){
+ WriteLog 'Getting Lenovo drivers'
+ Get-LenovoDrivers -Model $Model -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease
+ WriteLog 'Getting Lenovo drivers completed successfully'
+ }
+ if ($make -eq 'Dell'){
+ WriteLog 'Getting Dell drivers'
+ #Dell mixes Win10 and 11 drivers, hence no WindowsRelease parameter
+ Get-DellDrivers -Model $Model -WindowsArch $WindowsArch
+ WriteLog 'Getting Dell drivers completed successfully'
+ }
+ }
+ catch {
+ Writelog "Getting drivers failed with error $_"
+ throw $_
+ }
+
+}
#Get Windows ADK
try {
@@ -2029,13 +2931,6 @@ catch {
throw $_
}
-#Check if environment is dirty
-If (Test-Path -Path "$FFUDevelopmentPath\dirty.txt") {
- Get-FFUEnvironment
-}
-WriteLog 'Creating dirty.txt file'
-New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null
-
#Create apps ISO for Office and/or 3rd party apps
if ($InstallApps) {
try {
@@ -2073,6 +2968,7 @@ if ($InstallApps) {
WriteLog "Searching for $Name from Microsoft Update Catalog and saving to $DefenderPath"
$KBFilePath = Save-KB -Name $Name -Path $DefenderPath
WriteLog "Latest Defender Platform and Definitions saved to $DefenderPath\$KBFilePath"
+
#Modify InstallAppsandSysprep.cmd to add in $KBFilePath on the line after REM Install Defender Update Platform
WriteLog "Updating $AppsPath\InstallAppsandSysprep.cmd to include Defender Platform Update"
$CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd"
@@ -2102,7 +2998,7 @@ if ($InstallApps) {
$DefenderDefURL = 'https://go.microsoft.com/fwlink/?LinkID=121721&arch=arm'
}
try {
- Start-BitsTransfer -Source $DefenderDefURL -Destination "$DefenderPath\mpam-fe.exe"
+ Start-BitsTransferWithRetry -Source $DefenderDefURL -Destination "$DefenderPath\mpam-fe.exe"
WriteLog "Defender Definitions downloaded to $DefenderPath\mpam-fe.exe"
}
catch {
@@ -2120,6 +3016,7 @@ if ($InstallApps) {
}
#Download and Install OneDrive Per Machine
if ($UpdateOneDrive) {
+ WriteLog "`$UpdateOneDrive is set to true, checking for latest OneDrive client"
#Check if $OneDrivePath exists, if not, create it
If (-not (Test-Path -Path $OneDrivePath)) {
WriteLog "Creating $OneDrivePath"
@@ -2128,7 +3025,7 @@ if ($InstallApps) {
WriteLog "Downloading latest OneDrive client"
$OneDriveURL = 'https://go.microsoft.com/fwlink/?linkid=844652'
try {
- Start-BitsTransfer -Source $OneDriveURL -Destination "$OneDrivePath\OneDriveSetup.exe"
+ Start-BitsTransferWithRetry -Source $OneDriveURL -Destination "$OneDrivePath\OneDriveSetup.exe"
WriteLog "OneDrive client downloaded to $OneDrivePath\OneDriveSetup.exe"
}
catch {
@@ -2162,9 +3059,9 @@ if ($InstallApps) {
#Extract Edge cab file to same folder as $EdgeFilePath
$EdgeMSIFileName = "MicrosoftEdgeEnterprise$WindowsArch.msi"
$EdgeFullFilePath = "$EdgePath\$EdgeMSIFileName"
- WriteLog "Extracting $EdgeCABFilePath"
+ WriteLog "Expanding $EdgeCABFilePath"
Invoke-Process Expand "$EdgeCABFilePath -F:*.msi $EdgeFullFilePath"
- WriteLog "Extraction complete"
+ WriteLog "Expansion complete"
#Modify InstallAppsandSysprep.cmd to add in $KBFilePath on the line after REM Install Edge Stable
WriteLog "Updating $AppsPath\InstallAppsandSysprep.cmd to include Edge Stable $WindowsArch release"
@@ -2265,12 +3162,13 @@ try {
if ($UpdateLatestCU -or $UpdateLatestNet) {
try {
WriteLog "Adding KBs to $WindowsPartition"
+ WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient'
Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath -PreventPending | Out-Null
WriteLog "KBs added to $WindowsPartition"
WriteLog "Removing $KBPath"
Remove-Item -Path $KBPath -Recurse -Force | Out-Null
WriteLog "Clean Up the WinSxS Folder"
- Invoke-Process cmd "/c ""$DandIEnv"" && Dism /Image:$WindowsPartition /Cleanup-Image /StartComponentCleanup /ResetBase" | Out-Null
+ Dism /Image:$WindowsPartition /Cleanup-Image /StartComponentCleanup /ResetBase | Out-Null
WriteLog "Clean Up the WinSxS Folder completed"
}
catch {
@@ -2523,8 +3421,24 @@ If ($CleanupAppsISO) {
Writelog "Removing $AppsISO failed with error $_"
throw $_
}
+If ($CleanupDrivers){
+ try {
+ If (Test-Path -Path $Driversfolder\$Make) {
+ WriteLog "Removing $Driversfolder\$Make"
+ Remove-Item -Path $Driversfolder\$Make -Force -Recurse
+ WriteLog "Removal complete"
+ }
+ }
+ catch {
+ Writelog "Removing $Driversfolder\$Make failed with error $_"
+ throw $_
+ }
+
+}
}
#Clean up dirty.txt file
Remove-Item -Path .\dirty.txt -Force | out-null
-Write-Host "Script complete"
-WriteLog "Script complete"
+if ($VerbosePreference -ne 'Continue'){
+ Write-Host 'Script complete'
+}
+WriteLog 'Script complete'
From 08e354ad173146836f10a861a9ccc50b71fce9dc Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Mon, 17 Jun 2024 12:08:01 -0700
Subject: [PATCH 02/17] updated version in applyffu.ps1
---
FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
index ad06c3c..c6b9491 100644
--- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
+++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
@@ -117,7 +117,7 @@ $LogFileName = 'ScriptLog.txt'
$USBDrive = Get-USBDrive
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
$LogFile = $USBDrive + $LogFilename
-$version = '2405.1'
+$version = '2406.1'
WriteLog 'Begin Logging'
WriteLog "Script version: $version"
From 181eed8f12991f19aca9dfc93174ca34c1c3e7f1 Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Mon, 17 Jun 2024 14:40:55 -0700
Subject: [PATCH 03/17] docs
---
ChangeLog.md | 135 ++++++++++++++++++++++++++++++++++
FFUDevelopment/BuildFFUVM.ps1 | 2 +-
README.md | 118 -----------------------------
image-1.png | Bin 0 -> 33610 bytes
image.png | Bin 0 -> 203504 bytes
5 files changed, 136 insertions(+), 119 deletions(-)
create mode 100644 ChangeLog.md
create mode 100644 image-1.png
create mode 100644 image.png
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
index 0000000..1c75221
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,135 @@
+# Change Log
+
+**2406.1**
+This is a major release that includes the ability to download drivers from the 4 major OEMs (Microsoft, Dell, HP, Lenovo) by simply passing the -Make and -Model parameters to the command line.
+
+For Dell, HP, and Lenovo, the script leverages a similar process to their corresponding tools that automate driver downloads (Dell SupportAssist, HP Image Assistant, Lenovo System Update/Update Retriever). For Microsoft Surface, it scrapes the Surface Downloads page for the appropriate MSI file to download. Using this method, the drives that are downloaded will be the latest provided by the OEM, unlike other tools that download out of date enteprise CAB files that are made for ConfigMgr.
+
+The script supports lookups using the -model parameter. For example, if you want to download the drivers for a Surface Laptop Go 3, but don't know the exact model name, you could set -Make 'Microsoft' -Model 'Laptop Go' and it'll give you a list of Surface devices to pick from. If you know the exact name, it'll use that.
+
+
+
+
+
+The goal here is to make it easy to discover the drivers you want to download without having to know the exact model names.
+
+There are likely going to be bugs with this, but in my testing things seem to work well for the makes and models that I've tried. If you notice something, please fill out an issue in the repro and I'll take a look. If you want to fix whatever issue you're running into, submit a pull request.
+
+
+
+**2405.1**
+- Moved the resetbase command from within the VM to after servicing the VHDX. This will make it so the FFU size is smaller after the latest CU or .NET framework are installed. (Thanks to Mike Kelly for the PR [Commit](https://github.com/rbalsleyMSFT/FFU/pull/24))
+- Some additional FFU size reduction enhancements (Thanks Zehadi Alam [Commit](https://github.com/rbalsleyMSFT/FFU/pull/25)):
+ - Disk cleanup is now run before sysprep to help reduce FFU file size
+ - Before FFU capture, Optimize-FFU is run to defrag and slabconsolidate the VHDX
+
+
+**2404.3**
+- Fixed an issue where the latest Windows CU wasn't downloading properly [Commit](https://github.com/rbalsleyMSFT/FFU/commit/ae59183a199f39b310c79b31c9b4980fafdeb79b)
+
+**2404.2**
+
+- If setting -installdrivers to $true and -logicalsectorsizebytes to 4096, the script will now set $copyDrivers to $true. This will create a drivers folder on the deploy partition of the USB drive with the drivers that were supposed to be added to the FFU. There's currently a bug with servicing FFUs with 4096 logical sector byte sizes. Prior to this fix, the script would tell the user to manually set -copydrivers to $true as workaround. This fix just does the workaround automatically.
+
+**2404.1**
+
+There's a big change with this release related to the ADK. The ADK will now be automatically updated to the latest ADK release. This is required in order to fix an issue with optimized FFUs not applying due to an issue with DISM/FFUProvider.dll. The FFUProvider.dll fix was added to the Sept 2023 ADK. Since we now have the ability to auto upgrade the ADK, I'm more confident in having the BuildFFUVM script creating a complete FFU now (prior it was only creating 3 partitions instead of 4 with the recovery partition - at deployment time, the ApplyFFU.ps1 script would create an empty recovery partition and Windows would populate it on first boot). Please open an issue if this creates a problem for you. I do realize that any new ADK release can have it's own challenges and issues and I do suspect we'll see a new ADK released later this year.
+
+- Allow for ISOs with single index WIMs to work [Issue 10](https://github.com/rbalsleyMSFT/FFU/issues/10) - [Commit](https://github.com/rbalsleyMSFT/FFU/commit/9e2da741d53652e6e600ca19cfd38f507bd01fde)
+- Added more robust ADK handling. Will now check for the latest ADK and download it if not installed. Thanks to [Zehadi Alam](https://github.com/zehadialam) [PR 18](https://github.com/rbalsleyMSFT/FFU/pull/18)
+- Revert code back to allow optimized FFUs to be applied via ApplyFFU.ps1 now that Sept 2023 ADK release has FFUProvider.dll fix. [Commit](https://github.com/rbalsleyMSFT/FFU/commit/79364e334d6d09ff150e70dab7bfb2637d0ad8a8)
+- Changed how the script searches for the latest CU. Instead of relying on the Windows release info page to grab the KB number, will just use the MU Catalog, the same as what we do for the .NET Framework. Windows release info page is updated manually and is unknown as to when it will be updated. [Commit](https://github.com/rbalsleyMSFT/FFU/commit/6fd5a4a41fd9ce2f842f43dc3a69bda264c29fa6)
+- Added fix to not allow computer names with spaces. Thanks to [JoeMama54 (Rob)](https://github.com/JoeMama54) [PR 20](https://github.com/rbalsleyMSFT/FFU/pull/20)
+
+**2403.1**
+
+Fixed an issue with the SecurityHealthSetup.exe file giving an error when building the VM if -UpdateLatestDefender was set to $true. A new update for this came out on 3/21 which included a x64 and ARM64 binary. This file doesn't have an architecture designation to it, so it's impossible to know which file is for which architecture. Investigating to see if we can fix this in the Microsoft Update catalog. There is a web site to pull this from, but the support article is out of date.
+
+Included ADK functions from Zehadi Alam [Introduce Automated ADK Retrieval and Installation Functions #14](https://github.com/rbalsleyMSFT/FFU/pull/14) to automate the installation of the ADK if it's not present. Thanks, Zehadi!
+
+**2402.1**
+
+**New functionality**
+
+* If -BuildUSBDrve $true, script will now check for USB drive before continuing. If not present, script exits
+* Added a number of new parameters.
+
+| Parameter | Type | Description |
+| -------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| CopyPEDrivers | Bool | When set to\$true, will copy the drivers from the \$FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is \$false. |
+| RemoveFFU | Bool | 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. |
+| UpdateLatestCU | Bool | When set to\$true, will download and install the latest cumulative update for Windows 10/11. Default is \$false. |
+| UpdateLatestNet | Bool | When set to\$true, will download and install the latest .NET Framework for Windows 10/11. Default is \$false. |
+| UpdateLatestDefender | Bool | When set to\$true, will download and install the latest Windows Defender definitions and Defender platform update. Default is \$false. |
+| UpdateEdge | Bool | When set to\$true, will download and install the latest Microsoft Edge for Windows 10/11. Default is \$false. |
+| UpdateOneDrive | Bool | 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. |
+| CopyPPKG | Bool | 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. |
+| CopyUnattend | Bool | When set to\$true, will copy the \$FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Default is \$false. |
+| CopyAutopilot | Bool | When set to\$true, will copy the \$FFUDevelopmentPath\Autopilot folder to the Deployment partition of the USB drive. Default is \$false. |
+| CompactOS | Bool | When set to\$true, will compact the OS when building the FFU. Default is \$true. |
+| CleanupCaptureISO | Bool | When set to\$true, will remove the WinPE capture ISO after the FFU has been captured. Default is \$true. |
+| CleanupDeployISO | Bool | When set to\$true, will remove the WinPE deployment ISO after the FFU has been captured. Default is \$true. |
+| CleanupAppsISO | Bool | When set to\$true, will remove the Apps ISO after the FFU has been captured. Default is \$true. |
+
+* Updated the docs with the new variables and made some minor modifications.
+* Changed version variable to 2402.1
+
+**2401.1**
+
+- Added -CopyDrivers boolean parameter to control the ability to copy drivers to the USB drive in the deploy partition drivers folder.
+- Changed version varaible to 2401.1
+- When creating the scratch VHDX, switched it to create a dynamic VHDX instead of fixed
+- Fixed an issue where adding drivers to the FFU would sometimes fail and would cause the script to exit unexpectedly
+- Added -optimize boolean parameter to control whether the FFU is optimized or not. This defaults to $true and in most cases should be left this way.
+- Fixed an issue where if the script failed to create the FFU and the old VM was left behind, it wouldn't clean it up if the VM was in the running state. Will now turn off any running VM with a name prefix of _FFU- and then remove any VMs with a name _FFU- if the environment is flagged as dirty.
+- Fixed an issue where devices that ship with UFS drives were unable to image due to the script setting a LogicalSectorSizeBytes value of 512. If you're creating a FFU for devices that have UFS drives, you'll need to set -LogicalSectorSizeBytes 4096.
+- There's a known issue where adding drivers to a FFU that has a LogicalSectorSizeBytes value of 4096. Added some code to prevent allowing this to happen. Please use -copydrivers $true as a workaround for now. We're investigating whether this is a bug or not.
+- Fixed an issue where VHDX only captures (i.e. where -installapps $false) would not install Windows updates.
+- Changed Office deployment to use Current channel instead of Monthly enterprise. If you want to change to Monthly Enterprise channel, it's recommended to leverage Intune.
+
+**2309.2**
+
+New Features
+
+**Multiple USB Drive Support**
+
+You can now plug in multiple USB drives (even using a USB hub) to create multiple USB drives for deployment. This is great for partners or customers who need to provide USB drives to their employees to image a large number of devices. It will copy the content to one USB drive at a time. The most USB drives we've seen created so far is 23 via a USB hub. Open an issue if you see any problems with this.
+
+**Robocopy support**
+
+Replaced Copy-Item with Robocopy when copying content to the USB drive(s). Copy-Item uses buffered IO, which can take a long time to copy large files. Robocopy with the /J switch allows for unbuffered IO support, which reduces the amount of time to copy.
+
+**Better error handling**
+
+Prior to 2309.2, if the script failed or you manually killed the script (ctrl+c, or closing the PowerShell window), the environment would end up in a bad state and you had to do a number of things to manually clean up the environment. Added a new function called Get-FFUEnvironment and a new text file called dirty.txt that gets created in the FFUDevelopment folder. When the script starts, it checks for the dirty.txt file and if it sees it, Get-FFUEnvironment runs and cleans out a number of things to help ensure the next run will complete successfully. Open an issue if you still see problems when the script fails and the next run of the script fails.Â
+
+Bug Fixes
+
+- In 2309.1, added a 15 second sleep to allow for the registry to unload to fix a Critical Process Died error on deployment. In this build, increased that to 60 seconds.
+- Fixed an issue where the script was incorrectly detecting the USB drive boot and deploy drive letters which caused issues when attempting to copy the WinPE files to the boot partition.
+
+**2309.1**
+
+- Fixed an issue with a Critical Process Died BSOD that would happen when using -installapps $false. More detailed information in the [commit](https://github.com/rbalsleyMSFT/FFU/pull/2/commits/34efbda7ec56dc7cb43ac42b058725d56c8b8899)
+
+**2306.1.2**
+
+- Fixed an issue where manually entering a name wouldn't name the computer as expected
+
+**2306.1.1**
+
+- Included some better error handling if defining optionalfeatures that require source folders (netfx3). ESD files don't have source folders like ISO media, which means installing .net 3.5 as an optional feature would fail. Also cleaned up some formatting.
+
+**2306.1**
+
+- Added support to automatically download the latest Windows 10 or 11 media via the media creation tool (thanks to [Michael](https://oofhours.com/2022/09/14/want-your-own-windows-11-21h2-arm64-isos/) for the idea). This also allows for different architecture, language, and media type support. If you omit the -ISOPath, the script will download the Windows 11 x64 English (US) consumer media.
+
+ An example command to download Windows 11 Pro x64 English (US) consumer media with Office and install drivers (it won't download drivers, you'll put those in your c:\FFUDevelopment\Drivers folder)
+
+ .\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
+
+ An example command to download Windows 11 Pro x64 French (CA) consumer media with Office and install 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
+- Changed default size of System/EFI partition to 260MB from 256MB to accomodate 4Kn drives. 4Kn support needs more testing. I'm not confident yet that this can be done with VMs and FFUs.
+- Added versioning with a new version parameter. Using YYMM as the format followed by a point release.
\ No newline at end of file
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index 56ca111..533e740 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -2889,7 +2889,7 @@ WriteLog 'Creating dirty.txt file'
New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null
#Get drivers first since user could be prompted for additional info
-if (($make) -and ($model)){
+if (($make -and $model) -and ($installdrivers -or $copydrivers)) {
try {
if ($Make -eq 'HP'){
WriteLog 'Getting HP drivers'
diff --git a/README.md b/README.md
index b3c4ff8..b2a9100 100644
--- a/README.md
+++ b/README.md
@@ -6,124 +6,6 @@ This process will copy Windows in about 2-3 minutes to the target device, option
While we use this in Education at Microsoft, other industries can use it as well. We esepcially see a need for something like this with partners who do re-imaging on behalf of customers. The difference in Education is that they typically have large deployments that tend to happen at the beginning of the school year and any amount of time saved is helpful. Microsoft Deployment Toolkit, Configuration Manager, and other community solutions are all great solutions, but are typically slower due to WIM deployments being file-based while FFU files are sector-based.
-# Updates
-**2405.1**
-- Moved the resetbase command from within the VM to after servicing the VHDX. This will make it so the FFU size is smaller after the latest CU or .NET framework are installed. (Thanks to Mike Kelly for the PR [Commit](https://github.com/rbalsleyMSFT/FFU/pull/24))
-- Some additional FFU size reduction enhancements (Thanks Zehadi Alam [Commit](https://github.com/rbalsleyMSFT/FFU/pull/25)):
- - Disk cleanup is now run before sysprep to help reduce FFU file size
- - Before FFU capture, Optimize-FFU is run to defrag and slabconsolidate the VHDX
-
-
-**2404.3**
-- Fixed an issue where the latest Windows CU wasn't downloading properly [Commit](https://github.com/rbalsleyMSFT/FFU/commit/ae59183a199f39b310c79b31c9b4980fafdeb79b)
-
-**2404.2**
-
-- If setting -installdrivers to $true and -logicalsectorsizebytes to 4096, the script will now set $copyDrivers to $true. This will create a drivers folder on the deploy partition of the USB drive with the drivers that were supposed to be added to the FFU. There's currently a bug with servicing FFUs with 4096 logical sector byte sizes. Prior to this fix, the script would tell the user to manually set -copydrivers to $true as workaround. This fix just does the workaround automatically.
-
-**2404.1**
-
-There's a big change with this release related to the ADK. The ADK will now be automatically updated to the latest ADK release. This is required in order to fix an issue with optimized FFUs not applying due to an issue with DISM/FFUProvider.dll. The FFUProvider.dll fix was added to the Sept 2023 ADK. Since we now have the ability to auto upgrade the ADK, I'm more confident in having the BuildFFUVM script creating a complete FFU now (prior it was only creating 3 partitions instead of 4 with the recovery partition - at deployment time, the ApplyFFU.ps1 script would create an empty recovery partition and Windows would populate it on first boot). Please open an issue if this creates a problem for you. I do realize that any new ADK release can have it's own challenges and issues and I do suspect we'll see a new ADK released later this year.
-
-- Allow for ISOs with single index WIMs to work [Issue 10](https://github.com/rbalsleyMSFT/FFU/issues/10) - [Commit](https://github.com/rbalsleyMSFT/FFU/commit/9e2da741d53652e6e600ca19cfd38f507bd01fde)
-- Added more robust ADK handling. Will now check for the latest ADK and download it if not installed. Thanks to [Zehadi Alam](https://github.com/zehadialam) [PR 18](https://github.com/rbalsleyMSFT/FFU/pull/18)
-- Revert code back to allow optimized FFUs to be applied via ApplyFFU.ps1 now that Sept 2023 ADK release has FFUProvider.dll fix. [Commit](https://github.com/rbalsleyMSFT/FFU/commit/79364e334d6d09ff150e70dab7bfb2637d0ad8a8)
-- Changed how the script searches for the latest CU. Instead of relying on the Windows release info page to grab the KB number, will just use the MU Catalog, the same as what we do for the .NET Framework. Windows release info page is updated manually and is unknown as to when it will be updated. [Commit](https://github.com/rbalsleyMSFT/FFU/commit/6fd5a4a41fd9ce2f842f43dc3a69bda264c29fa6)
-- Added fix to not allow computer names with spaces. Thanks to [JoeMama54 (Rob)](https://github.com/JoeMama54) [PR 20](https://github.com/rbalsleyMSFT/FFU/pull/20)
-
-**2403.1**
-
-Fixed an issue with the SecurityHealthSetup.exe file giving an error when building the VM if -UpdateLatestDefender was set to $true. A new update for this came out on 3/21 which included a x64 and ARM64 binary. This file doesn't have an architecture designation to it, so it's impossible to know which file is for which architecture. Investigating to see if we can fix this in the Microsoft Update catalog. There is a web site to pull this from, but the support article is out of date.
-
-Included ADK functions from Zehadi Alam [Introduce Automated ADK Retrieval and Installation Functions #14](https://github.com/rbalsleyMSFT/FFU/pull/14) to automate the installation of the ADK if it's not present. Thanks, Zehadi!
-
-**2402.1**
-
-**New functionality**
-
-* If -BuildUSBDrve $true, script will now check for USB drive before continuing. If not present, script exits
-* Added a number of new parameters.
-
-| Parameter | Type | Description |
-| -------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| CopyPEDrivers | Bool | When set to\$true, will copy the drivers from the \$FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is \$false. |
-| RemoveFFU | Bool | 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. |
-| UpdateLatestCU | Bool | When set to\$true, will download and install the latest cumulative update for Windows 10/11. Default is \$false. |
-| UpdateLatestNet | Bool | When set to\$true, will download and install the latest .NET Framework for Windows 10/11. Default is \$false. |
-| UpdateLatestDefender | Bool | When set to\$true, will download and install the latest Windows Defender definitions and Defender platform update. Default is \$false. |
-| UpdateEdge | Bool | When set to\$true, will download and install the latest Microsoft Edge for Windows 10/11. Default is \$false. |
-| UpdateOneDrive | Bool | 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. |
-| CopyPPKG | Bool | 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. |
-| CopyUnattend | Bool | When set to\$true, will copy the \$FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Default is \$false. |
-| CopyAutopilot | Bool | When set to\$true, will copy the \$FFUDevelopmentPath\Autopilot folder to the Deployment partition of the USB drive. Default is \$false. |
-| CompactOS | Bool | When set to\$true, will compact the OS when building the FFU. Default is \$true. |
-| CleanupCaptureISO | Bool | When set to\$true, will remove the WinPE capture ISO after the FFU has been captured. Default is \$true. |
-| CleanupDeployISO | Bool | When set to\$true, will remove the WinPE deployment ISO after the FFU has been captured. Default is \$true. |
-| CleanupAppsISO | Bool | When set to\$true, will remove the Apps ISO after the FFU has been captured. Default is \$true. |
-
-* Updated the docs with the new variables and made some minor modifications.
-* Changed version variable to 2402.1
-
-**2401.1**
-
-- Added -CopyDrivers boolean parameter to control the ability to copy drivers to the USB drive in the deploy partition drivers folder.
-- Changed version varaible to 2401.1
-- When creating the scratch VHDX, switched it to create a dynamic VHDX instead of fixed
-- Fixed an issue where adding drivers to the FFU would sometimes fail and would cause the script to exit unexpectedly
-- Added -optimize boolean parameter to control whether the FFU is optimized or not. This defaults to $true and in most cases should be left this way.
-- Fixed an issue where if the script failed to create the FFU and the old VM was left behind, it wouldn't clean it up if the VM was in the running state. Will now turn off any running VM with a name prefix of _FFU- and then remove any VMs with a name _FFU- if the environment is flagged as dirty.
-- Fixed an issue where devices that ship with UFS drives were unable to image due to the script setting a LogicalSectorSizeBytes value of 512. If you're creating a FFU for devices that have UFS drives, you'll need to set -LogicalSectorSizeBytes 4096.
-- There's a known issue where adding drivers to a FFU that has a LogicalSectorSizeBytes value of 4096. Added some code to prevent allowing this to happen. Please use -copydrivers $true as a workaround for now. We're investigating whether this is a bug or not.
-- Fixed an issue where VHDX only captures (i.e. where -installapps $false) would not install Windows updates.
-- Changed Office deployment to use Current channel instead of Monthly enterprise. If you want to change to Monthly Enterprise channel, it's recommended to leverage Intune.
-
-**2309.2**
-
-New Features
-
-**Multiple USB Drive Support**
-
-You can now plug in multiple USB drives (even using a USB hub) to create multiple USB drives for deployment. This is great for partners or customers who need to provide USB drives to their employees to image a large number of devices. It will copy the content to one USB drive at a time. The most USB drives we've seen created so far is 23 via a USB hub. Open an issue if you see any problems with this.
-
-**Robocopy support**
-
-Replaced Copy-Item with Robocopy when copying content to the USB drive(s). Copy-Item uses buffered IO, which can take a long time to copy large files. Robocopy with the /J switch allows for unbuffered IO support, which reduces the amount of time to copy.
-
-**Better error handling**
-
-Prior to 2309.2, if the script failed or you manually killed the script (ctrl+c, or closing the PowerShell window), the environment would end up in a bad state and you had to do a number of things to manually clean up the environment. Added a new function called Get-FFUEnvironment and a new text file called dirty.txt that gets created in the FFUDevelopment folder. When the script starts, it checks for the dirty.txt file and if it sees it, Get-FFUEnvironment runs and cleans out a number of things to help ensure the next run will complete successfully. Open an issue if you still see problems when the script fails and the next run of the script fails.Â
-
-Bug Fixes
-
-- In 2309.1, added a 15 second sleep to allow for the registry to unload to fix a Critical Process Died error on deployment. In this build, increased that to 60 seconds.
-- Fixed an issue where the script was incorrectly detecting the USB drive boot and deploy drive letters which caused issues when attempting to copy the WinPE files to the boot partition.
-
-**2309.1**
-
-- Fixed an issue with a Critical Process Died BSOD that would happen when using -installapps $false. More detailed information in the [commit](https://github.com/rbalsleyMSFT/FFU/pull/2/commits/34efbda7ec56dc7cb43ac42b058725d56c8b8899)
-
-**2306.1.2**
-
-- Fixed an issue where manually entering a name wouldn't name the computer as expected
-
-**2306.1.1**
-
-- Included some better error handling if defining optionalfeatures that require source folders (netfx3). ESD files don't have source folders like ISO media, which means installing .net 3.5 as an optional feature would fail. Also cleaned up some formatting.
-
-**2306.1**
-
-- Added support to automatically download the latest Windows 10 or 11 media via the media creation tool (thanks to [Michael](https://oofhours.com/2022/09/14/want-your-own-windows-11-21h2-arm64-isos/) for the idea). This also allows for different architecture, language, and media type support. If you omit the -ISOPath, the script will download the Windows 11 x64 English (US) consumer media.
-
- An example command to download Windows 11 Pro x64 English (US) consumer media with Office and install drivers (it won't download drivers, you'll put those in your c:\FFUDevelopment\Drivers folder)
-
- .\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
-
- An example command to download Windows 11 Pro x64 French (CA) consumer media with Office and install 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
-- Changed default size of System/EFI partition to 260MB from 256MB to accomodate 4Kn drives. 4Kn support needs more testing. I'm not confident yet that this can be done with VMs and FFUs.
-- Added versioning with a new version parameter. Using YYMM as the format followed by a point release.
-
# Getting Started
If you're not familiar with Github, you can click the Green code button above and select download zip. Extract the zip file and make sure to copy the FFUDevelopment folder to the root of your C: drive. That will make it easy to follow the guide and allow the scripts to work properly.
diff --git a/image-1.png b/image-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..85d21c0647f884691777726da91894ce39771c4d
GIT binary patch
literal 33610
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 d
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
PhfB*zD@#>Nybu0g4^T^U
literal 0
HcmV?d00001
diff --git a/image.png b/image.png
new file mode 100644
index 0000000000000000000000000000000000000000..509b6c80a81e356fac2182680ad3adaecc2284e1
GIT binary patch
literal 203504
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 |