lib-runscm.sh (9873B)
1 # lib-runscm.sh — driver-agnostic harness for run.scm-driven stages. 2 # 3 # Boot3/4/5 each drive a per-stage pipeline by invoking scheme1 against a 4 # host-generated run.scm. Two transports: 5 # DRIVER=podman → bind-mount in/ ro and out/ rw under /work in a 6 # scratch+busybox container, exec in/scheme1 7 # in/combined.scm with cwd=/work. 8 # DRIVER=seed → pack the staging dir into a cpio on a virtio-blk read- 9 # only disk (init at cpio root + in/ subtree), boot the 10 # seed kernel with init=init and combined.scm, recover 11 # outputs via the SEEDFS dump on a second virtio-blk 12 # disk. The host extractor filters to out/-prefixed 13 # entries, strips the prefix, writes to $STAGE/out/. 14 # 15 # Both drivers see the same flat namespace; run.scm uses explicit 16 # in/<name> for reads and out/<name> for writes. 17 # 18 # DSL (source as `. boot/lib-runscm.sh`): 19 # 20 # runscm_init <staging-dir> <out-dir> 21 # runscm_scheme1 <path> # init=scheme1 (boot2) 22 # runscm_prelude <path> # scheme1/prelude.scm 23 # runscm_runscm <path> # driver script (run.scm) 24 # runscm_input <name> <host-path> # repeatable; staged at in/<name> 25 # runscm_input_tree <prefix> <src-root> # repeatable; tree under in/<prefix> 26 # runscm_export <name>... # one or more output names 27 # runscm_run [timeout-s] # default 600s 28 # 29 # Required env per driver: 30 # podman: IMAGE, PLATFORM 31 # seed: KERNEL_IMAGE, EXTRACT, optional QEMU_MEM (default 2048M) 32 # both: DRIVER=podman|seed 33 34 S_STAGE_DIR= 35 S_OUT_DIR= 36 S_SCHEME1= 37 S_PRELUDE= 38 S_RUNSCM= 39 S_EXPORTS= 40 41 runscm_init() { 42 S_STAGE_DIR=$1; S_OUT_DIR=$2 43 rm -rf "$S_STAGE_DIR" 44 mkdir -p "$S_STAGE_DIR/in" "$S_STAGE_DIR/out" "$S_OUT_DIR" 45 S_SCHEME1=; S_PRELUDE=; S_RUNSCM= 46 S_EXPORTS= 47 } 48 49 runscm_scheme1() { S_SCHEME1=$1; } 50 runscm_prelude() { S_PRELUDE=$1; } 51 runscm_runscm() { S_RUNSCM=$1; } 52 53 runscm_input() { 54 name=$1; src=$2 55 case "$name" in 56 */*) mkdir -p "$S_STAGE_DIR/in/$(dirname "$name")" ;; 57 esac 58 cp "$src" "$S_STAGE_DIR/in/$name" 59 } 60 61 # Stage every regular file under <src-root> into in/<prefix>/..., 62 # preserving the relative directory tree. 63 runscm_input_tree() { 64 prefix=$1; src_root=$2 65 [ -d "$src_root" ] || { echo "runscm: input_tree: $src_root not a dir" >&2; exit 2; } 66 ( cd "$src_root" && find . -type f ) | sed 's|^\./||' | sort | while read -r rel; do 67 [ -n "$rel" ] || continue 68 mkdir -p "$S_STAGE_DIR/in/$prefix/$(dirname "$rel")" 69 cp "$src_root/$rel" "$S_STAGE_DIR/in/$prefix/$rel" 70 done 71 } 72 73 # runscm_input_from_src <subpath> [<name>] 74 # Pull a file from the canonical generated source tree at 75 # build/$ARCH/src/src/<subpath>. Stages it under in/<name>; 76 # <name> defaults to basename(subpath). For the rare `bin/` case, 77 # call runscm_input directly with build/$ARCH/src/bin/<file>. 78 runscm_input_from_src() { 79 _subpath=$1; _name=${2:-} 80 [ -n "$_name" ] || _name=$(basename "$_subpath") 81 runscm_input "$_name" "build/$ARCH/src/src/$_subpath" 82 } 83 84 # runscm_input_tree_from_src <prefix> <subpath> 85 # Same as runscm_input_tree, but the source root is 86 # build/$ARCH/src/src/<subpath>. 87 runscm_input_tree_from_src() { 88 _prefix=$1; _subpath=$2 89 runscm_input_tree "$_prefix" "build/$ARCH/src/src/$_subpath" 90 } 91 92 runscm_export() { 93 for _n in "$@"; do S_EXPORTS="$S_EXPORTS $_n"; done 94 } 95 96 runscm_run() { 97 timeout=${1:-600} 98 [ -n "$S_SCHEME1" ] || { echo "runscm: scheme1 not set" >&2; exit 2; } 99 [ -n "$S_PRELUDE" ] || { echo "runscm: prelude not set" >&2; exit 2; } 100 [ -n "$S_RUNSCM" ] || { echo "runscm: run.scm not set" >&2; exit 2; } 101 cp "$S_SCHEME1" "$S_STAGE_DIR/in/scheme1" 102 chmod +x "$S_STAGE_DIR/in/scheme1" 103 cat "$S_PRELUDE" "$S_RUNSCM" > "$S_STAGE_DIR/in/combined.scm" 104 # Top-level reference copy of run.scm for human inspection. 105 cp "$S_RUNSCM" "$S_STAGE_DIR/run.scm" 106 107 case "${DRIVER:-podman}" in 108 podman) _runscm_run_podman "$timeout" ;; 109 seed) _runscm_run_seed "$timeout" ;; 110 *) echo "runscm: unknown DRIVER=$DRIVER (expected podman|seed)" >&2; exit 2 ;; 111 esac 112 113 for n in $S_EXPORTS; do 114 if [ ! -f "$S_STAGE_DIR/out/$n" ]; then 115 echo "[runscm/$DRIVER] FAIL: missing output '$n'" >&2 116 ls "$S_STAGE_DIR/out" >&2 || true 117 exit 5 118 fi 119 cp "$S_STAGE_DIR/out/$n" "$S_OUT_DIR/$n" 120 chmod 0700 "$S_OUT_DIR/$n" 121 done 122 } 123 124 # Podman: bind-mount in/ ro and out/ rw under /work; exec in/scheme1. 125 # Outputs land in $S_STAGE_DIR/out/ directly via the rw bind mount. 126 _runscm_run_podman() { 127 : "${IMAGE:?lib-runscm: IMAGE not set}" 128 : "${PLATFORM:?lib-runscm: PLATFORM not set}" 129 in_abs=$(cd "$S_STAGE_DIR/in" && pwd) 130 out_abs=$(cd "$S_STAGE_DIR/out" && pwd) 131 echo "[runscm/podman] scheme1 combined.scm under $IMAGE" >&2 132 podman run --rm -i --pull=never --platform "$PLATFORM" \ 133 -v "$in_abs:/work/in:ro" \ 134 -v "$out_abs:/work/out:rw" \ 135 -w /work "$IMAGE" \ 136 in/scheme1 in/combined.scm 137 } 138 139 # Seed: pack cpio with `init` at the root and the in/ subtree under it; 140 # boot kernel with init=init combined.scm; recover outputs from the 141 # SEEDFS dump on hd1; extract filters out/-prefixed entries directly 142 # into $S_STAGE_DIR/out/. 143 _runscm_run_seed() { 144 timeout=$1 145 : "${KERNEL_IMAGE:?lib-runscm: KERNEL_IMAGE not set}" 146 : "${EXTRACT:?lib-runscm: EXTRACT not set}" 147 mem=${QEMU_MEM:-2048M} 148 cp "$S_STAGE_DIR/in/scheme1" "$S_STAGE_DIR/init" 149 chmod +x "$S_STAGE_DIR/init" 150 ( cd "$S_STAGE_DIR" && { echo init; find in -type f; } | sort -u | cpio -o -H newc 2>/dev/null ) \ 151 > "$S_STAGE_DIR/initramfs.cpio" 152 sz=$(wc -c < "$S_STAGE_DIR/initramfs.cpio") 153 pad=$(( (512 - sz % 512) % 512 )) 154 if [ "$pad" -gt 0 ]; then 155 head -c "$pad" /dev/zero >> "$S_STAGE_DIR/initramfs.cpio" 156 fi 157 mv "$S_STAGE_DIR/initramfs.cpio" "$S_STAGE_DIR/in.img" 158 truncate -s 256M "$S_STAGE_DIR/out.img" 159 160 TRANSCRIPT=$S_STAGE_DIR/transcript.txt 161 echo "[runscm/seed] booting scheme1 + run.scm (timeout ${timeout}s)" >&2 162 seed_arch=${SEED_ARCH:-aarch64} 163 case "$seed_arch" in 164 aarch64) 165 qemu-system-aarch64 \ 166 -machine virt,gic-version=3,accel=hvf -cpu host -m "$mem" \ 167 -nographic -no-reboot \ 168 -global virtio-mmio.force-legacy=false \ 169 -kernel "$KERNEL_IMAGE" \ 170 -drive file="$S_STAGE_DIR/in.img",if=none,format=raw,id=hd0,readonly=on \ 171 -device virtio-blk-device,drive=hd0 \ 172 -drive file="$S_STAGE_DIR/out.img",if=none,format=raw,id=hd1 \ 173 -device virtio-blk-device,drive=hd1 \ 174 -append "init in/combined.scm" \ 175 > "$TRANSCRIPT" 2>&1 & 176 ;; 177 riscv64) 178 # No hvf accel on Apple Silicon for riscv64 — TCG only. 179 qemu-system-riscv64 \ 180 -machine virt -m "$mem" \ 181 -nographic -no-reboot \ 182 -global virtio-mmio.force-legacy=false \ 183 -kernel "$KERNEL_IMAGE" \ 184 -drive file="$S_STAGE_DIR/in.img",if=none,format=raw,id=hd0,readonly=on \ 185 -device virtio-blk-device,drive=hd0 \ 186 -drive file="$S_STAGE_DIR/out.img",if=none,format=raw,id=hd1 \ 187 -device virtio-blk-device,drive=hd1 \ 188 -append "init in/combined.scm" \ 189 > "$TRANSCRIPT" 2>&1 & 190 ;; 191 amd64) 192 # microvm + isa-debug-exit mirrors seed-kernel/run.sh: the 193 # kernel writes to port 0x501 on user exit_group(0) so QEMU 194 # exits cleanly (no `-no-reboot` triple-fault gymnastics). 195 qemu-system-x86_64 \ 196 -machine microvm,acpi=off,pic=off,pit=off,rtc=off,isa-serial=on,auto-kernel-cmdline=off \ 197 -cpu max -m "$mem" \ 198 -nodefaults -display none -serial stdio -no-reboot \ 199 -global virtio-mmio.force-legacy=false \ 200 -device isa-debug-exit,iobase=0x501,iosize=2 \ 201 -kernel "$KERNEL_IMAGE" \ 202 -drive file="$S_STAGE_DIR/in.img",if=none,format=raw,id=hd0,readonly=on \ 203 -device virtio-blk-device,drive=hd0 \ 204 -drive file="$S_STAGE_DIR/out.img",if=none,format=raw,id=hd1 \ 205 -device virtio-blk-device,drive=hd1 \ 206 -append "init in/combined.scm" \ 207 > "$TRANSCRIPT" 2>&1 & 208 ;; 209 *) 210 echo "[runscm/seed] unsupported SEED_ARCH=$seed_arch" >&2 211 exit 2 212 ;; 213 esac 214 QPID=$! 215 ( sleep "$timeout"; kill -9 $QPID 2>/dev/null ) </dev/null >/dev/null 2>&1 & 216 WATCHER=$! 217 # `disown` removes the watcher from the shell's job table so that 218 # killing it on the happy path doesn't trigger bash's 219 # "Terminated: 15 PID ( sleep … )" job-status message — that 220 # message looks like a real failure but is just a noisy SIGTERM 221 # notification fired when qemu exited normally before the watcher's 222 # sleep elapsed. 223 disown $WATCHER 2>/dev/null || true 224 wait $QPID 2>/dev/null || true 225 kill $WATCHER 2>/dev/null || true 226 227 if ! "$EXTRACT" "$S_STAGE_DIR/out" "$S_STAGE_DIR/out.img" >/dev/null 2>&1; then 228 echo "[runscm/seed] FAIL: extract-blk failed (kernel didn't reach exit?)" >&2 229 tail -40 "$TRANSCRIPT" >&2 230 exit 3 231 fi 232 EXIT_LINE=$(grep -E "user exit_group" "$TRANSCRIPT" | tail -1 || true) 233 case "$EXIT_LINE" in 234 *"exit_group(0)"*) : ;; 235 *) echo "[runscm/seed] FAIL: driver did not exit 0: $EXIT_LINE" >&2 236 tail -40 "$TRANSCRIPT" >&2 237 exit 4 ;; 238 esac 239 }