Skip to main content

max / alloy

4.5 KB · 103 lines History Blame Raw
1 #!/usr/bin/env bash
2 #
3 # write-device.sh — dd an artifact to a block device, with the guards.
4 #
5 # The single implementation of the dd path. build/build-image.sh and
6 # build/build-iso.sh both call this; neither one writes a device itself.
7 # The rule is in wiki alloy-distribution ("Do not rewrite the dd path"):
8 # a second copy of these checks is how a disk-eating bug gets introduced,
9 # because the copy is the one that does not get the fix.
10 #
11 # Usage:
12 # build/write-device.sh <artifact> <device> [label]
13 #
14 # label is what the device is called in the closing line ("raw", "installer
15 # ISO"); it is cosmetic and defaults to "image".
16 #
17 # Refuses partitions and anything with a mounted filesystem, warns on a
18 # non-removable target, demands the device path typed back, and verifies the
19 # result with cmp before claiming success.
20
21 set -euo pipefail
22
23 die() { printf 'error: %s\n' "$*" >&2; exit 1; }
24
25 # priv / privc. run0 where it exists, sudo where it does not; see the header
26 # of build/privilege.sh for which of the two a call site wants.
27 # shellcheck source=build/privilege.sh
28 . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/privilege.sh"
29
30 ARTIFACT="${1:?usage: write-device.sh <artifact> <device> [label]}"
31 DEVICE="${2:?usage: write-device.sh <artifact> <device> [label]}"
32 LABEL="${3:-image}"
33
34 privc test -f "$ARTIFACT" || die "$ARTIFACT is not a file"
35 [ -b "$DEVICE" ] || die "$DEVICE is not a block device"
36
37 # A whole disk, not a partition. An ISO written to /dev/sda1 produces
38 # nothing bootable and quietly eats a filesystem on the way.
39 [ "$(lsblk -dnro TYPE "$DEVICE")" = "disk" ] \
40 || die "$DEVICE is not a whole disk; pass the disk, not a partition"
41
42 # Any mountpoint at or below the device, not just / and /boot. The old
43 # guard matched those two patterns only, so a disk holding /home, /var or
44 # an active swap passed it and got written. This is the rule the installer
45 # itself applies (install.rs, Disk::blocker): anything mounted blocks.
46 # `lsblk -r` escapes a newline as \x0a, so a device with two mountpoints
47 # arrives as one run-together line. Unescape before printing: this list is
48 # read by someone deciding whether to erase a disk.
49 mounts="$(lsblk -nro MOUNTPOINTS "$DEVICE" 2>/dev/null \
50 | sed 's/\\x0a/\n/g' | grep -v '^$' || true)"
51 if [ -n "$mounts" ]; then
52 printf 'error: %s has mounted filesystems; refusing to write:\n' "$DEVICE" >&2
53 printf '%s\n' "$mounts" | sed 's/^/ /' >&2
54 exit 1
55 fi
56
57 echo
58 lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL,SERIAL,TRAN,RM,RO "$DEVICE"
59 echo
60 # Removable is worth saying out loud: on this box the system disk is nvme
61 # and a USB stick reports usb/RM=1, so a non-removable target is the shape
62 # of a mistake even when nothing is mounted on it.
63 if [ "$(lsblk -dnro RM "$DEVICE")" != "1" ]; then
64 echo "WARNING: $DEVICE is not removable. This is the shape of an internal disk."
65 echo
66 fi
67 printf 'This ERASES all data on %s. Type the device path to confirm: ' "$DEVICE"
68 read -r reply
69 [ "$reply" = "$DEVICE" ] || die "confirmation did not match; not writing"
70
71 echo "==> Writing $ARTIFACT to $DEVICE"
72 # `status=progress` redraws one line with carriage returns, which is what a
73 # person watching a five-gigabyte write wants and exactly wrong for anything
74 # that keeps what it is given. A log, a pipe or an agent's transcript has no
75 # cursor to move, so every update is retained and the write arrives as a single
76 # line thousands of characters long with the useful part at the far end.
77 #
78 # So the progress goes to a terminal and nowhere else. Without one, dd's default
79 # still prints the transfer summary once, at the end, which is the whole of what
80 # a log wants from it. Checked on stderr because that is where dd writes both.
81 if [ -t 2 ]; then
82 priv dd if="$ARTIFACT" of="$DEVICE" bs=4M oflag=direct conv=fsync status=progress
83 else
84 privc dd if="$ARTIFACT" of="$DEVICE" bs=4M oflag=direct conv=fsync
85 fi
86 sync
87
88 # Verify, because dd reporting success is not evidence the bytes landed.
89 # cmp over exactly the artifact's length is the authoritative check; a
90 # `dd | head -c N | sha256sum` pipeline reported phantom corruption on a
91 # write that cmp proved perfect (wiki alloy-build-notes).
92 echo "==> Verifying the write"
93 size="$(privc stat -c %s "$ARTIFACT")"
94 # A check that cannot fail is not a check: prove cmp can still disagree
95 # before trusting it to agree.
96 if privc cmp -s -n 4096 "$DEVICE" /dev/zero; then
97 die "negative control passed, which means cmp is not comparing anything"
98 fi
99 privc cmp -n "$size" "$DEVICE" "$ARTIFACT" \
100 || die "$DEVICE does not match $ARTIFACT; the write is bad"
101 echo "==> Verified $size bytes."
102 echo "==> Done. $DEVICE is now a bootable Alloy $LABEL."
103