#!/usr/bin/env bash set -Eeuo pipefail startWindows() { 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 } html "Starting $APP..." if needsInstall; then startInstall || exit $? else bootWindows || exit $? return 0 fi if ! hasImage "$ISO"; then if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then removeImage "$ISO" || : exit 68 fi fi local boot="$BOOT" local dir="$TMP/unpack" local handled=0 extracted=0 selectWindowsImage "$ISO" "$dir" "$boot" || exit $? (( handled )) && return 0 configureMachine "$ISO" "$dir" "$boot" || exit $? (( handled )) && return 0 prepareWindowsImage "$ISO" "$dir" "$boot" || exit $? (( handled )) && return 0 finishInstall "$BOOT" "N" "$boot" || exit 100 return 0 } needsInstall() { local file boot iso if isCustomImage; then iso="$BOOT" else file=$(getInstallFile) || exit $? boot=$(getBootFile "$file") || exit $? iso="$STORAGE/$boot" fi local previousBase previousBase=$(readBase) || exit $? if [ -n "$previousBase" ] && [[ "${STORAGE,,}/${previousBase,,}" != "${iso,,}" ]]; then # Removing custom installation media does not invalidate an installation # that has already completed successfully. if [[ "${previousBase,,}" == "windows."* ]] && [[ "${iso,,}" != "${STORAGE,,}/windows."* ]] && hasCompletedInstall; then info "Detected that your custom .iso file was removed, will be ignored." return 1 fi return 0 fi if [ -n "$previousBase" ] && hasSystemImage; then return 1 fi hasData && hasBootMarker && return 1 return 0 } bootWindows() { setBoot || return $? cleanupTemp || return $? local previousBase previousBase=$(readBase) || return $? cleanupSource "$previousBase" || return $? selectBoot "$BOOT" "$previousBase" || return $? if ! restoreMachineState; then error "Failed to restore the saved machine state!" return 66 fi if ! restoreBootMode; then error "Failed to restore the saved boot mode!" return 66 fi if ! restoreMachine; then error "Failed to restore the saved machine type!" return 66 fi if ! reserveSambaPorts; then error "Failed to reserve Samba ports!" return 66 fi return 0 } startInstall() { setBoot || return $? cleanupTemp || return $? local previousBase previousBase=$(readBase) || return $? cleanupSource "$previousBase" || return $? cleanupInstall "$BOOT" "$previousBase" || return $? if ! makeDir "$TMP"; then error "Failed to create directory \"$TMP\" !" return 50 fi if ! isCustomImage; then if hasImage "$BOOT"; then ISO="$TMP/$(basename "$BOOT")" else local file file=$(getInstallFile) || return $? ISO="$TMP/$file" fi fi cleanupStorage "$BOOT" || return $? if ! isCustomImage && ! isURL "$VERSION"; then checkMemory "$VERSION" || return 67 setDiskMinimum "$VERSION" || return 67 fi # Work from the temporary directory so the persistent source path can # later contain either the preserved ISO or the rebuilt installation image. if ! isCustomImage && hasImage "$BOOT"; then if ! mv -f -- "$BOOT" "$ISO"; then error "Failed to move ISO file from \"$BOOT\" to \"$ISO\" !" return 50 fi fi return 0 } selectWindowsImage() { local iso="$1" local dir="$2" local boot="$3" XML="" SYSTEM="" FB="falling back to manual installation!" normalizeDetected || : if [ -n "$DETECTED" ]; then if ! setImage; then error "Failed to configure the detected Windows image!" return 70 fi if ! needsExtraction "$iso"; then needsPreparation "$DETECTED" || return 0 fi if ! extractImage "$iso" "$dir" "$VERSION"; then error "Failed to extract the Windows installation image!" removeImage "$iso" || : return 72 fi extracted=1 return 0 fi # Inspect unknown bootable media directly before falling back to extraction. if ! needsExtraction "$iso"; then detectIsoImage "$iso" && return 0 local rc=$? 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 extracted=1 detectImage "$dir" && return 0 local rc=$? if (( rc != 1 )); then error "Failed to detect the extracted Windows installation image!" return 76 fi skipUnattended "$dir" "$iso" "$boot" || { error "Failed to fall back to manual installation!" return 76 } handled=1 return 0 } configureMachine() { local iso="$1" local dir="$2" local boot="$3" local desc desc=$(printVariant "$DETECTED" "$DETECTED") || return 78 if ! checkMemory "$DETECTED"; then if ! isCustomImage; then useOriginalImage "$iso" || { error "Failed to preserve the original installation image!" return 79 } fi return 79 fi setDiskMinimum "$DETECTED" || return 79 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" || { error "Failed to fall back to manual installation!" return 83 } handled=1 return 0 fi return 0 } prepareWindowsImage() { local iso="$1" local dir="$2" local boot="$3" if [ -n "$SYSTEM" ]; then hasImage "$SYSTEM" || { error "Failed to find the generated Windows system image!" return 94 } removeImage "$iso" || { error "Failed to remove the source installation image!" return 96 } BOOT="$SYSTEM" return 0 fi # Keep all run-specific automation on the setup image for XML-capable media. 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" || { error "Failed to preserve the original installation image!" return 88 } return 0 fi fi # 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" || { error "Failed to remove the source installation image!" return 96 } buildImage "$dir" || { error "Failed to build the Windows installation image!" return 98 } return 0 } finishInstall() { local iso="$1" local aborted="$2" local boot="$3" local base secure=0 if ! hasImage "$iso"; then error "Failed to find ISO file: $iso" && return 1 fi if [[ "$iso" == "$STORAGE/"* ]]; then if ! setOwner "$iso"; then warn "failed to set the owner for \"$iso\" !" fi fi local file="$STORAGE/windows.ver" 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\" !" fi if [[ "$boot" == "$STORAGE/"* ]]; then if [[ "$aborted" != [Yy1]* ]] || ! isCustomImage; then base=$(basename "$boot") writeState "base" "$base" || { error "Failed to save the Windows installation source!" return 1 } fi fi if isPlatform "x64"; then if isLegacyBoot; then writeState "mode" "$BOOT_MODE" || { error "Failed to save the Windows boot mode!" return 1 } else # Aborted Win11 installs boot without any answer file present, # so enable Secure Boot and TPM to satisfy its hardware checks. if enabled "$aborted" || enabled "$MANUAL"; then [[ "${DETECTED,,}" == "win11"* ]] && secure=1 fi if (( secure )); then BOOT_MODE="windows_secure" writeState "mode" "$BOOT_MODE" || { error "Failed to save the Windows boot mode!" return 1 } fi fi fi reserveSambaPorts || { error "Failed to reserve Samba ports!" return 1 } if [ -n "$SYSTEM" ]; then if [[ "$SYSTEM" == "$TMP/"* ]]; then if ! mv -f -- "$SYSTEM" "$STORAGE/windows.img"; then error "Failed to finalize the Windows system image!" return 1 fi BOOT="$STORAGE/windows.img" else if ! hasImage "$SYSTEM"; then error "Failed to find the Windows system image!" return 1 fi writeState "system" "$SYSTEM" || { error "Failed to save the Windows system image!" return 1 } BOOT="$SYSTEM" fi fi if ! rm -rf -- "$TMP"; then error "Failed to remove directory \"$TMP\" !" return 1 fi return 0 } skipUnattended() { local dir="$1" local iso="$2" local boot="$3" local aborted="${4:-Y}" local efi efi32 efi64 # Standalone ESD files and nested archives are not directly bootable media, # so they cannot use the manual-install fallback. if needsExtraction "$iso"; then error "Failed to boot \"$iso\" because it is not a directly bootable ISO image!" return 1 fi # When automatic preparation fails, inspect extracted media to determine # whether it can still be booted manually using legacy firmware. if enabled "$aborted" && isPlatform "x64" && [ -d "$dir" ]; then efi=$(find "$dir" -maxdepth 1 -type d -iname efi -print -quit) || return 1 efi32=$(find "$dir" -maxdepth 3 -type f -ipath '*/efi/boot/bootia32.efi' -print -quit) || return 1 efi64=$(find "$dir" -maxdepth 3 -type f -ipath '*/efi/boot/bootx64.efi' -print -quit) || return 1 if [ -z "$efi" ] || { [ -n "$efi32" ] && [ -z "$efi64" ]; }; then writeState "mode" "windows_legacy" || return 1 restoreBootMode || return 1 fi fi # Preserve custom media in place. Downloaded or reused media must be moved # back to persistent storage before the manual fallback is started. useOriginalImage "$iso" || return 1 finishInstall "$BOOT" "$aborted" "$boot" && return 0 return 1 } readBase() { local previousBase if ! previousBase=$(readState "base"); then error "Failed to read the previous installation state!" return 50 fi # Older releases stored the original download name in windows.base. Current # releases store an ISO source identity, so migrate legacy state once. if [ -n "$previousBase" ] && [[ "${previousBase,,}" != *.iso ]]; then if isCompressed "$previousBase" || [[ "${previousBase,,}" == *.esd ]]; then previousBase="${previousBase%.*}" fi [ -n "$previousBase" ] || previousBase="download" previousBase+=".iso" if ! writeState "base" "$previousBase"; then error "Failed to migrate the previous installation state!" return 50 fi fi printf '%s\n' "$previousBase" return 0 } setBoot() { local file boot if ! isCustomImage; then file=$(getInstallFile) || return $? boot=$(getBootFile "$file") || return $? BOOT="$STORAGE/$boot" fi return 0 } selectBoot() { local iso="$1" local previousBase="$2" [ -n "$previousBase" ] || return 0 [[ "${STORAGE,,}/${previousBase,,}" == "${iso,,}" ]] || return 0 local system if system=$(getSystemImage); then BOOT="$system" fi return 0 } getInstallFile() { local file="${VERSION//\//}.iso" if isURL "$VERSION"; then file=$(basename "${VERSION%%[\?#]*}") printf -v file '%b' "${file//%/\\x}" file="${file//[!A-Za-z0-9._-]/_}" else local language if ! language=$(getLanguage "$LANGUAGE" "culture"); then error "Failed to determine the Windows language!" return 62 fi language="${language%%-*}" if [ -n "$language" ] && [[ "${language,,}" != "en" ]]; then file="${VERSION//\//}_${language,,}.iso" fi fi printf '%s\n' "$file" return 0 } getBootFile() { local file="$1" local boot="$file" if isURL "$VERSION"; then if isCompressed "$VERSION" || [[ "${boot,,}" == *.esd ]]; then boot="${boot%.*}" fi [ -n "$boot" ] || boot="download" [[ "${boot,,}" == *.iso ]] || boot+=".iso" case "${boot,,}" in "windows."* ) error "The download filename \"$file\" uses the reserved \"windows.*\" namespace!" return 58 ;; esac fi printf '%s\n' "$boot" return 0 } cleanupTemp() { TMP="$STORAGE/tmp" if ! rm -rf -- "$TMP"; then error "Failed to remove directory \"$TMP\" !" return 50 fi local setup="$STORAGE/setup.img" if ! rm -f -- "$setup" "${setup}.tmp"; then error "Failed to remove setup image \"$setup\" !" return 50 fi return 0 } cleanupSource() { local previousBase="$1" [ -n "$previousBase" ] || return 0 [[ "${previousBase,,}" == "windows."* ]] || return 0 hasCompletedInstall || return 0 # Older releases may have left a rebuilt custom ISO at its synthetic source # identity. A completed installation no longer needs that installation media. if ! rm -f -- "$STORAGE/$previousBase"; then error "Failed to remove obsolete ISO file \"$STORAGE/$previousBase\" !" return 50 fi return 0 } cleanupStorage() { local boot="$1" # Keep existing media at its persistent path until all storage cleanup has # completed successfully, so a later failure cannot strand it under $TMP. if isCustomImage || ! hasImage "$boot"; then if ! rm -f -- "$boot"; then error "Failed to remove obsolete ISO file \"$boot\" !" 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\" !" 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\" !" 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\" !" return 50 fi return 0 } cleanupInstall() { local iso="$1" local previousBase="$2" if [ -n "$previousBase" ] && [[ "${STORAGE,,}/${previousBase,,}" != "${iso,,}" ]]; then # A changed source invalidates an unfinished installation. Back up an # existing installation, but discard stale media when no disk exists yet. if ! hasInstalledDisk; then if ! rm -f -- "$STORAGE/$previousBase"; then error "Failed to remove ISO file \"$STORAGE/$previousBase\" !" return 50 fi return 0 fi local method if [[ "${iso,,}" == "${STORAGE,,}/windows."* ]]; then method="your custom .iso file was changed" elif [[ "${previousBase,,}" != "windows."* ]]; then method="the VERSION variable was changed" else method="your custom .iso file was removed" if hasCompletedInstall; then info "Detected that $method, will be ignored." return 0 fi fi if ! hasCompletedInstall && ! disabled "${SHUTDOWN:-}"; then discardPrevious "$STORAGE/$previousBase" || return 50 return 0 fi info "Detected that $method, a backup of your previous installation will be saved..." if ! backupPrevious "$STORAGE/$previousBase"; then warn "the backup was incomplete, continuing with installation..." fi return 0 fi if [ -z "$previousBase" ] && hasInstalledDisk; then if ! hasCompletedInstall && ! disabled "${SHUTDOWN:-}"; then discardPrevious "" || return 50 else if ! backupPrevious ""; then warn "the backup was incomplete, continuing with installation..." fi fi fi return 0 } discardPrevious() { local iso="$1" if [ -n "$iso" ] && [ -f "$iso" ]; then if ! rm -f -- "$iso"; then error "Failed to remove ISO file \"$iso\" !" return 1 fi fi if ! find "$STORAGE" -maxdepth 1 -type f \ \( -iname 'data.*' -or -iname 'windows.*' -or -iname '*.rom' -or -iname '*.vars' \) \ -not -iname '*.iso' -delete; then error "Failed to remove unfinished installation files from \"$STORAGE\" !" return 1 fi return 0 } backupPrevious () { local iso="$1" local count=1 local name="unknown" local root="$STORAGE/backups" local failed="" file previous previous=$(readState "base") || return 1 [ -n "$previous" ] && name="${previous%.*}" 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 if [ -f "$iso" ]; then if ! mv -f -- "$iso" "$dir/"; then error "Failed to move \"$iso\" to \"$dir\"." failed="Y" fi fi while IFS= read -r -d '' file; do if ! mv -n -- "$file" "$dir/"; then error "Failed to move \"$file\" to \"$dir\"." failed="Y" fi done < <( find "$STORAGE" -maxdepth 1 -type f \ \( -iname 'data.*' -or -iname 'windows.*' -or -iname '*.rom' -or -iname '*.vars' \) \ -not -iname '*.iso' -print0 ) # Wait for the process-substitution find command so enumeration failures are # detected rather than being mistaken for a successful backup. local find_pid=$! if ! wait "$find_pid"; then error "Failed to enumerate files in \"$STORAGE\"." failed="Y" fi rmdir "$dir" 2>/dev/null || : rmdir "$root" 2>/dev/null || : [ -n "$failed" ] && return 1 return 0 } detectCustom() { CUSTOM="" findFile "custom.iso" || return 1 if isCustomImage; then DETECTED="" return 0 fi findFile "boot.iso" || return 1 if isCustomImage; then DETECTED="" return 0 fi return 0 } findFile() { local fname="$1" local dir file base dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) || return 1 if [ ! -d "$dir" ]; then dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" -print -quit) || return 1 fi if [ -d "$dir" ]; then if ! hasSystemImage && { ! hasDisk || ! hasBootMarker; }; 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) || return 1 if [ ! -s "$file" ]; then file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" -print -quit) || return 1 fi if [ ! -s "$file" ] && ! isURL "$VERSION"; then base=$(basename "$VERSION") file="$STORAGE/$base" fi if ! hasImage "$file"; then return 0 fi local size size=$(stat -c%s "$file") || return 1 if [ -z "$size" ] || [[ "$size" == "0" ]]; then return 0 fi 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" return 0 } isCustomImage() { [ -n "${CUSTOM:-}" ] } isURL() { local value="$1" [[ "${value,,}" == "http"* ]] } isPlatform() { local platform="$1" [[ "${PLATFORM,,}" == "${platform,,}" ]] } isLegacyBoot() { [[ "${BOOT_MODE,,}" == "windows_legacy" ]] } hasBootMarker() { [ -f "$STORAGE/windows.boot" ] } hasImage() { local iso="$1" [ -f "$iso" ] && [ -s "$iso" ] } getSystemImage() { local image image=$(readState "system") || return 1 if [ -n "$image" ]; then # Older development builds stored only the basename in windows.system. [[ "$image" == /* ]] || image="$STORAGE/$image" hasImage "$image" || return 1 echo "$image" return 0 fi image="$STORAGE/windows.img" hasImage "$image" || return 1 echo "$image" return 0 } hasSystemImage() { getSystemImage >/dev/null } hasInstalledDisk() { hasData || hasSystemImage } hasCompletedInstall() { hasSystemImage && return 0 hasData && hasBootMarker } needsExtraction() { local iso="$1" enabled "${UNPACK:-}" || [[ "${iso,,}" == *.esd ]] } needsPreparation() { local id="$1" supportsSIF "$id" && return 0 case "${id,,}" in "win9"* ) supportsUnattended "$id" && return 0 ;; esac return 1 } useOriginalImage() { local iso="$1" if isCustomImage; then BOOT="$iso" elif [[ "$iso" != "$BOOT" ]]; then if ! mv -f -- "$iso" "$BOOT"; then error "Failed to move ISO file: $iso" return 1 fi fi # Keep SYSTEM pointing at the same image when preserving it changes its path. if [[ "${SYSTEM:-}" == "$iso" ]]; then SYSTEM="$BOOT" fi return 0 } removeImage() { local iso="$1" isCustomImage && return 0 if ! rm -f -- "$iso" 2>/dev/null; then warn "failed to remove image \"$iso\"!" return 1 fi return 0 } normalizeDetected() { # Known catalog versions already provide the required image metadata. if [ -z "$DETECTED" ] && ! isCustomImage && ! isURL "$VERSION"; then DETECTED="$VERSION" fi DETECTED="${DETECTED/-enterprise-iot/-iot}" DETECTED="${DETECTED/-enterprise-ltsc/-ltsc}" return 0 } setImage() { local rc=0 supportsXML "${DETECTED,,}" || return 0 setXML "" || rc=$? if (( rc == 0 )); then return 0 fi enabled "$MANUAL" && return 0 # Only a genuinely missing answer file may fall back to manual setup. (( rc == 1 )) || return "$rc" MANUAL="Y" local desc desc=$(printEdition "$DETECTED" "this version") || return 1 warn "the answer file for $desc was not found ($DETECTED.xml), $FB." return 0 } detectImage() { local dir="$1" local desc rc info "Detecting version from ISO image..." # 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" fi local wim wim=$(findImage "$dir") || return $? local image_info image_info=$(readImageInfo "$wim") || return $? detectImageInfo "$image_info" || { error "Failed to process the Windows image metadata!" return 2 } return 0 } prepareImage() { local iso="$1" local dir="$2" local desc missing desc=$(printVariant "$DETECTED" "$DETECTED") # Use the standard Windows BIOS boot image unless legacy preparation # already selected a media-specific El Torito image. ETFS="${ETFS:-boot/etfsboot.com}" # Legacy rebuilt media must retain the source ISO's El Torito boot-load size. if isLegacyBoot; then getBootLoadSize "$iso" "$dir" "$desc" || return 1 fi supportsXML "$DETECTED" || return 0 if isLegacyBoot; then extractBootImage "$iso" "$dir" "$desc" && return 0 error "Failed to extract boot image from ISO image \"${iso}\"!" return 1 fi EFISYS="efi/microsoft/boot/efisys_noprompt.bin" # A modern rebuilt ISO requires both its BIOS and no-prompt UEFI boot images. [ -f "$dir/$ETFS" ] && [ -s "$dir/$ETFS" ] && [ -f "$dir/$EFISYS" ] && [ -s "$dir/$EFISYS" ] && return 0 missing=$(basename "$dir/$EFISYS") if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then missing=$(basename "$dir/$ETFS") fi error "Failed to locate file \"${missing,,}\" in ISO image!" return 1 } createOverlay() { local asset="$1" local language="$2" local stage="$3" local msg="Creating overlay image..." info "$msg" && html "$msg" if ! rm -rf -- "$stage"; then error "Failed to remove previous image files!" return 1 fi if ! mkdir -p "$stage"; then error "Failed to create image staging directory!" return 1 fi if ! addDrivers "$stage" "$DETECTED"; then error "Failed to include Windows drivers!" return 1 fi if ! addFolder "$stage" "overlay"; then error "Failed to include OEM folder!" return 1 fi addAnswerFile "$asset" "$language" "$stage" || { error "Failed to include the Windows answer file!" return 1 } return 0 } addFolder() { local src="$1" local mode="${2:-copy}" local file="" source="" folder local dest="$src/\$OEM\$/\$1/OEM" local install="$src/.overlay-install.bat" if [ "$mode" = "win9x" ]; then dest="$src/OEM" fi folder=$(getOemFolder) || return 1 [ -z "$folder" ] && [ -z "$COMMAND" ] && return 0 local msg="Adding OEM files to image..." info "$msg" && html "$msg" # Setup-image mode cannot modify the original ISO, so create a temporary # writable copy of install.bat for the overlay image. if [ "$mode" = "overlay" ]; then rm -f -- "$install" || return 1 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 fi file="$install" fi fi else mkdir -p "$dest" || return 1 if [ -n "$folder" ]; then cp -Lr "$folder/." "$dest" || return 1 fi file=$(find "$dest" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 fi if [ -s "$file" ]; then normalizeBatch "$file" || return 1 fi if [ -n "$COMMAND" ]; then if [ -z "$file" ]; then if [ "$mode" = "overlay" ]; then file="$install" else file="$dest/install.bat" fi fi if [ -s "$file" ]; then printf '\n' >> "$file" || return 1 fi printf '%s\n' "$COMMAND" >> "$file" || return 1 fi if [ -s "$file" ]; then if ! unix2dos -q "$file"; then error "Failed to convert $file to DOS format!" return 1 fi checkBatch "$file" fi return 0 } getOemFolder() { local folder="/oem" [ ! -d "$folder" ] && folder="/OEM" [ ! -d "$folder" ] && folder="$STORAGE/oem" [ ! -d "$folder" ] && folder="$STORAGE/OEM" [ -d "$folder" ] && echo "$folder" return 0 } addDrivers() { local stage="$1" local version="$2" local drivers="$stage/drivers" local msg="Windows version and architecture are unknown; cannot select drivers!" rm -rf "$drivers" || return 1 mkdir -p "$drivers" || return 1 info "Adding drivers to image..." if [ -z "$version" ]; then if [ -z "${IMAGE_PLATFORM:-}" ]; then error "$msg" && return 1 fi case "${IMAGE_PLATFORM,,}" in "x86" ) version="win7x86" ;; "x64" ) version="win11x64" ;; "arm64" ) version="win11arm64" ;; * ) error "$msg" && return 1 ;; esac local desc desc=$(printVersion "$version" "") || return 1 warn "Windows version unknown, falling back to $desc 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 selectDrivers "$version" "$drivers" "$target" || return 1 local dst="$stage/\$OEM\$/\$\$/Drivers" mkdir -p "$dst" || return 1 cp -Lr "$dest/." "$dst" || return 1 # Install the VirtIO display driver explicitly from SetupComplete.cmd so it # cannot disrupt Windows Setup by loading through the WinPE driver path. if ! isLegacy "$version"; then rm -rf "$dest/viogpudo" || return 1 fi local winpe="$stage/$target" rm -rf "$winpe" || return 1 mkdir -p "$winpe" || return 1 cp -Lr "$dest/." "$winpe" || return 1 rm -rf "$drivers" || return 1 return 0 } selectDrivers() { local version="$1" local drivers="$2" local target="$3" local driver_list=( qxl viofs sriov smbus qxldod viorng viostor viomem NetKVM Balloon vioscsi pvpanic vioinput viogpudo vioserial qemupciserial ) local driver for driver in "${driver_list[@]}"; do addDriver "$version" "$drivers" "$target" "$driver" || return 1 done return 0 } addDriver() { local id="$1" local path="$2" local target="$3" local driver="$4" local folder desc if [ -z "$id" ]; then warn "no Windows version specified for \"$driver\" driver!" return 1 fi if ! folder=$(getDriverFolder "$id"); then folder="" fi if [ -z "$folder" ]; then desc=$(printVersion "$id" "$id") if [[ "${id,,}" == *"x86"* ]]; then warn "no \"$driver\" driver available for the 32-bit version of \"$desc\" !" else warn "no \"$driver\" driver available for \"$desc\" !" fi return 1 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 } normalizeBatch() { local file="$1" local bom tmp encoding [ ! -s "$file" ] && return 0 bom=$(od -An -N2 -tx1 "$file" | tr -d ' \n') || return 1 # Convert only BOM-marked UTF-16 files; unmarked ANSI and UTF-8 batch files # are deliberately left unchanged. case "$bom" in "fffe" ) encoding="UTF-16LE" ;; "feff" ) encoding="UTF-16BE" ;; * ) return 0 ;; esac if ! tmp=$(mktemp "${file}.XXXXXX"); then error "Failed to create temporary batch file!" return 1 fi if ! tail -c +3 "$file" | iconv -f "$encoding" -t UTF-8 > "$tmp"; then rm -f "$tmp" error "Failed to convert $file from $encoding to UTF-8!" return 1 fi if ! chmod --reference="$file" "$tmp" || ! mv -f "$tmp" "$file"; then rm -f "$tmp" error "Failed to replace batch file: $file" return 1 fi return 0 } checkBatch() { local file="$1" local tmp output local matches line local enabled_rules [ -s "$file" ] || return 0 if ! tmp=$(mktemp -d /tmp/blinter.XXXXXX); then warn "failed to create temporary Blinter directory." return 0 fi local source="your install.bat file" [ -n "${COMMAND:-}" ] && source="your COMMAND variable" # Only check rules that indicate likely execution or behavioural failures. enabled_rules="E001,E002,E003,E004,E005,E006,E007,E008" enabled_rules+=",E009,E010,E011,E012,E013,E014,E015,E016" enabled_rules+=",E017,E018,E019,E020,E021,E022,E023,E024" enabled_rules+=",E025,E027,E028,E029,E030,E031,E032,E033,E034" enabled_rules+=",W004,W005,W013,W017,W021,W022,W034,W038,W040" if enabled "$DEBUG"; then if LC_ALL=C grep -Pq '[^\x09\x0D\x20-\x7E]' "$file"; then warn "non-ASCII characters were detected in $source and may not execute correctly in Windows Command Prompt." fi fi cat > "$tmp/blinter.ini" <&1 || true ) # Remove header. output=$( awk ' /^DETAILED ISSUES:/ { found = 1 next } found && !started { if (/^-+$/) { started = 1 } next } started { print } ' <<< "$output" ) output="${output#"${output%%[!$'\r\n ']*}"}" output="${output%"${output##*[!$'\r\n ']}"}" if grep -Eq '^(ERROR|WARNING|SECURITY) LEVEL ISSUES:$' <<< "$output"; then warn "possible issues were detected in $source:" printf '\n%s\n\n' "$output" >&2 fi rm -rf "$tmp" || true reportBatchMatches \ "$file" \ "$source" \ '(?&2 done <<< "$matches" printf ' %s\n\n' "$suggestion" >&2 return 0 } checkMemory() { local id="$1" local required name required=$(getRequiredMemory "$id") || return RAM_MINIMUM="$required" name=$(printVersion "$id" "") || return checkMemoryRequirement "$name" || return return 0 } setDiskMinimum() { local id="$1" local required required=$(getRequiredDisk "$id") || return DISK_MINIMUM="$required" return 0 } reserveSambaPorts() { disabled "${SAMBA:-Y}" && return 0 disabled "${NETWORK:-Y}" && return 0 enabled "${DHCP:-N}" && return 0 # NAT can fall back to user-mode networking after this point, # so always protect the Samba listeners for non-DHCP networking. HOST_PORTS="${HOST_PORTS:+$HOST_PORTS,}139/tcp,445/tcp" return 0 } startWindows return 0