Shell 153 lines
#!/usr/bin/env bash
# Create the 64 MiB SD-card backing image for QEMU raspi4b runs
# (-drive if=sd,file=zig-out/test_sd.img,format=raw). Used by build.zig
# as a prerequisite of the rpi4b `run` and `test-rpi4b` steps.
#
# Args (both optional, passed by build.zig):
# $1 — generated shadow file (gen_shadow output) → seeded as ::/SHADOW
# $2 — permission-overlay seed (user_space/etc/perms.tab) → ::/PERMS.TAB
# Without them the identity seeds are skipped and the kernel runs the
# initramfs-fallback path (auth works, [TEST] passwd SKIPs).
#
# The image is a real FAT32 volume, not a zero fill. Layout matches
# scripts/format_sd.sh (MBR, one FAT32 partition at LBA 2048 = the
# 1 MiB alignment offset), so src/fat32_backend.zig
# (FAT32_PARTITION_LBA = 2048) mounts it. Two seed files are
# pre-created in the FAT32 root for [TEST] fs-roundtrip (Variant B,
# magic-file): ROUNDTR.DAT (4 KiB zero) + ROUNDTR.MAG (1 byte zero).
# 8.3 names — fat32.encode8_3 rejects basenames > 8. The identity
# seeds SHADOW (the writable password database [TEST] passwd rewrites
# and the boot login reads first) and PERMS.TAB (the permission overlay
# protecting it, 0600 root:root) are also included.
#
# Toolchain: mtools (mformat / mmd / mcopy), NOT mkfs.fat. dosfstools
# is absent on the dev box; mtools needs no sudo/loopback and formats
# straight into a file at a byte offset (img@@1M = LBA 2048). The
# canonical mkfs.fat -F 32 -s 1 -S 512 maps byte-equivalent onto
# mformat -F -c 1 under the available tool. One sector per cluster
# (not 8): at 64 MiB only -c 1 yields ≥65525 data clusters, the FAT32
# spec minimum that newer mtools enforces (older builds let an
# undersized "FAT32" slide). The kernel reads SecPerClus from the BPB
# (src/fat32.flash mount), so cluster size is not wired in anywhere.
#
# CREATE-IF-ABSENT (NOT idempotent-overwrite). The Variant-B roundtrip
# needs the disk to PERSIST across two consecutive `zig build run`
# invocations (run 1 writes magic=1, run 2 verifies + resets). Because
# make_test_disk.sh is a build dependency of every run/test-rpi4b, an
# unconditional re-format would reset magic=0 every run and PASS_VERIFY
# could never be reached. So: if a valid FAT32 image with every seed
# file already exists, leave it untouched ([TEST] passwd is self-healing
# against the password drift this allows — it root-resets the flash
# record first). `zig build clean` removes zig-out and starts a fresh
# cycle (magic=0 → PASS_WRITE). An image missing SHADOW/PERMS.TAB
# fails the probe and is regenerated automatically.
#
# Reproducibility: a fresh build is byte-deterministic
# — pinned volume serial (-N 12345678), pinned label (-v SCRATCH),
# SOURCE_DATE_EPOCH=0 and fixed seed-file mtimes so mtools writes
# stable directory timestamps (the gen_shadow output is itself a pure
# function of its in-tool constants). Verify with:
# rm -f zig-out/test_sd.img && scripts/make_test_disk.sh && \
# shasum zig-out/test_sd.img && rm -f zig-out/test_sd.img && \
# scripts/make_test_disk.sh && shasum zig-out/test_sd.img
set -eu
# $0 (not ${BASH_SOURCE[0]}): build.zig invokes this via `sh …`, so the
# shebang is bypassed and on Ubuntu CI the interpreter is dash, which has
# no BASH_SOURCE array — ${BASH_SOURCE[0]} is a "Bad substitution" there.
# $0 is POSIX and resolves the same under both dash and bash.
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
SHADOW_SRC="${1:-}"
PERMS_SRC="${2:-}"
IMG=zig-out/test_sd.img
TOTAL_SECTORS=131072 # 64 MiB / 512
PART_LBA=2048 # 1 MiB MBR offset (matches format_sd.sh)
PART_SECTORS=$((TOTAL_SECTORS - PART_LBA)) # 129024 = 0x1F800
export MTOOLS_SKIP_CHECK=1
export SOURCE_DATE_EPOCH=0
mkdir -p zig-out
# ---- create-if-absent guard ----
# Preserve an existing populated image so the two-run roundtrip can
# persist its magic byte. Probe with minfo (valid FAT32 at LBA 2048)
# + mdir for every seed file; any failure → regenerate from scratch.
# The SHADOW / PERMS.TAB probes only gate when identity seeds were
# requested (args present), so a bare script run keeps accepting
# roundtrip-only images.
probe_ok=1
if [ -f "$IMG" ] \
&& minfo -i "$IMG@@1M" :: >/dev/null 2>&1 \
&& mdir -i "$IMG@@1M" ::/ROUNDTR.DAT >/dev/null 2>&1 \
&& mdir -i "$IMG@@1M" ::/ROUNDTR.MAG >/dev/null 2>&1 \
&& mdir -i "$IMG@@1M" ::/EMPTY.TXT >/dev/null 2>&1; then
if [ -n "$SHADOW_SRC" ]; then
mdir -i "$IMG@@1M" ::/SHADOW >/dev/null 2>&1 || probe_ok=0
mdir -i "$IMG@@1M" ::/PERMS.TAB >/dev/null 2>&1 || probe_ok=0
fi
else
probe_ok=0
fi
if [ "$probe_ok" = 1 ]; then
echo "make_test_disk: keeping existing FAT32 image (roundtrip persistence)"
exit 0
fi
echo "make_test_disk: creating fresh 64 MiB FAT32 image"
rm -f "$IMG"
dd if=/dev/zero of="$IMG" bs=1M count=64 status=none
# ---- MBR: one FAT32-LBA partition (type 0x0C) at LBA 2048 ----
# Partition entry @ 0x1BE (446): status=00 CHS=FEFFFF type=0C CHS=FEFFFF
# LBA-start=2048 (00 08 00 00) sectors=129024 (00 F8 01 00)
# Boot signature 55 AA @ 0x1FE (510). Rest of sector 0 stays zero.
printf '\x00\xFE\xFF\xFF\x0C\xFE\xFF\xFF\x00\x08\x00\x00\x00\xF8\x01\x00' \
| dd of="$IMG" bs=1 seek=446 conv=notrunc status=none
printf '\x55\xAA' | dd of="$IMG" bs=1 seek=510 conv=notrunc status=none
# ---- FAT32 filesystem at byte offset 1 MiB (LBA 2048) ----
# -F force FAT32
# -c 1 1 sector/cluster = 512 B; only this keeps ≥65525 clusters
# at 64 MiB so newer mtools accepts it as valid FAT32
# -T n total sectors of the filesystem (= partition size)
# -N hex pinned volume serial (reproducible)
# -v lbl pinned volume label
mformat -i "$IMG@@1M" -F -c 1 -T "$PART_SECTORS" -N 12345678 -v SCRATCH ::
# ---- seed files (deterministic content + mtime) ----
TMP_DAT="$(mktemp -t roundtr_dat.XXXXXX)"
TMP_MAG="$(mktemp -t roundtr_mag.XXXXXX)"
TMP_EMP="$(mktemp -t empty_seed.XXXXXX)"
TMP_SHD="$(mktemp -t shadow_seed.XXXXXX)"
TMP_PRM="$(mktemp -t perms_seed.XXXXXX)"
trap 'rm -f "$TMP_DAT" "$TMP_MAG" "$TMP_EMP" "$TMP_SHD" "$TMP_PRM"' EXIT
dd if=/dev/zero of="$TMP_DAT" bs=4096 count=1 status=none # 4 KiB zero
dd if=/dev/zero of="$TMP_MAG" bs=1 count=1 status=none # 1 byte zero
: > "$TMP_EMP" # 0 bytes -> first_cluster 0
touch -t 197001010000.00 "$TMP_DAT" "$TMP_MAG" "$TMP_EMP"
mcopy -i "$IMG@@1M" "$TMP_DAT" ::/ROUNDTR.DAT
mcopy -i "$IMG@@1M" "$TMP_MAG" ::/ROUNDTR.MAG
# 0-byte seed for [TEST] fs-empty-write: the first write must allocate
# its first data cluster (fat32_backend.write step 0). Pi-only; under
# QEMU /mnt never mounts so the scenario SKIPs and this stays 0 bytes.
mcopy -i "$IMG@@1M" "$TMP_EMP" ::/EMPTY.TXT
# ---- identity seeds: SHADOW + PERMS.TAB ----
# The shadow file is the gen_shadow build artifact (same content as the
# initramfs /etc/shadow seed); the overlay is the repo seed file. Both
# get the pinned mtime so the image stays byte-deterministic.
if [ -n "$SHADOW_SRC" ] && [ -n "$PERMS_SRC" ]; then
cp "$SHADOW_SRC" "$TMP_SHD"
cp "$PERMS_SRC" "$TMP_PRM"
touch -t 197001010000.00 "$TMP_SHD" "$TMP_PRM"
mcopy -i "$IMG@@1M" "$TMP_SHD" ::/SHADOW
mcopy -i "$IMG@@1M" "$TMP_PRM" ::/PERMS.TAB
echo "make_test_disk: FAT32 image ready (ROUNDTR + SHADOW + PERMS.TAB, magic=0)"
else
echo "make_test_disk: FAT32 image ready (ROUNDTR only — no identity seeds, magic=0)"
fi