From d349e5e4fb955947748f60b432061f27b2bb3c11 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:06:03 -0800 Subject: [PATCH] Prunes stale MSUs before servicing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes older update packages from the local update cache so servicing tools don’t pick up multiple stale MSUs as sources. Keeps only the MSUs expected for the current run across Windows, .NET (including LTSC folder layout), and Microcode updates, while logging failures and continuing. --- FFUDevelopment/BuildFFUVM.ps1 | 100 ++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 0634366..dfb50e6 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -6322,6 +6322,106 @@ try { WriteLog "Creating $KBPath"; New-Item -Path $KBPath -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 { + # Build allow-lists from update metadata for this run + $expectedWindowsMsuNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($updateInfo in $ssuUpdateInfos) { + if (-not [string]::IsNullOrWhiteSpace($updateInfo.Name) -and $updateInfo.Name.ToLowerInvariant().EndsWith('.msu')) { + [void]$expectedWindowsMsuNames.Add($updateInfo.Name) + } + } + foreach ($updateInfo in $cuUpdateInfos) { + if (-not [string]::IsNullOrWhiteSpace($updateInfo.Name) -and $updateInfo.Name.ToLowerInvariant().EndsWith('.msu')) { + [void]$expectedWindowsMsuNames.Add($updateInfo.Name) + } + } + foreach ($updateInfo in $cupUpdateInfos) { + if (-not [string]::IsNullOrWhiteSpace($updateInfo.Name) -and $updateInfo.Name.ToLowerInvariant().EndsWith('.msu')) { + [void]$expectedWindowsMsuNames.Add($updateInfo.Name) + } + } + + $expectedNetMsuNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($updateInfo in $netUpdateInfos) { + if (-not [string]::IsNullOrWhiteSpace($updateInfo.Name) -and $updateInfo.Name.ToLowerInvariant().EndsWith('.msu')) { + [void]$expectedNetMsuNames.Add($updateInfo.Name) + } + } + foreach ($updateInfo in $netFeatureUpdateInfos) { + if (-not [string]::IsNullOrWhiteSpace($updateInfo.Name) -and $updateInfo.Name.ToLowerInvariant().EndsWith('.msu')) { + [void]$expectedNetMsuNames.Add($updateInfo.Name) + } + } + + $expectedMicrocodeMsuNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($updateInfo in $microcodeUpdateInfos) { + if (-not [string]::IsNullOrWhiteSpace($updateInfo.Name) -and $updateInfo.Name.ToLowerInvariant().EndsWith('.msu')) { + [void]$expectedMicrocodeMsuNames.Add($updateInfo.Name) + } + } + + # Prune older Windows update MSUs (CU/SSU/Preview CU) from KB 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 { + $_.Name -match '(?i)^windows\d+\.0-kb\d+.*\.msu$' -and $_.Name -notmatch '(?i)ndp' + }) + foreach ($msu in $existingWindowsMsus) { + if (-not $expectedWindowsMsuNames.Contains($msu.Name)) { + WriteLog "Removing old Windows update MSU: $($msu.FullName)" + Remove-Item -LiteralPath $msu.FullName -Force -ErrorAction SilentlyContinue + } + } + } + + # Prune older .NET MSUs from KB root (non-LTSC stores .NET MSUs in KB 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' }) + foreach ($msu in $existingNetMsus) { + if (-not $expectedNetMsuNames.Contains($msu.Name)) { + WriteLog "Removing old .NET MSU: $($msu.FullName)" + Remove-Item -LiteralPath $msu.FullName -Force -ErrorAction SilentlyContinue + } + } + } + + # Prune older .NET MSUs from KB\NET (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' + if (Test-Path -Path $netFolder) { + WriteLog "Pruning older .NET MSU files in $netFolder" + $existingNetMsus = @(Get-ChildItem -Path $netFolder -Filter '*.msu' -File -ErrorAction SilentlyContinue) + foreach ($msu in $existingNetMsus) { + if (-not $expectedNetMsuNames.Contains($msu.Name)) { + WriteLog "Removing old .NET MSU: $($msu.FullName)" + Remove-Item -LiteralPath $msu.FullName -Force -ErrorAction SilentlyContinue + } + } + } + } + + # Prune older Microcode MSUs from KB\Microcode (only when Microcode is part of this run) + if ($expectedMicrocodeMsuNames.Count -gt 0) { + $microcodeFolder = Join-Path -Path $KBPath -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) + foreach ($msu in $existingMicrocodeMsus) { + if (-not $expectedMicrocodeMsuNames.Contains($msu.Name)) { + WriteLog "Removing old Microcode MSU: $($msu.FullName)" + Remove-Item -LiteralPath $msu.FullName -Force -ErrorAction SilentlyContinue + } + } + } + } + } + catch { + WriteLog "Failed to prune old MSU files in $($KBPath): $($_.Exception.Message). Continuing." + } + foreach ($update in $requiredUpdates) { $destinationPath = $KBPath if (($netUpdateInfos -and ($netUpdateInfos.Name -contains $update.Name)) -or `