summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 15:53:29 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 15:53:29 +0100
commit7958276c9f0d230053520ba20f2a00f5ecda18e4 (patch)
tree596f5f71ad597ed951bae98de818cb37f171a18d
parentfs: /bin alias for the system volume (diff)
downloadrs-dos-7958276c9f0d230053520ba20f2a00f5ecda18e4.tar.gz
rs-dos-7958276c9f0d230053520ba20f2a00f5ecda18e4.zip
scripts: rebuild the dev loop around build.sh/run.sh
`make` did not parse at all (missing separator at line 139), so every make target was dead. The folder also carried three overlapping launchers (boot.sh, qemu.sh, boot-image.sh) plus a fourth partial one in the Makefile, `cargo build --package rsdos` copy-pasted eight times, the QEMU invocation copy-pasted per script, and the test helpers (fail/wait_log/wait_qemu_exit/qemu_hmp) duplicated in five tests. New layout: - scripts/lib.sh: single config (image, OVMF, mem/smp), the one cargo invocation (build_all), the one QEMU shape (qemu_args), and the test harness (test_boot, wait_log, wait_qemu_exit, qemu_hmp, sendkeys) - scripts/build.sh: kernel + every bins/* in one pass - scripts/mksystem.sh: freshness-aware image build that now stages every bins/*.efi into /bin/cli|gui (they were never copied before) - scripts/run.sh: build + stage + boot in one command; --headless, --debug (GDB stub), --monitor (HMP socket), --fresh, --mem/--smp/ --bios/--image; on Wayland it auto-selects XWayland plus grab-on-hover so the mouse can be captured (Ctrl+Alt+G toggles) - scripts/tests/: the five automated tests on the shared harness - deleted: boot.sh, qemu.sh, boot-image.sh, lib-boot-args.sh, build-image-release.sh, build-bootloader.sh (rsboot chain dropped) The Makefile is now a thin wrapper: make run, make test, make system, make inspect, make help. Two-command flow: ./scripts/build.sh then ./scripts/run.sh (run.sh alone also works). Stale test assumptions fixed so the suite is green again: test-gfx waited for "gfx: check" while the shell prints "gfx check: PASS"; test-mouse/test-msg hardcoded msgrecv = task 3 (wm-input took slot 3 — ids are parsed from the log now); the queue-full probe targeted task 0, whose exec pump drains its own queue as completion codes (moved to the cursor task, which never recvs). Verified in QEMU: make test -> test-disk, test-gfx, test-mouse, test-msg, test-fault all PASS.
-rw-r--r--.gitignore1
-rw-r--r--CONTEXT.md51
-rw-r--r--Makefile217
-rw-r--r--docs/adr/0002-keyword-redirection-and-post-filters.md2
-rw-r--r--docs/plan-executable-binaries.md4
-rwxr-xr-xscripts/boot-image.sh65
-rwxr-xr-xscripts/boot.sh90
-rwxr-xr-xscripts/build-bootloader.sh35
-rwxr-xr-xscripts/build-image-release.sh30
-rwxr-xr-xscripts/build.sh49
-rwxr-xr-xscripts/debug-kernel.py16
-rw-r--r--scripts/lib-boot-args.sh38
-rw-r--r--scripts/lib.sh168
-rwxr-xr-xscripts/mksystem.sh144
-rwxr-xr-xscripts/qemu.sh99
-rwxr-xr-xscripts/run.sh142
-rwxr-xr-xscripts/test-disk.sh131
-rwxr-xr-xscripts/test-fault.sh85
-rwxr-xr-xscripts/test-gfx.sh84
-rwxr-xr-xscripts/test-mouse.sh125
-rwxr-xr-xscripts/test-msg.sh140
-rwxr-xr-xscripts/tests/sendkeys.py (renamed from scripts/sendkeys.py)12
-rwxr-xr-xscripts/tests/test-disk.sh67
-rwxr-xr-xscripts/tests/test-fault.sh47
-rwxr-xr-xscripts/tests/test-focus.py (renamed from scripts/test-focus.py)7
-rwxr-xr-xscripts/tests/test-gfx.sh37
-rwxr-xr-xscripts/tests/test-mouse.sh83
-rwxr-xr-xscripts/tests/test-msg.sh98
28 files changed, 897 insertions, 1170 deletions
diff --git a/.gitignore b/.gitignore
index ae44880..1ca16c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
3/esp/ 3/esp/
4*.img 4*.img
5NvVars 5NvVars
6__pycache__/
6/.pi-subagents/ 7/.pi-subagents/
diff --git a/CONTEXT.md b/CONTEXT.md
index 01f5c69..ec35e26 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -135,7 +135,10 @@ The single dd-able FAT filesystem image the build produces: bootloader,
135kernel, /bin, /etc and seed content on one volume. Imaging it onto a disk 135kernel, /bin, /etc and seed content on one volume. Imaging it onto a disk
136yields a complete installed system. Binaries live on the system image 136yields a complete installed system. Binaries live on the system image
137under /bin/cli and /bin/gui; the shell finds them by scanning mounted 137under /bin/cli and /bin/gui; the shell finds them by scanning mounted
138volumes — no volume is privileged. 138volumes — no volume is privileged. For user-facing paths, /bin is a
139transparent VFS alias: `ls /bin/cli` resolves to the first mounted
140volume whose root has \bin, so nobody needs /disk/<UID>/bin/... (ADR
1410006).
139 142
140## Loader 143## Loader
141 144
@@ -194,7 +197,18 @@ reaped) without taking the kernel down.
194 197
195Standalone project — no integration with the RNG initramfs build system. 198Standalone project — no integration with the RNG initramfs build system.
196Testing via QEMU with OVMF. The kernel PE binary is placed on a FAT 199Testing via QEMU with OVMF. The kernel PE binary is placed on a FAT
197image as `\bzImage` (matching rsbl's expected path). 200image as `\bzImage` (matching rsbl's expected path) and also as
201`\EFI\BOOT\BOOTX64.EFI` (what the firmware boots directly).
202
203Two commands, compile + boot:
204
205```sh
206./scripts/build.sh # cargo build --workspace --target x86_64-unknown-uefi --release
207./scripts/run.sh # stage /tmp/system.img if stale, then boot QEMU
208```
209
210(`./scripts/run.sh` alone does both; `--headless`, `--debug`, `--monitor`,
211`--fresh` as per its `--help`. `make` / `make run` are thin aliases.)
198 212
199## Versioning 213## Versioning
200 214
@@ -397,20 +411,35 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target:
397 411
398 The whole disk workflow is scripted (no manual mkfs/mcopy): 412 The whole disk workflow is scripted (no manual mkfs/mcopy):
399 413
400- `make disk` / `./scripts/mkdisk.sh` — create the FAT32 image with 414- `./scripts/build.sh` — the single cargo invocation: kernel +
401 `mkfs.fat -C` (the flag that actually creates the file; plain 415 every `/bin` binary (`bins/*.efi`), release profile by default
402 `mkfs.fat` needs an existing file), label `RSDOS`, seed from 416- `./scripts/mksystem.sh` / `make system` — assemble the dd-able
403 `seed/` via mtools; `--fresh` re-formats, `--size 128M` sizes it 417 FAT32 image (`mkfs.fat -C`, label `RSDOS`, seed from `seed/` via
418 mtools, stage `/bin/cli` + `/bin/gui`); rebuilt only when a
419 kernel/binary/seed file is newer than it, `--force` wipes runtime
420 data, `--gpt` wraps it for USB sticks
421- `./scripts/run.sh` / `make run` — build + stage + boot QEMU
422 (`--headless`, `--debug` GDB stub, `--monitor` HMP socket,
423 `--fresh`, `--mem/--smp/--bios/--image`); the two-command flow is
424 `./scripts/build.sh && ./scripts/run.sh`
404- `make inspect` — `mdir -i` listing of the image (mtools) 425- `make inspect` — `mdir -i` listing of the image (mtools)
405- `make test-disk` / `./scripts/test-disk.sh` — full automated round 426- `make test` — all automated round trips; each boots QEMU headless,
406 trip: build → deploy → ensure image → boot QEMU headless → drive 427 drives the shell via `sendkeys.py` through the monitor and asserts
428 on the serial log
429- `./scripts/tests/test-disk.sh` / `make test-disk` — full automated
430 round trip: build → stage → boot QEMU headless → drive
407 the shell via `sendkey` through the monitor (`ls /disk`, `mkdir`, 431 the shell via `sendkey` through the monitor (`ls /disk`, `mkdir`,
408 `echo WRITING`, `cat`, `poweroff`) → fsck-clean check → `mtype` verifies 432 `echo WRITING`, `cat`, `poweroff`) → fsck-clean check → `mtype` verifies
409 the written file survived poweroff 433 the written file survived poweroff
410- `./scripts/sendkeys.py` — types lines into the guest via the QEMU 434- `./scripts/tests/sendkeys.py` — types lines into the guest via the QEMU
411 monitor (`--sock`, US layout, named keys for punctuation; bare 435 monitor (`--sock`, US layout, named keys for punctuation; bare
412 `-`/`.`/`/` are not valid sendkey names) 436 `-`/`.`/`/` are not valid sendkey names)
413 437
438 Shared config and helpers (image path, OVMF, QEMU flags, `build_all`,
439 `stage_image`, `test_boot`, `wait_log`, `fail`) live in
440 `scripts/lib.sh` — every script sources it; there is exactly one
441 `cargo build` and one QEMU invocation shape in the tree.
442
414✅ **`klog` kernel log ring** — a RAM ring of the last 128 serial log 443✅ **`klog` kernel log ring** — a RAM ring of the last 128 serial log
415 lines (128 chars each, ~16 KB), fed by `serial_println!` (one choke 444 lines (128 chars each, ~16 KB), fed by `serial_println!` (one choke
416 point — every boot/low-level message captured for free). Sequence 445 point — every boot/low-level message captured for free). Sequence
@@ -806,7 +835,7 @@ Resolved this session:
806 see intermediate steps of the filtering. 835 see intermediate steps of the filtering.
807- **`> >> < <<` are gone.** Keywords are the *only* redirection syntax; 836- **`> >> < <<` are gone.** Keywords are the *only* redirection syntax;
808 `>` is now ordinary text (`echo a > b` prints `a > b`). Collateral: 837 `>` is now ordinary text (`echo a > b` prints `a > b`). Collateral:
809 `scripts/test-disk.sh` and the `help` text must be rewritten to the 838 `scripts/tests/test-disk.sh` and the `help` text must be rewritten to the
810 keyword syntax; the milestone bullets above describing `echo >`/`>>` 839 keyword syntax; the milestone bullets above describing `echo >`/`>>`
811 record the old behavior (superseded by these notes). 840 record the old behavior (superseded by these notes).
812- **Quoting (C): `"..."` groups + literalizes; `\x` escapes one char.** 841- **Quoting (C): `"..."` groups + literalizes; `\x` escapes one char.**
@@ -888,7 +917,7 @@ Everything above is now implemented in `shell/src/lib.rs`:
888- **^D** — EOF everywhere, at the prompt it is a no-op (fresh 917- **^D** — EOF everywhere, at the prompt it is a no-op (fresh
889 prompt); ^C keeps line-cancel / stream-abort; `poweroff`/`exit` 918 prompt); ^C keeps line-cancel / stream-abort; `poweroff`/`exit`
890 are the only shutdown verbs. 919 are the only shutdown verbs.
891- **Collateral** — `help` text rewritten; `scripts/test-disk.sh` and 920- **Collateral** — `help` text rewritten; `scripts/tests/test-disk.sh` and
892 the `sendkeys.py` docstring updated to keyword syntax. 921 the `sendkeys.py` docstring updated to keyword syntax.
893 922
894Bugs found by QEMU verification and fixed: 923Bugs found by QEMU verification and fixed:
diff --git a/Makefile b/Makefile
index d18a78a..ee9755f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,176 +1,93 @@
1# shellcheck shell=sh disable=SC2046,SC2091,SC2086,SC2155 1# Thin wrapper over scripts/ — all real logic lives in scripts/*.sh, with
2# shared config and helpers in scripts/lib.sh. `make help` lists targets.
3#
2# NOTE: $(VAR) in this file is MAKE syntax, not shell substitution. 4# NOTE: $(VAR) in this file is MAKE syntax, not shell substitution.
3export TARGET=x86_64-unknown-uefi 5
4export PROFILE=release 6SYSTEM_IMG ?= /tmp/system.img
5export OUT_DIR=target/$(TARGET)/$(PROFILE) 7
6export ESP_DIR=esp 8.PHONY: all build run run-headless debug system system-fresh inspect \
7export KERNEL=$(OUT_DIR)/rsdos.efi 9 test test-disk test-fault test-gfx test-mouse test-msg fmt clean help
8export BZIMAGE=$(ESP_DIR)/bzImage 10
9export INITRD=$(ESP_DIR)/initramfs.cpio.gz 11# ---- build ----------------------------------------------------------------
10export BOOTLOADER=rsboot/rsboot.efi 12
11export OVMF=/usr/share/edk2/x64/OVMF.4m.fd 13all: build
12export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI 14
13# Attach the single dd-able system image as the SATA drive (ide.0). 15build:
14# Create/re-create with: make system-fresh (or: ./scripts/mksystem.sh --force) 16 ./scripts/build.sh
15export SYSTEM_IMG ?= /tmp/system.img 17
16 18# ---- boot -----------------------------------------------------------------
17 19
18# ---- Build --------------------------------------------------------------- 20# Build + stage the image + boot QEMU (window + serial on stdio).
19 21run:
20.PHONY: all run run-gui clean image debug release fmt bootloader boot-image system system-fresh inspect test-disk test-mouse test-gfx test-fault test-msg 22 ./scripts/run.sh
21 23
22release: PROFILE=release 24# Serial console only, no window.
23release: export PROFILE=release 25run-headless:
24release: export OUT_DIR=target/$(TARGET)/release 26 ./scripts/run.sh --headless
25release: CARGO_FLAGS=--release 27
26release: all 28# GDB stub on :1234, CPU paused (attach: gdb -x scripts/debug-kernel.py).
27 29debug:
28# CARGO_FLAGS set by PROFILE above 30 ./scripts/run.sh --debug
29CARGO_FLAGS=--release 31
30 32# ---- system image ---------------------------------------------------------
31$(OUT_DIR)/rsdos.efi: $(shell find rsnix shell rsdos -name '*.rs') Cargo.toml rsnix/Cargo.toml shell/Cargo.toml rsdos/Cargo.toml 33
32 cargo build --package rsdos --target $(TARGET) $(CARGO_FLAGS) 34# Stage the image if it is stale (runtime files survive).
33
34$(BZIMAGE): $(KERNEL) | $(ESP_DIR)
35 cp "$<" "$@"
36
37$(ESP_DIR):
38 mkdir -p $(ESP_DIR)
39
40$(INITRD): | $(ESP_DIR)
41 printf '' | gzip > "$@"
42
43# ---- QEMU runners --------------------------------------------------------
44
45run: all system
46 qemu-system-x86_64 \
47 -bios $(OVMF) \
48 -drive file=$(SYSTEM_IMG),format=raw,if=none,id=disk \
49 -device ide-hd,drive=disk,bus=ide.0 \
50 -serial stdio \
51 -m 256M \
52 -machine q35 \
53 -cpu qemu64 \
54 -no-reboot \
55 -display none
56
57run-gui: all system
58 qemu-system-x86_64 \
59 -bios $(OVMF) \
60 -drive file=$(SYSTEM_IMG),format=raw,if=none,id=disk \
61 -device ide-hd,drive=disk,bus=ide.0 \
62 -serial stdio \
63 -m 256M \
64 -machine q35 \
65 -cpu qemu64 \
66 -no-reboot \
67 -vga std
68
69# GDB stub on :1234
70debug: all system
71 qemu-system-x86_64 \
72 -bios $(OVMF) \
73 -drive file=$(SYSTEM_IMG),format=raw,if=none,id=disk \
74 -device ide-hd,drive=disk,bus=ide.0 \
75 -serial stdio \
76 -m 256M \
77 -machine q35 \
78 -cpu qemu64 \
79 -no-reboot \
80 -display none \
81 -s -S
82
83# Boot from a raw disk image (delegates to boot-image.sh which builds the
84# full chain if the image doesn't exist yet)
85run-image: all
86
87# ---- System image --------------------------------------------------------
88
89# One dd-able FAT32 image: kernel + ESP + /etc + /bin + seed data.
90# Stamp rule: rebuilt only when the kernel or this script changes — an
91# up-to-date image is NEVER touched, so files written at runtime survive.
92system: 35system:
93 @if [ ! -f "$(SYSTEM_IMG)" ] || [ "$$(stat -c %Y $(BZIMAGE) 2>/dev/null || echo 0)" -gt "$$(stat -c %Y $(SYSTEM_IMG) 2>/dev/null || echo 0)" ]; then \ 36 ./scripts/mksystem.sh
94 ./scripts/mksystem.sh --force "$(SYSTEM_IMG)"; \
95 else \
96 echo "system image up to date: $(SYSTEM_IMG)"; \
97 fi
98 37
99# Re-create the system image from scratch (wipes runtime data). 38# Re-create the image from scratch (wipes runtime data).
100system-fresh: 39system-fresh:
101 ./scripts/mksystem.sh --force "$(SYSTEM_IMG)" 40 ./scripts/mksystem.sh --force
102 41
103# List the system image contents with mtools (requires mtools). 42# List the image contents (needs mtools: sudo apt install mtools).
104inspect: 43inspect:
105 @command -v mdir >/dev/null 2>&1 || (echo "inspect needs mtools: sudo apt install mtools"; exit 1)
106 @mdir -i "$(SYSTEM_IMG)" :: 44 @mdir -i "$(SYSTEM_IMG)" ::
107 45
108# Full automated round trip: build -> boot -> shell commands -> poweroff 46# ---- tests ----------------------------------------------------------------
109# -> fsck + mtools verification.
110test-disk:
111 ./scripts/test-disk.sh
112 47
113# Automated PS/2 mouse round trip: boot -> inject mouse_move/mouse_button 48test: test-disk test-gfx test-mouse test-msg test-fault
114# via the monitor -> verify /dev/mouse + the `mouse` stream -> poweroff.
115test-mouse:
116 ./scripts/test-mouse.sh
117 49
118test-gfx: 50test-disk:
119 ./scripts/test-gfx.sh 51 ./scripts/tests/test-disk.sh
120 52
121# Fault-diagnostics round trip: boot -> `fault` (deliberate page fault) ->
122# verify the full dump (vec/err/rip/cr2/task) hits serial + screen -> halt.
123test-fault: 53test-fault:
124 ./scripts/test-fault.sh 54 ./scripts/tests/test-fault.sh
125
126# Automated scheduler message-queue round trip: msgrecv/send, real-input
127# routing into a task's queue, queue-full boundary.
128test-msg:
129 ./scripts/test-msg.sh
130
131# ---- Full boot chain -----------------------------------------------------
132 55
133# Build bootloader only (rsboot submodule) 56test-gfx:
134bootloader: 57 ./scripts/tests/test-gfx.sh
135 ./scripts/build-bootloader.sh
136 58
137# Build the full boot chain disk image (kernel + bootloader) 59test-mouse:
138image: all 60 ./scripts/tests/test-mouse.sh
139./scripts/mksystem.sh --chain rs-dos.img
140 61
141# Boot the full chain disk image (builds it first if needed) 62test-msg:
142boot-image: all 63 ./scripts/tests/test-msg.sh
143 ./scripts/boot-image.sh rs-dos.img
144 64
145# ---- Utilities ----------------------------------------------------------- 65# ---- utilities ------------------------------------------------------------
146 66
147fmt: 67fmt:
148 cargo fmt 68 cargo fmt
149 69
150clean: 70clean:
151 cargo clean 71 cargo clean
152 rm -rf $(ESP_DIR)
153 72
154help: 73help:
155 @echo "rs-dos — Make targets" 74 @echo "raisernics — make targets"
156 @echo "" 75 @echo ""
157 @echo " make build kernel + ESP (default)" 76 @echo " make build kernel + /bin binaries (release)"
158 @echo " make release build with --release (smaller binary)" 77 @echo " make run build + stage image + boot QEMU (window)"
159 @echo " make bootloader build rsboot from source" 78 @echo " make run-headless same, serial console only"
160 @echo " make image build full chain disk image (bootloader + kernel)" 79 @echo " make debug same, GDB stub on :1234 (CPU paused)"
161 @echo " make boot-image build + boot full chain via QEMU" 80 @echo " make system stage the system image if stale"
162 @echo " make run QEMU serial console (direct boot, no bootloader)" 81 @echo " make system-fresh re-create the system image (wipes data)"
163 @echo " make system create the system image if stale/missing (/tmp/system.img)" 82 @echo " make inspect list image contents (mtools)"
164 @echo " make system-fresh re-create the system image (wipes runtime data)" 83 @echo " make test run every automated test"
165 @echo " make inspect list system image contents (mtools)" 84 @echo " make test-disk disk round trip (build -> boot -> fsck)"
166 @echo " make test-disk automated disk round trip (build->boot->fsck)" 85 @echo " make test-gfx graphics self-test"
167 @echo " make clean remove build artifacts" 86 @echo " make test-mouse PS/2 mouse + cursor tracking"
87 @echo " make test-msg scheduler message queues"
88 @echo " make test-fault fault dump (screen + serial)"
168 @echo " make fmt format Rust source" 89 @echo " make fmt format Rust source"
169 @echo " ./scripts/mksystem.sh build the dd-able system image" 90 @echo " make clean remove build artifacts"
170 @echo " ./scripts/boot.sh quick direct boot (dev loop)" 91 @echo ""
171 @echo " ./scripts/test-disk.sh automated disk round trip" 92 @echo "Two-command flow: ./scripts/build.sh && ./scripts/run.sh"
172 @echo " ./scripts/sendkeys.py type into QEMU via the monitor" 93 @echo "Scripts: build.sh run.sh mksystem.sh lib.sh tests/"
173 @echo " ./scripts/boot-image.sh boot full chain (builds if needed)"
174 @echo " ./scripts/build-bootloader.sh build rsboot from source"
175 @echo " ./scripts/qemu.sh flexible QEMU launcher"
176 @echo " ./scripts/debug-kernel.py GDB plugin (attach after 'make debug')"
diff --git a/docs/adr/0002-keyword-redirection-and-post-filters.md b/docs/adr/0002-keyword-redirection-and-post-filters.md
index 1584077..227ce2c 100644
--- a/docs/adr/0002-keyword-redirection-and-post-filters.md
+++ b/docs/adr/0002-keyword-redirection-and-post-filters.md
@@ -65,7 +65,7 @@ cmd args [USE src] [TIL marker] [filters…] [output kw dst]
65 -t 10`) is expressible without a pipe; ^C flushes the held-back 65 -t 10`) is expressible without a pipe; ^C flushes the held-back
66 window instead of losing it. 66 window instead of losing it.
67- **Negative:** Breaks existing muscle memory and tooling: POSIX users 67- **Negative:** Breaks existing muscle memory and tooling: POSIX users
68 must learn the keywords; `scripts/test-disk.sh` and the `help` text 68 must learn the keywords; `scripts/tests/test-disk.sh` and the `help` text
69 must be rewritten from `echo >` to the keyword syntax. 69 must be rewritten from `echo >` to the keyword syntax.
70- **Negative:** Keywords are reserved, so printing text like `WRITING` 70- **Negative:** Keywords are reserved, so printing text like `WRITING`
71 requires quoting; unknown commands cannot be filtered with flags 71 requires quoting; unknown commands cannot be filtered with flags
diff --git a/docs/plan-executable-binaries.md b/docs/plan-executable-binaries.md
index cd6f1d0..138b8e0 100644
--- a/docs/plan-executable-binaries.md
+++ b/docs/plan-executable-binaries.md
@@ -11,7 +11,7 @@ the glossary rules.
11The build produces one dd-able FAT32 image instead of the current 11The build produces one dd-able FAT32 image instead of the current
12two-source setup (vvfat `esp/` directory + separate `/tmp/disk.img`). 12two-source setup (vvfat `esp/` directory + separate `/tmp/disk.img`).
13 13
14- [ ] Extend `scripts/mkdisk.sh` (or add `scripts/mksystem.sh`) to create 14- [x] `scripts/mksystem.sh` creates
15 `system.img`: `mkfs.fat -C`, then `mcopy` in 15 `system.img`: `mkfs.fat -C`, then `mcopy` in
16 `\bzImage`, `\EFI\` (as rsbl requires), `\etc\*` (motd moves out of 16 `\bzImage`, `\EFI\` (as rsbl requires), `\etc\*` (motd moves out of
17 the `fs.rs` code literal? — optional here), seed content. 17 the `fs.rs` code literal? — optional here), seed content.
@@ -97,7 +97,7 @@ is absent; **version bump to 0.6.0**.
97- [ ] Migrate `cat` last (input consumer of every corner: heredoc, USE, 97- [ ] Migrate `cat` last (input consumer of every corner: heredoc, USE,
98 TIL sugar, multi-path). 98 TIL sugar, multi-path).
99 99
100**Gate:** full `scripts/test-disk.sh` grammar suite passes unchanged with 100**Gate:** full `scripts/tests/test-disk.sh` grammar suite passes unchanged with
101external binaries; `-h/-t/-g/-p` chains work across the task boundary; 101external binaries; `-h/-t/-g/-p` chains work across the task boundary;
102^C flushes a tail window on an infinite stream. 102^C flushes a tail window on an infinite stream.
103 103
diff --git a/scripts/boot-image.sh b/scripts/boot-image.sh
deleted file mode 100755
index f70dcd4..0000000
--- a/scripts/boot-image.sh
+++ /dev/null
@@ -1,65 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/boot-image.sh — boot rs-dos full chain disk image in QEMU
4#
5# Builds the image first if needed, then boots with UEFI firmware through
6# the real boot chain: firmware → rsboot → rsnix kernel.
7#
8# Usage:
9# ./scripts/boot-image.sh [--gui] [--debug] [--mem 512M] [--smp 2]
10#
11# Keybindings:
12# Ctrl-A X — quit
13# Ctrl-A C — QEMU monitor
14#
15# Environment:
16# OVMF path to OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd)
17# MEM memory size (default: 256M)
18# SMP CPU count (default: 1)
19#
20set -euo pipefail
21
22SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
24cd "$PROJECT_DIR"
25
26IMAGE="${1:-rs-dos.img}"
27
28# ---- config ----
29OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
30MEM="${MEM:-256M}"
31SMP="${SMP:-1}"
32GUI=""
33DEBUG=""
34
35# ---- args ----
36source "$SCRIPT_DIR/lib-boot-args.sh"
37parse_boot_args "$@"
38
39# ---- build image if missing ----
40if [ ! -f "$IMAGE" ]; then
41 echo "Image not found, building..."
42 "$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE"
43fi
44
45# ---- boot ----
46echo ""
47echo "=== Booting (full chain: firmware → rsboot → rsnix) ==="
48echo " image: $IMAGE"
49echo " ovmf: $OVMF"
50echo " mem: $MEM smp: $SMP"
51echo " gui: $([ -n "$GUI" ] && echo yes || echo no)"
52echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)"
53echo ""
54
55exec qemu-system-x86_64 \
56 -bios "$OVMF" \
57 -drive file="$IMAGE",format=raw,if=none,id=hd \
58 -device ide-hd,drive=hd \
59 -serial stdio \
60 -m "$MEM" \
61 -machine q35 \
62 -cpu qemu64 \
63 -no-reboot \
64 -smp "$SMP" \
65 $GUI $DEBUG
diff --git a/scripts/boot.sh b/scripts/boot.sh
deleted file mode 100755
index cb017d8..0000000
--- a/scripts/boot.sh
+++ /dev/null
@@ -1,90 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/boot.sh — quick direct boot in QEMU (dev loop)
4#
5# Builds the kernel, stages the system image, and boots directly as
6# BOOTX64.EFI via QEMU's FAT driver. One image = one drive.
7#
8# Usage:
9# ./scripts/boot.sh [--gui] [--debug] [--release] [--mem 512M] [--smp 2]
10#
11# Boot chain: firmware → rsnix (direct)
12# Full chain: use ./scripts/boot-image.sh (firmware → rsboot → rsnix)
13#
14# Keybindings:
15# Ctrl-A X — quit
16# Ctrl-A C — QEMU monitor
17#
18# Environment:
19# OVMF path to OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd)
20# MEM memory size (default: 256M)
21# SMP CPU count (default: 1)
22# SYSTEM_IMG system image path (default: /tmp/system.img)
23#
24set -euo pipefail
25
26SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
28cd "$PROJECT_DIR"
29
30# ---- config ----
31OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
32MEM="${MEM:-256M}"
33SMP="${SMP:-1}"
34SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
35PROFILE="release"
36
37GUI=""
38DEBUG=""
39
40# ---- args ----
41source "$SCRIPT_DIR/lib-boot-args.sh"
42handle_arg() {
43 case "$1" in
44 --debug)
45 PROFILE="debug"
46 CARGO_FLAGS=""
47 ;;
48 *)
49 echo "unknown: $1"
50 exit 1
51 ;;
52 esac
53}
54parse_boot_args "$@"
55
56OUT_DIR="target/x86_64-unknown-uefi/$PROFILE"
57
58# ---- build ----
59echo "=== Building kernel ($PROFILE) ==="
60if [ "$PROFILE" = "release" ]; then
61 cargo build --release --package rsdos --target x86_64-unknown-uefi
62else
63 cargo build --package rsdos --target x86_64-unknown-uefi
64fi
65
66# ---- stage system image ----
67echo "=== Staging system image ==="
68"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
69echo " system image: $SYSTEM_IMG"
70
71# ---- boot ----
72echo ""
73echo "=== Booting (direct: firmware → rsnix) ==="
74echo " ovmf: $OVMF"
75echo " mem: $MEM smp: $SMP"
76echo " gui: $([ -n "$GUI" ] && echo yes || echo no)"
77echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)"
78echo ""
79
80exec qemu-system-x86_64 \
81 -bios "$OVMF" \
82 -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \
83 -device ide-hd,drive=disk,bus=ide.0 \
84 -serial stdio \
85 -m "$MEM" \
86 -machine q35 \
87 -cpu qemu64 \
88 -no-reboot \
89 -smp "$SMP" \
90 $GUI $DEBUG
diff --git a/scripts/build-bootloader.sh b/scripts/build-bootloader.sh
deleted file mode 100755
index f0f0ce7..0000000
--- a/scripts/build-bootloader.sh
+++ /dev/null
@@ -1,35 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/build-bootloader.sh — build rsboot (UEFI bootloader)
4#
5# rsboot is a git submodule at rsboot/. This builds it and places
6# BOOTX64.EFI into build/bootloader/.
7#
8# Usage:
9# ./scripts/build-bootloader.sh
10#
11# Output:
12# build/bootloader/BOOTX64.EFI
13#
14set -euo pipefail
15
16SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
18BUILD_DIR="$PROJECT_DIR/build"
19BOOTLOADER_DIR="$BUILD_DIR/bootloader"
20
21echo "=== Building rsboot (UEFI bootloader) ==="
22
23# Ensure submodule is checked out, tracking the latest upstream HEAD
24# (--remote fetches and checks out the newest main instead of reverting
25# to the commit recorded in the superproject's gitlink).
26cd "$PROJECT_DIR"
27git submodule update --init --remote rsboot
28
29cd "$PROJECT_DIR/rsboot"
30make clean 2>/dev/null || true
31make -j"$(nproc)" 2>&1 | sed 's/^/ /'
32
33echo ""
34echo "=== rsboot build complete ==="
35ls -lh "$BOOTLOADER_DIR/BOOTX64.EFI"
diff --git a/scripts/build-image-release.sh b/scripts/build-image-release.sh
deleted file mode 100755
index e4c5250..0000000
--- a/scripts/build-image-release.sh
+++ /dev/null
@@ -1,30 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/build-image-release.sh — build the release GPT disk image
4#
5# Produces a single dd-able system image with the release build of
6# the kernel, rsboot bootloader, and all binaries in /bin/.
7#
8# Usage:
9# ./scripts/build-image-release.sh [output.img]
10#
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15cd "$PROJECT_DIR"
16
17IMAGE="${1:-rs-dos.img}"
18
19echo "=== Building release kernel ==="
20cargo build --release --package rsdos --target x86_64-unknown-uefi
21
22echo "=== Building system image (chain + GPT) ==="
23"$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE"
24
25echo ""
26echo "=== Done: $IMAGE ==="
27ls -lh "$IMAGE"
28echo ""
29echo "Boot with: ./scripts/boot-image.sh --gui"
30echo "USB: sudo dd if=$IMAGE of=/dev/sdX bs=4M status=progress"
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..71430a4
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,49 @@
1#!/usr/bin/env bash
2#
3# scripts/build.sh — compile the kernel and every external binary.
4#
5# The only cargo invocation in the project. One pass builds the kernel
6# (rsdos.efi) and all /bin binaries (bins/*.efi), which is exactly what
7# the system image stages.
8#
9# Usage:
10# ./scripts/build.sh release (default — what the image uses)
11# ./scripts/build.sh --debug debug profile (slower guest, GDB symbols)
12# ./scripts/build.sh --help
13#
14# Output:
15# target/x86_64-unknown-uefi/<profile>/rsdos.efi kernel
16# target/x86_64-unknown-uefi/<profile>/<name>.efi /bin binaries
17#
18# Typical flow:
19# ./scripts/build.sh && ./scripts/run.sh
20#
21set -euo pipefail
22
23usage() {
24 awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"
25}
26
27# PROFILE is read by lib.sh (it derives OUT_DIR/KERNEL from it), so parse
28# before sourcing.
29while [ $# -gt 0 ]; do
30 case "$1" in
31 --debug) PROFILE="debug" ;;
32 --release) PROFILE="release" ;;
33 --help | -h)
34 usage
35 exit 0
36 ;;
37 *)
38 echo "build.sh: unknown argument: $1" >&2
39 exit 1
40 ;;
41 esac
42 shift
43done
44
45# shellcheck source=lib.sh
46source "$(dirname "$0")/lib.sh"
47
48build_all
49echo "kernel: $KERNEL"
diff --git a/scripts/debug-kernel.py b/scripts/debug-kernel.py
index c794ad6..c675045 100755
--- a/scripts/debug-kernel.py
+++ b/scripts/debug-kernel.py
@@ -2,20 +2,22 @@
2GDB Python script for debugging raisernics under QEMU. 2GDB Python script for debugging raisernics under QEMU.
3 3
4Usage: 4Usage:
5 1. Terminal 1: ./tools/qemu.sh --debug 5 1. Terminal 1: ./scripts/run.sh --debug (GDB stub on :1234)
6 2. Terminal 2: gdb -x tools/debug-kernel.py target/x86_64-unknown-uefi/debug/raisernics.efi 6 Add PROFILE=debug for line-level symbols (slower guest).
7 2. Terminal 2: gdb -x scripts/debug-kernel.py
7""" 8"""
8 9
9import gdb 10import gdb
10import os 11import os
11 12
12# Find the kernel binary 13# Find the kernel binary (debug profile preferred, release as fallback)
13script_dir = os.path.dirname(os.path.abspath(__file__)) 14script_dir = os.path.dirname(os.path.abspath(__file__))
14project_root = os.path.dirname(script_dir) 15project_root = os.path.dirname(script_dir)
15binary = os.path.join( 16candidates = [
16 project_root, 17 os.path.join(project_root, "target/x86_64-unknown-uefi/debug/rsdos.efi"),
17 "target/x86_64-unknown-uefi/debug/raisernics.efi" 18 os.path.join(project_root, "target/x86_64-unknown-uefi/release/rsdos.efi"),
18) 19]
20binary = next((c for c in candidates if os.path.exists(c)), candidates[0])
19 21
20# Connect to QEMU's GDB stub 22# Connect to QEMU's GDB stub
21gdb.execute("target remote :1234") 23gdb.execute("target remote :1234")
diff --git a/scripts/lib-boot-args.sh b/scripts/lib-boot-args.sh
deleted file mode 100644
index ec97500..0000000
--- a/scripts/lib-boot-args.sh
+++ /dev/null
@@ -1,38 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/lib-boot-args.sh — shared CLI parsing for the QEMU boot scripts.
4# Sourced, not executed. The caller sets default values for GUI/DEBUG/
5# MEM/SMP first, then calls `parse_boot_args "$@"`. A caller with extra
6# flags defines its own `handle_arg()` to intercept them before the
7# shared cases run (unknown args still fail loudly).
8set -euo pipefail
9
10parse_boot_args() {
11 while [ $# -gt 0 ]; do
12 case "$1" in
13 --gui | -g) GUI="-vga std" ;;
14 --debug | -d) DEBUG="-s -S" ;;
15 --mem)
16 shift
17 MEM="$1"
18 ;;
19 --smp)
20 shift
21 SMP="$1"
22 ;;
23 --help | -h)
24 head -20 "$0" | grep "^#" | sed 's/^# //'
25 exit 0
26 ;;
27 *) handle_arg "$1" ;;
28 esac
29 shift
30 done
31}
32
33# Default: no extra flags. A script with more defines its own before
34# calling parse_boot_args.
35handle_arg() {
36 echo "unknown: $1"
37 exit 1
38}
diff --git a/scripts/lib.sh b/scripts/lib.sh
new file mode 100644
index 0000000..d5aecac
--- /dev/null
+++ b/scripts/lib.sh
@@ -0,0 +1,168 @@
1#!/usr/bin/env bash
2#
3# scripts/lib.sh — shared config and helpers for the rs-dos scripts.
4#
5# Sourced, never executed. Everything here is the single definition of
6# "how we build and how we boot": the build script, the image assembler,
7# the QEMU runner and the tests all read their settings from this file.
8#
9# Config is environment-overridable (TARGET, PROFILE, SYSTEM_IMG, OVMF,
10# QEMU, MEM, SMP) so callers and CI can tune it without editing scripts.
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15TESTS_DIR="$SCRIPT_DIR/tests"
16
17# ---- config ---------------------------------------------------------------
18TARGET="${TARGET:-x86_64-unknown-uefi}"
19PROFILE="${PROFILE:-release}"
20OUT_DIR="${OUT_DIR:-$PROJECT_DIR/target/$TARGET/$PROFILE}"
21KERNEL="$OUT_DIR/rsdos.efi"
22SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
23OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
24QEMU="${QEMU:-qemu-system-x86_64}"
25MEM="${MEM:-256M}"
26SMP="${SMP:-1}"
27
28# Binaries whose launch surface is a window rather than a terminal
29# (ADR 0006): staged into /bin/gui, everything else into /bin/cli.
30GUI_BINS="paint"
31
32cd "$PROJECT_DIR"
33
34# ---- build ----------------------------------------------------------------
35#
36# The one and only cargo invocation in the repo. Builds the kernel and
37# every external binary in a single pass so the image can stage both.
38
39cargo_flags() {
40 [ "$PROFILE" = "release" ] && printf '%s' "--release"
41}
42
43build_all() {
44 echo "=== build (kernel + bins, $PROFILE) ==="
45 # shellcheck disable=SC2046 # cargo_flags is a flag word, intentionally split
46 cargo build --workspace --target "$TARGET" $(cargo_flags)
47}
48
49# ---- system image ---------------------------------------------------------
50
51# stage_image [mksystem args...] — assemble /tmp/system.img if it is stale.
52# Freshness is decided inside mksystem.sh; an up-to-date image is never
53# touched, so files written at runtime survive.
54stage_image() {
55 "$SCRIPT_DIR/mksystem.sh" "$@"
56}
57
58# ---- QEMU -----------------------------------------------------------------
59
60# qemu_args — the common machine every boot shares: OVMF, the system
61# image as the single SATA drive, 256M q35. Callers append display,
62# monitor and serial choices.
63qemu_args() {
64 printf '%s\n' \
65 -bios "$OVMF" \
66 -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \
67 -device ide-hd,drive=disk,bus=ide.0 \
68 -m "$MEM" \
69 -machine q35 \
70 -cpu qemu64 \
71 -no-reboot \
72 -smp "$SMP"
73}
74
75# ---- test harness ---------------------------------------------------------
76#
77# Every test-*.sh does:
78# TNAME=disk; source lib.sh; test_boot # waits for the shell
79# sendkeys "ls /disk" "poweroff"
80# wait_qemu_exit
81# Runtime files are namespaced per test so parallel runs cannot collide.
82
83TNAME="${TNAME:-rsdos}"
84QPID="/tmp/rsdos-$TNAME-qpid"
85SOCK="/tmp/rsdos-$TNAME-qmon.sock"
86LOG="/tmp/rsdos-$TNAME-serial.log"
87QLOG="/tmp/rsdos-$TNAME-qemu.log"
88
89fail() {
90 echo "FAIL: $1"
91 [ -f "$LOG" ] && tail -40 "$LOG"
92 exit 1
93}
94
95# wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex.
96wait_log() {
97 local i=0
98 while [ "$i" -lt $((${3:-60} * 10)) ]; do
99 grep -q "$2" "$1" 2>/dev/null && return 0
100 sleep 0.1
101 i=$((i + 1))
102 done
103 return 1
104}
105
106# wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone.
107wait_qemu_exit() {
108 local i=0
109 while [ "$i" -lt $((${1:-30} * 10)) ]; do
110 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
111 sleep 0.1
112 i=$((i + 1))
113 done
114 return 1
115}
116
117# qemu_hmp CMD — run one HMP command on the monitor socket.
118qemu_hmp() {
119 python3 - "$SOCK" "$1" <<'EOF'
120import socket, sys, time
121s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
122s.connect(sys.argv[1])
123s.sendall((sys.argv[2] + "\n").encode())
124time.sleep(0.4)
125EOF
126}
127
128# sendkeys "line" ... — type shell commands into the guest via sendkeys.py.
129sendkeys() {
130 python3 "$TESTS_DIR/sendkeys.py" --sock "$SOCK" "$@"
131}
132
133# test_cleanup — reap a leftover QEMU from a previous run of this test
134# and clear this test's runtime files. Called before test_boot.
135test_cleanup() {
136 kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
137 rm -f "$SOCK" "$LOG"
138}
139
140# test_on_exit — EXIT trap: never leave a QEMU behind, but keep the logs
141# (they are the post-mortem for a failed test).
142test_on_exit() {
143 kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
144}
145
146# test_boot [extra-qemu-args...] — boot the system image headless with the
147# serial log at $LOG and a monitor socket at $SOCK, then wait for the
148# shell. Extra args attach more drives (e.g. a CRASHLOG stick).
149test_boot() {
150 test_cleanup
151 trap test_on_exit EXIT
152 # A stale/poisoned OVMF varstore makes the firmware skip the
153 # \EFI\BOOT fallback entirely — every test boots from a fresh copy.
154 # Namespaced per test so parallel runs cannot race on it.
155 local nv="/tmp/rsdos-ovmf-vars-$TNAME.fd"
156 rm -f "$nv"
157 cp "$OVMF" "$nv"
158 local -a qargs
159 mapfile -t qargs < <(qemu_args)
160 "$QEMU" -bios "$nv" \
161 "${qargs[@]}" \
162 -serial "file:$LOG" \
163 -monitor "unix:$SOCK,server,nowait" \
164 -display none \
165 "$@" >"$QLOG" 2>&1 &
166 echo $! >"$QPID"
167 wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
168}
diff --git a/scripts/mksystem.sh b/scripts/mksystem.sh
index 044c86c..053fb3b 100755
--- a/scripts/mksystem.sh
+++ b/scripts/mksystem.sh
@@ -2,44 +2,49 @@
2# 2#
3# scripts/mksystem.sh — assemble the single dd-able rs-dos system image 3# scripts/mksystem.sh — assemble the single dd-able rs-dos system image
4# 4#
5# Replaces the old two-source setup (vvfat `esp/` directory for boot + 5# One FAT32 image holds EVERYTHING (boot medium and data medium are the
6# separate /tmp/disk.img for data). One FAT32 image holds EVERYTHING: 6# same disk, mounted by the kernel at /disk/<UID>/):
7# 7#
8# \bzImage kernel (rsbl requires this exact path) 8# \EFI\BOOT\BOOTX64.EFI the kernel — the firmware boots it directly
9# \initramfs.cpio.gz empty initramfs (rsbl requires it) 9# \bzImage kernel copy (rsbl's expected path, real hw)
10# \EFI\BOOT\BOOTX64.EFI boot entry: the kernel (default) or rsboot 10# \initramfs.cpio.gz empty initramfs (rsbl requires the path)
11# \bin\cli\*.efi terminal binaries (invoked by the shell)
12# \bin\gui\*.efi windowed binaries (launched with no shell)
11# \etc\motd.txt seed config 13# \etc\motd.txt seed config
12# \bin\cli\ \bin\gui\ binary tree (empty until Phase 2/4 migrate tools)
13# \*.txt seed files (seed/) 14# \*.txt seed files (seed/)
14# 15#
15# The firmware boots from this image and the kernel mounts the same 16# Freshness: an existing image is only rebuilt when a kernel/binary/seed
16# volume at /disk/<UID>/ — boot medium and data medium are one disk. 17# file is newer than it, so files written at runtime survive across
18# boots. --force always re-creates (wipes runtime data).
17# 19#
18# Usage: 20# Usage:
19# ./scripts/mksystem.sh [options] [output.img] # default /tmp/system.img 21# ./scripts/mksystem.sh [options] [output.img] default: $SYSTEM_IMG
20# 22#
21# Options: 23# Options:
22# --force rebuild even if the image exists (Makefile stamps handle 24# --force rebuild even if the image is up to date
23# normal freshness; without --force an existing image is kept,
24# so user files survive across reboots)
25# --size NM image size in MB (default 64) 25# --size NM image size in MB (default 64)
26# --chain \EFI\BOOT\BOOTX64.EFI = rsboot bootloader (full chain:
27# firmware → rsboot → rsnix). Default is the kernel itself
28# (direct: firmware → rsnix), matching the old dev loop.
29# --gpt wrap the FAT volume in a GPT partition table (for dd-ing
30# to real USB sticks whose firmware refuses superfloppies)
31# --label NAME FAT volume label (default RSDOS) 26# --label NAME FAT volume label (default RSDOS)
27# --gpt wrap the FAT volume in a GPT partition table (for
28# dd-ing to real USB sticks whose firmware refuses
29# superfloppies)
30# --help show this help
31#
32# Needs: dosfstools (mkfs.fat), mtools (mcopy/mmd), cargo (kernel+bins
33# built via scripts/build.sh when missing).
32# 34#
33set -euo pipefail 35set -euo pipefail
34 36
35SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 37SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
36PROJECT_DIR="$(dirname "$SCRIPT_DIR")" 38# Absolute self path: lib.sh cd's to the project root, so a relative $0
37cd "$PROJECT_DIR" 39# would no longer resolve by the time --help prints the header.
40SELF="$SCRIPT_DIR/$(basename "$0")"
41
42# shellcheck source=lib.sh
43source "$SCRIPT_DIR/lib.sh"
38 44
39IMG="/tmp/system.img" 45IMG="$SYSTEM_IMG"
40SIZE_MB=64 46SIZE_MB=64
41FORCE=0 47FORCE=0
42CHAIN=0
43GPT=0 48GPT=0
44LABEL="RSDOS" 49LABEL="RSDOS"
45 50
@@ -52,18 +57,15 @@ while [ $# -gt 0 ]; do
52 shift 57 shift
53 SIZE_MB="$1" 58 SIZE_MB="$1"
54 ;; 59 ;;
55 --chain)
56 CHAIN=1
57 ;;
58 --gpt)
59 GPT=1
60 ;;
61 --label) 60 --label)
62 shift 61 shift
63 LABEL="$1" 62 LABEL="$1"
64 ;; 63 ;;
64 --gpt)
65 GPT=1
66 ;;
65 --help | -h) 67 --help | -h)
66 head -30 "$0" | grep "^#" | sed 's/^# \?//' 68 awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$SELF"
67 exit 0 69 exit 0
68 ;; 70 ;;
69 -*) 71 -*)
@@ -77,15 +79,9 @@ while [ $# -gt 0 ]; do
77 shift 79 shift
78done 80done
79 81
80TARGET=x86_64-unknown-uefi 82SEED_DIR="$PROJECT_DIR/seed"
81PROFILE=release
82OUT_DIR="target/$TARGET/$PROFILE"
83KERNEL="$OUT_DIR/rsdos.efi"
84BZIMAGE="esp/bzImage"
85INITRD="esp/initramfs.cpio.gz"
86BOOTLOADER="build/bootloader/BOOTX64.EFI"
87 83
88# ---- checks ---- 84# ---- checks ---------------------------------------------------------------
89for cmd in mkfs.fat mcopy mmd; do 85for cmd in mkfs.fat mcopy mmd; do
90 if ! command -v "$cmd" >/dev/null 2>&1; then 86 if ! command -v "$cmd" >/dev/null 2>&1; then
91 echo "mksystem: $cmd not found (install dosfstools + mtools)" >&2 87 echo "mksystem: $cmd not found (install dosfstools + mtools)" >&2
@@ -101,29 +97,26 @@ if [ "$GPT" -eq 1 ]; then
101 done 97 done
102fi 98fi
103 99
104if [ -f "$IMG" ] && [ "$FORCE" -eq 0 ]; then 100# ---- build (only when artifacts are missing; build.sh is the only cargo
105 echo "mksystem: $IMG already exists (use --force to re-create)" 101# invocation, so an already-compiled tree costs nothing) --------------------
106 exit 0 102if [ ! -f "$KERNEL" ] || ! ls "$OUT_DIR"/*.efi >/dev/null 2>&1; then
103 "$SCRIPT_DIR/build.sh"
107fi 104fi
108 105
109# ---- build kernel ---- 106# ---- freshness stamp ------------------------------------------------------
110if [ ! -f "$KERNEL" ]; then 107newest=""
111 echo "mksystem: building kernel..." 108for f in "$OUT_DIR"/*.efi "$SEED_DIR"/*; do
112 cargo build --release --package rsdos --target "$TARGET" 109 [ -f "$f" ] || continue
113fi 110 if [ -z "$newest" ] || [ "$f" -nt "$newest" ]; then
114 111 newest="$f"
115# ---- build bootloader (chain mode only) ---- 112 fi
116if [ "$CHAIN" -eq 1 ] && [ ! -f "$BOOTLOADER" ]; then 113done
117 echo "mksystem: building bootloader..." 114if [ "$FORCE" -eq 0 ] && [ -f "$IMG" ] && { [ -z "$newest" ] || [ ! "$newest" -nt "$IMG" ]; }; then
118 "$SCRIPT_DIR/build-bootloader.sh" 115 echo "mksystem: up to date: $IMG (use --force to re-create)"
116 exit 0
119fi 117fi
120 118
121# ---- deploy kernel artifacts to esp/ (shared with the old layout) ---- 119# ---- create the FAT volume ------------------------------------------------
122mkdir -p esp/EFI/BOOT
123cp "$KERNEL" "$BZIMAGE"
124[ -f "$INITRD" ] || printf '' | gzip >"$INITRD"
125
126# ---- create the FAT volume ----
127rm -f "$IMG" 120rm -f "$IMG"
128SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks 121SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks
129echo "mksystem: creating $IMG (${SIZE_MB}M FAT32, label $LABEL)" 122echo "mksystem: creating $IMG (${SIZE_MB}M FAT32, label $LABEL)"
@@ -131,34 +124,47 @@ mkfs.fat -C -F 32 -n "$LABEL" "$IMG" "$SIZE_BLOCKS" >/dev/null
131 124
132echo "mksystem: staging system tree" 125echo "mksystem: staging system tree"
133mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui 126mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui
134mcopy -i "$IMG" "$BZIMAGE" ::/bzImage 127
128# The firmware entry point and rsbl's kernel path are the same file.
129mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
130mcopy -i "$IMG" "$KERNEL" ::/bzImage
131INITRD="$(mktemp -t rsdos-initrd.XXXXXX)"
132trap 'rm -f "$INITRD"' EXIT
133printf '' | gzip >"$INITRD"
135mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz 134mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz
136if [ "$CHAIN" -eq 1 ]; then 135echo " \\EFI\\BOOT\\BOOTX64.EFI \\bzImage \\initramfs.cpio.gz"
137 mcopy -i "$IMG" "$BOOTLOADER" ::/EFI/BOOT/BOOTX64.EFI 136
138 echo " \\EFI\\BOOT\\BOOTX64.EFI <- rsboot (full chain)" 137# External binaries: everything under bins/ was compiled into $OUT_DIR.
139else 138# Launch surface decides the directory (ADR 0006): GUI_BINS -> /bin/gui,
140 mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI 139# everything else -> /bin/cli.
141 echo " \\EFI\\BOOT\\BOOTX64.EFI <- kernel (direct)" 140for src in "$OUT_DIR"/*.efi; do
142fi 141 [ -f "$src" ] || continue
143echo " \\bzImage \\initramfs.cpio.gz \\bin{\\cli,\\gui} \\etc" 142 name="$(basename "$src" .efi)"
143 [ "$name" = "rsdos" ] && continue # the kernel is not a /bin binary
144 case " $GUI_BINS " in
145 *" $name "*) kind="gui" ;;
146 *) kind="cli" ;;
147 esac
148 mcopy -i "$IMG" "$src" "::/bin/$kind/$name.efi"
149 printf ' \\bin\\%s\\%s.efi\n' "$kind" "$name"
150done
144 151
145# Seed files: motd lives in \etc, everything else at the volume root. 152# Seed files: motd lives in \etc, everything else at the volume root.
146SEED_DIR="$PROJECT_DIR/seed"
147if [ -d "$SEED_DIR" ]; then 153if [ -d "$SEED_DIR" ]; then
148 for f in "$SEED_DIR"/*; do 154 for f in "$SEED_DIR"/*; do
149 [ -f "$f" ] || continue 155 [ -f "$f" ] || continue
150 name="$(basename "$f")" 156 name="$(basename "$f")"
151 if [ "$name" = "motd.txt" ]; then 157 if [ "$name" = "motd.txt" ]; then
152 mcopy -i "$IMG" "$f" ::/etc/"$name" 158 mcopy -i "$IMG" "$f" ::/etc/"$name"
153 echo " \\etc\\$name" 159 printf ' \\etc\\%s\n' "$name"
154 else 160 else
155 mcopy -i "$IMG" "$f" ::/"$name" 161 mcopy -i "$IMG" "$f" ::/"$name"
156 echo " \\$name" 162 printf ' \\%s\n' "$name"
157 fi 163 fi
158 done 164 done
159fi 165fi
160 166
161# ---- optional GPT wrapper (real-hardware dd target) ---- 167# ---- optional GPT wrapper (real-hardware dd target) -----------------------
162if [ "$GPT" -eq 1 ]; then 168if [ "$GPT" -eq 1 ]; then
163 GPT_START=2048 # 1 MiB offset (standard alignment) 169 GPT_START=2048 # 1 MiB offset (standard alignment)
164 GPT_IMG="$(mktemp)" 170 GPT_IMG="$(mktemp)"
@@ -179,6 +185,6 @@ fi
179echo "" 185echo ""
180echo "=== Done: $IMG ===" 186echo "=== Done: $IMG ==="
181ls -lh "$IMG" 187ls -lh "$IMG"
182echo "Boot with: make run (or: make boot-image)" 188echo "Boot with: ./scripts/run.sh (or: make run)"
183[ "$GPT" -eq 0 ] && echo "USB dd tip: add --gpt for firmwares that refuse superfloppies" 189[ "$GPT" -eq 0 ] && echo "USB dd tip: add --gpt for firmwares that refuse superfloppies"
184exit 0 190exit 0
diff --git a/scripts/qemu.sh b/scripts/qemu.sh
deleted file mode 100755
index eeaf4de..0000000
--- a/scripts/qemu.sh
+++ /dev/null
@@ -1,99 +0,0 @@
1#!/usr/bin/env bash
2# =============================================================================
3# scripts/qemu.sh — flexible QEMU launcher for rs-dos
4#
5# Usage:
6# ./scripts/qemu.sh # serial console (no graphics)
7# ./scripts/qemu.sh --gui # graphical window
8# ./scripts/qemu.sh --debug # GDB stub on :1234
9# ./scripts/qemu.sh --chain # full boot chain (firmware → rsboot → rsnix)
10# ./scripts/qemu.sh --bios /path/to.fd # custom OVMF
11# ./scripts/qemu.sh --mem 512M # memory size
12# ./scripts/qemu.sh --smp 2 # SMP cores
13#
14# One image, one drive — the system image contains kernel + ESP + data.
15# =============================================================================
16set -euo pipefail
17
18SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20cd "$PROJECT_DIR"
21
22# Defaults
23GUI_FLAG=""
24DEBUG_FLAG=""
25CHAIN_FLAG=""
26OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
27MEM="${MEM:-256M}"
28SMP="${SMP:-1}"
29SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
30
31# Pre-build
32make all
33
34# Parse args
35while [ $# -gt 0 ]; do
36 case "$1" in
37 --gui | -g)
38 GUI_FLAG="-vga std"
39 shift
40 ;;
41 --debug | -d)
42 DEBUG_FLAG="-s -S"
43 shift
44 ;;
45 --chain | -c)
46 CHAIN_FLAG=1
47 SYSTEM_IMG="${SYSTEM_IMG:-rs-dos.img}"
48 shift
49 ;;
50 --image | -i)
51 SYSTEM_IMG="$2"
52 shift 2
53 ;;
54 --bios | -b)
55 OVMF="$2"
56 shift 2
57 ;;
58 --mem)
59 MEM="$2"
60 shift 2
61 ;;
62 --smp)
63 SMP="$2"
64 shift 2
65 ;;
66 *)
67 echo "unknown: $1"
68 exit 1
69 ;;
70 esac
71done
72
73# Stage system image if needed
74if [ -n "$CHAIN_FLAG" ]; then
75 ./scripts/mksystem.sh --chain --gpt "$SYSTEM_IMG"
76else
77 ./scripts/mksystem.sh "$SYSTEM_IMG"
78fi
79
80echo ""
81echo "=== Booting ==="
82echo " image: $SYSTEM_IMG"
83echo " ovmf: $OVMF"
84echo " mem: $MEM smp: $SMP"
85echo " gui: $([ -n "$GUI_FLAG" ] && echo yes || echo no)"
86echo " gdb: $([ -n "$DEBUG_FLAG" ] && echo ':1234' || echo off)"
87echo ""
88
89exec qemu-system-x86_64 \
90 -bios "$OVMF" \
91 -drive "file=$SYSTEM_IMG,format=raw,if=none,id=hd" \
92 -device ide-hd,drive=hd \
93 -serial stdio \
94 -m "$MEM" \
95 -machine q35 \
96 -cpu qemu64 \
97 -no-reboot \
98 -smp "$SMP" \
99 $GUI_FLAG $DEBUG_FLAG
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100755
index 0000000..4b6f481
--- /dev/null
+++ b/scripts/run.sh
@@ -0,0 +1,142 @@
1#!/usr/bin/env bash
2#
3# scripts/run.sh — build, stage and boot the system image in QEMU.
4#
5# Usage:
6# ./scripts/run.sh build + stage + boot (window + serial)
7# ./scripts/run.sh --headless serial console only, no window
8# ./scripts/run.sh --debug GDB stub on :1234, CPU starts paused
9# (attach: gdb -x scripts/debug-kernel.py)
10# ./scripts/run.sh --monitor HMP monitor socket for sendkeys.py /
11# test-focus.py (default path below)
12# ./scripts/run.sh --fresh re-create the system image first
13# (wipes runtime data on the image)
14# ./scripts/run.sh --mem 512M guest memory (default 256M)
15# ./scripts/run.sh --smp 2 guest CPUs (default 1)
16# ./scripts/run.sh --bios FILE OVMF firmware (default: system path)
17# ./scripts/run.sh --image FILE system image (default: /tmp/system.img)
18# ./scripts/run.sh --grab confine the pointer: GTK runs as an X11
19# (XWayland) client and grabs on hover
20# ./scripts/run.sh --no-grab Wayland-native window, no pointer grab
21#
22# Mouse: Wayland forbids global pointer grabs, so a Wayland-native window
23# cannot confine the cursor (QEMU's grab silently does nothing).
24# In a Wayland session with XWayland, run.sh defaults to --grab;
25# Ctrl+Alt+G toggles the grab in either mode.
26#
27# Environment (same names the other scripts honour): TARGET, PROFILE,
28# SYSTEM_IMG, OVMF, QEMU, MEM, SMP.
29#
30# Two-command flow: ./scripts/build.sh && ./scripts/run.sh
31# One-command flow: ./scripts/run.sh (builds what is missing)
32#
33# Monitor socket: /tmp/rsdos-monitor.sock
34#
35set -euo pipefail
36
37SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
38
39usage() {
40 awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"
41}
42
43HEADLESS=0
44DEBUG=0
45FRESH=0
46MON=0
47MON_SOCK="/tmp/rsdos-monitor.sock"
48GRAB="" # "" = auto: grab when a Wayland session has XWayland
49
50while [ $# -gt 0 ]; do
51 case "$1" in
52 --headless | -H) HEADLESS=1 ;;
53 # GDB stub + keep whatever profile the env asked for (release is the
54 # default; PROFILE=debug ./scripts/run.sh --debug adds line symbols).
55 --debug | -d) DEBUG=1 ;;
56 --fresh) FRESH=1 ;;
57 --monitor) MON=1 ;;
58 --mem) shift; MEM="$1" ;;
59 --smp) shift; SMP="$1" ;;
60 --bios) shift; OVMF="$1" ;;
61 --image | -i) shift; SYSTEM_IMG="$1" ;;
62 --grab) GRAB=1 ;;
63 --no-grab) GRAB=0 ;;
64 --help | -h)
65 usage
66 exit 0
67 ;;
68 *)
69 echo "run.sh: unknown argument: $1" >&2
70 usage >&2
71 exit 1
72 ;;
73 esac
74 shift
75done
76
77# shellcheck source=lib.sh
78source "$SCRIPT_DIR/lib.sh" # reads MEM/SMP/OVMF/SYSTEM_IMG from the env
79
80# build.sh is a separate process: it re-derives OUT_DIR/KERNEL from these,
81# so whatever we resolved above (including a --mem/--bios override) wins
82# there too.
83export TARGET PROFILE QEMU MEM SMP OVMF SYSTEM_IMG
84
85# ---- mouse grab (Wayland) --------------------------------------------------
86# Wayland forbids global pointer grabs, so a Wayland-native GTK window cannot
87# confine the cursor — QEMU's grab silently does nothing there. XWayland can,
88# so a Wayland session that has XWayland (DISPLAY set) defaults to GTK-on-X11
89# plus grab-on-hover. Ctrl+Alt+G toggles the grab; --no-grab keeps the
90# Wayland-native window.
91if [ -z "$GRAB" ]; then
92 if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] && [ -n "${DISPLAY:-}" ]; then
93 GRAB=1
94 else
95 GRAB=0
96 fi
97fi
98[ "$HEADLESS" -eq 1 ] && GRAB=0 # no window, nothing to grab
99if [ "$GRAB" -eq 1 ]; then
100 if [ -n "${DISPLAY:-}" ]; then
101 export GDK_BACKEND=x11
102 else
103 echo "run.sh: --grab needs XWayland (DISPLAY is unset); not grabbing" >&2
104 GRAB=0
105 fi
106fi
107
108# ---- build + stage --------------------------------------------------------
109"$SCRIPT_DIR/build.sh"
110if [ "$FRESH" -eq 1 ]; then
111 stage_image --force
112else
113 stage_image
114fi
115
116# ---- boot -----------------------------------------------------------------
117mapfile -t QARGS < <(qemu_args)
118if [ "$HEADLESS" -eq 1 ]; then
119 QARGS+=(-display none)
120else
121 QARGS+=(-vga std)
122 if [ "$GRAB" -eq 1 ]; then
123 QARGS+=(-display gtk,grab-on-hover=on)
124 fi
125fi
126[ "$MON" -eq 1 ] && QARGS+=(-monitor "unix:$MON_SOCK,server,nowait")
127[ "$DEBUG" -eq 1 ] && QARGS+=(-s -S)
128QARGS+=(-serial stdio)
129
130echo ""
131echo "=== boot ==="
132echo " image: $SYSTEM_IMG"
133echo " ovmf: $OVMF"
134echo " mem: $MEM smp: $SMP"
135echo " mode: $([ "$HEADLESS" -eq 1 ] && echo headless || echo window) gdb: $([ "$DEBUG" -eq 1 ] && echo ':1234 (paused)' || echo off)"
136if [ "$HEADLESS" -eq 0 ]; then
137 echo " mouse: $([ "$GRAB" -eq 1 ] && echo 'grab on hover (XWayland; Ctrl+Alt+G toggles)' || echo 'not grabbed (Wayland cannot confine the pointer)')"
138fi
139[ "$MON" -eq 1 ] && echo " monitor: unix:$MON_SOCK"
140echo ""
141
142exec "$QEMU" "${QARGS[@]}"
diff --git a/scripts/test-disk.sh b/scripts/test-disk.sh
deleted file mode 100755
index 881b475..0000000
--- a/scripts/test-disk.sh
+++ /dev/null
@@ -1,131 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-disk.sh — full automated data-disk round trip.
4#
5# Builds the kernel, ensures the FAT32 image exists (with --fresh it is
6# re-formatted and re-seeded), boots QEMU headless, drives the shell via
7# the monitor, then verifies the results with mtools and fsck.
8#
9# Usage:
10# ./scripts/test-disk.sh # reuse existing image
11# ./scripts/test-disk.sh --fresh # re-format the image first
12#
13# Environment:
14# DISK_IMG data disk path (default: /tmp/disk.img)
15# OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd)
16# QEMU qemu binary (default: qemu-system-x86_64)
17#
18set -euo pipefail
19
20SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
22cd "$PROJECT_DIR"
23
24FRESH=0
25[ "${1:-}" = "--fresh" ] && FRESH=1
26
27OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
28QEMU="${QEMU:-qemu-system-x86_64}"
29IMG="${SYSTEM_IMG:-/tmp/system.img}"
30QPID=/tmp/rsdos-test-qpid
31SOCK=/tmp/rsdos-test-qmon.sock
32LOG=/tmp/rsdos-test-serial.log
33QLOG=/tmp/rsdos-test-qemu.log
34STAMP="$(date +%s)"
35
36# Never leave a QEMU behind (an orphan holding our stdout would hang any
37# caller that pipes our output).
38trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
39
40fail() {
41 echo "FAIL: $1"
42 [ -f "$LOG" ] && tail -20 "$LOG"
43 exit 1
44}
45
46# wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex.
47wait_log() {
48 local i=0
49 while [ "$i" -lt $((${3:-60} * 10)) ]; do
50 grep -q "$2" "$1" 2>/dev/null && return 0
51 sleep 0.1
52 i=$((i + 1))
53 done
54 return 1
55}
56
57# wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone.
58wait_qemu_exit() {
59 local i=0
60 while [ "$i" -lt $((${1:-30} * 10)) ]; do
61 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
62 sleep 0.1
63 i=$((i + 1))
64 done
65 return 1
66}
67
68echo "==> 1/6 build kernel (debug)"
69cargo build --release --package rsdos --target x86_64-unknown-uefi
70
71echo "==> 2/6 ensure system image"
72if [ "$FRESH" -eq 1 ]; then
73 "$SCRIPT_DIR/mksystem.sh" --force "$IMG"
74else
75 "$SCRIPT_DIR/mksystem.sh" "$IMG"
76fi
77
78echo "==> 3/6 boot QEMU (headless)"
79kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
80sleep 1
81rm -f "$SOCK" "$LOG"
82# Throwaway OVMF varstore: a stale/poisoned NvVars (e.g. left by the old
83# vvfat setup) makes the firmware skip the \EFI\BOOT fallback entirely.
84NV="/tmp/rsdos-test-ovmf-vars.fd"
85rm -f "$NV"
86cp /usr/share/edk2/x64/OVMF.4m.fd "$NV"
87"$QEMU" -bios "$NV" \
88 -drive file="$IMG",format=raw,if=none,id=disk \
89 -device ide-hd,drive=disk,bus=ide.0 \
90 -serial file:"$LOG" \
91 -monitor unix:"$SOCK",server,nowait \
92 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
93 >"$QLOG" 2>&1 &
94
95wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
96sleep 1
97
98# Volumes mount under /disk/<UID>/ where UID = "LABEL-SERIAL" (e.g.
99# RSDOS-5BD1E55F). Discover it from the boot log so the test works on
100# any image without knowing the serial in advance.
101VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)"
102[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)"
103echo " mounted volume UID: $VOL_UID"
104
105echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)"
106# The filename is deliberately >13 chars so the FAT LFN path gets
107# multi-entry entries (they're stored last-first on disk).
108TESTFILE="disk-test-$STAMP.txt"
109python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
110 "ls /disk" \
111 "mkdir /disk/$VOL_UID/sub" \
112 "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \
113 "cat /disk/$VOL_UID/sub/$TESTFILE" \
114 "poweroff"
115
116echo "==> 5/6 wait for shutdown"
117wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
118
119echo "==> 6/6 verify"
120grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log"
121if ! fsck.fat -n "$IMG" >/tmp/rsdos-fsck.out 2>&1; then
122 fail "fsck.fat reports problems: $(cat /tmp/rsdos-fsck.out | tail -3)"
123fi
124MT="$(mtype -i "$IMG" "::/sub/$TESTFILE" 2>/dev/null || true)"
125[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'"
126
127echo ""
128echo "PASS — disk round trip OK (fsck clean, file survived poweroff)"
129echo " image: $IMG"
130echo " volume: /disk/$VOL_UID"
131echo " written: /disk/$VOL_UID/sub/$TESTFILE = \"disk-test-$STAMP\""
diff --git a/scripts/test-fault.sh b/scripts/test-fault.sh
deleted file mode 100755
index be2d7cd..0000000
--- a/scripts/test-fault.sh
+++ /dev/null
@@ -1,85 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-fault.sh — fault-diagnostics verification.
4#
5# Proves the fault-dump path: every CPU exception prints a white-on-red
6# band to the framebuffer and a parseable serial line before halting.
7#
8# Usage: ./scripts/test-fault.sh
9# Env: OVMF, QEMU, SYSTEM_IMG
10#
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15cd "$PROJECT_DIR"
16
17OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
18QEMU="${QEMU:-qemu-system-x86_64}"
19SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
20QPID=/tmp/rsdos-fault-qpid
21SOCK=/tmp/rsdos-fault-qmon.sock
22LOG=/tmp/rsdos-fault-serial.log
23QLOG=/tmp/rsdos-fault-qemu.log
24
25trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
26
27fail() {
28 echo "FAIL: $1"
29 [ -f "$LOG" ] && tail -40 "$LOG"
30 exit 1
31}
32
33wait_log() {
34 local i=0
35 while [ "$i" -lt $((${3:-60} * 10)) ]; do
36 grep -q "$2" "$1" 2>/dev/null && return 0
37 sleep 0.1
38 i=$((i + 1))
39 done
40 return 1
41}
42
43echo "==> 1/5 build kernel (debug)"
44cargo build --release --package rsdos --target x86_64-unknown-uefi
45
46echo "==> 2/5 stage system image"
47"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
48
49echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)"
50kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
51sleep 1
52rm -f "$SOCK" "$LOG"
53CRASH_IMG=/tmp/rsdos-crash.img
54mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" >/dev/null
55"$QEMU" -bios "$OVMF" \
56 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
57 -device ide-hd,drive=disk,bus=ide.0 \
58 -drive file="$CRASH_IMG",format=raw,if=none,id=crash \
59 -device ide-hd,drive=crash,bus=ide.1 \
60 -serial file:"$LOG" \
61 -monitor unix:"$SOCK",server,nowait \
62 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
63 >"$QLOG" 2>&1 &
64echo $! >"$QPID"
65
66wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
67
68echo "==> 4/5 boot diagnostics present"
69grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing"
70grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing"
71grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted"
72
73echo "==> 5/5 inject a page fault (fault command)"
74python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
75 "clear" \
76 "fault"
77sleep 2
78
79grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing"
80grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)"
81grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified"
82grep -aq "!!! halting" "$LOG" || fail "no halt message"
83
84echo ""
85echo "PASS — fault dump verified"
diff --git a/scripts/test-gfx.sh b/scripts/test-gfx.sh
deleted file mode 100755
index 1c7f9ac..0000000
--- a/scripts/test-gfx.sh
+++ /dev/null
@@ -1,84 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-gfx.sh — automated graphics round-trip test.
4#
5# Builds kernel, boots headless, runs the in-kernel gfx self-test and
6# the draw/paint demo.
7#
8# Env: OVMF, QEMU, SYSTEM_IMG
9#
10set -euo pipefail
11
12SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14cd "$PROJECT_DIR"
15
16OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
17QEMU="${QEMU:-qemu-system-x86_64}"
18SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
19QPID=/tmp/rsdos-gfx-qpid
20SOCK=/tmp/rsdos-gfx-qmon.sock
21LOG=/tmp/rsdos-gfx-serial.log
22QLOG=/tmp/rsdos-gfx-qemu.log
23
24trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
25
26fail() {
27 echo "FAIL: $1"
28 [ -f "$LOG" ] && tail -40 "$LOG"
29 exit 1
30}
31
32wait_log() {
33 local i=0
34 while [ "$i" -lt $((${3:-60} * 10)) ]; do
35 grep -q "$2" "$1" 2>/dev/null && return 0
36 sleep 0.1
37 i=$((i + 1))
38 done
39 return 1
40}
41
42echo "==> 1/5 build kernel (debug)"
43cargo build --release --package rsdos --target x86_64-unknown-uefi
44
45echo "==> 2/5 stage system image"
46"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
47
48echo "==> 3/5 boot QEMU (headless)"
49kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
50sleep 1
51rm -f "$SOCK" "$LOG"
52"$QEMU" -bios "$OVMF" \
53 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
54 -device ide-hd,drive=disk,bus=ide.0 \
55 -serial file:"$LOG" \
56 -monitor unix:"$SOCK",server,nowait \
57 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
58 >"$QLOG" 2>&1 &
59echo $! >"$QPID"
60
61wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
62wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
63
64echo "==> 4/5 in-kernel pixel self-test (gfx check)"
65python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx check"
66wait_log "$LOG" "gfx: check" 30 || fail "gfx check did not run"
67
68echo "==> 5/5 gfx demo (draw scene)"
69python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx" "poweroff"
70wait_qemu_exit() {
71 local i=0
72 while [ "$i" -lt $((${1:-30} * 10)) ]; do
73 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
74 sleep 0.1
75 i=$((i + 1))
76 done
77 return 1
78}
79wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
80
81grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw"
82
83echo ""
84echo "PASS — gfx round trip"
diff --git a/scripts/test-mouse.sh b/scripts/test-mouse.sh
deleted file mode 100755
index 9f9f018..0000000
--- a/scripts/test-mouse.sh
+++ /dev/null
@@ -1,125 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-mouse.sh — automated PS/2 mouse round-trip test.
4#
5# Boots QEMU headless, injects mouse movement via QEMU monitor,
6# verifies the on-screen cursor tracks it, and tests the mouse
7# stream routing command.
8#
9# Env: OVMF, QEMU, SYSTEM_IMG
10#
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15cd "$PROJECT_DIR"
16
17OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
18QEMU="${QEMU:-qemu-system-x86_64}"
19SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
20QPID=/tmp/rsdos-mouse-qpid
21SOCK=/tmp/rsdos-mouse-qmon.sock
22LOG=/tmp/rsdos-mouse-serial.log
23QLOG=/tmp/rsdos-mouse-qemu.log
24
25trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
26
27fail() {
28 echo "FAIL: $1"
29 [ -f "$LOG" ] && tail -40 "$LOG"
30 exit 1
31}
32
33wait_log() {
34 local i=0
35 while [ "$i" -lt $((${3:-60} * 10)) ]; do
36 grep -q "$2" "$1" 2>/dev/null && return 0
37 sleep 0.1
38 i=$((i + 1))
39 done
40 return 1
41}
42
43qemu_hmp() {
44 python3 - "$SOCK" "$1" <<'EOF'
45import socket, sys, time
46s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
47s.connect(sys.argv[1])
48s.sendall((sys.argv[2] + "\n").encode())
49time.sleep(0.4)
50EOF
51}
52
53cursor_state() {
54 python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "cursor"
55 sleep 0.5
56 sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1
57}
58
59echo "==> 1/6 build kernel (debug)"
60cargo build --release --package rsdos --target x86_64-unknown-uefi
61
62echo "==> 2/6 stage system image"
63"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
64
65echo "==> 3/6 boot QEMU (headless)"
66kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
67sleep 1
68rm -f "$SOCK" "$LOG"
69"$QEMU" -bios "$OVMF" \
70 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
71 -device ide-hd,drive=disk,bus=ide.0 \
72 -serial file:"$LOG" \
73 -monitor unix:"$SOCK",server,nowait \
74 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
75 >"$QLOG" 2>&1 &
76echo $! >"$QPID"
77
78wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
79grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot"
80wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
81
82echo "==> 4/6 cursor position tracks injected movement"
83INIT=$(cursor_state)
84[ -n "$INIT" ] || fail "no initial cursor position"
85IX=$(echo "$INIT" | awk '{print $1}')
86IY=$(echo "$INIT" | awk '{print $2}')
87IB=$(echo "$INIT" | awk '{print $3}')
88echo " initial: x=$IX y=$IY buttons=$IB"
89[ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB"
90
91qemu_hmp "mouse_move 20 7"
92S1=$(cursor_state)
93X1=$(echo "$S1" | awk '{print $1}')
94Y1=$(echo "$S1" | awk '{print $2}')
95[ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)"
96[ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)"
97echo " moved: x=$X1 y=$Y1 (delta +20,+7)"
98
99echo "==> 5/6 left button edge detection"
100qemu_hmp "mouse_button 1"
101S2=$(cursor_state)
102IB2=$(echo "$S2" | awk '{print $3}')
103[ "$IB2" = "L--" ] || [ "$IB2" = "L--" ] || fail "expected L--, got $IB2"
104
105qemu_hmp "mouse_button 0"
106S3=$(cursor_state)
107IB3=$(echo "$S3" | awk '{print $3}')
108[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3"
109echo " button: $IB2 → $IB3"
110
111echo "==> 6/6 mouse stream routing to task 3"
112python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "msgrecv"
113sleep 1
114python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3"
115qemu_hmp "mouse_move 20 7"
116qemu_hmp "mouse_button 1"
117sleep 0.5
118qemu_hmp "mouse_button 0"
119sleep 0.5
120python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "ctrl-c"
121sleep 0.5
122grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged"
123
124echo ""
125echo "PASS — mouse round trip"
diff --git a/scripts/test-msg.sh b/scripts/test-msg.sh
deleted file mode 100755
index ff7fd72..0000000
--- a/scripts/test-msg.sh
+++ /dev/null
@@ -1,140 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-msg.sh — automated scheduler message-queue round trip.
4#
5# Tests all message types (user, timer, key, mouse), real-input routing,
6# and queue-full boundary.
7#
8# Env: OVMF, QEMU, SYSTEM_IMG
9#
10set -euo pipefail
11
12SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14cd "$PROJECT_DIR"
15
16OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
17QEMU="${QEMU:-qemu-system-x86_64}"
18SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
19QPID=/tmp/rsdos-msg-qpid
20SOCK=/tmp/rsdos-msg-qmon.sock
21LOG=/tmp/rsdos-msg-serial.log
22QLOG=/tmp/rsdos-msg-qemu.log
23
24trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
25
26fail() {
27 echo "FAIL: $1"
28 [ -f "$LOG" ] && tail -40 "$LOG"
29 exit 1
30}
31
32wait_log() {
33 local i=0
34 while [ "$i" -lt $((${3:-60} * 10)) ]; do
35 grep -q "$2" "$1" 2>/dev/null && return 0
36 sleep 0.1
37 i=$((i + 1))
38 done
39 return 1
40}
41
42qemu_hmp() {
43 python3 - "$SOCK" "$1" <<'EOF'
44import socket, sys, time
45s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
46s.connect(sys.argv[1])
47s.sendall((sys.argv[2] + "\n").encode())
48time.sleep(0.4)
49EOF
50}
51
52wait_qemu_exit() {
53 local i=0
54 while [ "$i" -lt $((${1:-30} * 10)) ]; do
55 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
56 sleep 0.1
57 i=$((i + 1))
58 done
59 return 1
60}
61
62echo "==> 1/5 build kernel (debug)"
63cargo build --release --package rsdos --target x86_64-unknown-uefi
64
65echo "==> 2/5 stage system image"
66"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
67
68echo "==> 3/5 boot QEMU (headless)"
69kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
70sleep 1
71rm -f "$SOCK" "$LOG"
72"$QEMU" -bios "$OVMF" \
73 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
74 -device ide-hd,drive=disk,bus=ide.0 \
75 -serial file:"$LOG" \
76 -monitor unix:"$SOCK",server,nowait \
77 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
78 >"$QLOG" 2>&1 &
79echo $! >"$QPID"
80
81wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
82
83echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)"
84python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
85 "msgrecv" \
86 "send 3 user 42" \
87 "send 3 timer" \
88 "send 3 key enter" \
89 "send 3 key ctrl-c" \
90 "send 3 key f3" \
91 "send 3 mouse 5 -3 3" \
92 "sleep 800" \
93 "jobs"
94
95python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3"
96qemu_hmp "mouse_move 20 7"
97qemu_hmp "mouse_button 1"
98qemu_hmp "mouse_move -5 -3"
99qemu_hmp "mouse_button 0"
100qemu_hmp "sendkey ctrl-c"
101
102python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
103 "send 0 user 1" \
104 "send 0 user 2" \
105 "send 0 user 3" \
106 "send 0 user 4" \
107 "send 0 user 5" \
108 "send 0 user 6" \
109 "send 0 user 7" \
110 "send 0 user 8" \
111 "send 0 user 9" \
112 "send 0 user 10" \
113 "send 0 user 11" \
114 "send 0 user 12" \
115 "send 0 user 13" \
116 "send 0 user 14" \
117 "send 0 user 15" \
118 "send 0 user 16" \
119 "send 0 user 17" \
120 "sleep 800" \
121 "jobs" \
122 "poweroff"
123
124echo "==> 5/5 wait for shutdown + verify"
125wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
126
127grep -aq "msgrecv: task 3 listening" "$LOG" || fail "msgrecv did not spawn"
128grep -aq "msgrecv\[3\]: User(42)" "$LOG" || fail "user message missing"
129grep -aq "msgrecv\[3\]: Timer" "$LOG" || fail "timer message missing"
130grep -aq "msgrecv\[3\]: Key(Enter)" "$LOG" || fail "key enter missing"
131grep -aq "msgrecv\[3\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing"
132grep -aq "msgrecv\[3\]: Key(F3)" "$LOG" || fail "key f3 missing"
133grep -aq "msgrecv\[3\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing"
134grep -aq "msgrecv\[3\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue"
135grep -aq "msgrecv\[3\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing"
136grep -aq "queue full" "$LOG" || fail "17th send did not report queue full"
137grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)"
138
139echo ""
140echo "PASS — message queues OK"
diff --git a/scripts/sendkeys.py b/scripts/tests/sendkeys.py
index a67903d..9300242 100755
--- a/scripts/sendkeys.py
+++ b/scripts/tests/sendkeys.py
@@ -5,18 +5,20 @@ Each positional argument is one line: it is typed character by character
5(US layout) followed by Enter. Use it to drive the rs-dos shell from a 5(US layout) followed by Enter. Use it to drive the rs-dos shell from a
6test script without a display. 6test script without a display.
7 7
8Usage: 8Used by scripts/tests/test-*.sh, and manually against an interactive
9 sendkeys.py "ls /disk" "echo hi WRITING /disk/a.txt" "poweroff" 9session started with `./scripts/run.sh --monitor`:
10 sendkeys.py --sock /tmp/qmon.sock "date"
11 10
12Environment: QMON_SOCK (default: /tmp/qmon.sock) 11 ./scripts/tests/sendkeys.py "ls /disk" "date" "poweroff"
12 ./scripts/tests/sendkeys.py --sock /tmp/rsdos-monitor.sock "date"
13
14Environment: QMON_SOCK (default: /tmp/rsdos-monitor.sock)
13""" 15"""
14import os 16import os
15import socket 17import socket
16import sys 18import sys
17import time 19import time
18 20
19SOCK = os.environ.get("QMON_SOCK", "/tmp/qmon.sock") 21SOCK = os.environ.get("QMON_SOCK", "/tmp/rsdos-monitor.sock")
20 22
21# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are 23# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are
22# not valid sendkey names (and '-' is the modifier separator). 24# not valid sendkey names (and '-' is the modifier separator).
diff --git a/scripts/tests/test-disk.sh b/scripts/tests/test-disk.sh
new file mode 100755
index 0000000..3836b6d
--- /dev/null
+++ b/scripts/tests/test-disk.sh
@@ -0,0 +1,67 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-disk.sh — full automated data-disk round trip.
4#
5# Builds, stages the system image, boots QEMU headless, drives the shell
6# via the monitor (ls / mkdir / echo WRITING / cat / poweroff), then
7# verifies the results with mtools and fsck.
8#
9# Usage:
10# ./scripts/tests/test-disk.sh reuse the existing image
11# ./scripts/tests/test-disk.sh --fresh reformat + reseed first
12#
13# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
14#
15set -euo pipefail
16TNAME="disk"
17# shellcheck source=../lib.sh
18source "$(dirname "$0")/../lib.sh"
19
20FRESH=0
21[ "${1:-}" = "--fresh" ] && FRESH=1
22STAMP="$(date +%s)"
23
24echo "==> 1/6 build (kernel + bins)"
25build_all
26
27echo "==> 2/6 stage system image"
28if [ "$FRESH" -eq 1 ]; then stage_image --force; else stage_image; fi
29
30echo "==> 3/6 boot QEMU (headless)"
31test_boot
32sleep 1
33
34# Volumes mount under /disk/<UID>/ where UID = "LABEL-SERIAL" (e.g.
35# RSDOS-5BD1E55F). Discover it from the boot log so the test works on
36# any image without knowing the serial in advance.
37VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)"
38[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)"
39echo " mounted volume UID: $VOL_UID"
40
41echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)"
42# The filename is deliberately >13 chars so the FAT LFN path gets
43# multi-entry entries (they're stored last-first on disk).
44TESTFILE="disk-test-$STAMP.txt"
45sendkeys \
46 "ls /disk" \
47 "mkdir /disk/$VOL_UID/sub" \
48 "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \
49 "cat /disk/$VOL_UID/sub/$TESTFILE" \
50 "poweroff"
51
52echo "==> 5/6 wait for shutdown"
53wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
54
55echo "==> 6/6 verify"
56grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log"
57if ! fsck.fat -n "$SYSTEM_IMG" >/tmp/rsdos-fsck.out 2>&1; then
58 fail "fsck.fat reports problems: $(tail -3 /tmp/rsdos-fsck.out)"
59fi
60MT="$(mtype -i "$SYSTEM_IMG" "::/sub/$TESTFILE" 2>/dev/null || true)"
61[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'"
62
63echo ""
64echo "PASS — disk round trip OK (fsck clean, file survived poweroff)"
65echo " image: $SYSTEM_IMG"
66echo " volume: /disk/$VOL_UID"
67echo " written: /disk/$VOL_UID/sub/$TESTFILE = \"disk-test-$STAMP\""
diff --git a/scripts/tests/test-fault.sh b/scripts/tests/test-fault.sh
new file mode 100755
index 0000000..0c39cde
--- /dev/null
+++ b/scripts/tests/test-fault.sh
@@ -0,0 +1,47 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-fault.sh — fault-diagnostics verification.
4#
5# Proves the fault-dump path: every CPU exception prints a white-on-red
6# band to the framebuffer and a parseable serial line before halting.
7# Boots with a dedicated CRASHLOG FAT stick on ide.1 so the crash band
8# has somewhere to write.
9#
10# Usage: ./scripts/tests/test-fault.sh
11# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
12#
13set -euo pipefail
14TNAME="fault"
15# shellcheck source=../lib.sh
16source "$(dirname "$0")/../lib.sh"
17
18echo "==> 1/5 build (kernel + bins)"
19build_all
20
21echo "==> 2/5 stage system image"
22stage_image --force
23
24echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)"
25CRASH_IMG=/tmp/rsdos-crash.img
26rm -f "$CRASH_IMG"
27mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" 65536 >/dev/null
28test_boot \
29 -drive "file=$CRASH_IMG,format=raw,if=none,id=crash" \
30 -device ide-hd,drive=crash,bus=ide.1
31
32echo "==> 4/5 boot diagnostics present"
33grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing"
34grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing"
35grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted"
36
37echo "==> 5/5 inject a page fault (fault command)"
38sendkeys "clear" "fault"
39sleep 2
40
41grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing"
42grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)"
43grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified"
44grep -aq "!!! halting" "$LOG" || fail "no halt message"
45
46echo ""
47echo "PASS — fault dump verified"
diff --git a/scripts/test-focus.py b/scripts/tests/test-focus.py
index e1bbe2d..cfa317a 100755
--- a/scripts/test-focus.py
+++ b/scripts/tests/test-focus.py
@@ -2,16 +2,17 @@
2"""Drive the focus-routing gate: 3 windows, type markers, verify routing. 2"""Drive the focus-routing gate: 3 windows, type markers, verify routing.
3 3
4Connects to the QEMU monitor once and keeps the socket open (the per-call 4Connects to the QEMU monitor once and keeps the socket open (the per-call
5scripts/sendkeys.py pattern loses the socket when the monitor resets). 5sendkeys.py pattern loses the socket when the monitor resets).
6 6
7Usage: python3 scripts/test-focus.py /tmp/qmon.sock 7Usage: start the guest with `./scripts/run.sh --monitor`, then
8 python3 scripts/tests/test-focus.py [/tmp/rsdos-monitor.sock]
8""" 9"""
9import socket 10import socket
10import sys 11import sys
11import time 12import time
12 13
13sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 14sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
14sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysboot-mon.sock") 15sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/rsdos-monitor.sock")
15 16
16 17
17def sk(k, d=0.35): 18def sk(k, d=0.35):
diff --git a/scripts/tests/test-gfx.sh b/scripts/tests/test-gfx.sh
new file mode 100755
index 0000000..613318f
--- /dev/null
+++ b/scripts/tests/test-gfx.sh
@@ -0,0 +1,37 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-gfx.sh — automated graphics round-trip test.
4#
5# Builds, boots headless, runs the in-kernel gfx self-test and the
6# status-bar demo, then powers off.
7#
8# Usage: ./scripts/tests/test-gfx.sh
9# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
10#
11set -euo pipefail
12TNAME="gfx"
13# shellcheck source=../lib.sh
14source "$(dirname "$0")/../lib.sh"
15
16echo "==> 1/5 build (kernel + bins)"
17build_all
18
19echo "==> 2/5 stage system image"
20stage_image --force
21
22echo "==> 3/5 boot QEMU (headless)"
23test_boot
24wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
25
26echo "==> 4/5 in-kernel pixel self-test (gfx check)"
27sendkeys "gfx check"
28wait_log "$LOG" "gfx check: PASS" 30 || fail "gfx check did not run"
29
30echo "==> 5/5 gfx demo (draw scene)"
31sendkeys "gfx" "poweroff"
32wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
33
34grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw"
35
36echo ""
37echo "PASS — gfx round trip"
diff --git a/scripts/tests/test-mouse.sh b/scripts/tests/test-mouse.sh
new file mode 100755
index 0000000..bb835be
--- /dev/null
+++ b/scripts/tests/test-mouse.sh
@@ -0,0 +1,83 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-mouse.sh — automated PS/2 mouse round-trip test.
4#
5# Boots QEMU headless, injects mouse movement via the QEMU monitor,
6# verifies the on-screen cursor tracks it, and tests the mouse stream
7# routing command.
8#
9# Usage: ./scripts/tests/test-mouse.sh
10# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
11#
12set -euo pipefail
13TNAME="mouse"
14# shellcheck source=../lib.sh
15source "$(dirname "$0")/../lib.sh"
16
17# cursor_state — one-shot `cursor` command, parsed to "x y buttons".
18cursor_state() {
19 sendkeys "cursor"
20 sleep 0.5
21 sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1
22}
23
24echo "==> 1/6 build (kernel + bins)"
25build_all
26
27echo "==> 2/6 stage system image"
28stage_image --force
29
30echo "==> 3/6 boot QEMU (headless)"
31test_boot
32grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot"
33wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
34
35echo "==> 4/6 cursor position tracks injected movement"
36INIT=$(cursor_state)
37[ -n "$INIT" ] || fail "no initial cursor position"
38IX=$(echo "$INIT" | awk '{print $1}')
39IY=$(echo "$INIT" | awk '{print $2}')
40IB=$(echo "$INIT" | awk '{print $3}')
41echo " initial: x=$IX y=$IY buttons=$IB"
42[ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB"
43
44qemu_hmp "mouse_move 20 7"
45S1=$(cursor_state)
46X1=$(echo "$S1" | awk '{print $1}')
47Y1=$(echo "$S1" | awk '{print $2}')
48[ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)"
49[ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)"
50echo " moved: x=$X1 y=$Y1 (delta +20,+7)"
51
52echo "==> 5/6 left button edge detection"
53qemu_hmp "mouse_button 1"
54S2=$(cursor_state)
55IB2=$(echo "$S2" | awk '{print $3}')
56[ "$IB2" = "L--" ] || fail "expected L--, got $IB2"
57
58qemu_hmp "mouse_button 0"
59S3=$(cursor_state)
60IB3=$(echo "$S3" | awk '{print $3}')
61[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3"
62echo " button: $IB2 → $IB3"
63
64echo "==> 6/6 mouse stream routing to msgrecv's task"
65# The spawned task id shifts with the task table (wm-input took slot 3) —
66# parse it rather than hardcoding.
67sendkeys "msgrecv"
68sleep 1
69MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)"
70[ -n "$MID" ] || fail "msgrecv did not spawn"
71echo " msgrecv task id: $MID"
72sendkeys "mouse $MID"
73qemu_hmp "mouse_move 20 7"
74qemu_hmp "mouse_button 1"
75sleep 0.5
76qemu_hmp "mouse_button 0"
77sleep 0.5
78sendkeys "ctrl-c"
79sleep 0.5
80grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged"
81
82echo ""
83echo "PASS — mouse round trip"
diff --git a/scripts/tests/test-msg.sh b/scripts/tests/test-msg.sh
new file mode 100755
index 0000000..7786beb
--- /dev/null
+++ b/scripts/tests/test-msg.sh
@@ -0,0 +1,98 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-msg.sh — automated scheduler message-queue round trip.
4#
5# Tests all message types (user, timer, key, mouse), real-input routing,
6# and the queue-full boundary.
7#
8# Usage: ./scripts/tests/test-msg.sh
9# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
10#
11set -euo pipefail
12TNAME="msg"
13# shellcheck source=../lib.sh
14source "$(dirname "$0")/../lib.sh"
15
16echo "==> 1/5 build (kernel + bins)"
17build_all
18
19echo "==> 2/5 stage system image"
20stage_image --force
21
22echo "==> 3/5 boot QEMU (headless)"
23test_boot
24
25echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)"
26# msgrecv's task id is assigned at spawn time and shifts when the task
27# table grows (the Windowed GUI added wm-input at slot 3) — read it from
28# the log instead of hardcoding.
29sendkeys "msgrecv"
30sleep 1
31MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)"
32[ -n "$MID" ] || fail "msgrecv did not spawn (no 'msgrecv: task N listening' in the log)"
33echo " msgrecv task id: $MID"
34
35sendkeys \
36 "send $MID user 42" \
37 "send $MID timer" \
38 "send $MID key enter" \
39 "send $MID key ctrl-c" \
40 "send $MID key f3" \
41 "send $MID mouse 5 -3 3" \
42 "sleep 800" \
43 "jobs"
44
45sendkeys "mouse $MID"
46qemu_hmp "mouse_move 20 7"
47qemu_hmp "mouse_button 1"
48qemu_hmp "mouse_move -5 -3"
49qemu_hmp "mouse_button 0"
50qemu_hmp "sendkey ctrl-c"
51
52# Queue-full probe: overflow a queue that nobody drains. The shell used
53# to be that task, but its exec pump now consumes its own User messages
54# as completion codes (q=0 at the prompt), and msgrecv consumes on wake —
55# the cursor task never calls recv, so its 16-slot ring fills for real.
56CID="$(awk '$2 == "cursor" { print $1 }' "$LOG" | tail -1)"
57[ -n "$CID" ] || fail "no cursor task in jobs output (cannot probe queue-full)"
58echo " queue-full target: task $CID (cursor, never recv'd)"
59
60sendkeys \
61 "send $CID user 1" \
62 "send $CID user 2" \
63 "send $CID user 3" \
64 "send $CID user 4" \
65 "send $CID user 5" \
66 "send $CID user 6" \
67 "send $CID user 7" \
68 "send $CID user 8" \
69 "send $CID user 9" \
70 "send $CID user 10" \
71 "send $CID user 11" \
72 "send $CID user 12" \
73 "send $CID user 13" \
74 "send $CID user 14" \
75 "send $CID user 15" \
76 "send $CID user 16" \
77 "send $CID user 17" \
78 "sleep 800" \
79 "jobs" \
80 "poweroff"
81
82echo "==> 5/5 wait for shutdown + verify"
83wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
84
85grep -aq "msgrecv: task $MID listening" "$LOG" || fail "msgrecv did not spawn"
86grep -aq "msgrecv\[$MID\]: User(42)" "$LOG" || fail "user message missing"
87grep -aq "msgrecv\[$MID\]: Timer" "$LOG" || fail "timer message missing"
88grep -aq "msgrecv\[$MID\]: Key(Enter)" "$LOG" || fail "key enter missing"
89grep -aq "msgrecv\[$MID\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing"
90grep -aq "msgrecv\[$MID\]: Key(F3)" "$LOG" || fail "key f3 missing"
91grep -aq "msgrecv\[$MID\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing"
92grep -aq "msgrecv\[$MID\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue"
93grep -aq "msgrecv\[$MID\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing"
94grep -aq "queue full" "$LOG" || fail "17th send did not report queue full"
95grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)"
96
97echo ""
98echo "PASS — message queues OK"