mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Skips CU downloads when ESD version is current or newer
Extracts ESD metadata resolution into a separate function to enable version comparison before downloading cumulative updates. Parses Windows version from both ESD filenames and KB article search results to determine if the ESD already contains the latest updates, avoiding redundant downloads and installations. Improves VHDX cache matching by tracking update names that were skipped due to version matching, ensuring cached images are correctly reused when updates are already integrated in the base image. Adds check to skip downloading updates that already exist locally. Removes prior behavior of always removing the KB folder. The `$RemoveUpdates` parameter now controls whether the KB folder is removed or not. This change was made due to the size of the Windows 11 CU being > 3-4GB. This will reduce bandwidth, however will require setting `$RemoveUpdates` to true to cleanup old update files.
This commit is contained in:
+205
-40
@@ -1942,7 +1942,7 @@ function Get-ProductsCab {
|
||||
return $OutFile
|
||||
}
|
||||
|
||||
function Get-WindowsESD {
|
||||
function Get-WindowsESDMetadata {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet(10, 11)]
|
||||
@@ -1959,12 +1959,10 @@ function Get-WindowsESD {
|
||||
[ValidateSet('consumer', 'business')]
|
||||
[string]$MediaType
|
||||
)
|
||||
WriteLog "Downloading Windows $WindowsRelease ESD file"
|
||||
WriteLog "Windows Architecture: $WindowsArch"
|
||||
WriteLog "Windows Language: $WindowsLang"
|
||||
WriteLog "Windows Media Type: $MediaType"
|
||||
|
||||
WriteLog "Resolving Windows $WindowsRelease ESD metadata"
|
||||
$cabFilePath = Join-Path $PSScriptRoot "tempCabFile.cab"
|
||||
$xmlFilePath = Join-Path $PSScriptRoot "products.xml"
|
||||
$esdMetadata = $null
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
try {
|
||||
@@ -1995,46 +1993,101 @@ function Get-WindowsESD {
|
||||
else {
|
||||
throw "Downloading Windows $WindowsRelease is not supported. Please use the -ISOPath parameter to specify the path to the Windows $WindowsRelease ISO file."
|
||||
}
|
||||
WriteLog "Download succeeded"
|
||||
WriteLog "products.cab download succeeded"
|
||||
}
|
||||
finally {
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
}
|
||||
|
||||
# Extract XML from cab file
|
||||
WriteLog "Extracting Products XML from cab"
|
||||
$xmlFilePath = Join-Path $PSScriptRoot "products.xml"
|
||||
Invoke-Process Expand "-F:*.xml $cabFilePath $xmlFilePath" | Out-Null
|
||||
WriteLog "Products XML extracted"
|
||||
|
||||
# Load XML content
|
||||
[xml]$xmlContent = Get-Content -Path $xmlFilePath
|
||||
|
||||
# Define the client type to look for in the FilePath
|
||||
$clientType = if ($MediaType -eq 'consumer') { 'CLIENTCONSUMER' } else { 'CLIENTBUSINESS' }
|
||||
|
||||
# Find FilePath values based on WindowsArch, WindowsLang, and MediaType
|
||||
foreach ($file in $xmlContent.MCT.Catalogs.Catalog.PublishedMedia.Files.File) {
|
||||
if ($file.Architecture -eq $WindowsArch -and $file.LanguageCode -eq $WindowsLang -and $file.FilePath -like "*$clientType*") {
|
||||
$esdFilePath = Join-Path $PSScriptRoot (Split-Path $file.FilePath -Leaf)
|
||||
#Download if ESD file doesn't already exist
|
||||
If (-not (Test-Path $esdFilePath)) {
|
||||
WriteLog "Downloading $($file.filePath) to $esdFIlePath"
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
|
||||
Start-BitsTransferWithRetry -Source $file.FilePath -Destination $esdFilePath
|
||||
Clear-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
WriteLog "Download succeeded"
|
||||
WriteLog "Cleanup cab and xml file"
|
||||
Remove-Item -Path $cabFilePath -Force
|
||||
Remove-Item -Path $xmlFilePath -Force
|
||||
WriteLog "Cleanup done"
|
||||
$fileName = Split-Path $file.FilePath -Leaf
|
||||
$esdFilePath = Join-Path $PSScriptRoot $fileName
|
||||
$esdVersion = $null
|
||||
if ($file.FileName -match '^([0-9]+\.[0-9]+)') {
|
||||
$esdVersion = $matches[1]
|
||||
}
|
||||
return $esdFilePath
|
||||
|
||||
$esdMetadata = [pscustomobject]@{
|
||||
FileUrl = $file.FilePath
|
||||
FileName = $fileName
|
||||
LocalPath = $esdFilePath
|
||||
Version = $esdVersion
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($esdMetadata) {
|
||||
WriteLog "Resolved ESD metadata: $($esdMetadata.FileName) (Version: $($esdMetadata.Version))"
|
||||
}
|
||||
else {
|
||||
WriteLog "No matching ESD entry found in products.xml."
|
||||
}
|
||||
|
||||
WriteLog "Cleaning up temporary cab and xml files"
|
||||
Remove-Item -Path $cabFilePath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $xmlFilePath -Force -ErrorAction SilentlyContinue
|
||||
|
||||
return $esdMetadata
|
||||
}
|
||||
|
||||
function Get-WindowsESD {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet(10, 11)]
|
||||
[int]$WindowsRelease,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet('x86', 'x64', 'ARM64')]
|
||||
[string]$WindowsArch,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$WindowsLang,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet('consumer', 'business')]
|
||||
[string]$MediaType,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[pscustomobject]$Metadata
|
||||
)
|
||||
WriteLog "Downloading Windows $WindowsRelease ESD file"
|
||||
WriteLog "Windows Architecture: $WindowsArch"
|
||||
WriteLog "Windows Language: $WindowsLang"
|
||||
WriteLog "Windows Media Type: $MediaType"
|
||||
|
||||
$esdMetadata = $Metadata
|
||||
if (-not $esdMetadata) {
|
||||
$esdMetadata = Get-WindowsESDMetadata -WindowsRelease $WindowsRelease -WindowsArch $WindowsArch -WindowsLang $WindowsLang -MediaType $MediaType
|
||||
}
|
||||
if (-not $esdMetadata) {
|
||||
throw "Unable to resolve Windows ESD metadata."
|
||||
}
|
||||
|
||||
$esdFilePath = $esdMetadata.LocalPath
|
||||
if (-not (Test-Path $esdFilePath)) {
|
||||
WriteLog "Downloading $($esdMetadata.FileUrl) to $esdFilePath"
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
|
||||
Start-BitsTransferWithRetry -Source $esdMetadata.FileUrl -Destination $esdFilePath
|
||||
Clear-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
WriteLog "ESD download succeeded"
|
||||
}
|
||||
else {
|
||||
WriteLog "Found existing ESD at $esdFilePath, skipping download"
|
||||
}
|
||||
|
||||
return $esdFilePath
|
||||
}
|
||||
|
||||
function Get-ODTURL {
|
||||
@@ -2123,17 +2176,24 @@ function Get-KBLink {
|
||||
$results = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=$Name" -Headers $Headers -UserAgent $UserAgent
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
|
||||
# Extract the first KB article ID from the HTML content and store it globally
|
||||
# Extract the first KB article ID and Windows version (if present) from the HTML content and store globally
|
||||
# Edge and Defender do not have KB article IDs
|
||||
if ($Name -notmatch 'Defender|Edge') {
|
||||
if ($results.Content -match '>\s*([^\(<]+)\(KB(\d+)\)(?:\s*\([^)]+\))*\s*<') {
|
||||
$global:LastKBArticleID = $null
|
||||
$global:LastKBWindowsVersion = $null
|
||||
if ($results.Content -match '\(KB(\d+)\)[^(<]*\(([0-9]+\.[0-9]+)\)\s*<') {
|
||||
$kbArticleID = "KB$($matches[1])"
|
||||
$global:LastKBArticleID = $kbArticleID
|
||||
$global:LastKBWindowsVersion = $matches[2]
|
||||
WriteLog "Found KB article ID: $kbArticleID with Windows version $($matches[2])"
|
||||
}
|
||||
elseif ($results.Content -match '>\s*([^\(<]+)\(KB(\d+)\)(?:\s*\([^)]+\))*\s*<') {
|
||||
$kbArticleID = "KB$($matches[2])"
|
||||
$global:LastKBArticleID = $kbArticleID
|
||||
WriteLog "Found KB article ID: $kbArticleID"
|
||||
WriteLog "Found KB article ID: $kbArticleID (no Windows version found)"
|
||||
}
|
||||
else {
|
||||
WriteLog "No KB article ID found in search results."
|
||||
$global:LastKBArticleID = $null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5561,6 +5621,28 @@ try {
|
||||
$netUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$netFeatureUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$microcodeUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$cachedIncludedUpdateNames = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
$esdMetadata = $null
|
||||
$esdVersion = $null
|
||||
$cuKbWindowsVersion = $null
|
||||
$cupKbWindowsVersion = $null
|
||||
|
||||
if ($WindowsRelease -eq 11 -and -not $ISOPath) {
|
||||
try {
|
||||
$esdMetadata = Get-WindowsESDMetadata -WindowsRelease $WindowsRelease -WindowsArch $WindowsArch -WindowsLang $WindowsLang -MediaType $mediaType
|
||||
if ($esdMetadata -and $esdMetadata.Version) {
|
||||
$esdVersion = $esdMetadata.Version
|
||||
WriteLog "ESD version identified as $esdVersion"
|
||||
}
|
||||
elseif ($esdMetadata) {
|
||||
WriteLog "ESD metadata resolved but no version could be parsed from filename."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to resolve Windows ESD metadata: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($UpdateLatestCU -or $UpdatePreviewCU -or $UpdateLatestNet -or $UpdateLatestMicrocode) {
|
||||
# Determine required updates without downloading them yet
|
||||
@@ -5588,6 +5670,7 @@ try {
|
||||
WriteLog "Searching for $Name from Microsoft Update Catalog"
|
||||
(Get-UpdateFileInfo -Name $Name) | ForEach-Object { $cuUpdateInfos.Add($_) }
|
||||
$cuKbArticleId = $global:LastKBArticleID
|
||||
$cuKbWindowsVersion = $global:LastKBWindowsVersion
|
||||
}
|
||||
|
||||
if ($UpdatePreviewCU -and $installationType -eq 'Client' -and $WindowsSKU -notlike "*LTSC") {
|
||||
@@ -5596,6 +5679,7 @@ try {
|
||||
WriteLog "Searching for $Name from Microsoft Update Catalog"
|
||||
(Get-UpdateFileInfo -Name $Name) | ForEach-Object { $cupUpdateInfos.Add($_) }
|
||||
$cupKbArticleId = $global:LastKBArticleID
|
||||
$cupKbWindowsVersion = $global:LastKBWindowsVersion
|
||||
}
|
||||
|
||||
if ($UpdateLatestNet) {
|
||||
@@ -5639,6 +5723,46 @@ try {
|
||||
(Get-UpdateFileInfo -Name $name) | ForEach-Object { $microcodeUpdateInfos.Add($_) }
|
||||
}
|
||||
|
||||
$esdVerObj = $null
|
||||
$cuVerObj = $null
|
||||
$cupVerObj = $null
|
||||
if ($esdVersion) { try { $esdVerObj = [version]$esdVersion } catch { } }
|
||||
if ($cuKbWindowsVersion) { try { $cuVerObj = [version]$cuKbWindowsVersion } catch { } }
|
||||
if ($cupKbWindowsVersion) { try { $cupVerObj = [version]$cupKbWindowsVersion } catch { } }
|
||||
|
||||
if ($esdVerObj -and $cuVerObj) {
|
||||
if ($esdVerObj -eq $cuVerObj -or $esdVerObj -gt $cuVerObj) {
|
||||
$skipReason = if ($esdVerObj -eq $cuVerObj) { 'matches' } else { 'is newer than' }
|
||||
WriteLog "Windows 11 ESD version $esdVersion $skipReason CU version $cuKbWindowsVersion. Skipping CU download and installation."
|
||||
if ($AllowVHDXCaching -and $cuUpdateInfos -and $cuUpdateInfos.Count -gt 0) {
|
||||
foreach ($cuUpdateInfo in $cuUpdateInfos) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($cuUpdateInfo.Name) -and -not $cachedIncludedUpdateNames.Contains($cuUpdateInfo.Name)) {
|
||||
$cachedIncludedUpdateNames.Add($cuUpdateInfo.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
$cuUpdateInfos.Clear()
|
||||
$UpdateLatestCU = $false
|
||||
$CUPath = $null
|
||||
}
|
||||
}
|
||||
if ($esdVerObj -and $cupVerObj) {
|
||||
if ($esdVerObj -eq $cupVerObj -or $esdVerObj -gt $cupVerObj) {
|
||||
$skipReason = if ($esdVerObj -eq $cupVerObj) { 'matches' } else { 'is newer than' }
|
||||
WriteLog "Windows 11 ESD version $esdVersion $skipReason Preview CU version $cupKbWindowsVersion. Skipping Preview CU download and installation."
|
||||
if ($AllowVHDXCaching -and $cupUpdateInfos -and $cupUpdateInfos.Count -gt 0) {
|
||||
foreach ($cupUpdateInfo in $cupUpdateInfos) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($cupUpdateInfo.Name) -and -not $cachedIncludedUpdateNames.Contains($cupUpdateInfo.Name)) {
|
||||
$cachedIncludedUpdateNames.Add($cupUpdateInfo.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
$cupUpdateInfos.Clear()
|
||||
$UpdatePreviewCU = $false
|
||||
$CUPPath = $null
|
||||
}
|
||||
}
|
||||
|
||||
$requiredUpdates.AddRange($ssuUpdateInfos)
|
||||
$requiredUpdates.AddRange($cuUpdateInfos)
|
||||
$requiredUpdates.AddRange($cupUpdateInfos)
|
||||
@@ -5655,10 +5779,23 @@ try {
|
||||
$vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending)
|
||||
WriteLog "Found $($vhdxJsons.Count) cached VHDX config files"
|
||||
|
||||
# Extract file names from URLs for comparison
|
||||
# Build comparison list from update names and cached names
|
||||
$requiredUpdateFileNames = @()
|
||||
if ($requiredUpdates.Count -gt 0) {
|
||||
$requiredUpdateFileNames = @(($requiredUpdates.Url | ForEach-Object { ($_ -split '/')[-1] }) | Sort-Object)
|
||||
$requiredUpdateFileNames += $requiredUpdates | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_.Name)) {
|
||||
$_.Name
|
||||
}
|
||||
elseif (-not [string]::IsNullOrWhiteSpace($_.Url)) {
|
||||
($_.Url -split '/')[-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($cachedIncludedUpdateNames.Count -gt 0) {
|
||||
$requiredUpdateFileNames += $cachedIncludedUpdateNames
|
||||
}
|
||||
if ($requiredUpdateFileNames.Count -gt 0) {
|
||||
$requiredUpdateFileNames = @($requiredUpdateFileNames | Where-Object { $_ } | Sort-Object -Unique)
|
||||
}
|
||||
|
||||
foreach ($vhdxJson in $vhdxJsons) {
|
||||
@@ -5730,6 +5867,15 @@ try {
|
||||
if (-not (Test-Path -Path $destinationPath)) {
|
||||
New-Item -Path $destinationPath -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
# Skip download if expected file is already present
|
||||
$expectedFilePath = Join-Path -Path $destinationPath -ChildPath $update.Name
|
||||
|
||||
if (Test-Path -LiteralPath $expectedFilePath) {
|
||||
WriteLog "Update already exists at $expectedFilePath, skipping download"
|
||||
continue
|
||||
}
|
||||
|
||||
WriteLog "Downloading $($update.Name) to $destinationPath"
|
||||
Start-BitsTransferWithRetry -Source $update.Url -Destination $destinationPath
|
||||
}
|
||||
@@ -5784,7 +5930,7 @@ try {
|
||||
$wimPath = Get-WimFromISO
|
||||
}
|
||||
else {
|
||||
$wimPath = Get-WindowsESD -WindowsRelease $WindowsRelease -WindowsArch $WindowsArch -WindowsLang $WindowsLang -MediaType $mediaType
|
||||
$wimPath = Get-WindowsESD -WindowsRelease $WindowsRelease -WindowsArch $WindowsArch -WindowsLang $WindowsLang -MediaType $mediaType -Metadata $esdMetadata
|
||||
}
|
||||
#If index not specified by user, try and find based on WindowsSKU
|
||||
if (-not($index) -and ($WindowsSKU)) {
|
||||
@@ -5861,10 +6007,22 @@ try {
|
||||
WriteLog "KBs added to $WindowsPartition"
|
||||
if ($AllowVHDXCaching) {
|
||||
$cachedVHDXInfo = [VhdxCacheItem]::new()
|
||||
$includedUpdates = Get-ChildItem -Path $KBPath -File -Recurse
|
||||
|
||||
foreach ($includedUpdate in $includedUpdates) {
|
||||
$cachedVHDXInfo.IncludedUpdates += ([VhdxCacheUpdateItem]::new($includedUpdate.Name))
|
||||
# Record only updates from this build (current required updates and any cached names carried forward)
|
||||
$includedUpdateNames = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($includedUpdate in $requiredUpdates) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($includedUpdate.Name)) {
|
||||
$includedUpdateNames.Add($includedUpdate.Name)
|
||||
}
|
||||
}
|
||||
foreach ($cachedName in $cachedIncludedUpdateNames) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($cachedName)) {
|
||||
$includedUpdateNames.Add($cachedName)
|
||||
}
|
||||
}
|
||||
foreach ($includedName in ($includedUpdateNames | Sort-Object -Unique)) {
|
||||
if (-not ($cachedVHDXInfo.IncludedUpdates | Where-Object { $_.Name -eq $includedName })) {
|
||||
$cachedVHDXInfo.IncludedUpdates += ([VhdxCacheUpdateItem]::new($includedName))
|
||||
}
|
||||
}
|
||||
}
|
||||
WriteLog 'Clean Up the WinSxS Folder'
|
||||
@@ -5946,6 +6104,13 @@ try {
|
||||
if ($null -eq $cachedVHDXInfo) {
|
||||
$cachedVHDXInfo = [VhdxCacheItem]::new()
|
||||
}
|
||||
if ($AllowVHDXCaching -and $cachedIncludedUpdateNames.Count -gt 0) {
|
||||
foreach ($cachedName in $cachedIncludedUpdateNames) {
|
||||
if (-not ($cachedVHDXInfo.IncludedUpdates | Where-Object { $_.Name -eq $cachedName })) {
|
||||
$cachedVHDXInfo.IncludedUpdates += ([VhdxCacheUpdateItem]::new($cachedName))
|
||||
}
|
||||
}
|
||||
}
|
||||
$cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx")
|
||||
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
|
||||
$cachedVHDXInfo.WindowsSKU = $WindowsSKU
|
||||
|
||||
Reference in New Issue
Block a user