Refresh Windows SKU dynamically after fallback image selection

Previously, when a requested Windows SKU was not found in the provided ISO/ESD and the user manually selected a fallback image, the script kept the original (stale) `$WindowsSKU`. This caused downstream features like FFU file naming, VHDX cache metadata, and cumulative update planning to enforce logic against the wrong edition.

- Refactored `Get-Index` into `Get-WindowsImageSelection` to return rich image metadata (including EditionId and InstallationType) instead of just the image index.
- Added `Get-ResolvedWindowsSKUFromImage` to resolve raw image metadata back into the repository's native friendly SKU vocabulary.
- Added `Get-WindowsTargetRuntimeState` to centralize and recalculate dependent variables (`installationType`, `WindowsVersion`, LTSC flags) after the SKU updates mid-flight.
This commit is contained in:
rbalsleyMSFT
2026-03-30 12:57:15 -07:00
parent c135ad0fba
commit 5aaa1ad732
+178 -49
View File
@@ -587,6 +587,55 @@ public static extern uint GetPrivateProfileSection(
'@ '@
Add-Type -MemberDefinition $definition -Namespace Win32 -Name Kernel32 -PassThru | Out-Null Add-Type -MemberDefinition $definition -Namespace Win32 -Name Kernel32 -PassThru | Out-Null
function Get-WindowsTargetRuntimeState {
param(
[Parameter(Mandatory = $true)]
[int]$WindowsRelease,
[Parameter(Mandatory = $true)]
[string]$WindowsSKU,
[Parameter(Mandatory = $true)]
[string]$CurrentWindowsVersion,
[Parameter(Mandatory = $true)]
[bool]$UpdateLatestCU
)
$localInstallationType = if ($WindowsSKU -like 'Standard*' -or $WindowsSKU -like 'Datacenter*') { 'Server' } else { 'Client' }
$localWindowsVersion = $CurrentWindowsVersion
$localIsLTSC = $false
if ($localInstallationType -eq 'Server') {
switch ($WindowsRelease) {
2016 { $localWindowsVersion = '1607' }
2019 { $localWindowsVersion = '1809' }
2022 { $localWindowsVersion = '21H2' }
2025 { $localWindowsVersion = '24H2' }
}
}
if ($WindowsSKU -like '*LTS*') {
switch ($WindowsRelease) {
2016 { $localWindowsVersion = '1607' }
2019 { $localWindowsVersion = '1809' }
2021 { $localWindowsVersion = '21H2' }
2024 { $localWindowsVersion = '24H2' }
}
$localIsLTSC = $true
}
$localIsWindows10LtscClient = ($localInstallationType -eq 'Client') -and ($WindowsRelease -in 2016, 2019, 2021) -and $localIsLTSC
return [pscustomobject]@{
InstallationType = $localInstallationType
WindowsVersion = $localWindowsVersion
IsLTSC = $localIsLTSC
IsWindows10LtscClient = $localIsWindows10LtscClient
InstallLatestCuInVm = ($UpdateLatestCU -and $localIsWindows10LtscClient)
}
}
#Check if Hyper-V feature is installed (requires only checks the module) #Check if Hyper-V feature is installed (requires only checks the module)
$osInfo = Get-CimInstance -ClassName win32_OperatingSystem $osInfo = Get-CimInstance -ClassName win32_OperatingSystem
$isServer = $osInfo.Caption -match 'server' $isServer = $osInfo.Caption -match 'server'
@@ -648,31 +697,13 @@ if (-not $UnattendFolder) { $UnattendFolder = "$FFUDevelopmentPath\Unattend" }
if (-not $AutopilotFolder) { $AutopilotFolder = "$FFUDevelopmentPath\Autopilot" } if (-not $AutopilotFolder) { $AutopilotFolder = "$FFUDevelopmentPath\Autopilot" }
if (-not $PEDriversFolder) { $PEDriversFolder = "$FFUDevelopmentPath\PEDrivers" } if (-not $PEDriversFolder) { $PEDriversFolder = "$FFUDevelopmentPath\PEDrivers" }
if (-not $VHDXCacheFolder) { $VHDXCacheFolder = "$FFUDevelopmentPath\VHDXCache" } if (-not $VHDXCacheFolder) { $VHDXCacheFolder = "$FFUDevelopmentPath\VHDXCache" }
if (-not $installationType) { $installationType = if ($WindowsSKU -like "Standard*" -or $WindowsSKU -like "Datacenter*") { 'Server' } else { 'Client' } }
if ($installationType -eq 'Server') {
#Map $WindowsRelease to $WindowsVersion for Windows Server
switch ($WindowsRelease) {
2016 { $WindowsVersion = '1607' }
2019 { $WindowsVersion = '1809' }
2022 { $WindowsVersion = '21H2' }
2025 { $WindowsVersion = '24H2' }
}
}
if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" } if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" }
$windowsTargetRuntimeState = Get-WindowsTargetRuntimeState -WindowsRelease $WindowsRelease -WindowsSKU $WindowsSKU -CurrentWindowsVersion $WindowsVersion -UpdateLatestCU:$UpdateLatestCU
if ($WindowsSKU -like "*LTS*") { $installationType = $windowsTargetRuntimeState.InstallationType
switch ($WindowsRelease) { $WindowsVersion = $windowsTargetRuntimeState.WindowsVersion
2016 { $WindowsVersion = '1607' } $isLTSC = $windowsTargetRuntimeState.IsLTSC
2019 { $WindowsVersion = '1809' } $isWindows10LtscClient = $windowsTargetRuntimeState.IsWindows10LtscClient
2021 { $WindowsVersion = '21H2' } $installLatestCuInVm = $windowsTargetRuntimeState.InstallLatestCuInVm
2024 { $WindowsVersion = '24H2' }
}
$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 $refreshAppsIsoForLtscCu = $false
# Set the log path for the common logger # Set the log path for the common logger
@@ -2423,13 +2454,76 @@ function Get-WimFromISO {
return $wimPath return $wimPath
} }
function Get-Index { function Get-ResolvedWindowsSKUFromImage {
param(
[Parameter(Mandatory = $true)]
[string]$EditionId,
[string]$InstallationType,
[string]$ImageName,
[Parameter(Mandatory = $true)]
[int]$WindowsRelease
)
$normalizedInstallationType = if ([string]::IsNullOrWhiteSpace($InstallationType)) { '' } else { $InstallationType.Trim() }
switch ($EditionId) {
'Core' { return 'Home' }
'CoreN' { return 'Home N' }
'CoreSingleLanguage' { return 'Home Single Language' }
'Education' { return 'Education' }
'EducationN' { return 'Education N' }
'Professional' { return 'Pro' }
'ProfessionalN' { return 'Pro N' }
'ProfessionalEducation' { return 'Pro Education' }
'ProfessionalEducationN' { return 'Pro Education N' }
'ProfessionalWorkstation' { return 'Pro for Workstations' }
'ProfessionalWorkstationN' { return 'Pro N for Workstations' }
'Enterprise' { return 'Enterprise' }
'EnterpriseN' { return 'Enterprise N' }
'EnterpriseS' {
if ($WindowsRelease -eq 2016 -or $ImageName -match 'LTSB') {
return 'Enterprise 2016 LTSB'
}
return 'Enterprise LTSC'
}
'EnterpriseSN' {
if ($WindowsRelease -eq 2016 -or $ImageName -match 'LTSB') {
return 'Enterprise N 2016 LTSB'
}
return 'Enterprise N LTSC'
}
'IoTEnterpriseS' { return 'IoT Enterprise LTSC' }
'IoTEnterpriseSN' { return 'IoT Enterprise N LTSC' }
'ServerStandard' {
if ($normalizedInstallationType -eq 'Server') {
return 'Standard (Desktop Experience)'
}
return 'Standard'
}
'ServerDatacenter' {
if ($normalizedInstallationType -eq 'Server') {
return 'Datacenter (Desktop Experience)'
}
return 'Datacenter'
}
}
return $null
}
function Get-WindowsImageSelection {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$WindowsImagePath, [string]$WindowsImagePath,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$WindowsSKU [string]$WindowsSKU,
[Parameter(Mandatory = $true)]
[int]$WindowsRelease
) )
# Get the available indexes in the WIM/ESD # Get the available indexes in the WIM/ESD
@@ -2514,25 +2608,26 @@ function Get-Index {
} }
} }
# Build per-index metadata (EditionId, InstallationType, resolved SKU) once for deterministic matching and fallback prompts.
$imageMetadata = @(foreach ($imageIndex in $imageIndexes) {
try {
$details = Get-WindowsImage -ImagePath $WindowsImagePath -Index $imageIndex.ImageIndex
[pscustomobject]@{
ImageIndex = $details.ImageIndex
ImageName = $details.ImageName
ImageSize = $details.ImageSize
EditionId = $details.EditionId
InstallationType = $details.InstallationType
ResolvedWindowsSKU = Get-ResolvedWindowsSKUFromImage -EditionId $details.EditionId -InstallationType $details.InstallationType -ImageName $details.ImageName -WindowsRelease $WindowsRelease
}
}
catch {
$null
}
}) | Where-Object { $null -ne $_ }
# If we can map SKU -> EditionId, attempt a non-interactive match # If we can map SKU -> EditionId, attempt a non-interactive match
if ($editionIdCandidates.Count -gt 0) { if ($editionIdCandidates.Count -gt 0) {
# Build per-index metadata (EditionId, InstallationType) to match deterministically
$imageMetadata = @(foreach ($imageIndex in $imageIndexes) {
try {
$details = Get-WindowsImage -ImagePath $WindowsImagePath -Index $imageIndex.ImageIndex
[pscustomobject]@{
ImageIndex = $details.ImageIndex
ImageName = $details.ImageName
ImageSize = $details.ImageSize
EditionId = $details.EditionId
InstallationType = $details.InstallationType
}
}
catch {
$null
}
}) | Where-Object { $null -ne $_ }
# Match by EditionId first # Match by EditionId first
$imageMatches = $imageMetadata | Where-Object { $_.EditionId -in $editionIdCandidates } $imageMatches = $imageMetadata | Where-Object { $_.EditionId -in $editionIdCandidates }
@@ -2547,14 +2642,17 @@ function Get-Index {
# If multiple matches remain, pick the largest image (Desktop Experience tends to be larger) # If multiple matches remain, pick the largest image (Desktop Experience tends to be larger)
if ($imageMatches.Count -gt 0) { if ($imageMatches.Count -gt 0) {
$bestMatch = $imageMatches | Sort-Object -Property ImageSize -Descending | Select-Object -First 1 $bestMatch = $imageMatches | Sort-Object -Property ImageSize -Descending | Select-Object -First 1
WriteLog "Selected Windows image index $($bestMatch.ImageIndex) (SKU='$WindowsSKU', EditionId='$($bestMatch.EditionId)', InstallationType='$($bestMatch.InstallationType)'): $($bestMatch.ImageName)" WriteLog "Selected Windows image index $($bestMatch.ImageIndex) (RequestedSKU='$WindowsSKU', ResolvedSKU='$($bestMatch.ResolvedWindowsSKU)', EditionId='$($bestMatch.EditionId)', InstallationType='$($bestMatch.InstallationType)'): $($bestMatch.ImageName)"
return $bestMatch.ImageIndex return $bestMatch
} }
} }
# Final fallback: prompt the user to select an ImageName # Final fallback: prompt the user to select an ImageName
# Look for the numbers 10, 11, 2016, 2019, 2022+ in the ImageName # Look for the numbers 10, 11, 2016, 2019, 2022+ in the ImageName
$relevantImageIndexes = $imageIndexes | Where-Object { ($_.ImageName -match "(10|11|2016|2019|202\d)") } $relevantImageIndexes = @($imageMetadata | Where-Object { $_.ImageName -match "(10|11|2016|2019|202\d)" })
if ($relevantImageIndexes.Count -eq 0) {
$relevantImageIndexes = $imageMetadata
}
WriteLog "No matching image index found for SKU '$WindowsSKU' in '$WindowsImagePath'. Prompting user to select an ImageName." WriteLog "No matching image index found for SKU '$WindowsSKU' in '$WindowsImagePath'. Prompting user to select an ImageName."
@@ -2575,8 +2673,8 @@ function Get-Index {
$selectedImage = $relevantImageIndexes[$inputValue - 1] $selectedImage = $relevantImageIndexes[$inputValue - 1]
if ($selectedImage) { if ($selectedImage) {
WriteLog "User selected Windows image index $($selectedImage.ImageIndex) (SKU='$WindowsSKU'): $($selectedImage.ImageName)" WriteLog "User selected Windows image index $($selectedImage.ImageIndex) (RequestedSKU='$WindowsSKU', ResolvedSKU='$($selectedImage.ResolvedWindowsSKU)'): $($selectedImage.ImageName)"
return $selectedImage.ImageIndex return $selectedImage
} }
else { else {
Write-Host "Invalid selection, please try again." Write-Host "Invalid selection, please try again."
@@ -6844,7 +6942,38 @@ try {
} }
#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)) {
$index = Get-Index -WindowsImagePath $wimPath -WindowsSKU $WindowsSKU $requestedWindowsSKU = $WindowsSKU
$previousInstallationType = $installationType
$previousWindowsVersion = $WindowsVersion
$previousIsLTSC = [bool]$isLTSC
$windowsImageSelection = Get-WindowsImageSelection -WindowsImagePath $wimPath -WindowsSKU $WindowsSKU -WindowsRelease $WindowsRelease
$index = $windowsImageSelection.ImageIndex
if (-not [string]::IsNullOrWhiteSpace($windowsImageSelection.ResolvedWindowsSKU)) {
$WindowsSKU = $windowsImageSelection.ResolvedWindowsSKU
$windowsTargetRuntimeState = Get-WindowsTargetRuntimeState -WindowsRelease $WindowsRelease -WindowsSKU $WindowsSKU -CurrentWindowsVersion $WindowsVersion -UpdateLatestCU:$UpdateLatestCU
$installationType = $windowsTargetRuntimeState.InstallationType
$WindowsVersion = $windowsTargetRuntimeState.WindowsVersion
$isLTSC = $windowsTargetRuntimeState.IsLTSC
$isWindows10LtscClient = $windowsTargetRuntimeState.IsWindows10LtscClient
$installLatestCuInVm = $windowsTargetRuntimeState.InstallLatestCuInVm
if ($requestedWindowsSKU -ne $WindowsSKU) {
WriteLog "Resolved WindowsSKU from '$requestedWindowsSKU' to '$WindowsSKU' based on image selection '$($windowsImageSelection.ImageName)'."
}
if (($previousInstallationType -ne $installationType) -or ($previousWindowsVersion -ne $WindowsVersion) -or ($previousIsLTSC -ne [bool]$isLTSC)) {
WriteLog "Updated Windows target state after image selection: InstallationType='$installationType', WindowsVersion='$WindowsVersion', IsLTSC='$isLTSC'."
}
if (($InstallApps -eq $false) -and ($installLatestCuInVm -eq $true)) {
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, Edge, MSRT, or the latest Windows 10 LTSB/LTSC cumulative update"
}
}
else {
WriteLog "Could not resolve a friendly WindowsSKU for selected image '$($windowsImageSelection.ImageName)'. Continuing with requested SKU '$WindowsSKU'."
}
} }
$vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes $vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes