#!/usr/bin/env bash # # scripts/lib.sh — shared config and helpers for the rs-dos scripts. # # Sourced, never executed. Everything here is the single definition of # "how we build and how we boot": the build script, the image assembler, # the QEMU runner and the tests all read their settings from this file. # # Config is environment-overridable (TARGET, PROFILE, SYSTEM_IMG, OVMF, # QEMU, MEM, SMP) so callers and CI can tune it without editing scripts. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" TESTS_DIR="$SCRIPT_DIR/tests" # ---- config --------------------------------------------------------------- TARGET="${TARGET:-x86_64-unknown-uefi}" PROFILE="${PROFILE:-release}" OUT_DIR="${OUT_DIR:-$PROJECT_DIR/target/$TARGET/$PROFILE}" KERNEL="$OUT_DIR/rsdos.efi" SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" QEMU="${QEMU:-qemu-system-x86_64}" MEM="${MEM:-256M}" SMP="${SMP:-1}" # Binaries whose launch surface is a window rather than a terminal # (ADR 0006): staged into /bin/gui, everything else into /bin/cli. GUI_BINS="paint" cd "$PROJECT_DIR" # ---- build ---------------------------------------------------------------- # # The one and only cargo invocation in the repo. Builds the kernel and # every external binary in a single pass so the image can stage both. cargo_flags() { [ "$PROFILE" = "release" ] && printf '%s' "--release" } build_all() { echo "=== build (kernel + bins, $PROFILE) ===" # shellcheck disable=SC2046 # cargo_flags is a flag word, intentionally split cargo build --workspace --target "$TARGET" $(cargo_flags) } # ---- system image --------------------------------------------------------- # stage_image [mksystem args...] — assemble /tmp/system.img if it is stale. # Freshness is decided inside mksystem.sh; an up-to-date image is never # touched, so files written at runtime survive. stage_image() { "$SCRIPT_DIR/mksystem.sh" "$@" } # ---- QEMU ----------------------------------------------------------------- # qemu_args — the common machine every boot shares: OVMF, the system # image as the single SATA drive, 256M q35. Callers append display, # monitor and serial choices. qemu_args() { printf '%s\n' \ -bios "$OVMF" \ -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \ -device ide-hd,drive=disk,bus=ide.0 \ -m "$MEM" \ -machine q35 \ -cpu qemu64 \ -no-reboot \ -smp "$SMP" } # ---- test harness --------------------------------------------------------- # # Every test-*.sh does: # TNAME=disk; source lib.sh; test_boot # waits for the shell # sendkeys "ls /disk" "poweroff" # wait_qemu_exit # Runtime files are namespaced per test so parallel runs cannot collide. TNAME="${TNAME:-rsdos}" QPID="/tmp/rsdos-$TNAME-qpid" SOCK="/tmp/rsdos-$TNAME-qmon.sock" LOG="/tmp/rsdos-$TNAME-serial.log" QLOG="/tmp/rsdos-$TNAME-qemu.log" fail() { echo "FAIL: $1" [ -f "$LOG" ] && tail -40 "$LOG" exit 1 } # wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex. wait_log() { local i=0 while [ "$i" -lt $((${3:-60} * 10)) ]; do grep -q "$2" "$1" 2>/dev/null && return 0 sleep 0.1 i=$((i + 1)) done return 1 } # wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone. wait_qemu_exit() { local i=0 while [ "$i" -lt $((${1:-30} * 10)) ]; do kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 sleep 0.1 i=$((i + 1)) done return 1 } # qemu_hmp CMD — run one HMP command on the monitor socket. qemu_hmp() { python3 - "$SOCK" "$1" <<'EOF' import socket, sys, time s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(sys.argv[1]) s.sendall((sys.argv[2] + "\n").encode()) time.sleep(0.4) EOF } # sendkeys "line" ... — type shell commands into the guest via sendkeys.py. sendkeys() { python3 "$TESTS_DIR/sendkeys.py" --sock "$SOCK" "$@" } # test_cleanup — reap a leftover QEMU from a previous run of this test # and clear this test's runtime files. Called before test_boot. test_cleanup() { kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true rm -f "$SOCK" "$LOG" } # test_on_exit — EXIT trap: never leave a QEMU behind, but keep the logs # (they are the post-mortem for a failed test). test_on_exit() { kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true } # test_boot [extra-qemu-args...] — boot the system image headless with the # serial log at $LOG and a monitor socket at $SOCK, then wait for the # shell. Extra args attach more drives (e.g. a CRASHLOG stick). test_boot() { test_cleanup trap test_on_exit EXIT # A stale/poisoned OVMF varstore makes the firmware skip the # \EFI\BOOT fallback entirely — every test boots from a fresh copy. # Namespaced per test so parallel runs cannot race on it. local nv="/tmp/rsdos-ovmf-vars-$TNAME.fd" rm -f "$nv" cp "$OVMF" "$nv" local -a qargs mapfile -t qargs < <(qemu_args) # qemu_args() already carries -bios $OVMF; put the fresh varstore copy # last so it wins (QEMU uses the last -bios) without duplicating the # whole argument set. "$QEMU" \ "${qargs[@]}" \ -bios "$nv" \ -serial "file:$LOG" \ -monitor "unix:$SOCK,server,nowait" \ -display none \ "$@" >"$QLOG" 2>&1 & echo $! >"$QPID" wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" }