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.
Learning goal
Understand how `make` uses dependency graphs and file timestamps to do incremental builds.
Category
productivity
Components touched
Builds on
Unlocked by this phase
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
makebinary to disk image at/usr/bin/make. - Verify basic Makefile parsing and rule execution.
- Cross-compile
ar,install,touch,wc,statutilities. - Add them to the disk image.
- Create the demo multi-file project in the disk image.
- Boot, run
makein the project directory, verify incremental builds work. - Test shell scripting: write a
build.shthat 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
ldas 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
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
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
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
Phase 31
CompleteCompiler Bootstrap
Run a C compiler natively inside the OS. A C source file written at the shell prompt (using the editor from Phase 26) can be compiled and executed without leaving the OS. The ultimate target is self-hosting: the compiler compiles itself.
Phase 33
CompleteKernel Memory Improvements
The kernel heap allocator is robust, efficient, and recoverable. Out-of-memory conditions no longer panic the kernel — the allocator grows on demand and retries failed allocations. Buddy allocation, working `munmap()`, userspace-heap coalescing, and slab-cache infrastructure all land. Broad migration of hot kernel object families onto slab caches remains later work.