RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / build.rs
6276 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! 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}};

/// 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=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();
}