//! Build script, two jobs: //! //! 1. Compile the user programs in user/ into ELF executables in OUT_DIR. //! They're compiled with plain `rustc` for the built-in //! `x86_64-unknown-none` target (prebuilt `core`, so no build-std //! needed), as static executables (`relocation-model=static`) linked at a //! high user address -- which needs the `large` code model (the default //! ones assume code in the low/high 2 GiB). //! //! 2. Build the second QEMU disk, `target/disk.img` (64 MiB): //! * a GPT partition table (made with `sfdisk`) with one partition //! starting at sector 2048 (1 MiB, the usual alignment), //! * in it a FAT32 file system (`mkfs.vfat`), filled with `mtools` //! (`mmd`/`mcopy` write into an image file directly -- no root, no //! loop-mounting needed): everything in disk/ plus the user programs //! in /bin. //! The kernel finds the disk on the PCI bus, reads the GPT, mounts the //! FAT32 partition at /mnt/disk0 and loads programs from /mnt/disk0/bin. //! QEMU opens it with `snapshot=on`, so the kernel's writes never change //! this file (every boot and every test starts from the same disk). //! //! 3. Build the third QEMU disk, `target/ext2.img` (32 MiB): a whole-disk //! ext2 file system (no partition table) made by `mke2fs -d`, which //! copies a directory tree into the new file system. The tree is disk2/ //! plus generated files: big.txt (big enough for double-indirect blocks), //! sparse.bin (66 MiB of mostly holes, reaching the triple-indirect //! blocks), and two symlinks (one short = "fast", one long = "slow"). //! Mounted at /mnt/disk1 (drive C: in the shell). //! Two more small images are for `cargo test` only (see Cargo.toml //! test-args): ext2-4k.img (4 KiB blocks) and ext2-rev0.img (the original //! 1993 revision 0 format: fixed 128-byte inodes, no file types in //! directory entries). //! //! Needs dosfstools (mkfs.vfat), mtools (mmd, mcopy), fdisk (sfdisk) and //! e2fsprogs (mke2fs): //! sudo apt install dosfstools mtools fdisk e2fsprogs use std::{env, fs, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}}; /// Everything in /bin on the disk: the shell, its tools, and the test and /// demo programs of the earlier stages. const PROGRAMS: &[&str] = &[ "sh", "ls", "cat", "wc", "grep", "mkdir", "rm", "touch", "cp", "sleep", // the shell + tools "hello", "evil", "spin", "demo", "child", // stage 5/6 demos and tests ]; const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512; const PART_START: u64 = 2048; /// Leave 1 MiB at the end too (the backup GPT lives in the last 33 sectors). const PART_SECTORS: u64 = DISK_SECTORS - 2 * PART_START; fn main() { let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let user_dir = manifest.join("user"); println!("cargo:rerun-if-changed=user"); println!("cargo:rerun-if-changed=disk"); println!("cargo:rerun-if-changed=disk2"); println!("cargo:rerun-if-changed=build.rs"); for program in PROGRAMS { build_program(&user_dir, &out_dir, program); } build_disk(&manifest, &out_dir); build_ext2_disks(&manifest, &out_dir); } fn build_program(user_dir: &Path, out_dir: &Path, program: &str) { let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); let status = Command::new(&rustc) .args(["--edition", "2024", "--target", "x86_64-unknown-none"]) .args(["-C", "panic=abort", "-C", "opt-level=2", "-C", "debuginfo=0", "-C", "strip=debuginfo"]) .args(["-C", "relocation-model=static", "-C", "code-model=large"]) .arg("-C") .arg(format!("link-arg=-T{}", user_dir.join("link.ld").display())) .arg("-o") .arg(out_dir.join(program)) .arg(user_dir.join(format!("{program}.rs"))) .status() .expect("failed to run rustc for a user program"); assert!(status.success(), "building user program {program} failed"); } /// Find a tool in PATH or the sbin directories (often not in a user's PATH). fn tool(name: &str) -> PathBuf { let path = env::var("PATH").unwrap_or_default(); for dir in path.split(':').chain(["/usr/sbin", "/sbin", "/usr/bin"]) { let candidate = Path::new(dir).join(name); if candidate.is_file() { return candidate; } } panic!("`{name}` not found: install it with `sudo apt install dosfstools mtools fdisk e2fsprogs`"); } fn run(cmd: &mut Command, stdin: Option<&str>) { cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() }); let mut child = cmd.spawn().unwrap_or_else(|e| panic!("can't run {cmd:?}: {e}")); if let Some(input) = stdin { child.stdin.take().unwrap().write_all(input.as_bytes()).unwrap(); } let status = child.wait().unwrap(); assert!(status.success(), "{cmd:?} failed"); } fn build_disk(manifest: &Path, out_dir: &Path) { let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target")); fs::create_dir_all(&target_dir).unwrap(); let disk = target_dir.join("disk.img"); let part = out_dir.join("part.img"); // 1. The FAT32 file system, as its own image first. -s 1: one sector per // cluster (FAT32 needs >= 65525 clusters, so small disks need small // clusters). -n: volume label. let _ = fs::remove_file(&part); run( Command::new(tool("mkfs.vfat")) .args(["-F", "32", "-s", "1", "-n", "TYLERDISK", "-C"]) .arg(&part) .arg((PART_SECTORS / 2).to_string()), // size in KiB None, ); let mtools = |name: &str, args: &[&str]| { let mut cmd = Command::new(tool(name)); cmd.env("MTOOLS_SKIP_CHECK", "1").arg("-i").arg(&part).args(args); run(&mut cmd, None); }; // Copy disk/ (text files, subdirectories) to the root of the volume... for entry in fs::read_dir(manifest.join("disk")).unwrap() { let path = entry.unwrap().path(); mtools("mcopy", &["-s", path.to_str().unwrap(), "::/"]); } // ...and the user programs to /bin. mtools("mmd", &["::/bin"]); for program in PROGRAMS { mtools("mcopy", &[out_dir.join(program).to_str().unwrap(), &format!("::/bin/{program}")]); } // 2. The whole disk: zeros + GPT partition table (sfdisk works on image // files too), then the FAT32 image copied into the partition. let _ = fs::remove_file(&disk); fs::File::create(&disk).unwrap().set_len(DISK_SECTORS * 512).unwrap(); let layout = format!( "label: gpt\nstart={PART_START}, size={PART_SECTORS}, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name=\"fat32\"\n" ); run(Command::new(tool("sfdisk")).arg("--quiet").arg(&disk), Some(&layout)); let fat = fs::read(&part).unwrap(); assert_eq!(fat.len() as u64, PART_SECTORS * 512); let mut file = fs::OpenOptions::new().write(true).open(&disk).unwrap(); use std::io::{Seek, SeekFrom}; file.seek(SeekFrom::Start(PART_START * 512)).unwrap(); file.write_all(&fat).unwrap(); } /// The lines of big.txt: line i is `big.txt line {i:05}` (19 bytes with the /// newline), so tests can check any offset. 16000 lines = 304000 bytes: /// with 1 KiB blocks that's past the 12 direct + 256 single-indirect blocks. pub const BIG_LINES: usize = 16000; /// sparse.bin: 66 MiB, all holes except these 1 KiB chunks of text. With /// 1 KiB blocks, 40 MiB is in the double-indirect range and 65 MiB in the /// triple-indirect range (which starts at 12 + 256 + 256*256 KiB ~ 64.3 MiB). pub const SPARSE_SIZE: u64 = 66 * 1024 * 1024; pub const SPARSE_MARKS: &[u64] = &[0, 40 * 1024 * 1024, 65 * 1024 * 1024]; /// Copy a directory tree (files and directories only). fn copy_tree(from: &Path, to: &Path) { fs::create_dir_all(to).unwrap(); for entry in fs::read_dir(from).unwrap() { let path = entry.unwrap().path(); let dest = to.join(path.file_name().unwrap()); if path.is_dir() { copy_tree(&path, &dest); } else { fs::copy(&path, &dest).unwrap(); } } } fn build_ext2_disks(manifest: &Path, out_dir: &Path) { use std::os::unix::fs::symlink; let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target")); // The tree to copy in: disk2/ + generated files. let tree = out_dir.join("ext2-tree"); let _ = fs::remove_dir_all(&tree); copy_tree(&manifest.join("disk2"), &tree); let big: String = (0..BIG_LINES).map(|i| format!("big.txt line {i:05}\n")).collect(); fs::write(tree.join("big.txt"), &big).unwrap(); { use std::io::{Seek, SeekFrom}; let mut f = fs::File::create(tree.join("sparse.bin")).unwrap(); f.set_len(SPARSE_SIZE).unwrap(); // a file of holes... for &at in SPARSE_MARKS { // ...with a few 1 KiB blocks of text (mke2fs -d keeps all-zero // blocks as holes, so the image stays small). let text = format!("sparse.bin data at offset {at}\n"); let mut block = text.repeat(1024 / text.len()).into_bytes(); block.resize(1024, b'.'); f.seek(SeekFrom::Start(at)).unwrap(); f.write_all(&block).unwrap(); } } // A "fast" symlink: the target (< 60 bytes) fits inside the inode. symlink("hello.txt", tree.join("hello-link")).unwrap(); // A "slow" symlink: the target is too long, so it gets a data block. symlink("./deep/../deep/er/../er/./../../../docs/./deep/er/../../deep/er/nested.txt", tree.join("docs/long-link")).unwrap(); // -t ext2: no journal, no extents. -b 1024: 1 KiB blocks. -m 0: no // blocks reserved for root. -E root_owner: the root directory belongs to // root (the other files keep their owner from the build machine). // -F: it's a file, not a block device; just do it. let mke2fs = |img: &Path, size: &str, extra: &[&str], src: &Path| { let _ = fs::remove_file(img); run( Command::new(tool("mke2fs")) .args(["-q", "-F", "-t", "ext2", "-m", "0"]) .args(extra) .arg("-d") .arg(src) .arg(img) .arg(size), None, ); }; mke2fs(&target_dir.join("ext2.img"), "32M", &["-b", "1024", "-L", "RUSTBELT", "-E", "root_owner=0:0"], &tree); // Smaller tree for the test-only images. let small = out_dir.join("ext2-small"); let _ = fs::remove_dir_all(&small); copy_tree(&manifest.join("disk2/docs"), &small.join("docs")); fs::copy(manifest.join("disk2/hello.txt"), small.join("hello.txt")).unwrap(); fs::write(small.join("big.txt"), &big).unwrap(); symlink("hello.txt", small.join("hello-link")).unwrap(); mke2fs(&target_dir.join("ext2-4k.img"), "8M", &["-b", "4096", "-L", "FOURK", "-E", "root_owner=0:0"], &small); mke2fs(&target_dir.join("ext2-rev0.img"), "4M", &["-b", "1024", "-E", "root_owner=0:0,revision=0"], &small); }