diff --git a/src/answer.sh b/src/answer.sh index 448d5d65..63800e1d 100644 --- a/src/answer.sh +++ b/src/answer.sh @@ -30,10 +30,21 @@ updateXML() { [ -z "${HEIGHT:-}" ] && HEIGHT="720" validateXMLSettings || return 1 - ensureXMLDefaultNamespace "$asset" || return 1 - updateUserXML "$asset" || return 1 - updateLocaleXML "$asset" "$language" || return 1 + if ! ensureXMLDefaultNamespace "$asset"; then + error "Failed to prepare the answer file XML namespace!" + return 1 + fi + + if ! updateUserXML "$asset"; then + error "Failed to update user and display settings in answer file!" + return 1 + fi + + if ! updateLocaleXML "$asset" "$language"; then + error "Failed to update regional settings in answer file!" + return 1 + fi if [ -n "$domain" ]; then @@ -46,13 +57,24 @@ updateXML() { else - updateLocalAccount "$asset" || return 1 + if ! updateLocalAccount "$asset"; then + error "Failed to update local account settings in answer file!" + return 1 + fi fi updateMembership "$asset" "$domain" "$workgroup" "$account" "$auth" || return 1 - updateAutologinXML "$asset" || return 1 - updateEditionXML "$asset" || return 1 + + if ! updateAutologinXML "$asset"; then + error "Failed to update automatic logon settings in answer file!" + return 1 + fi + + if ! updateEditionXML "$asset"; then + error "Failed to update edition settings in answer file!" + return 1 + fi validateGeneratedXML "$asset" || return 1 @@ -73,7 +95,7 @@ setXML() { if [ -d "${custom_files[0]}" ]; then error "The bind ${custom_files[0]} maps to a file that does not exist!" - exit 67 + return 2 fi # A custom answer file always takes precedence over bundled or generated @@ -182,7 +204,7 @@ addAnswerFile() { if ! updateDiskID "$answer" "${DISK_TYPE:-}"; then error "Failed to adjust the Windows installation disk!" - exit 85 + return 1 fi if ! setConfigurationXML "$answer"; then @@ -194,7 +216,10 @@ addAnswerFile() { if [ -z "${CUSTOM_XML:-}" ]; then - prepareSetupScript "$asset" "$stage" || exit 84 + if ! prepareSetupScript "$asset" "$stage"; then + error "Failed to prepare the Windows setup script!" + return 1 + fi fi @@ -1080,7 +1105,13 @@ updateDiskID() { done <<< "$values" - mapfile -t ids < <(printf '%s\n' "${ids[@]}" | sort -u) + local unique + unique=$(printf '%s\n' "${ids[@]}" | sort -u) || { + error "Failed to normalize DiskID values from answer file: $asset" + return 1 + } + + mapfile -t ids <<< "$unique" # Leave explicit multi-disk configurations untouched. (( ${#ids[@]} == 1 )) || return 0 @@ -1946,10 +1977,16 @@ markGeneratedXML() { local file="$1" local marker='' + local first [ -s "$file" ] || return 1 - if head -n 1 "$file" | grep -q '^ "$setup" || return 1 + [ "${#pid}" -ge 3 ] || return 0 + echo "${pid::-3}270" > "$setup" || : return 0 fi @@ -2447,28 +2500,44 @@ setLegacyKey() { if [[ -n "$file" ]]; then + local key="" + # Prefer a staging or OEM key already shipped on the media before falling # back to Microsoft's documented generic installation keys. if [[ "$driver" == "2k3" ]]; then - key=$(grep -i -A 2 "StagingKey" "$file" | tail -n 2 | head -n 1) || key="" + block=$(grep -i -A 2 "StagingKey" "$file") || block="" + + if [ -n "$block" ]; then + key=$(printf '%s\n' "$block" | tail -n 2 | head -n 1) || key="" + fi else key="${pid: -8:5}" if [[ "${pid^^}" == *"OEM" ]]; then - key=$(grep -i -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" + + block=$(grep -i -A 2 "$key" "$file") || block="" + else - key=$(grep -i -m 1 -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" + + block=$(grep -i -m 1 -A 2 "$key" "$file") || block="" + + fi + + if [ -n "$block" ]; then + key=$(printf '%s\n' "$block" | tail -n 2 | head -n 1) || key="" fi key="${key#*= }" fi - key="${key%$'\r'}" - [[ "${#key}" == "29" ]] && KEY="$key" + if [ -n "$key" ]; then + key="${key%$'\r'}" + [[ "${#key}" == "29" ]] && KEY="$key" + fi fi @@ -2499,7 +2568,9 @@ setLegacyKey() { esac - echo "${pid::-3}000" > "$setup" || return 1 + if [ "${#pid}" -ge 3 ]; then + echo "${pid::-3}000" > "$setup" || : + fi return 0 } @@ -2768,12 +2839,10 @@ disableAutoReboot() { local target="$1" - local file + local file rc=0 local pattern='^[[:space:]]*HKLM[[:space:]]*,[[:space:]]*"SYSTEM\\CurrentControlSet\\Control\\CrashControl"[[:space:]]*,[[:space:]]*"AutoReboot"[[:space:]]*,[[:space:]]*[^,]*,' - file=$(find \ - "$target" -maxdepth 1 -type f -iname HIVESYS.INF -print -quit - ) || return 1 + file=$(find "$target" -maxdepth 1 -type f -iname HIVESYS.INF -print -quit) || return 1 if [ -z "$file" ]; then error "The file HIVESYS.INF could not be found!" @@ -2782,19 +2851,18 @@ disableAutoReboot() { # Keep setup crashes visible instead of immediately rebooting into an # opaque installation loop. - if grep -Eqi "${pattern}[[:space:]]*[^,;[:space:]]+" "$file"; then + grep -Eqi "${pattern}[[:space:]]*[^,;[:space:]]+" "$file" || rc=$? - sed -i -E \ - "s|(${pattern})[[:space:]]*[^,;[:space:]]+|\\1 0|I" \ - "$file" || return 1 - - else - - printf '%s\n' \ - 'HKLM,"SYSTEM\CurrentControlSet\Control\CrashControl","AutoReboot",0x00010001,0' | - unix2dos >> "$file" || return 1 - - fi + case "$rc" in + 0 ) + sed -i -E "s|(${pattern})[[:space:]]*[^,;[:space:]]+|\\1 0|I" "$file" || : + ;; + 1 ) + printf '%s\n' \ + 'HKLM,"SYSTEM\CurrentControlSet\Control\CrashControl","AutoReboot",0x00010001,0' | + unix2dos >> "$file" || : + ;; + esac return 0 } diff --git a/src/define.sh b/src/define.sh index d8670f6c..be4d97a9 100644 --- a/src/define.sh +++ b/src/define.sh @@ -647,23 +647,28 @@ getVersion() { local evaluation="" id=$(fromName "$name" "$arch") + [ -z "$id" ] && return 0 + [[ "${name,,}" == *"evaluation"* ]] && evaluation="-eval" case "${id,,}" in - "winvista"* | "win7"* | "win8"* | "win10"* | "win11"* ) - if edition=$(getEditionID "$name" "$id"); then - [ -n "$edition" ] && id+="-$edition" - [ -n "$evaluation" ] && id+="$evaluation" - fi - ;; + "win20"* ) + if [[ "${name,,}" == *"hyper-v server"* ]]; then id+="-hv" elif edition=$(getServerEditionID "$name" "$id"); then [ -n "$edition" ] && id+="-$edition" [ -n "$evaluation" ] && id+="$evaluation" - fi - ;; + fi ;; + + * ) + + if edition=$(getEditionID "$name" "$id"); then + [ -n "$edition" ] && id+="-$edition" + [ -n "$evaluation" ] && id+="$evaluation" + fi ;; + esac echo "$id" @@ -695,6 +700,7 @@ getEditionRank() { case "$base" in "win20"* ) + case "$edition" in "" ) echo 0 ;; "datacenter-azure-core" | "datacenter-azure-core-"* | \ @@ -709,9 +715,10 @@ getEditionRank() { "essentials" | "essentials-"* ) echo 5 ;; "hv" | "hv-"* ) echo 10 ;; * ) echo 99 ;; - esac - ;; + esac ;; + * ) + case "$edition" in "enterprise-iot" | "enterprise-iot-"* | "iot" | "iot-"* ) echo 3 ;; "enterprise-ltsc" | "enterprise-ltsc-"* | "ltsc" | "ltsc-"* ) echo 4 ;; @@ -723,8 +730,8 @@ getEditionRank() { "home" | "home-"* ) echo 6 ;; "starter" | "starter-"* ) echo 7 ;; * ) echo 99 ;; - esac - ;; + esac ;; + esac return 0 @@ -735,6 +742,7 @@ getEditionPolicy() { local base="${1,,}" case "$base" in + "win20"* ) printf '%s\n' \ "normalizeServerEditionID" \ @@ -750,9 +758,10 @@ getEditionPolicy() { "-datacenter-azure-core" \ "-enterprise-core" \ "-web-core" \ - "-hv" - ;; + "-hv" ;; + * ) + printf '%s\n' \ "normalizeEditionID" \ "-enterprise" \ @@ -764,8 +773,8 @@ getEditionPolicy() { "-home" \ "-home-premium" \ "-home-basic" \ - "-starter" - ;; + "-starter" ;; + esac return 0 @@ -774,18 +783,20 @@ getEditionPolicy() { normalizeEdition() { local source="${1,,}" - - local edition + local edition transliterated source="${source//evaluation/}" - source=$(printf '%s' "$source" | - uconv -x 'Any-Latin; Latin-ASCII' 2>/dev/null) || return 1 + + if transliterated=$(printf '%s' "$source" | + uconv -x 'Any-Latin; Latin-ASCII' 2>/dev/null); then + source="$transliterated" + fi edition=$(sed -E \ -e 's/[^a-z0-9]+/-/g' \ -e 's/^-+//' \ -e 's/-+$//' \ - <<< "$source") + <<< "$source") || edition="" echo "$edition" return 0 @@ -799,11 +810,15 @@ normalizeEditionID() { edition=$(normalizeEdition "$1") || return 1 case "$edition" in + "pro" | "professional" | "business" ) edition="" ;; + "pro-n" | "pron" | "professional-n" | "professionaln" | "business-n" | "businessn" ) edition="n" ;; + * ) + if ! isClientEdition "$edition"; then case "$edition" in *"-n" ) base="${edition%-n}" ;; @@ -816,10 +831,13 @@ normalizeEditionID() { fi fi ;; + esac case "${id,,}" in + "win10"* | "win11"* ) + case "$edition" in "iot-enterprise-ltsc" | \ "iot-enterprise-ltsc-"[0-9][0-9][0-9][0-9] ) @@ -827,8 +845,8 @@ normalizeEditionID() { "enterprise-ltsc" | \ "enterprise-ltsc-"[0-9][0-9][0-9][0-9] ) edition="ltsc" ;; - esac - ;; + esac ;; + esac echo "$edition" diff --git a/src/image.sh b/src/image.sh index 2d7edfe5..6679bbd5 100644 --- a/src/image.sh +++ b/src/image.sh @@ -273,13 +273,17 @@ detectLanguage() { for path in "${paths[@]}"; do - lang=$(xmlstarlet sel -T -t -v "normalize-space(string(($path)[1]))" - 2>/dev/null <<< "$xml") || lang="" + if ! lang=$(xmlstarlet sel -T -t -v "normalize-space(string(($path)[1]))" - 2>/dev/null <<< "$xml"); then + warn "failed to read language metadata from Windows image!" + return 0 + fi + [ -n "$lang" ] && break done if [ -z "$lang" ]; then - warn "Language could not be detected from ISO!" + warn "language could not be detected from ISO!" return 0 fi @@ -290,7 +294,7 @@ detectLanguage() { return 0 fi - warn "Invalid language detected: \"$lang\"" + warn "invalid language detected: \"$lang\"" return 0 } @@ -617,7 +621,7 @@ detectLegacy() { if [ -n "$marker" ]; then error "Windows IA-64 (Itanium) images are not supported by this container!" - exit 67 + return 2 fi marker=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit) || return 2 @@ -786,7 +790,6 @@ readWimHeader() { if ! rm -f -- "$header"; then enabled "$DEBUG" && echo "Failed to remove the previous temporary WIM header: $header" >&2 - error "Failed to prepare Windows image header!" return 1 fi @@ -794,35 +797,30 @@ readWimHeader() { # extracting install.wim or install.esd from the ISO. if ! udfread range --ignore-case -o "$header" "$iso" "$image" 0 208 >/dev/null 2>&1; then enabled "$DEBUG" && echo "udfread failed to read the first 208 bytes of $image from $iso." >&2 - error "Failed to read Windows image header!" rm -f -- "$header" return 1 fi if ! size=$(stat -c%s -- "$header"); then enabled "$DEBUG" && echo "Failed to determine the size of the temporary WIM header: $header" >&2 - error "Failed to read Windows image header!" rm -f -- "$header" return 1 fi if (( size != 208 )); then enabled "$DEBUG" && echo "The WIM header is $size bytes instead of the expected 208 bytes." >&2 - error "Failed to read Windows image header!" rm -f -- "$header" return 1 fi if ! signature=$(od -An -N8 -tx1 "$header" | tr -d ' \n'); then enabled "$DEBUG" && echo "Failed to read the WIM header signature from $header." >&2 - error "Failed to read Windows image header!" rm -f -- "$header" return 1 fi if [[ "$signature" != "4d5357494d000000" ]]; then enabled "$DEBUG" && echo "The WIM header has an invalid signature: ${signature:-empty}." >&2 - error "Failed to read Windows image header!" rm -f -- "$header" return 1 fi @@ -1241,7 +1239,7 @@ configureImage() { if [[ "$DETECTED" == "win81x86"* || "$DETECTED" == "win10x86"* ]]; then error "The 32-bit version of $desc is not supported!" - exit 67 + return 2 fi local msg="the answer file for $desc was not found ($DETECTED.xml)" @@ -1269,18 +1267,14 @@ configureImage() { detectImageInfo() { local image_info="$1" - local desc index rc - checkPlatform "$image_info" || { - enabled "$DEBUG" && echo "Platform validation failed for the Windows image metadata." >&2 - exit 67 - } + checkPlatform "$image_info" || return 2 local output output=$(detectVersion "$image_info") || { enabled "$DEBUG" && echo "Version detection failed while parsing the Windows image metadata." >&2 - error "Failed to detect Windows version from image metadata!" + error "Failed to detect the Windows version from image metadata!" return 1 } @@ -1292,16 +1286,18 @@ detectImageInfo() { validateEdition || { enabled "$DEBUG" && echo "Edition validation failed for detected image: ${DETECTED:-empty}, index: ${index:-empty}." >&2 - error "Failed to validate Windows edition from image metadata!" + error "Failed to validate the Windows edition from image metadata!" return 1 } if [ -z "$DETECTED" ]; then + unknownImage || { rc=$? enabled "$DEBUG" && echo "Unknown-image handling failed after no Windows version could be detected (status $rc)." >&2 return "$rc" } + return 0 fi @@ -1337,25 +1333,84 @@ detectIsoImage() { # the caller may extract the media. Metadata parsing/configuration errors use 2. image=$(findIsoImage "$iso") || { rc=$? - enabled "$DEBUG" && echo "Direct ISO image lookup stopped with status $rc." >&2 + enabled "$DEBUG" && echo "ISO image lookup failed (status $rc)." >&2 return "$rc" } header=$(readWimHeader "$iso" "$image") || { - enabled "$DEBUG" && echo "Reading the WIM header failed for $image." >&2 + error "Failed to read the Windows image header!" return 2 } image_info=$(readIsoImageInfo "$iso" "$image" "$header") || { - enabled "$DEBUG" && echo "Reading the WIM XML metadata failed for $image." >&2 - error "Failed to read Windows image metadata!" + error "Failed to read the Windows image metadata!" return 2 } info "Detecting version from ISO image..." detectImageInfo "$image_info" || { - enabled "$DEBUG" && echo "Processing the Windows image metadata failed." >&2 + error "Failed to process the Windows image metadata!" + return 2 + } + + return 0 +} + +detectESDImage() { + + local iso="$1" + + local image_info install_info output index rc + local -a detected=() + + image_info=$(wimlib-imagex info "$iso" --xml 2>/dev/null | + iconv -f UTF-16LE -t UTF-8 2>/dev/null) || { + rc=$? + error "Cannot read ESD file information (status $rc)." + return 2 + } + + # Microsoft download ESDs use images 1-3 for setup media, WinPE, and Windows + # Setup; images 4 and higher contain installable editions. + if ! install_info=$(xmlstarlet ed \ + -d '/WIM/IMAGE[number(@INDEX) < 4]' \ + <<< "$image_info" 2>/dev/null); then + error "Cannot read installable images from ESD file!" + return 2 + fi + + checkPlatform "$install_info" || return 2 + + local output + output=$(detectVersion "$install_info") || { + error "Failed to detect Windows version from the ESD metadata!" + return 2 + } + + mapfile -t detected <<< "$output" + index="${detected[1]:-}" + + if [ -z "$index" ]; then + error "Failed to select an installation image based on the ESD metadata!" + return 2 + fi + + # extractESD removes every other image, leaving the selected edition at + # index 1. Detect against that final layout so the generated answer file + # already references the index that will exist after extraction. + if ! image_info=$(xmlstarlet ed \ + -d "/WIM/IMAGE[number(@INDEX) != $index]" \ + -u "/WIM/IMAGE[@INDEX='$index']/@INDEX" -v '1' \ + <<< "$install_info" 2>/dev/null); then + error "Cannot prepare ESD image information!" + return 2 + fi + + info "Detecting version from ESD image..." + + detectImageInfo "$image_info" || { + error "Failed to process the ESD image metadata!" return 2 } @@ -1364,6 +1419,8 @@ detectIsoImage() { baseDir() { + # TODO: Can be removed with base image 7.45+ + local path="${1%/}" [[ -z "$path" || "$path" == "/" ]] && { @@ -1584,7 +1641,7 @@ extractESD() { return 1 fi - checkPlatform "$xml" || return + checkPlatform "$installXml" || return output=$(detectVersion "$installXml") || return mapfile -t detected <<< "$output" @@ -1990,7 +2047,11 @@ buildImage() { base=$(basename "$BOOT") local out="$TMP/${base%.*}.tmp" - rm -f "$out" + + if ! rm -f "$out"; then + error "Failed to remove temporary ISO image: $out" + return 1 + fi desc=$(printVariant "$DETECTED" "ISO") diff --git a/src/install.sh b/src/install.sh index 32139d7a..51db6f4b 100644 --- a/src/install.sh +++ b/src/install.sh @@ -3,20 +3,43 @@ set -Eeuo pipefail startWindows() { - parseVersion || exit 58 - parseLanguage || exit 62 - detectCustom || exit 64 + parseVersion || { + error "Failed to parse the Windows version!" + exit 58 + } + + parseLanguage || { + error "Failed to parse the Windows language!" + exit 62 + } + + detectCustom || { + error "Failed to scan for custom installation media!" + exit 64 + } + + local rc=0 + startInstall || rc=$? + (( rc > 1 )) && exit "$rc" + + if (( rc )); then + + bootWindows || { + error "Failed to boot Windows!" + exit 66 + } - if ! startInstall; then - bootWindows || exit 66 return 0 + fi if ! hasImage "$ISO"; then + if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then removeImage "$ISO" || : exit 68 fi + fi local boot="$BOOT" @@ -48,11 +71,12 @@ selectWindowsImage() { XML="" FB="falling back to manual installation!" - normalizeDetected || return 70 + normalizeDetected || : if [ -n "$DETECTED" ]; then if ! setImage; then + error "Failed to configure the detected Windows image!" return 70 fi @@ -61,6 +85,7 @@ selectWindowsImage() { fi if ! extractImage "$iso" "$dir" "$VERSION"; then + error "Failed to extract the Windows installation image!" removeImage "$iso" || : return 72 fi @@ -76,11 +101,21 @@ selectWindowsImage() { detectIsoImage "$iso" && return 0 rc=$? - (( rc == 1 )) || return 76 + if (( rc != 1 )); then + error "Failed to inspect the Windows installation ISO!" + return 76 + fi + + elif [[ "${iso,,}" == *.esd ]]; then + + detectESDImage "$iso" && return 0 + error "Failed to inspect the Windows installation ESD!" + return 76 fi if ! extractImage "$iso" "$dir" "$VERSION"; then + error "Failed to extract the Windows installation image!" removeImage "$iso" || : return 74 fi @@ -90,9 +125,15 @@ selectWindowsImage() { detectImage "$dir" && return 0 rc=$? - (( rc == 1 )) || return 76 + if (( rc != 1 )); then + error "Failed to detect the extracted Windows installation image!" + return 76 + fi - skipUnattended "$dir" "$iso" "$boot" || return 76 + skipUnattended "$dir" "$iso" "$boot" || { + error "Failed to fall back to manual installation!" + return 76 + } handled=1 return 0 @@ -108,24 +149,37 @@ configureMachine() { desc=$(printVariant "$DETECTED" "$DETECTED") || return 78 if ! checkMemory "$DETECTED"; then + if [ -z "$CUSTOM" ]; then - useOriginalImage "$iso" || return 79 + useOriginalImage "$iso" || { + error "Failed to preserve the original installation image!" + return 79 + } fi + return 79 fi if ! setMachine "$DETECTED" "$iso" "$dir" "$desc"; then + error "Failed to configure the virtual machine for $desc!" return 80 fi if ! restoreMachineState; then + error "Failed to restore the saved machine state!" return 82 fi if ! supportsUnattended "$DETECTED"; then - skipUnattended "$dir" "$iso" "$boot" "N" || return 83 + + skipUnattended "$dir" "$iso" "$boot" "N" || { + error "Failed to fall back to manual installation!" + return 83 + } + handled=1 return 0 + fi return 0 @@ -141,16 +195,23 @@ prepareWindowsImage() { if supportsXML "$DETECTED"; then if ! createOverlay "$XML" "$LANGUAGE" "$TMP/setup"; then + error "Failed to create the Windows setup overlay!" return 84 fi if ! createSetupImage "$TMP/setup" "$STORAGE/setup.img"; then + error "Failed to create the Windows setup image!" return 86 fi # Bootable ISOs can be reused unchanged with the generated setup image. - if (( ! extracted )); then - useOriginalImage "$iso" || return 88 + if (( ! extracted )) && isDirectImage "$iso"; then + + useOriginalImage "$iso" || { + error "Failed to preserve the original installation image!" + return 88 + } + return 0 fi @@ -158,28 +219,54 @@ prepareWindowsImage() { # Extracted modern sources and SIF-based legacy media require a clean rebuild. if (( ! extracted )); then + if ! extractImage "$iso" "$dir" "$VERSION"; then + error "Failed to extract the Windows installation image!" removeImage "$iso" || : return 90 fi + fi if ! prepareImage "$iso" "$dir"; then + error "Failed to prepare the Windows installation image!" return 92 fi - removeImage "$iso" || return 96 - buildImage "$dir" || return 98 + removeImage "$iso" || { + error "Failed to remove the source installation image!" + return 96 + } + + buildImage "$dir" || { + error "Failed to build the Windows installation image!" + return 98 + } return 0 } bootWindows() { - restoreMachineState || return - restoreBootMode || return - restoreMachine || return - reserveSambaPorts || return + if ! restoreMachineState; then + error "Failed to restore the saved machine state!" + return 1 + fi + + if ! restoreBootMode; then + error "Failed to restore the saved boot mode!" + return 1 + fi + + if ! restoreMachine; then + error "Failed to restore the saved machine type!" + return 1 + fi + + if ! reserveSambaPorts; then + error "Failed to reserve Samba ports!" + return 1 + fi return 0 } @@ -211,13 +298,17 @@ startInstall() { case "${boot,,}" in "windows."* ) error "The download filename \"$file\" uses the reserved \"windows.*\" namespace!" - exit 58 ;; + return 58 ;; esac else local language - language=$(getLanguage "$LANGUAGE" "culture") + if ! language=$(getLanguage "$LANGUAGE" "culture"); then + error "Failed to determine the Windows language!" + return 62 + fi + language="${language%%-*}" if [ -n "$language" ] && [[ "${language,,}" != "en" ]]; then @@ -235,28 +326,32 @@ startInstall() { if ! rm -rf -- "$TMP"; then error "Failed to remove directory \"$TMP\" !" - exit 50 + return 50 fi local setup="$STORAGE/setup.img" if ! rm -f -- "$setup" "${setup}.tmp"; then error "Failed to remove setup image \"$setup\" !" - exit 50 + return 50 fi local previousBase if ! previousBase=$(readState "base"); then error "Failed to read the previous installation state!" - exit 50 + return 50 fi - skipInstall "$BOOT" "$previousBase" && return 1 + local rc=0 + skipInstall "$BOOT" "$previousBase" || rc=$? + + (( rc > 1 )) && return "$rc" + (( rc )) || return 1 if [ -z "$previousBase" ] && hasData; then if enabled "$SHUTDOWN" && [ ! -f "$STORAGE/windows.boot" ]; then - discardPrevious "" || exit 50 + discardPrevious "" || return 50 else if ! backupPrevious ""; then warn "the backup was incomplete, continuing with installation..." @@ -267,7 +362,7 @@ startInstall() { if ! makeDir "$TMP"; then error "Failed to create directory \"$TMP\" !" - exit 50 + return 50 fi if [ -z "$CUSTOM" ]; then @@ -285,27 +380,27 @@ startInstall() { if [ -n "$CUSTOM" ] || [ ! -s "$BOOT" ]; then if ! rm -f -- "$BOOT"; then error "Failed to remove obsolete ISO file \"$BOOT\" !" - exit 50 + return 50 fi fi if ! find "$STORAGE" -maxdepth 1 -type f -iname 'data.*' -not -iname '*.iso' -delete; then error "Failed to remove obsolete disk files from \"$STORAGE\" !" - exit 50 + return 50 fi if ! find "$STORAGE" -maxdepth 1 -type f -iname 'windows.*' -not -iname '*.iso' -delete; then error "Failed to remove obsolete Windows files from \"$STORAGE\" !" - exit 50 + return 50 fi if ! find "$STORAGE" -maxdepth 1 -type f \( -iname '*.rom' -or -iname '*.vars' \) -delete; then error "Failed to remove obsolete firmware files from \"$STORAGE\" !" - exit 50 + return 50 fi if [ -z "$CUSTOM" ] && [[ "${VERSION,,}" != "http"* ]]; then - checkMemory "$VERSION" || exit 67 + checkMemory "$VERSION" || return 67 fi # Work from the temporary directory so the persistent source path can @@ -313,7 +408,7 @@ startInstall() { if [ -z "$CUSTOM" ] && [ -f "$BOOT" ] && [ -s "$BOOT" ]; then if ! mv -f -- "$BOOT" "$ISO"; then error "Failed to move ISO file from \"$BOOT\" to \"$ISO\" !" - exit 50 + return 50 fi fi @@ -333,7 +428,7 @@ skipUnattended() { # so they cannot use the manual-install fallback. if ! isDirectImage "$iso"; then error "Failed to boot \"$iso\" because it is not a directly bootable ISO image!" - exit 60 + return 1 fi # When automatic preparation fails, inspect extracted media to determine @@ -382,7 +477,7 @@ skipInstall() { if ! writeState "base" "$previousBase"; then error "Failed to migrate the previous installation state!" - exit 50 + return 50 fi fi @@ -390,10 +485,12 @@ skipInstall() { # Older releases may have left a rebuilt custom ISO at its synthetic source # identity. A completed installation no longer needs that installation media. if [[ "${previousBase,,}" == "windows."* ]] && hasData && [ -f "$marker" ]; then + if ! rm -f -- "$STORAGE/$previousBase"; then error "Failed to remove obsolete ISO file \"$STORAGE/$previousBase\" !" - exit 50 + return 50 fi + fi # A changed source invalidates an unfinished installation. Back up an @@ -404,7 +501,7 @@ skipInstall() { if ! rm -f -- "$STORAGE/$previousBase"; then error "Failed to remove ISO file \"$STORAGE/$previousBase\" !" - exit 50 + return 50 fi return 1 @@ -430,7 +527,7 @@ skipInstall() { fi if enabled "$SHUTDOWN" && [ ! -f "$marker" ]; then - discardPrevious "$STORAGE/$previousBase" || exit 50 + discardPrevious "$STORAGE/$previousBase" || return 50 return 1 fi @@ -469,7 +566,10 @@ finishInstall() { fi local file="$STORAGE/windows.ver" - cp -f /etc/version "$file" || return 1 + cp -f /etc/version "$file" || { + error "Failed to save the Windows installation version!" + return 1 + } if ! setOwner "$file"; then warn "Failed to set the owner for \"$file\" !" @@ -477,15 +577,23 @@ finishInstall() { if [[ "$boot" == "$STORAGE/"* ]]; then if [[ "$aborted" != [Yy1]* ]] || [ -z "$CUSTOM" ]; then + base=$(basename "$boot") - writeState "base" "$base" || return 1 + writeState "base" "$base" || { + error "Failed to save the Windows installation source!" + return 1 + } + fi fi if [[ "${PLATFORM,,}" == "x64" ]]; then if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then - writeState "mode" "$BOOT_MODE" || return 1 + writeState "mode" "$BOOT_MODE" || { + error "Failed to save the Windows boot mode!" + return 1 + } else @@ -496,14 +604,22 @@ finishInstall() { fi if (( secure )); then + BOOT_MODE="windows_secure" - writeState "mode" "$BOOT_MODE" || return 1 + writeState "mode" "$BOOT_MODE" || { + error "Failed to save the Windows boot mode!" + return 1 + } + fi fi fi - reserveSambaPorts || return 1 + reserveSambaPorts || { + error "Failed to reserve Samba ports!" + return 1 + } if ! rm -rf -- "$TMP"; then error "Failed to remove directory \"$TMP\" !" @@ -516,7 +632,6 @@ finishInstall() { findFile() { local fname="$1" - local dir file base local boot="$STORAGE/windows.boot" @@ -896,7 +1011,12 @@ detectImage() { local image_info image_info=$(readImageInfo "$wim") || return $? - detectImageInfo "$image_info" || return 2 + detectImageInfo "$image_info" || { + error "Failed to process the Windows image metadata!" + return 2 + } + + return 0 } prepareImage() { @@ -1212,7 +1332,10 @@ createOverlay() { return 1 fi - addAnswerFile "$asset" "$language" "$stage" || return 1 + addAnswerFile "$asset" "$language" "$stage" || { + error "Failed to include the Windows answer file!" + return 1 + } return 0 } @@ -1309,8 +1432,8 @@ backupPrevious () { failed="Y" fi - [ -z "$(ls -A "$dir")" ] && rm -rf "$dir" - [ -z "$(ls -A "$root")" ] && rm -rf "$root" + rmdir "$dir" 2>/dev/null || : + rmdir "$root" 2>/dev/null || : [ -n "$failed" ] && return 1 diff --git a/src/mido.sh b/src/mido.sh index 54cec385..cc3237fa 100644 --- a/src/mido.sh +++ b/src/mido.sh @@ -175,13 +175,16 @@ downloadWindowsLink() { --max-filesize 100K \ -- "$skuUrl") || return - # Let jq parsing failures propagate so malformed API data is not mistaken - # for a normal missing-result response. The same applies to the link data. - skuId=$(printf '%s\n' "$skuJson" | jq --arg LANG "$language" -r 'first(.Skus[]? | select(.Language == $LANG) | .Id) // empty') 2>/dev/null || return + skuId=$(printf '%s\n' "$skuJson" | jq --arg LANG "$language" -r 'first(.Skus[]? | select(.Language == $LANG) | .Id) // empty') 2>/dev/null || skuId="" if [ -z "$skuId" ] || [[ "${skuId,,}" == "null" ]]; then - language=$(getLanguage "$lang" "desc") - error "No download in the $language language available for $desc!" + if [[ "${lang,,}" != "en" && "${lang,,}" != "en-"* ]]; then + language=$(getLanguage "$lang" "desc") + error "No download in the $language language available for $desc!" + else + error "Microsoft server provided us no SKU ID in response to our request!" + info "Response: $skuJson" + fi return 1 fi @@ -214,10 +217,10 @@ downloadWindowsLink() { return 1 fi - link=$(printf '%s\n' "$linkJson" | jq --argjson TYPE "$type" -r 'first(.ProductDownloadOptions[]? | select(.DownloadType == $TYPE) | .Uri) // empty') 2>/dev/null || return + link=$(printf '%s\n' "$linkJson" | jq --argjson TYPE "$type" -r 'first(.ProductDownloadOptions[]? | select(.DownloadType == $TYPE) | .Uri) // empty') 2>/dev/null || link="" if [ -z "$link" ] || [[ "${link,,}" == "null" ]]; then - error "Microsoft server gave us no download link to our request for an automated download!" + error "Microsoft server provided us no download link to our request for an automated download!" info "Response: $linkJson" return 1 fi @@ -287,7 +290,7 @@ downloadWindows() { grep -Eio "]*value=[\"'][0-9]+[\"'][^>]*>[[:space:]]*Windows[^<]*" | sed -nE "s/.*value=[\"']([0-9]+)[\"'].*/\1/p" | sed -n '1p' | - cut -c 1-16) || return + cut -c 1-16) || productId="" enabled "$DEBUG" && echo "$productId" if [ -z "$productId" ]; then @@ -1119,7 +1122,7 @@ verifyFile() { info "Successfully verified $type!" && return 0 fi - error "The downloaded file has an unknown $algo checksum: $hash , as the expected value was: $check. Please report this at $SUPPORT/issues" + warn "the downloaded file has an unknown $algo checksum: $hash , as the expected value was: $check. Please report this at $SUPPORT/issues" return 1 } @@ -1280,7 +1283,11 @@ downloadImage() { desc=$(fromFile "$base") web_desc="$desc" - tryDownload "$iso" "$version" "" "" "$desc" "$seconds" "$web_desc" || return + tryDownload "$iso" "$version" "" "" "$desc" "$seconds" "$web_desc" || { + rc=$? + error "Failed to download the Windows image from the specified URL!" + return "$rc" + } return 0 fi @@ -1450,6 +1457,8 @@ downloadImage() { if [[ "$tried" == "n" ]]; then error "No download method is available for $desc!" + else + error "All download methods failed for $desc!" fi return 1