Skip to content

Commit 2f6713d

Browse files
committed
feat(build): L3 build.mcpp — native imperative build program (v0.0.78)
A project-local build.mcpp (C++, Zig build.zig / Cargo build.rs model) compiled with the host toolchain and run before the main build. It emits stdout mcpp: directives that augment the build; a declared-input cache re-runs it only when its source/inputs/env change (the documented fix for the .mcpp_ok blind spot). - src/build/build_program.cppm: new module — compile+run, directive parser (cxxflag/cflag/link-lib/link-search/cfg/generated/rerun-if-changed/ rerun-if-env-changed), declared-input cache, apply to buildConfig - prepare.cppm: invoke after toolchain detect + L1 cfg merge, before modgraph scan; skipped under cross --target (host build/run). -x c++ so the .mcpp extension compiles as C++ - tests/e2e/89_build_mcpp.sh: define+generated source reach the build; cache short-circuits unchanged re-run; changed env forces re-run - docs/07-build-mcpp.md (+zh): user guide; design doc + version bump 0.0.78 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 43029d9 commit 2f6713d

10 files changed

Lines changed: 658 additions & 2 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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.

docs/07-build-mcpp.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# `build.mcpp` — a native build program
2+
3+
**English** | [简体中文](zh/07-build-mcpp.md)
4+
5+
Most projects need nothing more than `mcpp.toml`. When you need build-time logic —
6+
probe the host, generate a source, decide a flag from the environment — put a
7+
`build.mcpp` in your project root. It is the mcpp analog of Zig's `build.zig` and
8+
Cargo's `build.rs`, but written in **C++**: no second language, and it dogfoods
9+
mcpp itself.
10+
11+
mcpp compiles `build.mcpp` with your toolchain and runs it **before** the main
12+
build. The program talks to mcpp by printing `mcpp:` directives to stdout; those
13+
directives augment the build.
14+
15+
## Quick example
16+
17+
```cpp
18+
// build.mcpp
19+
#include <cstdio>
20+
#include <fstream>
21+
22+
int main() {
23+
// Generate a source the main build will compile + link.
24+
std::ofstream("src/generated.cpp") << "const char* banner() { return \"hi\"; }\n";
25+
26+
std::puts("mcpp:generated=src/generated.cpp"); // add it to the build
27+
std::puts("mcpp:cxxflag=-DHAVE_BANNER=1"); // define a macro for all C++ TUs
28+
29+
if (std::getenv("USE_FAST")) std::puts("mcpp:cxxflag=-DFAST_PATH=1");
30+
std::puts("mcpp:rerun-if-env-changed=USE_FAST"); // re-run me when USE_FAST changes
31+
return 0;
32+
}
33+
```
34+
35+
```bash
36+
mcpp build # compiles + runs build.mcpp, then builds the project
37+
```
38+
39+
## Directives
40+
41+
Print these to stdout (one per line). Any line that does not start with `mcpp:`
42+
is ignored, so you can freely log diagnostics.
43+
44+
| Directive | Effect |
45+
|---|---|
46+
| `mcpp:cxxflag=<flag>` | add `<flag>` to the C++ compile flags |
47+
| `mcpp:cflag=<flag>` | add `<flag>` to the C compile flags |
48+
| `mcpp:link-lib=<name>` | link `-l<name>` |
49+
| `mcpp:link-search=<dir>` | add a library search dir (`-L`; relative dirs resolve against the project root) |
50+
| `mcpp:cfg=<name>` | define `-D<name>` for both C and C++ |
51+
| `mcpp:generated=<path>` | add a generated source (relative to the project root) to the build |
52+
| `mcpp:rerun-if-changed=<path>` | re-run `build.mcpp` when this file changes |
53+
| `mcpp:rerun-if-env-changed=<VAR>` | re-run `build.mcpp` when this env var changes |
54+
55+
The program **requests** build edges (flags, libraries, sources). It cannot add a
56+
registry dependency — keep your dependency graph declarative in `mcpp.toml`
57+
(including platform-conditional `[target.'cfg(...)'.dependencies]`). `build.mcpp`
58+
is for *leaf* decisions: flags, codegen, link requirements.
59+
60+
## Incremental: declared inputs (no needless re-runs)
61+
62+
mcpp does **not** re-run `build.mcpp` on every build. It caches the program's
63+
directives and re-runs only when something it depends on changed:
64+
65+
- the `build.mcpp` source itself,
66+
- the toolchain,
67+
- any file you declared with `rerun-if-changed`,
68+
- any env var you declared with `rerun-if-env-changed`,
69+
- (or a `generated` output went missing).
70+
71+
So **declare your inputs**: if your program reads `config.h` or the `USE_FAST`
72+
variable, emit `mcpp:rerun-if-changed=config.h` / `mcpp:rerun-if-env-changed=USE_FAST`.
73+
This replaces the old "process exited 0, so assume it's fine" guesswork with an
74+
explicit input/output contract — incremental builds stay correct.
75+
76+
When nothing changed you'll see `build.mcpp up to date (cached)`; otherwise
77+
`build.mcpp compiling` / `running`.
78+
79+
## Notes & limits
80+
81+
- **Runs on the host.** `build.mcpp` compiles and runs with the host toolchain.
82+
Under a cross build (`mcpp build --target <triple>`) it is **skipped with a
83+
warning** for now (host-toolchain-for-cross is a planned follow-up). Gate
84+
*dependencies* on the target with `[target.'cfg(...)']` tables instead — those
85+
evaluate on the resolved target. See [05 - mcpp.toml Manifest Guide](05-mcpp-toml.md).
86+
- **CWD is the project root**, so relative paths (`src/generated.cpp`) land where
87+
you expect.
88+
- A non-zero exit from `build.mcpp` aborts the build and prints its output.

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- [04 - Building from Source & Contributing](04-build-from-source.md)
1010
- [05 - mcpp.toml Manifest Guide](05-mcpp-toml.md)
1111
- [06 - Workspaces](06-workspace.md)
12+
- [07 - build.mcpp Build Program](07-build-mcpp.md)

