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
+893 -477
View File
File diff suppressed because it is too large Load Diff
+144 -196
View File
@@ -47,116 +47,79 @@ parseVersion() {
case "${VERSION,,}" in
"11" | "11p" | "win11" | "pro11" | "win11p" | "windows11" | "windows 11" )
VERSION="win11x64"
;;
VERSION="win11x64" ;;
"11e" | "win11e" | "windows11e" | "windows 11e" )
VERSION="win11x64-enterprise-eval"
;;
VERSION="win11x64-enterprise-eval" ;;
"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" )
VERSION="win11x64-enterprise-iot-eval"
;;
VERSION="win11x64-enterprise-iot-eval" ;;
"10" | "10p" | "win10" | "pro10" | "win10p" | "windows10" | "windows 10" )
VERSION="win10x64"
;;
VERSION="win10x64" ;;
"10e" | "win10e" | "windows10e" | "windows 10e" )
VERSION="win10x64-enterprise-eval"
;;
VERSION="win10x64-enterprise-eval" ;;
"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" )
VERSION="win10x64-enterprise-iot-eval"
;;
VERSION="win10x64-enterprise-iot-eval" ;;
"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" )
VERSION="win81x64-enterprise-eval"
;;
VERSION="win81x64-enterprise-eval" ;;
"7" | "win7" | "windows7" | "windows 7" )
VERSION="win7x64"
;;
VERSION="win7x64" ;;
"7u" | "win7u" | "windows7u" | "windows 7u" )
VERSION="win7x64-ultimate"
;;
VERSION="win7x64-ultimate" ;;
"7e" | "win7e" | "windows7e" | "windows 7e" )
VERSION="win7x64-enterprise"
;;
VERSION="win7x64-enterprise" ;;
"7x86" | "win7x86" | "win732" | "windows7x86" )
VERSION="win7x86"
;;
VERSION="win7x86" ;;
"7ux86" | "7u32" | "win7x86-ultimate" )
VERSION="win7x86-ultimate"
;;
VERSION="win7x86-ultimate" ;;
"7ex86" | "7e32" | "win7x86-enterprise" )
VERSION="win7x86-enterprise"
;;
VERSION="win7x86-enterprise" ;;
"vista" | "vs" | "6" | "winvista" | "windowsvista" | "windows vista" )
VERSION="winvistax64"
;;
VERSION="winvistax64" ;;
"vistu" | "vu" | "6u" | "winvistu" )
VERSION="winvistax64-ultimate"
;;
VERSION="winvistax64-ultimate" ;;
"viste" | "ve" | "6e" | "winviste" )
VERSION="winvistax64-enterprise"
;;
VERSION="winvistax64-enterprise" ;;
"vistax86" | "vista32" | "6x86" | "winvistax86" | "windowsvistax86" )
VERSION="winvistax86"
;;
VERSION="winvistax86" ;;
"vux86" | "vu32" | "winvistax86-ultimate" )
VERSION="winvistax86-ultimate"
;;
VERSION="winvistax86-ultimate" ;;
"vex86" | "ve32" | "winvistax86-enterprise" )
VERSION="winvistax86-enterprise"
;;
VERSION="winvistax86-enterprise" ;;
"xp" | "xp32" | "xpx86" | "5" | "5x86" | "winxp" | "winxp86" | "windowsxp" | "windows xp" )
VERSION="winxpx86"
;;
VERSION="winxpx86" ;;
"xp64" | "xpx64" | "5x64" | "winxp64" | "winxpx64" | "windowsxp64" | "windowsxpx64" )
VERSION="winxpx64"
;;
VERSION="winxpx64" ;;
"2k" | "2000" | "win2k" | "win2000" | "windows2k" | "windows2000" )
VERSION="win2kx86"
;;
VERSION="win2kx86" ;;
"25" | "2025" | "win25" | "win2025" | "windows2025" | "windows 2025" )
VERSION="win2025-eval"
;;
VERSION="win2025-eval" ;;
"22" | "2022" | "win22" | "win2022" | "windows2022" | "windows 2022" )
VERSION="win2022-eval"
;;
VERSION="win2022-eval" ;;
"19" | "2019" | "win19" | "win2019" | "windows2019" | "windows 2019" )
VERSION="win2019-eval"
;;
VERSION="win2019-eval" ;;
"16" | "2016" | "win16" | "win2016" | "windows2016" | "windows 2016" )
VERSION="win2016-eval"
;;
VERSION="win2016-eval" ;;
"hv" | "hyperv" | "hyper v" | "hyper-v" | "19hv" | "2019hv" | "win2019hv" )
VERSION="win2019-hv"
;;
VERSION="win2019-hv" ;;
"2012" | "2012r2" | "win2012" | "win2012r2" | "windows2012" | "windows 2012" )
VERSION="win2012r2-eval"
;;
VERSION="win2012r2-eval" ;;
"2008" | "2008r2" | "win2008" | "win2008r2" | "windows2008" | "windows 2008" )
VERSION="win2008r2"
;;
VERSION="win2008r2" ;;
"2003" | "2003r2" | "win2003" | "win2003r2" | "windows2003" | "windows 2003" )
VERSION="win2003r2"
;;
VERSION="win2003r2" ;;
"core11" | "core 11" )
VERSION="core11"
;;
VERSION="core11" ;;
"tiny11" | "tiny 11" )
VERSION="tiny11"
;;
VERSION="tiny11" ;;
"tiny10" | "tiny 10" )
VERSION="tiny10"
;;
VERSION="tiny10" ;;
"reactos" | "react os" )
VERSION="reactos"
;;
VERSION="reactos" ;;
esac
SUGGEST=$(getSuggestedVersion "$VERSION")
@@ -172,29 +135,21 @@ getSuggestedVersion() {
case "$id" in
"win10x64" | "win11x64" )
echo "$id"
;;
echo "$id" ;;
"win7x64" | "win7x86" | "winvistax64" | "winvistax86" )
echo "$id-ultimate"
;;
echo "$id-ultimate" ;;
"tiny10" )
echo "win10x64-ltsc"
;;
echo "win10x64-ltsc" ;;
*"-enterprise-ltsc-eval" )
echo "${id%-enterprise-ltsc-eval}-ltsc"
;;
echo "${id%-enterprise-ltsc-eval}-ltsc" ;;
*"-enterprise-iot-eval" )
echo "${id%-enterprise-iot-eval}-iot"
;;
echo "${id%-enterprise-iot-eval}-iot" ;;
*"-enterprise-ltsc" )
echo "${id%-enterprise-ltsc}-ltsc"
;;
echo "${id%-enterprise-ltsc}-ltsc" ;;
*"-enterprise-iot" )
echo "${id%-enterprise-iot}-iot"
;;
echo "${id%-enterprise-iot}-iot" ;;
*"-eval" )
echo "${id%-eval}"
;;
echo "${id%-eval}" ;;
esac
return 0
@@ -205,11 +160,8 @@ getLanguage() {
local source="$1"
local input="${1,,}"
local ret="$2"
local id="$source"
local lang=""
local desc=""
local short=""
local culture=""
local id="$source" lang="" desc="" short="" culture=""
case "$input" in
"ar" | "ar-"* | "arabic" | "arab" )
@@ -496,14 +448,11 @@ printVariant() {
case "${id,,}" in
*"-iot" | *"-iot-eval" )
desc+=" IoT"
;;
desc+=" IoT" ;;
*"-ltsc" | *"-ltsc-eval" )
desc+=" LTSC"
;;
desc+=" LTSC" ;;
*"-enterprise" | *"-enterprise-eval" )
desc+=" Enterprise"
;;
desc+=" Enterprise" ;;
esac
if enabled "$show_eval" && [[ "${id,,}" == *"-eval" ]]; then
@@ -540,6 +489,7 @@ printEdition() {
local id="$1"
local desc="$2"
local show_eval="${3:-N}"
local normalized="${id,,}"
local result edition="" suffix=""
@@ -561,20 +511,15 @@ printEdition() {
esac
;;
"home" )
edition="Home"
;;
edition="Home" ;;
"starter" )
edition="Starter"
;;
edition="Starter" ;;
"ultimate" )
edition="Ultimate"
;;
edition="Ultimate" ;;
"enterprise" )
edition="Enterprise"
;;
edition="Enterprise" ;;
"education" )
edition="Education"
;;
edition="Education" ;;
"n" )
case "$normalized" in
"win7"* ) edition="Professional N" ;;
@@ -582,22 +527,18 @@ printEdition() {
esac
;;
"iot" | "enterprise-iot" )
edition="IoT Enterprise LTSC"
;;
edition="IoT Enterprise LTSC" ;;
"ltsc" | "enterprise-ltsc" )
edition="Enterprise LTSC"
;;
edition="Enterprise LTSC" ;;
* )
edition=$(formatEdition "$suffix")
;;
esac
;;
"winxp"* )
edition="Professional"
;;
edition="Professional" ;;
"win2019-hv"* )
edition="2019"
;;
edition="2019" ;;
"win20"* )
[[ "$normalized" == *"-"* ]] && suffix="${normalized#*-}"
@@ -625,10 +566,10 @@ printEdition() {
fromFile() {
local id=""
local desc="$1"
local file="${1,,}"
local arch="${PLATFORM,,}"
local id="" arch="${PLATFORM,,}"
file="${file//-/_}"
file="${file// /_}"
@@ -694,10 +635,11 @@ fromFile() {
fromName() {
local id=""
local name="$1"
local arch="$2"
local id=""
local add=""
[[ "$arch" != "x64" ]] && add="$arch"
@@ -723,9 +665,23 @@ fromName() {
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() {
local source="${1,,}"
local edition
source="${source//evaluation/}"
@@ -745,7 +701,7 @@ normalizeEdition() {
normalizeEditionID() {
local edition
local edition base
local id="$2"
edition=$(normalizeEdition "$1")
@@ -753,8 +709,21 @@ normalizeEditionID() {
case "$edition" in
"pro" | "professional" | "business" )
edition="" ;;
"pro-n" | "pron" | "professional-n" | "professionaln" )
"pro-n" | "pron" | "professional-n" | "professionaln" | "business-n" | "businessn" )
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
case "${id,,}" in
@@ -778,6 +747,7 @@ getEditionID() {
local name="${1,,}"
local id="${2,,}"
local edition
case "$id" in
@@ -813,26 +783,19 @@ normalizeServerEdition() {
case "$edition" in
"core" | "core-installation" | "server-core-installation" )
edition="standard-core"
;;
edition="standard-core" ;;
"desktop-experience" | "server-with-a-gui" | "full-installation" )
edition="standard"
;;
edition="standard" ;;
*"-server-core-installation" )
edition="${edition%-server-core-installation}-core"
;;
edition="${edition%-server-core-installation}-core" ;;
*"-core-installation" )
edition="${edition%-core-installation}-core"
;;
edition="${edition%-core-installation}-core" ;;
*"-desktop-experience" )
edition="${edition%-desktop-experience}"
;;
edition="${edition%-desktop-experience}" ;;
*"-server-with-a-gui" )
edition="${edition%-server-with-a-gui}"
;;
edition="${edition%-server-with-a-gui}" ;;
*"-full-installation" )
edition="${edition%-full-installation}"
;;
edition="${edition%-full-installation}" ;;
esac
edition="${edition#server-}"
@@ -879,6 +842,7 @@ getServerEditionID() {
local name="${1,,}"
local id="${2,,}"
local edition
case "$id" in
@@ -901,42 +865,36 @@ getServerEditionID() {
getEditionOrder() {
local id="${1,,}"
local result_name="$2"
local -n result="$result_name"
result=()
case "$id" in
"win20"* )
result=(
"|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-*"
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-*"
)
;;
* )
result=(
"-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-*"
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
@@ -945,9 +903,10 @@ getEditionOrder() {
getVersion() {
local id edition
local name="$1"
local arch="$2"
local id edition
local evaluation=""
id=$(fromName "$name" "$arch")
@@ -1002,16 +961,11 @@ isLegacy() {
switchEdition() {
local -n id="$1"
local version="$1"
[[ "${id,,}" == *"-eval" ]] || return 1
id="${id::-5}"
if ! enabled "${DETECTED_ORG:-}"; then
DETECTED="${SUGGEST:-$id}"
fi
[[ "${version,,}" == *"-eval" ]] || return 1
echo "${version::-5}"
return 0
}
@@ -1020,9 +974,8 @@ getMido() {
local id="$1"
local lang="$2"
local ret="$3"
local url=""
local sum=""
local size=""
local url="" sum="" size=""
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1119,9 +1072,8 @@ getLink1() {
local id="$1"
local lang="$2"
local ret="$3"
local url=""
local sum=""
local size=""
local url="" sum="" size=""
local host="https://dl.bobpony.com/windows"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1258,9 +1210,8 @@ getLink2() {
local id="$1"
local lang="$2"
local ret="$3"
local url=""
local sum=""
local size=""
local url="" sum="" size=""
local host="https://files.dog/MSDN"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1352,9 +1303,8 @@ getLink3() {
local id="$1"
local lang="$2"
local ret="$3"
local url=""
local sum=""
local size=""
local url="" sum="" size=""
local host="https://iso.reactos.org"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1381,9 +1331,8 @@ getLink4() {
local id="$1"
local lang="$2"
local ret="$3"
local url=""
local sum=""
local size=""
local url="" sum="" size=""
local host="https://archive.org/download"
[[ "${lang,,}" != "en" && "${lang,,}" != "en-us" ]] && return 0
@@ -1552,12 +1501,13 @@ getLink4() {
getValue() {
local val=""
local id="$2"
local lang="$3"
local type="$4"
local func="getLink$1"
local val=""
if [ "$1" -gt 0 ] && [ "$1" -le "$MIRRORS" ]; then
val=$($func "$id" "$lang" "$type")
fi
@@ -1585,6 +1535,7 @@ isMido() {
local id="$1"
local lang="$2"
local sum
disabled "${MIDO:-}" && return 1
@@ -1607,8 +1558,7 @@ isESD() {
"win10${PLATFORM,,}" | \
"win11${PLATFORM,,}-enterprise" | \
"win10${PLATFORM,,}-enterprise" )
return 0
;;
return 0 ;;
esac
return 1
@@ -1627,10 +1577,8 @@ validVersion() {
isESD "$id" "$lang" && return 0
for ((i=1;i<=MIRRORS;i++)); do
url=$(getLink "$i" "$id" "$lang")
[ -n "$url" ] && return 0
done
return 1
+366 -409
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 ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then
removeIso "$ISO" && return 68
removeIso "$ISO" || :
return 68
fi
fi
local extracted=0
local boot="$BOOT"
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 ! setImage; then
abortInstall "$dir" "$ISO" "$boot" || return 70
abortInstall "$dir" "$iso" "$boot" || return 70
handled=1
return 0
fi
if needsExtraction "$DETECTED" "$ISO"; then
if ! extractImage "$ISO" "$dir" "$VERSION"; then
removeIso "$ISO" && return 72
if ! needsExtraction "$DETECTED" "$iso"; then
return 0
fi
if ! extractImage "$iso" "$dir" "$VERSION"; then
removeIso "$iso" || :
return 72
fi
extracted=1
return 0
fi
else
# Inspect unknown media directly before falling back to extraction.
detectIsoImage "$iso" || detect_rc=$?
local detect_rc=0
detectIsoImage "$ISO" || detect_rc=$?
if (( detect_rc == 0 )); then
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 ) ;;
1 )
if ! extractImage "$ISO" "$dir" "$VERSION"; then
removeIso "$ISO" && return 74
if ! extractImage "$iso" "$dir" "$VERSION"; then
removeIso "$iso" || :
return 74
fi
extracted=1
if ! detectImage "$dir"; then
abortInstall "$dir" "$ISO" "$boot" || return 76
if detectImage "$dir"; then
return 0
fi ;;
* )
abortInstall "$dir" "$ISO" "$boot" || return 76
return 0 ;;
esac
fi
abortInstall "$dir" "$iso" "$boot" || return 76
handled=1
return 0
}
configureMachine() {
local iso="$1"
local dir="$2"
local boot="$3"
local desc
if ! desc=$(printVariant "$DETECTED" "$DETECTED"); then
abortInstall "$dir" "$ISO" "$boot" || return 78
abortInstall "$dir" "$iso" "$boot" || return 78
handled=1
return 0
fi
if ! setMachine "$DETECTED" "$ISO" "$dir" "$desc"; then
abortInstall "$dir" "$ISO" "$boot" || return 80
if ! setMachine "$DETECTED" "$iso" "$dir" "$desc"; then
abortInstall "$dir" "$iso" "$boot" || return 80
handled=1
return 0
fi
if ! restoreMachineState; then
abortInstall "$dir" "$ISO" "$boot" || return 82
abortInstall "$dir" "$iso" "$boot" || return 82
handled=1
return 0
fi
# Direct-boot media skips all unattended installation preparation.
if bootDirect "$DETECTED"; then
abortInstall "$dir" "$ISO" "$boot" || return 83
abortInstall "$dir" "$iso" "$boot" || return 83
handled=1
return 0
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
abortInstall "$dir" "$ISO" "$boot" || return 84
abortInstall "$dir" "$iso" "$boot" || return 84
handled=1
return 0
fi
@@ -98,33 +153,34 @@ startWindows() {
exit 86
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 ! extractImage "$ISO" "$dir" "$VERSION"; then
removeIso "$ISO" && return 90
if ! extractImage "$iso" "$dir" "$VERSION"; then
removeIso "$iso" || :
return 90
fi
fi
if ! prepareImage "$ISO" "$dir"; then
abortInstall "$dir" "$ISO" "$boot" || return 92
if ! prepareImage "$iso" "$dir"; then
abortInstall "$dir" "$iso" "$boot" || return 92
handled=1
return 0
fi
if ! updateImage "$dir" "$XML" "$LANGUAGE"; then
abortInstall "$dir" "$ISO" "$boot" || return 94
abortInstall "$dir" "$iso" "$boot" || return 94
handled=1
return 0
fi
removeImage "$ISO" || return 96
removeImage "$iso" || return 96
buildImage "$dir" || return 98
fi
finishInstall "$BOOT" "N" "$boot" || return 100
return 0
}
@@ -218,6 +274,8 @@ startInstall() {
ISO=$(basename "$BOOT")
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 ! mv -f -- "$BOOT" "$ISO"; then
error "Failed to move ISO file from \"$BOOT\" to \"$ISO\" !"
@@ -255,11 +313,16 @@ abortInstall() {
local dir="$1"
local iso="$2"
local boot="$3"
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
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
efi=$(find "$dir" -maxdepth 1 -type d -iname efi -print -quit)
@@ -278,6 +341,8 @@ abortInstall() {
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
BOOT="$iso"
REMOVE="N"
@@ -317,9 +382,12 @@ skipInstall() {
local iso="$1"
local previousBase="$2"
local boot="$STORAGE/windows.boot"
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 ! hasDisk; then
@@ -372,6 +440,7 @@ finishInstall() {
local iso="$1"
local aborted="$2"
local boot="$3"
local base
if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then
@@ -428,8 +497,9 @@ finishInstall() {
findFile() {
local dir file base
local fname="$1"
local dir file base
local boot="$STORAGE/windows.boot"
dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit)
@@ -462,6 +532,8 @@ findFile() {
ISO="$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"
return 0
@@ -501,6 +573,8 @@ needsExtraction() {
local id="$1"
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
skipVersion "$id" ||
@@ -512,6 +586,7 @@ checkFreeSpace() {
local dir="$1"
local size="$2"
local size_gb space space_gb
size_gb=$(formatBytes "$size")
@@ -526,16 +601,6 @@ checkFreeSpace() {
return 0
}
getEsdField() {
local list="$1"
local index="$2"
sed -n "${index}p" <<< "$list" | tr -cd '0-9'
return 0
}
extractESD() {
local iso="$1"
@@ -543,11 +608,11 @@ extractESD() {
local version="$3"
local desc="$4"
local info count totals links
local bootTotal bootLinks
local wimTotal wimLinks
local installSize size
local edition imgEdition
local bootTotal bootLinks wimTotal wimLinks
local installSize size edition imgEdition
local bootWim installWim bootSize wimSize
local index line ret xml metadata count
local -a fields
local minSize=100000000
local freeSpace=9606127360
@@ -579,12 +644,30 @@ extractESD() {
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!"
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
error "Cannot read the image count in ESD file!"
return 1
@@ -595,34 +678,33 @@ extractESD() {
return 1
fi
totals=$(grep "Total Bytes:" <<< "$info" || true)
links=$(grep "Hard Link Bytes:" <<< "$info" || true)
bootTotal="${fields[1]:-}"
bootLinks="${fields[2]:-}"
bootTotal=$(getEsdField "$totals" 1)
bootLinks=$(getEsdField "$links" 1)
if [[ ! "$bootTotal" =~ ^[0-9]+$ ]] || [[ ! "$bootLinks" =~ ^[0-9]+$ ]]; then
if [[ ! "$bootTotal" =~ ^[0-9]+$ ]] ||
[[ ! "$bootLinks" =~ ^[0-9]+$ ]]; then
error "Cannot read bootdisk size from ESD file!"
return 1
fi
local bootSize=$(( bootTotal - bootLinks ))
bootSize=$(( bootTotal - bootLinks ))
wimTotal=$(getEsdField "$totals" 3)
wimLinks=$(getEsdField "$links" 3)
wimTotal="${fields[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!"
return 1
fi
local wimSize=$(( wimTotal - wimLinks + bootPad ))
wimSize=$(( wimTotal - wimLinks + bootPad ))
/run/progress.sh "$dir" "$bootSize" "$msg ([P])..." &
local index="1"
index="1"
wimlib-imagex apply "$iso" "$index" "$dir" --quiet 2>/dev/null || {
local ret=$?
ret=$?
fKill "progress.sh"
error "Extracting $desc bootdisk failed ($ret)"
return 1
@@ -630,8 +712,8 @@ extractESD() {
fKill "progress.sh"
local bootWim="$dir/sources/boot.wim"
local installWim="$dir/sources/install.wim"
bootWim="$dir/sources/boot.wim"
installWim="$dir/sources/install.wim"
msg="Extracting $desc environment"
info "$msg..." && html "$msg..."
@@ -639,8 +721,9 @@ extractESD() {
index="2"
/run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$bootWim" --compress=none --quiet || {
local ret=$?
wimlib-imagex export "$iso" "$index" "$bootWim" \
--compress=none --quiet || {
ret=$?
fKill "progress.sh"
error "Adding WinPE failed ($ret)"
return 1
@@ -654,8 +737,9 @@ extractESD() {
index="3"
/run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$bootWim" --compress=none --boot --quiet || {
local ret=$?
wimlib-imagex export "$iso" "$index" "$bootWim" \
--compress=none --boot --quiet || {
ret=$?
fKill "progress.sh"
error "Adding Windows Setup failed ($ret)"
return 1
@@ -678,18 +762,19 @@ extractESD() {
return 1
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
installSize=$(stat -c%s "$iso")
installSize=$(( installSize + installPad ))
installSize=$(( size + installPad ))
/run/progress.sh "$installWim" "$installSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$installWim" --quiet || {
local ret=$?
ret=$?
fKill "progress.sh"
error "Addition of $index to the $desc image failed ($ret)"
return 1
@@ -710,8 +795,9 @@ extractImage() {
local iso="$1"
local dir="$2"
local version="$3"
local desc="local ISO"
local file size
local desc="local ISO"
if [ -z "$CUSTOM" ]; then
desc="downloaded ISO"
@@ -766,6 +852,8 @@ extractImage() {
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)
if [ -z "$file" ]; then
@@ -799,7 +887,11 @@ setMachine() {
local dir="$3"
local desc="$4"
if [[ "${id,,}" != "win9"* ]]; then
ETFS="boot/etfsboot.com"
else
ETFS="[BOOT]/Boot-1.44M.img"
fi
local version=""
case "${id,,}" in
@@ -903,12 +995,13 @@ prepareImage() {
local iso="$1"
local dir="$2"
local desc missing
desc=$(printVariant "$DETECTED" "$DETECTED")
if [[ "${BOOT_MODE,,}" == "windows_legacy" &&
"${DETECTED,,}" != "win9"* ]]; then
# Legacy rebuilt media must retain the source ISO's El Torito boot-load size.
if [[ "${BOOT_MODE,,}" == "windows_legacy" && "${DETECTED,,}" != "win9"* ]]; then
getBootLoadSize "$iso" "$dir" "$desc" || return 1
fi
@@ -917,13 +1010,14 @@ prepareImage() {
if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then
extractBootImage "$iso" "$dir" "$desc" && return 0
error "Failed to extract boot image from ISO image \"${iso}\"!"
return 1
fi
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/$EFISYS" ] && [ -s "$dir/$EFISYS" ] && return 0
@@ -954,7 +1048,8 @@ addFolder() {
local target="${2:-image}"
local log="${3:-Y}"
local mode="${4:-copy}"
local folder file="" source=""
local file="" source="" folder
local dest="$src/\$OEM\$/\$1/OEM"
local install="$src/.overlay-install.bat"
@@ -967,6 +1062,8 @@ addFolder() {
info "$msg" && html "$msg"
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
rm -f -- "$install" || return 1
@@ -1037,6 +1134,7 @@ addDriver() {
local path="$2"
local target="$3"
local driver="$4"
local folder="" desc
if [ -z "$id" ]; then
@@ -1074,8 +1172,7 @@ addDriver() {
case "${id,,}" in
"winvista"* )
[[ "${driver,,}" == "viorng" ]] && return 0
;;
[[ "${driver,,}" == "viorng" ]] && return 0 ;;
esac
local dest="$path/$target/$driver"
@@ -1093,6 +1190,7 @@ addDrivers() {
local file="${4:-}"
local index="${5:-}"
local log="${6:-Y}"
local drivers="$tmp/drivers"
rm -rf "$drivers"
@@ -1114,6 +1212,7 @@ addDrivers() {
local target="\$WinPEDriver\$"
local dest="$drivers/$target"
mkdir -p "$dest" || return 1
if [ -n "$file" ]; then
@@ -1221,12 +1320,13 @@ updateImage() {
local dir="$1"
local asset="$2"
local language="$3"
local script=""
local tmp="/tmp/install"
local xml="autounattend.xml"
local bak="${xml//.xml/.org}"
local dat="${xml//.xml/.dat}"
local desc path src wim name info
local script=""
skipVersion "${DETECTED,,}" && return 0
@@ -1255,6 +1355,7 @@ updateImage() {
return 1
fi
# Windows Setup normally resides in boot image 2; single-image media uses 1.
local idx="1"
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!"
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" "/$dat" "--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
if [ -z "${CUSTOM_XML:-}" ]; then
prepareSetupScript "$asset" "$tmp/setup" script || exit 84
script=$(prepareSetupScript "$asset" "$tmp/setup") || exit 84
fi
if ! wimlib-imagex update "$wim" "$idx" --command "add $answer /$xml" > /dev/null; then
@@ -1328,6 +1431,8 @@ updateImage() {
fi
# Manual mode removes generated automation and restores the original answer
# file when one was backed up earlier.
if enabled "$MANUAL"; then
removeGeneratedXML "$asset" || return 1
@@ -1342,6 +1447,8 @@ updateImage() {
fi
# Prevent a root-level answer file from overriding the selected automatic or
# manual behavior when Windows Setup first boots.
name="$xml"
enabled "$MANUAL" && name="$bak"
path=$(find "$dir" -maxdepth 1 -type f -iname "$name" -print -quit) || return 1
@@ -1392,10 +1499,11 @@ reserveSambaPorts() {
backup () {
local iso="$1"
local count=1
local name="unknown"
local root="$STORAGE/backups"
local file previous failed=""
local failed="" file previous
previous=$(readState "base") || return 1
[ -n "$previous" ] && name="${previous%.*}"
@@ -1437,6 +1545,8 @@ backup () {
-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=$!
if ! wait "$find_pid"; then
@@ -1461,6 +1571,8 @@ restoreBootMode() {
[ -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
BOOT_MODE="$mode"
return 0
@@ -1476,6 +1588,8 @@ restoreBootMode() {
restoreMachine() {
# Restore the saved machine only when q35 is still the default; an explicit
# user-selected machine must remain untouched.
[[ "${PLATFORM,,}" != "x64" ]] && return 0
[[ "${MACHINE,,}" != "q35" ]] && return 0
+214 -153
View File
@@ -6,6 +6,7 @@ handleCurlError() {
local code="$1"
local server="$2"
local reason="${3:-}"
local signal
if [ -n "$reason" ] && (( code <= 125 )); then
@@ -29,8 +30,7 @@ handleCurlError() {
SEGV | ABRT) error "Curl crashed with signal $signal." ;;
"") error "Curl terminated with exit status $code." ;;
*) error "Curl terminated due to signal $signal." ;;
esac
;;
esac ;;
esac
return 1
@@ -38,10 +38,9 @@ handleCurlError() {
curlRequest() {
local output="$1"
local server="$2"
local agent="$3"
shift 3
local server="$1"
local agent="$2"
shift 2
local log reason response
@@ -50,6 +49,8 @@ curlRequest() {
return 1
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 \
--silent \
@@ -76,10 +77,7 @@ curlRequest() {
rm -f "$log"
if [ -n "$output" ]; then
printf -v "$output" '%s' "$response"
fi
printf '%s' "$response"
return 0
}
@@ -93,13 +91,13 @@ downloadWindowsLink() {
local desc="$6"
local type="$7"
local skuId skuJson
local linkJson link
local ovData ovTime session
local ovToken="" ovTicks=""
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
error "Failed to generate session ID!"
return 1
@@ -112,32 +110,32 @@ downloadWindowsLink() {
return 1
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 vlsUrl="https://vlscppe.microsoft.com/tags?org_id=$orgId&session_id=$session"
enabled "$DEBUG" && echo "Getting Session ID: $session"
# Permit Session ID
curlRequest "" "Microsoft" "$agent" \
curlRequest "Microsoft" "$agent" \
--output /dev/null \
--header "Accept:" \
--max-filesize 100K \
-- "$vlsUrl" || return 1
# Microsoft download "protection" also requires an ov-df.microsoft.com request/reply
# 1) Request mdt.js to get w and rticks. InstanceId is (currently) constant.
# Complete Microsoft's ov-df challenge by retrieving a token and timing
# value, then returning both with the current timestamp.
local instance="560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175"
local ovUrl="https://ov-df.microsoft.com/mdt.js?instanceId=$instance&PageId=si&session_id=$session"
enabled "$DEBUG" && echo -n "Getting OV data: "
curlRequest ovData "Microsoft" "$agent" \
ovData=$(curlRequest "Microsoft" "$agent" \
--header "Accept:" \
--max-filesize 1M \
-- "$ovUrl" || return 1
-- "$ovUrl") || return 1
if [[ $ovData =~ [\?\&]w=([A-Fa-f0-9]+) ]]; then
ovToken="${BASH_REMATCH[1]}"
@@ -156,14 +154,12 @@ downloadWindowsLink() {
sleep 0.2
# 2) Send a reply with session ID, current epoch and previously retrieved w and rticks
ovTime=$(date +%s%3N)
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"
curlRequest "" "Microsoft" "$agent" \
curlRequest "Microsoft" "$agent" \
--output /dev/null \
--header "Accept:" \
--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"
curlRequest skuJson "Microsoft" "$agent" \
skuJson=$(curlRequest "Microsoft" "$agent" \
--referer "$url" \
--header "Accept:" \
--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=$?; } || :
if [ -z "$skuId" ] || [[ "${skuId,,}" == "null" ]] || (( rc != 0 )); then
@@ -190,19 +188,18 @@ downloadWindowsLink() {
enabled "$DEBUG" && echo "$skuId"
enabled "$DEBUG" && echo "Getting ISO download link..."
# Get ISO download link
# If any request is going to be blocked by Microsoft it's always this last one (the previous requests always seem to succeed)
# Microsoft normally applies request or IP blocking on this final connector
# 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"
curlRequest linkJson "Microsoft" "$agent" \
linkJson=$(curlRequest "Microsoft" "$agent" \
--referer "$url" \
--header "Accept:" \
--max-filesize 100K \
-- "$linkUrl" || return 1
-- "$linkUrl") || return 1
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."
return 1
fi
@@ -235,9 +232,8 @@ downloadWindows() {
local lang="$2"
local desc="$3"
local agent language
local page productId
local type winVer
local agent language page
local productId type winVer
agent=$(getAgent)
language=$(getLanguage "$lang" "name")
@@ -246,22 +242,18 @@ downloadWindows() {
"win10x64" )
productId="2618"
winVer="10"
type="1"
;;
type="1" ;;
"win11x64" )
productId="3321"
winVer="11"
type="1"
;;
type="1" ;;
"win11arm64" )
productId="3324"
winVer="11arm64"
type="2"
;;
type="2" ;;
* )
error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1
;;
return 1 ;;
esac
local url="https://www.microsoft.com/en-us/software-download/windows$winVer"
@@ -275,14 +267,16 @@ downloadWindows() {
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..."
info "Microsoft download request failed, $msg"
enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \
page=$(curlRequest "Microsoft" "$agent" \
--header "Accept:" \
--max-filesize 1M \
-- "$url" || return 1
-- "$url") || return 1
enabled "$DEBUG" && echo -n "Getting Product edition ID: "
productId=$(printf '%s' "$page" |
@@ -310,6 +304,7 @@ downloadWindowsEval() {
local id="$1"
local lang="$2"
local desc="$3"
local culture compare type
local agent language winVer
@@ -354,13 +349,12 @@ downloadWindowsEval() {
enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \
page=$(curlRequest "Microsoft" "$agent" \
--location \
--max-filesize 1M \
-- "$url" || return 1
-- "$url") || return 1
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"
return 1
fi
@@ -375,7 +369,8 @@ downloadWindowsEval() {
grep -Eio "https://go\.microsoft\.com/fwlink(/p)?/\?[^\"'<>[:space:]]+" |
grep -Ei '(^|[?&])culture='"${culture,,}"'(&|$)' |
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
error "Windows server download page gave us no download link!"
else
@@ -385,6 +380,8 @@ downloadWindowsEval() {
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
"iot" )
case "${PLATFORM,,}" in
@@ -420,19 +417,21 @@ downloadWindowsEval() {
[ -z "$link" ] && error "Could not parse download link from page!" && return 1
# Follow redirect so proceeding log message is useful
# This is a request we make that Fido doesn't
# Resolve the fwlink now so later logging and platform validation use the
# actual ISO URL rather than Microsoft's generic redirect.
curlRequest link "Microsoft" "$agent" \
link=$(curlRequest "Microsoft" "$agent" \
--location \
--output /dev/null \
--write-out "%{url_effective}" \
--head \
-- "$link" || return 1
-- "$link") || return 1
local lower="${link,,}"
local separator='(^|[[:space:]_./-])'
# Guard against page-order changes resolving to the wrong architecture
# before downloading a multi-gigabyte image.
case "${PLATFORM,,}" in
"x64" )
if [[ "$lower" =~ ${separator}(arm64|a64) ]]; then
@@ -450,6 +449,8 @@ downloadWindowsEval() {
fi ;;
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
compare=$(getMido "$id" "$lang" "")
@@ -494,14 +495,11 @@ getMidoDetected() {
# Derive the normal answer-file identity from the requested download route.
case "$default" in
*"-enterprise-ltsc-eval" )
default="${default%-enterprise-ltsc-eval}-ltsc"
;;
default="${default%-enterprise-ltsc-eval}-ltsc" ;;
*"-enterprise-iot-eval" )
default="${default%-enterprise-iot-eval}-iot"
;;
default="${default%-enterprise-iot-eval}-iot" ;;
*"-eval" )
default="${default%-eval}"
;;
default="${default%-eval}" ;;
esac
# Preserve a genuinely different DETECTED override.
@@ -513,17 +511,13 @@ getMidoDetected() {
# Select the answer-file identity for the source that actually succeeded.
case "$source" in
*"-enterprise-ltsc-eval" )
detected="${source%-enterprise-ltsc-eval}-ltsc-eval"
;;
detected="${source%-enterprise-ltsc-eval}-ltsc-eval" ;;
*"-enterprise-iot-eval" )
detected="${source%-enterprise-iot-eval}-iot-eval"
;;
detected="${source%-enterprise-iot-eval}-iot-eval" ;;
*"-eval" )
detected="$source"
;;
detected="$source" ;;
* )
detected="${current:-$default}"
;;
detected="${current:-$default}" ;;
esac
echo "$detected"
@@ -535,21 +529,21 @@ downloadWindowsLtsc() {
local id="$1"
local lang="$2"
local desc="$3"
local alternate alternate_desc
case "${id,,}" in
"win11${PLATFORM,,}-enterprise-iot-eval" )
alternate="win11${PLATFORM,,}-enterprise-ltsc-eval"
;;
alternate="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!"
return 1
;;
return 1 ;;
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
MIDO_SOURCE="$id"
return 0
@@ -574,6 +568,7 @@ getWindows() {
local lang="$2"
local desc="$3"
local web_desc="$4"
local language edition
MIDO_SOURCE=""
@@ -586,6 +581,8 @@ getWindows() {
local web_msg="Requesting $web_desc from the Microsoft servers..."
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
"win2008r2"* | \
"win81${PLATFORM,,}"* | \
@@ -598,6 +595,8 @@ getWindows() {
fi ;;
esac
# ARM64 downloads exist only for the explicitly supported Windows 11
# routes; all other catalog entries remain x64-only.
case "${version,,}" in
"win10x64" ) ;;
"win11${PLATFORM,,}" ) ;;
@@ -610,6 +609,8 @@ getWindows() {
fi ;;
esac
# Prefer live Microsoft download routes. Unsupported or failed live routes
# fall through to the configured static catalog below.
case "${version,,}" in
"win10x64" | "win11${PLATFORM,,}" )
@@ -621,8 +622,7 @@ getWindows() {
"win11${PLATFORM,,}-enterprise-iot-eval" | \
"win11${PLATFORM,,}-enterprise-ltsc-eval" )
downloadWindowsLtsc "$version" "$lang" "$edition" && return 0
;;
downloadWindowsLtsc "$version" "$lang" "$edition" && return 0 ;;
"win11${PLATFORM,,}-enterprise"* )
@@ -643,10 +643,11 @@ getWindows() {
* )
error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1
;;
return 1 ;;
esac
# Static catalog URLs are the last resort after live Microsoft methods are
# unavailable or have failed.
MIDO_URL=$(getMido "$version" "$lang" "")
[ -z "$MIDO_URL" ] && return 1
@@ -665,11 +666,10 @@ getBuild() {
local id="$1"
local ret="$2"
local url=""
local name=""
local build="$3"
local edition=""
local file="catalog.xml"
local url="" name="" edition=""
case "${id,,}" in
"win11${PLATFORM,,}" )
@@ -695,10 +695,9 @@ getCatalog() {
local id="$1"
local ret="$2"
local url=""
local name=""
local edition=""
local file="catalog.cab"
local url="" name="" edition=""
if [[ "${id,,}" == "win11"* ]] && ! isCompatible; then
# ARMv8.0 cannot run Windows 11 builds 24H2 and up.
@@ -728,19 +727,108 @@ getCatalog() {
"url" ) echo "$url" ;;
"file" ) echo "$file" ;;
"name" ) echo "$name" ;;
"edition" ) echo '[Edition="'"${edition}"'"]' ;;
"edition" ) echo "$edition" ;;
*) echo "";;
esac
return 0
}
getXmlTag() {
parseESD() {
local tag="$1"
local file="$2"
local xml="$1"
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
}
@@ -751,12 +839,10 @@ getESD() {
local version="$2"
local lang="$3"
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 esdFile="esd_edition.xml"
local filterFile="products_filter.xml"
local log
file=$(getCatalog "$version" "file")
catalog=$(getCatalog "$version" "url")
@@ -783,10 +869,12 @@ getESD() {
return 1
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 \
--no-http-keep-alive --output-file="$log"
local rc=$?
rc=$?
} || :
if (( rc != 0 )); then
@@ -813,6 +901,8 @@ getESD() {
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 ! mv -f "$dir/$file" "$dir/$xmlFile"; then
@@ -832,61 +922,13 @@ getESD() {
fi
if [ ! -f "$dir/$xmlFile" ] || [ ! -s "$dir/$xmlFile" ]; then
if [ ! -s "$dir/$xmlFile" ]; then
error "Failed to find $xmlFile in $file!"
return 1
fi
local query='//File[Architecture="'${PLATFORM,,}'"]'"${edition}"''
result=$(xmllint --nonet --xpath "${query}" "$dir/$xmlFile" 2>/dev/null || true)
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!"
if ! parseESD \
"$dir/$xmlFile" "$version" "$lang" "$desc" "$edition" "$culture"; then
return 1
fi
@@ -898,11 +940,12 @@ isCompressed() {
local url="${1%%\?*}"
# The ReactOS latest-build endpoint returns an archive without a filename
# extension, so recognize its path explicitly.
case "${url,,}" in
*.7z | *.zip | *.rar | *.tar | *.cab | *.cpio | \
*.lzh | *.lha | *.xar | */latest-x86-gcc-lin-rel )
return 0
;;
return 0 ;;
esac
return 1
@@ -926,6 +969,9 @@ verifyFile() {
[ -z "$check" ] && 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"
local msg="Verifying downloaded ISO..."
@@ -975,10 +1021,13 @@ downloadFile() {
local desc="$4"
local web_desc="$5"
local connections="${6:-1}"
local domain dots
local msg="Downloading $web_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}')
dots=$(echo "$domain" | tr -cd '.' | wc -c)
(( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)')
@@ -1007,8 +1056,11 @@ tryDownload() {
local desc="$6"
local seconds="$7"
local web_desc="$8"
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
minimum="10485760"
fi
@@ -1059,8 +1111,8 @@ fallbackEnglish() {
local lang="$3"
local desc="$4"
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..."
info "$msg"
@@ -1086,12 +1138,11 @@ downloadImage() {
local iso="$1"
local version="$2"
local lang="$3"
local requested="$version"
local tried="n"
local success="n"
local seconds="5"
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
@@ -1127,6 +1178,8 @@ downloadImage() {
desc+=" in $language"
fi
# Prefer a live Microsoft URL and retry link generation once before moving
# on to ESD catalogs or mirrors.
if isMido "$version" "$lang"; then
tried="y"
@@ -1167,7 +1220,13 @@ downloadImage() {
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")
web_desc=$(printVariant "$DETECTED" "")
@@ -1196,6 +1255,8 @@ downloadImage() {
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"
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
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)
[ -z "$line" ] && return 1
@@ -38,6 +40,9 @@ bootStatus() {
return 2
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 \
-e "Boot failed: not a bootable disk" \
-e "Boot failed: Could not read from CDROM" \
@@ -59,6 +64,8 @@ bootStatus() {
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 \
'BdsDxe: starting Boot[[:xdigit:]]{4} ' \
"$QEMU_PTY" | tail -1)
@@ -105,6 +112,8 @@ waitForBoot() {
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 keyDelay=$(bootKeyDelay); then
@@ -151,6 +160,9 @@ waitForBoot() {
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
pendingLine="$marker"
pendingType=$status
@@ -173,6 +185,8 @@ waitForBoot() {
5)
# A failed device attempt is transitional because firmware may continue
# with another target. Discard any pending success decision.
pendingType=0
pendingLine=""
pendingDeadline=0
@@ -213,6 +227,8 @@ legacyBootReady() {
last="${line#*:}"
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
grep -Fq "Loading FreeLoader..." <<< "$recent" && return 0
@@ -226,6 +242,8 @@ legacyBootReady() {
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
[ ! -s "$QEMU_PTY" ] && return 1
@@ -239,6 +257,8 @@ ready() {
'BdsDxe: starting Boot[[:xdigit:]]{4} ' \
"$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 \
'BdsDxe: starting Boot[[:xdigit:]]{4} "Windows Boot Manager" from .*HD\(' \
<<< "$last" && return 0
@@ -258,6 +278,8 @@ sendKey() {
[ ! -S "$ACPI_SOCKET" ] && return 1
[[ "$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=$(
{
for ((i = 1; i <= repeat; i++)); do
@@ -272,6 +294,8 @@ sendKey() {
return 1
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 \
-e 'unknown command' \
-e 'unknown key' \
@@ -312,11 +336,14 @@ bootKeyDelay() {
[ ! -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
echo 0
return 0
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
grep -Fq "Booting from DVD/CD" "$QEMU_PTY" || return 1
else
@@ -351,7 +378,8 @@ markWindowsBooted() {
return 0
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
if ! touch "$file"; then
@@ -376,6 +404,8 @@ finish() {
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
failed=1
fi
@@ -412,6 +442,8 @@ abortDuringSetup() {
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
info "Cannot send ACPI signal during $(app) setup, aborting..."
else
@@ -432,12 +464,16 @@ gracefulShutdown() {
local sig="$1"
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
code=$(signalCode "$sig")
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
SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..."
@@ -448,12 +484,16 @@ gracefulShutdown() {
return
fi
# Signal handlers must complete their own error handling and cleanup without
# errexit terminating the shell partway through the shutdown sequence.
set +e
SHUTDOWN_SIGNAL=$code
touch "$QEMU_END"
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 ! interactive || ! waitQemuPid pid; then
warn "QEMU PID file does not exist?"
@@ -479,6 +519,8 @@ gracefulShutdown() {
enabled "$SHUTDOWN" || return 0
[ -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
_trap gracefulShutdown SIGINT
fi
+53 -24
View File
@@ -22,29 +22,32 @@ configureNetwork() {
if enabled "$DHCP"; then
hostname="$UPLINK"
interfaces="$DEV"
SAMBA_HOSTNAME="$UPLINK"
SAMBA_INTERFACES="$DEV"
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
interfaces="lo"
SAMBA_INTERFACES="lo"
else
interfaces="$BRIDGE"
SAMBA_INTERFACES="$BRIDGE"
fi
if [ -n "${SAMBA_INTERFACE:-}" ]; then
interfaces+=",$SAMBA_INTERFACE"
SAMBA_INTERFACES+=",$SAMBA_INTERFACE"
fi
fi
netbios="${hostname%%.*}"
netbios="${netbios:0:15}"
# NetBIOS names are limited to 15 visible characters.
SAMBA_NETBIOS="${SAMBA_HOSTNAME%%.*}"
SAMBA_NETBIOS="${SAMBA_NETBIOS:0:15}"
[ -z "$netbios" ] && netbios="host"
[ -z "$SAMBA_NETBIOS" ] && SAMBA_NETBIOS="host"
return 0
}
@@ -110,6 +113,8 @@ addShare() {
empty="Y"
fi
# The generated fallback share contains only instructions and must never be
# writable from the guest.
if [[ "$dir" == "$tmp" ]]; then
readonly="Y"
@@ -118,6 +123,8 @@ addShare() {
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
writable="Y"
@@ -127,6 +134,8 @@ addShare() {
return 1
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
if probe=$(mktemp "$dir/.samba-write-test.XXXXXX" 2>/dev/null); then
@@ -146,6 +155,7 @@ addShare() {
if [[ "$empty" == "Y" ]]; then
# Keep newly created content in the shared group through the setgid bit.
if ! chmod 2777 "$dir"; then
error "Failed to set permissions for directory $dir" && return 1
fi
@@ -155,6 +165,8 @@ addShare() {
return 1
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 ! chown "1000:1000" "$dir"; then
error "Failed to set ownership for directory $dir" && return 1
@@ -165,6 +177,7 @@ addShare() {
elif [[ "$readonly" != "Y" ]]; then
# Preserve access to non-writable mounts by exporting them read-only.
readonly="Y"
fi
@@ -200,20 +213,27 @@ writeConfig() {
if ! {
echo "[global]"
echo " server string = Dockur"
echo " netbios name = $netbios"
echo " netbios name = $SAMBA_NETBIOS"
echo " workgroup = WORKGROUP"
echo " interfaces = $interfaces"
echo " interfaces = $SAMBA_INTERFACES"
echo " bind interfaces only = yes"
echo " security = user"
echo " guest account = nobody"
echo " map to guest = Bad User"
# Retain SMB1 negotiation for legacy Windows guests.
echo " server min protocol = NT1"
# Allow bind-mounted shares to follow symlinks outside their share root.
echo " follow symlinks = yes"
echo " wide links = yes"
echo " unix extensions = no"
echo " inherit owner = yes"
echo " create mask = 0666"
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 group = root"
echo " force create mode = 0666"
@@ -241,11 +261,13 @@ selectPrimaryShare() {
return 1
fi
share="/shared"
[ ! -d "$share" ] && [ -d "$STORAGE/shared" ] && share="$STORAGE/shared"
[ ! -d "$share" ] && [ -d "/data" ] && share="/data"
[ ! -d "$share" ] && [ -d "$STORAGE/data" ] && share="$STORAGE/data"
[ ! -d "$share" ] && share="$tmp"
# Prefer explicit root-level bind mounts, then storage-local compatibility
# paths. When none exist, publish an instructional read-only share.
SAMBA_SHARE="/shared"
[ ! -d "$SAMBA_SHARE" ] && [ -d "$STORAGE/shared" ] && SAMBA_SHARE="$STORAGE/shared"
[ ! -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
}
@@ -256,6 +278,8 @@ addOptionalShare() {
local ref="/shared$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
addShare "$ref" "$ref" "$name" "Shared" "$SAMBA_CONFIG" || :
elif [ -d "/data$index" ]; then
@@ -267,13 +291,13 @@ addOptionalShare() {
prepareSambaDirs() {
# Create directories if missing
mkdir -p \
/var/lib/samba/sysvol \
/var/lib/samba/private \
/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 /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 || :
@@ -300,6 +324,8 @@ startDaemon() {
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
SAMBA_DEBUG="Y"
error "Failed to start $name daemon!"
@@ -319,7 +345,6 @@ startSamba() {
startNetbios() {
# Enable NetBIOS on Windows 7 and lower
enabled "$DEBUG" && echo "Starting NetBIOS daemon..."
startDaemon "NetBIOS" "/var/log/samba/log.nmbd" \
@@ -330,11 +355,12 @@ startNetbios() {
startWsddn() {
# Enable Web Service Discovery on Vista and up
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" \
wsddn -i "${interfaces%%,*}" -H "$hostname" \
wsddn -i "${SAMBA_INTERFACES%%,*}" -H "$SAMBA_HOSTNAME" \
--unixd --log-file=/var/log/wsddn.log --pid-file="$DDN_PID"
return 0
@@ -346,19 +372,22 @@ html "Initializing shared folder..."
enabled "$DEBUG" && echo "Starting Samba daemon..."
writeConfig || return 0
# Add shared folders
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 "3" || :
prepareSambaDirs || return 0
startSamba || return 0
# User-mode networking does not expose a LAN interface where discovery
# broadcasts would be useful.
isUserMode && return 0
# Older Windows versions discover shares through NetBIOS, while modern Windows
# uses Web Services Discovery.
if [[ "${BOOT_MODE:-}" == "windows_legacy" ]]; then
startNetbios || :
else