//! 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). //! //! Needs dosfstools (mkfs.vfat), mtools (mmd, mcopy) and fdisk (sfdisk): //! sudo apt install dosfstools mtools fdisk use std::{env, fs, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}}; const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo", "demo", "child"]; 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=build.rs"); for program in PROGRAMS { build_program(&user_dir, &out_dir, program); } build_disk(&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`"); } 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(); }