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:
rbalsleyMSFT
2025-12-20 15:52:28 -08:00
parent 9737d5c930
commit 86d122aacf
+201 -36
View File
@@ -1942,7 +1942,7 @@ function Get-ProductsCab {
return $OutFile return $OutFile
} }
function Get-WindowsESD { function Get-WindowsESDMetadata {
param( param(
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[ValidateSet(10, 11)] [ValidateSet(10, 11)]
@@ -1959,12 +1959,10 @@ function Get-WindowsESD {
[ValidateSet('consumer', 'business')] [ValidateSet('consumer', 'business')]
[string]$MediaType [string]$MediaType
) )
WriteLog "Downloading Windows $WindowsRelease ESD file" WriteLog "Resolving Windows $WindowsRelease ESD metadata"
WriteLog "Windows Architecture: $WindowsArch"
WriteLog "Windows Language: $WindowsLang"
WriteLog "Windows Media Type: $MediaType"
$cabFilePath = Join-Path $PSScriptRoot "tempCabFile.cab" $cabFilePath = Join-Path $PSScriptRoot "tempCabFile.cab"
$xmlFilePath = Join-Path $PSScriptRoot "products.xml"
$esdMetadata = $null
$OriginalVerbosePreference = $VerbosePreference $OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue'
try { try {
@@ -1995,47 +1993,102 @@ function Get-WindowsESD {
else { else {
throw "Downloading Windows $WindowsRelease is not supported. Please use the -ISOPath parameter to specify the path to the Windows $WindowsRelease ISO file." 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 { finally {
$VerbosePreference = $OriginalVerbosePreference $VerbosePreference = $OriginalVerbosePreference
} }
# Extract XML from cab file
WriteLog "Extracting Products XML from cab" WriteLog "Extracting Products XML from cab"
$xmlFilePath = Join-Path $PSScriptRoot "products.xml"
Invoke-Process Expand "-F:*.xml $cabFilePath $xmlFilePath" | Out-Null Invoke-Process Expand "-F:*.xml $cabFilePath $xmlFilePath" | Out-Null
WriteLog "Products XML extracted" WriteLog "Products XML extracted"
# Load XML content
[xml]$xmlContent = Get-Content -Path $xmlFilePath [xml]$xmlContent = Get-Content -Path $xmlFilePath
# Define the client type to look for in the FilePath
$clientType = if ($MediaType -eq 'consumer') { 'CLIENTCONSUMER' } else { 'CLIENTBUSINESS' } $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) { 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*") { if ($file.Architecture -eq $WindowsArch -and $file.LanguageCode -eq $WindowsLang -and $file.FilePath -like "*$clientType*") {
$esdFilePath = Join-Path $PSScriptRoot (Split-Path $file.FilePath -Leaf) $fileName = Split-Path $file.FilePath -Leaf
#Download if ESD file doesn't already exist $esdFilePath = Join-Path $PSScriptRoot $fileName
If (-not (Test-Path $esdFilePath)) { $esdVersion = $null
WriteLog "Downloading $($file.filePath) to $esdFIlePath" if ($file.FileName -match '^([0-9]+\.[0-9]+)') {
$esdVersion = $matches[1]
}
$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 $OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue'
Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
Start-BitsTransferWithRetry -Source $file.FilePath -Destination $esdFilePath Start-BitsTransferWithRetry -Source $esdMetadata.FileUrl -Destination $esdFilePath
Clear-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath Clear-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
$VerbosePreference = $OriginalVerbosePreference $VerbosePreference = $OriginalVerbosePreference
WriteLog "Download succeeded" WriteLog "ESD download succeeded"
WriteLog "Cleanup cab and xml file"
Remove-Item -Path $cabFilePath -Force
Remove-Item -Path $xmlFilePath -Force
WriteLog "Cleanup done"
} }
else {
WriteLog "Found existing ESD at $esdFilePath, skipping download"
}
return $esdFilePath return $esdFilePath
} }
}
}
function Get-ODTURL { function Get-ODTURL {
try { try {
@@ -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 $results = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=$Name" -Headers $Headers -UserAgent $UserAgent
$VerbosePreference = $OriginalVerbosePreference $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 # Edge and Defender do not have KB article IDs
if ($Name -notmatch 'Defender|Edge') { 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])" $kbArticleID = "KB$($matches[2])"
$global:LastKBArticleID = $kbArticleID $global:LastKBArticleID = $kbArticleID
WriteLog "Found KB article ID: $kbArticleID" WriteLog "Found KB article ID: $kbArticleID (no Windows version found)"
} }
else { else {
WriteLog "No KB article ID found in search results." WriteLog "No KB article ID found in search results."
$global:LastKBArticleID = $null
} }
} }
@@ -5561,6 +5621,28 @@ try {
$netUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new() $netUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new()
$netFeatureUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new() $netFeatureUpdateInfos = [System.Collections.Generic.List[pscustomobject]]::new()
$microcodeUpdateInfos = [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) { if ($UpdateLatestCU -or $UpdatePreviewCU -or $UpdateLatestNet -or $UpdateLatestMicrocode) {
# Determine required updates without downloading them yet # Determine required updates without downloading them yet
@@ -5588,6 +5670,7 @@ try {
WriteLog "Searching for $Name from Microsoft Update Catalog" WriteLog "Searching for $Name from Microsoft Update Catalog"
(Get-UpdateFileInfo -Name $Name) | ForEach-Object { $cuUpdateInfos.Add($_) } (Get-UpdateFileInfo -Name $Name) | ForEach-Object { $cuUpdateInfos.Add($_) }
$cuKbArticleId = $global:LastKBArticleID $cuKbArticleId = $global:LastKBArticleID
$cuKbWindowsVersion = $global:LastKBWindowsVersion
} }
if ($UpdatePreviewCU -and $installationType -eq 'Client' -and $WindowsSKU -notlike "*LTSC") { if ($UpdatePreviewCU -and $installationType -eq 'Client' -and $WindowsSKU -notlike "*LTSC") {
@@ -5596,6 +5679,7 @@ try {
WriteLog "Searching for $Name from Microsoft Update Catalog" WriteLog "Searching for $Name from Microsoft Update Catalog"
(Get-UpdateFileInfo -Name $Name) | ForEach-Object { $cupUpdateInfos.Add($_) } (Get-UpdateFileInfo -Name $Name) | ForEach-Object { $cupUpdateInfos.Add($_) }
$cupKbArticleId = $global:LastKBArticleID $cupKbArticleId = $global:LastKBArticleID
$cupKbWindowsVersion = $global:LastKBWindowsVersion
} }
if ($UpdateLatestNet) { if ($UpdateLatestNet) {
@@ -5639,6 +5723,46 @@ try {
(Get-UpdateFileInfo -Name $name) | ForEach-Object { $microcodeUpdateInfos.Add($_) } (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($ssuUpdateInfos)
$requiredUpdates.AddRange($cuUpdateInfos) $requiredUpdates.AddRange($cuUpdateInfos)
$requiredUpdates.AddRange($cupUpdateInfos) $requiredUpdates.AddRange($cupUpdateInfos)
@@ -5655,10 +5779,23 @@ try {
$vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending) $vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending)
WriteLog "Found $($vhdxJsons.Count) cached VHDX config files" 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 = @() $requiredUpdateFileNames = @()
if ($requiredUpdates.Count -gt 0) { 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) { foreach ($vhdxJson in $vhdxJsons) {
@@ -5730,6 +5867,15 @@ try {
if (-not (Test-Path -Path $destinationPath)) { if (-not (Test-Path -Path $destinationPath)) {
New-Item -Path $destinationPath -ItemType Directory -Force | Out-Null 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" WriteLog "Downloading $($update.Name) to $destinationPath"
Start-BitsTransferWithRetry -Source $update.Url -Destination $destinationPath Start-BitsTransferWithRetry -Source $update.Url -Destination $destinationPath
} }
@@ -5784,7 +5930,7 @@ try {
$wimPath = Get-WimFromISO $wimPath = Get-WimFromISO
} }
else { 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 index not specified by user, try and find based on WindowsSKU
if (-not($index) -and ($WindowsSKU)) { if (-not($index) -and ($WindowsSKU)) {
@@ -5861,10 +6007,22 @@ try {
WriteLog "KBs added to $WindowsPartition" WriteLog "KBs added to $WindowsPartition"
if ($AllowVHDXCaching) { if ($AllowVHDXCaching) {
$cachedVHDXInfo = [VhdxCacheItem]::new() $cachedVHDXInfo = [VhdxCacheItem]::new()
$includedUpdates = Get-ChildItem -Path $KBPath -File -Recurse # 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 $includedUpdates) { foreach ($includedUpdate in $requiredUpdates) {
$cachedVHDXInfo.IncludedUpdates += ([VhdxCacheUpdateItem]::new($includedUpdate.Name)) 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' WriteLog 'Clean Up the WinSxS Folder'
@@ -5946,6 +6104,13 @@ try {
if ($null -eq $cachedVHDXInfo) { if ($null -eq $cachedVHDXInfo) {
$cachedVHDXInfo = [VhdxCacheItem]::new() $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.VhdxFileName = $("$VMName.vhdx")
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes $cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
$cachedVHDXInfo.WindowsSKU = $WindowsSKU $cachedVHDXInfo.WindowsSKU = $WindowsSKU