foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Grok Bot <grokbot@localhost> | 2026-10-31 19:42:42 -0700 |
|---|---|---|
| commit | 8950fae6ee56f66ef0918e92a70b2a4a6d9d493f | |
| parent | 15035eada55eda065a0ceb6f556a1eb576fa2867 | |
| tree | browse files at this commit | |
rbsh: C: is the share (/mnt/share) when mounted, disks take B:, D:, ...; getty prefers /bin/rbsh; host tests (make rbsh-test)
| M | Makefile | +8 -1 |
| A | rust/rbsh/host-tests/parse_test.rs | +53 -0 |
| M | rust/rbsh/src/main.rs | +4 -2 |
| M | rust/rbsh/src/parse.rs | +30 -14 |
| M | rust/sys/src/bin/getty.rs | +8 -3 |
5 files changed, 103 insertions, 20 deletions
@@ -22,7 +22,7 @@ ALL_BIN := $(BIN_ORDER) $(FOUNDRY_C) $(FOUNDRY_PORTS) PORTS_OUT := $(OUT)/ports .DEFAULT_GOAL := image-.PHONY: help image rust fixtures cprogs ports staging kernel kernel-src run test test-reference compare k1-check musl clean distclean+.PHONY: help image rust fixtures cprogs ports staging kernel kernel-src run test test-reference compare k1-check rbsh-test musl clean distclean help: @echo "Foundry OS (userland for the Rustbelt kernel $(KERNEL_VERSION) @ $(KERNEL_REV))"@@ -38,6 +38,7 @@ help: @echo " make test headless boot + the shell self-test + expected-line checks" @echo " make test-reference the same headless test, but booting the kernel's own disk.img (baseline)" @echo " make compare compare out/$(IMAGE_NAME) with the kernel's own disk.img"+ @echo " make rbsh-test host unit tests for rbsh's parser and drive letters (no QEMU)" @echo " make k1-check build the kernel with RUSTBELT_DISK0=our image (K1) and check it boots ours" @echo " make musl fetch (cache/) + build musl $(MUSL_VERSION) into out/sysroot" @echo " make clean rm -rf out (distclean also removes cache/)"@@ -107,6 +108,12 @@ test-reference: kernel compare: image kernel FOUNDRY_BIN="$(FOUNDRY_C) $(FOUNDRY_PORTS)" FOUNDRY_FILES="$(FOUNDRY_FILES)" scripts/compare-image.sh $(IMAGE) $(KTARGET)/disk.img $(OUT) +# rbsh's parser is pure Rust, so it is also tested on the host.+rbsh-test:+ mkdir -p $(OUT)/host-tests+ rustc --edition 2024 --test rust/rbsh/host-tests/parse_test.rs -o $(OUT)/host-tests/parse_test+ $(OUT)/host-tests/parse_test+ # K1 (kernel 0.21.0+): the kernel's own build takes our image as disk 0. # Uses its own target dir, so out/kernel-target (the reference) is untouched. k1-check: image kernel-src
@@ -0,0 +1,53 @@+//! Host unit tests for rbsh's parser (rbsh/src/parse.rs is pure: no+//! syscalls). Run with `make rbsh-test` (plain `rustc --test`, no Cargo+//! target needed).+extern crate alloc;++#[path = "../src/parse.rs"]+mod parse;++use parse::*;++fn letters(table: &str) -> Vec<String> {+ assign_drives(table).iter().map(|d| format!("{}:{}", d.letter, d.path)).collect()+}++#[test]+fn contract_v1_drives() {+ // Ramfs root, our FAT32 disk and the kernel's three test disks.+ let t = "/ ramfs\n/tmp ramfs\n/mnt/disk0 fat32\n/mnt/disk1 ext2\n/mnt/disk2 exfat\n/mnt/disk3 btrfs\n";+ assert_eq!(letters(t), ["A:/", "B:/mnt/disk0", "C:/mnt/disk1", "D:/mnt/disk2", "E:/mnt/disk3"]);+}++#[test]+fn contract_v2_drives() {+ // ext2 root, FAT32 data disk, exFAT share (mounted last by the kernel).+ let t = "/ ext2\n/tmp ramfs\n/dev devfs\n/proc procfs\n/mnt/disk0 fat32\n/mnt/share exfat\n";+ assert_eq!(letters(t), ["A:/", "B:/mnt/disk0", "C:/mnt/share"]);+ let d = assign_drives(t);+ assert_eq!(d[0].fs, "ext2");+ assert_eq!(d[2].fs, "exfat");+}++#[test]+fn share_skips_c_for_more_disks() {+ let t = "/ ext2\n/mnt/disk0 fat32\n/mnt/disk1 ext2\n/mnt/disk2 btrfs\n/mnt/share exfat\n";+ assert_eq!(letters(t), ["A:/", "B:/mnt/disk0", "C:/mnt/share", "D:/mnt/disk1", "E:/mnt/disk2"]);+}++#[test]+fn share_without_disks_is_still_c() {+ assert_eq!(letters("/ ext2\n/mnt/share exfat\n"), ["A:/", "C:/mnt/share"]);+}++#[test]+fn translate_and_display_with_share() {+ let d = assign_drives("/ ext2\n/mnt/disk0 fat32\n/mnt/share exfat\n");+ assert_eq!(translate("C:/notes.txt", &d).as_deref(), Some("/mnt/share/notes.txt"));+ assert_eq!(translate("c:", &d).as_deref(), Some("/mnt/share"));+ assert_eq!(translate("B:bin", &d).as_deref(), Some("/mnt/disk0/bin"));+ assert_eq!(translate("A:/etc/motd", &d).as_deref(), Some("/etc/motd"));+ assert_eq!(display("/mnt/share/x", &d), "C:/x");+ assert_eq!(display("/mnt/disk0", &d), "B:/");+ assert_eq!(display("/etc", &d), "A:/etc");+}
@@ -1,4 +1,6 @@-//! sh -- the Rustbelt shell, a normal user program (/bin/sh on the disk).+//! rbsh -- the Foundry OS shell with DOS commands and drive letters, a+//! normal user program: /bin/rbsh on the root partition (boot contract v2;+//! /bin/sh is dash there), or /bin/sh on the FAT32 disk under contract v1. //! //! sh interactive: prompt, line editing, history //! sh script.sh run the commands in a file (echoing each one)@@ -59,7 +61,7 @@ Rustbelt shell commands start prog args run it in the background (or: prog args &) a | b | c pipes; > file >> file < file redirection Ctrl+C interrupt the running program (SIGINT); Ctrl+Z stops it- A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt+ A: is /, B: the data disk, C: the share, D: ... more disks (see `drives`): cd B:, type C:/README.TXT "; /// A background (or stopped) job: one pipeline, one process group.
@@ -161,25 +161,41 @@ pub struct Drive { /// Assign letters from the kernel's mount table (the text from the /// `mounts` syscall: one "path fstype" per line, in mount order).-/// A: is always the root; B:, C:, ... go to the mounts under /mnt (the-/// disks), in the order they were mounted. Other mounts such as /tmp are-/// reachable through A: (A:/tmp).+///+/// * A: is always the root, `/`.+/// * C: is the share, `/mnt/share` (boot contract v2's exFAT partition),+/// whenever it is mounted.+/// * The disks (the other mounts under /mnt: /mnt/disk0, /mnt/disk1, ...)+/// take the free letters in mount order: B:, then D:, E:, ... (or B:, C:,+/// D:, ... when there is no share, as under contract v1).+///+/// Other mounts such as /tmp are reachable through A: (A:/tmp). pub fn assign_drives(mount_table: &str) -> Vec<Drive> {- let mut drives = Vec::new();- let root_fs = mount_table+ const SHARE: &str = "/mnt/share";+ let mounts: Vec<(&str, &str)> = mount_table .lines()- .find_map(|l| l.split_once(' ').filter(|(p, _)| *p == "/").map(|(_, fs)| fs))- .unwrap_or("ramfs");- drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs.trim()) });+ .filter_map(|l| l.split_once(' '))+ .map(|(p, fs)| (p, fs.trim()))+ .collect();+ let root_fs = mounts.iter().find(|(p, _)| *p == "/").map(|(_, fs)| *fs).unwrap_or("ramfs");+ let mut drives = Vec::new();+ drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs) });+ let share = mounts.iter().find(|(p, _)| *p == SHARE);+ if let Some((path, fs)) = share {+ drives.push(Drive { letter: 'C', path: String::from(*path), fs: String::from(*fs) });+ } let mut letter = b'B';- for line in mount_table.lines() {- if let Some((path, fs)) = line.split_once(' ') {- if path.starts_with("/mnt/") && letter <= b'Z' {- drives.push(Drive { letter: letter as char, path: String::from(path), fs: String::from(fs.trim()) });- letter += 1;- }+ for (path, fs) in mounts.iter().filter(|(p, _)| p.starts_with("/mnt/") && *p != SHARE) {+ if share.is_some() && letter == b'C' {+ letter += 1; // C: is the share's+ }+ if letter > b'Z' {+ break; }+ drives.push(Drive { letter: letter as char, path: String::from(*path), fs: String::from(*fs) });+ letter += 1; }+ drives.sort_by_key(|d| d.letter); // `drives` lists them A:, B:, C:, ... drives }
@@ -26,7 +26,10 @@ mod ulib; use ulib::*; -const SHELL: &str = "/mnt/disk0/bin/sh";+/// The shell to start, first one that exists: rbsh on the root partition+/// (boot contract v2), else the FAT32 disk's /bin/sh (contract v1, where+/// that is rbsh).+const SHELLS: [(&str, &str); 2] = [("/bin/rbsh", "rbsh"), ("/mnt/disk0/bin/sh", "sh")]; /// The child's side (steps 2-5). Only returns on failure. fn become_shell(dev: &str) -> i64 {@@ -44,8 +47,10 @@ fn become_shell(dev: &str) -> i64 { } let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n"; write(1, greeting.as_bytes());- let err = execve(SHELL, &["sh"], &environ());- eprintln!("getty: {}: {}", SHELL, strerror(err));+ let mut st = Stat::default();+ let (path, argv0) = SHELLS.iter().copied().find(|(p, _)| stat(p, &mut st) == 0).unwrap_or(SHELLS[1]);+ let err = execve(path, &[argv0], &environ());+ eprintln!("getty: {}: {}", path, strerror(err)); 127 }