m³OS docs GitHub

Phase 32

Build Tools

Multi-file C projects can be built inside the OS using a `make`-compatible build tool and shell scripts. This transforms the OS from a "compile one file" environment into one where real software projects can be developed.

Complete

Learning goal

Understand how `make` uses dependency graphs and file timestamps to do incremental builds.

Category

productivity

Why this phase matters

Multi-file C projects can be built inside the OS using a make-compatible build tool and shell scripts. This transforms the OS from a “compile one file” environment into one where real software projects can be developed.

How it works in m3OS

Phase 32 adds a POSIX-compatible make build tool and supporting utilities, enabling multi-file C projects to be built inside the OS. This transforms the OS from a “compile one file” environment into one where real software projects can be developed with incremental builds.

How it builds on earlier phases

This phase builds on Compiler Bootstrap and turns that foundation into Build Tools.

Implementation outline

  • Cross-compile pdpmake (or chosen make) with musl.
  • Add make binary to disk image at /usr/bin/make.
  • Verify basic Makefile parsing and rule execution.
  • Cross-compile ar, install, touch, wc, stat utilities.
  • Add them to the disk image.
  • Create the demo multi-file project in the disk image.
  • Boot, run make in the project directory, verify incremental builds work.
  • Test shell scripting: write a build.sh that automates a build-and-test cycle.

Task progress snapshot

45 of 76 tracked tasks are complete.

Documentation targets

  • Documentation deliverables have not been listed yet.

Deferred follow-on work

  • GNU make extensions
  • CMake, Meson, or other meta-build systems
  • ld as a standalone linker (TCC has a built-in linker)
  • nm, objdump, or other binary inspection tools
  • Autoconf/configure script support

Source materials

Important code

Code spotlight cards

Code spotlight

sys_fstat / sys_stat returns meaningful st_mtime

kernel/src/arch/x86_64/syscall.rs L4399-L4422

sys_fstat / sys_stat returns meaningful st_mtime. Why it matters: make compares st_mtime of source and object files to decide what to rebuild; a zero or stale timestamp breaks incremental builds entirely. Focus on `sys_linux_fstat` and `make` in `kernel/src/arch/x86_64/syscall.rs` while you trace the snippet.

fn sys_linux_fstat(fd: u64, stat_ptr: u64) -> u64 {
    let fd_idx = fd as usize;
    if fd_idx >= MAX_FDS {
        return NEG_EBADF;
    }
    let entry = match current_fd_entry(fd_idx) {
        Some(e) => e,
        None => return NEG_EBADF,
    };
    // x86_64 stat struct layout (144 bytes):
    //  0: st_dev (u64)      8: st_ino (u64)    16: st_nlink (u64)
    // 24: st_mode (u32)    28: st_uid (u32)    32: st_gid (u32)
    // 36: __pad0 (u32)     40: st_rdev (u64)   48: st_size (i64)
    // 56: st_blksize (i64) 64: st_blocks (i64)
    let mut stat = [0u8; 144];
    let blksize: u64 = 4096;

    // Determine mode, uid, gid, size, rdev based on backend type.
    let (mode, uid, gid, size, rdev): (u32, u32, u32, u64, u64) = match &entry.backend {
        FdBackend::Dir { path } => {
            // Try to get metadata from tmpfs for dirs under /tmp
            let (u, g, m) = dir_metadata(path);
            (0x4000 | m as u32, u, g, 0, 0)
        }

Code spotlight

utimensat / utimensat_now wrappers in syscall-lib

userspace/syscall-lib/src/lib.rs L1104-L1118

utimensat / utimensat_now wrappers in syscall-lib. Why it matters: C programs using musl's utime() / utimensat() interface must reach the kernel through these wrappers; without them touch cannot update timestamps. Focus on `utimensat` and `utime` in `userspace/syscall-lib/src/lib.rs` while you trace the snippet.

/// `utimensat(dirfd, path, times, flags)` — update file timestamps.
/// Sets atime to `atime_sec` and mtime to `mtime_sec` (seconds since epoch).
pub fn utimensat(path: &[u8], atime_sec: i64, mtime_sec: i64) -> isize {
    // struct timespec { tv_sec: i64, tv_nsec: i64 } × 2 = 32 bytes
    let times: [i64; 4] = [atime_sec, 0, mtime_sec, 0];
    unsafe {
        syscall4(
            SYS_UTIMENSAT,
            (-100_i64) as u64, // AT_FDCWD
            path.as_ptr() as u64,
            times.as_ptr() as u64,
            0,
        ) as isize
    }
}

Code spotlight

Evaluate pdpmake source

xtask/src/main.rs L358-L381

Evaluate pdpmake source. Why it matters: auditing pdpmake's libc dependencies before attempting to cross-compile prevents wasted iteration if a missing syscall would block the port. Focus on `build_pdpmake` and `stat` in `xtask/src/main.rs` while you trace the snippet.

/// Phase 32: Cross-compile pdpmake (POSIX make) for the OS.
///
/// Strategy: clone pdpmake from GitHub (or use cached clone in target/pdpmake-src/),
/// build with `musl-gcc -static -O2`, and place the resulting binary in
/// kernel/initrd/make.elf.
fn build_pdpmake() {
    let root = workspace_root();
    let initrd = root.join("kernel/initrd");
    let make_elf = initrd.join("make.elf");

    // Check cache.
    if make_elf.exists() && make_elf.metadata().map(|m| m.len() > 0).unwrap_or(false) {
        println!("pdpmake: using cached {}", make_elf.display());
        return;
    }

    // Clone pdpmake source.
    let pdpmake_src = root.join("target/pdpmake-src");
    if !pdpmake_src.join("main.c").exists() {
        println!("pdpmake: cloning from GitHub...");
        let _ = fs::remove_dir_all(&pdpmake_src);
        let status = Command::new("git")
            .args([
                "clone",

Key files

  • userspace/demo-project/ — A multi-file C project (userspace/demo-project/) is included in the disk image to
  • kernel/Cargo.toml — kernel/Cargo.toml — version bump to 0.32.0
  • kernel/src/arch/x86_64/syscall.rs — kernel/src/arch/x86_64/syscall.rs — stat timestamps, sys_utimensat, write mtime
  • kernel/src/fs/ramdisk.rs — kernel/src/fs/ramdisk.rs — register new binaries
  • userspace/coreutils-rs/src/{touch,stat_cmd,wc,ar,install}.rs — userspace/coreutils-rs/src/{touch,stat_cmd,wc,ar,install}.rs — Rust implementations
  • userspace/coreutils/{touch,stat,wc,ar,install}.c — userspace/coreutils/{touch,stat,wc,ar,install}.c — C reference implementations
  • userspace/syscall-lib/src/lib.rs — userspace/syscall-lib/src/lib.rs — Stat struct, utimensat/stat wrappers
  • xtask/src/main.rs — xtask/src/main.rs — pdpmake build, demo project population, new coreutils

How real OSes differ

  • GNU make extensions (pattern rules, functions, recursive make)
  • CMake, Meson, Ninja for cross-platform builds
  • Autoconf/Automake for portability
  • Package managers (apt, pkg) handle fetching, building, and installing software

Success criteria

  • `make --version` (or equivalent) runs inside the OS.
  • A Makefile with inference rules, variables, and phony targets is parsed correctly.
  • Incremental builds work: only modified files are recompiled.
  • `make clean` removes generated files.
  • `ar` can create a static library from `.o` files.
  • Shell scripts with loops, conditionals, and command substitution work.
  • The multi-file demo project builds, runs, and incrementally rebuilds correctly.

Related phases

Keep exploring the roadmap