foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Grok Bot <grokbot@localhost> | 2026-10-31 19:55:48 -0700 |
|---|---|---|
| commit | c8f1cf11021adb985d3d928c170b40a5595d20cd | |
| parent | d53297804ff1320f8ca72f01c75460da6c0f33f9 | |
| tree | browse 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
| M | Makefile | +74 -27 |
| M | config.mk | +18 -6 |
| A | scripts/check-v2-image.sh | +58 -0 |
| A | scripts/expect-v2.txt | +84 -0 |
| M | scripts/run-headless.sh | +61 -19 |
| M | scripts/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 \