The KiCad working copy and the Pi image work dir moved out of ~/kicad and ~/Documents into ~/Documents/projects/sandTable-local. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
7.7 KiB
Bash
Executable File
184 lines
7.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# Builds a ready-to-flash SD card image for a Raspberry Pi Zero 2 W that runs Gyre by itself:
|
|
# Raspberry Pi OS Lite + the Gyre software as a service on http://<hostname>.local
|
|
#
|
|
# pi/build-image.sh [work dir] (default work dir: ~/Documents/projects/sandTable-local/gyre-pi)
|
|
#
|
|
# The work dir holds settings.env (your Wi-Fi; see settings.example.env), the downloads, the
|
|
# build scratch area and out/gyre-pi.img.xz. Nothing secret is written inside the repository.
|
|
# No root needed: the boot partition is edited with mtools, the root filesystem with debugfs.
|
|
set -euo pipefail
|
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
|
PI="$REPO/pi"
|
|
W=$(realpath -m "${1:-$HOME/Documents/projects/sandTable-local/gyre-pi}")
|
|
mkdir -p "$W"/{download,debs,tools,out}
|
|
[ -f "$W/settings.env" ] || { cp "$PI/settings.example.env" "$W/settings.env"; echo "Fill in $W/settings.env, then run again."; exit 1; }
|
|
. "$W/settings.env"
|
|
[ -n "${SSID:-}" ] && [ "$SSID" != "your network" ] || { echo "Set SSID in $W/settings.env"; exit 1; }
|
|
SSH_PUBKEY=${SSH_PUBKEY/#\~/$HOME}
|
|
|
|
BASE_URL=https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2026-10-06/2026-10-06-raspios-trixie-arm64-lite.img.xz
|
|
BASE="$W/download/$(basename "$BASE_URL")"
|
|
B="$W/build"; OUT="$W/out/gyre-pi.img"
|
|
rm -rf "$B"; mkdir -p "$B/gen" "$B/sw"
|
|
|
|
echo "== downloads"
|
|
if [ ! -f "$BASE" ]; then
|
|
curl -fSL -o "$BASE" "$BASE_URL"
|
|
curl -fsSL -o "$BASE.sha256" "$BASE_URL.sha256"
|
|
fi
|
|
(cd "$W/download" && sha256sum -c "$(basename "$BASE").sha256" >/dev/null) || { echo "base image checksum failed"; exit 1; }
|
|
grep -v '^#' "$PI/debs.txt" | while read -r url sum; do
|
|
f="$W/debs/$(basename "$url")"
|
|
[ -f "$f" ] || curl -fsSL -o "$f" "$url"
|
|
echo "$sum $f" | sha256sum -c --quiet || { echo "checksum failed: $f"; exit 1; }
|
|
done
|
|
if command -v mcopy >/dev/null; then MT=$(dirname "$(command -v mcopy)")
|
|
else
|
|
MT="$W/tools/root/usr/bin"
|
|
[ -x "$MT/mcopy" ] || (cd "$W/tools" && apt-get download mtools >/dev/null && dpkg -x mtools_*.deb root)
|
|
fi
|
|
|
|
echo "== keys and passwords"
|
|
if [ ! -f "$SSH_PUBKEY" ]; then ssh-keygen -q -t ed25519 -N '' -C gyre-pi -f "${SSH_PUBKEY%.pub}"; fi
|
|
umask 077
|
|
if [ -z "${USER_PASSWORD:-}" ]; then
|
|
if [ -f "$W/CREDENTIALS.txt" ]; then USER_PASSWORD=$(sed -n 's/^user gyre password: //p' "$W/CREDENTIALS.txt"); fi
|
|
[ -n "$USER_PASSWORD" ] || USER_PASSWORD=$(openssl rand -base64 12 | tr -d '/+=' | cut -c1-14)
|
|
fi
|
|
cat > "$W/CREDENTIALS.txt" <<EOF
|
|
Gyre Raspberry Pi ($HOSTNAME)
|
|
page: http://$HOSTNAME.local
|
|
user gyre password: $USER_PASSWORD
|
|
ssh: ssh -i ${SSH_PUBKEY%.pub} gyre@$HOSTNAME.local (key only; the password is for the console and sudo)
|
|
EOF
|
|
HASH=$(openssl passwd -6 "$USER_PASSWORD")
|
|
umask 022
|
|
|
|
echo "== unpack base image"
|
|
xz -dc "$BASE" > "$OUT.tmp"
|
|
part() { sfdisk -d "$OUT.tmp" | sed -n "s/.*img.tmp$1 : start= *\([0-9]*\), size= *\([0-9]*\),.*/\1 \2/p"; }
|
|
read -r BSTART BSIZE < <(part 1)
|
|
read -r RSTART RSIZE < <(part 2)
|
|
dd if="$OUT.tmp" of="$B/boot.vfat" bs=512 skip="$BSTART" count="$BSIZE" status=none
|
|
dd if="$OUT.tmp" of="$B/root.ext4" bs=512 skip="$RSTART" count="$RSIZE" status=none
|
|
dbg() { debugfs -R "$1" "$B/root.ext4" 2>/dev/null; }
|
|
|
|
echo "== first-boot settings (cloud-init, on the boot partition)"
|
|
python3 - "$B/gen" "$HOSTNAME" "$TIMEZONE" "$HASH" "$SSH_PUBKEY" "$SSID" "$WIFI_PASSWORD" "$COUNTRY" <<'PY'
|
|
import json, sys, time
|
|
out, host, tz, hsh, key, ssid, pw, cc = sys.argv[1:9]
|
|
q = json.dumps # a JSON string is a valid YAML double-quoted string
|
|
pub = open(key).read().strip()
|
|
open(f'{out}/user-data', 'w').write(f"""#cloud-config
|
|
# Gyre sand art table: set up on the first boot
|
|
hostname: {q(host)}
|
|
manage_etc_hosts: true
|
|
timezone: {q(tz)}
|
|
users:
|
|
- name: gyre
|
|
gecos: Gyre sand art table
|
|
groups: [users, dialout, sudo, plugdev, netdev, video]
|
|
shell: /bin/bash
|
|
lock_passwd: false
|
|
passwd: {q(hsh)}
|
|
ssh_authorized_keys:
|
|
- {q(pub)}
|
|
ssh_pwauth: false
|
|
runcmd:
|
|
- [sh, -c, "dpkg -i /var/lib/gyre/debs/*.deb || (apt-get update && apt-get -y -f install)"]
|
|
- [chown, -R, "gyre:gyre", /opt/gyre]
|
|
- [systemctl, enable, --now, ssh]
|
|
- [systemctl, enable, --now, gyre]
|
|
""")
|
|
open(f'{out}/network-config', 'w').write(f"""network:
|
|
version: 2
|
|
wifis:
|
|
wlan0:
|
|
dhcp4: true
|
|
optional: true
|
|
regulatory-domain: {q(cc)}
|
|
access-points:
|
|
{q(ssid)}:
|
|
password: {q(pw)}
|
|
""")
|
|
open(f'{out}/meta-data', 'w').write(f"dsmode: local\ninstance_id: gyre-{int(time.time())}\n")
|
|
PY
|
|
chmod 600 "$B/gen/network-config"
|
|
"$MT/mtype" -i "$B/boot.vfat" ::/config.txt > "$B/gen/config.txt"
|
|
grep -q 'Gyre:' "$B/gen/config.txt" || cat >> "$B/gen/config.txt" <<'EOF'
|
|
|
|
[pi02]
|
|
# Gyre: the micro-USB data port is a USB host for the Arduino, whatever cable is used
|
|
dtoverlay=dwc2,dr_mode=host
|
|
[all]
|
|
EOF
|
|
for f in user-data network-config meta-data config.txt; do
|
|
"$MT/mcopy" -o -i "$B/boot.vfat" "$B/gen/$f" "::/$f"
|
|
done
|
|
|
|
echo "== Gyre software and packages (root filesystem)"
|
|
SW="$REPO/software"
|
|
# pre-made thumbnails, so the Pi doesn't spend its first minutes drawing them
|
|
(cd "$SW" && python3 -c "from sandtable import config; from sandtable.web import Library; Library(config.load()).warm()")
|
|
(cd "$SW" && find sandtable patterns firmware -type f ! -path '*/__pycache__/*' ! -name '*.pyc'
|
|
echo sandtable.toml; echo requirements.txt; echo README.md
|
|
find .cache/thumbs -type f) | sort > "$B/sw.list"
|
|
# every folder the files need, parents first
|
|
mapfile -t DIRS < <(python3 - "$B/sw.list" <<'PY'
|
|
import sys
|
|
dirs = {'/opt/gyre', '/opt/gyre/software', '/var/lib/gyre', '/var/lib/gyre/debs'}
|
|
for line in open(sys.argv[1]):
|
|
parts = line.strip().split('/')[:-1]
|
|
for i in range(1, len(parts) + 1):
|
|
dirs.add('/opt/gyre/software/' + '/'.join(parts[:i]))
|
|
for d in sorted(dirs, key=lambda d: (d.count('/'), d)):
|
|
print(d)
|
|
PY
|
|
)
|
|
FILES=()
|
|
while read -r f; do FILES+=("/opt/gyre/software/$f 644 $SW/$f"); done < "$B/sw.list"
|
|
for d in "$W"/debs/*.deb; do FILES+=("/var/lib/gyre/debs/$(basename "$d") 644 $d"); done
|
|
FILES+=("/etc/systemd/system/gyre.service 644 $PI/overlay/etc/systemd/system/gyre.service")
|
|
FILES+=("/etc/sudoers.d/gyre-power 440 $PI/overlay/etc/sudoers.d/gyre-power")
|
|
|
|
CMD="$B/debugfs.cmd"; : > "$CMD"
|
|
for d in "${DIRS[@]}"; do
|
|
dbg "stat $d" | grep -q 'Type: directory' || echo "mkdir $d" >> "$CMD"
|
|
printf 'sif %s uid 0\nsif %s gid 0\nsif %s mode 040755\n' "$d" "$d" "$d" >> "$CMD"
|
|
done
|
|
for e in "${FILES[@]}"; do
|
|
read -r path mode src <<<"$e"
|
|
printf 'rm %s\nwrite %s %s\nsif %s uid 0\nsif %s gid 0\nsif %s mode 0100%s\n' \
|
|
"$path" "$src" "$path" "$path" "$path" "$path" "$mode" >> "$CMD"
|
|
done
|
|
debugfs -w -f "$CMD" "$B/root.ext4" > "$B/debugfs.log" 2>&1
|
|
if grep -vE '^debugfs |^rm: File not found by ext2_lookup' "$B/debugfs.log" | grep -iE 'error|fail|not found|cannot|could not|bad'; then
|
|
echo "debugfs reported problems (see $B/debugfs.log)"; exit 1
|
|
fi
|
|
|
|
echo "== check"
|
|
e2fsck -fn "$B/root.ext4" > "$B/fsck.log" 2>&1 || { cat "$B/fsck.log"; exit 1; }
|
|
tail -1 "$B/fsck.log"
|
|
fail=0
|
|
for e in "${FILES[@]}"; do
|
|
read -r path mode src <<<"$e"
|
|
cmp -s <(dbg "cat $path") "$src" || { echo "MISMATCH $path"; fail=1; }
|
|
done
|
|
for f in user-data network-config meta-data config.txt; do
|
|
cmp -s <("$MT/mtype" -i "$B/boot.vfat" "::/$f") "$B/gen/$f" || { echo "MISMATCH boot/$f"; fail=1; }
|
|
done
|
|
[ $fail = 0 ] || exit 1
|
|
echo "all ${#FILES[@]} files and 4 boot files verified"
|
|
|
|
echo "== assemble"
|
|
dd if="$B/boot.vfat" of="$OUT.tmp" bs=512 seek="$BSTART" conv=notrunc status=none
|
|
dd if="$B/root.ext4" of="$OUT.tmp" bs=512 seek="$RSTART" conv=notrunc status=none
|
|
mv "$OUT.tmp" "$OUT"
|
|
rm -f "$OUT.xz"
|
|
xz -T0 -6 -k "$OUT"
|
|
(cd "$W/out" && sha256sum gyre-pi.img gyre-pi.img.xz > SHA256SUMS)
|
|
rm -rf "$B"
|
|
ls -l "$W/out"
|
|
echo "Done. Write $OUT.xz with Raspberry Pi Imager: Choose OS > Use custom, and answer NO to OS customisation."
|