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] 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*\|]', '_' + 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'