RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorGrok Bot <grokbot@localhost>2026-10-31 19:55:48 -0700
commitc8f1cf11021adb985d3d928c170b40a5595d20cd
parentd53297804ff1320f8ca72f01c75460da6c0f33f9
treebrowse files at this commit
make test is contract v2 (init runs the self-test, host fsck + share read-back); make test-v1/image-v1 keep the old layout; compare uses v1
MMakefile+74 -27
Mconfig.mk+18 -6
Ascripts/check-v2-image.sh+58 -0
Ascripts/expect-v2.txt+84 -0
Mscripts/run-headless.sh+61 -19
Mscripts/run-qemu.sh+17 -8

6 files changed, 312 insertions, 60 deletions

@@ -6,8 +6,11 @@ include config.mk  OUT        := $(CURDIR)/out-IMAGE      := $(OUT)/$(IMAGE_NAME)-STAGING    := $(OUT)/staging+IMAGE_V2   := $(OUT)/$(IMAGE_NAME)+IMAGE_V1   := $(OUT)/$(IMAGE_V1_NAME)+IMAGE      := $(if $(filter v1,$(CONTRACT)),$(IMAGE_V1),$(IMAGE_V2))+STAGING_V1 := $(OUT)/staging-v1+STAGING_V2 := $(OUT)/staging-v2 RUST_OUT   := $(OUT)/rust-target/x86_64-unknown-none/release FIXTURES   := $(OUT)/fixtures KSRC       := $(OUT)/kernel-src@@ -20,24 +23,31 @@ C_OUT      := $(OUT)/c # Everything in /bin, in order: the kernel's list, then Foundry OS's own. ALL_BIN    := $(BIN_ORDER) $(FOUNDRY_C) $(FOUNDRY_PORTS) PORTS_OUT  := $(OUT)/ports+# Contract v2: the kernel's ABI test programs live on B: (/mnt/disk0/bin),+# in the kernel's order; everything else is in A:/bin.+DATA_BIN   := $(filter-out $(RUST_PROGRAMS),$(BIN_ORDER))  .DEFAULT_GOAL := image-.PHONY: help image rust fixtures cprogs ports staging kernel kernel-src run test test-reference compare k1-check rbsh-test musl clean distclean+.PHONY: help image image-v1 image-v2 rust fixtures cprogs ports staging-v1 staging-v2 kernel kernel-src run test test-v1 test-v2 test-reference compare k1-check rbsh-test musl clean distclean  help: 	@echo "Foundry OS (userland for the Rustbelt kernel $(KERNEL_VERSION) @ $(KERNEL_REV))" 	@echo ""-	@echo "  make / make image   build out/$(IMAGE_NAME), the disk image (contract $(CONTRACT))"+	@echo "  make / make image   build the disk image for contract $(CONTRACT): $(notdir $(IMAGE))"+	@echo "  make image-v1       build out/$(IMAGE_V1_NAME) (contract v1: one FAT32 partition)"+	@echo "  make image-v2       build out/$(IMAGE_NAME) (contract v2: ext2 root + FAT32 + exFAT share)" 	@echo "  make rust           cargo build the Rust programs in rust/" 	@echo "  make fixtures       build the kernel's ABI test programs from the pinned kernel" 	@echo "  make cprogs         build our C programs (c/) as static-PIEs with toolchain/foundry-cc" 	@echo "  make ports          fetch (cache/) + build the ports ($(FOUNDRY_PORTS)) the same way"-	@echo "  make staging        assemble out/staging (rootfs/ + /bin)"+	@echo "  make staging-v1/-v2 assemble the image trees in out/staging-v1, out/staging-v2" 	@echo "  make kernel         build the pinned kernel into out/kernel-target" 	@echo "  make run            boot the image in a QEMU window"-	@echo "  make test           headless boot + the shell self-test + expected-line checks"-	@echo "  make test-reference  the same headless test, but booting the kernel's own disk.img (baseline)"-	@echo "  make compare        compare out/$(IMAGE_NAME) with the kernel's own disk.img"+	@echo "  make test           make test-$(CONTRACT)"+	@echo "  make test-v2        headless boot: init runs the v2 self-test; then host fsck + share checks"+	@echo "  make test-v1        headless boot of the v1 image (the kernel runs the shell self-test)"+	@echo "  make test-reference  the v1 test, but booting the kernel's own disk.img (baseline)"+	@echo "  make compare        compare out/$(IMAGE_V1_NAME) with the kernel's own disk.img" 	@echo "  make rbsh-test      host unit tests for rbsh's parser and drive letters (no QEMU)" 	@echo "  make k1-check       build the kernel with RUSTBELT_DISK0=our image (K1) and check it boots ours" 	@echo "  make musl           fetch (cache/) + build musl $(MUSL_VERSION) into out/sysroot"@@ -74,39 +84,76 @@ $(PORTS_OUT)/bin/%: $(SYSROOT)/lib/musl-pie.specs ports/%/build.sh 	ports/$*/build.sh cache $(PORTS_OUT)/$*-build $@  # ---- the image ----------------------------------------------------------------# Contract v1: rootfs/ minus etc/ (etc/ is for the contract-v2 ext2 root,-# see docs/plan.md) goes to the top of the FAT32 volume, programs to /bin.-staging: rust fixtures cprogs ports-	rm -rf $(STAGING) && mkdir -p $(STAGING)/bin-	cd rootfs && for e in $$(ls -A); do [ "$$e" = etc ] && [ "$(CONTRACT)" = v1 ] && continue; cp -r "$$e" $(STAGING)/; done-	for p in $(filter-out sh,$(RUST_PROGRAMS)); do cp $(RUST_OUT)/$$p $(STAGING)/bin/; done-	cp $(RUST_OUT)/rbsh $(STAGING)/bin/sh    # contract v1: the kernel runs /mnt/disk0/bin/sh-	cp $(FIXTURES)/* $(STAGING)/bin/-	for p in $(FOUNDRY_C); do cp $(C_OUT)/$$p $(STAGING)/bin/; done-	for p in $(FOUNDRY_PORTS); do cp $(PORTS_OUT)/bin/$$p $(STAGING)/bin/; done--image: staging-	scripts/mkimage.sh $(STAGING) $(IMAGE) $(IMAGE_MIB) $(IMAGE_LABEL) "$(ALL_BIN)"+image: image-$(CONTRACT)++# Contract v1: rootfs/ goes to the top of the FAT32 volume, all programs to+# /bin, rbsh as /bin/sh (the kernel runs /mnt/disk0/bin/sh).+staging-v1: rust fixtures cprogs ports+	rm -rf $(STAGING_V1) && mkdir -p $(STAGING_V1)/bin+	cp -r rootfs/. $(STAGING_V1)/+	for p in $(filter-out sh,$(RUST_PROGRAMS)); do cp $(RUST_OUT)/$$p $(STAGING_V1)/bin/; done+	cp $(RUST_OUT)/rbsh $(STAGING_V1)/bin/sh+	cp $(FIXTURES)/* $(STAGING_V1)/bin/+	for p in $(FOUNDRY_C); do cp $(C_OUT)/$$p $(STAGING_V1)/bin/; done+	for p in $(FOUNDRY_PORTS); do cp $(PORTS_OUT)/bin/$$p $(STAGING_V1)/bin/; done++image-v1: staging-v1+	scripts/mkimage.sh $(STAGING_V1) $(IMAGE_V1) $(IMAGE_MIB) $(IMAGE_LABEL) "$(ALL_BIN)"++# Contract v2 (docs/boot-contract.md): three trees, one per partition.+#   root/  -> A: (ext2 root): /sbin/init, our programs in /bin, /etc, /scripts+#   data/  -> B: (FAT32, /mnt/disk0): rootfs/ minus scripts/, + the ABI fixtures in bin/+#   share/ -> C: (exFAT, /mnt/share): share/+staging-v2: rust fixtures cprogs ports+	rm -rf $(STAGING_V2) && mkdir -p $(STAGING_V2)/data/bin $(STAGING_V2)/share+	cp -r root $(STAGING_V2)/root && rm -f $(STAGING_V2)/root/README.md+	cd $(STAGING_V2)/root && mkdir -p sbin bin usr/bin tmp root home/tyler mnt/disk0 mnt/share+	cp $(RUST_OUT)/init $(STAGING_V2)/root/sbin/init+	cp $(RUST_OUT)/rbsh $(STAGING_V2)/root/bin/rbsh+	for p in $(filter-out sh,$(RUST_PROGRAMS)); do cp $(RUST_OUT)/$$p $(STAGING_V2)/root/bin/; done+	for p in $(FOUNDRY_C); do cp $(C_OUT)/$$p $(STAGING_V2)/root/bin/; done+	for p in $(FOUNDRY_PORTS); do cp $(PORTS_OUT)/bin/$$p $(STAGING_V2)/root/bin/; done+	ln -s dash $(STAGING_V2)/root/bin/sh+	cp rootfs/scripts/dash-test.sh $(STAGING_V2)/root/scripts/+	cd rootfs && for e in $$(ls -A); do [ "$$e" = scripts ] || cp -r "$$e" $(STAGING_V2)/data/; done+	cp $(FIXTURES)/* $(STAGING_V2)/data/bin/+	cp share/* $(STAGING_V2)/share/++image-v2: staging-v2+	ROOT_MIB=$(ROOT_MIB) DATA_MIB=$(DATA_MIB) SHARE_MIB=$(SHARE_MIB) \+	    scripts/mkimage-v2.sh $(STAGING_V2) $(IMAGE_V2) "$(DATA_BIN)"  # ---- booting ---------------------------------------------------------------- run: image kernel-	scripts/run-qemu.sh $(IMAGE) $(KBOOT) $(KTARGET) $(OUT)+	CONTRACT=$(CONTRACT) scripts/run-qemu.sh $(IMAGE) $(KBOOT) $(KTARGET) $(OUT)  # What the expected-line checks need to know about the pinned kernel. TEST_ENV   := KERNEL_BANNER="$(KERNEL_BANNER)" KERNEL_VERSION=$(KERNEL_VERSION) -test: image kernel+test: test-$(CONTRACT)++# v2: boots a writable copy (out/test-v2.img) so the host can check what+# the run left behind: fsck of all three file systems, and the file the+# self-test wrote to the share (scripts/check-v2-image.sh).+test-v2: image-v2 kernel+	cp $(IMAGE_V2) $(OUT)/test-v2.img+	$(TEST_ENV) CONTRACT=v2 BIN_COUNT=$(words $(DATA_BIN)) ROOT_BIN_COUNT=$$(ls $(STAGING_V2)/root/bin | wc -l) \+	    EXTRA_EXPECT=scripts/expect-v2.txt \+	    scripts/run-headless.sh $(OUT)/test-v2.img $(KBOOT_HL) $(KTARGET) $(OUT)/v2+	scripts/check-v2-image.sh $(OUT)/test-v2.img++test-v1: image-v1 kernel 	$(TEST_ENV) BIN_COUNT=$(words $(ALL_BIN)) EXTRA_EXPECT=scripts/expect-foundry.txt \-	    scripts/run-headless.sh $(IMAGE) $(KBOOT_HL) $(KTARGET) $(OUT)+	    scripts/run-headless.sh $(IMAGE_V1) $(KBOOT_HL) $(KTARGET) $(OUT) -# The baseline: the kernel's own disk.img through the very same checks+# The baseline: the kernel's own disk.img through the very same v1 checks # (without Foundry OS's own programs and expected lines, which it lacks). test-reference: kernel 	$(TEST_ENV) BIN_COUNT=$(words $(BIN_ORDER)) \ 	    scripts/run-headless.sh $(KTARGET)/disk.img $(KBOOT_HL) $(KTARGET) $(OUT)/reference -compare: image kernel-	FOUNDRY_BIN="$(FOUNDRY_C) $(FOUNDRY_PORTS)" FOUNDRY_FILES="$(FOUNDRY_FILES)" scripts/compare-image.sh $(IMAGE) $(KTARGET)/disk.img $(OUT)+compare: image-v1 kernel+	FOUNDRY_BIN="$(FOUNDRY_C) $(FOUNDRY_PORTS)" FOUNDRY_FILES="$(FOUNDRY_FILES)" scripts/compare-image.sh $(IMAGE_V1) $(KTARGET)/disk.img $(OUT)  # rbsh's parser is pure Rust, so it is also tested on the host. rbsh-test:
@@ -16,14 +16,26 @@ KERNEL_BANNER  ?= Rustbelt 0.21.0 (stage 21: root partition and init)  # ---- The image ------------------------------------------------------------- ARCH           ?= x86_64-# Boot contract version (docs/boot-contract.md). v1 = today's layout:-# GPT + one FAT32 partition, programs in /bin, kernel runs /mnt/disk0/bin/sh.-CONTRACT       ?= v1+# Boot contract version (docs/boot-contract.md), picks what `make image`,+# `make run` and `make test` use:+#   v2 (default since M3, Rustbelt 0.21+): one GPT disk with an ext2 root+#      (A: = /), a FAT32 data partition (B: = /mnt/disk0) and an exFAT+#      share (C: = /mnt/share); the kernel runs our /sbin/init as pid 1.+#      Image: out/foundry.img (scripts/mkimage-v2.sh).+#   v1 (the old layout, kept for `make test-v1` and `make compare`): GPT ++#      one FAT32 partition, programs in /bin, the kernel runs+#      /mnt/disk0/bin/sh. Image: out/foundry-v1.img (scripts/mkimage.sh).+CONTRACT       ?= v2 IMAGE_NAME     ?= foundry.img+IMAGE_V1_NAME  ?= foundry-v1.img+# v1 only: image size, and the FAT volume label. Kept as the kernel's+# TYLERDISK so the v1 image is the same as the kernel's own disk.img. IMAGE_MIB      ?= 64-# FAT volume label. Kept as the kernel's TYLERDISK under contract v1 so the-# image is the same as the kernel's own disk.img. IMAGE_LABEL    ?= TYLERDISK+# v2 only: partition sizes in MiB (the disk is 2 MiB bigger: GPT + backup).+ROOT_MIB       ?= 32+DATA_MIB       ?= 40+SHARE_MIB      ?= 16  # ---- Programs in /bin ------------------------------------------------------ # Built from this repo (rust/):@@ -61,7 +73,7 @@ FOUNDRY_C      := hello-musl fprobe orphan # Third-party programs from ports/NAME (pinned tarball in cache/, built # static-PIE with foundry-cc), installed as /bin/NAME. FOUNDRY_PORTS  := dash-# Files outside /bin that only Foundry OS's image has (make compare skips them).+# Files outside /bin that only Foundry OS's v1 image has (make compare skips them). FOUNDRY_FILES  := scripts/dash-test.sh  # ---- Toolchain pins (downloaded into cache/, checked by SHA256) -----------
@@ -0,0 +1,58 @@+#!/usr/bin/env bash+# After `make test-v2`: check, on the host, the disk image the run wrote to.+#   * every file system is still consistent: e2fsck (A:), fsck.fat (B:),+#     fsck.exfat (C:), all read-only (-n);+#   * the share is a plain single-FAT exFAT volume, and the file the+#     self-test left there (dash's /mnt/share/from-foundry.txt) reads back+#     on the host -- through scripts/exfat.py, not the kernel's driver;+#   * the files the self-test created and deleted are really gone.+# Nothing is mounted (no root needed). Usage: scripts/check-v2-image.sh IMAGE+set -euo pipefail+IMAGE=$1+PATH=$PATH:/usr/sbin:/sbin+HERE=$(cd "$(dirname "$0")" && pwd)+WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT+fail=0+ok()  { echo "[check-v2] ok   $*"; }+bad() { echo "[check-v2] FAIL $*"; fail=1; }++# Partition start/size in sectors, from the GPT.+read -r S1 N1 S2 N2 S3 N3 < <(sfdisk --dump "$IMAGE" | sed -n 's/.*start= *\([0-9]*\), size= *\([0-9]*\).*/\1 \2/p' | tr '\n' ' '; echo)+for i in 1 2 3; do+    eval "dd if=\"\$IMAGE\" of=\"\$WORK/p$i\" bs=512 skip=\$S$i count=\$N$i status=none"+done++e2fsck -fn "$WORK/p1" > "$WORK/e2fsck.txt" 2>&1 && ok "e2fsck -fn: A: (ext2 root) is clean: $(tail -1 "$WORK/e2fsck.txt")" \+    || { bad "e2fsck on A:"; cat "$WORK/e2fsck.txt"; }+# The kernel's FAT driver (the fatfs crate) sets the volume's "dirty" bit+# (boot sector byte 65, bit 0) on the first write and clears it only on+# unmount, which Rustbelt 0.21 never does before QEMU exits. That is a+# known kernel issue (docs/kernel-requests.md), so it is a warning here:+# clear the bit in our extracted copy, then check everything else.+if [ $(( $(od -An -tu1 -j65 -N1 "$WORK/p2") & 1 )) = 1 ]; then+    echo "[check-v2] WARN B: has the FAT dirty bit set (kernel never unmounts it; see docs/kernel-requests.md K12)"+    printf '\x00' | dd of="$WORK/p2" bs=1 seek=65 conv=notrunc status=none+fi+fsck.fat -n "$WORK/p2" > "$WORK/fsck-fat.txt" 2>&1 && ok "fsck.fat -n: B: (FAT32) is clean: $(tail -1 "$WORK/fsck-fat.txt")" \+    || { bad "fsck.fat on B:"; cat "$WORK/fsck-fat.txt"; }+fsck.exfat -n "$WORK/p3" > "$WORK/fsck-exfat.txt" 2>&1 && ok "fsck.exfat -n: C: (exFAT share) is clean: $(tail -1 "$WORK/fsck-exfat.txt")" \+    || { bad "fsck.exfat on C:"; cat "$WORK/fsck-exfat.txt"; }++info=$(python3 "$HERE/exfat.py" "$WORK/p3" info)+case $info in *"1 FAT(s)"*) ok "share: $info" ;; *) bad "share is not single-FAT: $info" ;; esac+got=$(python3 "$HERE/exfat.py" "$WORK/p3" cat /from-foundry.txt 2>&1 || true)+[ "$got" = "written on Foundry OS by dash" ] && ok "host reads C:/from-foundry.txt: \"$got\"" \+    || bad "C:/from-foundry.txt on the host: '$got'"+listing=$(python3 "$HERE/exfat.py" "$WORK/p3" ls)+echo "$listing" | sed 's/^/[check-v2]      C:\//'+for gone in v2-test.txt v2dir todo.txt made-here seq.txt; do+    echo "$listing" | grep -qw -- "$gone" && bad "C:/$gone should have been deleted" || true+done+echo "$listing" | grep -q "README.TXT" || bad "C:/README.TXT is missing"+# A: and B: through e2fsprogs/mtools: the test files are gone, /etc is there.+debugfs -R "ls -l /" "$WORK/p1" 2>/dev/null | grep -q "v2-test-root.txt" && bad "A:/v2-test-root.txt should be gone" || true+[ "$(debugfs -R 'cat /etc/motd' "$WORK/p1" 2>/dev/null)" = "Welcome to Foundry OS, running on the Rustbelt kernel." ] \+    && ok "A:/etc/motd reads back through debugfs" || bad "A:/etc/motd"+MTOOLS_SKIP_CHECK=1 mdir -b -i "$WORK/p2" ::/ 2>/dev/null | grep -qi "v2-test-b" && bad "B:/v2-test-b.txt should be gone" || true++[ $fail = 0 ] && echo "[check-v2] all host checks passed" || { echo "[check-v2] FAILED"; exit 1; }
@@ -0,0 +1,84 @@+# Lines the boot-contract-v2 self-test must print (root/scripts/selftest.sh,+# run by /sbin/init). `make test-v2` passes this file to+# scripts/run-headless.sh (CONTRACT=v2). One line per check, matched as a+# fixed string anywhere in the serial log. Substitutions:+# @KERNEL_BANNER@, @KERNEL_VERSION@ (config.mk), @BIN_COUNT@ (programs on+# B:/bin), @ROOT_BIN_COUNT@ (programs in A:/bin).+# Blank lines and lines starting with # are skipped.++# ---- the kernel's boot (stage 21): what it found on our one disk ----+@KERNEL_BANNER@ x86_64+[smp] 4 CPUs online+mounted at / (root partition)+mounted at /mnt/disk0+exfat+mounted at /mnt/share++# ---- init (rust/init) ----+[init] Foundry OS init, pid 1, on Rustbelt @KERNEL_VERSION@ (headless: running the self-test)+[init] mounts: / ext2+[init] pid 1 (/sbin/init) exited:++# ---- drives and the root (A:) ----+A:  /            ext2+B:  /mnt/disk0   fat32+C:  /mnt/share   exfat+etc/+sbin/+scripts/+ init+sh -> dash+tyler:x:1000:1000:Tyler:/home/tyler:/bin/rbsh+Welcome to Foundry OS, running on the Rustbelt kernel.++# ---- B:, the FAT32 data disk ----+B:/ $ ls+In the Rustbelt shell this disk is drive B:+second line+       125  todo.txt++# ---- C:, the exFAT share ----+C:/ $ ls -l+This is the Foundry OS share (drive C:, /mnt/share).+written by Foundry OS+       125  todo.txt+     <DIR>  made-here/+(start sleep 1), exit code 0++# ---- the rest of the kernel's stages, as under v1 ----+[exit code 139]+(overflow) killed: stack overflow+4 CPUs online (this program ran on CPU+[net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3+3 packets transmitted, 3 received, 0% packet loss+Hello from the host, via QEMU guestfwd!+fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/share/seq.txt+[httpd] served 1 request, bye+crw-------      <DEV>  hvc0+tile: 4 windows on 1024x768, console 61x46+[forktest] all checks passed+[cowtest] all checks passed+[mmaptest] all checks passed+[sigtest] all checks passed+(start spin 1000), exit code 143+[threads] all checks passed+[echoserver] all checks passed+[ptytest] all checks passed+[nbtest] all checks passed+[fputest] all checks passed+[pathtest] all checks passed+rawabi: uname: Rustbelt @KERNEL_VERSION@+rawabi: all checks passed++# ---- Foundry OS's own programs ----+hello from musl on Rustbelt @KERNEL_VERSION@ (Foundry OS, static-PIE)+fprobe: all 40 checks passed+orphan: all 4 checks passed+dash-test: @ROOT_BIN_COUNT@ programs in /bin+dash-test: all 15 checks passed+v2-test: ok share-write-read-delete+v2-test: ok sh-is-dash+v2-test: all 18 checks passed+sh: nosuchprogram: command not found+cd: there is no drive Q:+A:/tmp $ pwd
@@ -1,12 +1,21 @@ #!/usr/bin/env bash-# Boot the Foundry OS image headless on the pinned Rustbelt kernel, let the-# kernel run the shell self-test (B:/scripts/selftest.sh from rootfs/), and-# check the output. Moved from the kernel's scripts/run-headless.sh; the-# expected lines are the same, so a pass here means "same behaviour as the-# kernel's own image".+# Boot a Foundry OS image headless on the pinned Rustbelt kernel and check+# the self-test's output on the serial console. Two boot contracts+# (docs/boot-contract.md), picked with CONTRACT=v1|v2 (default v1): #-# Usage: scripts/run-headless.sh IMAGE KERNEL_BOOTIMAGE KERNEL_TARGET OUT_DIR-#   IMAGE             our disk image (drive B:, /mnt/disk0)+#   v1: the kernel runs the shell self-test (B:/scripts/selftest.sh from+#       rootfs/) itself. Our image is disk 0, the kernel's three test disks+#       C:, D:, E: (snapshot: nothing is written back). The expected lines+#       are the kernel repo's own (moved from its scripts/run-headless.sh),+#       so a pass means "same behaviour as the kernel's own image".+#   v2: our /sbin/init runs /scripts/selftest.sh (from root/) and ends the+#       run with its exit status. Only our image is attached (A:, B:, C:+#       are all partitions of it), WITHOUT snapshot, so the caller can+#       check afterwards what was written (make test-v2 boots a copy).+#       The expected lines are all in EXTRA_EXPECT (scripts/expect-v2.txt).+#+# Usage: [CONTRACT=v2] scripts/run-headless.sh IMAGE KERNEL_BOOTIMAGE KERNEL_TARGET OUT_DIR+#   IMAGE             our disk image #   KERNEL_BOOTIMAGE  out/kernel/bootimage-headless.bin #   KERNEL_TARGET     out/kernel-target (has the kernel-owned test disks #                     ext2.img, exfat.img, btrfs.img: drives C:, D:, E:)@@ -14,7 +23,8 @@ # Env (the Makefile sets these from config.mk; they change with the kernel pin): #   KERNEL_BANNER   the kernel's first line, e.g. "Rustbelt 0.21.0 (stage 21: ...)" #   KERNEL_VERSION  e.g. 0.21.0-#   BIN_COUNT       how many programs the image has in /bin (`dir bin | wc -l`)+#   BIN_COUNT       how many programs the image has in B:/bin (`dir bin | wc -l`)+#   ROOT_BIN_COUNT  v2: how many programs are in A:/bin (@ROOT_BIN_COUNT@) #   EXTRA_EXPECT    optional file: more lines the log must contain, one per #                   line (Foundry OS's own checks; # starts a comment) #   QEMU_CPU (default max), QEMU_TIMEOUT (default 120 s).@@ -24,12 +34,25 @@ set -euo pipefail cd "$(dirname "$0")/.."            # repo root: QEMU's guestfwd runs scripts/http-reply.sh IMAGE=$1 IMG=$2 KT=$3 OUT=$4+CONTRACT=${CONTRACT:-v1} : "${KERNEL_BANNER:?set KERNEL_BANNER (see config.mk)}" "${KERNEL_VERSION:?}" "${BIN_COUNT:?}" STAGE=${KERNEL_BANNER#*(}; STAGE=${STAGE%%:*}     # "stage 20" mkdir -p "$OUT"-for f in "$IMAGE" "$IMG" "$KT/ext2.img" "$KT/exfat.img" "$KT/btrfs.img"; do+if [ "$CONTRACT" = v2 ]; then+    DISKS=(-drive file="$IMAGE",format=raw,if=virtio)+else+    DISKS=(-drive file="$IMAGE",format=raw,if=virtio,snapshot=on)+    for d in ext2 exfat btrfs; do+        DISKS+=(-drive file="$KT/$d.img",format=raw,if=virtio,snapshot=on)+    done+fi+for f in "$IMAGE" "$IMG"; do     [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; } done+for d in "${DISKS[@]}"; do+    case $d in file=*) f=${d#file=}; f=${f%%,*}+        [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; } ;; esac+done  PIDFILE=$OUT/qemu-headless.pid rm -f "$PIDFILE"@@ -66,15 +89,12 @@ VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci  LOG=$OUT/headless-serial.log echo "[run-headless] kernel $IMG"-echo "[run-headless] disk 0 (B:) = $IMAGE"+echo "[run-headless] contract $CONTRACT, disk 0 = $IMAGE" set +e timeout "${QEMU_TIMEOUT:-120}" qemu-system-x86_64 -smp 4 -cpu "${QEMU_CPU:-max}" \     -pidfile "$PIDFILE" \     -drive format=raw,file="$IMG",snapshot=on \-    -drive file="$IMAGE",format=raw,if=virtio,snapshot=on \-    -drive file="$KT/ext2.img",format=raw,if=virtio,snapshot=on \-    -drive file="$KT/exfat.img",format=raw,if=virtio,snapshot=on \-    -drive file="$KT/btrfs.img",format=raw,if=virtio,snapshot=on \+    "${DISKS[@]}" \     -netdev "$NETDEV" -device virtio-net-pci,netdev=net0 \     "${VIRTIO_DEVICES[@]}" \     -device isa-debug-exit,iobase=0xf4,iosize=0x04 \@@ -84,7 +104,17 @@ kill $CURL_PID 2>/dev/null; wait $CURL_PID 2>/dev/null set -e  # Lines the shell self-test must have printed (same list as the kernel repo at the pinned commit).-if [ "$code" = 33 ]; then+if [ "$code" = 33 ] && [ "$CONTRACT" = v2 ]; then+    expect=()   # all in EXTRA_EXPECT+    # Pipe results: `dir bin | wc -l` on B: -> $BIN_COUNT, the todo.txt grep -> 1,+    # `cat C:/seq.txt | wc -l` (downloaded by wget) -> 20000.+    PIPE_RESULTS=("$BIN_COUNT" 1 20000)+    # init's own memory check: frames before the self-test == after it and+    # every process it started (orphans included) were gone.+    FRAMES_RE='^\[init\] memory: ([0-9]+) frames in use before the self-test, \1 after'+elif [ "$code" = 33 ]; then+    PIPE_RESULTS=(1 "$BIN_COUNT" 3 4 20000)+    FRAMES_RE='^\[memory\] ([0-9]+) frames in use before the shell, \1 after'     expect=(         "$KERNEL_BANNER x86_64"                       # ver         "[smp] 4 CPUs online"                          # smp.rs started 3 more cores@@ -163,11 +193,15 @@ if [ "$code" = 33 ]; then         "cd: there is no drive Q:"         "A:/tmp $ pwd"     )+fi+if [ "$code" = 33 ]; then     if [ -n "${EXTRA_EXPECT:-}" ]; then         while IFS= read -r line; do             case $line in ''|'#'*) continue ;; esac             line=${line//@KERNEL_VERSION@/$KERNEL_VERSION}             line=${line//@BIN_COUNT@/$BIN_COUNT}+            line=${line//@ROOT_BIN_COUNT@/${ROOT_BIN_COUNT:-}}+            line=${line//@KERNEL_BANNER@/$KERNEL_BANNER}             expect+=("$line")         done < "$EXTRA_EXPECT"     fi@@ -175,16 +209,16 @@ if [ "$code" = 33 ]; then     for line in "${expect[@]}"; do         grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }     done-    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> $BIN_COUNT (the programs),+    # Pipe results (v1): `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> $BIN_COUNT (the programs),     # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3,     # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and-    # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.-    for n in 1 "$BIN_COUNT" 3 4 20000; do+    # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000. v2: see above.+    for n in "${PIPE_RESULTS[@]}"; do         grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }     done     # No memory leaks: once the shell and all ~60 programs it ran have     # exited, exactly as many frames must be in use as before it started.-    if grep -qE '^\[memory\] ([0-9]+) frames in use before the shell, \1 after' "$LOG"; then+    if grep -qE "$FRAMES_RE" "$LOG"; then         echo "[run-headless] frames in use returned to the baseline"     else         echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1@@ -207,6 +241,14 @@ if [ "$code" = 33 ]; then         grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; }     done     grep -qxE -- "[0-9]+ [0-9]+ [0-9]+" "$HVC_OUT" || { echo "[run-headless] MISSING on hvc0: random -d 20 3"; missing=1; }+    if [ "$CONTRACT" = v2 ]; then+        # init adopted and reaped at least one orphan (c/orphan + v2-test.sh make some).+        if grep -qE '^\[init\] self-test finished: exit code 0; init reaped [1-9][0-9]* adopted' "$LOG"; then+            echo "[run-headless] $(grep -E '^\[init\] self-test finished' "$LOG")"+        else+            echo "[run-headless] MISSING: init reaped >= 1 orphan and the self-test exited 0"; missing=1+        fi+    fi     [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+12 expected shell and hvc0 lines found" || code=35 fi 
@@ -1,23 +1,32 @@ #!/usr/bin/env bash # Boot the Foundry OS image interactively on the pinned Rustbelt kernel:-# a QEMU window (or add NOGRAPHIC=1 for serial only), our image as disk 0-# (B:), the kernel's test disks as C:, D:, E:. Same devices as the kernel's-# `cargo run`. The hvc0 console is on the unix socket OUT/hvc0.sock:-#   socat - UNIX-CONNECT:out/hvc0.sock     (then `getty /dev/hvc0 &` in Foundry)+# a QEMU window (or add NOGRAPHIC=1 for serial only).+#   CONTRACT=v2 (default via make): only our image; init starts rbsh on the+#     console and on hvc0. Writes go to a temporary overlay (snapshot) and+#     are thrown away at exit; set KEEP=1 to write to the image for real.+#   CONTRACT=v1: our image as disk 0 (B:), the kernel's test disks as C:,+#     D:, E:, like the kernel's `cargo run`.+# The hvc0 console is on the unix socket OUT/hvc0.sock:+#   socat - UNIX-CONNECT:out/hvc0.sock     (v1: then `getty /dev/hvc0 &` in Foundry) # # Usage: scripts/run-qemu.sh IMAGE KERNEL_BOOTIMAGE KERNEL_TARGET OUT_DIR set -euo pipefail cd "$(dirname "$0")/.." IMAGE=$1 IMG=$2 KT=$3 OUT=$4 display=()+snap=,snapshot=on+[ -n "${KEEP:-}" ] && snap=+if [ "${CONTRACT:-v1}" = v2 ]; then+    disks=(-drive file="$IMAGE",format=raw,if=virtio$snap)+else+    disks=(-drive file="$IMAGE",format=raw,if=virtio,snapshot=on)+    for d in ext2 exfat btrfs; do disks+=(-drive file="$KT/$d.img",format=raw,if=virtio,snapshot=on); done+fi [ -n "${NOGRAPHIC:-}" ] && display=(-display none) exec qemu-system-x86_64 -smp 4 -cpu "${QEMU_CPU:-max}" \     -pidfile "$OUT/qemu-run.pid" \     -drive format=raw,file="$IMG",snapshot=on \-    -drive file="$IMAGE",format=raw,if=virtio,snapshot=on \-    -drive file="$KT/ext2.img",format=raw,if=virtio,snapshot=on \-    -drive file="$KT/exfat.img",format=raw,if=virtio,snapshot=on \-    -drive file="$KT/btrfs.img",format=raw,if=virtio,snapshot=on \+    "${disks[@]}" \     -netdev "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat,hostfwd=tcp:127.0.0.1:5580-:80" \     -device virtio-net-pci,netdev=net0 \     -vga none -device virtio-vga -device virtio-rng-pci \