|
| 1 | +# L3 `build.mcpp` — native imperative build program (implementation design) |
| 2 | + |
| 3 | +Companion to `2026-06-29-manifest-environment-and-platform-design.md` (§L3). This |
| 4 | +doc nails down the concrete MVP shipped in mcpp 0.0.78. |
| 5 | + |
| 6 | +## What it is |
| 7 | + |
| 8 | +A project-local `build.mcpp` (a C++ source file, Zig's `build.zig` / Cargo's |
| 9 | +`build.rs` model — but in the project's own language, so no second language and it |
| 10 | +dogfoods mcpp). mcpp compiles it with the **host** toolchain and runs it **before** |
| 11 | +the main build; the program emits stdout directives that augment the main build. |
| 12 | + |
| 13 | +```cpp |
| 14 | +// build.mcpp |
| 15 | +#include <cstdio> |
| 16 | +int main() { |
| 17 | + std::puts("mcpp:cxxflag=-DHAVE_FEATURE=1"); |
| 18 | + std::puts("mcpp:link-lib=m"); |
| 19 | + std::puts("mcpp:rerun-if-env-changed=USE_FAST"); |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +## Directive protocol (Discipline 1 — structured output, not global mutation) |
| 24 | + |
| 25 | +The program communicates **only** via stdout lines; everything else is ignored |
| 26 | +(so the program may freely log to stderr/stdout). Recognized directives: |
| 27 | + |
| 28 | +| Directive | Effect | |
| 29 | +|---|---| |
| 30 | +| `mcpp:cxxflag=<flag>` | append `<flag>` to `buildConfig.cxxflags` | |
| 31 | +| `mcpp:cflag=<flag>` | append `<flag>` to `buildConfig.cflags` | |
| 32 | +| `mcpp:link-lib=<name>` | append `-l<name>` to `buildConfig.ldflags` | |
| 33 | +| `mcpp:link-search=<dir>` | append `-L<abs dir>` to `buildConfig.ldflags` (dir resolved against the project root) | |
| 34 | +| `mcpp:cfg=<name>` | append `-D<name>` to **both** cflags and cxxflags | |
| 35 | +| `mcpp:generated=<path>` | add `<path>` (relative to project root) to `buildConfig.sources` so the modgraph scanner picks it up | |
| 36 | +| `mcpp:rerun-if-changed=<path>`| declare a file input (re-run gate, see Discipline 2) | |
| 37 | +| `mcpp:rerun-if-env-changed=<VAR>` | declare an env input (re-run gate) | |
| 38 | + |
| 39 | +It *requests* graph edges (flags/libs/sources); it never silently mutates build state. |
| 40 | +Unknown `mcpp:` directives are ignored with a one-line warning (forward-compat). |
| 41 | + |
| 42 | +## Declared-I/O re-run contract (Discipline 2 — fixes the `.mcpp_ok` blind spot) |
| 43 | + |
| 44 | +The program is **not** re-run every build. Its parsed directives + declared inputs |
| 45 | +are cached at `<proj>/.mcpp/build.mcpp.cache`. On each build we re-run iff: |
| 46 | + |
| 47 | +- the cache is missing, **or** |
| 48 | +- the `build.mcpp` source content hash changed, **or** |
| 49 | +- the host compiler identity changed, **or** |
| 50 | +- any declared `rerun-if-changed` file's content hash changed (or the file vanished), **or** |
| 51 | +- any declared `rerun-if-env-changed` variable's current value changed, **or** |
| 52 | +- any `generated=` output path no longer exists. |
| 53 | + |
| 54 | +Otherwise the cached directives are reused without recompiling/running. This is the |
| 55 | +documented replacement for the bare `.mcpp_ok` success marker ("process exited 0 ≠ |
| 56 | +outputs correct"): a **declared-input / declared-output contract**. Hashing reuses |
| 57 | +the existing FNV-1a helpers (`mcpp::toolchain::hash_file` / `hash_string`). |
| 58 | + |
| 59 | +Because the applied directives land in `buildConfig.{cflags,cxxflags,ldflags}` — |
| 60 | +which already feed `canonical_compile_flags` → the fingerprint — and generated |
| 61 | +sources feed the modgraph, the **main** build is automatically sensitive to a |
| 62 | +changed `build.mcpp` output. The cache only avoids needless re-execution / file |
| 63 | +regeneration (which would otherwise bump mtimes and force spurious rebuilds). |
| 64 | + |
| 65 | +## Constraints (à la carte + supply-chain) |
| 66 | + |
| 67 | +- **Leaf only.** `build.mcpp` chooses flags/sources/codegen and emits link |
| 68 | + requirements; it must **not** gate the top-level dependency graph (that stays in |
| 69 | + the applicative L1 `[target.'cfg(...)']` tables). The directive set deliberately |
| 70 | + excludes "add a registry dependency". |
| 71 | +- **Host build, target cfg.** It compiles+runs on the **host**. The MVP therefore |
| 72 | + runs it only for **native** builds; under an explicit cross `--target` it is |
| 73 | + **skipped with a warning** (compiling it with the cross frontend would yield a |
| 74 | + binary that can't run on the host). Host-toolchain-for-cross is a follow-up. |
| 75 | +- **Isolation.** Executed as a build action: child-only env (no calling-process |
| 76 | + mutation, via `capture_exec`), declared inputs/outputs. Extending the same |
| 77 | + declared-I/O contract to recipe `install()` is future work. |
| 78 | + |
| 79 | +## Integration (src/build/prepare.cppm) |
| 80 | + |
| 81 | +New module `src/build/build_program.cppm` exports |
| 82 | +`run_build_program(Manifest&, root, hostCompiler, cppStandard)`. Called from |
| 83 | +`prepare.cppm` right after toolchain detection (`tc`), i.e. **after** target |
| 84 | +resolution + the L1 cfg-flag merge (buildConfig flags final) and **before** the |
| 85 | +modgraph scanner (so `generated=` sources are scanned). Compile line: |
| 86 | + |
| 87 | +``` |
| 88 | +<hostCompiler> -std=<cppStandard> -O0 -o <proj>/.mcpp/build.mcpp.bin <proj>/build.mcpp |
| 89 | +``` |
| 90 | + |
| 91 | +Compile/run failures are hard errors surfaced with captured output. |
| 92 | + |
| 93 | +**Host toolchain flags (sysroot).** A bare `g++ build.mcpp -o bin` works on a warm |
| 94 | +dev box but fails on a fresh sandbox: the sandbox compiler can't find crt/libc |
| 95 | +without the sysroot wiring the main build adds. So the compile reuses the host |
| 96 | +subset of that wiring from the resolved `Toolchain` (`host_base_flags`): GCC gets |
| 97 | +`--sysroot=<tc.sysroot>` (or, with no sysroot, the glibc-payload `-idirafter` / |
| 98 | +`-B` / `-L`) plus binutils `-B` and the link-runtime `-L`/`-rpath` dirs; Clang |
| 99 | +trusts its sibling `.cfg`. This mirrors `flags.cppm`'s GCC branch (kept a small |
| 100 | +parallel copy rather than refactoring the platform-sensitive `compute_flags` |
| 101 | +pre-release — a future unification should share one helper). |
| 102 | + |
| 103 | +**Artifacts under `target/`.** The compiled program + the declared-input cache live |
| 104 | +at `target/.build-mcpp/{build.mcpp.bin, build.mcpp.cache}` (a stable, non- |
| 105 | +fingerprint-keyed subdir, since build.mcpp runs before the fingerprint exists), so |
| 106 | +they persist across builds and aren't rebuilt needlessly. |
| 107 | + |
| 108 | +## Tests |
| 109 | + |
| 110 | +- `tests/e2e/89_build_mcpp.sh` — a `build.mcpp` emitting a `cxxflag` define + a |
| 111 | + `generated` source; assert the define reaches the TU (a `#ifdef` gate) and the |
| 112 | + generated source links. Second build asserts the cache short-circuits re-run; |
| 113 | + touching a declared `rerun-if-changed` input forces re-run. |
| 114 | + |
| 115 | +## Forward note — `.mcpp` as a first-class C++ extension |
| 116 | + |
| 117 | +The compiler doesn't know the `.mcpp` extension, so we compile build.mcpp with an |
| 118 | +explicit `-x c++` (otherwise the driver hands it to the linker as a "linker |
| 119 | +script"). This is a special case of a broader convention worth adopting: **inside |
| 120 | +an mcpp project, `.mcpp` is just C++.** A natural next step is to add `.mcpp` to the |
| 121 | +main build's source glob (`src/**/*.{cppm,cpp,cc,c}` → `+ .mcpp`) with the same |
| 122 | +`-x c++` treatment, so a project may use `.mcpp` for ordinary sources/modules — the |
| 123 | +extension becomes a marker of "an mcpp-native C++ file" rather than a separate |
| 124 | +language. `build.mcpp` is the first instance; the `-x c++` handling here is the |
| 125 | +seed. Deferred (out of MVP scope) but the direction is intentional. |
| 126 | + |
| 127 | +## Forward note — typed `import mcpp;` library (Zig-style code API over the wire protocol) |
| 128 | + |
| 129 | +The stdout `mcpp:` text protocol is the **substrate**: it decouples `build.mcpp` |
| 130 | +from mcpp's ABI/version, is language-agnostic, and ignores unknown directives |
| 131 | +(forward-compatible). This is the Cargo `build.rs` model. Zig sits at the other |
| 132 | +end — `build.zig` constructs the graph through a typed `std.Build` **library**. |
| 133 | + |
| 134 | +The chosen direction is the hybrid both ecosystems converge on (cf. Rust's |
| 135 | +`build-rs` crate): **keep the text protocol as the wire format, and ship a thin |
| 136 | +typed `import mcpp;` module on top** that just emits those strings. So instead of |
| 137 | + |
| 138 | +```cpp |
| 139 | +import std; |
| 140 | +int main() { std::puts("mcpp:link-lib=m"); } |
| 141 | +``` |
| 142 | +
|
| 143 | +a user writes the modules-first, no-headers form: |
| 144 | +
|
| 145 | +```cpp |
| 146 | +import mcpp; // bundled in the mcpp binary |
| 147 | +int main() { mcpp::link_lib("m"); mcpp::cxxflag("-DX"); } |
| 148 | +``` |
| 149 | + |
| 150 | +Design constraints for that iteration (per project direction): |
| 151 | +- **Bundled in the mcpp binary.** mcpp embeds the `mcpp` module source, writes + |
| 152 | + compiles it (cached BMI + object under `target/`, not rebuilt unless the |
| 153 | + toolchain changes), and makes it importable when compiling `build.mcpp`. |
| 154 | +- **No `import std;` requirement.** The `mcpp` module implements its I/O with |
| 155 | + minimal C-level primitives (no `import std;` in its interface), so neither it nor |
| 156 | + `build.mcpp` forces the std-module staging cost on a tiny build script. |
| 157 | + (Empirically, a standalone `import std;` needs `gcm.cache/std.gcm` staged at the |
| 158 | + compile CWD + `std.o` linked — GCC ignores `-fmodule-file=std=` for C++ — so the |
| 159 | + module is found via the same `gcm.cache/` staging the ninja backend uses.) |
| 160 | +- **Typed API mirrors the directive set** 1:1 (`cxxflag`/`cflag`/`link_lib`/ |
| 161 | + `link_search`/`cfg`/`generated`/`rerun_if_changed`/`rerun_if_env_changed`). |
| 162 | +- The string protocol stays as the documented low-level escape hatch. |
| 163 | + |
| 164 | +This is the next iteration (post-0.0.78); the 0.0.78 core ships the wire-protocol |
| 165 | +substrate so everything above layers on a stable foundation. |
| 166 | + |
| 167 | +## mcpp-index dual perspective |
| 168 | + |
| 169 | +A new workspace member `tests/examples/build-mcpp` whose `build.mcpp` emits a |
| 170 | +define consumed by `main.cpp`, exercising the feature through the real pipeline. |
0 commit comments