feat: Simplify WIM platform and version detection (#2108)

This commit is contained in:
Kroese
2026-08-08 03:53:42 +02:00
committed by GitHub
parent 0b76ed9744
commit 4ae04facfa
2 changed files with 193 additions and 396 deletions
+16 -71
View File
@@ -762,27 +762,23 @@ normalizeServerEdition() {
edition="${edition#r2-}"
case "$edition" in
"core" | "core-installation" | "server-core-installation" )
"core" | "server-core" | "servercore" | \
"core-installation" | "server-core-installation" )
edition="standard-core" ;;
"desktop-experience" | "server-with-a-gui" | "full-installation" )
edition="standard" ;;
*"-server-core-installation" )
edition="${edition%-server-core-installation}-core" ;;
*"-core-installation" )
*"-server-core-installation" | *"-core-installation" )
edition="${edition%-server-core-installation}"
edition="${edition%-core-installation}-core" ;;
*"-desktop-experience" )
edition="${edition%-desktop-experience}" ;;
*"-server-with-a-gui" )
edition="${edition%-server-with-a-gui}" ;;
*"-full-installation" )
*"-desktop-experience" | *"-server-with-a-gui" | *"-full-installation" )
edition="${edition%-desktop-experience}"
edition="${edition%-server-with-a-gui}"
edition="${edition%-full-installation}" ;;
esac
edition="${edition#server-}"
edition="${edition#server}"
[ "$edition" == "core" ] && edition="standard-core"
echo "$edition"
return 0
}
@@ -794,24 +790,13 @@ normalizeServerEditionID() {
edition=$(normalizeServerEdition "$1") || return 1
case "$edition" in
"" | "standard" | "serverstandard" ) edition="" ;;
"core" | "standard-core" | "standardcore" | "serverstandardcore" ) edition="standard-core" ;;
"datacenter" | "serverdatacenter" ) edition="datacenter" ;;
"datacenter-core" | "datacentercore" | "serverdatacentercore" ) edition="datacenter-core" ;;
"datacenter-azure" | "datacenter-azure-edition" | "datacenterazureedition" | \
"serverdatacenterazureedition" | "serverturbine" ) edition="datacenter-azure" ;;
"datacenter-azure-core" | "datacenter-azure-edition-core" | \
"datacenterazureeditioncore" | "serverdatacenterazureeditioncore" | \
"serverturbinecore" ) edition="datacenter-azure-core" ;;
"enterprise" | "serverenterprise" ) edition="enterprise" ;;
"enterprise-core" | "enterprisecore" | "serverenterprisecore" ) edition="enterprise-core" ;;
"web" | "serverweb" ) edition="web" ;;
"web-core" | "webcore" | "serverwebcore" ) edition="web-core" ;;
"foundation" | "serverfoundation" ) edition="foundation" ;;
"essentials" | "serveressentials" ) edition="essentials" ;;
# Keep unrecognized internal edition IDs deterministic and unique.
# Known aliases above only provide stable, friendlier public names.
* ) : ;;
"standard" ) edition="" ;;
"standardcore" | "datacentercore" | "enterprisecore" | "webcore" )
edition="${edition%core}-core" ;;
"datacenter-azure-edition" | "datacenterazureedition" )
edition="datacenter-azure" ;;
"datacenter-azure-edition-core" | "datacenterazureeditioncore" )
edition="datacenter-azure-core" ;;
esac
echo "$edition"
@@ -842,45 +827,6 @@ getServerEditionID() {
return 0
}
getEditionOrder() {
local id="${1,,}"
case "$id" in
"win20"* )
printf '%s\n' \
"|default|@default" \
"-datacenter|datacenter|datacenter datacenter-*" \
"-datacenter-azure|datacenter|datacenter-azure" \
"-enterprise|enterprise|enterprise enterprise-*" \
"-web|web|web web-*" \
"-foundation|foundation|foundation foundation-*" \
"-essentials|essentials|essentials essentials-*" \
"-standard-core|standard-core|standard-core standard-core-*" \
"-datacenter-core|datacenter-core|datacenter-core datacenter-core-*" \
"-datacenter-azure-core|datacenter-core|datacenter-azure-core" \
"-enterprise-core|enterprise-core|enterprise-core enterprise-core-*" \
"-web-core|web-core|web-core web-core-*" \
"-hv|hv|hv hv-*"
;;
* )
printf '%s\n' \
"-enterprise|enterprise|enterprise enterprise-*" \
"-ultimate|ultimate|ultimate ultimate-*" \
"|default|@default n pro pro-* professional professional-* business business-*" \
"-iot|iot|iot iot-* enterprise-iot enterprise-iot-*" \
"-ltsc|ltsc|ltsc ltsc-* enterprise-ltsc enterprise-ltsc-*" \
"-education|education|education education-* pro-education pro-education-*" \
"-home|home|home home-*" \
"-home-premium|home|home-premium home-premium-*" \
"-home-basic|home|home-basic home-basic-*" \
"-starter|starter|starter starter-*"
;;
esac
return 0
}
getVersion() {
local name="$1"
@@ -899,8 +845,7 @@ getVersion() {
[ -n "$evaluation" ] && id+="$evaluation"
fi
;;
"win2025"* | "win2022"* | "win2019"* | "win2016"* | \
"win2012"* | "win2008"* | "win2003"* )
"win20"* )
if [[ "${name,,}" == *"hyper-v server"* ]]; then
id+="-hv"
elif edition=$(getServerEditionID "$name" "$id"); then
@@ -1453,7 +1398,7 @@ getLink4() {
"win2008r2" )
size=3166584832
sum="dfd9890881b7e832a927c38310fb415b7ea62ac5a896671f2ce2a111998f0df8"
url="en_windows_server_2008_r2_with_sp1_x64_dvd_617601_202006/en_windows_server_2008_r2_with_sp1_x64_dvd_617601.iso"
url="en_windows_server_2008_r2_with_sp1_x64_dvd_617601_202405/en_windows_server_2008_r2_with_sp1_x64_dvd_617601.iso"
;;
"win7x64" | "win7x64-ultimate" )
size=3320903680
+177 -325
View File
@@ -4,39 +4,17 @@ set -Eeuo pipefail
getVersions() {
local xml="$1"
local versions_name="$2"
local bases_name="$3"
local groups_name="$4"
local indexes_name="$5"
local -n bases_ref="$bases_name"
local -n groups_ref="$groups_name"
local -n indexes_ref="$indexes_name"
local -n versions_ref="$versions_name"
local platform image_count records record_count=0
local image_index display product image edition_id
local install_type flags candidate candidate_id
local candidate_base evaluation key name structured
local image_index image edition_id
local install_type candidate_base
local candidate candidate_id flags
local evaluation key name structured
local platform records display product
local separator=$'\x1f'
bases_ref=()
groups_ref=()
indexes_ref=()
versions_ref=()
local -A indexes=()
platform=$(getPlatform "$xml") || return 1
image_count=$(xmlstarlet sel -T -t -v 'count(/WIM/IMAGE)' - 2>/dev/null <<< "$xml") || return 1
if [[ ! "$image_count" =~ ^[0-9]+$ ]]; then
error "Invalid image count in WIM metadata: '$image_count'"
return 1
fi
(( image_count > 0 )) || return 0
# Keep one compact record per image. XML 1.0 cannot contain U+001F, so it
# can safely separate fields while all edition logic remains in Bash.
if ! records=$(xmlstarlet sel \
-T -t \
-m '/WIM/IMAGE' \
@@ -54,8 +32,6 @@ getVersions() {
while IFS="$separator" read -r image_index display product image edition_id install_type flags; do
((record_count += 1))
[ -n "$image_index" ] || continue
if [[ ! "$image_index" =~ ^[1-9][0-9]*$ ]]; then
@@ -66,47 +42,41 @@ getVersions() {
candidate_id=""
candidate_base=""
# NAME normally contains the most precise edition identifier (including
# Server Core), while DISPLAYNAME is the best fallback for other images.
for candidate in "$image" "$display" "$product"; do
[[ "$candidate" == *"Operating System"* ]] && continue
[ -n "$candidate" ] || continue
candidate_base=$(fromName "$candidate" "$platform")
candidate_id=$(getVersion "$candidate" "$platform")
candidate_base="${candidate_id%%-*}"
[ -n "$candidate_base" ] && [ -n "$candidate_id" ] && break
[ -n "$candidate_id" ] && break
done
# Preserve NAME precedence, but allow a recognized DISPLAYNAME edition to
# refine an otherwise generic family name.
# NAME sometimes contains only the Windows family while DISPLAYNAME
# contains the actual edition. Use the more specific DISPLAYNAME then.
if [ -n "$candidate_base" ] && [ -n "$candidate_id" ] && [ -n "$display" ]; then
name=$(normalizeEdition "$(printVersion "$candidate_base" "")") || return 1
candidate=$(normalizeEdition "$candidate") || return 1
if [ "$candidate" = "$name" ]; then
structured=$(getVersion "$display" "$platform")
if [ "$(fromName "$display" "$platform")" = "$candidate_base" ] &&
[ -n "$structured" ] && [[ "${structured%-eval}" != "$candidate_base" ]] &&
[[ "$(getVersionPriority "$structured" "$candidate_base")" != "other" ]]; then
if [ -n "$structured" ] && [ "${structured%%-*}" = "$candidate_base" ] &&
[[ "${structured%-eval}" != "$candidate_base" ]] &&
[ "$(getEditionRank "$structured")" -lt 99 ]; then
candidate_id="$structured"
fi
fi
fi
if [ -z "$candidate_base" ] || [ -z "$candidate_id" ]; then
name="${display:-${image:-$product}}"
[ -n "$name" ] && warn "Unknown image name: '$name'"
continue
fi
evaluation=""
@@ -122,10 +92,9 @@ getVersions() {
key="${candidate_id,,}"
# Some client media use the same friendly name-derived ID for distinct
# editions. Preserve the established unsuffixed Pro ID, and use the
# structured edition metadata only to disambiguate a collision.
if [[ -v "indexes_ref[$key]" ]]; then
# Friendly names can collapse two different images to the same ID.
# Structured metadata is only needed to disambiguate that collision.
if [[ -v "indexes[$key]" ]]; then
structured=""
@@ -136,9 +105,6 @@ getVersions() {
"win20"* )
structured=$(normalizeServerEditionID "${flags:-$edition_id}") || return 1
# Some media use the same EDITIONID for Core and Desktop images.
# INSTALLATIONTYPE provides the structural distinction without
# requiring a hardcoded marketing name.
if [[ "${install_type,,}" == *"core"* && "$structured" != *"-core" ]]; then
structured+="-core"
fi
@@ -152,262 +118,187 @@ getVersions() {
fi
if [[ -v "indexes_ref[$key]" ]]; then
warn "Duplicate image identity '$candidate_id' at indexes ${indexes_ref[$key]} and $image_index"
if [[ -v "indexes[$key]" ]]; then
warn "Duplicate image identity '$candidate_id' at indexes ${indexes[$key]} and $image_index"
continue
fi
indexes_ref["$key"]="$image_index"
versions_ref+=( "$candidate_id" )
bases_ref+=( "$candidate_base" )
groups_ref+=( "$(getVersionPriority "$candidate_id" "$candidate_base")" )
indexes["$key"]="$image_index"
printf '%s\t%s\n' "$candidate_id" "$image_index"
done <<< "$records"
if (( record_count != image_count )); then
error "Expected $image_count image records in WIM metadata, found $record_count!"
return 1
fi
return 0
}
getVersionPriority() {
getEditionRank() {
local id="${1,,}"
local base="${2,,}"
local entry priority patterns pattern
local result="other" score best_score=-1
local -a order=() pattern_list=()
local base="${id%%-*}"
local edition
id="${id%-eval}"
mapfile -t order < <(getEditionOrder "$base")
local edition="${id#"$base"}"
edition="${id#"$base"}"
edition="${edition#-}"
# Use the most specific matching pattern. This prevents broad patterns
# such as enterprise-* from taking precedence over enterprise-iot-*.
for entry in "${order[@]}"; do
IFS='|' read -r _ priority patterns <<< "$entry"
read -r -a pattern_list <<< "$patterns"
for pattern in "${pattern_list[@]}"; do
if [ "$pattern" = "@default" ]; then
[ -z "$edition" ] || continue
score=1
elif [[ "$pattern" == *"*" ]]; then
local prefix="${pattern%\*}"
[[ "$edition" == "$prefix"* ]] || continue
score="${#pattern}"
elif [ "$edition" = "$pattern" ]; then
score="${#pattern}"
else
continue
fi
if (( score > best_score )); then
result="$priority"
best_score="$score"
fi
done
done
echo "$result"
return 0
case "$base" in
"win20"* )
case "$edition" in
"" ) echo 0 ;;
"datacenter-azure-core" | "datacenter-azure-core-"* | \
"datacenter-core" | "datacenter-core-"* ) echo 7 ;;
"enterprise-core" | "enterprise-core-"* ) echo 8 ;;
"web-core" | "web-core-"* ) echo 9 ;;
"standard-core" | "standard-core-"* ) echo 6 ;;
"datacenter" | "datacenter-"* ) echo 1 ;;
"enterprise" | "enterprise-"* ) echo 2 ;;
"web" | "web-"* ) echo 3 ;;
"foundation" | "foundation-"* ) echo 4 ;;
"essentials" | "essentials-"* ) echo 5 ;;
"hv" | "hv-"* ) echo 10 ;;
* ) echo 99 ;;
esac
;;
* )
case "$edition" in
"enterprise-iot" | "enterprise-iot-"* | "iot" | "iot-"* ) echo 3 ;;
"enterprise-ltsc" | "enterprise-ltsc-"* | "ltsc" | "ltsc-"* ) echo 4 ;;
"pro-education" | "pro-education-"* | "education" | "education-"* ) echo 5 ;;
"enterprise" | "enterprise-"* ) echo 0 ;;
"ultimate" | "ultimate-"* ) echo 1 ;;
"" | "n" | "pro" | "pro-"* | "professional" | "professional-"* | \
"business" | "business-"* ) echo 2 ;;
"home" | "home-"* ) echo 6 ;;
"starter" | "starter-"* ) echo 7 ;;
* ) echo 99 ;;
esac
;;
esac
}
selectVersion() {
local versions_name="$1"
local indexes_name="$2"
local preferred_name="$3"
local result_name="$4"
local index_name="$5"
local wanted="$1"
shift
local -a candidates=()
local -n index_map="$indexes_name"
local -n version_list="$versions_name"
local -n selected_version="$result_name"
local -n preference_list="$preferred_name"
local -n selected_image_index="$index_name"
local wanted candidate match
local candidate record id index
local -a candidates=("$wanted")
# A detected edition is only selectable when a matching answer file can
# actually be staged for it.
for wanted in "${preference_list[@]}"; do
# Normal and Evaluation variants of the same edition are compatible.
if [[ "${wanted,,}" == *"-eval" ]]; then
candidates+=("${wanted%-eval}")
else
candidates+=("$wanted-eval")
fi
[ -n "$wanted" ] || continue
mapfile -t candidates < <(getCompatibleVersions "$wanted")
for candidate in "${candidates[@]}"; do
for record in "$@"; do
for candidate in "${candidates[@]}"; do
match=$(hasVersion "$candidate" "${version_list[@]}") || continue
hasAnswerFile "$match" || continue
local key="${match,,}"
selected_version="$match"
selected_image_index="${index_map[$key]}"
IFS=$'\t' read -r id index <<< "$record"
[[ "${id,,}" == "${candidate,,}" ]] || continue
hasAnswerFile "$id" || continue
printf '%s\n%s\n' "$id" "$index"
return 0
done
done
return 1
}
getCompatibleVersions() {
local wanted="$1"
printf '%s\n' "$wanted"
# Treat normal and Evaluation variants of the same edition as compatible.
# The exact requested variant is always checked first.
if [[ "${wanted,,}" == *"-eval" ]]; then
printf '%s\n' "${wanted%-eval}"
else
printf '%s\n' "$wanted-eval"
fi
}
selectEdition() {
local versions_name="$1"
local bases_name="$2"
local groups_name="$3"
local indexes_name="$4"
local suggested="$5"
local result_name="$6"
local index_name="$7"
local normalize_name="$8"
local order_name="$9"
local -A seen=()
local -a preferred=()
local -n edition_bases="$bases_name"
local -n edition_order="$order_name"
local -n edition_groups="$groups_name"
local -n edition_versions="$versions_name"
local base edition entry suffix priority i
# Selection precedence is explicit EDITION, source suggestion, canonical
# edition order, then noncanonical editions from the same priority groups.
if [ -n "$EDITION" ]; then
for base in "${edition_bases[@]}"; do
edition=$("$normalize_name" "$EDITION" "$base") || return 1
preferred+=("$base${edition:+-$edition}")
done
if selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"; then
return 0
fi
warn "edition '$EDITION' is not supported by this image, using automatic selection instead."
fi
if [ -n "$suggested" ]; then
preferred=("$suggested")
if selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"; then
return 0
fi
fi
# First try each canonical edition in its configured order.
preferred=()
for entry in "${edition_order[@]}"; do
IFS='|' read -r suffix _ _ <<< "$entry"
for base in "${edition_bases[@]}"; do
preferred+=("$base$suffix")
done
done
if selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"; then
return 0
fi
# Then try noncanonical editions from the same preference groups.
seen=()
preferred=()
for entry in "${edition_order[@]}"; do
IFS='|' read -r _ priority _ <<< "$entry"
[[ -v "seen[$priority]" ]] && continue
seen["$priority"]="Y"
for ((i=0;i<${#edition_versions[@]};i++)); do
[[ "${edition_groups[$i]}" == "$priority" ]] || continue
preferred+=("${edition_versions[$i]}")
done
done
selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"
}
detectVersion() {
local xml="$1"
local suggested="${2:-}"
local -a bases=()
local -a groups=()
local -a versions=()
local -A image_indexes=()
local -a selection_order=()
local result="" index=""
local normalize="normalizeEditionID"
local output record id index base edition suffix
local -a images=() bases=() order=()
getVersions "$xml" versions bases groups image_indexes || return
output=$(getVersions "$xml") || return
[ -n "$output" ] && mapfile -t images <<< "$output"
if [ "${#versions[@]}" -eq 0 ]; then
printf '%s\n%s\n' "$result" "$index"
if [ "${#images[@]}" -eq 0 ]; then
printf '\n\n'
return 0
fi
local normalize="normalizeEditionID"
for record in "${images[@]}"; do
IFS=$'\t' read -r id _ <<< "$record"
bases+=("${id%%-*}")
done
case "${bases[0],,}" in
"win20"* )
normalize="normalizeServerEditionID" ;;
normalize="normalizeServerEditionID"
order=(
"" "-datacenter" "-datacenter-azure" "-enterprise" "-web"
"-foundation" "-essentials" "-standard-core" "-datacenter-core"
"-datacenter-azure-core" "-enterprise-core" "-web-core" "-hv"
) ;;
* )
order=(
"-enterprise" "-ultimate" "" "-iot" "-ltsc" "-education"
"-home" "-home-premium" "-home-basic" "-starter"
) ;;
esac
mapfile -t selection_order < <(getEditionOrder "${bases[0]}")
# An explicit EDITION always gets first choice.
if [ -n "$EDITION" ]; then
if selectEdition versions bases groups image_indexes "$suggested" result index "$normalize" selection_order; then
printf '%s\n%s\n' "$result" "$index"
for base in "${bases[@]}"; do
edition=$("$normalize" "$EDITION" "$base") || return 1
if output=$(selectVersion "$base${edition:+-$edition}" "${images[@]}"); then
printf '%s\n' "$output"
return 0
fi
done
warn "edition '$EDITION' is not supported by this image, using automatic selection instead."
fi
# Prefer the normal editions in their established order.
for suffix in "${order[@]}"; do
for base in "${bases[@]}"; do
if output=$(selectVersion "$base$suffix" "${images[@]}"); then
printf '%s\n' "$output"
return 0
fi
done
done
# Otherwise keep the established family preference for noncanonical
# editions, while preserving WIM order within the same family.
local rank best_rank=99 result="" result_index=""
for record in "${images[@]}"; do
IFS=$'\t' read -r id index <<< "$record"
hasAnswerFile "$id" || continue
rank=$(getEditionRank "$id")
(( rank < best_rank )) || continue
best_rank="$rank"
result="$id"
result_index="$index"
done
if [ -n "$result" ]; then
printf '%s\n%s\n' "$result" "$result_index"
return 0
fi
# Keep the first detected image identity when no edition with a usable answer
# file was found, so manual and generic fallback handling can still continue.
result="${versions[0]}"
local key="${result,,}"
index="${image_indexes[$key]}"
# Keep the first detected identity when only manual/generic fallback remains.
IFS=$'\t' read -r id index <<< "${images[0]}"
printf '%s\n%s\n' "$result" "$index"
printf '%s\n%s\n' "$id" "$index"
return 0
}
@@ -513,22 +404,6 @@ getImageSize() {
printf '%s\n' "$size"
}
hasVersion() {
local wanted="$1"
shift
local actual
for actual in "$@"; do
[[ "${actual,,}" == "${wanted,,}" ]] || continue
echo "$actual"
return 0
done
return 1
}
checkPlatform() {
local xml="$1"
@@ -571,76 +446,53 @@ getPlatform() {
local xml="$1"
local image_count invalid_count
local x86 x64 arm64 unknown value
local platform="" count=0 output
local -a counts=()
local arch current platform=""
local image_count output records
local separator=$'\x1f'
if ! output=$(xmlstarlet sel \
-T -t \
-v 'count(/WIM/IMAGE)' -n \
-v 'count(/WIM/IMAGE[count(WINDOWS/ARCH) != 1])' -n \
-v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)="0"])' -n \
-v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)="9"])' -n \
-v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)="12"])' -n \
-v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)!="0" and normalize-space(.)!="9" and normalize-space(.)!="12"])' -n \
-m '/WIM/IMAGE' \
-v 'count(WINDOWS/ARCH)' -o "$separator" \
-v 'normalize-space(WINDOWS/ARCH)' -n \
- 2>/dev/null <<< "$xml"); then
error "Failed to read architecture metadata from WIM image!"
return 1
fi
mapfile -t counts <<< "$output"
image_count="${output%%$'\n'*}"
if (( ${#counts[@]} != 6 )); then
error "Failed to read architecture counts from WIM metadata!"
return 1
fi
for value in "${counts[@]}"; do
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
error "Invalid architecture count in WIM metadata: '$value'"
return 1
fi
done
image_count="${counts[0]}"
invalid_count="${counts[1]}"
x86="${counts[2]}"
x64="${counts[3]}"
arm64="${counts[4]}"
unknown="${counts[5]}"
if (( image_count == 0 )); then
if [ "$image_count" = "0" ]; then
error "No images were found in WIM metadata!"
return 1
fi
if (( invalid_count > 0 )); then
error "Missing or duplicate architecture metadata in WIM image!"
return 1
fi
records="${output#*$'\n'}"
if (( unknown > 0 )); then
error "Unsupported architecture value in WIM metadata!"
return 1
fi
while IFS="$separator" read -r count arch; do
(( x86 > 0 )) && ((count += 1))
(( x64 > 0 )) && ((count += 1))
(( arm64 > 0 )) && ((count += 1))
if [ "$count" != "1" ]; then
error "Missing or duplicate architecture metadata in WIM image!"
return 1
fi
if (( count > 1 )); then
platform="mixed"
elif (( x86 > 0 )); then
platform="x86"
elif (( x64 > 0 )); then
platform="x64"
elif (( arm64 > 0 )); then
platform="arm64"
else
error "Failed to determine architecture from WIM metadata!"
return 1
fi
case "$arch" in
"0" ) current="x86" ;;
"9" ) current="x64" ;;
"12" ) current="arm64" ;;
* )
error "Unsupported architecture value in WIM metadata!"
return 1 ;;
esac
if [ -z "$platform" ]; then
platform="$current"
elif [ "$platform" != "$current" ]; then
platform="mixed"
fi
done <<< "$records"
echo "$platform"
return 0