#!/usr/bin/env bash # # isotousb.sh — write an ISO to a USB stick the "Etcher way": raw byte copy # to the whole device, forced flush, then a read-back verification. Plus the # safety rails that make dd dangerous when done by hand: # - lists ONLY removable/USB block devices (won't show your system disk), # - shows model + size + current partitions so you pick the right one, # - refuses mounted targets and unmounts partitions first, # - requires you to type the device size to confirm, # - detects whether the ISO supports UEFI Secure Boot and tells you. # # It cannot "add" Secure Boot: that lives inside the ISO (a signed shim). This # script only reports whether the ISO already has it, so you know whether you # must disable Secure Boot in firmware before booting. # # Usage: sudo ./isotousb.sh path/to/image.iso # sudo ./isotousb.sh # will prompt for the ISO path # set -euo pipefail # ---- pretty output --------------------------------------------------------- if [[ -t 1 ]]; then BOLD=$'\e[1m'; DIM=$'\e[2m'; RED=$'\e[31m'; GRN=$'\e[32m' YEL=$'\e[33m'; CYN=$'\e[36m'; RST=$'\e[0m' # High-contrast, NEUTRAL (no warm tint, so it stays legible under # Redshift/night-light) colour for the selectable index numbers. SEL=$'\e[1;92m' else BOLD=''; DIM=''; RED=''; GRN=''; YEL=''; CYN=''; RST=''; SEL='' fi info() { printf '%s\n' "${CYN}==>${RST} $*"; } warn() { printf '%s\n' "${YEL}!! ${RST}$*" >&2; } die() { printf '%s\n' "${RED}ERR ${RST}$*" >&2; exit 1; } ok() { printf '%s\n' "${GRN}OK ${RST}$*"; } # ---- preflight ------------------------------------------------------------- # Show help without requiring root (scan args early; full parse happens later). for _a in "$@"; do case "$_a" in -h|--help) cat < raw byte-for-byte dd --windows Treat the ISO as a Windows installer -> WoeUSB partitioned copy --auto Auto-detect the ISO type (default) -h, --help Show this help If ISO is omitted you'll be prompted for it. You can also override the detected type interactively before anything is written. Must be run as root (sudo) to write to a raw block device. EOF exit 0 ;; esac done [[ $EUID -eq 0 ]] || die "must run as root (writing to a raw block device). Re-run with sudo." # Detect the distro package manager once, so we can offer to install anything # that's missing. Returns via globals PM / PM_INSTALL. detect_pm() { if command -v apt-get >/dev/null; then PM="apt"; PM_INSTALL="apt-get install -y" elif command -v dnf >/dev/null; then PM="dnf"; PM_INSTALL="dnf install -y" elif command -v pacman >/dev/null; then PM="pacman"; PM_INSTALL="pacman -S --noconfirm --needed" elif command -v zypper >/dev/null; then PM="zypper"; PM_INSTALL="zypper install -y" else PM=""; PM_INSTALL=""; fi } detect_pm # Map a generic command name to its package on each distro family. Most match # their command name; the exceptions are listed here. pkg_for() { local cmd="$1" case "$PM:$cmd" in apt:xxd) echo "xxd" ;; apt:7z) echo "p7zip-full" ;; apt:bsdtar) echo "libarchive-tools" ;; apt:isoinfo) echo "genisoimage" ;; apt:*) echo "$cmd" ;; dnf:7z) echo "p7zip" ;; dnf:bsdtar) echo "bsdtar" ;; dnf:isoinfo) echo "genisoimage" ;; dnf:*) echo "$cmd" ;; pacman:7z) echo "p7zip" ;; pacman:bsdtar) echo "libarchive" ;; pacman:isoinfo) echo "cdrtools" ;; pacman:pv) echo "pv" ;; pacman:*) echo "$cmd" ;; zypper:7z) echo "p7zip" ;; zypper:bsdtar) echo "bsdtar" ;; zypper:isoinfo) echo "genisoimage" ;; zypper:*) echo "$cmd" ;; *) echo "$cmd" ;; esac } # Offer to install missing tools. $1 = "required" (abort if declined) or # "optional" (warn and continue). Remaining args = command names. ensure_tools() { local level="$1"; shift local missing=() cmd for cmd in "$@"; do command -v "$cmd" >/dev/null || missing+=("$cmd") done (( ${#missing[@]} == 0 )) && return 0 local pkgs=() m for m in "${missing[@]}"; do pkgs+=("$(pkg_for "$m")"); done # de-dupe packages (e.g. two commands from one package) mapfile -t pkgs < <(printf '%s\n' "${pkgs[@]}" | awk '!seen[$0]++') warn "Missing ${level} tools: ${missing[*]}" if [[ -z "$PM" ]]; then [[ "$level" == "required" ]] \ && die "No known package manager (apt/dnf/pacman/zypper). Install manually: ${missing[*]}" \ || { warn "Cannot auto-install (unknown package manager); continuing without: ${missing[*]}"; return 1; } fi # Build the copy-paste command(s) as a LIST, shown with a leading 'sudo' so # they work if pasted into the user's own terminal. Kept as separate lines # (not one '&&' chain) so each can be run/inspected individually. local -a CMDS=() [[ "$PM" == "apt" ]] && CMDS+=("sudo apt-get update") CMDS+=("sudo $PM_INSTALL ${pkgs[*]}") # helper: print the command list (reused for the initial show and [M]). print_cmds() { local c for c in "${CMDS[@]}"; do printf ' %s%s%s\n' "$BOLD" "$c" "$RST" done } printf '\n To install, run %s:\n\n' "$( (( ${#CMDS[@]} > 1 )) && echo 'these commands' || echo 'this command' )" print_cmds printf '\n Options:\n' printf ' %s[A]%s Auto-install now (this script runs it for you)\n' "$BOLD" "$RST" printf ' %s[M]%s I will run it myself — then come back\n' "$BOLD" "$RST" printf ' %s[S]%s Skip%s\n' "$BOLD" "$RST" \ "$( [[ "$level" == required ]] && echo ' / abort (these are required)' || echo ' (continue without them)')" while true; do read -r -p " Choose [A/M/S]: " a case "${a,,}" in a|auto) [[ "$PM" == "apt" ]] && apt-get update -qq || true # shellcheck disable=SC2086 $PM_INSTALL "${pkgs[@]}" || { [[ "$level" == "required" ]] && die "Install failed. Aborting." \ || { warn "Install failed; continuing without: ${missing[*]}"; return 1; } } break ;; m|manual|"") printf '\n Run %s in another terminal, then come back here:\n\n' \ "$( (( ${#CMDS[@]} > 1 )) && echo 'these' || echo 'this' )" print_cmds printf '\n' read -r -p " Press ENTER when done (or type 's' to skip): " done_flag [[ "${done_flag,,}" == s* ]] && { a="s"; } || { a="recheck"; } ;; s|skip|abort) [[ "$level" == "required" ]] && die "Required tools not installed. Aborting." warn "Continuing without: ${missing[*]}"; return 1 ;; *) warn "Please answer A, M, or S."; continue ;; esac # After auto-install OR a manual "I did it", re-check what's present. local still=() for cmd in "${missing[@]}"; do command -v "$cmd" >/dev/null || still+=("$cmd") done if (( ${#still[@]} == 0 )); then ok "All ${level} tools now present." return 0 fi # Something still missing. if [[ "$a" == "s" ]]; then [[ "$level" == "required" ]] && die "Still missing (required): ${still[*]}. Aborting." warn "Still missing (optional): ${still[*]}"; return 1 fi warn "Still missing: ${still[*]}" # Loop back so the user can try manual again, auto, or give up. missing=("${still[@]}") done # Final verification after the [A]uto path. local still=() for cmd in "${missing[@]}"; do command -v "$cmd" >/dev/null || still+=("$cmd") done if (( ${#still[@]} > 0 )); then [[ "$level" == "required" ]] && die "Still missing after install: ${still[*]}" \ || { warn "Still missing (optional): ${still[*]}"; return 1; } fi return 0 } # Hard requirements — abort if we can't get them. ensure_tools required lsblk dd cmp udevadm blockdev numfmt xxd # Nice-to-haves — offer, but the script degrades without them. ensure_tools optional pv eject || true # ISO inspection: we only need ONE of these for the Secure Boot check. Offer # the best available for this distro if none is present. if ! command -v 7z >/dev/null && ! command -v bsdtar >/dev/null && ! command -v isoinfo >/dev/null; then ensure_tools optional bsdtar || true fi HAVE_PV=0; command -v pv >/dev/null && HAVE_PV=1 HAVE_EJECT=0; command -v eject >/dev/null && HAVE_EJECT=1 # ---- parse arguments ------------------------------------------------------- # Usage: isotousb.sh [--linux|--windows|--auto] [ISO] # --linux : force the Linux/hybrid raw-dd method (skip auto-detect) # --windows : force the Windows partitioned (WoeUSB) method # --auto : auto-detect (default) # The OS-family choice can also be made interactively later. FORCE_TYPE="" # "", "linux", or "windows" ISO="" while (( $# )); do case "$1" in --linux|--linux-iso) FORCE_TYPE="linux" ;; --windows|--windows-iso) FORCE_TYPE="windows" ;; --auto) FORCE_TYPE="" ;; -h|--help) exit 0 ;; # already handled by the early scan -*) die "Unknown option: $1 (try --help)" ;; *) [[ -z "$ISO" ]] && ISO="$1" || die "Unexpected extra argument: $1" ;; esac shift done # ---- resolve the ISO ------------------------------------------------------- if [[ -z "$ISO" ]]; then read -r -e -p "Path to ISO image: " ISO fi [[ -f "$ISO" ]] || die "ISO not found: $ISO" ISO_SIZE=$(stat -c%s "$ISO") (( ISO_SIZE > 0 )) || die "ISO is empty: $ISO" ISO_HUMAN=$(numfmt --to=iec --suffix=B "$ISO_SIZE" 2>/dev/null || echo "${ISO_SIZE} bytes") info "Image: ${BOLD}${ISO}${RST} (${ISO_HUMAN})" # ---- inspect the ISO once (listing reused for SB + type classification) ---- # We peek at the ISO's file listing without mounting it. Best-effort: if no # listing tool is available, downstream checks fall back to "unknown". ISO_CMD_USED='' # remember which tool worked, so we can print it for the user get_iso_listing() { if command -v 7z >/dev/null; then ISO_CMD_USED="7z l -- \"$ISO\"" 7z l -- "$ISO" 2>/dev/null || true elif command -v bsdtar >/dev/null; then ISO_CMD_USED="bsdtar -tf \"$ISO\"" bsdtar -tf "$ISO" 2>/dev/null || true elif command -v isoinfo >/dev/null; then ISO_CMD_USED="isoinfo -f -i \"$ISO\"" isoinfo -f -i "$ISO" 2>/dev/null || true fi } LISTING="$(get_iso_listing)" # isohybrid check: does the image have an MBR boot signature so a raw copy is # bootable? (0x55AA in the last 2 bytes of sector 0.) detect_hybrid() { local sig sig=$(dd if="$ISO" bs=512 count=1 2>/dev/null | tail -c2 | xxd -p 2>/dev/null || true) [[ "$sig" == "55aa" ]] && echo "yes" || echo "no" } # Secure Boot capability, from the listing. detect_secureboot() { [[ -z "$LISTING" ]] && { echo "unknown"; return; } if grep -qiE 'EFI/BOOT/BOOT(X64|AA64|IA32)\.EFI' <<<"$LISTING"; then if grep -qiE 'EFI/BOOT/(grub|mm|fb)(x64|aa64)?\.efi' <<<"$LISTING"; then echo "yes" else echo "uefi" fi else echo "no" fi } # Classify the ISO so we can pick the WRITE METHOD: # linux -> hybrid image, correct method is a raw dd byte copy # windows -> contains bootmgr + sources/install.wim, needs a partitioned # FAT/NTFS copy (delegated to WoeUSB-ng) # other -> non-hybrid and not obviously Windows (rare; treat like windows # path is unsafe, so we default to raw + a loud warning) # unknown -> couldn't inspect; ask the user detect_iso_type() { local hybrid="$1" # NOTE on anchoring: listing tools (esp. 7z) prefix each row with columns # (DATE TIME ATTR SIZE ...), so the path token is preceded by WHITESPACE, # never '^' or '/'. Anchor on [[:space:]] or line-start, not (^|/). if [[ -n "$LISTING" ]] \ && grep -qiE '(^|[[:space:]])(bootmgr(\.efi)?|sources[\\/]install\.(wim|esd|swm))([[:space:]]|$)' <<<"$LISTING"; then echo "windows"; return fi if [[ "$hybrid" == "yes" ]]; then echo "linux"; return fi [[ -z "$LISTING" ]] && { echo "unknown"; return; } echo "other" } HYBRID=$(detect_hybrid) SB=$(detect_secureboot) ISO_TYPE=$(detect_iso_type "$HYBRID") ISO_TYPE_AUTO="$ISO_TYPE" # remember what auto-detect concluded # A command-line --linux/--windows flag overrides the heuristic outright. if [[ -n "$FORCE_TYPE" ]]; then ISO_TYPE="$FORCE_TYPE" info "ISO type forced by flag: ${BOLD}${ISO_TYPE}${RST} (auto-detect said: ${ISO_TYPE_AUTO})." fi # Is install.wim/esd > 4GB? (only meaningful for Windows; drives FAT-vs-NTFS.) # 7z's `l` columns vary (a non-compressed ISO omits the "Compressed" column), # so instead of trusting a fixed column index we grab the LARGEST integer on # the matched install.* line — the uncompressed byte size is by far the biggest # number there (date/time contain separators, attr is alphabetic). WIM_BIG="unknown" if [[ "$ISO_TYPE" == "windows" ]] && command -v 7z >/dev/null; then biggest=$(7z l -- "$ISO" 2>/dev/null \ | grep -iE 'sources[\\/]install\.(wim|esd)' \ | grep -oE '[0-9]{4,}' | sort -n | tail -1) if [[ "$biggest" =~ ^[0-9]+$ ]]; then (( biggest > 4294967296 )) && WIM_BIG="yes" || WIM_BIG="no" fi fi # ---- report findings ------------------------------------------------------- case "$SB" in yes) ok "Secure Boot: ISO ships a signed shim — should boot with Secure Boot ${BOLD}ON${RST}." ;; uefi) warn "UEFI-bootable, but no shim chain detected — Secure Boot may need to be ${BOLD}OFF${RST}." ;; no) warn "No UEFI removable-media loader found — likely legacy/BIOS, Windows, or Secure Boot must be OFF." ;; unknown) warn "Secure Boot: could not inspect ISO (install p7zip/bsdtar/isoinfo to enable this check)." ;; esac case "$ISO_TYPE" in linux) ok "ISO type: ${BOLD}Linux / hybrid${RST} — correct method is a raw byte-for-byte write (dd)." ;; windows) ok "ISO type: ${BOLD}Windows installer${RST} — needs a partitioned FAT/NTFS copy (via WoeUSB-ng)." ;; other) warn "ISO type: ${BOLD}non-hybrid, non-Windows${RST} — unusual. Raw dd may not boot." ;; unknown) warn "ISO type: ${BOLD}unknown${RST} (couldn't inspect the ISO). You'll be asked which method to use." ;; esac [[ "$WIM_BIG" == "yes" ]] && warn "install.wim is >4GB — FAT32 can't hold it; NTFS/split required (WoeUSB handles this)." # Let the user verify the classification themselves. printf '%s\n' "${DIM} Verify manually if you like:" [[ -n "$ISO_CMD_USED" ]] && printf '%s\n' " list files : ${ISO_CMD_USED}" printf '%s\n' " hybrid MBR : dd if=\"$ISO\" bs=512 count=1 2>/dev/null | tail -c2 | xxd" printf '%s\n' " (55aa in the last 2 bytes = hybrid/dd-writable)${RST}" # ---- OS-family confirmation, ONLY when detection is ambiguous -------------- # The common case is `isotousb foo.iso` with a confident linux/windows result: # we just proceed silently (auto-detect already printed what it found). We only # stop to ask when the ISO is genuinely unclear (non-hybrid & non-Windows, or # uninspectable). A wrong guess can always be corrected with --linux/--windows. if [[ -z "$FORCE_TYPE" && ( "$ISO_TYPE" == "other" || "$ISO_TYPE" == "unknown" ) ]]; then printf '\n%s\n' "${BOLD}Couldn't confidently identify this ISO. Which is it?${RST}" printf ' %s[L]%s Linux / BSD / any bootable Linux ISO → raw dd ${DIM}(default)${RST}\n' "$BOLD" "$RST" printf ' %s[W]%s Windows installer → WoeUSB\n' "$BOLD" "$RST" read -r -p " Family [L/W, Enter=L]: " fam case "${fam,,}" in w|win|windows) ISO_TYPE="windows" ;; *) ISO_TYPE="linux" ;; # Enter or anything else => Linux/raw esac info "Treating as: ${BOLD}${ISO_TYPE}${RST}." if [[ "$ISO_TYPE" == "linux" && "$HYBRID" != "yes" ]]; then warn "This ISO has no hybrid MBR signature — a raw dd copy may not boot. Continue only if you're sure it's a dd-writable image." fi else # Confident result: no prompt. Just remind how to override if it's ever wrong. printf '%s\n' "${DIM} (If this is wrong, re-run with --linux or --windows to force it.)${RST}" fi # ---- enumerate candidate USB / removable disks ----------------------------- # We restrict to whole disks that are removable OR on the usb transport, so the # internal system disk is never even offered as a target. info "Scanning for removable / USB disks..." mapfile -t CANDS < <( lsblk -dnp -o NAME,RM,TRAN,TYPE 2>/dev/null \ | awk '$4=="disk" && ($2=="1" || $3=="usb") {print $1}' ) (( ${#CANDS[@]} > 0 )) || die "No removable/USB disks found. Plug the stick in and retry." printf '\n%s\n' "${BOLD}Available target disks:${RST}" i=0 for dev in "${CANDS[@]}"; do size=$(lsblk -dn -o SIZE "$dev") model=$(lsblk -dn -o MODEL "$dev" | sed 's/ *$//') vendor=$(lsblk -dn -o VENDOR "$dev" 2>/dev/null | sed 's/ *$//') tran=$(lsblk -dn -o TRAN "$dev") printf ' %s[%d]%s %s%-12s%s %8s %s %s %s(%s)%s\n' \ "$SEL" "$i" "$RST" "$CYN" "$dev" "$RST" "$size" "$vendor" "${model:-?}" "$DIM" "$tran" "$RST" # show existing partitions so the user recognises their stick by its contents lsblk -np -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT "$dev" | tail -n +2 \ | sed "s/^/ ${DIM}/;s/$/${RST}/" i=$((i + 1)) # NOT ((i++)) — that returns the OLD value, which is 0 on the # first pass => exit status 1 => set -e would kill the script. done echo # ---- select target --------------------------------------------------------- read -r -p "Select target disk number [0-$((${#CANDS[@]}-1))]: " sel [[ "$sel" =~ ^[0-9]+$ ]] && (( sel < ${#CANDS[@]} )) || die "Invalid selection." TARGET="${CANDS[$sel]}" TGT_SIZE_BYTES=$(blockdev --getsize64 "$TARGET") TGT_SIZE_HUMAN=$(lsblk -dn -o SIZE "$TARGET") TGT_MODEL=$(lsblk -dn -o MODEL "$TARGET" | sed 's/ *$//') # sanity: image must fit (raw dd needs ISO<=disk; WoeUSB needs room for files) (( ISO_SIZE <= TGT_SIZE_BYTES )) \ || die "ISO (${ISO_HUMAN}) is larger than target ${TARGET} (${TGT_SIZE_HUMAN}). Aborting." # ---- derive the write METHOD from the (confirmed) ISO type ----------------- # raw = byte-for-byte dd (correct for Linux/hybrid ISOs) # woeusb= partitioned FAT/NTFS copy via WoeUSB-ng (correct for Windows ISOs) # The OS family was already confirmed/overridden above, so we don't ask again. case "$ISO_TYPE" in windows) METHOD="woeusb" ;; *) METHOD="raw" ;; # linux / other / unknown all use raw dd esac info "Write method: ${BOLD}${METHOD}${RST}$( [[ "$METHOD" == woeusb ]] && echo ' (partitioned Windows copy via WoeUSB)' || echo ' (raw dd byte-for-byte)')" # sanity: refuse if the ISO itself lives on the target disk — we'd corrupt the # source mid-write. Resolve the block device backing the ISO's filesystem and # compare its parent disk to the target. if command -v findmnt >/dev/null; then ISO_SRC=$(findmnt -no SOURCE -T "$ISO" 2>/dev/null || true) if [[ -n "$ISO_SRC" && -b "$ISO_SRC" ]]; then # strip partition to get the parent disk (e.g. /dev/sdb1 -> /dev/sdb) ISO_DISK=$(lsblk -no PKNAME "$ISO_SRC" 2>/dev/null || true) [[ -n "$ISO_DISK" ]] && ISO_DISK="/dev/$ISO_DISK" || ISO_DISK="$ISO_SRC" if [[ "$ISO_DISK" == "$TARGET" ]]; then die "The ISO is stored ON ${TARGET} — writing would destroy the source mid-copy. Move the ISO elsewhere first." fi fi fi # refuse if the disk (or any of its partitions) is mounted, e.g. it's your root MOUNTED=$(lsblk -nrp -o MOUNTPOINT "$TARGET" | grep -v '^$' || true) if [[ -n "$MOUNTED" ]]; then warn "Target has mounted filesystems:" printf ' %s\n' "$MOUNTED" read -r -p "Unmount them and continue? [y/N] " u [[ "$u" == [yY] ]] || die "Aborted (target was mounted)." fi # ---- final confirmation ---------------------------------------------------- printf '\n%s\n' "${RED}${BOLD}################ DESTRUCTIVE ################${RST}" printf '%s\n' "About to ERASE and overwrite:" printf ' Disk : %s%s%s %s (%s)\n' "$BOLD" "$TARGET" "$RST" "$TGT_SIZE_HUMAN" "${TGT_MODEL:-?}" printf ' With : %s\n' "$ISO" printf '%s\n' "${RED}Everything on ${TARGET} will be permanently lost.${RST}" printf '\nTo confirm, type the disk size exactly as shown (%s%s%s): ' "$BOLD" "$TGT_SIZE_HUMAN" "$RST" read -r confirm [[ "$confirm" == "$TGT_SIZE_HUMAN" ]] || die "Confirmation did not match. Aborted — nothing was written." # ---- unmount any partitions on the target ---------------------------------- info "Unmounting any partitions on ${TARGET}..." while read -r part; do [[ -n "$part" ]] || continue umount "$part" 2>/dev/null || true done < <(lsblk -nrp -o NAME,MOUNTPOINT "$TARGET" | awk '$2!=""{print $1}') if [[ "$METHOD" == "raw" ]]; then # ---- RAW dd path (Linux/hybrid ISOs) ----------------------------------- info "Writing image (raw, byte-for-byte) to ${BOLD}${TARGET}${RST}..." # We use dd's OWN status=progress, NOT a `pv | dd` pipe. The pipe measures # how fast bytes enter the kernel's write CACHE (RAM) — which is why it # shows a misleading instant "100%" and then hangs silently on the flush. # dd status=progress tracks the write itself. conv=fsync forces the data to # physical media before dd exits (so the count is real, not cached). # (We avoid oflag=direct: it gives even more accurate progress but fails # with EINVAL on the final partial block on some block devices / 4Kn.) printf ' %sTip: dd holds output until the first block completes, then updates every second.%s\n' "$DIM" "$RST" dd if="$ISO" of="$TARGET" bs=4M conv=fsync status=progress dd_rc=$? (( dd_rc == 0 )) || die "dd failed (exit $dd_rc). The write did not complete." # Even with conv=fsync, do a final global sync + device flush and SHOW it, # because the last chunk can still be draining to slow flash. A spinner so # you can see it's working, not frozen. printf '%s' "${CYN}==>${RST} Flushing remaining buffers to the device (do NOT unplug)... " ( sync; blockdev --flushbufs "$TARGET" 2>/dev/null || true ) & _flush_pid=$! _spin='|/-\'; _n=0 while kill -0 "$_flush_pid" 2>/dev/null; do printf '%s\b' "${_spin:_n++%4:1}" sleep 0.3 done wait "$_flush_pid" 2>/dev/null || true printf 'done.\n' ok "Write complete." # ---- verify (the part plain dd skips) ---------------------------------- # Read the first ISO_SIZE bytes back off the device and byte-compare to the # source. This is what makes Etcher trustworthy: it proves the bytes landed # (catches dying/counterfeit sticks and truncated writes). info "Verifying: reading back ${ISO_HUMAN} from ${TARGET} and comparing..." # Drop caches so we truly read from the device, not RAM. sync; echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true verify_ok=0 if (( HAVE_PV )); then # Stream the device through pv (real read progress + ETA) and diff it # against the source. pv stops the moment head closes the pipe at # ISO_SIZE, so it never reads the whole (possibly huge) device. # pv's bar goes to the terminal via /dev/tty (stdout is the data pipe); # if /dev/tty isn't available (non-interactive), send it to /dev/null. if { : >/dev/tty; } 2>/dev/null; then _pverr=/dev/tty; else _pverr=/dev/null; fi printf ' %sreading the stick back (this is the slow part — pv shows real progress):%s\n' "$DIM" "$RST" if pv -s "$ISO_SIZE" -pterb "$TARGET" 2>"$_pverr" \ | head -c "$ISO_SIZE" | cmp -s - "$ISO"; then verify_ok=1 fi else # No pv: cmp directly (silent but correct). Announce the wait. printf ' %sreading %s back (no pv installed — no bar; please wait)...%s\n' "$DIM" "$ISO_HUMAN" "$RST" cmp -n "$ISO_SIZE" "$ISO" "$TARGET" && verify_ok=1 fi if (( verify_ok == 1 )); then ok "${BOLD}Verification PASSED${RST} — device matches the image byte-for-byte." else die "Verification FAILED — the USB stick does not match the image. Do NOT trust this stick (bad/failing/counterfeit drive?)." fi else # ---- WoeUSB path (Windows / non-hybrid ISOs) --------------------------- # We delegate the partitioning + FAT/NTFS file copy to WoeUSB-ng (GPL-3.0), # the proven open-source tool for this. It creates the boot partition, # handles the >4GB install.wim (NTFS), and sets up UEFI:NTFS booting. info "Windows/non-hybrid method: delegating to WoeUSB-ng." # Is the woeusb CLI itself present? if ! command -v woeusb >/dev/null; then warn "WoeUSB-ng (the 'woeusb' command) is not installed." # Build the install steps as a LIST (deps first, then WoeUSB-ng itself), # so each shows as its own copy-paste command instead of one '&&' chain. local -a WCMDS=() case "$PM" in apt) WCMDS=("sudo apt-get update" "sudo apt-get install -y git p7zip-full python3-pip grub2-common grub-pc-bin parted dosfstools ntfs-3g" "sudo pip3 install WoeUSB-ng") ;; dnf) WCMDS=("sudo dnf install -y git p7zip p7zip-plugins python3-pip parted dosfstools ntfsprogs grub2-tools" "sudo pip3 install WoeUSB-ng") ;; pacman) WCMDS=("sudo pacman -S --needed woeusb-ng" "# not in your repos? use the AUR: yay -S woeusb-ng") ;; *) WCMDS=("pip3 install WoeUSB-ng" "# also needs: p7zip, grub, parted, dosfstools, ntfs-3g") ;; esac print_wcmds() { local c; for c in "${WCMDS[@]}"; do printf ' %s%s%s\n' "$BOLD" "$c" "$RST"; done; } printf '\n To install WoeUSB-ng, run these commands:\n\n' print_wcmds printf '\n Options:\n' printf ' %s[A]%s Auto-install now\n' "$BOLD" "$RST" printf ' %s[M]%s I will run it myself — then come back\n' "$BOLD" "$RST" printf ' %s[S]%s Skip / abort\n' "$BOLD" "$RST" while true; do read -r -p " Choose [A/M/S]: " wa case "${wa,,}" in a|auto) # We're already root; run each real command (skip '#' notes), # stripping any leading 'sudo ' (may be absent in a root shell). local c for c in "${WCMDS[@]}"; do [[ "$c" == \#* ]] && continue # shellcheck disable=SC2086 eval "${c//sudo /}" || warn "Step failed: $c" done ;; m|manual|"") printf '\n Run these in another terminal, then come back here:\n\n' print_wcmds printf '\n' read -r -p " Press ENTER when done (or 's' to skip): " d [[ "${d,,}" == s* ]] && die "Aborted — WoeUSB not installed." ;; s|skip|abort) die "Aborted — WoeUSB not installed." ;; *) warn "Answer A, M, or S."; continue ;; esac command -v woeusb >/dev/null && { ok "woeusb is now available."; break; } warn "'woeusb' still not on PATH." done fi # Pick the target filesystem: NTFS is the safe default (handles >4GB wim). WOE_FS="NTFS" [[ "$WIM_BIG" == "no" ]] && WOE_FS="FAT" # small wim => FAT for widest UEFI/SB support info "Running: ${BOLD}woeusb --device \"$ISO\" $TARGET --target-filesystem $WOE_FS${RST}" printf ' %s(WoeUSB does its own partitioning, formatting, and file copy — this can take several minutes.)%s\n' "$DIM" "$RST" if woeusb --device "$ISO" "$TARGET" --target-filesystem "$WOE_FS"; then ok "${BOLD}WoeUSB finished successfully.${RST}" warn "Note: read-back verification is only done for the raw method; WoeUSB does a file-level copy, so trust its own exit status above." else die "WoeUSB failed. See its output above. (Common causes: target busy/mounted, missing grub/ntfs deps, or a bad ISO.)" fi fi # ---- finish ---------------------------------------------------------------- sync if (( HAVE_EJECT )); then eject "$TARGET" 2>/dev/null && ok "Ejected ${TARGET} — safe to remove." \ || warn "Could not eject (device may be busy); run 'sync' then remove." else ok "Done. Run 'sync' once more, then it's safe to remove ${TARGET}." fi # ---- Secure Boot advice, tailored to what we detected ---------------------- if [[ "$ISO_TYPE" == "windows" ]]; then printf '\n%s\n' "${GRN}Windows installers boot Microsoft's own signed loader — Secure Boot can stay ON.${RST}" else case "$SB" in yes) printf '\n%s\n' "${GRN}This ISO supports Secure Boot — you can leave Secure Boot ON in firmware.${RST}" ;; uefi|no|unknown) printf '\n%s\n' "${YEL}If the machine won't boot this stick, disable Secure Boot in UEFI firmware settings.${RST}" ;; esac fi