Fix: Windows 10 LTSB/LTSC Cumulative Update Installation

Since Windows 10 is out of support and only allows ESU updates, LTSB/LTSC builds are impacted by this and are unable to be offline serviced.

This commit fixes that by installing the CU in the VM, staging the update in a LTSCUpdate folder in the Apps folder.
This commit is contained in:
rbalsleyMSFT
2026-02-13 17:25:26 -08:00
parent 6e6abfe833
commit 42b0b0c350
5 changed files with 250 additions and 20 deletions
@@ -30,6 +30,7 @@ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
# Define the list of scripts to run # Define the list of scripts to run
$scriptList = @( $scriptList = @(
"Install-LTSCUpdate.ps1",
"Update-Defender.ps1", "Update-Defender.ps1",
"Install-Office.ps1", "Install-Office.ps1",
"Update-MSRT.ps1", "Update-MSRT.ps1",
+212 -19
View File
@@ -619,6 +619,11 @@ if (-not $InstallDefenderPath) { $InstallDefenderPath = "$OrchestrationPath\Upda
if (-not $InstallMSRTPath) { $InstallMSRTPath = "$OrchestrationPath\Update-MSRT.ps1" } if (-not $InstallMSRTPath) { $InstallMSRTPath = "$OrchestrationPath\Update-MSRT.ps1" }
if (-not $InstallODPath) { $InstallODPath = "$OrchestrationPath\Update-OneDrive.ps1" } if (-not $InstallODPath) { $InstallODPath = "$OrchestrationPath\Update-OneDrive.ps1" }
if (-not $InstallEdgePath) { $InstallEdgePath = "$OrchestrationPath\Update-Edge.ps1" } if (-not $InstallEdgePath) { $InstallEdgePath = "$OrchestrationPath\Update-Edge.ps1" }
# Set default LTSC CU in-VM orchestration paths
if (-not $InstallLTSCUpdatePath) { $InstallLTSCUpdatePath = "$OrchestrationPath\Install-LTSCUpdate.ps1" }
if (-not $LtscCUStagePath) { $LtscCUStagePath = "$AppsPath\LTSCUpdate" }
if (-not $AppsScriptVarsJsonPath) { $AppsScriptVarsJsonPath = "$OrchestrationPath\AppsScriptVariables.json" } if (-not $AppsScriptVarsJsonPath) { $AppsScriptVarsJsonPath = "$OrchestrationPath\AppsScriptVariables.json" }
if (-not $DeployISO) { $DeployISO = "$FFUDevelopmentPath\WinPE_FFU_Deploy_$WindowsArch.iso" } if (-not $DeployISO) { $DeployISO = "$FFUDevelopmentPath\WinPE_FFU_Deploy_$WindowsArch.iso" }
if (-not $CaptureISO) { $CaptureISO = "$FFUDevelopmentPath\WinPE_FFU_Capture_$WindowsArch.iso" } if (-not $CaptureISO) { $CaptureISO = "$FFUDevelopmentPath\WinPE_FFU_Capture_$WindowsArch.iso" }
@@ -666,6 +671,11 @@ if ($WindowsSKU -like "*LTS*") {
$isLTSC = $true $isLTSC = $true
} }
# Determine runtime LTSC CU handling flags
$isWindows10LtscClient = ($installationType -eq 'Client') -and ($WindowsRelease -in 2016, 2019, 2021) -and ($WindowsSKU -like '*LTS*')
$installLatestCuInVm = ($UpdateLatestCU -and $isWindows10LtscClient)
$refreshAppsIsoForLtscCu = $false
# Set the log path for the common logger # Set the log path for the common logger
Set-CommonCoreLogPath -Path $LogFile Set-CommonCoreLogPath -Path $LogFile
Set-BitsTransferPriority -Priority $BitsPriority Set-BitsTransferPriority -Priority $BitsPriority
@@ -4428,22 +4438,38 @@ Function Remove-DisabledArtifacts {
if ($removed) { WriteLog 'Removal complete' } if ($removed) { WriteLog 'Removal complete' }
} }
# Remove Edge artifacts if Edge update is disabled # Remove Edge artifacts if Edge update is disabled
if (-not $UpdateEdge) { if (-not $UpdateEdge) {
$removed = $false $removed = $false
if (Test-Path -Path $installEdgePath) { if (Test-Path -Path $installEdgePath) {
WriteLog "Update Edge disabled - removing $installEdgePath" WriteLog "Update Edge disabled - removing $installEdgePath"
Remove-Item -Path $installEdgePath -Force -ErrorAction SilentlyContinue Remove-Item -Path $installEdgePath -Force -ErrorAction SilentlyContinue
$removed = $true $removed = $true
}
if (Test-Path -Path $EdgePath) {
WriteLog "Update Edge disabled - removing $EdgePath"
Remove-Item -Path $EdgePath -Recurse -Force -ErrorAction SilentlyContinue
$removed = $true
}
if ($removed) { WriteLog 'Removal complete' }
} }
if (Test-Path -Path $EdgePath) {
WriteLog "Update Edge disabled - removing $EdgePath" # Remove LTSC CU in-VM artifacts when this scenario is not selected
Remove-Item -Path $EdgePath -Recurse -Force -ErrorAction SilentlyContinue if (-not ($UpdateLatestCU -and $installationType -eq 'Client' -and $WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like '*LTS*')) {
$removed = $true $removed = $false
if (Test-Path -Path $InstallLTSCUpdatePath) {
WriteLog "Windows 10 LTSB/LTSC latest CU in-VM install not selected - removing $InstallLTSCUpdatePath"
Remove-Item -Path $InstallLTSCUpdatePath -Force -ErrorAction SilentlyContinue
$removed = $true
}
if (Test-Path -Path $LtscCUStagePath) {
WriteLog "Windows 10 LTSB/LTSC latest CU in-VM install not selected - removing $LtscCUStagePath"
Remove-Item -Path $LtscCUStagePath -Recurse -Force -ErrorAction SilentlyContinue
$removed = $true
}
if ($removed) { WriteLog 'Removal complete' }
} }
if ($removed) { WriteLog 'Removal complete' }
} }
}
function Export-ConfigFile { function Export-ConfigFile {
[CmdletBinding()] [CmdletBinding()]
@@ -5252,9 +5278,9 @@ if (($LogicalSectorSizeBytes -eq 4096) -and ($installdrivers -eq $true)) {
if ($BuildUSBDrive -eq $true) { if ($BuildUSBDrive -eq $true) {
$USBDrives, $USBDrivesCount = Get-USBDrive $USBDrives, $USBDrivesCount = Get-USBDrive
} }
if (($InstallApps -eq $false) -and (($UpdateLatestDefender -eq $true) -or ($UpdateOneDrive -eq $true) -or ($UpdateEdge -eq $true) -or ($UpdateLatestMSRT -eq $true))) { if (($InstallApps -eq $false) -and (($UpdateLatestDefender -eq $true) -or ($UpdateOneDrive -eq $true) -or ($UpdateEdge -eq $true) -or ($UpdateLatestMSRT -eq $true) -or ($installLatestCuInVm -eq $true))) {
WriteLog 'You have selected to update Defender, Malicious Software Removal Tool, OneDrive, or Edge, however you are setting InstallApps to false. These updates require the InstallApps variable to be set to true. Please set InstallApps to true and try again.' WriteLog 'You have selected to update Defender, Malicious Software Removal Tool, OneDrive, Edge, or the latest Windows 10 LTSB/LTSC cumulative update, however you are setting InstallApps to false. These updates require the InstallApps variable to be set to true. Please set InstallApps to true and try again.'
throw "InstallApps variable must be set to `$true to update Defender, OneDrive, or Edge" throw "InstallApps variable must be set to `$true to update Defender, OneDrive, Edge, MSRT, or the latest Windows 10 LTSB/LTSC cumulative update"
} }
if (($WindowsArch -eq 'ARM64') -and ($InstallOffice -eq $true)) { if (($WindowsArch -eq 'ARM64') -and ($InstallOffice -eq $true)) {
$InstallOffice = $false $InstallOffice = $false
@@ -6610,9 +6636,14 @@ try {
} }
# Break out CU and NET updates to be added separately to abide by Checkpoint Update recommendations # Break out CU and NET updates to be added separately to abide by Checkpoint Update recommendations
if ($UpdateLatestCU) { if ($UpdateLatestCU) {
WriteLog "Adding $CUPath to $WindowsPartition" if ($installLatestCuInVm) {
Add-WindowsPackage -Path $WindowsPartition -PackagePath $CUPath | Out-Null WriteLog "Skipping offline CU install for Windows 10 LTSB/LTSC. CU will be installed in VM from Apps ISO."
WriteLog "$CUPath added to $WindowsPartition" }
else {
WriteLog "Adding $CUPath to $WindowsPartition"
Add-WindowsPackage -Path $WindowsPartition -PackagePath $CUPath | Out-Null
WriteLog "$CUPath added to $WindowsPartition"
}
} }
if ($UpdatePreviewCU) { if ($UpdatePreviewCU) {
WriteLog "Adding $CUPPath to $WindowsPartition" WriteLog "Adding $CUPPath to $WindowsPartition"
@@ -6775,6 +6806,149 @@ catch {
} }
# Prepare Windows 10 LTSB/LTSC CU assets for in-VM install when required
if ($InstallApps -and $installLatestCuInVm) {
try {
# Ensure CUPath is resolved and the CU package is available locally
if (([string]::IsNullOrWhiteSpace($CUPath)) -or -not (Test-Path -Path $CUPath)) {
$ltscKbCacheRootPath = Join-Path -Path (Join-Path -Path $KBPath -ChildPath 'Windows10') -ChildPath "LTSC$WindowsRelease"
if (-not (Test-Path -Path $ltscKbCacheRootPath)) {
WriteLog "Creating LTSC KB cache folder $ltscKbCacheRootPath"
New-Item -Path $ltscKbCacheRootPath -ItemType Directory -Force | Out-Null
}
foreach ($cuUpdateInfo in $cuUpdateInfos) {
$expectedCuName = $cuUpdateInfo.Name
if ([string]::IsNullOrWhiteSpace($expectedCuName)) {
continue
}
$expectedCuPath = Join-Path -Path $ltscKbCacheRootPath -ChildPath $expectedCuName
if (-not (Test-Path -Path $expectedCuPath)) {
WriteLog "Downloading $expectedCuName to $ltscKbCacheRootPath"
Start-BitsTransferWithRetry -Source $cuUpdateInfo.Url -Destination $ltscKbCacheRootPath
}
}
if (-not [string]::IsNullOrWhiteSpace($cuKbArticleId)) {
$resolvedCu = Get-ChildItem -Path $ltscKbCacheRootPath -Filter "*$cuKbArticleId*" -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -ne $resolvedCu) {
$CUPath = $resolvedCu.FullName
}
}
if (([string]::IsNullOrWhiteSpace($CUPath)) -and $cuUpdateInfos.Count -gt 0) {
$fallbackCuName = $cuUpdateInfos[0].Name
if (-not [string]::IsNullOrWhiteSpace($fallbackCuName)) {
$resolvedCu = Get-ChildItem -Path $ltscKbCacheRootPath -Filter $fallbackCuName -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -ne $resolvedCu) {
$CUPath = $resolvedCu.FullName
}
}
}
if (([string]::IsNullOrWhiteSpace($CUPath)) -or -not (Test-Path -Path $CUPath)) {
throw "Unable to resolve LTSC CU package path for in-VM install."
}
}
# Stage CU payload into Apps content so it is included in Apps ISO
if (-not (Test-Path -Path $LtscCUStagePath)) {
WriteLog "Creating LTSC CU staging folder $LtscCUStagePath"
New-Item -Path $LtscCUStagePath -ItemType Directory -Force | Out-Null
}
$ltscCuFileName = Split-Path -Path $CUPath -Leaf
$stagedLtscCuPath = Join-Path -Path $LtscCUStagePath -ChildPath $ltscCuFileName
Copy-Item -Path $CUPath -Destination $stagedLtscCuPath -Force | Out-Null
WriteLog "Staged LTSC CU package to $stagedLtscCuPath"
# Create Install-LTSCUpdate.ps1 for in-VM execution via orchestrator
$installLtscUpdateCommand = @"
# Validate LTSC CU package exists on Apps ISO mount
`$kbPath = "D:\LTSCUpdate\$ltscCuFileName"
# Extract KB ID from filename for idempotent checks
`$kbFileName = Split-Path -Path `$kbPath -Leaf
`$kbMatch = [regex]::Match(`$kbFileName, 'KB\d+', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
`$kbId = if (`$kbMatch.Success) { `$kbMatch.Value.ToUpperInvariant() } else { `$null }
# Detect whether Windows has a pending reboot
function Test-PendingReboot {
if (Test-Path -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') { return `$true }
if (Test-Path -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') { return `$true }
`$sessionManagerPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
try {
`$pendingRename = Get-ItemProperty -Path `$sessionManagerPath -Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue
if (`$null -ne `$pendingRename) { return `$true }
}
catch {
}
return `$false
}
# Skip if the target KB is already installed
if (`$kbId -and (Get-HotFix -Id `$kbId -ErrorAction SilentlyContinue)) {
Write-Host "`$kbId is already installed. Skipping LTSC CU install."
if (Test-PendingReboot) {
Write-Host "Pending reboot detected. Restarting before continuing orchestration..."
Restart-Computer -Force
}
return
}
# Stop with a non-zero code if the package is missing
if (-not (Test-Path -Path `$kbPath)) {
Write-Host "LTSC CU package not found at `$kbPath"
exit 1
}
# Start silent LTSC CU installation and capture exit code
Write-Host "Starting LTSC CU install from `$kbPath"
`$wusaArguments = @(`$kbPath, '/quiet', '/norestart')
`$wusaProcess = Start-Process -FilePath "wusa.exe" -ArgumentList `$wusaArguments -PassThru -Wait -ErrorAction Stop
`$wusaExitCode = `$wusaProcess.ExitCode
Write-Host "WUSA exit code: `$wusaExitCode"
# 3010 means reboot required; restart now so sysprep won't fail later
if (`$wusaExitCode -eq 3010) {
Write-Host "LTSC CU requires reboot. Restarting now..."
Restart-Computer -Force
return
}
# 0 = success, 2359302 = not applicable/already present
if (`$wusaExitCode -eq 0 -or `$wusaExitCode -eq 2359302) {
if (Test-PendingReboot) {
Write-Host "Pending reboot detected after LTSC CU step. Restarting now..."
Restart-Computer -Force
return
}
Write-Host "LTSC CU install completed."
return
}
throw "LTSC CU install failed with WUSA exit code `$wusaExitCode."
"@
Set-Content -Path $InstallLTSCUpdatePath -Value $installLtscUpdateCommand -Force
if (-not (Test-Path -Path $InstallLTSCUpdatePath)) {
throw "Failed to create $InstallLTSCUpdatePath"
}
WriteLog "$InstallLTSCUpdatePath created successfully"
$refreshAppsIsoForLtscCu = $true
WriteLog "Prepared Windows 10 LTSB/LTSC CU assets for in-VM installation"
}
catch {
WriteLog "Preparing Windows 10 LTSB/LTSC CU assets for in-VM installation failed with error $_"
throw $_
}
}
#Inject unattend after caching so cached VHDX never contains audit-mode unattend #Inject unattend after caching so cached VHDX never contains audit-mode unattend
if ($InstallApps) { if ($InstallApps) {
# Determine mount state and only mount if needed to avoid redundant mount/dismount cycles # Determine mount state and only mount if needed to avoid redundant mount/dismount cycles
@@ -6805,6 +6979,25 @@ if ($InstallApps) {
#If installing apps (Office or 3rd party), we need to build a VM and capture that FFU, if not, just cut the FFU from the VHDX file #If installing apps (Office or 3rd party), we need to build a VM and capture that FFU, if not, just cut the FFU from the VHDX file
if ($InstallApps) { if ($InstallApps) {
Set-Progress -Percentage 41 -Message "Starting VM for app installation..." Set-Progress -Percentage 41 -Message "Starting VM for app installation..."
# Refresh Apps ISO to include LTSC CU assets when staged for in-VM install
if ($refreshAppsIsoForLtscCu) {
try {
WriteLog "Refreshing Apps ISO to include LTSC CU assets"
if (Test-Path -Path $AppsISO) {
WriteLog "Removing $AppsISO"
Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue
WriteLog 'Removal complete'
}
New-AppsISO
WriteLog "Apps ISO refreshed with LTSC CU assets"
}
catch {
WriteLog "Refreshing Apps ISO for LTSC CU assets failed with error $_"
throw $_
}
}
#Create VM and attach VHDX #Create VM and attach VHDX
try { try {
WriteLog 'Creating new FFU VM' WriteLog 'Creating new FFU VM'
@@ -95,6 +95,15 @@ function Invoke-FFUPostBuildCleanup {
} }
} }
# Always remove LTSC update staging folder (out-of-band cleanup exception)
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath -PathType Container)) {
$ltscUpdateFolder = Join-Path $AppsPath 'LTSCUpdate'
if (Test-Path -LiteralPath $ltscUpdateFolder) {
WriteLog "CommonCleanup: Removing LTSC update staging folder $ltscUpdateFolder"
try { Remove-Item -LiteralPath $ltscUpdateFolder -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $ltscUpdateFolder : $($_.Exception.Message)" }
}
}
if ($RemoveUpdates) { if ($RemoveUpdates) {
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) { if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) {
# Remove per-run app update payloads stored under Apps # Remove per-run app update payloads stored under Apps
@@ -317,6 +317,9 @@ function Register-EventHandlers {
Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $localState.Controls.txtISOPath.Text -State $localState Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $localState.Controls.txtISOPath.Text -State $localState
Update-WindowsSkuCombo -State $localState Update-WindowsSkuCombo -State $localState
Update-WindowsArchCombo -State $localState Update-WindowsArchCombo -State $localState
# Re-evaluate Install Apps dependency when Windows release changes
Update-InstallAppsState -State $localState
}) })
$State.Controls.cmbWindowsVersion.Add_SelectionChanged({ $State.Controls.cmbWindowsVersion.Add_SelectionChanged({
@@ -369,6 +372,8 @@ function Register-EventHandlers {
$State.Controls.chkUpdateOneDrive.Add_Unchecked($updateCheckboxHandler) $State.Controls.chkUpdateOneDrive.Add_Unchecked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestMSRT.Add_Checked($updateCheckboxHandler) $State.Controls.chkUpdateLatestMSRT.Add_Checked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestMSRT.Add_Unchecked($updateCheckboxHandler) $State.Controls.chkUpdateLatestMSRT.Add_Unchecked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestCU.Add_Checked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestCU.Add_Unchecked($updateCheckboxHandler)
# Also attach the handler to the Office checkbox # Also attach the handler to the Office checkbox
$State.Controls.chkInstallOffice.Add_Checked($updateCheckboxHandler) $State.Controls.chkInstallOffice.Add_Checked($updateCheckboxHandler)
+23 -1
View File
@@ -320,6 +320,23 @@ function Update-ApplicationPanelVisibility {
} }
} }
# Function to identify whether current Windows release selection is Windows 10 LTSB/LTSC
function Test-IsWindows10LtscReleaseSelection {
param([PSCustomObject]$State)
$releaseItem = $State.Controls.cmbWindowsRelease.SelectedItem
if ($null -eq $releaseItem) {
return $false
}
$releaseDisplay = [string]$releaseItem.Display
if ([string]::IsNullOrWhiteSpace($releaseDisplay)) {
return $false
}
return (($releaseDisplay -like 'Windows 10*') -and (($releaseDisplay -like '*LTSB*') -or ($releaseDisplay -like '*LTSC*')))
}
# Function to manage the state of the main "Install Apps" checkbox based on selections in Updates/Office # Function to manage the state of the main "Install Apps" checkbox based on selections in Updates/Office
function Update-InstallAppsState { function Update-InstallAppsState {
param([PSCustomObject]$State) param([PSCustomObject]$State)
@@ -327,11 +344,16 @@ function Update-InstallAppsState {
$installAppsChk = $State.Controls.chkInstallApps $installAppsChk = $State.Controls.chkInstallApps
$installOfficeChk = $State.Controls.chkInstallOffice $installOfficeChk = $State.Controls.chkInstallOffice
# Determine if Windows 10 LTSB/LTSC + Update Latest CU is selected
$isWindows10LtscRelease = Test-IsWindows10LtscReleaseSelection -State $State
$isLtscCuChecked = $State.Controls.chkUpdateLatestCU.IsChecked -and $isWindows10LtscRelease
# Determine if any checkbox that forces "Install Apps" is checked # Determine if any checkbox that forces "Install Apps" is checked
$anyUpdateChecked = $State.Controls.chkUpdateLatestDefender.IsChecked -or ` $anyUpdateChecked = $State.Controls.chkUpdateLatestDefender.IsChecked -or `
$State.Controls.chkUpdateEdge.IsChecked -or ` $State.Controls.chkUpdateEdge.IsChecked -or `
$State.Controls.chkUpdateOneDrive.IsChecked -or ` $State.Controls.chkUpdateOneDrive.IsChecked -or `
$State.Controls.chkUpdateLatestMSRT.IsChecked $State.Controls.chkUpdateLatestMSRT.IsChecked -or `
$isLtscCuChecked
$isForced = $anyUpdateChecked -or $installOfficeChk.IsChecked $isForced = $anyUpdateChecked -or $installOfficeChk.IsChecked