refactor: cleanup scripts

This commit is contained in:
Alex Maldonado
2026-05-12 15:21:31 -04:00
parent 9bdd8bd9d5
commit 74dc3215fd
10 changed files with 350 additions and 300 deletions
Regular → Executable
+67 -177
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env bash
# make-usb.sh
#
# Write a NixOS installer ISO to a USB stick (raw dd)
# and optionally append a writable "extras" partition with files
# (this repo, SSH keys, etc.) that you want available during install.
# Write a NixOS installer ISO to a USB stick (raw dd).
#
# The script self-elevates via sudo if not already running as root, so
# you can run it as your normal user.
#
# After booting the resulting USB on the target machine, fetch this repo
# over the network with `git clone <url>` from the installer shell.
set -euo pipefail
### Styling ###
if [[ -t 1 ]]; then
GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'; BLUE=$'\e[34m'
BOLD=$'\e[1m'; RESET=$'\e[0m'
@@ -23,23 +23,25 @@ err() { printf '%s[-]%s %s\n' "$RED" "$RESET" "$*" >&2; }
info() { printf '%s[i]%s %s\n' "$BLUE" "$RESET" "$*"; }
die() { err "$1"; exit "${2:-1}"; }
ISO_PATH=""
DEVICE=""
SKIP_EXTRAS=0
ALLOW_INTERNAL=0
ASSUME_YES=0
REPO_ROOT=""
# Save original args before the parser shifts them away; needed if we
# later re-exec under sudo.
ORIG_ARGS=("$@")
while (($#)); do
case "$1" in
--iso) ISO_PATH="${2:?missing path}"; shift 2 ;;
--device|--dev) DEVICE="${2:?missing path}"; shift 2 ;;
--no-extras) SKIP_EXTRAS=1; shift ;;
--allow-internal) ALLOW_INTERNAL=1; shift ;;
--yes|-y) ASSUME_YES=1; shift ;;
--repo-root) REPO_ROOT="${2:?missing path}"; shift 2 ;;
-h|--help)
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
awk '/^#!/ {next} /^[^#]/ {exit} {sub(/^# ?/, ""); print}' "$0"
exit 0 ;;
*) die "unknown arg: $1" 2 ;;
esac
@@ -47,10 +49,19 @@ done
### Preflight checks ###
(( EUID == 0 )) || die "run as root: sudo $0 $*" 1
# Self-elevate via sudo if not already root. We re-exec rather than tell
# the user to prefix the command, so `just make-usb` and `./scripts/make-usb.sh`
# both Just Work.
if (( EUID != 0 )); then
if ! command -v sudo >/dev/null 2>&1; then
die "must run as root and sudo is not installed" 1
fi
# ORIG_ARGS may be empty; ${ORIG_ARGS[@]+"${ORIG_ARGS[@]}"} expands
# safely under set -u even when the array is unset.
exec sudo --preserve-env=PATH -- "$0" ${ORIG_ARGS[@]+"${ORIG_ARGS[@]}"}
fi
# Require an interactive tty for any prompt we still need.
need_tty=0
[[ -z "$DEVICE" || -z "$ISO_PATH" ]] && need_tty=1
(( ASSUME_YES )) || need_tty=1
@@ -58,8 +69,8 @@ if (( need_tty )) && ! [[ -t 0 ]]; then
die "stdin is not a tty; pass --iso, --device, and --yes for non-interactive use" 2
fi
# Required commands. We try to be helpful about missing ones on NixOS.
REQUIRED=(lsblk dd sync wipefs sgdisk partprobe blockdev mount umount mountpoint mkfs.exfat awk)
# Required commands.
REQUIRED=(lsblk dd sync wipefs blockdev mount umount awk)
missing=()
for cmd in "${REQUIRED[@]}"; do
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
@@ -69,42 +80,16 @@ if ((${#missing[@]})); then
cat >&2 <<'EOF'
On NixOS / with Nix installed, re-run inside a shell that has them:
nix shell nixpkgs#util-linux nixpkgs#coreutils nixpkgs#gptfdisk \
nixpkgs#exfatprogs nixpkgs#parted -c sudo ./scripts/make-usb.sh
nix shell nixpkgs#util-linux nixpkgs#coreutils -c sudo ./scripts/make-usb.sh
On Debian/Ubuntu:
sudo apt install util-linux coreutils gdisk exfatprogs parted
sudo apt install util-linux coreutils
EOF
exit 3
fi
# Locate repo root if not provided (used as a copy candidate for extras).
if [[ -z "$REPO_ROOT" ]]; then
if REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null)"; then
:
else
REPO_ROOT=""
fi
fi
### Cleanup trap ###
TMP_MNT=""
cleanup() {
set +e
if [[ -n "$TMP_MNT" && -d "$TMP_MNT" ]]; then
mountpoint -q "$TMP_MNT" && umount "$TMP_MNT"
rmdir "$TMP_MNT" 2>/dev/null
fi
}
trap cleanup EXIT INT TERM
### Helpers ###
human() { numfmt --to=iec --suffix=B "$1" 2>/dev/null || echo "$1"; }
is_removable() {
@@ -182,8 +167,6 @@ select_device() {
### Pick device ###
if [[ -z "$DEVICE" ]]; then
select_device
else
@@ -205,8 +188,6 @@ log "Target: $DEVICE ($(human "$DEV_BYTES"))"
### Pick ISO ###
if [[ -z "$ISO_PATH" ]]; then
while :; do
read -r -e -p "Path to NixOS ISO: " ISO_PATH
@@ -228,18 +209,20 @@ if command -v file >/dev/null 2>&1; then
fi
fi
# Sanity: ISO must fit on disk with room for an extras partition.
MIN_EXTRAS_MB=64
if (( ISO_BYTES + MIN_EXTRAS_MB*1024*1024 > DEV_BYTES )); then
if (( ISO_BYTES > DEV_BYTES )); then
die "ISO is larger than the disk" 4
fi
warn "Less than ${MIN_EXTRAS_MB}MiB free after ISO; extras partition will be skipped."
SKIP_EXTRAS=1
# Sanity: ISO must fit on disk.
if (( ISO_BYTES > DEV_BYTES )); then
die "ISO is larger than the disk" 4
fi
# Optional sha256 sidecar verification.
if [[ -f "$ISO_PATH.sha256" ]]; then
# Resolve through symlinks so `make-usb --iso iso/latest-...iso` finds
# the sidecar that lives next to the real file.
ISO_REAL="$(readlink -f "$ISO_PATH")"
if [[ -f "$ISO_REAL.sha256" ]]; then
log "Verifying $ISO_REAL.sha256..."
( cd "$(dirname "$ISO_REAL")" && sha256sum -c "$(basename "$ISO_REAL").sha256" ) \
|| die "sha256 verification failed" 5
elif [[ -f "$ISO_PATH.sha256" ]]; then
log "Verifying $ISO_PATH.sha256..."
( cd "$(dirname "$ISO_PATH")" && sha256sum -c "$(basename "$ISO_PATH").sha256" ) \
|| die "sha256 verification failed" 5
@@ -247,14 +230,11 @@ fi
### Final confirmation ###
cat <<EOF
${BOLD}About to:${RESET}
${RED}WIPE${RESET} $DEVICE ($(human "$DEV_BYTES"))
Write ISO $ISO_PATH
Add extras $( ((SKIP_EXTRAS)) && echo "no" || echo "yes (exFAT, in trailing free space)" )
${YELLOW}All data on $DEVICE will be destroyed.${RESET}
EOF
@@ -266,8 +246,6 @@ fi
### Unmount anything on the target ###
log "Unmounting any partitions on $DEVICE..."
while read -r part mnt; do
[[ -n "$mnt" ]] || continue
@@ -275,131 +253,43 @@ while read -r part mnt; do
umount "$part" 2>/dev/null || umount -l "$part" 2>/dev/null || warn "could not umount $part"
done < <(lsblk -lnpo NAME,MOUNTPOINT "$DEVICE" | awk '$2!=""')
### Wipe & write ###
log "Wiping signatures..."
wipefs -a "$DEVICE" >/dev/null
log "Writing ISO with dd (this can take a few minutes)..."
dd if="$ISO_PATH" of="$DEVICE" bs=4M status=progress conv=fsync oflag=direct \
|| die "dd failed" 5
sync
log "ISO written."
partprobe "$DEVICE" 2>/dev/null || true
sleep 1
### Extras partition ###
if (( SKIP_EXTRAS )); then
log "Skipping extras partition."
else
log "Preparing extras partition in trailing free space..."
# The NixOS ISO uses an isohybrid GPT. The backup GPT header still sits at
# the *original ISO end*, not the actual disk end. `sgdisk -e` relocates it
# so we can add a partition in the freed trailing region.
# `sgdisk -n` with 0:0:0 creates a new partition starting at the first free
# sector and ending at the last free sector (i.e., filling the rest).
sgdisk -e "$DEVICE" >/dev/null
sgdisk -n 0:0:0 -t 0:0700 -c 0:"EXTRAS" "$DEVICE" >/dev/null
partprobe "$DEVICE" 2>/dev/null || true
sleep 1
# Find the new partition (highest-numbered one on the disk).
EXTRAS_PART="$(lsblk -lnpo NAME,TYPE "$DEVICE" \
| awk '$2=="part"{print $1}' | tail -n1)"
[[ -b "$EXTRAS_PART" ]] || die "extras partition didn't appear" 5
log "Extras partition: $EXTRAS_PART"
log "Formatting $EXTRAS_PART as exFAT..."
mkfs.exfat -L EXTRAS "$EXTRAS_PART" >/dev/null 2>&1 \
|| die "mkfs.exfat failed" 5
TMP_MNT="$(mktemp -d)"
mount "$EXTRAS_PART" "$TMP_MNT"
echo
log "What to copy onto the extras partition? (space-separated numbers; Enter for none)"
echo
echo " 1) This repo ${REPO_ROOT:-<not detected>}"
echo " 2) Your public SSH keys ~/.ssh/*.pub"
echo " 3) An arbitrary file/dir (you'll be prompted for the path)"
echo " 4) A custom message/README (you type it inline)"
echo
read -r -p "Selection: " sel || sel=""
for tok in $sel; do
case "$tok" in
1)
if [[ -z "$REPO_ROOT" ]]; then
warn "no repo detected; pass --repo-root or run from a git checkout"
continue
fi
log "Copying repo -> /repo (git tracked files only)..."
# Use `git archive` if possible, it respects .gitignore and skips .git.
if git -C "$REPO_ROOT" rev-parse >/dev/null 2>&1; then
mkdir -p "$TMP_MNT/repo"
git -C "$REPO_ROOT" archive --format=tar HEAD \
| tar -x -C "$TMP_MNT/repo"
else
mkdir -p "$TMP_MNT/repo"
cp -aT "$REPO_ROOT" "$TMP_MNT/repo"
fi
;;
2)
# Run as the invoking user, not root, so ~ resolves correctly.
user_home="$(getent passwd "${SUDO_USER:-$USER}" | cut -d: -f6)"
if compgen -G "$user_home/.ssh/*.pub" >/dev/null; then
mkdir -p "$TMP_MNT/ssh-keys"
cp "$user_home"/.ssh/*.pub "$TMP_MNT/ssh-keys/"
log "Copied $(ls "$user_home"/.ssh/*.pub | wc -l) public key(s)."
else
warn "no .pub keys found in $user_home/.ssh"
fi
;;
3)
read -r -e -p "Path to copy: " extra_path
extra_path="${extra_path/#\~/$HOME}"
if [[ -e "$extra_path" ]]; then
cp -a "$extra_path" "$TMP_MNT/"
log "Copied $extra_path"
else
warn "not found: $extra_path"
fi
;;
4)
info "Type your note. End with a line containing only EOF:"
: >"$TMP_MNT/NOTE.txt"
while IFS= read -r line; do
[[ "$line" == "EOF" ]] && break
printf '%s\n' "$line" >>"$TMP_MNT/NOTE.txt"
done
;;
*) warn "unknown option: $tok" ;;
esac
done
sync
umount "$TMP_MNT"
rmdir "$TMP_MNT"
TMP_MNT=""
if ! dd if="$ISO_PATH" of="$DEVICE" bs=4M status=progress conv=fsync oflag=direct; then
err "dd failed."
# Surface the most recent kernel complaints about this device so the
# user can distinguish bad-block / hardware failure ("Medium Error",
# "Hardware Error") from cable/port flakiness ("usb ... reset",
# "Communication failure") without having to dig through dmesg.
dev_short="$(basename "$DEVICE")"
if dmesg_out="$(dmesg 2>/dev/null | grep -E "(${dev_short}|usb [0-9]+-[0-9]+)" | tail -10)" \
&& [[ -n "$dmesg_out" ]]; then
warn "Recent kernel messages mentioning ${dev_short} or USB events:"
printf '%s\n' "$dmesg_out" | sed 's/^/ /' >&2
warn "Look for 'Medium Error' / 'Hardware Error' (bad flash → replace stick),"
warn "or 'reset' / 'Communication failure' (cable/port → try a different one)."
else
warn "Run \`sudo dmesg | tail -30\` to see why the kernel rejected the write."
fi
exit 5
fi
### Done ###
log "Final sync..."
log "Final sync (flushing kernel buffers; may take a moment)..."
sync
blockdev --flushbufs "$DEVICE" 2>/dev/null || true
echo
log "${BOLD}Done.${RESET} You can unplug $DEVICE now."
echo
info "Boot the new machine from this USB. Once at the installer shell:"
info " - If you copied the repo, it's on the EXTRAS partition (mountable as exFAT)."
info " - To install: partition the target disk, mount at /mnt, then"
info " nixos-install --flake /mnt/extras/repo#<hostname>"
info " - Connect to the network (Ethernet, or 'sudo systemctl start wpa_supplicant' + 'wpa_cli', or 'nmtui' on the graphical ISO)."
info " - Clone your repo: git clone <your-repo-url> /tmp/notfiles"
info " - Partition the target disk, mount at /mnt, then:"
info " nixos-install --flake /tmp/notfiles#<hostname>"
echo