10. Build system
This note covers MEP-53's build driver: how Driver.Build orchestrates cargo, what flags it sets, and the cross / wasm / embedded special cases.
Driver shape
type Driver struct {
CacheDir string // overrides ~/.cache/mochi/rust/
NoCache bool // disables on-disk cache
Deterministic bool // SOURCE_DATE_EPOCH=0 + RUSTFLAGS=-C strip=symbols + --locked
cargoPath string // resolved by resolveCargo()
}
func (d *Driver) Build(src, outDir string, target Target) (string, error)
The driver:
- Resolves cargo via
resolveCargo()(envMOCHI_CARGO, then~/.cargo/bin/cargo, thenPATH). - Parses, typechecks, lowers the Mochi source.
- Computes a SHA-256 cache key (source content + workspace path + target + Deterministic flag).
- Returns the cached binary if present (unless
NoCache). - Generates a workspace directory (cargo crate layout).
- Invokes cargo with target-specific flags.
- Copies the binary to
outDir(or, forTargetRustCrate, copies the crate tree). - Updates the cache.
Workspace layout
~/.cache/mochi/rust/<sha8>/
Cargo.toml
Cargo.lock # only when Deterministic=true
build.rs # only when extern fn is lowered
src/
main.rs
cffi/ # only when extern fn is lowered
*.c
*.h
<sha8> is the first 8 hex chars of SHA-256(source content + workspace path + target + Deterministic). This gives stable cache hits across runs of the same source and a clean separation between Deterministic and non-Deterministic builds (so a switch flip rebuilds from scratch).
Cargo.toml generation
generateCargoToml emits:
[package]
name = "mochi-emitted"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0" # phase 15 onwards
description = "Mochi program emitted by transpiler3/rust" # phase 15 onwards
repository = "https://github.com/mochilang/mochi" # phase 15 onwards
[dependencies]
mochi-runtime = { path = "..." } # path back to runtime3/rust/mochi-runtime/
itertools = "0.13" # only when query DSL is exercised
[build-dependencies]
cc = "1" # only when extern fn is lowered
[profile.release]
opt-level = 3
lto = false
codegen-units = 1
strip = "symbols" # only when Deterministic=true
codegen-units = 1 is required for reproducibility (multi-unit codegen has nondeterministic symbol layout across runs). lto = false because thin-LTO would be nondeterministic without further flags and we don't gain enough from it for the typical Mochi program.
Cargo invocation
For TargetNativeExecutable:
cargo build --release [--locked]
For TargetLinuxStaticX64:
cargo zigbuild --release --target x86_64-unknown-linux-musl [--locked]
For TargetLinuxStaticArm64:
cargo zigbuild --release --target aarch64-unknown-linux-musl [--locked]
For TargetWasm32WASI:
cargo build --release --target wasm32-wasip1 [--locked]
For TargetRustSource: no cargo invocation; just write <workDir>/src/main.rs to outDir.
For TargetRustCrate: no cargo invocation; copy the full workDir tree to outDir, skipping target/.
[--locked] is added when Driver.Deterministic=true. The Cargo.lock is generated by the driver itself (phase 16) with pinned versions of itertools and cc.
Deterministic env
When Driver.Deterministic=true:
SOURCE_DATE_EPOCH=0
RUSTFLAGS=-C strip=symbols
Cargo respects SOURCE_DATE_EPOCH for build timestamps. RUSTFLAGS=-C strip=symbols strips symbol tables that carry path-dependent timestamps. Combined with codegen-units = 1 and --locked, this gives SHA-256-byte-identical binaries on Linux.
On macOS, the LC_UUID load command is randomised per link by ld64. Phase 16 platform-skips macOS for this reason.
cargo-zigbuild
cargo zigbuild is a thin wrapper around cargo build that uses Zig's C compiler as the linker. This gives:
- Cross-compilation to musl Linux from any host (no rustup target install required, but a
rustup target addfor the target triple is still needed). - Static linking by default (musl libc is statically linked).
- Single-binary output that runs on any 2.6.32+ Linux kernel.
Installation: cargo install cargo-zigbuild (which also installs the Zig SDK). The driver detects it via exec.LookPath("cargo-zigbuild"); if missing, the Linux static targets fail clean.
wasmtime
The wasm gate (phase 17) runs the emitted .wasm under wasmtime:
wasmtime run path/to/foo.wasm
wasmtime is the canonical WASI Preview 1 runtime as of 2026-05 (wasmer and WasmEdge are alternatives but less ergonomic). Installation: cargo install wasmtime-cli or via Homebrew / package manager. The driver detects it via exec.LookPath("wasmtime"); missing wasmtime skips the wasm gate.
cffi/ sidecar + cc-rs build script
When the Mochi source contains extern fn name(...): U from "header.h", the driver emits:
cffi/header.hwith the user-supplied C header.cffi/header.c(or named per the source's header) with the user-supplied C body.build.rs:
fn main() {
cc::Build::new()
.file("cffi/header.c")
.compile("mochi_cffi");
println!("cargo:rerun-if-changed=cffi/header.c");
}
Cargo.tomladdscc = "1"as a build-dependency.
cargo's build-script integration calls build.rs before the main compilation. cc-rs auto-detects the host C compiler (cc, gcc, clang, msvc); when no C compiler is on PATH, the build fails with a clear "cc not found" message.
Cache invalidation
The cache key is SHA-256 of source content + workspace path + target + Deterministic. Any change to the source, the directory, the target, or the flag invalidates. The driver does not include the cargo version or the rustc version in the cache key, which is a known gap: bumping Rust toolchain version while keeping the cache could give stale binaries. Workaround: Driver.NoCache=true after a toolchain bump.
Cross-references
- design-philosophy for the "single-thread, no tokio" rationale.
- rust-target-portability for the target triple matrix.
- testing-gates for the gate that validates the build output.
- MEP-53 §5 for the normative target list.