RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorGrok Bot <grokbot@localhost>2026-10-31 18:56:23 -0700
commitb3a7cc7ae7b8dd158853a83411c3346eec91103c
parent0ffe635cd6a184be44f131a43a33b657615d13ec
treebrowse files at this commit
Build system: make image/run/test/compare against the pinned kernel a55b41d

- config.mk: KERNEL_DIR, KERNEL_REV=a55b41d (Rustbelt 0.19.0), image name
  foundry.img, the program lists and their /bin order.
- scripts/build-kernel.sh: git archive the pinned commit into out/kernel-src
  and build it with CARGO_TARGET_DIR=out/kernel-target (never touches the
  kernel repo's tree or target/).
- scripts/build-fixtures.sh: the kernel's ABI test programs, compiled from
  the pinned kernel source with build.rs's exact commands.
- scripts/mkimage.sh: GPT + FAT32 with sfdisk/mkfs.vfat/mtools, no root.
- scripts/run-qemu.sh, run-headless.sh (moved from the kernel, same expected
  lines; QEMU PID tracked with -pidfile), compare-image.sh,
  http-reply.sh and hvc0-session.txt (copied from the kernel at a55b41d).
AMakefile+81 -0
Aconfig.mk+50 -0
Ascripts/build-fixtures.sh+45 -0
Ascripts/build-kernel.sh+50 -0
Ascripts/compare-image.sh+51 -0
Ascripts/http-reply.sh+26 -0
Ascripts/hvc0-session.txt+5 -0
Ascripts/mkimage.sh+51 -0
Ascripts/run-headless.sh+203 -0
Ascripts/run-qemu.sh+27 -0

10 files changed, 589 insertions, 0 deletions

@@ -0,0 +1,81 @@+# Foundry OS -- the single entry point. `make help` lists the targets.+#+# Everything generated goes to out/ (git-ignored); `make clean` removes it.+# Downloads (musl source etc.) go to cache/ (git-ignored, SHA256-checked).++include config.mk++OUT        := $(CURDIR)/out+IMAGE      := $(OUT)/$(IMAGE_NAME)+STAGING    := $(OUT)/staging+RUST_OUT   := $(OUT)/rust-target/x86_64-unknown-none/release+FIXTURES   := $(OUT)/fixtures+KSRC       := $(OUT)/kernel-src+KTARGET    := $(OUT)/kernel-target+KBOOT      := $(OUT)/kernel/bootimage.bin+KBOOT_HL   := $(OUT)/kernel/bootimage-headless.bin++.DEFAULT_GOAL := image+.PHONY: help image rust fixtures staging kernel kernel-src run test compare 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 rust           cargo build the Rust programs in rust/"+	@echo "  make fixtures       build the kernel's ABI test programs from the pinned kernel"+	@echo "  make staging        assemble out/staging (rootfs/ + /bin)"+	@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 compare        compare out/$(IMAGE_NAME) with the kernel's own disk.img"+	@echo "  make musl           fetch (cache/) + build musl $(MUSL_VERSION) into out/sysroot (not in the image yet)"+	@echo "  make clean          rm -rf out   (distclean also removes cache/)"++# ---- the pinned kernel -------------------------------------------------------+kernel-src:+	scripts/build-kernel.sh $(KERNEL_DIR) $(KERNEL_REV) $(OUT) --export-only++kernel:+	scripts/build-kernel.sh $(KERNEL_DIR) $(KERNEL_REV) $(OUT)++# ---- programs ---------------------------------------------------------------+rust:+	cd rust && CARGO_TARGET_DIR=$(OUT)/rust-target cargo build --release++fixtures: kernel-src+	scripts/build-fixtures.sh $(KSRC) $(FIXTURES) "$(FIXTURE_RUST)" "$(FIXTURE_C)" "$(FIXTURE_MUSL)"++# ---- 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+	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/++image: staging+	scripts/mkimage.sh $(STAGING) $(IMAGE) $(IMAGE_MIB) $(IMAGE_LABEL) "$(BIN_ORDER)"++# ---- booting ----------------------------------------------------------------+run: image kernel+	scripts/run-qemu.sh $(IMAGE) $(KBOOT) $(KTARGET) $(OUT)++test: image kernel+	scripts/run-headless.sh $(IMAGE) $(KBOOT_HL) $(KTARGET) $(OUT)++compare: image kernel+	scripts/compare-image.sh $(IMAGE) $(KTARGET)/disk.img $(OUT)++# ---- toolchain (M2 groundwork; not used by the v1 image) ---------------------+musl:+	toolchain/musl/fetch.sh cache+	toolchain/musl/build.sh cache $(OUT)/musl-build $(OUT)/sysroot++clean:+	rm -rf out++distclean: clean+	rm -rf cache
@@ -0,0 +1,50 @@+# Foundry OS build settings. The Makefile includes this file; you can also+# override any value on the command line: `make KERNEL_DIR=~/rk test`.++# ---- The kernel we build against ------------------------------------------+# Foundry OS runs on the Rustbelt kernel. We never build "whatever is checked+# out": we export one exact commit (KERNEL_REV) from the kernel's git store+# into out/kernel-src and build it there, with its own target dir. Bump+# KERNEL_REV deliberately (and re-run `make test`) when the kernel moves on.+KERNEL_DIR     ?= /workspace/rust-kernel+KERNEL_REV     ?= a55b41d+KERNEL_VERSION ?= 0.19.0++# ---- 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+IMAGE_NAME     ?= foundry.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++# ---- Programs in /bin ------------------------------------------------------+# Built from this repo (rust/):+RUST_PROGRAMS  := sh ls cat wc grep mkdir rm touch cp sleep \+                  ifconfig ping host fetch httpd getty tile random sysinfo+# ABI test fixtures that stay in the kernel repo (proposal section 7). For+# contract v1 the image still carries them (the self-test runs them), so+# they are compiled from the pinned kernel source in out/kernel-src/user.+FIXTURE_RUST   := hello evil spin demo child overflow forktest cowtest \+                  mmaptest sigtest threads echoserver ptytest nbtest \+                  fputest pathtest libctest coretest unixtest+FIXTURE_C      := rawabi+# name:kind, kind = pie (musl-gcc -static-pie), exec (static ET_EXEC at the+# user base) or dyn (dynamic; Rustbelt must refuse it).+FIXTURE_MUSL   := vdsotest:exec piehello:pie piethreads:pie dynhello:dyn remaptest:pie++# The order programs are copied into /bin: the same order as the kernel's+# build.rs (PROGRAMS, then C_PROGRAMS, then MUSL_PROGRAMS), so `ls` lists+# them the same way.+BIN_ORDER      := sh ls cat wc grep mkdir rm touch cp sleep \+                  hello evil spin demo child sysinfo overflow \+                  ifconfig ping host fetch httpd random getty tile \+                  forktest cowtest mmaptest sigtest threads echoserver ptytest nbtest \+                  fputest pathtest libctest coretest unixtest \+                  rawabi vdsotest piehello piethreads dynhello remaptest++# ---- Toolchain pins (downloaded into cache/, checked by SHA256) -----------+MUSL_VERSION   ?= 1.2.5
@@ -0,0 +1,45 @@+#!/usr/bin/env bash+# Build the kernel's ABI test programs ("fixtures") from the pinned kernel+# source (out/kernel-src/user, exported by build-kernel.sh), with exactly the+# commands the kernel's build.rs uses. Under boot contract v1 the image+# carries them in /bin because the self-test runs them; they are *not* part+# of Foundry OS and their source stays in the kernel repo.+#+# Usage: scripts/build-fixtures.sh KERNEL_SRC OUT_DIR "rust names" "c names" "musl name:kind ..."+set -euo pipefail+SRC=$(realpath "$1")/user OUT=$(realpath -m "$2")+RUST=$3 C=$4 MUSL=$5+mkdir -p "$OUT"+RUSTC=${RUSTC:-rustc}+# Use the kernel's toolchain (rust-toolchain.toml in KERNEL_SRC): run from there.+cd "$SRC/.."+for p in $RUST; do+    "$RUSTC" --edition 2024 --target x86_64-unknown-none \+        -C panic=abort -C opt-level=2 -C debuginfo=0 -C strip=debuginfo \+        -C relocation-model=static -C code-model=large \+        -C "link-arg=-T$SRC/link.ld" -o "$OUT/$p" "$SRC/$p.rs"+done+# No libc at all: raw Linux syscalls, an ordinary static ET_EXEC.+for p in $C; do+    ${CC:-cc} -O2 -static -nostdlib -ffreestanding -fno-stack-protector \+        -fpie -no-pie -Wall -Wextra \+        -Wl,-Ttext-segment=0x8000400000 -Wl,--build-id=none -z noexecstack \+        -o "$OUT/$p" "$SRC/$p.c"+done+# musl (Debian's musl-tools). Rustbelt 0.19.0 loads static-PIE, so "pie"+# is plain `musl-gcc -static-pie` with no tricks; "exec" is the old+# ET_EXEC-at-the-user-base recipe (kept as a fixture for the loader).+MUSL_LIB=/usr/lib/x86_64-linux-musl+for entry in $MUSL; do+    p=${entry%%:*} kind=${entry#*:}+    common=(-O2 -Wall -Wextra -pthread -Wl,--build-id=none)+    case $kind in+        pie)  musl-gcc "${common[@]}" -static-pie "$SRC/$p.c" -o "$OUT/$p" ;;+        dyn)  musl-gcc "${common[@]}" -fPIE -pie "$SRC/$p.c" -o "$OUT/$p" ;;+        exec) musl-gcc "${common[@]}" -static -no-pie -nostartfiles \+                  -Wl,-Ttext-segment=0x8000400000 -Wl,--defsym=_DYNAMIC=0x8000400000 -Wl,--no-relax \+                  $MUSL_LIB/crt1.o $MUSL_LIB/crti.o "$SRC/$p.c" $MUSL_LIB/crtn.o -o "$OUT/$p" ;;+        *) echo "unknown musl kind $kind for $p"; exit 1 ;;+    esac+done+echo "[fixtures] $(ls "$OUT" | wc -l) programs in $OUT"
@@ -0,0 +1,50 @@+#!/usr/bin/env bash+# Build the pinned Rustbelt kernel for Foundry OS, without touching the+# kernel repo's working tree or its target/ directory.+#+#   1. `git archive KERNEL_REV` reads the commit straight from the kernel+#      repo's git object store and unpacks it into out/kernel-src. Nothing in+#      KERNEL_DIR is written.+#   2. cargo builds it there with CARGO_TARGET_DIR=out/kernel-target.+#      (Cargo reads .cargo/config.toml and rust-toolchain.toml from the+#      directory it runs in, so we must cd into the kernel source first.)+#   3. Two boot images are kept: out/kernel/bootimage-headless.bin (runs the+#      self-test, then powers QEMU off) and out/kernel/bootimage.bin+#      (interactive).+#+# Side effect we rely on: the kernel's build.rs also writes its own disk+# images into out/kernel-target: disk.img (the reference for `make compare`)+# and the kernel-owned test disks ext2.img, exfat.img, btrfs.img, which+# `make run`/`make test` attach as drives C:, D:, E: (contract v1, point 5).+#+# Usage: scripts/build-kernel.sh KERNEL_DIR KERNEL_REV OUT_DIR [--export-only]+set -euo pipefail+KERNEL_DIR=$1 KERNEL_REV=$2 OUT=$(realpath -m "$3")+SRC=$OUT/kernel-src TARGET=$OUT/kernel-target DEST=$OUT/kernel++want=$(git -C "$KERNEL_DIR" rev-parse --verify "$KERNEL_REV^{commit}")+if [ "$(cat "$SRC/.foundry-kernel-rev" 2>/dev/null)" != "$want" ]; then+    echo "[kernel] exporting $KERNEL_REV ($want) from $KERNEL_DIR into $SRC"+    rm -rf "$SRC" && mkdir -p "$SRC"+    git -C "$KERNEL_DIR" archive "$want" | tar -x -C "$SRC"+    echo "$want" > "$SRC/.foundry-kernel-rev"+fi+[ "${4:-}" = --export-only ] && exit 0+mkdir -p "$TARGET" "$DEST"+cd "$SRC"+export CARGO_TARGET_DIR=$TARGET+ELF=$TARGET/x86_64-rust_kernel/debug/rustbelt+BOOT=$TARGET/x86_64-rust_kernel/debug/bootimage-rustbelt.bin+for variant in headless interactive; do+    if [ $variant = headless ]; then feat=(--features headless); out=$DEST/bootimage-headless.bin+    else feat=(); out=$DEST/bootimage.bin; fi+    echo "[kernel] cargo bootimage ${feat[*]:-} (target dir $TARGET)"+    cargo build "${feat[@]}"+    # bootimage caches by timestamp; switching features can reuse an old+    # ELF, so touch it (the kernel's scripts/runner.sh does the same).+    touch "$ELF"+    cargo bootimage "${feat[@]}"+    cp "$BOOT" "$out"+done+echo "$want" > "$DEST/rev"+echo "[kernel] done: $DEST/bootimage{,-headless}.bin, test disks in $TARGET"
@@ -0,0 +1,51 @@+#!/usr/bin/env bash+# M1 check: compare our image with the kernel's own disk.img, file by file.+# Both are GPT + FAT32; mtools reads the partition at byte offset 1 MiB+# (`-i image@@1M`), so nothing is mounted.+#+# Prints: the /bin program lists side by side (must be equal), the other+# files (must be equal, byte for byte), and for each program whether the+# bytes are identical. Rust programs built by Cargo are expected to differ+# in bytes from the kernel's single-file rustc builds (Cargo adds crate+# metadata), so only list equality and non-program files fail the check.+#+# Usage: scripts/compare-image.sh OURS KERNELS OUT_DIR+set -euo pipefail+OURS=$1 THEIRS=$2 OUT=$(realpath -m "$3")/compare+export MTOOLS_SKIP_CHECK=1+rm -rf "$OUT" && mkdir -p "$OUT/ours" "$OUT/kernel"+mcopy -s -n -i "$OURS@@1M" '::/*' "$OUT/ours/"+mcopy -s -n -i "$THEIRS@@1M" '::/*' "$OUT/kernel/"++(cd "$OUT/ours/bin" && ls | LC_ALL=C sort) > "$OUT/bin-ours.txt"+(cd "$OUT/kernel/bin" && ls | LC_ALL=C sort) > "$OUT/bin-kernel.txt"+fail=0+echo "== /bin program list: ours $(wc -l < "$OUT/bin-ours.txt"), kernel $(wc -l < "$OUT/bin-kernel.txt")"+if diff -u "$OUT/bin-kernel.txt" "$OUT/bin-ours.txt"; then+    echo "   same list"+else+    echo "   LISTS DIFFER (- kernel, + ours)"; fail=1+fi++echo "== files outside /bin (must be byte-identical)"+(cd "$OUT/ours" && find . -path ./bin -prune -o -type f -print | LC_ALL=C sort) > "$OUT/files-ours.txt"+(cd "$OUT/kernel" && find . -path ./bin -prune -o -type f -print | LC_ALL=C sort) > "$OUT/files-kernel.txt"+if ! diff -u "$OUT/files-kernel.txt" "$OUT/files-ours.txt"; then fail=1; fi+while read -r f; do+    if [ -f "$OUT/kernel/$f" ] && cmp -s "$OUT/ours/$f" "$OUT/kernel/$f"; then echo "   same      $f"+    else echo "   DIFFERENT $f"; fail=1; fi+done < "$OUT/files-ours.txt"++echo "== /bin bytes (Rust programs from rust/ may differ: Cargo vs plain rustc)"+same=0 diff=0+while read -r p; do+    [ -f "$OUT/kernel/bin/$p" ] || continue+    a=$(stat -c %s "$OUT/kernel/bin/$p") b=$(stat -c %s "$OUT/ours/bin/$p")+    if cmp -s "$OUT/kernel/bin/$p" "$OUT/ours/bin/$p"; then+        printf '   same      %-12s %8d bytes\n' "$p" "$a"; same=$((same + 1))+    else+        printf '   different %-12s %8d -> %8d bytes\n' "$p" "$a" "$b"; diff=$((diff + 1))+    fi+done < "$OUT/bin-ours.txt"+echo "== $same identical, $diff different in bytes; extracted trees in $OUT"+[ $fail = 0 ] && echo "[compare] OK: same program list and same non-program files" || { echo "[compare] FAILED"; exit 1; }
@@ -0,0 +1,26 @@+#!/bin/sh+# A one-shot HTTP/1.0 server on stdin/stdout, for QEMU's guestfwd:+#   -netdev user,...,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh+# QEMU runs this once per connection the guest makes to 10.0.2.100:80, with+# the connection as stdin/stdout. No host port is opened, so tests can't+# collide with anything else on the machine.+#   GET /     -> a short greeting+#   GET /big  -> `seq 1 20000` (108894 bytes: exercises TCP flow control)+#   other     -> 404+cr=$(printf '\r')+read -r method path version+while IFS= read -r line; do               # skip the request headers+    [ "$line" = "$cr" ] || [ -z "$line" ] && break+done+reply() { # status, body+    printf 'HTTP/1.0 %s\r\nContent-Type: text/plain\r\nContent-Length: %s\r\nServer: rustbelt-test\r\n\r\n%s' \+        "$1" "$(printf '%s' "$2" | wc -c)" "$2"+}+case "$path" in+    /)    reply "200 OK" "Hello from the host, via QEMU guestfwd!+" ;;+    /big) printf 'HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 108894\r\n\r\n'+          seq 1 20000 ;;+    *)    reply "404 Not Found" "not found+" ;;+esac
@@ -0,0 +1,5 @@+ver+echo hi over hvc0+random -d 20 3+ls /dev+exit 0
@@ -0,0 +1,51 @@+#!/usr/bin/env bash+# Turn the staging directory into a disk image (boot contract v1):+#   * a raw file of IMAGE_MIB MiB,+#   * a GPT partition table (sfdisk) with one partition at sector 2048,+#   * a FAT32 file system in it (mkfs.vfat), filled with mtools.+# mtools (mmd/mcopy) write straight into the image file, so no root and no+# loop-mounting is needed. This is what the kernel's build.rs does.+#+# Usage: scripts/mkimage.sh STAGING IMAGE MIB LABEL "bin order..."+#   The /bin files are copied in the given order (same as the kernel's+#   build.rs), then any other file in STAGING/bin.+set -euo pipefail+STAGING=$1 IMAGE=$2 MIB=$3 LABEL=$4 ORDER=$5+PATH=$PATH:/usr/sbin:/sbin+export MTOOLS_SKIP_CHECK=1++SECTORS=$((MIB * 1024 * 1024 / 512))+START=2048+PART_SECTORS=$((SECTORS - 2 * START))   # 1 MiB free at the end for the backup GPT+PART=$IMAGE.part+rm -f "$PART" "$IMAGE"++# 1. The FAT32 file system as its own file first. -s 1: one sector per+#    cluster (FAT32 needs >= 65525 clusters). -i: a fixed volume serial+#    number, so two builds of the same tree give the same bytes.+mkfs.vfat -F 32 -s 1 -n "$LABEL" -i 466f6e64 -C "$PART" $((PART_SECTORS / 2)) >/dev/null++# Top-level entries (everything except bin/) in sorted order...+for entry in $(cd "$STAGING" && ls -A | LC_ALL=C sort); do+    [ "$entry" = bin ] && continue+    mcopy -s -i "$PART" "$STAGING/$entry" ::/+done+# ...then the programs.+mmd -i "$PART" ::/bin+done_list=" "+for p in $ORDER; do+    [ -f "$STAGING/bin/$p" ] || { echo "[mkimage] missing $STAGING/bin/$p"; exit 1; }+    mcopy -i "$PART" "$STAGING/bin/$p" "::/bin/$p"+    done_list+="$p "+done+for f in $(cd "$STAGING/bin" && ls | LC_ALL=C sort); do+    case "$done_list" in *" $f "*) ;; *) mcopy -i "$PART" "$STAGING/bin/$f" "::/bin/$f" ;; esac+done++# 2. The whole disk: zeros + GPT, then the FAT32 file system in the partition.+truncate -s $((SECTORS * 512)) "$IMAGE"+printf 'label: gpt\nstart=%d, size=%d, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="fat32"\n' \+    $START $PART_SECTORS | sfdisk --quiet "$IMAGE"+dd if="$PART" of="$IMAGE" bs=512 seek=$START conv=notrunc status=none+rm -f "$PART"+echo "[mkimage] $IMAGE: $MIB MiB, GPT + FAT32 '$LABEL', $(ls "$STAGING/bin" | wc -l) programs in /bin"
@@ -0,0 +1,203 @@+#!/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".+#+# Usage: scripts/run-headless.sh IMAGE KERNEL_BOOTIMAGE KERNEL_TARGET OUT_DIR+#   IMAGE             our disk image (drive B:, /mnt/disk0)+#   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:)+#   OUT_DIR           where logs go (out/)+# Env: QEMU_CPU (default max), QEMU_TIMEOUT (default 120 s).+#+# Only the QEMU this script starts is ever killed: QEMU writes its PID to+# OUT_DIR/qemu-headless.pid and the exit trap kills exactly that PID.+set -euo pipefail+cd "$(dirname "$0")/.."            # repo root: QEMU's guestfwd runs scripts/http-reply.sh+IMAGE=$1 IMG=$2 KT=$3 OUT=$4+mkdir -p "$OUT"+for f in "$IMAGE" "$IMG" "$KT/ext2.img" "$KT/exfat.img" "$KT/btrfs.img"; do+    [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; }+done++PIDFILE=$OUT/qemu-headless.pid+rm -f "$PIDFILE"+cleanup() {+    if [ -s "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then+        kill "$(cat "$PIDFILE")" 2>/dev/null || true+    fi+    [ -n "${CURL_PID:-}" ] && kill "$CURL_PID" 2>/dev/null || true+}+trap cleanup EXIT++# Network: QEMU's user-mode NAT. guestfwd runs a tiny test web server for+# 10.0.2.100:80 (and `cat` as an echo server on :7); hostfwd forwards a free+# host port to the guest's port 80, where the self-test's `httpd -n 1`+# waits, and a background curl below fetches its page.+HTTP_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')+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"+NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80"+CURL_OUT=$OUT/headless-curl.txt+rm -f "$CURL_OUT"+( for _ in $(seq 1 80); do+      sleep 1+      curl -s -m 5 -o "$CURL_OUT" "http://127.0.0.1:$HTTP_PORT/" 2>/dev/null && [ -s "$CURL_OUT" ] && break+  done ) &+CURL_PID=$!++# virtio-gpu, virtio-rng, and a virtio console (hvc0) fed from+# scripts/hvc0-session.txt; its output goes to OUT/headless-hvc0.txt.+HVC_OUT=$OUT/headless-hvc0.txt+rm -f "$HVC_OUT"+VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci+    -device virtio-serial-pci -device virtconsole,chardev=hvc+    -chardev "file,id=hvc,path=$HVC_OUT,input-path=scripts/hvc0-session.txt")++LOG=$OUT/headless-serial.log+echo "[run-headless] kernel $IMG"+echo "[run-headless] disk 0 (B:) = $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 \+    -netdev "$NETDEV" -device virtio-net-pci,netdev=net0 \+    "${VIRTIO_DEVICES[@]}" \+    -device isa-debug-exit,iobase=0xf4,iosize=0x04 \+    -serial stdio -display none -no-reboot | tee "$LOG"+code=${PIPESTATUS[0]}+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+    expect=(+        "Rustbelt 0.19.0 (stage 19: vDSO and static PIE) x86_64" # ver+        "[smp] 4 CPUs online"                          # smp.rs started 3 more cores+        "[fpu] "                                       # XSAVE (-cpu max) or FXSAVE (qemu64)+        "B:  /mnt/disk0   fat32"                       # drives+        "C:  /mnt/disk1   ext2"+        "D:  /mnt/disk2   exfat"+        "E:  /mnt/disk3   btrfs"+        "B:/ $ ls"                                     # cd B: changed the prompt+        "readme.txt  scripts/"                         # ls in columns+        "In the Rustbelt shell this disk is drive B:"  # type B:/readme.txt+        "second line"                                  # >> and <+        "       125  todo.txt"                         # cp + ls -l+        '[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3'   # run --trace+        "sleep started in the background"              # start+        "[done] pid 19 (start sleep 1), exit code 0"   # wait+        "C:/ $ ls"                                     # the ext2 disk+        "Hello from the ext2 disk!"                    # cat through a symlink+        "big.txt line 12345"                           # double-indirect block+        "written by Rustbelt"                          # mkdir + create on ext2+        "-rw-r--r--        125  todo.txt"              # cp B: -> C:, ls -l+        "lrwxrwxrwx             hello-link -> hello.txt"+        "drwxr-xr-x      <DIR>  made-here/"+        "D:/ $ ls"                                     # the exFAT disk+        "This file has a long name with spaces in it." # quoted name, other case+        "MixedCase.TXT: exFAT keeps the case"          # cp D: -> C:, cat+        "hi from exFAT"                                # md + echo >, other case+        "       125  todo.txt"                         # cp B: -> D:, ls -l D:/+        "     <DIR>  New Folder/"+        "    340000  numbers.txt"+        "E:/ $ ls -l"                                  # the btrfs disk+        "lrwxrwxrwx             hello-link -> hello.txt"+        "Hello from btrfs! (an inline extent"          # inline, through a symlink+        "btrfs never overwrites a block in place"      # compressed inline extent+        " 120000  360000 2160000 big.txt"              # 17 zlib extents+        "btrfs line 119999"+        "Stage 10: a read-only btrfs driver"           # inside subvolume projects+        "sh: new.txt: Read-only file system"           # writes refused+        "rm: hello.txt: Read-only file system"+        "mkdir: newdir: Read-only file system"+        "[exit code 139]"                              # run evil+        "(overflow) killed: stack overflow"            # user stack guard zone+        "4 CPUs online (this program ran on CPU"       # sysinfo+        "  CPU3 ["+        "[net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3" # net/+        "      inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)" # ifconfig+        "64 bytes from 10.0.2.2: icmp_seq=3"           # ping the slirp gateway+        "3 packets transmitted, 3 received, 0% packet loss"+        "Hello from the host, via QEMU guestfwd!"      # fetch (TCP + HTTP)+        "fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt" # wget -o D:+        "[httpd] served 1 request, bye"                # httpd, reached via hostfwd+        "[virtio] rng: virtio-rng at 00:04.0"          # stage 13: virtio/+        "[virtio] hvc0: virtio-console at 00:05.0"+        "[virtio] gpu: virtio-gpu 2D at 00:03.0, display 1280x800, framebuffer 1024x768"+        "[virtio] fbcon: text console on the framebuffer, 128x48 characters"+        "crw-rw----      <DEV>  fb0"                   # ls -l /dev (Linux modes since stage 19)+        "crw-------      <DEV>  hvc0"+        "tile: 4 windows on 1024x768, console 61x46"   # the tiling demo+        "console: full screen, 128x48 characters"      # tile -r+        "[forktest] all checks passed"                 # stage 14: processes+        "[forktest args] all checks passed"            # exec'd with argv/envp/auxv+        "[cowtest] all checks passed"+        "[mmaptest] all checks passed"+        "[sigtest] all checks passed"+        "(start spin 1000), exit code 143"             # kill %1 (SIGTERM)+        "15) SIGTERM"                                  # kill -l+        "[threads] all checks passed"                  # stage 15: libc plumbing+        "[echoserver] all checks passed"+        "[ptytest] all checks passed"+        "[nbtest] all checks passed"+        "[fputest] all checks passed"                  # stage 15.1: musl prerequisites+        "[pathtest] all checks passed"+        "rawabi: uname: Rustbelt 0.19.0"               # a no-libc gcc binary+        "rawabi: all checks passed"+        "sh: nosuchprogram: command not found"+        "cd: there is no drive Q:"+        "A:/tmp $ pwd"+    )+    missing=0+    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` -> 44 (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 44 3 4 20000; 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+        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+    fi+    # The other direction: the host fetched a page from httpd in the guest.+    if grep -qF "Hello from Rustbelt! Rustbelt 0.19.0" "$CURL_OUT" 2>/dev/null; then+        echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")"+    else+        echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1+    fi+    # random -n 8: 16 hex digits; random -d 6 5: five numbers 1-6.+    grep -qxE -- "[0-9a-f]{16}" "$LOG" || { echo "[run-headless] MISSING: random -n 8 output"; missing=1; }+    grep -qxE -- "[1-6]( [1-6]){4}" "$LOG" || { echo "[run-headless] MISSING: random -d 6 5 output"; missing=1; }+    # hvc0 is a tty now: its output turns "\n" into "\r\n" (ONLCR), so+    # drop the '\r's before comparing whole lines.+    tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT"+    # The shell on hvc0 ran the host's commands and answered over hvc0.+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.19.0 (stage 19" \+                "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do+        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; }+    [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+12 expected shell and hvc0 lines found" || code=35+fi++# isa-debug-exit: QEMU exit status = (value << 1) | 1, so Success(0x10) -> 33.+case $code in+    33)  echo "[run-headless] kernel exited OK"; exit 0 ;;+    35)  echo "[run-headless] kernel reported FAILURE"; exit 1 ;;+    124) echo "[run-headless] timed out (kernel hung?)"; exit 124 ;;+    *)   echo "[run-headless] QEMU exited with $code"; exit "$code" ;;+esac
@@ -0,0 +1,27 @@+#!/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)+#+# 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=()+[ -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 \+    -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 \+    -device virtio-serial-pci -device virtconsole,chardev=hvc \+    -chardev "socket,id=hvc,path=$OUT/hvc0.sock,server=on,wait=off" \+    -device isa-debug-exit,iobase=0xf4,iosize=0x04 \+    -serial stdio "${display[@]}"