#!/usr/bin/env bash # # Camera Bridge OS flasher: writes the newest Camera Bridge OS image to a USB stick or SD card. # (The project-specific settings are in the PROJECT CONFIG block below; the drive safety logic is generic.) # # curl -fsSL https://www.coreysilvia.com/flash.sh | bash # # Or download it, read it, then run it: # # curl -fsSLO https://www.coreysilvia.com/flash.sh && less flash.sh && bash flash.sh # # Writing an image erases a drive, so this script is deliberately careful: # - It only ever offers USB sticks, SD cards and USB drives: never a drive inside the computer, never the drive the # computer started from, never a drive holding a system volume, swap or a Time Machine backup, never a disk image, # RAID set or APFS container, nothing smaller than the image and nothing larger than 2 TB. # - It takes no arguments. In particular it never takes a drive name: it always asks, and always checks. # - You pick a drive by number, then type its name exactly, then type ERASE. # - It checks the drive again just before writing, and stops if anything about it changed. # - It downloads the image from GitHub and checks its SHA-256 checksum before anything is written. # - Ctrl-C at any question stops it without writing anything. # # FLASH_DRY_RUN=1 does everything except the write: it lists, asks and checks, but never unmounts, writes or ejects. # # Works on macOS (diskutil) and Linux (lsblk). Source: https://github.com/coalsi/camera-bridge set -euo pipefail # ===================================================================================================================== # PROJECT CONFIG: everything specific to Camera Bridge OS is in this block. Everything below it (downloading, # checking, choosing and writing a drive) is generic, so another project can reuse this script by changing only this. # ===================================================================================================================== PROJECT_NAME="Camera Bridge OS" REPO="coalsi/camera-bridge" # The GitHub repo whose releases hold the image. TAG_PREFIX="os-v" # Release tags look like os-v0.1.113; pre-releases count. ASSET_PREFIX="camera-bridge-os-" # The image is ... ASSET_SUFFIX="-amd64.img.xz" # ...an xz-compressed raw disk image. CHECKSUM_SUFFIX=".sha256" # Its checksum is the image's name plus this (" "). TARGET_DEVICE="mini PC" # What the stick is for, in plain words. FINISH_MESSAGE="Done. Plug it into the mini PC and boot from USB." FINISH_HINT="Then open http://camera-bridge.local in a browser on the same network." GUIDE_URL="https://github.com/coalsi/camera-bridge/blob/main/docs/linux/USER-GUIDE.md" # ===================================================================================================================== RELEASES_API="https://api.github.com/repos/${REPO}/releases?per_page=50" DOWNLOAD_BASE="https://github.com/${REPO}/releases/download" # Drive sizes are sold in decimal units, so the limits are decimal too. MAX_BYTES=2000000000000 # 2 TB: nothing larger is ever offered. WARN_BYTES=256000000000 # 256 GB: larger drives get an extra warning. DRY_RUN="${FLASH_DRY_RUN:-0}" WORK_DIR="" WRITING=0 # --------------------------------------------------------------------------------------------------------------------- # Output and questions # --------------------------------------------------------------------------------------------------------------------- if [[ -t 1 ]]; then BOLD=$'\033[1m' RED=$'\033[31m' YELLOW=$'\033[33m' GREEN=$'\033[32m' DIM=$'\033[2m' RESET=$'\033[0m' else BOLD="" RED="" YELLOW="" GREEN="" DIM="" RESET="" fi say() { printf '%s\n' "$*"; } step() { printf '\n%s== %s ==%s\n\n' "$BOLD" "$*" "$RESET"; } warn() { printf '%s%s%s\n' "$YELLOW" "$*" "$RESET"; } die() { printf '\n%sStopped: %s%s\n' "$RED" "$*" "$RESET" >&2 if [[ "$WRITING" != 1 ]]; then printf 'Nothing was written to any drive.\n' >&2; fi exit 1 } # Questions are read from the terminal itself (file descriptor 3, opened on /dev/tty), never from standard input: # under `curl ... | bash`, standard input is this script. open_tty() { if ! { exec 3/dev/null; then die "this script asks questions, so it needs a terminal. Open Terminal and paste the command there." fi } # ask "Question: " -> the answer in REPLY. End of input (Ctrl-D) stops the script. ask() { printf '%s' "$1" if ! IFS= read -r REPLY <&3; then printf '\n' die "no answer." fi } # confirm "Question" -> succeeds only on y or yes. confirm() { ask "$1 [y/N] " case "$(printf '%s' "$REPLY" | tr '[:upper:]' '[:lower:]')" in y | yes) return 0 ;; *) return 1 ;; esac } cleanup() { if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then rm -rf "$WORK_DIR"; fi } on_interrupt() { trap - INT TERM printf '\n' if [[ "$WRITING" == 1 ]]; then printf '%sStopped while writing. The drive is incomplete: run this again to write it properly.%s\n' "$RED" "$RESET" >&2 else printf 'Stopped. Nothing was written to any drive.\n' >&2 fi exit 130 } # 31914983424 -> "31.9 GB" (decimal units, as drives are sold). human_size() { awk -v b="$1" 'BEGIN { split("bytes KB MB GB TB", unit, " "); i = 1 while (b >= 1000 && i < 5) { b /= 1000; i++ } if (i == 1) printf "%d %s", b, unit[i]; else printf "%.1f %s", b, unit[i] }' } is_number() { [[ "$1" =~ ^[0-9]+$ ]]; } # --------------------------------------------------------------------------------------------------------------------- # macOS: which drives may be offered # --------------------------------------------------------------------------------------------------------------------- # plist_get FILE KEY.PATH -> the value as text ("true"/"false" for booleans), or nothing when the key is missing. plist_get() { if command -v plutil >/dev/null 2>&1; then plutil -extract "$2" raw -o - "$1" 2>/dev/null || true else python3 - "$1" "$2" <<'PY' 2>/dev/null || true import plistlib, sys value = plistlib.load(open(sys.argv[1], "rb")) for part in sys.argv[2].split("."): try: value = value[int(part)] if isinstance(value, list) else value[part] except (KeyError, IndexError, ValueError, TypeError): sys.exit(0) if isinstance(value, bool): print("true" if value else "false") elif isinstance(value, list): print(len(value)) elif not isinstance(value, dict): print(value) PY fi } # "disk4s2" -> "disk4"; "disk12" -> "disk12". mac_whole_of() { printf '%s\n' "$1" | sed -E 's/^(disk[0-9]+).*/\1/'; } # mac_eligibility INFO_PLIST IMAGE_BYTES BOOT_DISK "PROTECTED DISKS" # INFO_PLIST is `diskutil info -plist diskN`. Prints OK, or NO:. mac_eligibility() { local file="$1" image_bytes="$2" boot="$3" protected=" $4 " local id whole bus vop internal size removable raid id="$(plist_get "$file" DeviceIdentifier)" whole="$(plist_get "$file" WholeDisk)" bus="$(plist_get "$file" BusProtocol)" vop="$(plist_get "$file" VirtualOrPhysical)" internal="$(plist_get "$file" Internal)" if ! [[ "$id" =~ ^disk[0-9]+$ ]] || [[ "$whole" != true ]]; then echo "NO:not a whole drive"; return; fi if [[ "$bus" == "Disk Image" ]]; then echo "NO:a disk image, not a drive"; return; fi if [[ -n "$(plist_get "$file" APFSPhysicalStores)" || -n "$(plist_get "$file" APFSContainerReference)" ]]; then echo "NO:an APFS container (made by macOS on top of a drive), not a drive itself" return fi if [[ "$id" == disk0 ]]; then echo "NO:disk0 is always your Mac's own drive"; return; fi if [[ -n "$boot" && "$id" == "$boot" ]]; then echo "NO:your Mac started up from this drive"; return; fi if [[ "$internal" != false ]]; then echo "NO:it is inside your Mac"; return; fi if [[ "$vop" != Physical ]]; then echo "NO:a virtual disk (RAID set, container or similar), not a drive itself"; return; fi if [[ "$(plist_get "$file" OSInternalMedia)" == true || "$(plist_get "$file" SystemImage)" == true ]]; then echo "NO:macOS uses it for itself" return fi case "$bus" in USB | "Secure Digital") ;; *) echo "NO:connected by ${bus:-an unknown connection}; only USB sticks, SD cards and USB drives are allowed"; return ;; esac removable=no for key in Removable RemovableMedia Ejectable RemovableMediaOrExternalDevice; do if [[ "$(plist_get "$file" "$key")" == true ]]; then removable=yes; fi done if [[ "$removable" != yes ]]; then echo "NO:macOS does not see it as removable"; return; fi raid="$(plist_get "$file" RAIDMaster)$(plist_get "$file" RAIDSlice)" if [[ "$raid" == *true* || "$(plist_get "$file" Content)" == *RAID* ]]; then echo "NO:part of a RAID set"; return; fi if [[ "$protected" == *" $id "* ]]; then echo "NO:it holds a system volume or a Time Machine backup"; return; fi size="$(plist_get "$file" Size)" if ! is_number "$size" || [[ "$size" == 0 ]]; then size="$(plist_get "$file" TotalSize)"; fi if ! is_number "$size" || [[ "$size" == 0 ]]; then echo "NO:its size is unknown (is a card in the reader?)"; return; fi if ((size < image_bytes)); then echo "NO:too small ($(human_size "$size")); the image needs $(human_size "$image_bytes")"; return; fi if ((size > MAX_BYTES)); then echo "NO:larger than 2 TB ($(human_size "$size")), so it is probably not a USB stick or SD card"; return; fi echo OK } # mac_backup_disks_from_apfs APFS_LIST_PLIST -> the whole drives under any APFS volume with the Backup role (Time # Machine), one per line. APFS_LIST_PLIST is `diskutil apfs list -plist`. mac_backup_disks_from_apfs() { local file="$1" containers volumes stores i j k roles containers="$(plist_get "$file" Containers)" is_number "$containers" || return 0 for ((i = 0; i < containers; i++)); do volumes="$(plist_get "$file" "Containers.$i.Volumes")" is_number "$volumes" || continue for ((j = 0; j < volumes; j++)); do roles="$(plutil -extract "Containers.$i.Volumes.$j.Roles" json -o - "$file" 2>/dev/null || python3 -c 'import plistlib,sys; print(plistlib.load(open(sys.argv[1],"rb"))["Containers"][int(sys.argv[2])]["Volumes"][int(sys.argv[3])].get("Roles",[]))' "$file" "$i" "$j" 2>/dev/null || true)" if [[ "$roles" == *Backup* ]]; then stores="$(plist_get "$file" "Containers.$i.PhysicalStores")" is_number "$stores" || continue for ((k = 0; k < stores; k++)); do mac_whole_of "$(plist_get "$file" "Containers.$i.PhysicalStores.$k.DeviceIdentifier")" done fi done done } # mac_physical_disks DEVICE -> the physical whole drive(s) under a disk, partition or APFS volume. mac_physical_disks() { local info stores k store info="$WORK_DIR/pd.plist" diskutil info -plist "$1" >"$info" 2>/dev/null 0)); then for ((k = 0; k < stores; k++)); do store="$(plist_get "$info" "APFSPhysicalStores.$k.APFSPhysicalStore")" [[ -n "$store" ]] && mac_whole_of "$store" done else mac_whole_of "$(plist_get "$info" ParentWholeDisk)" fi } # Fills MAC_BOOT (the startup drive), MAC_PROTECTED (drives that must never be offered) and $WORK_DIR/mounts # (lines of "diskNmount point", for showing what is on a drive). mac_scan_system() { local line dev mp disk tm_mp MAC_BOOT="$(mac_physical_disks / | head -n 1)" MAC_PROTECTED="disk0 ${MAC_BOOT}" : >"$WORK_DIR/mounts" # Every mounted volume: anything mounted outside /Volumes (the startup volumes, the system's own disk images) marks # its drive as protected, and so does a Time Machine folder. while IFS= read -r line; do dev="${line%% on *}" mp="${line#* on }" mp="${mp% (*}" [[ "$dev" =~ ^/dev/disk[0-9] ]] || continue for disk in $(mac_physical_disks "$dev"); do printf '%s\t%s\n' "$disk" "$mp" >>"$WORK_DIR/mounts" if [[ "$mp" != /Volumes/* || -d "$mp/Backups.backupdb" ]]; then MAC_PROTECTED="$MAC_PROTECTED $disk"; fi done done < <(mount) # APFS Time Machine volumes, mounted or not. if diskutil apfs list -plist >"$WORK_DIR/apfs.plist" 2>/dev/null /dev/null one line per top-level device: # name, size in bytes, type, removable (0/1), hotplug (0/1), transport, model, mount points of it and everything on # it (comma-separated). Reads `lsblk -J -b -o NAME,SIZE,TYPE,RM,HOTPLUG,TRAN,MODEL,MOUNTPOINTS` (or MOUNTPOINT). linux_disks_tsv() { if command -v python3 >/dev/null 2>&1; then python3 - "$1" <<'PY' import json, sys def flag(v): return 1 if v in (True, 1, "1", "true") else 0 def mounts(dev): found = [m for m in (dev.get("mountpoints") or [dev.get("mountpoint")]) if m] for child in dev.get("children") or []: found += mounts(child) return found def clean(v): return " ".join(str(v or "").replace("\t", " ").split()) for dev in json.load(open(sys.argv[1]))["blockdevices"]: print("\t".join([clean(dev.get("name")), clean(dev.get("size")), clean(dev.get("type")), str(flag(dev.get("rm"))), str(flag(dev.get("hotplug"))), clean(dev.get("tran")), clean(dev.get("model")), ",".join(clean(m) for m in mounts(dev))])) PY elif command -v jq >/dev/null 2>&1; then jq -r ' def flag: if . == true or . == 1 or . == "1" or . == "true" then "1" else "0" end; def clean: (. // "") | tostring | gsub("[\t ]+"; " ") | gsub("^ | $"; ""); def mounts: [((.mountpoints // [.mountpoint]) // [])[] | select(. != null)] + ([(.children // [])[] | mounts] | add // []); .blockdevices[] | [(.name|clean), (.size|clean), (.type|clean), (.rm|flag), (.hotplug|flag), (.tran|clean), (.model|clean), (mounts | map(clean) | join(","))] | @tsv' "$1" else die "this needs python3 or jq to read the list of drives. Install one of them and run this again." fi } # linux_eligibility "TSV LINE" IMAGE_BYTES "SYSTEM DISKS" -> OK, or NO:. linux_eligibility() { local name size type rm hotplug tran model mounts system=" $3 " mp IFS=$'\t' read -r name size type rm hotplug tran model mounts <<<"$1" if [[ "$type" != disk ]]; then echo "NO:not a whole drive ($type)"; return; fi case "$name" in loop* | sr* | ram* | zram* | md* | dm-* | nbd* | fd* | *boot[0-9]* | *rpmb*) echo "NO:not a USB stick or SD card"; return ;; nvme*) echo "NO:an internal NVMe drive"; return ;; esac if [[ -z "$tran" && "$name" =~ ^mmcblk[0-9]+$ ]]; then tran=mmc; fi case "$tran" in nvme | sata | ata | sas | pcie | fc | iscsi | virtio | ide | spi | ufs) echo "NO:an internal ${tran} drive"; return ;; esac if [[ "$tran" != usb && "$tran" != mmc && "$rm" != 1 && "$hotplug" != 1 ]]; then echo "NO:not a removable drive" return fi if [[ "$system" == *" $name "* ]]; then echo "NO:it holds your running system"; return; fi local IFS=, for mp in $mounts; do case "$mp" in / | /boot | /boot/* | /efi | /home | /home/* | /usr | /usr/* | /var | /var/* | /etc | /opt | /srv | /srv/* | /root | "[SWAP]") echo "NO:it holds your running system ($mp)" return ;; esac done unset IFS if ! is_number "$size" || [[ "$size" == 0 ]]; then echo "NO:its size is unknown (is a card in the reader?)"; return; fi if ((size < $2)); then echo "NO:too small ($(human_size "$size")); the image needs $(human_size "$2")"; return; fi if ((size > MAX_BYTES)); then echo "NO:larger than 2 TB ($(human_size "$size")), so it is probably not a USB stick or SD card"; return; fi echo OK } # The whole drives under /, /boot, /boot/efi, /home, /usr, /var and every swap device, from findmnt and /proc/swaps. linux_system_disks() { local target src { for target in / /boot /boot/efi /efi /home /usr /var; do src="$(findmnt -nro SOURCE --target "$target" 2>/dev/null 1 && $1 ~ /^\/dev\// { print $1 }' /proc/swaps 2>/dev/null || true } | sort -u | while IFS= read -r src; do lsblk -nrso NAME,TYPE "$src" 2>/dev/null /dev/null || lsblk -J -b -o NAME,SIZE,TYPE,RM,HOTPLUG,TRAN,MODEL,MOUNTPOINT /dev/null 2>&1; then xz -dc "$1" elif [[ "$(uname -s)" == Darwin ]] && command -v compression_tool >/dev/null 2>&1; then compression_tool -decode -a lzma -i "$1" elif command -v python3 >/dev/null 2>&1; then python3 -c 'import lzma, shutil, sys; shutil.copyfileobj(lzma.open(sys.argv[1]), sys.stdout.buffer, 1 << 20)' "$1" else die "this needs xz to unpack the image. Install it (for example: sudo apt install xz-utils) and run this again." fi } sha256_of() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{ print $1 }'; else sha256sum "$1" | awk '{ print $1 }'; fi } # Escapes a fixed string for use in an extended regular expression. ere_escape() { printf '%s' "$1" | sed 's/[][\\.^$*+?(){}|/]/\\&/g'; } # Sets VERSION, IMAGE_NAME, IMAGE_URL and IMAGE_BYTES (the download's size) from the newest TAG_PREFIX release that has # an image. Versions are compared as dotted numbers. find_release() { local json tag prefix suffix json="$(curl -fsSL --retry 3 -H 'Accept: application/vnd.github+json' "$RELEASES_API" 0)) || die "the image could not be unpacked." say "The image is good. Written out, it takes $(human_size "$RAW_BYTES")." } # --------------------------------------------------------------------------------------------------------------------- # Listing and choosing a drive # --------------------------------------------------------------------------------------------------------------------- # Fills the CHOICE_* arrays with the eligible drives, and prints the table, plus what was skipped and why. list_drives() { CHOICE_DEV=() CHOICE_MODEL=() CHOICE_SIZE=() CHOICE_CONN=() CHOICE_VOLS=() local skipped="" verdict id line name size tran model mounts if [[ "$OS" == Darwin ]]; then mac_scan_system diskutil list -plist external physical >"$WORK_DIR/list.plist" 2>/dev/null /dev/null | grep -oE 'disk[0-9]+' || true); do diskutil info -plist "$id" >"$WORK_DIR/$id.plist" 2>/dev/null "$WORK_DIR/lsblk.json" || die "lsblk could not list the drives." LINUX_SYSTEM="$(linux_system_disks)" while IFS= read -r line; do [[ -n "$line" ]] || continue IFS=$'\t' read -r name size _ _ _ tran model mounts <<<"$line" verdict="$(linux_eligibility "$line" "$RAW_BYTES" "$LINUX_SYSTEM")" if [[ "$verdict" == OK ]]; then CHOICE_DEV+=("$name") CHOICE_MODEL+=("${model:-Unknown}") CHOICE_SIZE+=("$size") CHOICE_CONN+=("${tran:-removable}") CHOICE_VOLS+=("${mounts:-(none mounted)}") elif [[ "$name" != loop* && "$name" != zram* && "$name" != ram* ]]; then skipped="${skipped} ${name} (${model:-unknown}): ${verdict#NO:}"$'\n' fi done < <(linux_disks_tsv "$WORK_DIR/lsblk.json") fi if [[ -n "$skipped" ]]; then say "${DIM}Not offered, to keep you safe:" printf '%s' "$skipped" say "$RESET" fi if ((${#CHOICE_DEV[@]} == 0)); then return 0; fi say "Drives you can write to:" say "" local i for ((i = 0; i < ${#CHOICE_DEV[@]}; i++)); do printf ' %s%d)%s %-10s %-28s %10s %-15s volumes: %s\n' "$BOLD" "$((i + 1))" "$RESET" "${CHOICE_DEV[$i]}" \ "${CHOICE_MODEL[$i]}" "$(human_size "${CHOICE_SIZE[$i]}")" "${CHOICE_CONN[$i]}" "${CHOICE_VOLS[$i]}" done say "" } # Checks the chosen drive again, from scratch, right before writing: it must still be eligible, and still the same # drive (same name, size and model). Dies otherwise. recheck_drive() { local dev="$1" size="$2" model="$3" verdict now_size now_model line found="" if [[ "$OS" == Darwin ]]; then mac_scan_system diskutil info -plist "$dev" >"$WORK_DIR/recheck.plist" 2>/dev/null "$WORK_DIR/lsblk.json" || die "lsblk could not list the drives." while IFS= read -r line; do if [[ "${line%%$'\t'*}" == "$dev" ]]; then found="$line"; fi done < <(linux_disks_tsv "$WORK_DIR/lsblk.json") [[ -n "$found" ]] || die "$dev is gone. Was it unplugged?" verdict="$(linux_eligibility "$found" "$RAW_BYTES" "$(linux_system_disks)")" IFS=$'\t' read -r _ now_size _ _ _ _ now_model _ <<<"$found" fi [[ "$verdict" == OK ]] || die "$dev can no longer be written: ${verdict#NO:}." if [[ "$now_size" != "$size" || "${now_model:-Unknown}" != "$model" ]]; then die "$dev changed since you chose it (it is now ${now_model:-unknown}, $(human_size "${now_size:-0}")). Was a different drive plugged in?" fi } # --------------------------------------------------------------------------------------------------------------------- # Writing # --------------------------------------------------------------------------------------------------------------------- dd_progress_flag() { if dd if=/dev/zero of=/dev/null count=1 status=progress >/dev/null 2>&1 /dev/null 2>&1; then decompress "$IMAGE_FILE" | pv -s "$RAW_BYTES" | sudo dd of="$target" bs=4m else # Without status=progress, macOS dd prints its progress on Ctrl-T. [[ -n "$progress" ]] || say "(Press Ctrl-T to see progress.)" decompress "$IMAGE_FILE" | sudo dd of="$target" bs=4m ${progress:+"$progress"} fi sync WRITING=0 say "Ejecting $dev..." diskutil eject "/dev/$dev" 1 { print $1 }'); do sudo umount "$part" /dev/null 2>&1; then decompress "$IMAGE_FILE" | pv -s "$RAW_BYTES" | sudo dd of="$target" bs=4M conv=fsync else decompress "$IMAGE_FILE" | sudo dd of="$target" bs=4M conv=fsync ${progress:+"$progress"} fi sync WRITING=0 say "Ejecting $dev..." if command -v eject >/dev/null 2>&1; then sudo eject "$target" /dev/null 2>&1; then udisksctl power-off -b "$target" /dev/null 2>&1 || die "this needs $tool, which is missing." done if [[ "$OS" == Darwin ]]; then for tool in diskutil plutil shasum; do command -v "$tool" >/dev/null 2>&1 || die "this needs $tool, which is missing."; done else command -v lsblk >/dev/null 2>&1 || die "this needs lsblk (from util-linux), which is missing." command -v findmnt >/dev/null 2>&1 || die "this needs findmnt (from util-linux), which is missing." command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 || die "this needs sha256sum, which is missing." if ! command -v python3 >/dev/null 2>&1 && ! command -v jq >/dev/null 2>&1; then die "this needs python3 or jq to read the list of drives. Install one of them and run this again." fi fi } main() { if (($# > 0)); then case "$1" in -h | --help) sed -n '2,24p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true exit 0 ;; *) die "this script takes no arguments, and never a drive name. Run it on its own and it will ask which drive to use." ;; esac fi OS="$(uname -s)" case "$OS" in Darwin | Linux) ;; *) die "this works on macOS and Linux only. On Windows, use balenaEtcher (https://etcher.balena.io) to write the image." ;; esac open_tty trap cleanup EXIT trap on_interrupt INT TERM check_tools step "$PROJECT_NAME installer" say "This puts $PROJECT_NAME on a USB stick or SD card, so you can start a $TARGET_DEVICE from it." say "" say "Here is what happens:" say " 1. It downloads the newest $PROJECT_NAME from GitHub and checks it is not damaged." say " 2. You plug in the USB stick or SD card. Only removable drives are listed: never your computer's own drive." say " 3. You choose the drive, type its name and type ERASE to confirm." say " 4. It ERASES that drive and writes $PROJECT_NAME to it. Everything on the drive is lost." say "" say "Your password is asked for only at the end, for the write itself. Press Ctrl-C at any time to stop." if [[ "$DRY_RUN" == 1 ]]; then warn "Dry run (FLASH_DRY_RUN=1): everything happens except the write. No drive is touched."; fi say "" confirm "Continue?" || die "you chose not to continue." step "1. The newest $PROJECT_NAME" find_release say "$PROJECT_NAME ${BOLD}${VERSION}${RESET}, a $(human_size "$IMAGE_BYTES") download." say "${DIM}${IMAGE_URL}${RESET}" say "" confirm "Download it?" || die "you chose not to download it." WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/image-flash.XXXXXX")" download_and_verify step "2. Plug in the USB stick or SD card" say "Use one with at least $(human_size "$RAW_BYTES") and nothing on it you want to keep." ask "Plug it in now, then press Enter. " say "Looking for drives..." sleep 2 list_drives if ((${#CHOICE_DEV[@]} == 0)); then say "${BOLD}No USB stick or SD card that can be used was found.${RESET}" say "Plug one in (at least $(human_size "$RAW_BYTES"), at most 2 TB) and run this again." say "Nothing was written to any drive." exit 0 fi step "3. Choose the drive to erase" local n="${#CHOICE_DEV[@]}" pick i ask "Type the number of the drive (1-$n), or press Ctrl-C to stop: " pick="$REPLY" if ! [[ "$pick" =~ ^[0-9]+$ ]] || ((pick < 1 || pick > n)); then die "\"$pick\" is not one of the numbers listed."; fi i=$((pick - 1)) local dev="${CHOICE_DEV[$i]}" model="${CHOICE_MODEL[$i]}" size="${CHOICE_SIZE[$i]}" conn="${CHOICE_CONN[$i]}" vols="${CHOICE_VOLS[$i]}" printf '\n%s%s\n' "$RED" "$BOLD" say " ######################################################################" say " # WARNING: EVERYTHING ON THIS DRIVE WILL BE ERASED, FOR GOOD. #" say " ######################################################################" printf '%s\n' "$RESET" say " Drive: ${BOLD}${dev}${RESET}" say " Model: $model" say " Size: $(human_size "$size")" say " Connection: $conn" say " Volumes: $vols" say "" say " Everything on it now:" if [[ "$OS" == Darwin ]]; then diskutil list "/dev/$dev" /dev/null | sed 's/^/ /' || true else lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT "/dev/$dev" /dev/null | sed 's/^/ /' || true fi say "" if ((size > WARN_BYTES)); then warn " This drive is LARGER THAN 256 GB. USB sticks and SD cards are usually smaller." warn " Make sure it really is the stick or card you mean to erase, and not a backup or storage drive." say "" fi say "If this is not the drive you mean, press Ctrl-C now. Nothing has been written." say "" ask "To confirm, type the drive's name exactly ($dev): " [[ "$REPLY" == "$dev" ]] || die "\"$REPLY\" is not \"$dev\"." ask "Type ERASE (in capitals) to erase $dev and write $PROJECT_NAME to it: " [[ "$REPLY" == ERASE ]] || die "you did not type ERASE." step "4. Writing" say "Checking $dev once more..." recheck_drive "$dev" "$size" "$model" say "${GREEN}$dev is still the same drive, and still allowed.${RESET}" if [[ "$DRY_RUN" == 1 ]]; then warn "Dry run: stopping here. Without FLASH_DRY_RUN=1 this would now unmount $dev, write the image with sudo dd and eject it." say "Nothing was written to any drive." exit 0 fi say "Your computer's password is needed to write to the drive." sudo -v /dev/null || exit 0 fi main "$@"