feat(build): make write_stick DEVICE=/dev/sdX -- the PC stick onto a USB stick, checked and read back

scripts/write_stick.sh refuses a device that is not a whole, removable USB disk, that holds the
running system, that is smaller than the image, or that has a mounted partition (it prints the
udisksctl lines); shows the stick and asks for its name typed back; writes with conv=fsync,
flushes, reads the stick back and compares it with the image byte for byte; powers it off. On
Windows/macOS it points to balenaEtcher or Rufus. Captain Bob, 2026-10-28, after a terminal line
break dropped dd's bs=4M conv=fsync on the first SER5 stick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-29 06:22:26 -04:00
co-authored by Claude Opus 5.5
parent 954b4d5f70
commit 0a31e558be
2 changed files with 77 additions and 1 deletions
+11 -1
View File
@@ -20,6 +20,9 @@
# (build/boards/<board>/*.spdx*) and a fresh
# source SBOM -- needs syft
# make boot_images every board that has a kernel port
# make write_stick DEVICE=/dev/sdX write the PC stick image to a USB stick, checked
# first, read back and compared after
# (scripts/write_stick.sh; Linux, sudo)
# make docs [TARGET=<board>] the book; a board adds its appendix
#
# make clean all of build/ except build/cache/
@@ -59,7 +62,7 @@ BOARD_DIR := $(if $(TARGET),$(BOARD_DIR_$(TARGET)))
SUBMAKE := $(MAKE) --no-print-directory
.DEFAULT_GOAL := all
.PHONY: all boot_image boot_images docs clean clean-docs kernel boards help help-hosted sbom sbom-board \
.PHONY: all boot_image boot_images write_stick docs clean clean-docs kernel boards help help-hosted sbom sbom-board \
qemu qemu-esp qemu-gdb
# ARCH given on the command line and no TARGET: `clean` is the kernel's for that
@@ -222,6 +225,13 @@ boards:
@$(foreach b,$(BOARDS),printf ' %-9s %-8s %s\n' "$(b)" \
"$(if $(call board_ready,$(b)),ready,no port)" "$(call board_field,$(b),BOARD_DESC)";)
# write_stick -- the PC stick image (make boot_image TARGET=PC) onto a USB stick, safely:
# scripts/write_stick.sh refuses anything but a whole, removable, unmounted USB disk big enough,
# asks for the device to be typed back, writes, flushes, reads back and compares.
write_stick:
@[ -n "$(DEVICE)" ] || { echo "Usage: make write_stick DEVICE=/dev/sdX (lsblk lists the disks)"; exit 1; }
@bash scripts/write_stick.sh build/boards/pc/lithos-pc.img $(DEVICE)
help:
@sed -n '/^# TARGET is/,/^# =====/p' Makefile | sed '$$d; s/^# \{0,1\}//'
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# write_stick.sh -- write a StarshipOS stick image to a USB stick, safely, and prove it.
#
# scripts/write_stick.sh IMAGE DEVICE e.g. build/boards/pc/lithos-pc.img /dev/sdb
# make write_stick DEVICE=/dev/sdX (the PC stick; root Makefile)
#
# Linux only (lsblk, udisksctl, sudo). Refuses a device that is not a whole, removable USB disk,
# that holds the running system, that is smaller than the image, or that has a mounted partition.
# Shows the device and asks for its name to be typed back before writing. Writes with
# conv=fsync, flushes, reads the stick back and compares it with the image byte for byte, then
# powers the stick off so it can be pulled. Everything on the stick is erased.
#
# On Windows or macOS: balenaEtcher (https://etcher.balena.io) writes and verifies an image;
# on Windows, Rufus in "DD Image" mode does too.
set -euo pipefail
PATH="$PATH:/usr/sbin:/sbin"
die() { echo "write_stick: $*" >&2; exit 1; }
[ $# -eq 2 ] || die "usage: scripts/write_stick.sh IMAGE DEVICE (e.g. /dev/sdb; see lsblk)"
IMG="$1" DEV="$2"
[ -f "$IMG" ] || die "no image at $IMG -- build it first: make boot_image TARGET=PC"
[ -b "$DEV" ] || die "$DEV is not a block device (lsblk lists the disks)"
name=$(basename "$DEV")
[ "$(lsblk -dno TYPE "$DEV")" = disk ] || die "$DEV is a partition, not a whole disk: give the disk (e.g. /dev/sdb, not /dev/sdb1)"
[ "$(lsblk -dno RM "$DEV" | tr -d ' ')" = 1 ] || die "$DEV is not a removable disk -- refusing"
[ "$(lsblk -dno TRAN "$DEV" | tr -d ' ')" = usb ] || die "$DEV is not a USB disk -- refusing"
root_src=$(findmnt -no SOURCE / || true)
if [ -n "$root_src" ] && lsblk -no PKNAME "$root_src" 2>/dev/null | grep -qx "$name"; then
die "$DEV holds the running system -- refusing"
fi
img_bytes=$(stat -c %s "$IMG")
dev_bytes=$(lsblk -bdno SIZE "$DEV")
[ "$dev_bytes" -ge "$img_bytes" ] || die "$DEV ($dev_bytes bytes) is smaller than the image ($img_bytes bytes)"
mounted=$(lsblk -nro NAME,MOUNTPOINTS "$DEV" | awk 'NF > 1 {print "/dev/" $1}')
if [ -n "$mounted" ]; then
echo "write_stick: $DEV has mounted partitions. Unmount them first:" >&2
for p in $mounted; do echo " udisksctl unmount -b $p" >&2; done
exit 1
fi
echo "About to ERASE this whole stick and write the StarshipOS image to it:"
lsblk -o NAME,SIZE,TRAN,MODEL,VENDOR,FSTYPE,LABEL "$DEV"
echo
echo " image: $IMG ($img_bytes bytes)"
read -r -p "Type the device name ($DEV) to go on, anything else to stop: " answer
[ "$answer" = "$DEV" ] || die "stopped; nothing was written"
echo "== writing (sudo asks for your password)"
sudo dd if="$IMG" of="$DEV" bs=4M conv=fsync status=progress
echo "== flushing to the stick"
sudo sync
sudo blockdev --flushbufs "$DEV"
echo "== reading the stick back and comparing it with the image"
if sudo cmp -n "$img_bytes" "$IMG" "$DEV"; then
echo "== the stick matches the image"
else
die "the stick does NOT match the image -- do not boot it; try again, or another stick"
fi
if command -v udisksctl >/dev/null 2>&1 && udisksctl power-off -b "$DEV" >/dev/null 2>&1; then
echo "== done: the stick is powered off and can be pulled"
else
echo "== done: the stick can be pulled"
fi
echo "Boot it: Secure Boot off in the firmware setup, then choose the stick in the boot menu."