From 27eebeb9cbc6813c1a0ea72bb441a27661119f98 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:17:55 -0800 Subject: [PATCH] Adds OS-scoped update cache folders Prevents update packages for different Windows targets from mixing in a single cache, improving reuse across builds while avoiding DISM picking up stale MSUs. Keeps pruning, download destinations, and update discovery confined to the OS/version-specific cache, including shared-branch handling for Windows 11 25H2/24H2 and LTSC mappings. --- FFUDevelopment/BuildFFUVM.ps1 | 97 +++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index dfb50e6..fbcdbb9 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -6322,6 +6322,61 @@ try { WriteLog "Creating $KBPath"; New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } + # Create an OS/version-scoped KB cache folder under KB + # This allows caching across multiple Windows targets without mixing MSUs + $kbCacheReleaseFolder = $null + $kbCacheVersionFolder = $null + + if ($installationType -eq 'Server') { + $kbCacheReleaseFolder = "Server$WindowsRelease" + $kbCacheVersionFolder = $null + } + else { + # Client + if ($WindowsRelease -eq 10) { + $kbCacheReleaseFolder = 'Windows10' + $kbCacheVersionFolder = $WindowsVersion + } + elseif ($WindowsRelease -eq 11) { + $kbCacheReleaseFolder = 'Windows11' + $kbCacheVersionFolder = $WindowsVersion + } + elseif ($isLTSC -and $WindowsRelease -in 2016, 2019, 2021) { + $kbCacheReleaseFolder = 'Windows10' + $kbCacheVersionFolder = "LTSC$WindowsRelease" + } + elseif ($isLTSC -and $WindowsRelease -eq 2024) { + # Windows 11 LTSC 2024 shares the same CU branch as Windows 11 24H2/25H2 + $kbCacheReleaseFolder = 'Windows11' + $kbCacheVersionFolder = '24H2' + } + else { + $kbCacheReleaseFolder = "Windows$WindowsRelease" + $kbCacheVersionFolder = $WindowsVersion + } + } + + # Force Windows 11 25H2 to share Windows 11 24H2 cache folder (same CU branch) + if ($kbCacheReleaseFolder -eq 'Windows11' -and $kbCacheVersionFolder -match '(?i)^25H2$') { + $kbCacheVersionFolder = '24H2' + } + + # For Server, do not create a WindowsVersion subfolder (unnecessary for Server releases) + if ($installationType -eq 'Server') { + $kbCacheRootPath = Join-Path -Path $KBPath -ChildPath $kbCacheReleaseFolder + } + else { + if ([string]::IsNullOrWhiteSpace($kbCacheVersionFolder)) { + $kbCacheVersionFolder = 'UnknownVersion' + } + $kbCacheRootPath = Join-Path -Path (Join-Path -Path $KBPath -ChildPath $kbCacheReleaseFolder) -ChildPath $kbCacheVersionFolder + } + + if (-not (Test-Path -Path $kbCacheRootPath)) { + WriteLog "Creating KB cache folder $kbCacheRootPath" + New-Item -Path $kbCacheRootPath -ItemType Directory -Force | Out-Null + } + # Remove older MSU files for update types included in the current run # This avoids DISM considering multiple stale MSUs as local sources during servicing try { @@ -6362,10 +6417,10 @@ try { } } - # Prune older Windows update MSUs (CU/SSU/Preview CU) from KB root (exclude .NET "ndp" MSUs) + # Prune older Windows update MSUs (CU/SSU/Preview CU) from this OS cache root (exclude .NET "ndp" MSUs) if ($expectedWindowsMsuNames.Count -gt 0) { - WriteLog "Pruning older Windows update MSU files in $KBPath" - $existingWindowsMsus = @(Get-ChildItem -Path $KBPath -Filter '*.msu' -File -ErrorAction SilentlyContinue | Where-Object { + WriteLog "Pruning older Windows update MSU files in $kbCacheRootPath" + $existingWindowsMsus = @(Get-ChildItem -Path $kbCacheRootPath -Filter '*.msu' -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(?i)^windows\d+\.0-kb\d+.*\.msu$' -and $_.Name -notmatch '(?i)ndp' }) foreach ($msu in $existingWindowsMsus) { @@ -6376,10 +6431,10 @@ try { } } - # Prune older .NET MSUs from KB root (non-LTSC stores .NET MSUs in KB root) + # Prune older .NET MSUs from this OS cache root (non-LTSC stores .NET MSUs in the root) if ($expectedNetMsuNames.Count -gt 0 -and -not ($isLTSC -and $WindowsRelease -in 2016, 2019, 2021)) { - WriteLog "Pruning older .NET MSU files in $KBPath" - $existingNetMsus = @(Get-ChildItem -Path $KBPath -Filter '*.msu' -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(?i)ndp' }) + WriteLog "Pruning older .NET MSU files in $kbCacheRootPath" + $existingNetMsus = @(Get-ChildItem -Path $kbCacheRootPath -Filter '*.msu' -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(?i)ndp' }) foreach ($msu in $existingNetMsus) { if (-not $expectedNetMsuNames.Contains($msu.Name)) { WriteLog "Removing old .NET MSU: $($msu.FullName)" @@ -6388,9 +6443,9 @@ try { } } - # Prune older .NET MSUs from KB\NET (LTSC stores .NET updates under NET) + # Prune older .NET MSUs from NET folder (LTSC stores .NET updates under NET) if ($expectedNetMsuNames.Count -gt 0 -and ($isLTSC -and $WindowsRelease -in 2016, 2019, 2021)) { - $netFolder = Join-Path -Path $KBPath -ChildPath 'NET' + $netFolder = Join-Path -Path $kbCacheRootPath -ChildPath 'NET' if (Test-Path -Path $netFolder) { WriteLog "Pruning older .NET MSU files in $netFolder" $existingNetMsus = @(Get-ChildItem -Path $netFolder -Filter '*.msu' -File -ErrorAction SilentlyContinue) @@ -6403,9 +6458,9 @@ try { } } - # Prune older Microcode MSUs from KB\Microcode (only when Microcode is part of this run) + # Prune older Microcode MSUs from Microcode folder (only when Microcode is part of this run) if ($expectedMicrocodeMsuNames.Count -gt 0) { - $microcodeFolder = Join-Path -Path $KBPath -ChildPath 'Microcode' + $microcodeFolder = Join-Path -Path $kbCacheRootPath -ChildPath 'Microcode' if (Test-Path -Path $microcodeFolder) { WriteLog "Pruning older Microcode MSU files in $microcodeFolder" $existingMicrocodeMsus = @(Get-ChildItem -Path $microcodeFolder -Filter '*.msu' -File -ErrorAction SilentlyContinue) @@ -6419,19 +6474,19 @@ try { } } catch { - WriteLog "Failed to prune old MSU files in $($KBPath): $($_.Exception.Message). Continuing." + WriteLog "Failed to prune old MSU files in $($kbCacheRootPath): $($_.Exception.Message). Continuing." } foreach ($update in $requiredUpdates) { - $destinationPath = $KBPath + $destinationPath = $kbCacheRootPath if (($netUpdateInfos -and ($netUpdateInfos.Name -contains $update.Name)) -or ` ($netFeatureUpdateInfos -and ($netFeatureUpdateInfos.Name -contains $update.Name))) { if ($isLTSC -and $WindowsRelease -in 2016, 2019, 2021) { - $destinationPath = Join-Path -Path $KBPath -ChildPath "NET" + $destinationPath = Join-Path -Path $kbCacheRootPath -ChildPath "NET" } } if ($microcodeUpdateInfos -and ($microcodeUpdateInfos.Name -contains $update.Name)) { - $destinationPath = Join-Path -Path $KBPath -ChildPath "Microcode" + $destinationPath = Join-Path -Path $kbCacheRootPath -ChildPath "Microcode" } if (-not (Test-Path -Path $destinationPath)) { @@ -6454,33 +6509,33 @@ try { # Set file path variables for the patching process if ($ssuUpdateInfos.Count -gt 0) { $SSUFile = $ssuUpdateInfos[0].Name - $SSUFilePath = "$KBPath\$SSUFile" + $SSUFilePath = Join-Path -Path $kbCacheRootPath -ChildPath $SSUFile WriteLog "Latest SSU identified as $SSUFilePath" } if ($cuUpdateInfos.Count -gt 0) { if (-not $CUPath) { - $CUPath = (Get-ChildItem -Path $KBPath -Filter "*$cuKbArticleId*" -Recurse | Select-Object -First 1).FullName + $CUPath = (Get-ChildItem -Path $kbCacheRootPath -Filter "*$cuKbArticleId*" -Recurse | Select-Object -First 1).FullName } WriteLog "Latest CU identified as $CUPath" } if ($cupUpdateInfos.Count -gt 0) { if (-not $CUPPath) { - $CUPPath = (Get-ChildItem -Path $KBPath -Filter "*$cupKbArticleId*" -Recurse | Select-Object -First 1).FullName + $CUPPath = (Get-ChildItem -Path $kbCacheRootPath -Filter "*$cupKbArticleId*" -Recurse | Select-Object -First 1).FullName } WriteLog "Latest Preview CU identified as $CUPPath" } if ($netUpdateInfos.Count -gt 0 -or $netFeatureUpdateInfos.Count -gt 0) { if ($isLTSC -and $WindowsRelease -in 2016, 2019, 2021) { - $NETPath = Join-Path -Path $KBPath -ChildPath "NET" + $NETPath = Join-Path -Path $kbCacheRootPath -ChildPath "NET" WriteLog ".NET updates for LTSC are in $NETPath" } else { # Use the actual downloaded file name from the update info $NETFileName = $netUpdateInfos[0].Name - $NETPath = (Get-ChildItem -Path $KBPath -Filter $NETFileName -Recurse).FullName + $NETPath = (Get-ChildItem -Path $kbCacheRootPath -Filter $NETFileName -Recurse).FullName if (-not $NETPath) { # If exact match fails, try to find by KB article ID - $NETPath = (Get-ChildItem -Path $KBPath -Filter "*$netKbArticleId*" -Recurse | Select-Object -First 1).FullName + $NETPath = (Get-ChildItem -Path $kbCacheRootPath -Filter "*$netKbArticleId*" -Recurse | Select-Object -First 1).FullName if ($NETPath) { $NETFileName = Split-Path $NETPath -Leaf } @@ -6489,7 +6544,7 @@ try { } } if ($microcodeUpdateInfos.Count -gt 0) { - $MicrocodePath = "$KBPath\Microcode" + $MicrocodePath = Join-Path -Path $kbCacheRootPath -ChildPath "Microcode" WriteLog "Microcode updates are in $MicrocodePath" } }