feat: Improve Windows image detection and unattended setup (#2037)

This commit is contained in:
Kroese
2026-08-03 21:25:44 +02:00
committed by GitHub
parent cd8af5af51
commit 919222ef25
7 changed files with 1958 additions and 1391 deletions
+894 -478
View File
File diff suppressed because it is too large Load Diff
+144 -196
View File
@@ -47,116 +47,79 @@ parseVersion() {
case "${VERSION,,}" in case "${VERSION,,}" in
"11" | "11p" | "win11" | "pro11" | "win11p" | "windows11" | "windows 11" ) "11" | "11p" | "win11" | "pro11" | "win11p" | "windows11" | "windows 11" )
VERSION="win11x64" VERSION="win11x64" ;;
;;
"11e" | "win11e" | "windows11e" | "windows 11e" ) "11e" | "win11e" | "windows11e" | "windows 11e" )
VERSION="win11x64-enterprise-eval" VERSION="win11x64-enterprise-eval" ;;
;;
"11l" | "11ltsc" | "ltsc11" | "win11l" | "win11-ltsc" | "win11x64-ltsc" ) "11l" | "11ltsc" | "ltsc11" | "win11l" | "win11-ltsc" | "win11x64-ltsc" )
VERSION="win11x64-enterprise-ltsc-eval" VERSION="win11x64-enterprise-ltsc-eval" ;;
;;
"11i" | "11iot" | "iot11" | "win11i" | "win11-iot" | "win11x64-iot" ) "11i" | "11iot" | "iot11" | "win11i" | "win11-iot" | "win11x64-iot" )
VERSION="win11x64-enterprise-iot-eval" VERSION="win11x64-enterprise-iot-eval" ;;
;;
"10" | "10p" | "win10" | "pro10" | "win10p" | "windows10" | "windows 10" ) "10" | "10p" | "win10" | "pro10" | "win10p" | "windows10" | "windows 10" )
VERSION="win10x64" VERSION="win10x64" ;;
;;
"10e" | "win10e" | "windows10e" | "windows 10e" ) "10e" | "win10e" | "windows10e" | "windows 10e" )
VERSION="win10x64-enterprise-eval" VERSION="win10x64-enterprise-eval" ;;
;;
"10l" | "10ltsc" | "ltsc10" | "win10l" | "win10-ltsc" | "win10x64-ltsc" ) "10l" | "10ltsc" | "ltsc10" | "win10l" | "win10-ltsc" | "win10x64-ltsc" )
VERSION="win10x64-enterprise-ltsc-eval" VERSION="win10x64-enterprise-ltsc-eval" ;;
;;
"10i" | "10iot" | "iot10" | "win10i" | "win10-iot" | "win10x64-iot" ) "10i" | "10iot" | "iot10" | "win10i" | "win10-iot" | "win10x64-iot" )
VERSION="win10x64-enterprise-iot-eval" VERSION="win10x64-enterprise-iot-eval" ;;
;;
"8" | "8p" | "81" | "81p" | "pro8" | "8.1" | "win8" | "win8p" | "win81" | "win81p" | "windows 8" ) "8" | "8p" | "81" | "81p" | "pro8" | "8.1" | "win8" | "win8p" | "win81" | "win81p" | "windows 8" )
VERSION="win81x64" VERSION="win81x64" ;;
;;
"8e" | "81e" | "8.1e" | "win8e" | "win81e" | "windows 8e" ) "8e" | "81e" | "8.1e" | "win8e" | "win81e" | "windows 8e" )
VERSION="win81x64-enterprise-eval" VERSION="win81x64-enterprise-eval" ;;
;;
"7" | "win7" | "windows7" | "windows 7" ) "7" | "win7" | "windows7" | "windows 7" )
VERSION="win7x64" VERSION="win7x64" ;;
;;
"7u" | "win7u" | "windows7u" | "windows 7u" ) "7u" | "win7u" | "windows7u" | "windows 7u" )
VERSION="win7x64-ultimate" VERSION="win7x64-ultimate" ;;
;;
"7e" | "win7e" | "windows7e" | "windows 7e" ) "7e" | "win7e" | "windows7e" | "windows 7e" )
VERSION="win7x64-enterprise" VERSION="win7x64-enterprise" ;;
;;
"7x86" | "win7x86" | "win732" | "windows7x86" ) "7x86" | "win7x86" | "win732" | "windows7x86" )
VERSION="win7x86" VERSION="win7x86" ;;
;;
"7ux86" | "7u32" | "win7x86-ultimate" ) "7ux86" | "7u32" | "win7x86-ultimate" )
VERSION="win7x86-ultimate" VERSION="win7x86-ultimate" ;;
;;
"7ex86" | "7e32" | "win7x86-enterprise" ) "7ex86" | "7e32" | "win7x86-enterprise" )
VERSION="win7x86-enterprise" VERSION="win7x86-enterprise" ;;
;;
"vista" | "vs" | "6" | "winvista" | "windowsvista" | "windows vista" ) "vista" | "vs" | "6" | "winvista" | "windowsvista" | "windows vista" )
VERSION="winvistax64" VERSION="winvistax64" ;;
;;
"vistu" | "vu" | "6u" | "winvistu" ) "vistu" | "vu" | "6u" | "winvistu" )
VERSION="winvistax64-ultimate" VERSION="winvistax64-ultimate" ;;
;;
"viste" | "ve" | "6e" | "winviste" ) "viste" | "ve" | "6e" | "winviste" )
VERSION="winvistax64-enterprise" VERSION="winvistax64-enterprise" ;;
;;
"vistax86" | "vista32" | "6x86" | "winvistax86" | "windowsvistax86" ) "vistax86" | "vista32" | "6x86" | "winvistax86" | "windowsvistax86" )
VERSION="winvistax86" VERSION="winvistax86" ;;
;;
"vux86" | "vu32" | "winvistax86-ultimate" ) "vux86" | "vu32" | "winvistax86-ultimate" )
VERSION="winvistax86-ultimate" VERSION="winvistax86-ultimate" ;;
;;
"vex86" | "ve32" | "winvistax86-enterprise" ) "vex86" | "ve32" | "winvistax86-enterprise" )
VERSION="winvistax86-enterprise" VERSION="winvistax86-enterprise" ;;
;;
"xp" | "xp32" | "xpx86" | "5" | "5x86" | "winxp" | "winxp86" | "windowsxp" | "windows xp" ) "xp" | "xp32" | "xpx86" | "5" | "5x86" | "winxp" | "winxp86" | "windowsxp" | "windows xp" )
VERSION="winxpx86" VERSION="winxpx86" ;;
;;
"xp64" | "xpx64" | "5x64" | "winxp64" | "winxpx64" | "windowsxp64" | "windowsxpx64" ) "xp64" | "xpx64" | "5x64" | "winxp64" | "winxpx64" | "windowsxp64" | "windowsxpx64" )
VERSION="winxpx64" VERSION="winxpx64" ;;
;;
"2k" | "2000" | "win2k" | "win2000" | "windows2k" | "windows2000" ) "2k" | "2000" | "win2k" | "win2000" | "windows2k" | "windows2000" )
VERSION="win2kx86" VERSION="win2kx86" ;;
;;
"25" | "2025" | "win25" | "win2025" | "windows2025" | "windows 2025" ) "25" | "2025" | "win25" | "win2025" | "windows2025" | "windows 2025" )
VERSION="win2025-eval" VERSION="win2025-eval" ;;
;;
"22" | "2022" | "win22" | "win2022" | "windows2022" | "windows 2022" ) "22" | "2022" | "win22" | "win2022" | "windows2022" | "windows 2022" )
VERSION="win2022-eval" VERSION="win2022-eval" ;;
;;
"19" | "2019" | "win19" | "win2019" | "windows2019" | "windows 2019" ) "19" | "2019" | "win19" | "win2019" | "windows2019" | "windows 2019" )
VERSION="win2019-eval" VERSION="win2019-eval" ;;
;;
"16" | "2016" | "win16" | "win2016" | "windows2016" | "windows 2016" ) "16" | "2016" | "win16" | "win2016" | "windows2016" | "windows 2016" )
VERSION="win2016-eval" VERSION="win2016-eval" ;;
;;
"hv" | "hyperv" | "hyper v" | "hyper-v" | "19hv" | "2019hv" | "win2019hv" ) "hv" | "hyperv" | "hyper v" | "hyper-v" | "19hv" | "2019hv" | "win2019hv" )
VERSION="win2019-hv" VERSION="win2019-hv" ;;
;;
"2012" | "2012r2" | "win2012" | "win2012r2" | "windows2012" | "windows 2012" ) "2012" | "2012r2" | "win2012" | "win2012r2" | "windows2012" | "windows 2012" )
VERSION="win2012r2-eval" VERSION="win2012r2-eval" ;;
;;
"2008" | "2008r2" | "win2008" | "win2008r2" | "windows2008" | "windows 2008" ) "2008" | "2008r2" | "win2008" | "win2008r2" | "windows2008" | "windows 2008" )
VERSION="win2008r2" VERSION="win2008r2" ;;
;;
"2003" | "2003r2" | "win2003" | "win2003r2" | "windows2003" | "windows 2003" ) "2003" | "2003r2" | "win2003" | "win2003r2" | "windows2003" | "windows 2003" )
VERSION="win2003r2" VERSION="win2003r2" ;;
;;
"core11" | "core 11" ) "core11" | "core 11" )
VERSION="core11" VERSION="core11" ;;
;;
"tiny11" | "tiny 11" ) "tiny11" | "tiny 11" )
VERSION="tiny11" VERSION="tiny11" ;;
;;
"tiny10" | "tiny 10" ) "tiny10" | "tiny 10" )
VERSION="tiny10" VERSION="tiny10" ;;
;;
"reactos" | "react os" ) "reactos" | "react os" )
VERSION="reactos" VERSION="reactos" ;;
;;
esac esac
SUGGEST=$(getSuggestedVersion "$VERSION") SUGGEST=$(getSuggestedVersion "$VERSION")
@@ -172,29 +135,21 @@ getSuggestedVersion() {
case "$id" in case "$id" in
"win10x64" | "win11x64" ) "win10x64" | "win11x64" )
echo "$id" echo "$id" ;;
;;
"win7x64" | "win7x86" | "winvistax64" | "winvistax86" ) "win7x64" | "win7x86" | "winvistax64" | "winvistax86" )
echo "$id-ultimate" echo "$id-ultimate" ;;
;;
"tiny10" ) "tiny10" )
echo "win10x64-ltsc" echo "win10x64-ltsc" ;;
;;
*"-enterprise-ltsc-eval" ) *"-enterprise-ltsc-eval" )
echo "${id%-enterprise-ltsc-eval}-ltsc" echo "${id%-enterprise-ltsc-eval}-ltsc" ;;
;;
*"-enterprise-iot-eval" ) *"-enterprise-iot-eval" )
echo "${id%-enterprise-iot-eval}-iot" echo "${id%-enterprise-iot-eval}-iot" ;;
;;
*"-enterprise-ltsc" ) *"-enterprise-ltsc" )
echo "${id%-enterprise-ltsc}-ltsc" echo "${id%-enterprise-ltsc}-ltsc" ;;
;;
*"-enterprise-iot" ) *"-enterprise-iot" )
echo "${id%-enterprise-iot}-iot" echo "${id%-enterprise-iot}-iot" ;;
;;
*"-eval" ) *"-eval" )
echo "${id%-eval}" echo "${id%-eval}" ;;
;;
esac esac
return 0 return 0
@@ -205,11 +160,8 @@ getLanguage() {
local source="$1" local source="$1"
local input="${1,,}" local input="${1,,}"
local ret="$2" local ret="$2"
local id="$source"
local lang="" local id="$source" lang="" desc="" short="" culture=""
local desc=""
local short=""
local culture=""
case "$input" in case "$input" in
"ar" | "ar-"* | "arabic" | "arab" ) "ar" | "ar-"* | "arabic" | "arab" )
@@ -496,14 +448,11 @@ printVariant() {
case "${id,,}" in case "${id,,}" in
*"-iot" | *"-iot-eval" ) *"-iot" | *"-iot-eval" )
desc+=" IoT" desc+=" IoT" ;;
;;
*"-ltsc" | *"-ltsc-eval" ) *"-ltsc" | *"-ltsc-eval" )
desc+=" LTSC" desc+=" LTSC" ;;
;;
*"-enterprise" | *"-enterprise-eval" ) *"-enterprise" | *"-enterprise-eval" )
desc+=" Enterprise" desc+=" Enterprise" ;;
;;
esac esac
if enabled "$show_eval" && [[ "${id,,}" == *"-eval" ]]; then if enabled "$show_eval" && [[ "${id,,}" == *"-eval" ]]; then
@@ -540,6 +489,7 @@ printEdition() {
local id="$1" local id="$1"
local desc="$2" local desc="$2"
local show_eval="${3:-N}" local show_eval="${3:-N}"
local normalized="${id,,}" local normalized="${id,,}"
local result edition="" suffix="" local result edition="" suffix=""
@@ -561,20 +511,15 @@ printEdition() {
esac esac
;; ;;
"home" ) "home" )
edition="Home" edition="Home" ;;
;;
"starter" ) "starter" )
edition="Starter" edition="Starter" ;;
;;
"ultimate" ) "ultimate" )
edition="Ultimate" edition="Ultimate" ;;
;;
"enterprise" ) "enterprise" )
edition="Enterprise" edition="Enterprise" ;;
;;
"education" ) "education" )
edition="Education" edition="Education" ;;
;;
"n" ) "n" )
case "$normalized" in case "$normalized" in
"win7"* ) edition="Professional N" ;; "win7"* ) edition="Professional N" ;;
@@ -582,22 +527,18 @@ printEdition() {
esac esac
;; ;;
"iot" | "enterprise-iot" ) "iot" | "enterprise-iot" )
edition="IoT Enterprise LTSC" edition="IoT Enterprise LTSC" ;;
;;
"ltsc" | "enterprise-ltsc" ) "ltsc" | "enterprise-ltsc" )
edition="Enterprise LTSC" edition="Enterprise LTSC" ;;
;;
* ) * )
edition=$(formatEdition "$suffix") edition=$(formatEdition "$suffix")
;; ;;
esac esac
;; ;;
"winxp"* ) "winxp"* )
edition="Professional" edition="Professional" ;;
;;
"win2019-hv"* ) "win2019-hv"* )
edition="2019" edition="2019" ;;
;;
"win20"* ) "win20"* )
[[ "$normalized" == *"-"* ]] && suffix="${normalized#*-}" [[ "$normalized" == *"-"* ]] && suffix="${normalized#*-}"
@@ -625,10 +566,10 @@ printEdition() {
fromFile() { fromFile() {
local id=""
local desc="$1" local desc="$1"
local file="${1,,}" local file="${1,,}"
local arch="${PLATFORM,,}"
local id="" arch="${PLATFORM,,}"
file="${file//-/_}" file="${file//-/_}"
file="${file// /_}" file="${file// /_}"
@@ -694,10 +635,11 @@ fromFile() {
fromName() { fromName() {
local id=""
local name="$1" local name="$1"
local arch="$2" local arch="$2"
local id=""
local add="" local add=""
[[ "$arch" != "x64" ]] && add="$arch" [[ "$arch" != "x64" ]] && add="$arch"
@@ -723,9 +665,23 @@ fromName() {
return 0 return 0
} }
isClientEdition() {
case "${1,,}" in
"pro" | "professional" | "business" | \
"enterprise" | "ultimate" | "education" | \
"home" | "homepremium" | "home-premium" | \
"homebasic" | "home-basic" | "starter" | "core" )
return 0 ;;
esac
return 1
}
normalizeEdition() { normalizeEdition() {
local source="${1,,}" local source="${1,,}"
local edition local edition
source="${source//evaluation/}" source="${source//evaluation/}"
@@ -745,7 +701,7 @@ normalizeEdition() {
normalizeEditionID() { normalizeEditionID() {
local edition local edition base
local id="$2" local id="$2"
edition=$(normalizeEdition "$1") edition=$(normalizeEdition "$1")
@@ -753,8 +709,21 @@ normalizeEditionID() {
case "$edition" in case "$edition" in
"pro" | "professional" | "business" ) "pro" | "professional" | "business" )
edition="" ;; edition="" ;;
"pro-n" | "pron" | "professional-n" | "professionaln" ) "pro-n" | "pron" | "professional-n" | "professionaln" | "business-n" | "businessn" )
edition="n" ;; edition="n" ;;
* )
if ! isClientEdition "$edition"; then
case "$edition" in
*"-n" ) base="${edition%-n}" ;;
*"n" ) base="${edition%n}" ;;
* ) base="" ;;
esac
if [ -n "$base" ] && isClientEdition "$base"; then
edition="$base-n"
fi
fi ;;
esac esac
case "${id,,}" in case "${id,,}" in
@@ -778,6 +747,7 @@ getEditionID() {
local name="${1,,}" local name="${1,,}"
local id="${2,,}" local id="${2,,}"
local edition local edition
case "$id" in case "$id" in
@@ -813,26 +783,19 @@ normalizeServerEdition() {
case "$edition" in case "$edition" in
"core" | "core-installation" | "server-core-installation" ) "core" | "core-installation" | "server-core-installation" )
edition="standard-core" edition="standard-core" ;;
;;
"desktop-experience" | "server-with-a-gui" | "full-installation" ) "desktop-experience" | "server-with-a-gui" | "full-installation" )
edition="standard" edition="standard" ;;
;;
*"-server-core-installation" ) *"-server-core-installation" )
edition="${edition%-server-core-installation}-core" edition="${edition%-server-core-installation}-core" ;;
;;
*"-core-installation" ) *"-core-installation" )
edition="${edition%-core-installation}-core" edition="${edition%-core-installation}-core" ;;
;;
*"-desktop-experience" ) *"-desktop-experience" )
edition="${edition%-desktop-experience}" edition="${edition%-desktop-experience}" ;;
;;
*"-server-with-a-gui" ) *"-server-with-a-gui" )
edition="${edition%-server-with-a-gui}" edition="${edition%-server-with-a-gui}" ;;
;;
*"-full-installation" ) *"-full-installation" )
edition="${edition%-full-installation}" edition="${edition%-full-installation}" ;;
;;
esac esac
edition="${edition#server-}" edition="${edition#server-}"
@@ -879,6 +842,7 @@ getServerEditionID() {
local name="${1,,}" local name="${1,,}"
local id="${2,,}" local id="${2,,}"
local edition local edition
case "$id" in case "$id" in
@@ -901,42 +865,36 @@ getServerEditionID() {
getEditionOrder() { getEditionOrder() {
local id="${1,,}" local id="${1,,}"
local result_name="$2"
local -n result="$result_name"
result=()
case "$id" in case "$id" in
"win20"* ) "win20"* )
result=( printf '%s\n' \
"|default|@default" "|default|@default" \
"-datacenter|datacenter|datacenter datacenter-*" "-datacenter|datacenter|datacenter datacenter-*" \
"-datacenter-azure|datacenter|datacenter-azure" "-datacenter-azure|datacenter|datacenter-azure" \
"-enterprise|enterprise|enterprise enterprise-*" "-enterprise|enterprise|enterprise enterprise-*" \
"-web|web|web web-*" "-web|web|web web-*" \
"-foundation|foundation|foundation foundation-*" "-foundation|foundation|foundation foundation-*" \
"-essentials|essentials|essentials essentials-*" "-essentials|essentials|essentials essentials-*" \
"-standard-core|standard-core|standard-core standard-core-*" "-standard-core|standard-core|standard-core standard-core-*" \
"-datacenter-core|datacenter-core|datacenter-core datacenter-core-*" "-datacenter-core|datacenter-core|datacenter-core datacenter-core-*" \
"-datacenter-azure-core|datacenter-core|datacenter-azure-core" "-datacenter-azure-core|datacenter-core|datacenter-azure-core" \
"-enterprise-core|enterprise-core|enterprise-core enterprise-core-*" "-enterprise-core|enterprise-core|enterprise-core enterprise-core-*" \
"-web-core|web-core|web-core web-core-*" "-web-core|web-core|web-core web-core-*" \
"-hv|hv|hv hv-*" "-hv|hv|hv hv-*"
)
;; ;;
* ) * )
result=( printf '%s\n' \
"-enterprise|enterprise|enterprise enterprise-*" "-enterprise|enterprise|enterprise enterprise-*" \
"-ultimate|ultimate|ultimate ultimate-*" "-ultimate|ultimate|ultimate ultimate-*" \
"|default|@default n pro pro-* professional professional-* business business-*" "|default|@default n pro pro-* professional professional-* business business-*" \
"-iot|iot|iot iot-* enterprise-iot enterprise-iot-*" "-iot|iot|iot iot-* enterprise-iot enterprise-iot-*" \
"-ltsc|ltsc|ltsc ltsc-* enterprise-ltsc enterprise-ltsc-*" "-ltsc|ltsc|ltsc ltsc-* enterprise-ltsc enterprise-ltsc-*" \
"-education|education|education education-* pro-education pro-education-*" "-education|education|education education-* pro-education pro-education-*" \
"-home|home|home home-*" "-home|home|home home-*" \
"-home-premium|home|home-premium home-premium-*" "-home-premium|home|home-premium home-premium-*" \
"-home-basic|home|home-basic home-basic-*" "-home-basic|home|home-basic home-basic-*" \
"-starter|starter|starter starter-*" "-starter|starter|starter starter-*"
)
;; ;;
esac esac
@@ -945,9 +903,10 @@ getEditionOrder() {
getVersion() { getVersion() {
local id edition
local name="$1" local name="$1"
local arch="$2" local arch="$2"
local id edition
local evaluation="" local evaluation=""
id=$(fromName "$name" "$arch") id=$(fromName "$name" "$arch")
@@ -1002,16 +961,11 @@ isLegacy() {
switchEdition() { switchEdition() {
local -n id="$1" local version="$1"
[[ "${id,,}" == *"-eval" ]] || return 1 [[ "${version,,}" == *"-eval" ]] || return 1
id="${id::-5}"
if ! enabled "${DETECTED_ORG:-}"; then
DETECTED="${SUGGEST:-$id}"
fi
echo "${version::-5}"
return 0 return 0
} }
@@ -1020,9 +974,8 @@ getMido() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local ret="$3" local ret="$3"
local url=""
local sum="" local url="" sum="" size=""
local size=""
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0 [[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1119,9 +1072,8 @@ getLink1() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local ret="$3" local ret="$3"
local url=""
local sum="" local url="" sum="" size=""
local size=""
local host="https://dl.bobpony.com/windows" local host="https://dl.bobpony.com/windows"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0 [[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1258,9 +1210,8 @@ getLink2() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local ret="$3" local ret="$3"
local url=""
local sum="" local url="" sum="" size=""
local size=""
local host="https://files.dog/MSDN" local host="https://files.dog/MSDN"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0 [[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1352,9 +1303,8 @@ getLink3() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local ret="$3" local ret="$3"
local url=""
local sum="" local url="" sum="" size=""
local size=""
local host="https://iso.reactos.org" local host="https://iso.reactos.org"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0 [[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1381,9 +1331,8 @@ getLink4() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local ret="$3" local ret="$3"
local url=""
local sum="" local url="" sum="" size=""
local size=""
local host="https://archive.org/download" local host="https://archive.org/download"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0 [[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1552,12 +1501,13 @@ getLink4() {
getValue() { getValue() {
local val=""
local id="$2" local id="$2"
local lang="$3" local lang="$3"
local type="$4" local type="$4"
local func="getLink$1" local func="getLink$1"
local val=""
if [ "$1" -gt 0 ] && [ "$1" -le "$MIRRORS" ]; then if [ "$1" -gt 0 ] && [ "$1" -le "$MIRRORS" ]; then
val=$($func "$id" "$lang" "$type") val=$($func "$id" "$lang" "$type")
fi fi
@@ -1585,6 +1535,7 @@ isMido() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local sum local sum
disabled "${MIDO:-}" && return 1 disabled "${MIDO:-}" && return 1
@@ -1607,8 +1558,7 @@ isESD() {
"win10${PLATFORM,,}" | \ "win10${PLATFORM,,}" | \
"win11${PLATFORM,,}-enterprise" | \ "win11${PLATFORM,,}-enterprise" | \
"win10${PLATFORM,,}-enterprise" ) "win10${PLATFORM,,}-enterprise" )
return 0 return 0 ;;
;;
esac esac
return 1 return 1
@@ -1627,10 +1577,8 @@ validVersion() {
isESD "$id" "$lang" && return 0 isESD "$id" "$lang" && return 0
for ((i=1;i<=MIRRORS;i++)); do for ((i=1;i<=MIRRORS;i++)); do
url=$(getLink "$i" "$id" "$lang") url=$(getLink "$i" "$id" "$lang")
[ -n "$url" ] && return 0 [ -n "$url" ] && return 0
done done
return 1 return 1
+363 -406
View File
File diff suppressed because it is too large Load Diff
+211 -97
View File
@@ -14,83 +14,138 @@ startWindows() {
if ! hasImage "$ISO"; then if ! hasImage "$ISO"; then
if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then
removeIso "$ISO" && return 68 removeIso "$ISO" || :
return 68
fi fi
fi fi
local extracted=0
local boot="$BOOT" local boot="$BOOT"
local dir="$TMP/unpack" local dir="$TMP/unpack"
local handled=0 extracted=0
selectWindowsImage "$ISO" "$dir" "$boot" || return $?
(( handled )) && return 0
configureMachine "$ISO" "$dir" "$boot" || return $?
(( handled )) && return 0
prepareWindowsImage "$ISO" "$dir" "$boot" || return $?
(( handled )) && return 0
finishInstall "$BOOT" "N" "$boot" || return 100
return 0
}
selectWindowsImage() {
local iso="$1"
local dir="$2"
local boot="$3"
local detect_rc=0
# Known versions already provide the required image metadata.
if resolveImage "$VERSION"; then if resolveImage "$VERSION"; then
if ! setImage; then if ! setImage; then
abortInstall "$dir" "$ISO" "$boot" || return 70 abortInstall "$dir" "$iso" "$boot" || return 70
handled=1
return 0 return 0
fi fi
if needsExtraction "$DETECTED" "$ISO"; then if ! needsExtraction "$DETECTED" "$iso"; then
if ! extractImage "$ISO" "$dir" "$VERSION"; then return 0
removeIso "$ISO" && return 72 fi
if ! extractImage "$iso" "$dir" "$VERSION"; then
removeIso "$iso" || :
return 72
fi fi
extracted=1 extracted=1
return 0
fi fi
else # Inspect unknown media directly before falling back to extraction.
detectIsoImage "$iso" || detect_rc=$?
local detect_rc=0 if (( detect_rc == 0 )); then
detectIsoImage "$ISO" || detect_rc=$? return 0
fi
case "$detect_rc" in # Only code 1 indicates that extraction may recover detection.
if (( detect_rc != 1 )); then
abortInstall "$dir" "$iso" "$boot" || return 76
handled=1
return 0
fi
0 ) ;; if ! extractImage "$iso" "$dir" "$VERSION"; then
1 ) removeIso "$iso" || :
return 74
if ! extractImage "$ISO" "$dir" "$VERSION"; then
removeIso "$ISO" && return 74
fi fi
extracted=1 extracted=1
if ! detectImage "$dir"; then if detectImage "$dir"; then
abortInstall "$dir" "$ISO" "$boot" || return 76
return 0 return 0
fi ;;
* )
abortInstall "$dir" "$ISO" "$boot" || return 76
return 0 ;;
esac
fi fi
abortInstall "$dir" "$iso" "$boot" || return 76
handled=1
return 0
}
configureMachine() {
local iso="$1"
local dir="$2"
local boot="$3"
local desc local desc
if ! desc=$(printVariant "$DETECTED" "$DETECTED"); then if ! desc=$(printVariant "$DETECTED" "$DETECTED"); then
abortInstall "$dir" "$ISO" "$boot" || return 78 abortInstall "$dir" "$iso" "$boot" || return 78
handled=1
return 0 return 0
fi fi
if ! setMachine "$DETECTED" "$ISO" "$dir" "$desc"; then if ! setMachine "$DETECTED" "$iso" "$dir" "$desc"; then
abortInstall "$dir" "$ISO" "$boot" || return 80 abortInstall "$dir" "$iso" "$boot" || return 80
handled=1
return 0 return 0
fi fi
if ! restoreMachineState; then if ! restoreMachineState; then
abortInstall "$dir" "$ISO" "$boot" || return 82 abortInstall "$dir" "$iso" "$boot" || return 82
handled=1
return 0 return 0
fi fi
# Direct-boot media skips all unattended installation preparation.
if bootDirect "$DETECTED"; then if bootDirect "$DETECTED"; then
abortInstall "$dir" "$ISO" "$boot" || return 83 abortInstall "$dir" "$iso" "$boot" || return 83
handled=1
return 0 return 0
fi fi
if canUseSetupImage "$DETECTED" "$ISO"; then return 0
}
prepareWindowsImage() {
local iso="$1"
local dir="$2"
local boot="$3"
# Prefer the original ISO with a small setup image whenever possible.
if canUseSetupImage "$DETECTED" "$iso"; then
if ! stageSetup "$XML" "$LANGUAGE" "$TMP/setup"; then if ! stageSetup "$XML" "$LANGUAGE" "$TMP/setup"; then
abortInstall "$dir" "$ISO" "$boot" || return 84 abortInstall "$dir" "$iso" "$boot" || return 84
handled=1
return 0 return 0
fi fi
@@ -98,33 +153,34 @@ startWindows() {
exit 86 exit 86
fi fi
useOriginalImage "$ISO" || return 88 useOriginalImage "$iso" || return 88
return 0
else fi
# Legacy or modifiable media must be extracted, updated, and rebuilt.
if (( ! extracted )); then if (( ! extracted )); then
if ! extractImage "$ISO" "$dir" "$VERSION"; then if ! extractImage "$iso" "$dir" "$VERSION"; then
removeIso "$ISO" && return 90 removeIso "$iso" || :
return 90
fi fi
fi fi
if ! prepareImage "$ISO" "$dir"; then if ! prepareImage "$iso" "$dir"; then
abortInstall "$dir" "$ISO" "$boot" || return 92 abortInstall "$dir" "$iso" "$boot" || return 92
handled=1
return 0 return 0
fi fi
if ! updateImage "$dir" "$XML" "$LANGUAGE"; then if ! updateImage "$dir" "$XML" "$LANGUAGE"; then
abortInstall "$dir" "$ISO" "$boot" || return 94 abortInstall "$dir" "$iso" "$boot" || return 94
handled=1
return 0 return 0
fi fi
removeImage "$ISO" || return 96 removeImage "$iso" || return 96
buildImage "$dir" || return 98 buildImage "$dir" || return 98
fi
finishInstall "$BOOT" "N" "$boot" || return 100
return 0 return 0
} }
@@ -218,6 +274,8 @@ startInstall() {
ISO=$(basename "$BOOT") ISO=$(basename "$BOOT")
ISO="$TMP/$ISO" ISO="$TMP/$ISO"
# Work from the temporary directory so the persistent source path can
# later contain either the preserved ISO or the rebuilt installation image.
if [ -f "$BOOT" ] && [ -s "$BOOT" ]; then if [ -f "$BOOT" ] && [ -s "$BOOT" ]; then
if ! mv -f -- "$BOOT" "$ISO"; then if ! mv -f -- "$BOOT" "$ISO"; then
error "Failed to move ISO file from \"$BOOT\" to \"$ISO\" !" error "Failed to move ISO file from \"$BOOT\" to \"$ISO\" !"
@@ -255,11 +313,16 @@ abortInstall() {
local dir="$1" local dir="$1"
local iso="$2" local iso="$2"
local boot="$3" local boot="$3"
local efi efi32 efi64 local efi efi32 efi64
# Standalone ESD files and nested archives are not directly bootable media,
# so they cannot use the manual-install fallback.
[[ "${iso,,}" == *".esd" ]] && exit 60 [[ "${iso,,}" == *".esd" ]] && exit 60
enabled "${UNPACK:-}" && exit 60 enabled "${UNPACK:-}" && exit 60
# When automatic preparation fails, inspect extracted media to determine
# whether it can still be booted manually using legacy firmware.
if [[ "${PLATFORM,,}" == "x64" ]] && [ -d "$dir" ]; then if [[ "${PLATFORM,,}" == "x64" ]] && [ -d "$dir" ]; then
efi=$(find "$dir" -maxdepth 1 -type d -iname efi -print -quit) efi=$(find "$dir" -maxdepth 1 -type d -iname efi -print -quit)
@@ -278,6 +341,8 @@ abortInstall() {
fi fi
# Preserve custom media in place. Downloaded or reused media must be moved
# back to persistent storage before the manual fallback is started.
if [ -n "$CUSTOM" ]; then if [ -n "$CUSTOM" ]; then
BOOT="$iso" BOOT="$iso"
REMOVE="N" REMOVE="N"
@@ -317,9 +382,12 @@ skipInstall() {
local iso="$1" local iso="$1"
local previousBase="$2" local previousBase="$2"
local boot="$STORAGE/windows.boot" local boot="$STORAGE/windows.boot"
if [ -n "$previousBase" ]; then if [ -n "$previousBase" ]; then
# A changed source invalidates an unfinished installation. Back up an
# existing installation, but discard stale media when no disk exists yet.
if [[ "${STORAGE,,}/${previousBase,,}" != "${iso,,}" ]]; then if [[ "${STORAGE,,}/${previousBase,,}" != "${iso,,}" ]]; then
if ! hasDisk; then if ! hasDisk; then
@@ -372,6 +440,7 @@ finishInstall() {
local iso="$1" local iso="$1"
local aborted="$2" local aborted="$2"
local boot="$3" local boot="$3"
local base local base
if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then
@@ -428,8 +497,9 @@ finishInstall() {
findFile() { findFile() {
local dir file base
local fname="$1" local fname="$1"
local dir file base
local boot="$STORAGE/windows.boot" local boot="$STORAGE/windows.boot"
dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit)
@@ -462,6 +532,8 @@ findFile() {
ISO="$file" ISO="$file"
CUSTOM="$file" CUSTOM="$file"
# Include the custom ISO size in its persistent name so replacing a
# bind-mounted ISO is detected as a different installation source.
BOOT="$STORAGE/windows.$size.iso" BOOT="$STORAGE/windows.$size.iso"
return 0 return 0
@@ -501,6 +573,8 @@ needsExtraction() {
local id="$1" local id="$1"
local iso="$2" local iso="$2"
# Direct-boot media does not need rebuilding. Legacy/skipped versions,
# standalone ESD downloads, and nested archives require full extraction.
bootDirect "$id" && return 1 bootDirect "$id" && return 1
skipVersion "$id" || skipVersion "$id" ||
@@ -512,6 +586,7 @@ checkFreeSpace() {
local dir="$1" local dir="$1"
local size="$2" local size="$2"
local size_gb space space_gb local size_gb space space_gb
size_gb=$(formatBytes "$size") size_gb=$(formatBytes "$size")
@@ -526,16 +601,6 @@ checkFreeSpace() {
return 0 return 0
} }
getEsdField() {
local list="$1"
local index="$2"
sed -n "${index}p" <<< "$list" | tr -cd '0-9'
return 0
}
extractESD() { extractESD() {
local iso="$1" local iso="$1"
@@ -543,11 +608,11 @@ extractESD() {
local version="$3" local version="$3"
local desc="$4" local desc="$4"
local info count totals links local bootTotal bootLinks wimTotal wimLinks
local bootTotal bootLinks local installSize size edition imgEdition
local wimTotal wimLinks local bootWim installWim bootSize wimSize
local installSize size local index line ret xml metadata count
local edition imgEdition local -a fields
local minSize=100000000 local minSize=100000000
local freeSpace=9606127360 local freeSpace=9606127360
@@ -579,12 +644,30 @@ extractESD() {
checkFreeSpace "$dir" "$freeSpace" || return 1 checkFreeSpace "$dir" "$freeSpace" || return 1
info=$(wimlib-imagex info "$iso") || { if ! xml=$(wimlib-imagex info "$iso" --xml 2>/dev/null |
iconv -f UTF-16LE -t UTF-8 2>/dev/null); then
error "Cannot read ESD file information!" error "Cannot read ESD file information!"
return 1 return 1
} fi
count=$(awk '/Image Count:/ {print $3}' <<< "$info") # Microsoft download ESDs use images 1-3 for setup media, WinPE, and Windows
# Setup; images 4 and higher contain installable editions. Read all metadata
# once because repeatedly inspecting a solid-compressed ESD is expensive.
if ! metadata=$(xmlstarlet sel -t \
-v 'count(/WIM/IMAGE)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="1"]/TOTALBYTES)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="1"]/HARDLINKBYTES)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="3"]/TOTALBYTES)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="3"]/HARDLINKBYTES)' -n \
-m '/WIM/IMAGE[number(@INDEX) >= 4]' -v '@INDEX' -o $'\t' -v 'DESCRIPTION' -n \
<<< "$xml" 2>/dev/null); then
error "Cannot read ESD file information!"
return 1
fi
mapfile -t fields <<< "$metadata"
count="${fields[0]:-}"
if [[ ! "$count" =~ ^[0-9]+$ ]]; then if [[ ! "$count" =~ ^[0-9]+$ ]]; then
error "Cannot read the image count in ESD file!" error "Cannot read the image count in ESD file!"
return 1 return 1
@@ -595,34 +678,33 @@ extractESD() {
return 1 return 1
fi fi
totals=$(grep "Total Bytes:" <<< "$info" || true) bootTotal="${fields[1]:-}"
links=$(grep "Hard Link Bytes:" <<< "$info" || true) bootLinks="${fields[2]:-}"
bootTotal=$(getEsdField "$totals" 1) if [[ ! "$bootTotal" =~ ^[0-9]+$ ]] ||
bootLinks=$(getEsdField "$links" 1) [[ ! "$bootLinks" =~ ^[0-9]+$ ]]; then
if [[ ! "$bootTotal" =~ ^[0-9]+$ ]] || [[ ! "$bootLinks" =~ ^[0-9]+$ ]]; then
error "Cannot read bootdisk size from ESD file!" error "Cannot read bootdisk size from ESD file!"
return 1 return 1
fi fi
local bootSize=$(( bootTotal - bootLinks )) bootSize=$(( bootTotal - bootLinks ))
wimTotal=$(getEsdField "$totals" 3) wimTotal="${fields[3]:-}"
wimLinks=$(getEsdField "$links" 3) wimLinks="${fields[4]:-}"
if [[ ! "$wimTotal" =~ ^[0-9]+$ ]] || [[ ! "$wimLinks" =~ ^[0-9]+$ ]]; then if [[ ! "$wimTotal" =~ ^[0-9]+$ ]] ||
[[ ! "$wimLinks" =~ ^[0-9]+$ ]]; then
error "Cannot read boot.wim size from ESD file!" error "Cannot read boot.wim size from ESD file!"
return 1 return 1
fi fi
local wimSize=$(( wimTotal - wimLinks + bootPad )) wimSize=$(( wimTotal - wimLinks + bootPad ))
/run/progress.sh "$dir" "$bootSize" "$msg ([P])..." & /run/progress.sh "$dir" "$bootSize" "$msg ([P])..." &
local index="1" index="1"
wimlib-imagex apply "$iso" "$index" "$dir" --quiet 2>/dev/null || { wimlib-imagex apply "$iso" "$index" "$dir" --quiet 2>/dev/null || {
local ret=$? ret=$?
fKill "progress.sh" fKill "progress.sh"
error "Extracting $desc bootdisk failed ($ret)" error "Extracting $desc bootdisk failed ($ret)"
return 1 return 1
@@ -630,8 +712,8 @@ extractESD() {
fKill "progress.sh" fKill "progress.sh"
local bootWim="$dir/sources/boot.wim" bootWim="$dir/sources/boot.wim"
local installWim="$dir/sources/install.wim" installWim="$dir/sources/install.wim"
msg="Extracting $desc environment" msg="Extracting $desc environment"
info "$msg..." && html "$msg..." info "$msg..." && html "$msg..."
@@ -639,8 +721,9 @@ extractESD() {
index="2" index="2"
/run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." & /run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$bootWim" --compress=none --quiet || { wimlib-imagex export "$iso" "$index" "$bootWim" \
local ret=$? --compress=none --quiet || {
ret=$?
fKill "progress.sh" fKill "progress.sh"
error "Adding WinPE failed ($ret)" error "Adding WinPE failed ($ret)"
return 1 return 1
@@ -654,8 +737,9 @@ extractESD() {
index="3" index="3"
/run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." & /run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$bootWim" --compress=none --boot --quiet || { wimlib-imagex export "$iso" "$index" "$bootWim" \
local ret=$? --compress=none --boot --quiet || {
ret=$?
fKill "progress.sh" fKill "progress.sh"
error "Adding Windows Setup failed ($ret)" error "Adding Windows Setup failed ($ret)"
return 1 return 1
@@ -678,18 +762,19 @@ extractESD() {
return 1 return 1
fi fi
for (( index=4; index<=count; index++ )); do for line in "${fields[@]:5}"; do
imgEdition=$(wimlib-imagex info "$iso" "$index" | grep '^Description:' | sed 's/Description:[ \t]*//') IFS=$'\t' read -r index imgEdition <<< "$line"
[[ ! "$index" =~ ^[0-9]+$ ]] && continue
[[ "${imgEdition,,}" != "${edition,,}" ]] && continue [[ "${imgEdition,,}" != "${edition,,}" ]] && continue
installSize=$(stat -c%s "$iso") installSize=$(( size + installPad ))
installSize=$(( installSize + installPad ))
/run/progress.sh "$installWim" "$installSize" "$msg ([P])..." & /run/progress.sh "$installWim" "$installSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$installWim" --quiet || { wimlib-imagex export "$iso" "$index" "$installWim" --quiet || {
local ret=$? ret=$?
fKill "progress.sh" fKill "progress.sh"
error "Addition of $index to the $desc image failed ($ret)" error "Addition of $index to the $desc image failed ($ret)"
return 1 return 1
@@ -710,8 +795,9 @@ extractImage() {
local iso="$1" local iso="$1"
local dir="$2" local dir="$2"
local version="$3" local version="$3"
local desc="local ISO"
local file size local file size
local desc="local ISO"
if [ -z "$CUSTOM" ]; then if [ -z "$CUSTOM" ]; then
desc="downloaded ISO" desc="downloaded ISO"
@@ -766,6 +852,8 @@ extractImage() {
else else
# UNPACK archives contain another ISO. Extract the nested ISO, then
# preserve it as the actual source media for subsequent processing.
file=$(find "$dir" -maxdepth 1 -type f -iname "*.iso" -print -quit) file=$(find "$dir" -maxdepth 1 -type f -iname "*.iso" -print -quit)
if [ -z "$file" ]; then if [ -z "$file" ]; then
@@ -799,7 +887,11 @@ setMachine() {
local dir="$3" local dir="$3"
local desc="$4" local desc="$4"
if [[ "${id,,}" != "win9"* ]]; then
ETFS="boot/etfsboot.com" ETFS="boot/etfsboot.com"
else
ETFS="[BOOT]/Boot-1.44M.img"
fi
local version="" local version=""
case "${id,,}" in case "${id,,}" in
@@ -903,12 +995,13 @@ prepareImage() {
local iso="$1" local iso="$1"
local dir="$2" local dir="$2"
local desc missing local desc missing
desc=$(printVariant "$DETECTED" "$DETECTED") desc=$(printVariant "$DETECTED" "$DETECTED")
if [[ "${BOOT_MODE,,}" == "windows_legacy" && # Legacy rebuilt media must retain the source ISO's El Torito boot-load size.
"${DETECTED,,}" != "win9"* ]]; then if [[ "${BOOT_MODE,,}" == "windows_legacy" && "${DETECTED,,}" != "win9"* ]]; then
getBootLoadSize "$iso" "$dir" "$desc" || return 1 getBootLoadSize "$iso" "$dir" "$desc" || return 1
fi fi
@@ -917,13 +1010,14 @@ prepareImage() {
if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then
extractBootImage "$iso" "$dir" "$desc" && return 0 extractBootImage "$iso" "$dir" "$desc" && return 0
error "Failed to extract boot image from ISO image \"${iso}\"!" error "Failed to extract boot image from ISO image \"${iso}\"!"
return 1 return 1
fi fi
EFISYS="efi/microsoft/boot/efisys_noprompt.bin" EFISYS="efi/microsoft/boot/efisys_noprompt.bin"
# A modern rebuilt ISO requires both its BIOS and no-prompt UEFI boot images.
[ -f "$dir/$ETFS" ] && [ -s "$dir/$ETFS" ] && [ -f "$dir/$ETFS" ] && [ -s "$dir/$ETFS" ] &&
[ -f "$dir/$EFISYS" ] && [ -s "$dir/$EFISYS" ] && return 0 [ -f "$dir/$EFISYS" ] && [ -s "$dir/$EFISYS" ] && return 0
@@ -954,7 +1048,8 @@ addFolder() {
local target="${2:-image}" local target="${2:-image}"
local log="${3:-Y}" local log="${3:-Y}"
local mode="${4:-copy}" local mode="${4:-copy}"
local folder file="" source=""
local file="" source="" folder
local dest="$src/\$OEM\$/\$1/OEM" local dest="$src/\$OEM\$/\$1/OEM"
local install="$src/.overlay-install.bat" local install="$src/.overlay-install.bat"
@@ -967,6 +1062,8 @@ addFolder() {
info "$msg" && html "$msg" info "$msg" && html "$msg"
fi fi
# Setup-image mode cannot modify the original ISO, so create a temporary
# writable copy of install.bat for the overlay image.
if [ "$mode" = "overlay" ]; then if [ "$mode" = "overlay" ]; then
rm -f -- "$install" || return 1 rm -f -- "$install" || return 1
@@ -1037,6 +1134,7 @@ addDriver() {
local path="$2" local path="$2"
local target="$3" local target="$3"
local driver="$4" local driver="$4"
local folder="" desc local folder="" desc
if [ -z "$id" ]; then if [ -z "$id" ]; then
@@ -1074,8 +1172,7 @@ addDriver() {
case "${id,,}" in case "${id,,}" in
"winvista"* ) "winvista"* )
[[ "${driver,,}" == "viorng" ]] && return 0 [[ "${driver,,}" == "viorng" ]] && return 0 ;;
;;
esac esac
local dest="$path/$target/$driver" local dest="$path/$target/$driver"
@@ -1093,6 +1190,7 @@ addDrivers() {
local file="${4:-}" local file="${4:-}"
local index="${5:-}" local index="${5:-}"
local log="${6:-Y}" local log="${6:-Y}"
local drivers="$tmp/drivers" local drivers="$tmp/drivers"
rm -rf "$drivers" rm -rf "$drivers"
@@ -1114,6 +1212,7 @@ addDrivers() {
local target="\$WinPEDriver\$" local target="\$WinPEDriver\$"
local dest="$drivers/$target" local dest="$drivers/$target"
mkdir -p "$dest" || return 1 mkdir -p "$dest" || return 1
if [ -n "$file" ]; then if [ -n "$file" ]; then
@@ -1221,12 +1320,13 @@ updateImage() {
local dir="$1" local dir="$1"
local asset="$2" local asset="$2"
local language="$3" local language="$3"
local script=""
local tmp="/tmp/install" local tmp="/tmp/install"
local xml="autounattend.xml" local xml="autounattend.xml"
local bak="${xml//.xml/.org}" local bak="${xml//.xml/.org}"
local dat="${xml//.xml/.dat}" local dat="${xml//.xml/.dat}"
local desc path src wim name info local desc path src wim name info
local script=""
skipVersion "${DETECTED,,}" && return 0 skipVersion "${DETECTED,,}" && return 0
@@ -1255,6 +1355,7 @@ updateImage() {
return 1 return 1
fi fi
# Windows Setup normally resides in boot image 2; single-image media uses 1.
local idx="1" local idx="1"
if ! info=$(wimlib-imagex info -xml "$wim" | iconv -f UTF-16LE -t UTF-8); then if ! info=$(wimlib-imagex info -xml "$wim" | iconv -f UTF-16LE -t UTF-8); then
@@ -1275,6 +1376,8 @@ updateImage() {
error "Failed to add OEM folder to image!" error "Failed to add OEM folder to image!"
fi fi
# Preserve an original answer file only once. The .dat marker identifies an
# image where our generated answer file has already been installed.
if wimlib-imagex extract "$wim" "$idx" "/$xml" "--dest-dir=$tmp" >/dev/null 2>&1; then if wimlib-imagex extract "$wim" "$idx" "/$xml" "--dest-dir=$tmp" >/dev/null 2>&1; then
if ! wimlib-imagex extract "$wim" "$idx" "/$dat" "--dest-dir=$tmp" >/dev/null 2>&1; then if ! wimlib-imagex extract "$wim" "$idx" "/$dat" "--dest-dir=$tmp" >/dev/null 2>&1; then
if ! wimlib-imagex extract "$wim" "$idx" "/$bak" "--dest-dir=$tmp" >/dev/null 2>&1; then if ! wimlib-imagex extract "$wim" "$idx" "/$bak" "--dest-dir=$tmp" >/dev/null 2>&1; then
@@ -1314,7 +1417,7 @@ updateImage() {
validateGeneratedXML "$answer" || return 1 validateGeneratedXML "$answer" || return 1
if [ -z "${CUSTOM_XML:-}" ]; then if [ -z "${CUSTOM_XML:-}" ]; then
prepareSetupScript "$asset" "$tmp/setup" script || exit 84 script=$(prepareSetupScript "$asset" "$tmp/setup") || exit 84
fi fi
if ! wimlib-imagex update "$wim" "$idx" --command "add $answer /$xml" > /dev/null; then if ! wimlib-imagex update "$wim" "$idx" --command "add $answer /$xml" > /dev/null; then
@@ -1328,6 +1431,8 @@ updateImage() {
fi fi
# Manual mode removes generated automation and restores the original answer
# file when one was backed up earlier.
if enabled "$MANUAL"; then if enabled "$MANUAL"; then
removeGeneratedXML "$asset" || return 1 removeGeneratedXML "$asset" || return 1
@@ -1342,6 +1447,8 @@ updateImage() {
fi fi
# Prevent a root-level answer file from overriding the selected automatic or
# manual behavior when Windows Setup first boots.
name="$xml" name="$xml"
enabled "$MANUAL" && name="$bak" enabled "$MANUAL" && name="$bak"
path=$(find "$dir" -maxdepth 1 -type f -iname "$name" -print -quit) || return 1 path=$(find "$dir" -maxdepth 1 -type f -iname "$name" -print -quit) || return 1
@@ -1392,10 +1499,11 @@ reserveSambaPorts() {
backup () { backup () {
local iso="$1" local iso="$1"
local count=1 local count=1
local name="unknown" local name="unknown"
local root="$STORAGE/backups" local root="$STORAGE/backups"
local file previous failed="" local failed="" file previous
previous=$(readState "base") || return 1 previous=$(readState "base") || return 1
[ -n "$previous" ] && name="${previous%.*}" [ -n "$previous" ] && name="${previous%.*}"
@@ -1437,6 +1545,8 @@ backup () {
-not -iname '*.iso' -print0 -not -iname '*.iso' -print0
) )
# Wait for the process-substitution find command so enumeration failures are
# detected rather than being mistaken for a successful backup.
local find_pid=$! local find_pid=$!
if ! wait "$find_pid"; then if ! wait "$find_pid"; then
@@ -1461,6 +1571,8 @@ restoreBootMode() {
[ -n "$mode" ] || return 0 [ -n "$mode" ] || return 0
# A saved legacy mode always wins. A saved modern mode only replaces the
# default mode and never an explicit user-selected boot configuration.
if [[ "${mode,,}" == "windows_legacy" ]]; then if [[ "${mode,,}" == "windows_legacy" ]]; then
BOOT_MODE="$mode" BOOT_MODE="$mode"
return 0 return 0
@@ -1476,6 +1588,8 @@ restoreBootMode() {
restoreMachine() { restoreMachine() {
# Restore the saved machine only when q35 is still the default; an explicit
# user-selected machine must remain untouched.
[[ "${PLATFORM,,}" != "x64" ]] && return 0 [[ "${PLATFORM,,}" != "x64" ]] && return 0
[[ "${MACHINE,,}" != "q35" ]] && return 0 [[ "${MACHINE,,}" != "q35" ]] && return 0
+214 -153
View File
@@ -6,6 +6,7 @@ handleCurlError() {
local code="$1" local code="$1"
local server="$2" local server="$2"
local reason="${3:-}" local reason="${3:-}"
local signal local signal
if [ -n "$reason" ] && (( code <= 125 )); then if [ -n "$reason" ] && (( code <= 125 )); then
@@ -29,8 +30,7 @@ handleCurlError() {
SEGV | ABRT) error "Curl crashed with signal $signal." ;; SEGV | ABRT) error "Curl crashed with signal $signal." ;;
"") error "Curl terminated with exit status $code." ;; "") error "Curl terminated with exit status $code." ;;
*) error "Curl terminated due to signal $signal." ;; *) error "Curl terminated due to signal $signal." ;;
esac esac ;;
;;
esac esac
return 1 return 1
@@ -38,10 +38,9 @@ handleCurlError() {
curlRequest() { curlRequest() {
local output="$1" local server="$1"
local server="$2" local agent="$2"
local agent="$3" shift 2
shift 3
local log reason response local log reason response
@@ -50,6 +49,8 @@ curlRequest() {
return 1 return 1
fi fi
# Preserve curl's status under errexit so its stderr can be translated
# into a useful error instead of terminating the script immediately.
{ {
response=$(LC_ALL=C curl \ response=$(LC_ALL=C curl \
--silent \ --silent \
@@ -76,10 +77,7 @@ curlRequest() {
rm -f "$log" rm -f "$log"
if [ -n "$output" ]; then printf '%s' "$response"
printf -v "$output" '%s' "$response"
fi
return 0 return 0
} }
@@ -93,13 +91,13 @@ downloadWindowsLink() {
local desc="$6" local desc="$6"
local type="$7" local type="$7"
local skuId skuJson
local linkJson link
local ovData ovTime session
local ovToken="" ovTicks="" local ovToken="" ovTicks=""
local profile="606624d44113" local profile="606624d44113"
local skuId skuJson linkJson
local link ovData ovTime session
# uuidgen: For MacOS (installed by default) and other systems (e.g. with no /proc) that don't have a kernel interface for generating random UUIDs # Prefer the Linux kernel UUID source, with uuidgen as a portable fallback
# for macOS and systems without /proc.
if ! session=$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random); then if ! session=$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random); then
error "Failed to generate session ID!" error "Failed to generate session ID!"
return 1 return 1
@@ -112,32 +110,32 @@ downloadWindowsLink() {
return 1 return 1
fi fi
# Microsoft download "protection" requires the sessionId to be whitelisted through vlscppe.microsoft.com/tags # Register the session with Microsoft's anti-abuse endpoint before
# requesting SKU or download links.
local orgId="y6jn8c31" local orgId="y6jn8c31"
local vlsUrl="https://vlscppe.microsoft.com/tags?org_id=$orgId&session_id=$session" local vlsUrl="https://vlscppe.microsoft.com/tags?org_id=$orgId&session_id=$session"
enabled "$DEBUG" && echo "Getting Session ID: $session" enabled "$DEBUG" && echo "Getting Session ID: $session"
# Permit Session ID curlRequest "Microsoft" "$agent" \
curlRequest "" "Microsoft" "$agent" \
--output /dev/null \ --output /dev/null \
--header "Accept:" \ --header "Accept:" \
--max-filesize 100K \ --max-filesize 100K \
-- "$vlsUrl" || return 1 -- "$vlsUrl" || return 1
# Microsoft download "protection" also requires an ov-df.microsoft.com request/reply # Complete Microsoft's ov-df challenge by retrieving a token and timing
# 1) Request mdt.js to get w and rticks. InstanceId is (currently) constant. # value, then returning both with the current timestamp.
local instance="560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175" local instance="560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175"
local ovUrl="https://ov-df.microsoft.com/mdt.js?instanceId=$instance&PageId=si&session_id=$session" local ovUrl="https://ov-df.microsoft.com/mdt.js?instanceId=$instance&PageId=si&session_id=$session"
enabled "$DEBUG" && echo -n "Getting OV data: " enabled "$DEBUG" && echo -n "Getting OV data: "
curlRequest ovData "Microsoft" "$agent" \ ovData=$(curlRequest "Microsoft" "$agent" \
--header "Accept:" \ --header "Accept:" \
--max-filesize 1M \ --max-filesize 1M \
-- "$ovUrl" || return 1 -- "$ovUrl") || return 1
if [[ $ovData =~ [\?\&]w=([A-Fa-f0-9]+) ]]; then if [[ $ovData =~ [\?\&]w=([A-Fa-f0-9]+) ]]; then
ovToken="${BASH_REMATCH[1]}" ovToken="${BASH_REMATCH[1]}"
@@ -156,14 +154,12 @@ downloadWindowsLink() {
sleep 0.2 sleep 0.2
# 2) Send a reply with session ID, current epoch and previously retrieved w and rticks
ovTime=$(date +%s%3N) ovTime=$(date +%s%3N)
ovUrl="https://ov-df.microsoft.com/?session_id=$session&CustomerId=$instance&PageId=si&w=$ovToken&mdt=$ovTime&rticks=$ovTicks" ovUrl="https://ov-df.microsoft.com/?session_id=$session&CustomerId=$instance&PageId=si&w=$ovToken&mdt=$ovTime&rticks=$ovTicks"
enabled "$DEBUG" && echo "Sending OV reply: $instance" enabled "$DEBUG" && echo "Sending OV reply: $instance"
curlRequest "" "Microsoft" "$agent" \ curlRequest "Microsoft" "$agent" \
--output /dev/null \ --output /dev/null \
--header "Accept:" \ --header "Accept:" \
--max-filesize 100K \ --max-filesize 100K \
@@ -173,12 +169,14 @@ downloadWindowsLink() {
local skuUrl="https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=$profile&ProductEditionId=$productId&SKU=undefined&friendlyFileName=undefined&Locale=en-US&sessionID=$session" local skuUrl="https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=$profile&ProductEditionId=$productId&SKU=undefined&friendlyFileName=undefined&Locale=en-US&sessionID=$session"
curlRequest skuJson "Microsoft" "$agent" \ skuJson=$(curlRequest "Microsoft" "$agent" \
--referer "$url" \ --referer "$url" \
--header "Accept:" \ --header "Accept:" \
--max-filesize 100K \ --max-filesize 100K \
-- "$skuUrl" || return 1 -- "$skuUrl") || return 1
# Guard jq under errexit so malformed API data can be handled as a normal
# missing-result error. The same pattern is reused for the link response.
{ skuId=$(printf '%s\n' "$skuJson" | jq --arg LANG "$language" -r 'first(.Skus[]? | select(.Language == $LANG) | .Id) // empty') 2>/dev/null; local rc=$?; } || : { skuId=$(printf '%s\n' "$skuJson" | jq --arg LANG "$language" -r 'first(.Skus[]? | select(.Language == $LANG) | .Id) // empty') 2>/dev/null; local rc=$?; } || :
if [ -z "$skuId" ] || [[ "${skuId,,}" == "null" ]] || (( rc != 0 )); then if [ -z "$skuId" ] || [[ "${skuId,,}" == "null" ]] || (( rc != 0 )); then
@@ -190,19 +188,18 @@ downloadWindowsLink() {
enabled "$DEBUG" && echo "$skuId" enabled "$DEBUG" && echo "$skuId"
enabled "$DEBUG" && echo "Getting ISO download link..." enabled "$DEBUG" && echo "Getting ISO download link..."
# Get ISO download link # Microsoft normally applies request or IP blocking on this final connector
# If any request is going to be blocked by Microsoft it's always this last one (the previous requests always seem to succeed) # call rather than during the preceding session setup.
local linkUrl="https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=$profile&ProductEditionId=undefined&SKU=$skuId&friendlyFileName=undefined&Locale=en-US&sessionID=$session" local linkUrl="https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=$profile&ProductEditionId=undefined&SKU=$skuId&friendlyFileName=undefined&Locale=en-US&sessionID=$session"
curlRequest linkJson "Microsoft" "$agent" \ linkJson=$(curlRequest "Microsoft" "$agent" \
--referer "$url" \ --referer "$url" \
--header "Accept:" \ --header "Accept:" \
--max-filesize 100K \ --max-filesize 100K \
-- "$linkUrl" || return 1 -- "$linkUrl") || return 1
if ! [ "$linkJson" ]; then if ! [ "$linkJson" ]; then
# This should only happen if there's been some change to how this API works
error "Microsoft servers gave us an empty response to our request for an automated download." error "Microsoft servers gave us an empty response to our request for an automated download."
return 1 return 1
fi fi
@@ -235,9 +232,8 @@ downloadWindows() {
local lang="$2" local lang="$2"
local desc="$3" local desc="$3"
local agent language local agent language page
local page productId local productId type winVer
local type winVer
agent=$(getAgent) agent=$(getAgent)
language=$(getLanguage "$lang" "name") language=$(getLanguage "$lang" "name")
@@ -246,22 +242,18 @@ downloadWindows() {
"win10x64" ) "win10x64" )
productId="2618" productId="2618"
winVer="10" winVer="10"
type="1" type="1" ;;
;;
"win11x64" ) "win11x64" )
productId="3321" productId="3321"
winVer="11" winVer="11"
type="1" type="1" ;;
;;
"win11arm64" ) "win11arm64" )
productId="3324" productId="3324"
winVer="11arm64" winVer="11arm64"
type="2" type="2" ;;
;;
* ) * )
error "Invalid VERSION specified, value \"$id\" is not recognized!" error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1 return 1 ;;
;;
esac esac
local url="https://www.microsoft.com/en-us/software-download/windows$winVer" local url="https://www.microsoft.com/en-us/software-download/windows$winVer"
@@ -275,14 +267,16 @@ downloadWindows() {
sleep 1 sleep 1
# Product edition IDs can change. If the configured ID fails, recover the
# current value from Microsoft's public download page and retry once.
local msg="retrying using a different method..." local msg="retrying using a different method..."
info "Microsoft download request failed, $msg" info "Microsoft download request failed, $msg"
enabled "$DEBUG" && echo "Parsing download page: ${url}" enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \ page=$(curlRequest "Microsoft" "$agent" \
--header "Accept:" \ --header "Accept:" \
--max-filesize 1M \ --max-filesize 1M \
-- "$url" || return 1 -- "$url") || return 1
enabled "$DEBUG" && echo -n "Getting Product edition ID: " enabled "$DEBUG" && echo -n "Getting Product edition ID: "
productId=$(printf '%s' "$page" | productId=$(printf '%s' "$page" |
@@ -310,6 +304,7 @@ downloadWindowsEval() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local desc="$3" local desc="$3"
local culture compare type local culture compare type
local agent language winVer local agent language winVer
@@ -354,13 +349,12 @@ downloadWindowsEval() {
enabled "$DEBUG" && echo "Parsing download page: ${url}" enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \ page=$(curlRequest "Microsoft" "$agent" \
--location \ --location \
--max-filesize 1M \ --max-filesize 1M \
-- "$url" || return 1 -- "$url") || return 1
if ! [ "$page" ]; then if ! [ "$page" ]; then
# This should only happen if there's been some change to where this download page is located
error "Windows server download page gave us an empty response" error "Windows server download page gave us an empty response"
return 1 return 1
fi fi
@@ -375,7 +369,8 @@ downloadWindowsEval() {
grep -Eio "https://go\.microsoft\.com/fwlink(/p)?/\?[^\"'<>[:space:]]+" | grep -Eio "https://go\.microsoft\.com/fwlink(/p)?/\?[^\"'<>[:space:]]+" |
grep -Ei '(^|[?&])culture='"${culture,,}"'(&|$)' | grep -Ei '(^|[?&])culture='"${culture,,}"'(&|$)' |
grep -Ei '(^|[?&])country='"${country,,}"'(&|$)') || { grep -Ei '(^|[?&])country='"${country,,}"'(&|$)') || {
# This should only happen if there's been some change to the download endpoint web address # Distinguish a changed or missing English page from an unavailable
# translation for an otherwise supported product.
if [[ "${lang,,}" == "en" || "${lang,,}" == "en-"* ]]; then if [[ "${lang,,}" == "en" || "${lang,,}" == "en-"* ]]; then
error "Windows server download page gave us no download link!" error "Windows server download page gave us no download link!"
else else
@@ -385,6 +380,8 @@ downloadWindowsEval() {
return 1 return 1
} }
# Evaluation pages currently expose several matching fwlinks in a known
# product/platform order, so select the entry for the requested variant.
case "$type" in case "$type" in
"iot" ) "iot" )
case "${PLATFORM,,}" in case "${PLATFORM,,}" in
@@ -420,19 +417,21 @@ downloadWindowsEval() {
[ -z "$link" ] && error "Could not parse download link from page!" && return 1 [ -z "$link" ] && error "Could not parse download link from page!" && return 1
# Follow redirect so proceeding log message is useful # Resolve the fwlink now so later logging and platform validation use the
# This is a request we make that Fido doesn't # actual ISO URL rather than Microsoft's generic redirect.
curlRequest link "Microsoft" "$agent" \ link=$(curlRequest "Microsoft" "$agent" \
--location \ --location \
--output /dev/null \ --output /dev/null \
--write-out "%{url_effective}" \ --write-out "%{url_effective}" \
--head \ --head \
-- "$link" || return 1 -- "$link") || return 1
local lower="${link,,}" local lower="${link,,}"
local separator='(^|[[:space:]_./-])' local separator='(^|[[:space:]_./-])'
# Guard against page-order changes resolving to the wrong architecture
# before downloading a multi-gigabyte image.
case "${PLATFORM,,}" in case "${PLATFORM,,}" in
"x64" ) "x64" )
if [[ "$lower" =~ ${separator}(arm64|a64) ]]; then if [[ "$lower" =~ ${separator}(arm64|a64) ]]; then
@@ -450,6 +449,8 @@ downloadWindowsEval() {
fi ;; fi ;;
esac esac
# During debug verification, compare the resolved filename with the static
# catalog entry to expose unexpected changes on Microsoft's page.
if enabled "$DEBUG" && enabled "$VERIFY" && [[ "${lang,,}" == "en"* ]]; then if enabled "$DEBUG" && enabled "$VERIFY" && [[ "${lang,,}" == "en"* ]]; then
compare=$(getMido "$id" "$lang" "") compare=$(getMido "$id" "$lang" "")
@@ -494,14 +495,11 @@ getMidoDetected() {
# Derive the normal answer-file identity from the requested download route. # Derive the normal answer-file identity from the requested download route.
case "$default" in case "$default" in
*"-enterprise-ltsc-eval" ) *"-enterprise-ltsc-eval" )
default="${default%-enterprise-ltsc-eval}-ltsc" default="${default%-enterprise-ltsc-eval}-ltsc" ;;
;;
*"-enterprise-iot-eval" ) *"-enterprise-iot-eval" )
default="${default%-enterprise-iot-eval}-iot" default="${default%-enterprise-iot-eval}-iot" ;;
;;
*"-eval" ) *"-eval" )
default="${default%-eval}" default="${default%-eval}" ;;
;;
esac esac
# Preserve a genuinely different DETECTED override. # Preserve a genuinely different DETECTED override.
@@ -513,17 +511,13 @@ getMidoDetected() {
# Select the answer-file identity for the source that actually succeeded. # Select the answer-file identity for the source that actually succeeded.
case "$source" in case "$source" in
*"-enterprise-ltsc-eval" ) *"-enterprise-ltsc-eval" )
detected="${source%-enterprise-ltsc-eval}-ltsc-eval" detected="${source%-enterprise-ltsc-eval}-ltsc-eval" ;;
;;
*"-enterprise-iot-eval" ) *"-enterprise-iot-eval" )
detected="${source%-enterprise-iot-eval}-iot-eval" detected="${source%-enterprise-iot-eval}-iot-eval" ;;
;;
*"-eval" ) *"-eval" )
detected="$source" detected="$source" ;;
;;
* ) * )
detected="${current:-$default}" detected="${current:-$default}" ;;
;;
esac esac
echo "$detected" echo "$detected"
@@ -535,21 +529,21 @@ downloadWindowsLtsc() {
local id="$1" local id="$1"
local lang="$2" local lang="$2"
local desc="$3" local desc="$3"
local alternate alternate_desc local alternate alternate_desc
case "${id,,}" in case "${id,,}" in
"win11${PLATFORM,,}-enterprise-iot-eval" ) "win11${PLATFORM,,}-enterprise-iot-eval" )
alternate="win11${PLATFORM,,}-enterprise-ltsc-eval" alternate="win11${PLATFORM,,}-enterprise-ltsc-eval" ;;
;;
"win11${PLATFORM,,}-enterprise-ltsc-eval" ) "win11${PLATFORM,,}-enterprise-ltsc-eval" )
alternate="win11${PLATFORM,,}-enterprise-iot-eval" alternate="win11${PLATFORM,,}-enterprise-iot-eval" ;;
;;
* ) * )
error "Invalid VERSION specified, value \"$id\" is not recognized!" error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1 return 1 ;;
;;
esac esac
# IoT and LTSC share related evaluation sources and may become unavailable
# independently, so use the sibling edition as a compatibility fallback.
if downloadWindowsEval "$id" "$lang" "$desc" > /dev/null 2>&1; then if downloadWindowsEval "$id" "$lang" "$desc" > /dev/null 2>&1; then
MIDO_SOURCE="$id" MIDO_SOURCE="$id"
return 0 return 0
@@ -574,6 +568,7 @@ getWindows() {
local lang="$2" local lang="$2"
local desc="$3" local desc="$3"
local web_desc="$4" local web_desc="$4"
local language edition local language edition
MIDO_SOURCE="" MIDO_SOURCE=""
@@ -586,6 +581,8 @@ getWindows() {
local web_msg="Requesting $web_desc from the Microsoft servers..." local web_msg="Requesting $web_desc from the Microsoft servers..."
info "$msg" && html "$web_msg" info "$msg" && html "$web_msg"
# These sources are only published in English, so avoid trying download
# routes that cannot satisfy the requested language.
case "${version,,}" in case "${version,,}" in
"win2008r2"* | \ "win2008r2"* | \
"win81${PLATFORM,,}"* | \ "win81${PLATFORM,,}"* | \
@@ -598,6 +595,8 @@ getWindows() {
fi ;; fi ;;
esac esac
# ARM64 downloads exist only for the explicitly supported Windows 11
# routes; all other catalog entries remain x64-only.
case "${version,,}" in case "${version,,}" in
"win10x64" ) ;; "win10x64" ) ;;
"win11${PLATFORM,,}" ) ;; "win11${PLATFORM,,}" ) ;;
@@ -610,6 +609,8 @@ getWindows() {
fi ;; fi ;;
esac esac
# Prefer live Microsoft download routes. Unsupported or failed live routes
# fall through to the configured static catalog below.
case "${version,,}" in case "${version,,}" in
"win10x64" | "win11${PLATFORM,,}" ) "win10x64" | "win11${PLATFORM,,}" )
@@ -621,8 +622,7 @@ getWindows() {
"win11${PLATFORM,,}-enterprise-iot-eval" | \ "win11${PLATFORM,,}-enterprise-iot-eval" | \
"win11${PLATFORM,,}-enterprise-ltsc-eval" ) "win11${PLATFORM,,}-enterprise-ltsc-eval" )
downloadWindowsLtsc "$version" "$lang" "$edition" && return 0 downloadWindowsLtsc "$version" "$lang" "$edition" && return 0 ;;
;;
"win11${PLATFORM,,}-enterprise"* ) "win11${PLATFORM,,}-enterprise"* )
@@ -643,10 +643,11 @@ getWindows() {
* ) * )
error "Invalid VERSION specified, value \"$version\" is not recognized!" error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1 return 1 ;;
;;
esac esac
# Static catalog URLs are the last resort after live Microsoft methods are
# unavailable or have failed.
MIDO_URL=$(getMido "$version" "$lang" "") MIDO_URL=$(getMido "$version" "$lang" "")
[ -z "$MIDO_URL" ] && return 1 [ -z "$MIDO_URL" ] && return 1
@@ -665,11 +666,10 @@ getBuild() {
local id="$1" local id="$1"
local ret="$2" local ret="$2"
local url=""
local name=""
local build="$3" local build="$3"
local edition=""
local file="catalog.xml" local file="catalog.xml"
local url="" name="" edition=""
case "${id,,}" in case "${id,,}" in
"win11${PLATFORM,,}" ) "win11${PLATFORM,,}" )
@@ -695,10 +695,9 @@ getCatalog() {
local id="$1" local id="$1"
local ret="$2" local ret="$2"
local url=""
local name=""
local edition=""
local file="catalog.cab" local file="catalog.cab"
local url="" name="" edition=""
if [[ "${id,,}" == "win11"* ]] && ! isCompatible; then if [[ "${id,,}" == "win11"* ]] && ! isCompatible; then
# ARMv8.0 cannot run Windows 11 builds 24H2 and up. # ARMv8.0 cannot run Windows 11 builds 24H2 and up.
@@ -728,19 +727,108 @@ getCatalog() {
"url" ) echo "$url" ;; "url" ) echo "$url" ;;
"file" ) echo "$file" ;; "file" ) echo "$file" ;;
"name" ) echo "$name" ;; "name" ) echo "$name" ;;
"edition" ) echo '[Edition="'"${edition}"'"]' ;; "edition" ) echo "$edition" ;;
*) echo "";; *) echo "";;
esac esac
return 0 return 0
} }
getXmlTag() { parseESD() {
local tag="$1" local xml="$1"
local file="$2" local version="$2"
local lang="$3"
local desc="$4"
local edition="$5"
local culture="$6"
xmllint --nonet --xpath "//$tag" "$file" 2>/dev/null | sed -E -e "s/<[\/]?$tag>//g" || true local xmlFile="${xml##*/}"
local file_path file_sum file_size file_edition
local file_culture file_match=0 language_match=0
local records architecture language separator=$'\x1f'
ESD=""
ESD_SUM=""
ESD_SIZE=""
# Microsoft catalogs have used different XML namespaces. Match elements by
# local name and flatten the catalog once so selection needs no temporary XML.
if ! records=$(xmlstarlet sel \
-T -t \
-m "//*[local-name()='File']" \
-v "normalize-space(*[local-name()='Architecture'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='Edition'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='LanguageCode'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='FilePath'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='Sha1'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='Size'])" \
-n \
"$xml" 2>/dev/null); then
error "Failed to parse $xmlFile!"
return 1
fi
# Track product/platform and language matches separately so failures can
# distinguish an unavailable edition from an unavailable translation.
while IFS="$separator" read -r \
architecture file_edition file_culture \
file_path file_sum file_size; do
[ -n "$architecture$file_path$file_sum$file_size$file_culture$file_edition" ] || continue
[ "${architecture,,}" = "${PLATFORM,,}" ] || continue
if [ -n "$edition" ] &&
[ "${file_edition,,}" != "${edition,,}" ]; then
continue
fi
file_match=1
[ "${file_culture,,}" = "${culture,,}" ] || continue
language_match=1
ESD="$file_path"
ESD_SUM="$file_sum"
ESD_SIZE="$file_size"
break
done <<< "$records"
if (( ! file_match )); then
desc=$(printEdition "$version" "$desc" "Y")
error "No download link available for $desc!"
return 1
fi
if (( ! language_match )); then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
return 1
fi
if [ -z "$ESD" ]; then
error "Failed to find ESD URL in $xmlFile!"
return 1
fi
if [ -z "$ESD_SUM" ]; then
error "Failed to find ESD checksum in $xmlFile!"
return 1
fi
if [ -z "$ESD_SIZE" ]; then
error "Failed to find ESD filesize in $xmlFile!"
return 1
fi
return 0 return 0
} }
@@ -751,12 +839,10 @@ getESD() {
local version="$2" local version="$2"
local lang="$3" local lang="$3"
local desc="$4" local desc="$4"
local file result culture
local language edition catalog local file culture log
local edition catalog rc=0
local xmlFile="products.xml" local xmlFile="products.xml"
local esdFile="esd_edition.xml"
local filterFile="products_filter.xml"
local log
file=$(getCatalog "$version" "file") file=$(getCatalog "$version" "file")
catalog=$(getCatalog "$version" "url") catalog=$(getCatalog "$version" "url")
@@ -783,10 +869,12 @@ getESD() {
return 1 return 1
fi fi
# Preserve wget's status under errexit so its log can provide the actual
# server or filesystem failure reason.
{ {
LC_ALL=C wget "$catalog" -O "$dir/$file" --no-verbose --timeout=30 \ LC_ALL=C wget "$catalog" -O "$dir/$file" --no-verbose --timeout=30 \
--no-http-keep-alive --output-file="$log" --no-http-keep-alive --output-file="$log"
local rc=$? rc=$?
} || : } || :
if (( rc != 0 )); then if (( rc != 0 )); then
@@ -813,6 +901,8 @@ getESD() {
rm -f "$log" rm -f "$log"
# Normal catalogs arrive as CAB archives, while pinned build catalogs are
# already XML and only need the common filename.
if [[ "$file" == *".xml" ]]; then if [[ "$file" == *".xml" ]]; then
if ! mv -f "$dir/$file" "$dir/$xmlFile"; then if ! mv -f "$dir/$file" "$dir/$xmlFile"; then
@@ -832,61 +922,13 @@ getESD() {
fi fi
if [ ! -f "$dir/$xmlFile" ] || [ ! -s "$dir/$xmlFile" ]; then if [ ! -s "$dir/$xmlFile" ]; then
error "Failed to find $xmlFile in $file!" error "Failed to find $xmlFile in $file!"
return 1 return 1
fi fi
local query='//File[Architecture="'${PLATFORM,,}'"]'"${edition}"'' if ! parseESD \
result=$(xmllint --nonet --xpath "${query}" "$dir/$xmlFile" 2>/dev/null || true) "$dir/$xmlFile" "$version" "$lang" "$desc" "$edition" "$culture"; then
if [ -z "$result" ]; then
query='//File[Architecture="'${PLATFORM^^}'"]'"${edition}"''
result=$(xmllint --nonet --xpath "${query}" "$dir/$xmlFile" 2>/dev/null || true)
if [ -z "$result" ]; then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download link available for $desc!"
return 1
fi
fi
echo -e '<Catalog>' > "$dir/$filterFile"
echo "$result" >> "$dir/$filterFile"
echo -e '</Catalog>'>> "$dir/$filterFile"
result=$(xmllint --nonet --xpath "//File[LanguageCode=\"${culture,,}\"]" "$dir/$filterFile" 2>/dev/null || true)
if [ -z "$result" ]; then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
return 1
fi
echo "$result" > "$dir/$esdFile"
ESD=$(getXmlTag "FilePath" "$dir/$esdFile")
if [ -z "$ESD" ]; then
error "Failed to find ESD URL in $esdFile!"
return 1
fi
ESD_SUM=$(getXmlTag "Sha1" "$dir/$esdFile")
if [ -z "$ESD_SUM" ]; then
error "Failed to find ESD checksum in $esdFile!"
return 1
fi
ESD_SIZE=$(getXmlTag "Size" "$dir/$esdFile")
if [ -z "$ESD_SIZE" ]; then
error "Failed to find ESD filesize in $esdFile!"
return 1 return 1
fi fi
@@ -898,11 +940,12 @@ isCompressed() {
local url="${1%%\?*}" local url="${1%%\?*}"
# The ReactOS latest-build endpoint returns an archive without a filename
# extension, so recognize its path explicitly.
case "${url,,}" in case "${url,,}" in
*.7z | *.zip | *.rar | *.tar | *.cab | *.cpio | \ *.7z | *.zip | *.rar | *.tar | *.cab | *.cpio | \
*.lzh | *.lha | *.xar | */latest-x86-gcc-lin-rel ) *.lzh | *.lha | *.xar | */latest-x86-gcc-lin-rel )
return 0 return 0 ;;
;;
esac esac
return 1 return 1
@@ -926,6 +969,9 @@ verifyFile() {
[ -z "$check" ] && return 0 [ -z "$check" ] && return 0
enabled "$VERIFY" || return 0 enabled "$VERIFY" || return 0
# Microsoft ESD catalogs publish SHA1, while current mirror metadata normally
# uses SHA256; the digest length identifies which algorithm is required.
[[ "${#check}" == "40" ]] && algo="SHA1" [[ "${#check}" == "40" ]] && algo="SHA1"
local msg="Verifying downloaded ISO..." local msg="Verifying downloaded ISO..."
@@ -975,10 +1021,13 @@ downloadFile() {
local desc="$4" local desc="$4"
local web_desc="$5" local web_desc="$5"
local connections="${6:-1}" local connections="${6:-1}"
local domain dots
local msg="Downloading $web_desc" local msg="Downloading $web_desc"
local console_msg="Downloading $desc" local console_msg="Downloading $desc"
local domain dots
# Keep mirror messages concise by reducing subdomains to the final two
# labels, while Microsoft downloads retain the generic description.
domain=$(echo "$url" | awk -F/ '{print $3}') domain=$(echo "$url" | awk -F/ '{print $3}')
dots=$(echo "$domain" | tr -cd '.' | wc -c) dots=$(echo "$domain" | tr -cd '.' | wc -c)
(( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)') (( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)')
@@ -1007,8 +1056,11 @@ tryDownload() {
local desc="$6" local desc="$6"
local seconds="$7" local seconds="$7"
local web_desc="$8" local web_desc="$8"
local total minimum="104857600" local total minimum="104857600"
# Compressed archives can legitimately be much smaller than the ISO they
# contain, so use a lower sanity threshold until extraction.
if isCompressed "$url"; then if isCompressed "$url"; then
minimum="10485760" minimum="10485760"
fi fi
@@ -1059,8 +1111,8 @@ fallbackEnglish() {
local lang="$3" local lang="$3"
local desc="$4" local desc="$4"
local web_desc="$5" local web_desc="$5"
local culture web_msg
local culture web_msg
local msg="No working download method was found for $desc, falling back to English..." local msg="No working download method was found for $desc, falling back to English..."
info "$msg" info "$msg"
@@ -1086,12 +1138,11 @@ downloadImage() {
local iso="$1" local iso="$1"
local version="$2" local version="$2"
local lang="$3" local lang="$3"
local requested="$version" local requested="$version"
local tried="n"
local success="n"
local seconds="5"
local detected="$DETECTED" local detected="$DETECTED"
local url sum size base desc web_desc language i local tried="n" success="n" seconds="5"
local i url sum size base language desc web_desc
if [[ "${version,,}" == "http"* ]]; then if [[ "${version,,}" == "http"* ]]; then
@@ -1127,6 +1178,8 @@ downloadImage() {
desc+=" in $language" desc+=" in $language"
fi fi
# Prefer a live Microsoft URL and retry link generation once before moving
# on to ESD catalogs or mirrors.
if isMido "$version" "$lang"; then if isMido "$version" "$lang"; then
tried="y" tried="y"
@@ -1167,7 +1220,13 @@ downloadImage() {
fi fi
fi fi
if switchEdition version; then # Some editions share another download route. Update the effective version
# before looking up ESD catalogs and mirrors.
if version=$(switchEdition "$version"); then
if ! enabled "${DETECTED_ORG:-}"; then
DETECTED="${SUGGEST:-$version}"
fi
desc=$(printVariant "$DETECTED" "" "Y") desc=$(printVariant "$DETECTED" "" "Y")
web_desc=$(printVariant "$DETECTED" "") web_desc=$(printVariant "$DETECTED" "")
@@ -1196,6 +1255,8 @@ downloadImage() {
if [[ "$success" == "y" ]]; then if [[ "$success" == "y" ]]; then
# Standalone ESD media requires a different extraction path, so expose
# its real extension through the active ISO variable.
ISO="${ISO%.*}.esd" ISO="${ISO%.*}.esd"
if tryDownload "$ISO" "$ESD" "$ESD_SUM" "$ESD_SIZE" "$lang" "$desc" "$seconds" "$web_desc"; then if tryDownload "$ISO" "$ESD" "$ESD_SUM" "$ESD_SIZE" "$lang" "$desc" "$seconds" "$web_desc"; then
+43 -1
View File
@@ -23,6 +23,8 @@ bootStatus() {
if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then
local line last recent local line last recent
# Only inspect output produced after the most recent BIOS boot attempt so
# stale failures from an earlier device do not affect the current state.
line=$(grep -nE '^Booting from (Hard Disk|DVD/CD)' "$QEMU_PTY" | tail -1) line=$(grep -nE '^Booting from (Hard Disk|DVD/CD)' "$QEMU_PTY" | tail -1)
[ -z "$line" ] && return 1 [ -z "$line" ] && return 1
@@ -38,6 +40,9 @@ bootStatus() {
return 2 return 2
fi fi
# These BIOS messages only describe the failed device attempt. QEMU may
# immediately continue with another boot target, so clear pending success
# instead of treating them as a terminal failure.
if grep -Fq \ if grep -Fq \
-e "Boot failed: not a bootable disk" \ -e "Boot failed: not a bootable disk" \
-e "Boot failed: Could not read from CDROM" \ -e "Boot failed: Could not read from CDROM" \
@@ -59,6 +64,8 @@ bootStatus() {
local line last recent local line last recent
# OVMF logs every boot option it tries. Track the newest attempt and only
# evaluate messages emitted from that point onward.
line=$(grep -nE \ line=$(grep -nE \
'BdsDxe: starting Boot[[:xdigit:]]{4} ' \ 'BdsDxe: starting Boot[[:xdigit:]]{4} ' \
"$QEMU_PTY" | tail -1) "$QEMU_PTY" | tail -1)
@@ -105,6 +112,8 @@ waitForBoot() {
while isAlive "$pid"; do while isAlive "$pid"; do
# Send the boot key once, either immediately after the prompt appears or
# shortly after firmware starts the DVD when the prompt is not logged.
if (( ! keySent )) && needsBootKey; then if (( ! keySent )) && needsBootKey; then
if keyDelay=$(bootKeyDelay); then if keyDelay=$(bootKeyDelay); then
@@ -151,6 +160,9 @@ waitForBoot() {
marker=$(getBootMarker) marker=$(getBootMarker)
# A firmware boot line alone is not proof that the guest started. Wait
# briefly for a more definitive success or failure message, restarting
# the grace period whenever firmware begins a different boot attempt.
if [[ "$marker" != "$pendingLine" ]] || (( status != pendingType )); then if [[ "$marker" != "$pendingLine" ]] || (( status != pendingType )); then
pendingLine="$marker" pendingLine="$marker"
pendingType=$status pendingType=$status
@@ -173,6 +185,8 @@ waitForBoot() {
5) 5)
# A failed device attempt is transitional because firmware may continue
# with another target. Discard any pending success decision.
pendingType=0 pendingType=0
pendingLine="" pendingLine=""
pendingDeadline=0 pendingDeadline=0
@@ -213,6 +227,8 @@ legacyBootReady() {
last="${line#*:}" last="${line#*:}"
recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") recent=$(tail -n +"${line%%:*}" "$QEMU_PTY")
# ACPI shutdown is safe once BIOS has handed control to the hard disk, unless
# the same attempt already produced a known boot failure.
[[ "${last,,}" != "${hard,,}"* ]] && return 1 [[ "${last,,}" != "${hard,,}"* ]] && return 1
grep -Fq "Loading FreeLoader..." <<< "$recent" && return 0 grep -Fq "Loading FreeLoader..." <<< "$recent" && return 0
@@ -226,6 +242,8 @@ legacyBootReady() {
ready() { ready() {
# The marker means installation completed previously, so shutdown no longer
# needs to infer guest readiness from firmware output.
[ -f "$STORAGE/windows.boot" ] && return 0 [ -f "$STORAGE/windows.boot" ] && return 0
[ ! -s "$QEMU_PTY" ] && return 1 [ ! -s "$QEMU_PTY" ] && return 1
@@ -239,6 +257,8 @@ ready() {
'BdsDxe: starting Boot[[:xdigit:]]{4} ' \ 'BdsDxe: starting Boot[[:xdigit:]]{4} ' \
"$QEMU_PTY" | tail -1) "$QEMU_PTY" | tail -1)
# Only a Windows Boot Manager entry loaded from a hard disk proves that setup
# has progressed far enough for an ACPI shutdown request to be appropriate.
grep -Eq \ grep -Eq \
'BdsDxe: starting Boot[[:xdigit:]]{4} "Windows Boot Manager" from .*HD\(' \ 'BdsDxe: starting Boot[[:xdigit:]]{4} "Windows Boot Manager" from .*HD\(' \
<<< "$last" && return 0 <<< "$last" && return 0
@@ -258,6 +278,8 @@ sendKey() {
[ ! -S "$ACPI_SOCKET" ] && return 1 [ ! -S "$ACPI_SOCKET" ] && return 1
[[ "$delay" != "0" ]] && sleep "$delay" [[ "$delay" != "0" ]] && sleep "$delay"
# Send all repeats through one monitor connection so timing remains stable
# and QEMU receives the sequence as one operation.
if ! output=$( if ! output=$(
{ {
for ((i = 1; i <= repeat; i++)); do for ((i = 1; i <= repeat; i++)); do
@@ -272,6 +294,8 @@ sendKey() {
return 1 return 1
fi fi
# The human monitor may return success at the transport level while reporting
# a command error in its text response, so inspect that output explicitly.
if grep -Eqi \ if grep -Eqi \
-e 'unknown command' \ -e 'unknown command' \
-e 'unknown key' \ -e 'unknown key' \
@@ -312,11 +336,14 @@ bootKeyDelay() {
[ ! -s "$QEMU_PTY" ] && return 1 [ ! -s "$QEMU_PTY" ] && return 1
# A visible prompt is the safest trigger and should be answered immediately.
if grep -Fq "Press any key to" "$QEMU_PTY"; then if grep -Fq "Press any key to" "$QEMU_PTY"; then
echo 0 echo 0
return 0 return 0
fi fi
# Some firmware or Windows versions do not log the prompt. In that case wait
# briefly after the DVD boot attempt and send several short key presses.
if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then
grep -Fq "Booting from DVD/CD" "$QEMU_PTY" || return 1 grep -Fq "Booting from DVD/CD" "$QEMU_PTY" || return 1
else else
@@ -351,7 +378,8 @@ markWindowsBooted() {
return 0 return 0
fi fi
# Remove CD-ROM ISO after install # Do not remove installation media until firmware output confirms Windows is
# now booting from the installed disk rather than from setup media.
ready || return 0 ready || return 0
if ! touch "$file"; then if ! touch "$file"; then
@@ -376,6 +404,8 @@ finish() {
local reason=$1 failed=0 local reason=$1 failed=0
# QEMU_END distinguishes an expected shutdown path from an unexpected QEMU
# exit carrying the same process status.
if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then
failed=1 failed=1
fi fi
@@ -412,6 +442,8 @@ abortDuringSetup() {
local code="$1" local code="$1"
# Before Windows boots from disk, ACPI may be ignored or interpreted by setup
# itself. Terminate QEMU directly instead of waiting for a graceful shutdown.
if [[ "${DETECTED,,}" != "reactos" ]] || [ -n "${CUSTOM:-}" ]; then if [[ "${DETECTED,,}" != "reactos" ]] || [ -n "${CUSTOM:-}" ]; then
info "Cannot send ACPI signal during $(app) setup, aborting..." info "Cannot send ACPI signal during $(app) setup, aborting..."
else else
@@ -432,12 +464,16 @@ gracefulShutdown() {
local sig="$1" local sig="$1"
local pid code local pid code
# Traps can run in subshells created by pipelines or command substitutions;
# only the original shell may coordinate QEMU shutdown.
[[ $BASHPID != "$TRAP_PID" ]] && return [[ $BASHPID != "$TRAP_PID" ]] && return
code=$(signalCode "$sig") code=$(signalCode "$sig")
if [ -f "$QEMU_END" ]; then if [ -f "$QEMU_END" ]; then
# A second Ctrl-C during an active shutdown skips the remaining grace period
# and lets the shutdown loop force QEMU down immediately.
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
SHUTDOWN_SKIP=1 SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..." echo && info "Received SIGINT again, forcing shutdown..."
@@ -448,12 +484,16 @@ gracefulShutdown() {
return return
fi fi
# Signal handlers must complete their own error handling and cleanup without
# errexit terminating the shell partway through the shutdown sequence.
set +e set +e
SHUTDOWN_SIGNAL=$code SHUTDOWN_SIGNAL=$code
touch "$QEMU_END" touch "$QEMU_END"
echo && info "Received $sig signal, sending ACPI shutdown signal..." echo && info "Received $sig signal, sending ACPI shutdown signal..."
# Interactive startup may receive a signal before the PID file appears, so
# briefly wait for it there; non-interactive operation fails immediately.
if ! readQemuPid pid; then if ! readQemuPid pid; then
if ! interactive || ! waitQemuPid pid; then if ! interactive || ! waitQemuPid pid; then
warn "QEMU PID file does not exist?" warn "QEMU PID file does not exist?"
@@ -479,6 +519,8 @@ gracefulShutdown() {
enabled "$SHUTDOWN" || return 0 enabled "$SHUTDOWN" || return 0
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT" [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
# Keep Ctrl-C available to interactive users without installing an unnecessary
# SIGINT handler for background/container execution.
if interactive; then if interactive; then
_trap gracefulShutdown SIGINT _trap gracefulShutdown SIGINT
fi fi
+53 -24
View File
@@ -22,29 +22,32 @@ configureNetwork() {
if enabled "$DHCP"; then if enabled "$DHCP"; then
hostname="$UPLINK" SAMBA_HOSTNAME="$UPLINK"
interfaces="$DEV" SAMBA_INTERFACES="$DEV"
else else
hostname="host.lan" SAMBA_HOSTNAME="host.lan"
# User-mode networking has no host bridge to bind to, so expose Samba only
# through loopback and let QEMU's forwarding provide guest access.
if isUserMode; then if isUserMode; then
interfaces="lo" SAMBA_INTERFACES="lo"
else else
interfaces="$BRIDGE" SAMBA_INTERFACES="$BRIDGE"
fi fi
if [ -n "${SAMBA_INTERFACE:-}" ]; then if [ -n "${SAMBA_INTERFACE:-}" ]; then
interfaces+=",$SAMBA_INTERFACE" SAMBA_INTERFACES+=",$SAMBA_INTERFACE"
fi fi
fi fi
netbios="${hostname%%.*}" # NetBIOS names are limited to 15 visible characters.
netbios="${netbios:0:15}" SAMBA_NETBIOS="${SAMBA_HOSTNAME%%.*}"
SAMBA_NETBIOS="${SAMBA_NETBIOS:0:15}"
[ -z "$netbios" ] && netbios="host" [ -z "$SAMBA_NETBIOS" ] && SAMBA_NETBIOS="host"
return 0 return 0
} }
@@ -110,6 +113,8 @@ addShare() {
empty="Y" empty="Y"
fi fi
# The generated fallback share contains only instructions and must never be
# writable from the guest.
if [[ "$dir" == "$tmp" ]]; then if [[ "$dir" == "$tmp" ]]; then
readonly="Y" readonly="Y"
@@ -118,6 +123,8 @@ addShare() {
readonly="Y" readonly="Y"
# Test actual write access instead of relying on mount flags or mode bits,
# which may not reflect bind-mount and host filesystem restrictions.
elif probe=$(mktemp "$dir/.samba-write-test.XXXXXX" 2>/dev/null); then elif probe=$(mktemp "$dir/.samba-write-test.XXXXXX" 2>/dev/null); then
writable="Y" writable="Y"
@@ -127,6 +134,8 @@ addShare() {
return 1 return 1
fi fi
# Empty bind mounts are safe to initialize with shared-directory permissions.
# Retry the write probe afterward because the original mode may have blocked it.
elif [[ "$empty" == "Y" ]] && chmod 2777 "$dir" 2>/dev/null; then elif [[ "$empty" == "Y" ]] && chmod 2777 "$dir" 2>/dev/null; then
if probe=$(mktemp "$dir/.samba-write-test.XXXXXX" 2>/dev/null); then if probe=$(mktemp "$dir/.samba-write-test.XXXXXX" 2>/dev/null); then
@@ -146,6 +155,7 @@ addShare() {
if [[ "$empty" == "Y" ]]; then if [[ "$empty" == "Y" ]]; then
# Keep newly created content in the shared group through the setgid bit.
if ! chmod 2777 "$dir"; then if ! chmod 2777 "$dir"; then
error "Failed to set permissions for directory $dir" && return 1 error "Failed to set permissions for directory $dir" && return 1
fi fi
@@ -155,6 +165,8 @@ addShare() {
return 1 return 1
fi fi
# Docker commonly creates a missing bind source as root. Transfer an empty
# directory to the default non-root owner used for shared content.
if [[ "$owner" == "0" ]]; then if [[ "$owner" == "0" ]]; then
if ! chown "1000:1000" "$dir"; then if ! chown "1000:1000" "$dir"; then
error "Failed to set ownership for directory $dir" && return 1 error "Failed to set ownership for directory $dir" && return 1
@@ -165,6 +177,7 @@ addShare() {
elif [[ "$readonly" != "Y" ]]; then elif [[ "$readonly" != "Y" ]]; then
# Preserve access to non-writable mounts by exporting them read-only.
readonly="Y" readonly="Y"
fi fi
@@ -200,20 +213,27 @@ writeConfig() {
if ! { if ! {
echo "[global]" echo "[global]"
echo " server string = Dockur" echo " server string = Dockur"
echo " netbios name = $netbios" echo " netbios name = $SAMBA_NETBIOS"
echo " workgroup = WORKGROUP" echo " workgroup = WORKGROUP"
echo " interfaces = $interfaces" echo " interfaces = $SAMBA_INTERFACES"
echo " bind interfaces only = yes" echo " bind interfaces only = yes"
echo " security = user" echo " security = user"
echo " guest account = nobody" echo " guest account = nobody"
echo " map to guest = Bad User" echo " map to guest = Bad User"
# Retain SMB1 negotiation for legacy Windows guests.
echo " server min protocol = NT1" echo " server min protocol = NT1"
# Allow bind-mounted shares to follow symlinks outside their share root.
echo " follow symlinks = yes" echo " follow symlinks = yes"
echo " wide links = yes" echo " wide links = yes"
echo " unix extensions = no" echo " unix extensions = no"
echo " inherit owner = yes" echo " inherit owner = yes"
echo " create mask = 0666" echo " create mask = 0666"
echo " directory mask = 02777" echo " directory mask = 02777"
# Perform guest filesystem access as root so bind mounts with differing host
# ownership remain usable; share-level read-only checks still apply.
echo " force user = root" echo " force user = root"
echo " force group = root" echo " force group = root"
echo " force create mode = 0666" echo " force create mode = 0666"
@@ -241,11 +261,13 @@ selectPrimaryShare() {
return 1 return 1
fi fi
share="/shared" # Prefer explicit root-level bind mounts, then storage-local compatibility
[ ! -d "$share" ] && [ -d "$STORAGE/shared" ] && share="$STORAGE/shared" # paths. When none exist, publish an instructional read-only share.
[ ! -d "$share" ] && [ -d "/data" ] && share="/data" SAMBA_SHARE="/shared"
[ ! -d "$share" ] && [ -d "$STORAGE/data" ] && share="$STORAGE/data" [ ! -d "$SAMBA_SHARE" ] && [ -d "$STORAGE/shared" ] && SAMBA_SHARE="$STORAGE/shared"
[ ! -d "$share" ] && share="$tmp" [ ! -d "$SAMBA_SHARE" ] && [ -d "/data" ] && SAMBA_SHARE="/data"
[ ! -d "$SAMBA_SHARE" ] && [ -d "$STORAGE/data" ] && SAMBA_SHARE="$STORAGE/data"
[ ! -d "$SAMBA_SHARE" ] && SAMBA_SHARE="$tmp"
return 0 return 0
} }
@@ -256,6 +278,8 @@ addOptionalShare() {
local ref="/shared$index" local ref="/shared$index"
local name="Data$index" local name="Data$index"
# Optional shares are best-effort and must not prevent the primary share or
# Samba service from starting.
if [ -d "$ref" ]; then if [ -d "$ref" ]; then
addShare "$ref" "$ref" "$name" "Shared" "$SAMBA_CONFIG" || : addShare "$ref" "$ref" "$name" "Shared" "$SAMBA_CONFIG" || :
elif [ -d "/data$index" ]; then elif [ -d "/data$index" ]; then
@@ -267,13 +291,13 @@ addOptionalShare() {
prepareSambaDirs() { prepareSambaDirs() {
# Create directories if missing
mkdir -p \ mkdir -p \
/var/lib/samba/sysvol \ /var/lib/samba/sysvol \
/var/lib/samba/private \ /var/lib/samba/private \
/var/lib/samba/bind-dns || return 1 /var/lib/samba/bind-dns || return 1
# Try to repair Samba permissions # Runtime directories may retain restrictive modes from earlier daemon runs
# or package defaults, so repair only the known Samba lock and core paths.
[ -d /run/samba/msg.lock ] && chmod -R 0755 /run/samba/msg.lock 2>/dev/null || : [ -d /run/samba/msg.lock ] && chmod -R 0755 /run/samba/msg.lock 2>/dev/null || :
[ -d /var/log/samba/cores ] && chmod -R 0700 /var/log/samba/cores 2>/dev/null || : [ -d /var/log/samba/cores ] && chmod -R 0700 /var/log/samba/cores 2>/dev/null || :
[ -d /var/cache/samba/msg.lock ] && chmod -R 0755 /var/cache/samba/msg.lock 2>/dev/null || : [ -d /var/cache/samba/msg.lock ] && chmod -R 0755 /var/cache/samba/msg.lock 2>/dev/null || :
@@ -300,6 +324,8 @@ startDaemon() {
rm -f "$log" || : rm -f "$log" || :
# Keep initialization alive after a daemon startup failure so its log can be
# streamed and the actual Samba error remains visible to the user.
if ! "$@"; then if ! "$@"; then
SAMBA_DEBUG="Y" SAMBA_DEBUG="Y"
error "Failed to start $name daemon!" error "Failed to start $name daemon!"
@@ -319,7 +345,6 @@ startSamba() {
startNetbios() { startNetbios() {
# Enable NetBIOS on Windows 7 and lower
enabled "$DEBUG" && echo "Starting NetBIOS daemon..." enabled "$DEBUG" && echo "Starting NetBIOS daemon..."
startDaemon "NetBIOS" "/var/log/samba/log.nmbd" \ startDaemon "NetBIOS" "/var/log/samba/log.nmbd" \
@@ -330,11 +355,12 @@ startNetbios() {
startWsddn() { startWsddn() {
# Enable Web Service Discovery on Vista and up
enabled "$DEBUG" && echo "Starting wsddn daemon..." enabled "$DEBUG" && echo "Starting wsddn daemon..."
# wsddn accepts one interface, while Samba may bind to an additional
# user-supplied interface as well.
startDaemon "wsddn" "/var/log/wsddn.log" \ startDaemon "wsddn" "/var/log/wsddn.log" \
wsddn -i "${interfaces%%,*}" -H "$hostname" \ wsddn -i "${SAMBA_INTERFACES%%,*}" -H "$SAMBA_HOSTNAME" \
--unixd --log-file=/var/log/wsddn.log --pid-file="$DDN_PID" --unixd --log-file=/var/log/wsddn.log --pid-file="$DDN_PID"
return 0 return 0
@@ -346,19 +372,22 @@ html "Initializing shared folder..."
enabled "$DEBUG" && echo "Starting Samba daemon..." enabled "$DEBUG" && echo "Starting Samba daemon..."
writeConfig || return 0 writeConfig || return 0
# Add shared folders
selectPrimaryShare || return 0 selectPrimaryShare || return 0
addShare "$share" "/shared" "Data" "Shared" "$SAMBA_CONFIG" || return 0 addShare "$SAMBA_SHARE" "/shared" "Data" "Shared" "$SAMBA_CONFIG" || return 0
addOptionalShare "2" || : addOptionalShare "2" || :
addOptionalShare "3" || : addOptionalShare "3" || :
prepareSambaDirs || return 0 prepareSambaDirs || return 0
startSamba || return 0 startSamba || return 0
# User-mode networking does not expose a LAN interface where discovery
# broadcasts would be useful.
isUserMode && return 0 isUserMode && return 0
# Older Windows versions discover shares through NetBIOS, while modern Windows
# uses Web Services Discovery.
if [[ "${BOOT_MODE:-}" == "windows_legacy" ]]; then if [[ "${BOOT_MODE:-}" == "windows_legacy" ]]; then
startNetbios || : startNetbios || :
else else