#!/usr/bin/env bash set -Eeuo pipefail ETFS="boot/etfsboot.com" FB="falling back to manual installation!" EFISYS="efi/microsoft/boot/efisys_noprompt.bin" backup () { local count=1 local iso="$1" local name="unknown" local root="$STORAGE/backups" local previous="$STORAGE/windows.base" if [ -f "$previous" ]; then previous=$(<"$previous") previous="${previous//[![:print:]]/}" [ -n "$previous" ] && name="${previous%.*}" fi if ! makeDir "$root"; then error "Failed to create directory \"$root\" !" return 1 fi local folder="$name" local dir="$root/$folder" while [ -d "$dir" ] do (( count++ )) folder="${name}.${count}" dir="$root/$folder" done if ! makeDir "$dir"; then error "Failed to create directory \"$dir\" !" return 1 fi [ -f "$iso" ] && mv -f "$iso" "$dir/" find "$STORAGE" -maxdepth 1 -type f -iname 'data.*' -not -iname '*.iso' -exec mv -n {} "$dir/" \; find "$STORAGE" -maxdepth 1 -type f -iname 'windows.*' -not -iname '*.iso' -exec mv -n {} "$dir/" \; find "$STORAGE" -maxdepth 1 -type f \( -iname '*.rom' -or -iname '*.vars' \) -exec mv -n {} "$dir/" \; [ -z "$(ls -A "$dir")" ] && rm -rf "$dir" [ -z "$(ls -A "$root")" ] && rm -rf "$root" return 0 } skipInstall() { local iso="$1" local method="" local magic byte local boot="$STORAGE/windows.boot" local previous="$STORAGE/windows.base" if [ -f "$previous" ]; then previous=$(<"$previous") previous="${previous//[![:print:]]/}" if [ -n "$previous" ]; then if [[ "${STORAGE,,}/${previous,,}" != "${iso,,}" ]]; then if ! hasDisk; then rm -f "$STORAGE/$previous" return 1 fi if [[ "${iso,,}" == "${STORAGE,,}/windows."* ]]; then method="your custom .iso file was changed" else if [[ "${previous,,}" != "windows."* ]]; then method="the VERSION variable was changed" else method="your custom .iso file was removed" if [ -f "$boot" ]; then info "Detected that $method, will be ignored." return 0 fi fi fi info "Detected that $method, a backup of your previous installation will be saved..." ! backup "$STORAGE/$previous" && error "Backup failed!" return 1 fi fi fi [ -f "$boot" ] && hasDisk && return 0 [ ! -f "$iso" ] && return 1 [ ! -s "$iso" ] && return 1 # Check if the ISO was already processed by our script magic=$(dd if="$iso" bs=1 count=1 status=none | tr -d '\000') magic="$(printf '%s' "$magic" | od -A n -t x1 -v | tr -d ' \n')" byte="16" && enabled "$MANUAL" && byte="17" if [[ "$magic" != "$byte" ]]; then info "The ISO will be processed again because the configuration was changed..." return 1 fi return 0 } startInstall() { html "Starting $APP..." if [ -z "$CUSTOM" ]; then local file="${VERSION//\//}.iso" if [[ "${VERSION,,}" == "http"* ]]; then file=$(basename "${VERSION%%\?*}") printf -v file '%b' "${file//%/\\x}" file="${file//[!A-Za-z0-9._-]/_}" else local language language=$(getLanguage "$LANGUAGE" "culture") language="${language%%-*}" if [ -n "$language" ] && [[ "${language,,}" != "en" ]]; then file="${VERSION//\//}_${language,,}.iso" fi fi BOOT="$STORAGE/$file" fi TMP="$STORAGE/tmp" rm -rf "$TMP" skipInstall "$BOOT" && return 1 if hasDisk; then ! backup "" && error "Backup failed!" fi if ! makeDir "$TMP"; then error "Failed to create directory \"$TMP\" !" && exit 50 fi if [ -z "$CUSTOM" ]; then ISO=$(basename "$BOOT") ISO="$TMP/$ISO" if [ -f "$BOOT" ] && [ -s "$BOOT" ]; then mv -f "$BOOT" "$ISO" fi fi rm -f "$BOOT" find "$STORAGE" -maxdepth 1 -type f -iname 'data.*' -not -iname '*.iso' -delete find "$STORAGE" -maxdepth 1 -type f -iname 'windows.*' -not -iname '*.iso' -delete find "$STORAGE" -maxdepth 1 -type f \( -iname '*.rom' -or -iname '*.vars' \) -delete return 0 } writeFile() { local txt="$1" local path="$2" if ! printf '%s\n' "$txt" >"$path"; then error "Failed to write file \"$path\" !" return 1 fi if ! setOwner "$path"; then error "Failed to set the owner for \"$path\" !" return 1 fi return 0 } writeState() { local name="$1" local value="$2" local file="$STORAGE/windows.$name" [ -z "$value" ] && return 0 writeFile "$value" "$file" return $? } readState() { local file="$1" local value [ -s "$file" ] || return 0 value=$(<"$file") || return 1 value="${value//[![:print:]]/}" printf '%s\n' "$value" return 0 } restoreState() { local var="$1" local file="$2" local force="${3:-N}" local value if ! enabled "$force"; then [ -z "${!var:-}" ] || return 0 fi value=$(readState "$file") || return 1 [ -n "$value" ] || return 0 printf -v "$var" '%s' "$value" || return 1 return 0 } checkFreeSpace() { local dir="$1" local size="$2" local size_gb space space_gb size_gb=$(formatBytes "$size") space=$(df --output=avail -B 1 "$dir" | tail -n 1) space_gb=$(formatBytes "$space") if (( size > space )); then error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." return 1 fi return 0 } finishInstall() { local iso="$1" local aborted="$2" local base byte if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then error "Failed to find ISO file: $iso" && return 1 fi if [[ "$iso" == "$STORAGE/"* ]]; then if ! setOwner "$iso"; then error "Failed to set the owner for \"$iso\" !" return 1 fi fi if [[ "$aborted" != [Yy1]* ]]; then # Mark ISO as prepared via magic byte byte="16" && enabled "$MANUAL" && byte="17" if ! printf '%b' "\x$byte" | dd of="$iso" bs=1 seek=0 count=1 conv=notrunc status=none; then warn "failed to set magic byte in ISO file: $iso" fi fi local file="$STORAGE/windows.ver" cp -f /etc/version "$file" || return 1 if ! setOwner "$file"; then error "Failed to set the owner for \"$file\" !" return 1 fi if [[ "$iso" == "$STORAGE/"* ]]; then if [[ "$aborted" != [Yy1]* ]] || [ -z "$CUSTOM" ]; then base=$(basename "$iso") writeState "base" "$base" || return 1 fi fi if [[ "${PLATFORM,,}" == "x64" ]]; then if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then writeState "mode" "$BOOT_MODE" || return 1 if [[ "${MACHINE,,}" != "q35" ]]; then writeState "old" "$MACHINE" || return 1 fi else # Enable secure boot + TPM on manual installs as Win11 requires if enabled "$MANUAL" || [[ "$aborted" == [Yy1]* ]]; then if [[ "${DETECTED,,}" == "win11"* ]]; then BOOT_MODE="windows_secure" writeState "mode" "$BOOT_MODE" || return 1 fi fi # Enable secure boot on multi-socket systems to workaround freeze if [ -n "$SOCKETS" ] && [[ "$SOCKETS" != "1" ]]; then BOOT_MODE="windows_secure" writeState "mode" "$BOOT_MODE" || return 1 fi fi fi if [ -n "${ARGS:-}" ]; then ARGUMENTS="$ARGS ${ARGUMENTS:-}" writeState "args" "$ARGS" || return 1 fi if [ -n "${VGA:-}" ] && [[ "${VGA:-}" != "virtio"* ]]; then writeState "vga" "$VGA" || return 1 fi if [ -n "${USB:-}" ] && [[ "${USB:-}" != "qemu-xhci"* ]]; then writeState "usb" "$USB" || return 1 fi if [ -n "${DISK_TYPE:-}" ] && [[ "${DISK_TYPE:-}" != "scsi" ]]; then writeState "type" "$DISK_TYPE" || return 1 fi if [ -n "${ADAPTER:-}" ] && [[ "${ADAPTER:-}" != "virtio-net-pci" ]]; then writeState "net" "$ADAPTER" || return 1 fi rm -rf "$TMP" return 0 } abortInstall() { local dir="$1" local iso="$2" local efi [[ "${iso,,}" == *".esd" ]] && exit 60 enabled "${UNPACK:-}" && exit 60 efi=$(find "$dir" -maxdepth 1 -type d -iname efi -print -quit) if [ -z "$efi" ]; then [[ "${PLATFORM,,}" == "x64" ]] && BOOT_MODE="windows_legacy" fi if [ -n "$CUSTOM" ]; then BOOT="$iso" REMOVE="N" else if [[ "$iso" != "$BOOT" ]]; then if ! mv -f "$iso" "$BOOT"; then error "Failed to move ISO file: $iso" && return 1 fi fi fi finishInstall "$BOOT" "Y" && return 0 return 1 } findFile() { local dir file base local fname="$1" local boot="$STORAGE/windows.boot" dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) [ ! -d "$dir" ] && dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" -print -quit) if [ -d "$dir" ]; then if ! hasDisk || [ ! -f "$boot" ]; then error "The bind $dir maps to a file that does not exist!" && return 1 fi fi file=$(find / -maxdepth 1 -type f -iname "$fname" -print -quit) [ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" -print -quit) if [ ! -s "$file" ] && [[ "${VERSION,,}" != "http"* ]]; then base=$(basename "$VERSION") file="$STORAGE/$base" fi if [ ! -f "$file" ] || [ ! -s "$file" ]; then return 0 fi local size size="$(stat -c%s "$file")" [ -z "$size" ] || [[ "$size" == "0" ]] && return 0 ISO="$file" CUSTOM="$file" BOOT="$STORAGE/windows.$size.iso" return 0 } detectCustom() { CUSTOM="" ! findFile "custom.iso" && return 1 [ -n "$CUSTOM" ] && return 0 ! findFile "boot.iso" && return 1 [ -n "$CUSTOM" ] && return 0 return 0 } getEsdFieldValue() { local values="$1" local index="$2" awk -F: -v index="$index" ' NR == index { gsub(/^[ \t]+|[ \t]+$/, "", $2) print $2 } ' <<< "$values" return 0 } extractESD() { local iso="$1" local dir="$2" local version="$3" local desc="$4" local msg ret index local minSize freeSpace bootPad installPad local info count totals links local bootTotal bootLinks bootSize local wimTotal wimLinks wimSize local installSize local bootWim installWim local edition imgEdition minSize=100000000 freeSpace=9606127360 bootPad=60000000 installPad=3000000 msg="Extracting $desc bootdisk" info "$msg..." && html "$msg..." if [ "$(stat -c%s "$iso")" -lt "$minSize" ]; then error "Invalid ESD file: Size is smaller than 100 MB" return 1 fi rm -rf "$dir" if ! makeDir "$dir"; then error "Failed to create directory \"$dir\" !" return 1 fi checkFreeSpace "$dir" "$freeSpace" || return 1 info=$(wimlib-imagex info "$iso") || { error "Cannot read ESD file information!" return 1 } count=$(awk '/Image Count:/ {print $3}' <<< "$info") if [[ ! "$count" =~ ^[0-9]+$ ]]; then error "Cannot read the image count in ESD file!" return 1 fi if (( count < 3 )); then error "Invalid ESD file: expected at least 3 images, found $count." return 1 fi totals=$(grep "Total Bytes:" <<< "$info" || true) links=$(grep "Hard Link Bytes:" <<< "$info" || true) bootTotal=$(getEsdFieldValue "$totals" 1) bootLinks=$(getEsdFieldValue "$links" 1) if [[ ! "$bootTotal" =~ ^[0-9]+$ ]] || [[ ! "$bootLinks" =~ ^[0-9]+$ ]]; then error "Cannot read bootdisk size from ESD file!" return 1 fi bootSize=$(( bootTotal - bootLinks )) wimTotal=$(getEsdFieldValue "$totals" 3) wimLinks=$(getEsdFieldValue "$links" 3) if [[ ! "$wimTotal" =~ ^[0-9]+$ ]] || [[ ! "$wimLinks" =~ ^[0-9]+$ ]]; then error "Cannot read boot.wim size from ESD file!" return 1 fi wimSize=$(( wimTotal - wimLinks + bootPad )) /run/progress.sh "$dir" "$bootSize" "$msg ([P])..." & index="1" wimlib-imagex apply "$iso" "$index" "$dir" --quiet 2>/dev/null || { ret=$? fKill "progress.sh" error "Extracting $desc bootdisk failed ($ret)" return 1 } fKill "progress.sh" bootWim="$dir/sources/boot.wim" installWim="$dir/sources/install.wim" msg="Extracting $desc environment" info "$msg..." && html "$msg..." index="2" /run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." & wimlib-imagex export "$iso" "$index" "$bootWim" --compress=none --quiet || { ret=$? fKill "progress.sh" error "Adding WinPE failed ($ret)" return 1 } fKill "progress.sh" msg="Extracting $desc setup" info "$msg..." index="3" /run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." & wimlib-imagex export "$iso" "$index" "$bootWim" --compress=none --boot --quiet || { ret=$? fKill "progress.sh" error "Adding Windows Setup failed ($ret)" return 1 } fKill "progress.sh" if [[ "${PLATFORM,,}" == "x64" ]]; then LABEL="CCCOMA_X64FRE_EN-US_DV9" else LABEL="CPBA_A64FRE_EN-US_DV9" fi msg="Extracting $desc image" info "$msg..." && html "$msg..." edition=$(getCatalog "$version" "name") if [ -z "$edition" ]; then error "Invalid VERSION specified, value \"$version\" is not recognized!" return 1 fi for (( index=4; index<=count; index++ )); do imgEdition=$(wimlib-imagex info "$iso" "$index" | grep '^Description:' | sed 's/Description:[ \t]*//') [[ "${imgEdition,,}" != "${edition,,}" ]] && continue installSize=$(stat -c%s "$iso") installSize=$(( installSize + installPad )) /run/progress.sh "$installWim" "$installSize" "$msg ([P])..." & wimlib-imagex export "$iso" "$index" "$installWim" --compress=LZMS --chunk-size 128K --quiet || { ret=$? fKill "progress.sh" error "Addition of $index to the $desc image failed ($ret)" return 1 } fKill "progress.sh" return 0 done fKill "progress.sh" error "Failed to find product '$edition' in install.wim!" return 1 } extractImage() { local iso="$1" local dir="$2" local version="$3" local desc="local ISO" local file size if [ -z "$CUSTOM" ]; then desc="downloaded ISO" if [[ "$version" != "http"* ]]; then desc=$(printVersion "$version" "$desc") fi fi if [[ "${iso,,}" == *".esd" ]]; then extractESD "$iso" "$dir" "$version" "$desc" && return 0 return 1 fi local msg="Extracting $desc image" info "$msg..." && html "$msg..." rm -rf "$dir" if ! makeDir "$dir"; then error "Failed to create directory \"$dir\" !" && return 1 fi size=$(stat -c%s "$iso") if (( size < 100000000 )); then error "Invalid ISO file: Size is smaller than 100 MB" && return 1 fi checkFreeSpace "$dir" "$size" || return 1 rm -rf "$dir" /run/progress.sh "$dir" "$size" "$msg ([P])..." & if ! 7z x "$iso" -o"$dir" > /dev/null; then fKill "progress.sh" error "Failed to extract ISO file: $iso" && return 1 fi fKill "progress.sh" if ! enabled "${UNPACK:-}"; then LABEL=$(isoinfo -d -i "$iso" | sed -n 's/Volume id: //p') || LABEL="" else file=$(find "$dir" -maxdepth 1 -type f -iname "*.iso" -print -quit) if [ -z "$file" ]; then error "Failed to find any .iso file in archive!" && return 1 fi if ! 7z x "$file" -o"$dir" > /dev/null; then error "Failed to extract archive!" && return 1 fi LABEL=$(isoinfo -d -i "$file" | sed -n 's/Volume id: //p') || LABEL="" rm -f "$file" || warn "Failed to remove temporary ISO file: $file" fi return 0 } getPlatform() { local xml="$1" local tag="ARCH" local platform="x64" local arch arch=$(sed -n "/$tag/{s/.*<$tag>\(.*\)<\/$tag>.*/\1/;p}" <<< "$xml") case "${arch,,}" in "0" ) platform="x86" ;; "9" ) platform="x64" ;; "12" ) platform="arm64" ;; esac echo "$platform" return 0 } checkPlatform() { local xml="$1" local platform compat platform=$(getPlatform "$xml") case "${platform,,}" in "x86" ) compat="x64" ;; "x64" ) compat="$platform" ;; "arm64" ) compat="$platform" ;; * ) compat="${PLATFORM,,}" ;; esac [[ "${compat,,}" == "${PLATFORM,,}" ]] && return 0 error "You cannot boot ${platform^^} images on a $PLATFORM CPU!" return 1 } hasVersion() { local id="$1" local tag="$2" local xml="$3" local edition [ ! -f "/run/assets/$id.xml" ] && return 1 edition=$(printEdition "$id" "") [ -z "$edition" ] && return 1 [[ "${xml,,}" != *"<${tag,,}>${edition,,}"* ]] && return 1 return 0 } selectVersion() { local tag="$1" local xml="$2" local platform="$3" local id name prefer name=$(sed -n "/$tag/{s/.*<$tag>\(.*\)<\/$tag>.*/\1/;p}" <<< "$xml") [[ "$name" == *"Operating System"* ]] && name="" [ -z "$name" ] && return 0 id=$(fromName "$name" "$platform") [ -z "$id" ] && warn "Unknown ${tag,,}: '$name'" && return 0 prefer="$id-enterprise" hasVersion "$prefer" "$tag" "$xml" && echo "$prefer" && return 0 prefer="$id-ultimate" hasVersion "$prefer" "$tag" "$xml" && echo "$prefer" && return 0 prefer="$id" hasVersion "$prefer" "$tag" "$xml" && echo "$prefer" && return 0 prefer=$(getVersion "$name" "$platform") echo "$prefer" return 0 } detectVersion() { local xml="$1" local id platform platform=$(getPlatform "$xml") id=$(selectVersion "DISPLAYNAME" "$xml" "$platform") [ -z "$id" ] && id=$(selectVersion "PRODUCTNAME" "$xml" "$platform") [ -z "$id" ] && id=$(selectVersion "NAME" "$xml" "$platform") echo "$id" return 0 } detectLanguage() { local xml="$1" local lang="" if [[ "$xml" == *"LANGUAGE>"* ]]; then lang="${xml#*LANGUAGE>}" lang="${lang%%<*}" else if [[ "$xml" == *"FALLBACK>"* ]]; then lang="${xml#*FALLBACK>}" lang="${lang%%<*}" fi fi if [ -z "$lang" ]; then warn "Language could not be detected from ISO!" && return 0 fi local culture culture=$(getLanguage "$lang" "culture") [ -n "$culture" ] && LANGUAGE="$lang" && return 0 warn "Invalid language detected: \"$lang\"" return 0 } setXML() { local file="/custom.xml" if [ -d "$file" ]; then error "The bind $file maps to a file that does not exist!" && exit 67 fi [ ! -f "$file" ] || [ ! -s "$file" ] && file="$STORAGE/custom.xml" [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/custom.xml" [ ! -f "$file" ] || [ ! -s "$file" ] && file="$1" [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml" [ ! -f "$file" ] || [ ! -s "$file" ] && return 1 XML="$file" return 0 } detectImage() { local dir="$1" local version="$2" local desc msg language XML="" if [ -z "$DETECTED" ] && [ -z "$CUSTOM" ]; then [[ "${version,,}" != "http"* ]] && DETECTED="$version" fi if [ -n "$DETECTED" ]; then skipVersion "${DETECTED,,}" && return 0 if ! setXML "" && ! enabled "$MANUAL"; then MANUAL="Y" desc=$(printEdition "$DETECTED" "this version") warn "the answer file for $desc was not found ($DETECTED.xml), $FB." fi return 0 fi info "Detecting version from ISO image..." if detectLegacy "$dir"; then desc=$(printEdition "$DETECTED" "$DETECTED") info "Detected: $desc" return 0 fi local src wim info src=$(find "$dir" -maxdepth 1 -type d -iname sources -print -quit) if [ ! -d "$src" ]; then warn "failed to locate 'sources' folder in ISO image, $FB" && return 1 fi wim=$(find "$src" -maxdepth 1 -type f \( -iname install.wim -or -iname install.esd \) -print -quit) if [ ! -f "$wim" ]; then warn "failed to locate 'install.wim' or 'install.esd' in ISO image, $FB" && return 1 fi if ! info=$(wimlib-imagex info -xml "$wim" | iconv -f UTF-16LE -t UTF-8); then warn "failed to read Windows image information, $FB" return 1 fi checkPlatform "$info" || exit 67 DETECTED=$(detectVersion "$info") if [ -z "$DETECTED" ]; then msg="Failed to determine Windows version from image" if setXML "" || enabled "$MANUAL"; then info "${msg}!" else MANUAL="Y" warn "${msg}, $FB." fi return 0 fi desc=$(printEdition "$DETECTED" "$DETECTED") detectLanguage "$info" if [[ "${LANGUAGE,,}" != "en" && "${LANGUAGE,,}" != "en-"* ]]; then language=$(getLanguage "$LANGUAGE" "desc") desc+=" ($language)" fi info "Detected: $desc" setXML "" && return 0 if [[ "$DETECTED" == "win81x86"* || "$DETECTED" == "win10x86"* ]]; then error "The 32-bit version of $desc is not supported!" && return 1 fi msg="the answer file for $desc was not found ($DETECTED.xml)" local fallback="/run/assets/${DETECTED%%-*}.xml" if setXML "$fallback" || enabled "$MANUAL"; then ! enabled "$MANUAL" && warn "${msg}." else MANUAL="Y" warn "${msg}, $FB." fi return 0 } prepareImage() { local iso="$1" local dir="$2" local desc missing desc=$(printVersion "$DETECTED" "$DETECTED") setMachine "$DETECTED" "$iso" "$dir" "$desc" || return 1 skipVersion "$DETECTED" && return 0 if [[ "${BOOT_MODE,,}" != "windows_legacy" ]]; then [ -f "$dir/$ETFS" ] && [ -f "$dir/$EFISYS" ] && return 0 missing=$(basename "$dir/$EFISYS") [ ! -f "$dir/$ETFS" ] && missing=$(basename "$dir/$ETFS") error "Failed to locate file \"${missing,,}\" in ISO image!" return 1 fi prepareLegacy "$iso" "$dir" "$desc" && return 0 error "Failed to extract boot image from ISO image!" return 1 } updateXML() { local asset="$1" local language="$2" local culture region user admin pass pw keyboard [ -z "$HEIGHT" ] && HEIGHT="720" [ -z "$WIDTH" ] && WIDTH="1280" sed -i "s/>Windows for Docker$APP for $ENGINE1080<\/VerticalResolution>/$HEIGHT<\/VerticalResolution>/g" "$asset" sed -i "s/1920<\/HorizontalResolution>/$WIDTH<\/HorizontalResolution>/g" "$asset" culture=$(getLanguage "$language" "culture") if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then sed -i "s/en-US<\/UILanguage>/$culture<\/UILanguage>/g" "$asset" fi region="$REGION" [ -z "$region" ] && region="$culture" if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then sed -i "s/en-US<\/UserLocale>/$region<\/UserLocale>/g" "$asset" sed -i "s/en-US<\/SystemLocale>/$region<\/SystemLocale>/g" "$asset" fi keyboard="$KEYBOARD" [ -z "$keyboard" ] && keyboard="$culture" if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then sed -i "s/en-US<\/InputLocale>/$keyboard<\/InputLocale>/g" "$asset" sed -i "s/0409:00000409<\/InputLocale>/$keyboard<\/InputLocale>/g" "$asset" fi user=$(echo "$USERNAME" | sed 's/[^[:alnum:]@!._-]//g') if [ -n "$user" ]; then sed -i "s/-name \"Docker\"/-name \"$user\"/g" "$asset" sed -i "s/Docker<\/Name>/$user<\/Name>/g" "$asset" sed -i "s/where name=\"Docker\"/where name=\"$user\"/g" "$asset" sed -i "s/Docker<\/FullName>/$user<\/FullName>/g" "$asset" sed -i "s/Docker<\/Username>/$user<\/Username>/g" "$asset" fi [ -n "$PASSWORD" ] && pass="$PASSWORD" || pass="admin" pw=$(printf '%s' "${pass}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) admin=$(printf '%s' "${pass}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0) sed -i "s|password<\/Value>|$admin<\/Value>|g" "$asset" sed -i "s|true<\/PlainText>|<PlainText>false<\/PlainText>|g" "$asset" sed -i -z "s|<Password>...........<Value \/>|<Password>\n <Value>$pw<\/Value>|g" "$asset" sed -i -z "s|<Password>...............<Value \/>|<Password>\n <Value>$pw<\/Value>|g" "$asset" sed -i -z "s|<AdministratorPassword>...........<Value \/>|<AdministratorPassword>\n <Value>$admin<\/Value>|g" "$asset" sed -i -z "s|<AdministratorPassword>...............<Value \/>|<AdministratorPassword>\n <Value>$admin<\/Value>|g" "$asset" if [ -n "$EDITION" ]; then [[ "${EDITION^^}" == "CORE" ]] && EDITION="STANDARDCORE" sed -i "s/SERVERSTANDARD<\/Value>/SERVER${EDITION^^}<\/Value>/g" "$asset" fi if [ -n "$KEY" ]; then sed -i '/<ProductKey>/,/<\/ProductKey>/d' "$asset" sed -i "s/<\/UserData>/ <ProductKey>\n <Key>${KEY}<\/Key>\n <WillShowUI>OnError<\/WillShowUI>\n <\/ProductKey>\n <\/UserData>/g" "$asset" fi return 0 } addDriver() { local id="$1" local path="$2" local target="$3" local driver="$4" local desc="" local folder="" if [ -z "$id" ]; then warn "no Windows version specified for \"$driver\" driver!" && return 0 fi case "${id,,}" in "win7x86"* ) folder="w7/x86" ;; "win7x64"* ) folder="w7/amd64" ;; "win81x64"* ) folder="w8.1/amd64" ;; "win10x64"* ) folder="w10/amd64" ;; "win11x64"* ) folder="w11/amd64" ;; "win2025"* ) folder="2k25/amd64" ;; "win2022"* ) folder="2k22/amd64" ;; "win2019"* ) folder="2k19/amd64" ;; "win2016"* ) folder="2k16/amd64" ;; "win2012"* ) folder="2k12R2/amd64" ;; "win2008"* ) folder="2k8R2/amd64" ;; "win10arm64"* ) folder="w10/ARM64" ;; "win11arm64"* ) folder="w11/ARM64" ;; "winvistax86"* ) folder="2k8/x86" ;; "winvistax64"* ) folder="2k8/amd64" ;; esac if [ -z "$folder" ]; then desc=$(printVersion "$id" "$id") if [[ "${id,,}" != *"x86"* ]]; then warn "no \"$driver\" driver available for \"$desc\" !" && return 0 else warn "no \"$driver\" driver available for the 32-bit version of \"$desc\" !" && return 0 fi fi [ ! -d "$path/$driver/$folder" ] && return 0 case "${id,,}" in "winvista"* ) [[ "${driver,,}" == "viorng" ]] && return 0 ;; esac local dest="$path/$target/$driver" mkdir -p "$dest" || return 1 cp -Lr "$path/$driver/$folder/." "$dest" || return 1 return 0 } addDrivers() { local src="$1" local tmp="$2" local file="$3" local index="$4" local version="$5" local drivers="$tmp/drivers" rm -rf "$drivers" mkdir -p "$drivers" local msg="Adding drivers to image..." info "$msg" && html "$msg" if [ -z "$version" ]; then version="win11x64" warn "Windows version unknown, falling back to Windows 11 drivers..." fi if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then error "Failed to extract drivers from archive!" && return 1 fi local target="\$WinPEDriver\$" local dest="$drivers/$target" mkdir -p "$dest" || return 1 wimlib-imagex update "$file" "$index" --command "delete --force --recursive /$target" >/dev/null || true local driver local driver_list=( qxl viofs sriov smbus qxldod viorng viostor viomem NetKVM Balloon vioscsi pvpanic vioinput viogpudo vioserial qemupciserial ) for driver in "${driver_list[@]}"; do addDriver "$version" "$drivers" "$target" "$driver" || return 1 done local dst="$src/\$OEM\$/\$\$/Drivers" mkdir -p "$dst" || return 1 cp -Lr "$dest/." "$dst" || return 1 case "${version,,}" in "win11x64"* | "win2025"* ) # Workaround Virtio GPU driver bug rm -rf "$dest/viogpudo" ;; esac if ! wimlib-imagex update "$file" "$index" --command "add $dest /$target" >/dev/null; then return 1 fi rm -rf "$drivers" return 0 } updateImage() { local dir="$1" local asset="$2" local language="$3" 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 idx skipVersion "${DETECTED,,}" && return 0 if [ ! -s "$asset" ] || [ ! -f "$asset" ]; then asset="" if ! enabled "$MANUAL"; then MANUAL="Y" warn "no answer file provided, $FB." fi fi rm -rf "$tmp" mkdir -p "$tmp" src=$(find "$dir" -maxdepth 1 -type d -iname sources -print -quit) if [ ! -d "$src" ]; then error "failed to locate 'sources' folder in ISO image, $FB" return 1 fi wim=$(find "$src" -maxdepth 1 -type f \( -iname boot.wim -or -iname boot.esd \) -print -quit) if [ ! -f "$wim" ]; then error "failed to locate 'boot.wim' or 'boot.esd' in ISO image, $FB" return 1 fi idx="1" if ! info=$(wimlib-imagex info -xml "$wim" | iconv -f UTF-16LE -t UTF-8); then warn "failed to read boot image information, $FB" MANUAL="Y" info="" fi if [[ "${info^^}" == *"<IMAGE INDEX=\"2\">"* ]]; then idx="2" fi if ! addDrivers "$src" "$tmp" "$wim" "$idx" "$DETECTED"; then error "Failed to add drivers to image!" fi if ! addFolder "$src"; then error "Failed to add OEM folder to image!" fi 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 if ! wimlib-imagex update "$wim" "$idx" --command "rename /$xml /$bak" > /dev/null; then warn "failed to backup original answer file ($xml)." fi fi fi fi if ! enabled "$MANUAL"; then name=$(basename "$asset") local answer="$tmp/$name" info "Adding $name for automatic installation..." if ! cp "$asset" "$answer"; then error "Failed to copy answer file to $answer." return 1 fi if ! updateXML "$answer" "$language"; then error "Failed to update answer file: $answer" return 1 fi if ! wimlib-imagex update "$wim" "$idx" --command "add $answer /$xml" > /dev/null; then MANUAL="Y" warn "failed to add answer file ($name) to ISO image, $FB" else wimlib-imagex update "$wim" "$idx" --command "add $answer /$dat" > /dev/null || true fi fi if enabled "$MANUAL"; then wimlib-imagex update "$wim" "$idx" --command "delete --force /$xml" > /dev/null || true if wimlib-imagex extract "$wim" "$idx" "/$bak" "--dest-dir=$tmp" >/dev/null 2>&1; then if ! wimlib-imagex update "$wim" "$idx" --command "add $tmp/$bak /$xml" > /dev/null; then warn "failed to restore original answer file ($bak)." fi fi fi name="$xml" enabled "$MANUAL" && name="$bak" path=$(find "$dir" -maxdepth 1 -type f -iname "$name" -print -quit) if [ -f "$path" ]; then if ! enabled "$MANUAL"; then if ! mv -f "$path" "${path%.*}.org"; then error "Failed to rename answer file: $path" return 1 fi else if ! mv -f "$path" "${path%.*}.xml"; then error "Failed to rename answer file: $path" return 1 fi fi fi rm -rf "$tmp" return 0 } removeImage() { local iso="$1" [ ! -f "$iso" ] && return 0 [ -n "$CUSTOM" ] && return 0 rm -f "$iso" 2> /dev/null || warn "failed to remove $iso !" return 0 } buildImage() { local dir="$1" local failed="" local cat="BOOT.CAT" local log="/run/shm/iso.log" local base size desc if [ -f "$BOOT" ]; then error "File $BOOT does already exist?!" && return 1 fi base=$(basename "$BOOT") local out="$TMP/${base%.*}.tmp" rm -f "$out" desc=$(printVersion "$DETECTED" "ISO") local msg="Building $desc image" info "$msg..." && html "$msg..." [ -z "$LABEL" ] && LABEL="Windows" if [ ! -f "$dir/$ETFS" ]; then error "Failed to locate file \"$ETFS\" in ISO image!" && return 1 fi size=$(du -b --max-depth=0 "$dir" | cut -f1) checkFreeSpace "$TMP" "$size" || return 1 /run/progress.sh "$out" "$size" "$msg ([P])..." & if [[ "${BOOT_MODE,,}" != "windows_legacy" ]]; then genisoimage -o "$out" -b "$ETFS" -no-emul-boot -c "$cat" -iso-level 4 -J -l -D -N -joliet-long -relaxed-filenames -V "${LABEL::30}" \ -udf -boot-info-table -eltorito-alt-boot -eltorito-boot "$EFISYS" -no-emul-boot -allow-limited-size -quiet "$dir" 2> "$log" || failed="y" else case "${DETECTED,,}" in "win2k"* | "winxp"* | "win2003"* ) genisoimage -o "$out" -b "$ETFS" -no-emul-boot -boot-load-seg 1984 -boot-load-size 4 -c "$cat" -iso-level 2 -J -l -D -N -joliet-long \ -relaxed-filenames -V "${LABEL::30}" -quiet "$dir" 2> "$log" || failed="y" ;; "win9"* ) genisoimage -o "$out" -b "$ETFS" -J -r -V "${LABEL::30}" -quiet "$dir" 2> "$log" || failed="y" ;; * ) genisoimage -o "$out" -b "$ETFS" -no-emul-boot -c "$cat" -iso-level 2 -J -l -D -N -joliet-long -relaxed-filenames -V "${LABEL::30}" \ -udf -allow-limited-size -quiet "$dir" 2> "$log" || failed="y" ;; esac fi fKill "progress.sh" if [ -n "$failed" ]; then [ -s "$log" ] && echo "$(<"$log")" error "Failed to build image!" && return 1 fi local err="" local hide="Warning: creating filesystem that does not conform to ISO-9660." [ -s "$log" ] && err="$(<"$log")" [[ "$err" != "$hide" ]] && echo "$err" mv -f "$out" "$BOOT" || return 1 if ! setOwner "$BOOT"; then error "Failed to set the owner for \"$BOOT\" !" return 1 fi return 0 } bootWindows() { ARGS=$(readState "$STORAGE/windows.args") || return 1 if [ -n "$ARGS" ]; then ARGUMENTS="$ARGS ${ARGUMENTS:-}" fi restoreState "VGA" "$STORAGE/windows.vga" || return 1 restoreState "USB" "$STORAGE/windows.usb" || return 1 restoreState "ADAPTER" "$STORAGE/windows.net" || return 1 restoreState "DISK_TYPE" "$STORAGE/windows.type" || return 1 restoreState "BOOT_MODE" "$STORAGE/windows.mode" "Y" || return 1 if [[ "${PLATFORM,,}" == "x64" ]]; then restoreState "MACHINE" "$STORAGE/windows.old" "Y" || return 1 fi return 0 } ###################################### ! parseVersion && exit 58 ! parseLanguage && exit 56 ! detectCustom && exit 59 if ! startInstall; then bootWindows && return 0 exit 68 fi if [ ! -s "$ISO" ] || [ ! -f "$ISO" ]; then if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then rm -f "$ISO" 2> /dev/null || true exit 61 fi fi DIR="$TMP/unpack" if ! extractImage "$ISO" "$DIR" "$VERSION"; then rm -f "$ISO" 2> /dev/null || true exit 62 fi if ! detectImage "$DIR" "$VERSION"; then abortInstall "$DIR" "$ISO" && return 0 exit 60 fi if ! prepareImage "$ISO" "$DIR"; then abortInstall "$DIR" "$ISO" && return 0 exit 66 fi if ! updateImage "$DIR" "$XML" "$LANGUAGE"; then abortInstall "$DIR" "$ISO" && return 0 exit 63 fi if ! removeImage "$ISO"; then exit 64 fi if ! buildImage "$DIR"; then exit 65 fi if ! finishInstall "$BOOT" "N"; then exit 69 fi html "Successfully prepared image for installation..." return 0