From 43be9024e99b707d307359b665876bf4659fccac Mon Sep 17 00:00:00 2001 From: Kroese Date: Sat, 8 Aug 2026 16:33:27 +0200 Subject: [PATCH] fix: Do not fail if marker cannot be created (#2117) --- src/answer.sh | 156 ++++++++++++++++++++++++++----------------------- src/image.sh | 84 ++++++++++++++++---------- src/install.sh | 85 ++++++++++++++++----------- src/mido.sh | 36 +++++++----- src/power.sh | 2 +- 5 files changed, 207 insertions(+), 156 deletions(-) diff --git a/src/answer.sh b/src/answer.sh index 390be5c0..448d5d65 100644 --- a/src/answer.sh +++ b/src/answer.sh @@ -133,16 +133,10 @@ hasAnswerFile() { fi # Editions without a dedicated template can use the generic template. - case "${id,,}" in + file="/run/assets/${id%%-*}.xml" + [ -s "$file" ] || return 1 - "win7"* | "win8"* | "win10"* | "win11"* | "winvista"* | "win20"* ) - - file="/run/assets/${id%%-*}.xml" - [ -s "$file" ] && return 0 ;; - - esac - - return 1 + return 0 } addAnswerFile() { @@ -444,22 +438,25 @@ generateFallbackXML() { updateUserXML() { local asset="$1" - - local setup="$XML_COMPONENT_SETUP" - local specialize="$XML_COMPONENT_SHELL_SPECIALIZE" - local oobe="$XML_COMPONENT_SHELL_OOBE" local app="$APP for $ENGINE" + local xpath + xpath="$XML_COMPONENT_SETUP/u:UserData/u:Organization" + xpath+=" | $XML_COMPONENT_SHELL_SPECIALIZE/u:OEMInformation/u:Model" + xpath+=" | $XML_COMPONENT_SHELL_SPECIALIZE/u:OEMName" + xpath+=" | $XML_COMPONENT_SHELL_SPECIALIZE/u:RegisteredOwner" + xpath+=" | $XML_COMPONENT_SHELL_OOBE/u:RegisteredOwner" + local -a args=( -L -N "$XML_NS_UNATTEND_ARG" - -u "$setup/u:UserData/u:Organization | $specialize/u:OEMInformation/u:Model | $specialize/u:OEMName | $specialize/u:RegisteredOwner | $oobe/u:RegisteredOwner" -v "$app" - -u "$oobe/u:Display/u:VerticalResolution" -v "$HEIGHT" - -u "$oobe/u:Display/u:HorizontalResolution" -v "$WIDTH" + -u "$xpath" -v "$app" + -u "$XML_COMPONENT_SHELL_OOBE/u:Display/u:VerticalResolution" -v "$HEIGHT" + -u "$XML_COMPONENT_SHELL_OOBE/u:Display/u:HorizontalResolution" -v "$WIDTH" ) if [ -n "${HOST:-}" ]; then - args+=(-u "$specialize/u:ComputerName" -v "$HOST") + args+=(-u "$XML_COMPONENT_SHELL_SPECIALIZE/u:ComputerName" -v "$HOST") fi xmlstarlet ed "${args[@]}" "$asset" || return 1 @@ -537,23 +534,27 @@ updateWorkgroup() { local asset="$1" local workgroup="$2" - - local component="$XML_COMPONENT_UNATTENDED_JOIN" - local identification="$component/u:Identification" - local join="$identification/*[local-name()='JoinWorkgroup']" - local arch tmp + local arch tmp xpath arch=$(getXMLArchitecture "$asset") || return 1 + # Apply all membership changes to a copy and publish it only after the old # domain, credential, OU, and workgroup nodes have been replaced successfully. tmp=$(copyXMLAsset "$asset") || return 1 + xpath="$XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:Credentials" + xpath+=" | $XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:JoinDomain" + xpath+=" | $XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:JoinWorkgroup" + xpath+=" | $XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:MachineObjectOU" + if ! ensureUnattendedJoin "$tmp" "$arch" || ! xmlstarlet ed -L \ -N "$XML_NS_UNATTEND_ARG" \ - -d "$identification/u:Credentials | $identification/u:JoinDomain | $identification/u:JoinWorkgroup | $identification/u:MachineObjectOU" \ - -s "$identification" -t elem -n 'JoinWorkgroup' "$tmp" || - ! xmlstarlet ed -L -N "$XML_NS_UNATTEND_ARG" -u "$join" -v "$workgroup" "$tmp" || + -d "$xpath" \ + -s "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification" -t elem -n 'JoinWorkgroup' "$tmp" || + ! xmlstarlet ed -L \ + -N "$XML_NS_UNATTEND_ARG" \ + -u "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification/*[local-name()='JoinWorkgroup']" -v "$workgroup" "$tmp" || ! replaceXMLAsset "$asset" "$tmp"; then rm -f "$tmp" @@ -575,6 +576,7 @@ updateDomain() { local arch tmp arch=$(getXMLArchitecture "$asset") || return 1 + # Account and join settings are separate XML transformations, so update a # copy to keep the original answer file intact if either transformation fails. tmp=$(copyXMLAsset "$asset") || return 1 @@ -602,7 +604,8 @@ configureDomainAccounts() { local administrator="$accounts/u:AdministratorPassword" local autologon="$shell/u:AutoLogon" local domain_accounts="$accounts/u:DomainAccounts" - local counts shell_count accounts_count administrator_count autologon_count child_count + local counts shell_count administrator_count + local accounts_count autologon_count child_count counts=$(xmlstarlet sel \ -N "$XML_NS_UNATTEND_ARG" \ @@ -694,15 +697,19 @@ configureDomainJoin() { local arch="$6" local cred_domain="$domain" - local component="$XML_COMPONENT_UNATTENDED_JOIN" - local identification="$component/u:Identification" - local credentials="$identification/*[local-name()='Credentials']" + local credentials="$XML_COMPONENT_UNATTENDED_JOIN/u:Identification/*[local-name()='Credentials']" + + local xpath + xpath="$XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:Credentials" + xpath+=" | $XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:JoinDomain" + xpath+=" | $XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:JoinWorkgroup" + xpath+=" | $XML_COMPONENT_UNATTENDED_JOIN/u:Identification/u:MachineObjectOU" local -a args=( -L -N "$XML_NS_UNATTEND_ARG" - -d "$identification/u:Credentials | $identification/u:JoinDomain | $identification/u:JoinWorkgroup | $identification/u:MachineObjectOU" - -s "$identification" -t elem -n 'Credentials' + -d "$xpath" + -s "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification" -t elem -n 'Credentials' ) ensureUnattendedJoin "$asset" "$arch" || return 1 @@ -720,11 +727,11 @@ configureDomainJoin() { args+=( -s "$credentials" -t elem -n 'Username' -s "$credentials" -t elem -n 'Password' - -s "$identification" -t elem -n 'JoinDomain' + -s "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification" -t elem -n 'JoinDomain' ) if [ -n "$ou" ]; then - args+=(-s "$identification" -t elem -n 'MachineObjectOU') + args+=(-s "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification" -t elem -n 'MachineObjectOU') fi xmlstarlet ed "${args[@]}" "$asset" || return 1 @@ -734,7 +741,7 @@ configureDomainJoin() { -N "$XML_NS_UNATTEND_ARG" -u "$credentials/*[local-name()='Username']" -v "$auth" -u "$credentials/*[local-name()='Password']" -v "$pass" - -u "$identification/*[local-name()='JoinDomain']" -v "$domain" + -u "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification/*[local-name()='JoinDomain']" -v "$domain" ) if [ -n "$cred_domain" ]; then @@ -742,7 +749,7 @@ configureDomainJoin() { fi if [ -n "$ou" ]; then - values+=(-u "$identification/*[local-name()='MachineObjectOU']" -v "$ou") + values+=(-u "$XML_COMPONENT_UNATTENDED_JOIN/u:Identification/*[local-name()='MachineObjectOU']" -v "$ou") fi xmlstarlet ed "${values[@]}" "$asset" || return 1 @@ -761,10 +768,9 @@ findPrimaryLocalAccount() { local auto_primary=0 auto_matches=0 local admin_primary=0 admin_matches=0 local selected=0 separator=$'\x1f' - - local -a groups=() local counts records auto_user selected_user position name group local shell_count local_count found_admin found_autologon token + local -a groups=() counts=$(xmlstarlet sel \ -N "$XML_NS_UNATTEND_ARG" \ @@ -869,6 +875,7 @@ updateLocalAccount() { result=$(findPrimaryLocalAccount "$asset") || return 1 mapfile -t values <<< "$result" (( ${#values[@]} == 4 )) || return 1 + primary="${values[0]}" current_user="${values[1]}" admin_count="${values[2]}" @@ -991,9 +998,9 @@ updateEditionXML() { local asset="$1" + local setup="$XML_COMPONENT_SETUP" local upper='ABCDEFGHIJKLMNOPQRSTUVWXYZ' local lower='abcdefghijklmnopqrstuvwxyz' - local setup="$XML_COMPONENT_SETUP" local selector="$setup/u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[translate(normalize-space(u:Key), '$lower', '$upper')='/IMAGE/NAME']/u:Value" local edition count records position value replacement local separator=$'\x1f' @@ -1014,17 +1021,18 @@ updateEditionXML() { -N "$XML_NS_UNATTEND_ARG" -T -t -m "$selector" -v 'position()' -o "$separator" -v 'string(.)' -n "$asset") || return 1 while IFS="$separator" read -r position value; do + [ -n "$position" ] || continue - # Only Windows Server templates use EDITION as a mutable answer-file - # selector. Products such as Hyper-V Server have fixed SERVER* flags that - # must not be rewritten. + # Only Windows Server templates use EDITION as a mutable answer-file selector. + # Products such as Hyper-V Server have fixed SERVER* flags that must not be rewritten. [[ "${value,,}" == *"windows server"* ]] || continue [[ "$value" =~ ^(.*[[:space:]])SERVER[A-Za-z0-9_-]+[[:space:]]*$ ]] || continue replacement="${BASH_REMATCH[1]}SERVER$edition" xmlstarlet ed -L -N "$XML_NS_UNATTEND_ARG" -u "($selector)[$position]" -v "$replacement" "$asset" || return 1 + done <<< "$records" return 0 @@ -1169,29 +1177,30 @@ setConfigurationXML() { return 1 } - if [ "$config_count" = "0" ] && - ! ensureXMLDefaultNamespace "$tmp"; then + local msg="Failed to enable the Windows configuration set!" + if [ "$config_count" = "0" ] && ! ensureXMLDefaultNamespace "$tmp"; then rm -f "$tmp" + error "$msg (1)" return 1 fi if [ "$config_count" = "1" ]; then xmlstarlet ed -L -N "$XML_NS_UNATTEND_ARG" -u "$config" -v "true" "$tmp" || { rm -f "$tmp" - error "Failed to enable the Windows configuration set!" + error "$msg (2)" return 1 } elif [ "$userdata_count" = "1" ]; then xmlstarlet ed -L -N "$XML_NS_UNATTEND_ARG" -i "$userdata" -t elem -n "UseConfigurationSet" -v "true" "$tmp" || { rm -f "$tmp" - error "Failed to enable the Windows configuration set!" + error "$msg (3)" return 1 } else xmlstarlet ed -L -N "$XML_NS_UNATTEND_ARG" -s "$setup" -t elem -n "UseConfigurationSet" -v "true" "$tmp" || { rm -f "$tmp" - error "Failed to enable the Windows configuration set!" + error "$msg (4)" return 1 } fi @@ -1203,7 +1212,7 @@ setConfigurationXML() { if [ "$result_count" != "1" ]; then rm -f "$tmp" - error "Failed to enable the Windows configuration set!" + error "$msg (5)" return 1 fi @@ -1219,8 +1228,7 @@ removeSharedFolder() { local script="$1" - if ! disabled "${SHORTCUT:-}" && - ! disabled "${SAMBA:-}"; then + if ! disabled "${SHORTCUT:-}" && ! disabled "${SAMBA:-}"; then return 0 fi @@ -1299,13 +1307,14 @@ removeEmbeddedProductKeys() { validateXMLSettings() { - validateResolution "WIDTH" "$WIDTH" 320 || return 1 - validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 validateMembership || return 1 validateComputerName "${HOST:-}" || return 1 validateProductKey "${KEY:-}" || return 1 validatePassword "${PASSWORD:-}" || return 1 + validateResolution "WIDTH" "$WIDTH" 320 || return 1 + validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 + return 0 } @@ -1828,15 +1837,13 @@ encodeUnattendPassword() { # Windows unattend password fields use a field-specific suffix before # UTF-16LE/Base64 encoding; this is obfuscation rather than encryption. - printf '%s' "${password}${suffix}" | - iconv -f utf-8 -t utf-16le | - base64 -w 0 + printf '%s' "${password}${suffix}" | iconv -f utf-8 -t utf-16le | base64 -w 0 + } enableLog() { local script="$1" - local content enabled "${LOG:-}" || return 0 @@ -1887,7 +1894,6 @@ getXMLNodeCount() { local xpath="$2" xmlstarlet sel -N "$XML_NS_UNATTEND_ARG" -T -t -v "count($xpath)" "$asset" - } validateUniqueXMLNodes() { @@ -2001,8 +2007,11 @@ updateSetupScript() { fi if [ -n "$domain" ]; then + removeSetupBlock "$script" "LOCAL_ACCOUNT" || return 1 + elif [ -n "$user" ]; then + validateUsername "$user" "local" || return 1 id=$(basename "$asset") || return 1 @@ -2026,6 +2035,7 @@ updateSetupScript() { esac replaceSetupBlock "$script" "LOCAL_ACCOUNT" "$content" || return 1 + fi enableLog "$script" || return 1 @@ -2047,9 +2057,9 @@ findSetupScript() { dir=$(dirname "$asset") || return 1 name=$(basename "$asset") || return 1 + id="${name%.*}" normal="$id" - candidates+=("$dir/$id.cmd") if [[ "${normal,,}" == *"-eval" ]]; then @@ -2057,19 +2067,19 @@ findSetupScript() { candidates+=("$dir/$normal.cmd") fi - # Generated edition-specific answer files inherit the script belonging to - # their generic source template. - case "${normal,,}" in - "win7"* | "win8"* | "win10"* | "win11"* | "winvista"* | "win20"* ) - candidates+=("$dir/${normal%%-*}.cmd") - ;; - esac + # Generated edition-specific answer files inherit the + # script belonging to their generic source template. + if [[ "$normal" == *-* ]]; then + candidates+=("$dir/${normal%%-*}.cmd") + fi for candidate in "${candidates[@]}"; do + if [ -f "$candidate" ] && [ -s "$candidate" ]; then printf '%s' "$candidate" return 0 fi + done error "Failed to find setup script for answer file: $asset" @@ -2082,7 +2092,6 @@ stageSetupScript() { local stage="$2" local source target - source=$(findSetupScript "$asset") || return 1 [ -n "$source" ] || return 0 @@ -2139,6 +2148,7 @@ rewriteSetupBlock() { while IFS= read -r line || [ -n "$line" ]; do if [ "$line" = "$begin" ]; then + if [ "$action" = "replace" ]; then if ! printf '%s\n' "$line" >> "$tmp" || ! printf '%s\n' "$content" >> "$tmp"; then @@ -2152,6 +2162,7 @@ rewriteSetupBlock() { fi if [ "$line" = "$end" ]; then + inside=0 if [ "$action" = "replace" ]; then @@ -2165,10 +2176,12 @@ rewriteSetupBlock() { fi if (( ! inside )); then + if ! printf '%s\n' "$line" >> "$tmp"; then rm -f "$tmp" return 1 fi + fi done < "$file" @@ -2225,7 +2238,6 @@ escapeSIFValue() { escapeRegistryValue() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' - } extractDrivers() { @@ -2382,15 +2394,14 @@ addLegacyDrivers() { local arch="$4" local drivers="$5" - local file local msg="Adding drivers to image..." - info "$msg" && html "$msg" extractDrivers "$drivers" || return 1 copyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 addNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 + local file file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 if [ -z "$file" ]; then @@ -2414,7 +2425,6 @@ setLegacyKey() { local desc="$4" local setup pid key file - setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1 [[ -n "$setup" ]] || return 0 @@ -2440,7 +2450,9 @@ setLegacyKey() { # 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="" + else key="${pid: -8:5}" @@ -2473,8 +2485,7 @@ setLegacyKey() { else # Windows XP Professional x64 generic trial key (no activation) KEY="B2RBK-7KPT9-4JP6X-QQFWM-PJD6G" - fi - ;; + fi ;; "2k3" ) @@ -2484,8 +2495,7 @@ setLegacyKey() { else # Windows Server 2003 Standard x64 generic trial key (no activation) KEY="P4WJG-WK3W7-3HM8W-RWHCK-8JTRY" - fi - ;; + fi ;; esac diff --git a/src/image.sh b/src/image.sh index 01dfe666..e3811d51 100644 --- a/src/image.sh +++ b/src/image.sh @@ -195,6 +195,7 @@ selectVersion() { IFS=$'\t' read -r id index <<< "$record" [[ "${id,,}" == "${candidate,,}" ]] || continue + hasAnswerFile "$id" || continue printf '%s\n%s\n' "$id" "$index" @@ -278,6 +279,7 @@ detectVersion() { for record in "${images[@]}"; do IFS=$'\t' read -r id index <<< "$record" + hasAnswerFile "$id" || continue rank=$(getEditionRank "$id") @@ -327,7 +329,6 @@ detectLanguage() { for path in "${paths[@]}"; do lang=$(xmlstarlet sel -T -t -v "normalize-space(string(($path)[1]))" - 2>/dev/null <<< "$xml") || lang="" - [ -n "$lang" ] && break done @@ -502,8 +503,6 @@ createImageDirectory() { local image="$1" local directory="$2" - # Treat an existing directory as success; create it only when mdir cannot - # already resolve it. if mdir -i "$image" "$directory" >/dev/null 2>&1; then return 0 fi @@ -560,20 +559,25 @@ createSetupImage() { fi for entry in "${entries[@]}"; do + if ! mcopy -Q -s -i "$tmp" "$entry" ::; then rm -f -- "$tmp" error "Failed to copy image file: $entry" return 1 fi + done if [ -n "$folder" ] || [ -f "$install" ]; then + if ! createImageDirectory "$tmp" "::/\$OEM\$" || ! createImageDirectory "$tmp" "::/\$OEM\$/\$1" || ! createImageDirectory "$tmp" "$target"; then + rm -f -- "$tmp" error "Failed to create OEM directory in setup image!" return 1 fi + fi if [ -n "$folder" ]; then @@ -592,11 +596,13 @@ createSetupImage() { fi for entry in "${entries[@]}"; do + if ! mcopy -Q -s -o -i "$tmp" "$entry" "$target"; then rm -f -- "$tmp" error "Failed to copy OEM file: $entry" return 1 fi + done fi @@ -604,11 +610,13 @@ createSetupImage() { # Copy the generated overlay script last so it replaces an install.bat from # the mounted OEM folder when both are present. if [ -f "$install" ]; then + if ! mcopy -Q -o -i "$tmp" "$install" "$target/install.bat"; then rm -f -- "$tmp" error "Failed to replace install.bat in setup image!" return 1 fi + fi # Verify that mtools can read the completed filesystem before publishing it. @@ -773,7 +781,6 @@ detectLegacy() { detectReactOS() { local dir="$1" - local marker marker=$(find "$dir" -maxdepth 2 -type f \ @@ -885,8 +892,8 @@ readIsoImageInfo() { local image="$2" local header="$3" - local raw result root xml_count rc - local header_size version + local raw result xml_count rc + local root header_size version local part_number total_parts image_count local xml_offset xml_size xml_original xml_flags local -a bytes=() values=() @@ -1172,8 +1179,10 @@ findImage() { fi for name in install.wim install.esd; do + result=$(find "$sources" -maxdepth 1 -type f -iname "$name" -print -quit) || return 2 [ -n "$result" ] && break + done if [ ! -f "$result" ]; then @@ -1222,13 +1231,14 @@ validateEdition() { # Discard a stale server-edition override when it conflicts with the image # that was actually detected. EDITION="" + return 0 } unknownImage() { - local msg="Failed to determine Windows version from image" local rc=0 + local msg="Failed to determine Windows version from image" setXML "" || rc=$? @@ -1252,9 +1262,12 @@ describeImage() { result=$(printEdition "$DETECTED" "$DETECTED" "Y") || return 1 if [[ "${LANGUAGE,,}" != "en" && "${LANGUAGE,,}" != "en-"* ]]; then + local language language=$(getLanguage "$LANGUAGE" "desc") || return 1 + result+=" ($language)" + fi printf '%s' "$result" @@ -1373,7 +1386,6 @@ detectImageInfo() { detectIsoImage() { local iso="$1" - local image header image_info rc # Return 1 only when no directly inspectable WIM/ESD payload is available so @@ -1405,19 +1417,39 @@ detectIsoImage() { return 0 } +baseDir() { + + local path="${1%/}" + + [[ -z "$path" || "$path" == "/" ]] && { + echo "/" + return 0 + } + + path="${path#/}" + path="${path%%/*}" + + echo "/$path" + return 0 +} + checkFreeSpace() { local dir="$1" local size="$2" - local space size_gb space_gb + local base space size_gb space_gb + base=$(baseDir "$dir") - space=$(df --output=avail -B 1 "$dir" | tail -n 1) || return 1 - - [[ "$space" =~ ^[[:space:]]*[0-9]+[[:space:]]*$ ]] || { - error "Failed to determine available disk space for $dir!" + if ! space=$(df --output=avail -B 1 "$dir" | tail -n 1); then + error "Failed to check free space in $dir." return 1 - } + fi + + if [[ ! "$space" =~ ^[[:space:]]*[0-9]+[[:space:]]*$ ]]; then + error "Failed to determine available disk space for $dir." + return 1 + fi space="${space//[[:space:]]/}" @@ -1426,8 +1458,9 @@ checkFreeSpace() { size_gb=$(formatBytes "$size") space_gb=$(formatBytes "$space") - error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." + error "Not enough free space in $base, have $space_gb available but need at least $size_gb." return 1 + fi return 0 @@ -1462,7 +1495,7 @@ extractESD() { fi if (( size < minSize )); then - error "The downloaded ISO file is too small!" + error "The downloaded ESD file is too small!" return 1 fi @@ -1514,8 +1547,7 @@ extractESD() { bootTotal="${fields[1]:-}" bootLinks="${fields[2]:-}" - 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 @@ -1537,6 +1569,7 @@ extractESD() { # installation media. Peak additional usage consists of the extracted setup # files and boot.wim, plus the final ISO containing those files and the ESD. local freeSpace=$(( size + 2 * (bootSize + wimSize) + spacePad )) + checkFreeSpace "$dir" "$freeSpace" || return /run/progress.sh "$dir" "$bootSize" "$msg ([P])..." & @@ -1585,6 +1618,7 @@ extractESD() { } fKill "progress.sh" + html "$msg..." if [[ "${PLATFORM,,}" == "x64" ]]; then LABEL="CCCOMA_X64FRE_EN-US_DV9" @@ -1592,9 +1626,6 @@ extractESD() { LABEL="CPBA_A64FRE_EN-US_DV9" fi - msg="Extracting image from ESD file" - info "$msg..." && html "$msg..." - index="" if [[ "${version,,}" == "http"* ]]; then @@ -1649,10 +1680,8 @@ extractESD() { fi installSize=$(( size + installPad )) - /run/progress.sh "$installWim" "$installSize" "$msg ([P])..." & if ! rm -f -- "$dir/sources/install.wim" "$installWim"; then - fKill "progress.sh" error "Failed to remove previous Windows installation image!" return 1 fi @@ -1660,7 +1689,6 @@ extractESD() { # Reuse the downloaded solid ESD instead of exporting the selected image. # Both paths are below $TMP, so this is a same-filesystem rename. if ! mv -f -- "$iso" "$installWim"; then - fKill "progress.sh" error "Failed to move downloaded ESD file into the installation media!" return 1 fi @@ -1673,7 +1701,6 @@ extractESD() { wimlib-imagex delete "$installWim" "$image" --soft --quiet || { ret=$? - fKill "progress.sh" error "Failed to remove image $image from install.esd!" return "$ret" } @@ -1683,7 +1710,6 @@ extractESD() { result=$(wimlib-imagex info "$installWim" --xml 2>/dev/null | iconv -f UTF-16LE -t UTF-8 2>/dev/null) || { ret=$? - fKill "progress.sh" error "Cannot verify the prepared install.esd file!" return "$ret" } @@ -1693,7 +1719,6 @@ extractESD() { -v 'count(/WIM/IMAGE[@INDEX="1"])' -n \ -v 'normalize-space(/WIM/IMAGE[@INDEX="1"]/DESCRIPTION)' -n \ <<< "$result" 2>/dev/null); then - fKill "progress.sh" error "Cannot verify the prepared install.esd file!" return 1 fi @@ -1705,26 +1730,22 @@ extractESD() { resultEdition="${fields[2]:-}" if [[ "$resultCount" != "1" ]] || [[ "$resultIndex" != "1" ]]; then - fKill "progress.sh" error "Prepared install.esd does not contain exactly one image at index 1!" return 1 fi if [[ "${version,,}" != "http"* ]] && [[ "${resultEdition,,}" != "${edition,,}" ]]; then - fKill "progress.sh" error "Prepared install.esd does not contain only '$edition' at index 1!" return 1 fi - fKill "progress.sh" return 0 } normalizeBatch() { local file="$1" - local bom tmp encoding [ ! -s "$file" ] && return 0 @@ -1768,7 +1789,6 @@ reportBatchMatches() { local suggestion="$5" local matches line - matches=$(grep -Pin "$pattern" "$file" || true) [ -n "$matches" ] || return 0 diff --git a/src/install.sh b/src/install.sh index 1ac19302..74a416e1 100644 --- a/src/install.sh +++ b/src/install.sh @@ -43,16 +43,12 @@ selectWindowsImage() { local dir="$2" local boot="$3" + local rc + XML="" FB="falling back to manual installation!" - # Known catalog versions already provide the required image metadata. - if [ -z "$DETECTED" ] && [ -z "$CUSTOM" ] && [[ "${VERSION,,}" != "http"* ]]; then - DETECTED="$VERSION" - fi - - DETECTED="${DETECTED/-enterprise-iot/-iot}" - DETECTED="${DETECTED/-enterprise-ltsc/-ltsc}" + normalizeDetected || return 70 if [ -n "$DETECTED" ]; then @@ -74,11 +70,15 @@ selectWindowsImage() { fi - # Inspect unknown media directly before falling back to extraction. - detectIsoImage "$iso" && return 0 + # Inspect unknown bootable media directly before falling back to extraction. + if [[ "${iso,,}" != *.esd ]]; then - local rc=$? - (( rc == 1 )) || return 76 + detectIsoImage "$iso" && return 0 + + rc=$? + (( rc == 1 )) || return 76 + + fi if ! extractImage "$iso" "$dir" "$VERSION"; then removeImage "$iso" || : @@ -105,10 +105,9 @@ configureMachine() { local boot="$3" local desc - desc=$(printVariant "$DETECTED" "$DETECTED") || return 78 - if ! checkMemory "$DETECTED" "$desc"; then + if ! checkMemory "$DETECTED"; then if [ -z "$CUSTOM" ]; then useOriginalImage "$iso" || return 79 fi @@ -146,7 +145,7 @@ prepareWindowsImage() { fi if ! createSetupImage "$TMP/setup" "$STORAGE/setup.img"; then - exit 86 + return 86 fi # Bootable ISOs can be reused unchanged with the generated setup image. @@ -385,7 +384,7 @@ skipInstall() { local iso="$1" local previousBase="$2" - local boot="$STORAGE/windows.boot" + local marker="$STORAGE/windows.boot" if [ -n "$previousBase" ]; then @@ -409,7 +408,7 @@ 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."* ]] && [ -f "$boot" ] && hasData; then + if [[ "${previousBase,,}" == "windows."* ]] && hasData && [ -f "$marker" ]; then if ! rm -f -- "$STORAGE/$previousBase"; then error "Failed to remove obsolete ISO file \"$STORAGE/$previousBase\" !" exit 50 @@ -441,7 +440,7 @@ skipInstall() { else method="your custom .iso file was removed" - if [ -f "$boot" ] && hasData; then + if hasData && [ -f "$marker" ]; then info "Detected that $method, will be ignored." return 0 fi @@ -449,7 +448,7 @@ skipInstall() { fi fi - if enabled "$SHUTDOWN" && [ ! -f "$boot" ]; then + if enabled "$SHUTDOWN" && [ ! -f "$marker" ]; then discardPrevious "$STORAGE/$previousBase" || exit 50 return 1 fi @@ -465,7 +464,7 @@ skipInstall() { fi fi - [ -f "$boot" ] && hasData && return 0 + hasData && [ -f "$marker" ] && return 0 return 1 } @@ -476,7 +475,7 @@ finishInstall() { local aborted="$2" local boot="$3" - local base + local base secure=0 if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then error "Failed to find ISO file: $iso" && return 1 @@ -509,11 +508,9 @@ finishInstall() { else - local secure=0 - # Aborted Win11 installs boot without any answer file present, # so enable Secure Boot and TPM to satisfy its hardware checks. - if [[ "$aborted" == [Yy1]* ]] || enabled "$MANUAL"; then + if enabled "$aborted" || enabled "$MANUAL"; then [[ "${DETECTED,,}" == "win11"* ]] && secure=1 fi @@ -578,6 +575,7 @@ findFile() { ISO="$file" CUSTOM="$file" + # Encode the custom ISO size in a synthetic source identity so replacing a # bind-mounted ISO is detected as a different installation source. BOOT="$STORAGE/windows.$size.iso" @@ -585,6 +583,19 @@ findFile() { return 0 } +normalizeDetected() { + + # Known catalog versions already provide the required image metadata. + if [ -z "$DETECTED" ] && [ -z "$CUSTOM" ] && [[ "${VERSION,,}" != "http"* ]]; then + DETECTED="$VERSION" + fi + + DETECTED="${DETECTED/-enterprise-iot/-iot}" + DETECTED="${DETECTED/-enterprise-ltsc/-ltsc}" + + return 0 +} + detectCustom() { CUSTOM="" @@ -629,10 +640,10 @@ removeImage() { setImage() { - supportsXML "${DETECTED,,}" || return 0 - local rc=0 + supportsXML "${DETECTED,,}" || return 0 + setXML "" || rc=$? if (( rc == 0 )); then @@ -688,8 +699,7 @@ getArchiveSize() { local result_name="$2" local -n result="$result_name" - local listing line value - local found=0 rc + local found=0 listing line value rc result=0 @@ -728,8 +738,7 @@ extractImage() { local target="$dir" local desc="local ISO" local archive="${dir}.archive" - local file size required archiveSize - local rc + local file size required archiveSize rc if [ -z "$CUSTOM" ]; then desc="downloaded ISO" @@ -850,25 +859,30 @@ extractImage() { detectImage() { local dir="$1" - local desc rc info "Detecting version from ISO image..." - # Marker-based legacy and ReactOS detection must run before looking for a WIM. + # Marker-based legacy detection must run before looking for a WIM. if detectLegacy "$dir"; then + desc=$(printEdition "$DETECTED" "$DETECTED" "Y") || return 2 + info "Detected: $desc" return 0 + else rc=$? (( rc == 1 )) || return "$rc" fi if detectReactOS "$dir"; then + desc=$(printEdition "$DETECTED" "$DETECTED" "Y") || return 2 + info "Detected: $desc" return 0 + else rc=$? (( rc == 1 )) || return "$rc" @@ -962,9 +976,11 @@ addFolder() { rm -f -- "$install" || return 1 if [ -n "$folder" ]; then + source=$(find -L "$folder" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 if [ -n "$source" ]; then + if ! cp -L -- "$source" "$install"; then error "Failed to create a writable copy of $source!" return 1 @@ -972,6 +988,7 @@ addFolder() { file="$install" fi + fi else @@ -1338,10 +1355,10 @@ 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 + # Restore the saved machine only when q35 is still the default; + # an explicit user-selected machine must remain untouched. [[ "${MACHINE,,}" != "q35" ]] && return 0 + [[ "${PLATFORM,,}" != "x64" ]] && return 0 MACHINE="" restoreState "MACHINE" "old" || return 1 diff --git a/src/mido.sh b/src/mido.sh index 78d75c6e..54cec385 100644 --- a/src/mido.sh +++ b/src/mido.sh @@ -7,8 +7,6 @@ handleCurlError() { local server="$2" local reason="${3:-}" - local signal - if [ -n "$reason" ] && (( code <= 125 )); then error "Request to $server servers failed: ${reason%.}." return 1 @@ -23,6 +21,7 @@ handleCurlError() { return 1 fi + local signal signal=$(kill -l "$((code - 128))" 2>/dev/null || true) case "$signal" in @@ -72,10 +71,7 @@ curlRequest() { rm -f "$log" handleCurlError "$rc" "$server" "$reason" || : - if (( rc >= 129 )); then - return "$rc" - fi - + (( rc >= 129 )) && return "$rc" return 1 fi @@ -579,11 +575,13 @@ getWindows() { "win81${PLATFORM,,}"* | \ "win10${PLATFORM,,}-enterprise-ltsc-eval" | \ "win11${PLATFORM,,}-enterprise-iot-eval" ) + if [[ "${lang,,}" != "en" && "${lang,,}" != "en-"* ]]; then error "No download in the $language language available for $edition!" MIDO_URL="" return 1 fi ;; + esac # ARM64 downloads exist only for the explicitly supported Windows 11 @@ -593,24 +591,28 @@ getWindows() { "win11${PLATFORM,,}" ) ;; "win11${PLATFORM,,}-enterprise"* ) ;; * ) + if [[ "${PLATFORM,,}" != "x64" ]]; then error "No download for the ${PLATFORM^^} platform available for $edition!" MIDO_URL="" return 1 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,,}" ) if downloadWindows "$version" "$lang" "$edition"; then return 0 else rc=$? - (( rc == 1 )) || return "$rc" - fi ;; + fi + + (( rc == 1 )) || return "$rc" ;; "win11${PLATFORM,,}-enterprise"* ) @@ -618,8 +620,9 @@ getWindows() { return 0 else rc=$? - (( rc == 1 )) || return "$rc" - fi ;; + fi + + (( rc == 1 )) || return "$rc" ;; "win2025-eval" | "win2022-eval" | "win2019-eval" | \ "win2019-hv" | "win2016-eval" | "win2012r2-eval" ) @@ -628,14 +631,16 @@ getWindows() { return 0 else rc=$? - (( rc == 1 )) || return "$rc" - fi ;; + fi + + (( rc == 1 )) || return "$rc" ;; "win2008r2"*| "win81${PLATFORM,,}"* | "win10${PLATFORM,,}-enterprise"* ) ;; * ) error "Invalid VERSION specified, value \"$version\" is not recognized!" return 1 ;; + esac # Static catalog URLs are the last resort after live Microsoft methods are @@ -995,10 +1000,7 @@ getESD() { rm -f "$log" - if (( rc >= 129 )); then - return "$rc" - fi - + (( rc >= 129 )) && return "$rc" return 1 fi @@ -1209,6 +1211,7 @@ tryDownload() { if ! rm -f -- "$iso" "$iso.aria2"; then warn "failed to remove invalid download \"$iso\"!" fi + return 2 } @@ -1278,6 +1281,7 @@ downloadImage() { web_desc="$desc" tryDownload "$iso" "$version" "" "" "$desc" "$seconds" "$web_desc" || return + return 0 fi diff --git a/src/power.sh b/src/power.sh index 33e37c5e..26c58f58 100644 --- a/src/power.sh +++ b/src/power.sh @@ -429,7 +429,7 @@ finish() { failed=1 fi - touch "$QEMU_END" + touch "$QEMU_END" || : forceKillQemu "$reason"