1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
#!/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"
}
|