|
| 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 | +## Tests |
| 94 | + |
| 95 | +- `tests/e2e/89_build_mcpp.sh` — a `build.mcpp` emitting a `cxxflag` define + a |
| 96 | + `generated` source; assert the define reaches the TU (a `#ifdef` gate) and the |
| 97 | + generated source links. Second build asserts the cache short-circuits re-run; |
| 98 | + touching a declared `rerun-if-changed` input forces re-run. |
| 99 | + |
| 100 | +## Forward note — `.mcpp` as a first-class C++ extension |
| 101 | + |
| 102 | +The compiler doesn't know the `.mcpp` extension, so we compile build.mcpp with an |
| 103 | +explicit `-x c++` (otherwise the driver hands it to the linker as a "linker |
| 104 | +script"). This is a special case of a broader convention worth adopting: **inside |
| 105 | +an mcpp project, `.mcpp` is just C++.** A natural next step is to add `.mcpp` to the |
| 106 | +main build's source glob (`src/**/*.{cppm,cpp,cc,c}` → `+ .mcpp`) with the same |
| 107 | +`-x c++` treatment, so a project may use `.mcpp` for ordinary sources/modules — the |
| 108 | +extension becomes a marker of "an mcpp-native C++ file" rather than a separate |
| 109 | +language. `build.mcpp` is the first instance; the `-x c++` handling here is the |
| 110 | +seed. Deferred (out of MVP scope) but the direction is intentional. |
| 111 | + |
| 112 | +## mcpp-index dual perspective |
| 113 | + |
| 114 | +A new workspace member `tests/examples/build-mcpp` whose `build.mcpp` emits a |
| 115 | +define consumed by `main.cpp`, exercising the feature through the real pipeline. |
0 commit comments