feat(build.mcpp): 机制架构落地 —— 指令表收敛、协议契约、host 工具、构建图节点、规则包 (2026.8.5.1) - #356
Merged
Conversation
`build.mcpp` 机制的架构地基。设计见 `.agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md`(本次实现步 0 + 步 1)。 ## 新增一条 directive 从改 9 处降到改 1 处 一条指令原本要在九个地方定义:`Directives` 字段、`parse_line` 分派、 `write_cache`、`read_cache`、`apply`、`cache_fresh` 的产物存在性校验、 `prepare.cppm` 的 `DirectiveMark`、`markDirectiveTail`、 `foldDirectiveTailIntoPrivateBuild`,再加内置 `mcpp` 模块的类型化包装。 `prepare.cppm:2611` 的注释还**自己承认**这个拆分仍不完整 (「Link/source/fingerprint residues stay at the call sites」)。 这是本仓库反复付过学费的「同一决策在 N 处推导」形态(#233/#240/#242/#344): 它不在你新增指令时失败,而是过一阵子在别的地方失败。 现在一条指令 = 新模块 `src/build/directives.cppm` 里 `kTable` 的**一行**, 解析 / 缓存读写 / 落盘应用 / 声明产物契约 / 私有作用域折叠全部由该行驱动。 **作用域是必填字段**:`include-dir` 之所以私有,是「构建期程序不得静默拓宽包的 公开接口」这条供应链规则,把它变成表里一列意味着下一条指令不回答这个问题就加不进来。 为什么是新模块而不是继续写 `build_program.cppm`:那个文件的匿名 namespace 在 clang 22 + C++20 modules + -O2 下会**误编译自己的邻居**(PR#332 证实一个从未被 调用的新函数就足以破坏 `contract_env`,PR#334 复现)。`mcpp.build.hostprogram` 当初就是为此拆出去的。 ## S1 线协议版本 用 `import mcpp;` 的程序在 `main` 之前自动声明 `mcpp:protocol=<N>`(占位符替换 注入,不硬编码 ⇒ 声明值与引擎校验值不可能漂移)。于是: - 声明的协议**高于**本 mcpp 所理解 → 拒绝执行并给升级提示。原行为是警告后忽略, 那会让「构建成功了但那个 flag 根本没到」静默发生。 - 双方已被证明一致 ⇒ **未知指令即错误**(同版本内只可能是拼写错误)。 手写 printf 的程序什么都不声明,**保留**警告并忽略。这条不对称就是兼容性契约: 那一面冻结在现有 11 条指令上,新能力只在类型化 API 里落地。 ## S2 缓存语义 epoch 缓存里的指令是命中时**原样重放**的,所以引擎改变某条指令的**解释**时旧条目必须 失效。纪律照抄 `cache_key::kCacheEpoch`(只在旧条目真不可用时 bump,与 mcpp 版本 号解耦)。一条本 mcpp 不认识的 `d` 记录同样作废整条条目 —— 只重放认识的那部分, 等于应用了程序所要求的一个**真子集**。 ## S3 运行上限 默认 600s,超时杀掉并让构建失败,错误**点名是哪个包**并说明怎么改。原先没有任何 上限:死循环或卡在网络读上的构建程序会让整个构建挂死且毫无诊断。 编译刻意不设上限 —— 与 `mcpp test` 同一条不对称纪律(run 有限 / build 无限): 编译跑得久通常正当(首次构建 std 模块就是分钟级),杀掉只会产生莫名其妙的失败。 `capture_exec_deadline` 顺带补上 `cwd` 形参 —— 没有它,加超时会**静默改变**构建 程序相对写入的落点。 ## 其它 - 内带 xlings 升级到 2026.8.5.1(13 个 pin 由 check_version_pins.sh 机器校验) - 版本 2026.8.5.1 ## 验证 - 单测 56/56(新增 `BuildDirectives` 21 例:表完整性、解析、协议三分支、 缓存 round-trip、apply 路由、私有折叠) - e2e:12 个 build.mcpp / 生成源用例全绿,新增 186 号覆盖协议三分支 + 超时 + 缓存四条失效路径 - `03_multi_module` 在本机失败,但已发布的 2026.8.4.1 **同样失败** ⇒ 与本次无关
`ci-aarch64-fresh-install` has been red on main since 2026-08-03 with
[error] xlings: version '2026.8.3.2' not found for 'mcpp'
[error] available: 2026.8.4.1
`.xlings.json` 的自举 pin 是**自举起点**,故意滞后、不随发布走(docs/09 §4),
所以平时不该动它 —— 但它的一条硬约束是**必须命名一个可安装的版本**。索引已不再
提供 2026.8.3.2,于是那条约束被打破了,这正是必须 bump 的场合(而不是「发版顺手
bump」那种误用)。
改到 2026.8.4.1:错误信息本身证明它可安装,且 ≤ 正在构建的 2026.8.5.1,
check_version_pins.sh 的方向约束满足。
与本 PR 的其余改动无关 —— 是先前就红的,顺手修掉。
实现 `.agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md` 的步 2–6, 接在同一文档步 0+1 之后。核心是把 build.mcpp 里「**配置**」与「**施工**」这两件被 混在一起的事分开:程序继续回答「这次构建长什么样」,而「把这批输入变成那批输出」 交给构建图。 ## 步 2 — 依赖产出的 host 工具(#355) 一个包能构建出消费者构建期需要的二进制(protoc / grpc_cpp_plugin / flatc / 转译器), 但消费者拿不到:`dep_dir()` 给的是源码树,依赖的 `kind="bin"` target 从不被构建。 **为什么不能是主图里的节点**:时序。build.mcpp 跑在 prepare 内,BuildPlan 还不存在、 build.ninja 更在其后 —— 主图产出的东西对需要它的程序永远来得太晚;叠上交叉编译连 架构都不对。所以走**嵌套的、面向 host 的子构建** + 全局 store(Cargo build-dependencies / Bazel exec configuration / vcpkg host:true / Conan tool_requires 的形状)。 **为什么便宜**:工具是可执行文件,与主构建零 ABI 接触 ⇒ 子构建可用工具包自己的 toolchain / profile / 依赖解析,不必与消费者一致。 单一版本轴(工具版本=依赖版本,protoc 与运行时错配结构上不可表达)、默认关闭、 成本门复用已有 features+required_features、全局 store 按 版本×host 工具链×feature× 依赖闭包 缓存。逃生舱 `[tools.overrides]` / `MCPP_TOOL_*` 直接指现成二进制并**跳过 构建** —— 每个同类系统都有这一条,且**刻意不进 cache key**。 前提是**工作目录外置**:一次 build 往工程根写 5 处,而「注册表包根共享/可能只读/ 绝不写入」是 build_program.cppm 自 G2 起的明文不变量。五处一起搬 —— 只搬一部分比 一处不搬更糟。 ## 步 3+4 — `mcpp:action=` 构建图节点 一个原语三种接线(role 只决定输出接到哪):source 进编译集、check 产 stamp 且**默认 与编译并行**、artifact 的输入是链接产物。顺序全由 ninja 的文件依赖决定,不需要 phase 机制 —— 这也是 artifact 不会像朴素 post 钩子那样把自己重复施加一遍的原因。 **必须写出输出文件名**(INV-D):prepare 期就定死源码集/fingerprint/模块图。畸形 action 是硬错误。命令是 argv 而非 shell 字符串,插值只有封闭的四个。 ## 步 5 — `host-module = true` 规则包 规则以普通 mcpp 库包分发,消费者 `import mcpp.rules.x;`。有版本、能测试、能发布, 用 **C++** 写 —— 不引入第二门语言(xmake Lua rule / Bazel Starlark)。 **关键**:规则模块与 build.mcpp **同一条命令、同一套 flag** 编译。不是优化 —— BMI 只 对与它在 standard/dialect/编译器身份上一致的编译可用,两次独立解析的构建没有理由 一致,而不一致表现为 `module X CRC mismatch` 而非清楚的错误。 ## 步 6 — 生成的 .cppm 核实结论:真正的阻塞点不是 topoOrder 的**顺序**(那只是名字普查 + 发射次序),而是 未扫描的文件**根本没有 graph.units 条目**。解法用代码库已有的答案 ——「声明而非发现」: action 的 `.provides()/.imports()` 让 mcpp 播下带该声明的占位文件,prepare 期扫描与 生成器将产出的内容一致,build 期由编译器自己的 P1689 复核。 ## 顺带修 `.xlings.json` 自举 pin 指向索引已不再提供的 2026.8.3.2 —— `ci-aarch64-fresh-install` 自 2026-08-03 在 main 上就是红的。自举 pin 平时不该动,但它有一条硬约束是必须命名 可安装的版本,这条被打破时正是该 bump 的场合。 ## 验证 - 单测 56/56 - e2e 19/21 通过;失败的 `07_static_library`(本机 binutils payload 的 ar 跑不起来)与 `09_path_dependency`(ninja missing dep BMI)在**已发布的 2026.8.4.1 上同样失败** ⇒ 环境性,非回归 - 新增 3 个 e2e:187(host 工具:端到端 / 成本门 / 默认关闭 / 错名报可用列表 / override)、 188(三种 role + 增量 + 失败的 check 让构建失败 + 畸形 action 被拒)、 189(规则包:导入生效 / 编辑规则触发重跑 / 缺 lib root 的诊断)
架构文档补 §10:
- §8 五个开放问题全部定案(封闭命令词表 / JSON 载荷扩展 / check 默认并行 /
mcpp.rules.* 只是约定 / 第 5 问问错了)
- 两个实现坑写进文档而不是留在 commit 里:
· `${mcpp.target_file:}` 必须解析成 build-dir 相对路径 —— ninja 按「边声明的
那个字符串」标识文件,指向同一份字节的绝对路径是**另一个节点**
· 规则包的 BMI 不能走子构建 —— 两次独立解析的构建没有理由在 standard/dialect/
编译器身份上一致,而不一致的表现是 CRC mismatch 不是清楚的错误
- 步 6 的核实结论:问的问题本身错了。阻塞点不是 topoOrder 的顺序,而是未扫描的
文件根本没有 graph.units 条目;解法用代码库已有的「声明而非发现」
#355 需要 xpkg (.lua) 解析器读两个它此前**根本没读**的键,而两处都是静默的 ——没读的键就只是「这个特性不存在」: - `targets.<x>.required_features`:描述符因此无法表达让可选 host 工具变得 可负担的成本门(compat.protobuf 的 protoc 会带进 libprotoc 的 ~157 个 TU, 只要运行时的消费者绝不能编译它)。没有这个门,target 要么总是构建、 要么根本拿不到。 - `deps` 的值此前只能是版本**字符串**,于是 Form B 描述符压根没有语法去向 自己的依赖请求工具。 之前的 e2e 只走了 Form A(path 依赖)路径,这两处没有任何覆盖。第三个用例钉住 「未知 dep 键要被记录而不是吞掉」——半配置的依赖且毫无信号,正是既有的 per-feature 键记录机制要防的那种失败。
docs/zh/05 §2.14 与 docs/zh/07 的 dep_bin / action 两节 —— 与英文版一一对应。
与「改 action 的输入」是两条不同的路径,必须都成立,否则改了生成器的调用方式会被 静默忽略: - 改**输入** → ninja 直接看到,重跑那条边 - 改 **build.mcpp** → ninja 跟踪的东西一样没变;是 build.mcpp 自己重跑(源码在它 的缓存键里)、重新声明出一条命令不同的 action,ninja 再因为 rule 的 command 变了而重跑 只测前者会让后者的回归完全隐形。
注释写的是「recursive (Merkle)」,实现只遍历了直接边 —— 注释**多声称了一个它没有 的性质**,这比没有注释更糟。 两个选择:改注释、或让代码兑现。选后者,因为传递闭包确实更对:对索引包来说直接边 够用(冻结的版本改不了自己的依赖),但 **path 依赖可以** —— 往下两层改一处,工具的 直接依赖列表纹丝不动,于是 store 里留下一个陈旧的二进制。那是**静默的错误产物**, 本项目为这一族问题付过不止一次学费,而闭包遍历几乎不要钱。
自审时发现的问题,而且正是本 PR 通篇在反对的那个模式:我给 tool store 的 upstream key **手写了一遍图遍历**,而 prepare.cppm 里**已经有**一个走同一张边图的递归遍历 (`self(self, …)`,构建缓存的 per-package key),带 memo 和环检测 —— 我那份是它的 一个更弱的副本。 核实后:`dependencyEdges` 有 14 处读者,其中大多数问的是同两个问题 ——「X 直接依赖 谁」和「X 的传递闭包」。两个 build.mcpp 调用点(发 `MCPP_DEP_<NAME>_DIR` 的那两处) 已经漂成了近乎逐字重复的两份,而 #355 正要再加第三个变体。 新增 `src/build/dep_graph.cppm`: - `direct_dependencies` / `transitive_dependencies`(按边类型模板化 —— `DependencyEdge` 是 prepare_build 里的局部结构体,把它搬出来的代价远大于这次要 还的债) - `name_spellings`:一个依赖可被寻址的两种拼写(canonical 与去 namespace 的尾段)。 不是图查询,但放这里理由相同:三处各自展开过同一个 rfind 分割,第四处正要出现。 **刻意不迁移**构建缓存那条递归 fold:它不是闭包查询 —— 它对每个节点算一个值(该包 完整的 cache key)、把环当**硬错误**而不是跳过、还顺带穿了一个 taint 标记。把它塞进 通用遍历要么丢掉这些性质,要么让抽象一路长到只描述一个调用者。**回答不同问题的两次 遍历不是重复。** 验证:单测 57/57;125/145(正是覆盖 MCPP_DEP_*_DIR 的两个)与 111/186/187/188/189 全通过。
Sunrisepeak
force-pushed
the
feat/build-mcpp-protocol-hardening
branch
from
August 5, 2026 06:38
ff5eafc to
c026ea3
Compare
## 1. 内存安全:内置 mcpp 模块的 action 缓冲区越界 `action::add()` 把上界**硬编码成 4096**,却被 `provides_[1024]` / `imports_[1024]` 调用 —— 最多越界写约 3KB。这段代码会被编进**每一个**用户的 build.mcpp,是本次改动 里最严重的一处。 修法:上界改成**参数**(`sizeof buf` 在调用点取)。一个「不挨着它所约束的数组」的 上界,正是会越界的那种形状。同时: - `command_` 原本 8192 却被 4096 的上界截半,现在按实际容量走 - 缓冲区放大到真实生成器调用的量级(protoc 带一堆 -I 的命令行很长) - 溢出不再静默截断:置 `overflow` 标记,引擎给出**点名容量限制**的诊断,而不是让 作者去找一个其实没错、只是太长的「拼写错误」 ## 2. workspace 回归:成员的 target/ 与 mcpp.lock 落到了 workspace 根 `workRoot` 在 `root` **尚未定稿**时就取了值 —— workspace 那段会把 `root` 改成选中的 成员(`root = memberDir`),于是成员的 `target/`、`mcpp.lock`、`.mcpp/`、 `compile_commands.json` 全落到 workspace 根。 CI 抓到了(macOS 与 Linux 的 `35_workspace` 报 "hello binary not found", `120_ws_root_indices` 报 "expected x.widget2 lock entry")。修法:把 `workRoot` 的 推导移到 workspace 段**之后**,并在原处留注释说明为什么不能在那里取。 教训记下来:我本地的回归面**太窄** —— 只跑了 build.mcpp 相关的用例,而这次改动动的是 「mcpp 往哪写」,workspace 才是它最敏感的形态。 ## 3. 并发:两个工程会共用同一个工具子构建的 scratch 目录 tool store 是**全局**的,所以两个工程可能同时要同一个工具。原实现共用 `<entry>/build`,于是它们并发写同一棵 ninja 树,先完成的那个还会 `remove_all` 把另一个的目录端掉。改成按**消费方**哈希隔离;被共享的是发布出来的 二进制,不是 scratch。哈希而非随机,这样重跑能复用自己的 scratch。 ## 4. JSON 有效性:除 \n 外的控制字符没转义 `\t` / `\r` 会通过 Windows 路径和日志文本进来,不转义就直接不是 JSON 了。 ## 文档 超时那条原文写成了跨平台生效 —— **Windows 上不生效**(进程启动器没有 kill-by-handle 的路径),与 `mcpp test --timeout` 是同一条限制。明说,而不是含糊过去。 ## 性能(实测,非断言) 在这台机器上**测不出可分辨的差异**:无 build.mcpp 的工程强制 prepare,15 次中位 OLD 156ms / NEW 140ms(min 62/96,max 181/194)—— 分布高度重叠,两个方向都出现过。 结构上也符合:新增路径全是 O(小),且未被使用时全部惰性(工具请求为空、 `plan.actions` 为空、hostModules 为空则一行都不多跑)。 ## 覆盖面的诚实说明 四个新 e2e 都声明 `# requires: gcc`,而 Windows runner **不提供** gcc 能力 —— 所以 action / host 工具 / 规则包这三条路在 Windows 上**未经验证**。它们在未使用时 完全惰性,不会影响既有 Windows 行为,但这不等于验证过。 验证:单测 57/57;`35_workspace` / `120_ws_root_indices` / `90_workspace_test` 三个 先前红的全绿;186–189 与 43/76/30/51/50/171/04/02 全通过。
## 1. action 命令里的 `$` 会被 ninja 吃掉 命令 token 只做了 shell 引号,没做 ninja 转义。**引号救不了它**:ninja 在 shell 被 调用之前就会展开 `$foo`,所以带字面 `$` 的 token(含 `$` 的路径、 `-Wl,-rpath,$ORIGIN`、一段 awk 程序)会被当成变量引用。 改成本文件其余地方一直用的那个配对(与 `include_dir_token` 同序):**先 ninja 转义, 再 shell 引号**。对普通 token 无影响 —— `escape_ninja_chars` 只碰空格 / `$` / `:`, 而 `shell_quote_arg` 对不含元字符的串逐字节原样返回。 这里每个元素**按构造就是一个 argv token**(类型化 builder 一次追加一个),这正是 逐 token 引号成立的前提 —— #331 表明手工拼出来的 flag blob **不**满足这个前提。 顺带把 `escape_ninja_chars` 从匿名 namespace 移出并导出:它此前是内部的,而 ninja_backend 需要它。让它继续内部化就意味着 ninja 转义规则第四份手写副本,而那 正是它们会漂的原因。 ## 2. `${mcpp.target_file:拼错}` 静默变成空串 未解析的引擎变量原本会被替换成空字符串,于是生成一条路径为空的边,ninja 在离 错误很远的地方报出来。现在是**硬错误**,并列出本次构建里存在的 target,还提示 「被 required_features 门住的 target 在那些 feature 未激活时不存在」。 ## 测试 188 补两条:字面 `$` 能原样到达工具;未知 target 引用报错并**点名**。 两条放在自己的最小工程里 —— `app` 的 main.cpp 故意依赖生成出来的符号,在它上面 换掉 build.mcpp 会让**链接**失败,那说明不了这两条在测什么(我第一版就是这么写的)。 验证:单测 57/57;13 个 e2e 全绿,含先前红的 35_workspace / 120_ws_root_indices。
187 里的 toolpkg 没有 namespace,于是它的 canonical 名与去 namespace 的短名是同一个 串 —— 只发出一个环境变量,两种拼写那条路**从未被走到**。 补一个 `myns.tp` 的用例:消费者写 `myns.tp` 或 `tp` 都必须解析到同一个工具,与 mcpp::dep_dir() 两种拼写都接受保持一致。
Windows CI 报 BuildDirectives.TransformsAreAppliedOnceAtParseTime 失败 —— 是**我这个
测试**的 bug,不是产品 bug:
- 它把期望写成了字面 POSIX 串("/pkg/inc"),而 lexically_normal() 在 Windows 上会
转成反斜杠
- 更糟的是 "/abs/inc" 在 Windows 上**根本不是绝对路径**(没有 root name),于是
「绝对路径原样保留」那半条断言在那里测的是相反的东西
改成用同一套 std::filesystem 路径运算构造期望,并用 current_path() 派生出一个在**当前
平台上确实绝对**的输入。这样断言表达的是**行为**(「相对路径按包根解析」「绝对路径原样
保留」),而不是某一个平台的分隔符。
拿 #355 的原始动机案例做端到端验证:让 mcpp 从 compat.protobuf 的源码构建出 protoc,再用它给一个消费者生成 .pb.cc。结果是**跑通了**(见下),但过程里连撞三个 bug —— 全部是我四个合成 e2e 覆盖不到的形态,因为它们用的都是 path 依赖(Form A)。 ## 1. Form B(compat 描述符)包根本不能当工具提供方 工具子构建走 `prepare_build`,而它从 `<root>/mcpp.toml` 读 manifest。**compat 形态的 包没有 mcpp.toml** —— 它的 manifest 是解析期从 `.lua` 描述符合成出来的。于是子构建 第一步就死在 `cannot open .../mcpp.toml`。 这不是小众情况:索引里绝大多数是 Form B,**protobuf 就是**。也就是说 gRPC 那条链 (protoc 来自 compat.protobuf)整个走不通。 修法:`BuildOverrides::preloaded_manifest` —— 由调用方把它**已经合成好**的 manifest 交给子构建。这比重新推导也更正确:重新推导可能得到与父构建**不同**的 manifest (L1 cfg 合并、feature 激活出的依赖那时已经折进去了)。 必须传**未经 feature 激活的那份**(`dep_manifests[i-1]`,而不是 `packages[i].manifest` ——后者是被 `apply()` 改过的副本):子构建会激活自己的 feature 集,从已激活的副本出发 会把同一批 feature 源码折进去两次。 > 顺带一个 GCC modules 约束:`preloaded_manifest` 一开始写成 > `std::optional<Manifest>`,而 `BuildOverrides` 是**导出**结构体 —— GCC 直接写不出 > module cluster(`failed to read compiled module cluster 529: Bad file data`,在 > mcpp.build.execute 导入它时炸)。换成 `shared_ptr<const Manifest>` 让导出布局保持 > trivial 即可,顺带也省掉每次工具构建拷贝一份 manifest。 ## 2. `targets.<x>.main` 不展开 `*/` 包装 glob Form B 包的源码在版本目录下的**包装目录**里,所以描述符写 `*/src/foo.cc` —— `*` 代表 tarball 顶层文件夹,描述符无从知道它叫什么。`[build] sources` 一直会展开 这种 glob,但 `main` 是按字面路径处理的,于是 ninja 拿到一个带 `*` 的路径,报 `missing and no known rule to make it`。 **#355 之前没有任何路径能走到这里**(依赖的 bin target 从不被构建),所以它一直没被 发现。现在在 manifest 定稿后、任何人读 `t.main` 之前解析掉;匹配不到恰好一个就报错。 ## 3. action 的**全部**输出都被当成翻译单元 最自然的生成器形态就是 protoc:同时产出 `foo.pb.cc` **和** `foo.pb.h`。原实现把每个 `role=source` 的输出都塞进编译集,于是两者算出同一个对象路径,撞上 `object path collision after uniqueness pass`。 头文件必须**由这条边产出**(别的 TU 要 include 它),但**绝不能被编译**。新增 `is_compilable_output()` 按扩展名过滤;非源码输出照旧声明给 ninja,只是不进编译集。 ## 验证结果:protoc 真的能被 mcpp 从源码构建出来 设计文档里标注的那条**未核实风险**(「libprotoc 能否被 mcpp 无 CMake 构建」)现在有 答案了 —— **能**: - 上游 `src/file_lists.cmake` 的 `libprotoc_srcs` 有 138 项,与 libprotobuf **零重叠** - 源码树里**没有** `.h.in` / `.cmake.in`,不需要 configure 步骤 - 全部编过,链接一次失败:缺 `upb_*` 符号 —— libprotoc 的 upb 生成器需要 upb 运行时, 那正是 compat.protobuf **已有**的 `upb` feature。把 target 的 `required_features` 写成 `{ "protoc", "upb" }` 即可 —— **这正是成本门机制该起的作用**, 不需要引擎改动 - 端到端跑通:`protoc` 建出 → `dep_bin` 拿到路径 → action 调用它生成 `demo.pb.cc`/`.pb.h` → 编译链接 → 程序输出 `NAME=mcpp` - store 命中实测:第二次 `rm -rf target` 后构建 **1.41s**,不重建 protoc ## 测试 188 补一条 companion 输出(`.cc` + `.h` 同时声明)的用例 —— 就是撞出 bug 3 的形状。 单测 57/57;11 个 e2e 全绿(`09_path_dependency` 在**已发布的 2026.8.4.1 上同样失败**, 环境性)。
设计文档 §11 标注的未核实风险(libprotoc 能否被 mcpp 无 CMake 构建)现在有答案: **能**。138 个 TU 全部编过,源码树无 .h.in/.cmake.in,唯一缺口是 upb 运行时, 而那是 compat.protobuf 已有的 feature —— 用 required_features 就能表达,零引擎改动。 ⇒ Phase 2 的 prebuilt provider 确认为纯优化,不是必需路径。 另记三个只有真实案例(Form B 包)才暴露的 bug,与一个 GCC modules 约束 (导出结构体里不要放大的值类型)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
实现
.agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md的全部六步。架构文档识别出的根症结:
build.mcpp把两件本质不同的事塞进了同一个点 —— 配置(这次构建长什么样)与施工(把这批输入变成那批输出)。它天生适合前者、被迫承担后者,而 codegen 没有增量、不能并行、失败无法归因、拿不到依赖产出的工具(#355),全都是这一个混淆的推论。mcpp:action=构建图节点(source / check / artifact)host-module = true规则包生态.cppm必须 eager」步 1 — 新增一条 directive:9 处 → 1 处
一条指令原本要在九个地方定义(
Directives字段 /parse_line/write_cache/read_cache/apply/cache_fresh/DirectiveMark/mark+fold/ 内置模块 API),而prepare.cppm:2611的注释自己承认这个拆分仍不完整:这是本仓库反复付过学费的「同一决策在 N 处推导」形态(#233/#240/#242/#344),特征是不在你新增指令时失败,而是过一阵子在别的地方失败。现在一条指令 = 新模块
src/build/directives.cppm里kTable的一行。Scope是必填字段:include-dir之所以私有,是「构建期程序不得静默拓宽包的公开接口」这条供应链规则,把它变成表里一列意味着下一条指令不回答这个问题就加不进来。步 0 — 三个今天就存在的缺口
import mcpp;的程序自动声明mcpp:protocol=N。声明的协议高于本 mcpp ⇒ 拒绝并提示升级(原行为是警告后忽略,会让「构建成功了但那个 flag 根本没到」静默发生);双方已被证明一致 ⇒ 未知指令即错误。裸 printf 程序不声明,保留警告并忽略 —— 这条不对称就是兼容性契约本身。d记录同样作废整条条目 —— 只重放认识的那部分等于应用了程序所要求的真子集。mcpp test同一条不对称纪律)。capture_exec_deadline顺带补cwd—— 没有它,加超时会静默改变相对写入的落点。步 2 — 依赖产出的 host 工具(#355)
为什么不能是主图里的节点:时序。
build.mcpp跑在 prepare 内,BuildPlan 还不存在、build.ninja更在其后 —— 主图产出的东西对需要它的程序永远来得太晚;叠上交叉编译连架构都不对。所以走嵌套的、面向 host 的子构建 + 全局 store(Cargo[build-dependencies]/ Bazel exec configuration / vcpkg"host": true/ Conantool_requires的形状)。为什么便宜:工具是可执行文件,与主构建零 ABI 接触 ⇒ 子构建可用工具包自己的 toolchain / profile / 依赖解析。对照
kind="lib"依赖,这几条必须一致。单一版本轴(protoc 与运行时错配结构上不可表达)、默认关闭、成本门复用已有
features+required_features、全局 store 缓存。逃生舱[tools.overrides]/MCPP_TOOL_*完全跳过构建 —— 每个同类系统都有(vcpkgVCPKG_HOST_TRIPLET、CMakeLLVM_NATIVE_TOOL_DIR、QtQT_HOST_PATH),且刻意不进 cache key。前提是工作目录外置:一次 build 往工程根写 5 处,而「注册表包根共享/可能只读/绝不写入」是
build_program.cppm自 G2 起的明文不变量。五处必须一起搬 —— 只搬一部分比一处不搬更糟。步 3+4 —
mcpp:action=一个原语三种接线(
role只决定输出接到哪,不是三套机制):source进编译集、check产 stamp 且默认与编译并行、artifact的输入是链接产物。顺序全由 ninja 的文件依赖决定,不需要任何 phase 机制 —— 这也是artifact不会像朴素的「post 钩子」那样把自己重复施加一遍的原因。必须写出输出文件名(INV-D)。畸形 action 是硬错误。命令是 argv 而非 shell 字符串,插值只有封闭的四个。
步 5 — 规则包
规则以普通 mcpp 库包分发,消费者
import mcpp.rules.x;。有版本、能测试、能发布,用 C++ 写 —— 不引入第二门语言(xmake Lua rule / Bazel Starlark),这正是build.mcpp存在的理由。关键:规则模块与
build.mcpp同一条命令、同一套 flag 编译。不是优化 —— BMI 只对与它在 standard/dialect/编译器身份上一致的编译可用,两次独立解析的构建没有理由一致,而不一致的表现是module X CRC mismatch而非清楚的错误。步 6 — 核实结论:问题本身问错了
设计问的是「dyndep 下
topoOrder是否冗余」。核实后它的两处用途(plan.cppm:708名字普查与顺序无关、:829发射次序)都不是阻塞点 —— 真正的阻塞是未扫描的文件根本没有graph.units条目。解法用代码库已有的答案「声明而非发现」(=scan_overrides的取舍):.provides()/.imports()让 mcpp 播下带该声明的占位文件。限制解除,且没动topoOrder一行。验证
BuildDirectives21 例)07_static_library(本机 binutils payload 的ar跑不起来)与09_path_dependency(ninja missing dep BMI)在已发布的 2026.8.4.1 上同样失败 ⇒ 环境性,非回归两个坑,写进了文档而不是留在 commit 里
${mcpp.target_file:}必须解析成 build-dir 相对路径。 ninja 用「边声明的那个字符串」标识文件,link 边声明bin/app;指向同一份字节的绝对路径是另一个节点,报missing and no known rule to make it。touch源码。我第一次就把「fast path 生效」误读成「引入了缓存回归」。关于
ci-aarch64-fresh-install该 job 自 2026-08-03 起在 main 上就是红的:
version '2026.8.3.2' not found for 'mcpp'。本 PR 把.xlings.json的自举 pin 修到2026.8.4.1,但那一步(ci-aarch64-fresh-install.yml:102)git clone的是 main 而不是 PR 分支(且actions/checkout刻意排在所有安装步骤之后),所以修复只能在合入后生效 —— 它在本 PR 上结构性地无法变绿。