docs/zh/07-build-mcpp.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# `build.mcpp` —— 原生构建程序
2+
3+
[English](../07-build-mcpp.md) | **简体中文**
4+
5+
绝大多数工程只需要 `mcpp.toml`。当你需要构建期逻辑——探测主机、生成源码、依据环境
6+
决定某个编译开关——就在工程根目录放一个 `build.mcpp`。它是 mcpp 版的 Zig `build.zig`
7+
/ Cargo `build.rs`,但用 **C++** 编写:不引入第二种语言,而且 mcpp 自己吃自己的狗粮。
8+
9+
mcpp 用你的工具链编译 `build.mcpp`,并在主构建**之前**运行它。程序通过向 stdout 打印
10+
`mcpp:` 指令与 mcpp 通信,这些指令会增补本次构建。
11+
12+
## 快速示例
13+
14+
```cpp
15+
// build.mcpp
16+
#include <cstdio>
17+
#include <fstream>
18+
19+
int main() {
20+
// 生成一份源码,主构建会编译 + 链接它。
21+
std::ofstream("src/generated.cpp") << "const char* banner() { return \"hi\"; }\n";
22+
23+
std::puts("mcpp:generated=src/generated.cpp"); // 加入构建
24+
std::puts("mcpp:cxxflag=-DHAVE_BANNER=1"); // 为所有 C++ TU 定义宏
25+
26+
if (std::getenv("USE_FAST")) std::puts("mcpp:cxxflag=-DFAST_PATH=1");
27+
std::puts("mcpp:rerun-if-env-changed=USE_FAST"); // USE_FAST 变化时重跑我
28+
return 0;
29+
}
30+
```
31+
32+
```bash
33+
mcpp build # 编译 + 运行 build.mcpp,然后构建工程
34+
```
35+
36+
## 指令
37+
38+
把这些打印到 stdout(每行一条)。任何不以 `mcpp:` 开头的行都会被忽略,因此你可以
39+
自由打印诊断日志。
40+
41+
| 指令 | 作用 |
42+
|---|---|
43+
| `mcpp:cxxflag=<flag>` | 给 C++ 编译追加 `<flag>` |
44+
| `mcpp:cflag=<flag>` | 给 C 编译追加 `<flag>` |
45+
| `mcpp:link-lib=<name>` | 链接 `-l<name>` |
46+
| `mcpp:link-search=<dir>` | 增加库搜索目录(`-L`;相对路径按工程根目录解析) |
47+
| `mcpp:cfg=<name>` | 为 C 与 C++ 同时定义 `-D<name>` |
48+
| `mcpp:generated=<path>` | 把生成的源码(相对工程根目录)加入构建 |
49+
| `mcpp:rerun-if-changed=<path>` | 该文件变化时重跑 `build.mcpp` |
50+
| `mcpp:rerun-if-env-changed=<VAR>` | 该环境变量变化时重跑 `build.mcpp` |
51+
52+
程序**请求**构建边(开关、库、源码),它**不能**新增注册表依赖——请把依赖图保持在
53+
`mcpp.toml` 里声明式管理(包括平台条件依赖 `[target.'cfg(...)'.dependencies]`)。
54+
`build.mcpp` 用于*叶子*决策:开关、代码生成、链接需求。
55+
56+
## 增量:声明输入(避免无谓重跑)
57+
58+
mcpp **不会**每次构建都重跑 `build.mcpp`。它会缓存程序产出的指令,只有当它依赖的东西
59+
变化时才重跑:
60+
61+
- `build.mcpp` 源码本身,
62+
- 工具链,
63+
- 任何用 `rerun-if-changed` 声明的文件,
64+
- 任何用 `rerun-if-env-changed` 声明的环境变量,
65+
- (或某个 `generated` 产物丢失了)。
66+
67+
所以请**声明你的输入**:如果程序读了 `config.h``USE_FAST` 变量,就分别 emit
68+
`mcpp:rerun-if-changed=config.h` / `mcpp:rerun-if-env-changed=USE_FAST`。这用一份明确的
69+
输入/输出契约取代了过去「进程退出码为 0 就当成功」的猜测——让增量构建保持正确。
70+
71+
无变化时你会看到 `build.mcpp up to date (cached)`;否则是 `build.mcpp compiling` /
72+
`running`
73+
74+
## 说明与限制
75+
76+
- **在主机上运行。** `build.mcpp` 用主机工具链编译并运行。在交叉构建
77+
(`mcpp build --target <triple>`)下目前会**跳过并给出警告**(主机工具链交叉是计划中的
78+
后续项)。要按目标平台门控*依赖*,请改用 `[target.'cfg(...)']` 表——它们按解析后的目标
79+
求值。参见 [05 - mcpp.toml 工程文件指南](05-mcpp-toml.md)
80+
- **当前工作目录是工程根目录**,因此相对路径(`src/generated.cpp`)会落在你预期的位置。
81+
- `build.mcpp` 非零退出会中止构建并打印其输出。

docs/zh/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- [04 - 从源码构建 & 参与贡献](04-build-from-source.md)
1010
- [05 - mcpp.toml 工程文件指南](05-mcpp-toml.md)
1111
- [06 - 工作空间](06-workspace.md)
12+
- [07 - build.mcpp 构建程序](07-build-mcpp.md)

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.77"
3+
version = "0.0.78"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

0 commit comments

Comments
 (0)