Skip to content

Commit d1b2c11

Browse files
authored
feat(build.mcpp): 机制架构落地 —— 指令表收敛、协议契约、host 工具、构建图节点、规则包 (2026.8.5.1) (#356)
* feat(build.mcpp): 指令定义收敛成一张表 + 协议版本/缓存 epoch/运行上限 `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 **同样失败** ⇒ 与本次无关 * fix(ci): bootstrap pin named a version the index no longer serves `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 的其余改动无关 —— 是先前就红的,顺手修掉。 * feat: 依赖产出的 host 工具、构建图节点、规则包(架构文档步 2–6) 实现 `.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 的诊断) * docs: 记录步 2–6 的实施、五个开放问题的定案与两个硬坑 架构文档补 §10: - §8 五个开放问题全部定案(封闭命令词表 / JSON 载荷扩展 / check 默认并行 / mcpp.rules.* 只是约定 / 第 5 问问错了) - 两个实现坑写进文档而不是留在 commit 里: · `${mcpp.target_file:}` 必须解析成 build-dir 相对路径 —— ninja 按「边声明的 那个字符串」标识文件,指向同一份字节的绝对路径是**另一个节点** · 规则包的 BMI 不能走子构建 —— 两次独立解析的构建没有理由在 standard/dialect/ 编译器身份上一致,而不一致的表现是 CRC mismatch 不是清楚的错误 - 步 6 的核实结论:问的问题本身错了。阻塞点不是 topoOrder 的顺序,而是未扫描的 文件根本没有 graph.units 条目;解法用代码库已有的「声明而非发现」 * test: 覆盖 Form B 描述符的两处解析缺口 #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): 同步 host 工具、构建图节点、规则包三节 docs/zh/05 §2.14 与 docs/zh/07 的 dep_bin / action 两节 —— 与英文版一一对应。 * test(e2e): 钉住「改 action 的命令」也会生效 与「改 action 的输入」是两条不同的路径,必须都成立,否则改了生成器的调用方式会被 静默忽略: - 改**输入** → ninja 直接看到,重跑那条边 - 改 **build.mcpp** → ninja 跟踪的东西一样没变;是 build.mcpp 自己重跑(源码在它 的缓存键里)、重新声明出一条命令不同的 action,ninja 再因为 rule 的 command 变了而重跑 只测前者会让后者的回归完全隐形。 * fix(tool-store): upstream key 走传递闭包,与注释声称的一致 注释写的是「recursive (Merkle)」,实现只遍历了直接边 —— 注释**多声称了一个它没有 的性质**,这比没有注释更糟。 两个选择:改注释、或让代码兑现。选后者,因为传递闭包确实更对:对索引包来说直接边 够用(冻结的版本改不了自己的依赖),但 **path 依赖可以** —— 往下两层改一处,工具的 直接依赖列表纹丝不动,于是 store 里留下一个陈旧的二进制。那是**静默的错误产物**, 本项目为这一族问题付过不止一次学费,而闭包遍历几乎不要钱。 * refactor: 边图查询收敛成 mcpp.build.dep_graph,而不是再手写一遍 自审时发现的问题,而且正是本 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 全通过。 * fix: 合入前深度自审发现的四个问题(含一处内存安全、一处 workspace 回归) ## 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 全通过。 * fix(actions): ninja 转义与未解析目标 —— 自审第二轮 ## 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。 * test(e2e): 覆盖带 namespace 的包的两种工具寻址拼写 187 里的 toolpkg 没有 namespace,于是它的 canonical 名与去 namespace 的短名是同一个 串 —— 只发出一个环境变量,两种拼写那条路**从未被走到**。 补一个 `myns.tp` 的用例:消费者写 `myns.tp` 或 `tp` 都必须解析到同一个工具,与 mcpp::dep_dir() 两种拼写都接受保持一致。 * test: 让 directive 的路径断言跨平台成立 Windows CI 报 BuildDirectives.TransformsAreAppliedOnceAtParseTime 失败 —— 是**我这个 测试**的 bug,不是产品 bug: - 它把期望写成了字面 POSIX 串("/pkg/inc"),而 lexically_normal() 在 Windows 上会 转成反斜杠 - 更糟的是 "/abs/inc" 在 Windows 上**根本不是绝对路径**(没有 root name),于是 「绝对路径原样保留」那半条断言在那里测的是相反的东西 改成用同一套 std::filesystem 路径运算构造期望,并用 current_path() 派生出一个在**当前 平台上确实绝对**的输入。这样断言表达的是**行为**(「相对路径按包根解析」「绝对路径原样 保留」),而不是某一个平台的分隔符。 * fix: 真实案例(grpc-m/protobuf)暴露的三个 bug —— 合成 e2e 一个都没抓到 拿 #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 上同样失败**, 环境性)。 * docs(355): 记录真实案例验证结果 —— libprotoc 可构建,风险解除 设计文档 §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 约束 (导出结构体里不要放大的值类型)。 * docs(355): 状态改为已实施
1 parent 3f35d01 commit d1b2c11

39 files changed

Lines changed: 5321 additions & 287 deletions

.agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md

Lines changed: 477 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-08-05-issue355-dependency-host-tools-design.md

Lines changed: 871 additions & 0 deletions
Large diffs are not rendered by default.

.github/actions/bootstrap-mcpp/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ inputs:
2525
# `package.name`, so one of the two was simply unreachable — and which one
2626
# depended on the machine, which is why CI failed on `compat:lua` on
2727
# Windows and `mcpplibs.capi:lua` on Linux. Never pin below that.
28-
default: '2026.8.4.1'
28+
default: '2026.8.5.1'
2929
cache-target:
3030
description: also restore/save target/ (build artifacts + BMIs)
3131
required: false

.github/actions/setup-macos-llvm/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ inputs:
1515
# Floor imposed by the index, not a routine bump — see
1616
# .github/actions/bootstrap-mcpp/action.yml for why 0.4.69 is required
1717
# (two packages named `lua` in one repo need openxlings/xlings#381).
18-
default: '2026.8.4.1'
18+
default: '2026.8.5.1'
1919

2020
runs:
2121
using: composite

.github/workflows/bootstrap-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
# Dormant (workflow_dispatch only), but kept in step with the rest —
1818
# check_version_pins.sh holds it there. Floor: 0.4.69, below which the
1919
# index cannot resolve two packages that share a short name.
20-
XLINGS_VERSION: '2026.8.4.1'
20+
XLINGS_VERSION: '2026.8.5.1'
2121
steps:
2222
- uses: actions/checkout@v4
2323

.github/workflows/ci-fresh-install.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jobs:
152152
env:
153153
XLINGS_NON_INTERACTIVE: '1'
154154
run: |
155-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.4.1
155+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.5.1
156156
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
157157
158158
- name: Install mcpp and config mirror
@@ -292,7 +292,7 @@ jobs:
292292

293293
- name: Install xlings + mcpp
294294
run: |
295-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.4.1
295+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.5.1
296296
# Deliberately NOT writing to $GITHUB_PATH here. On container
297297
# images that declare no PATH in their config (opensuse/
298298
# tumbleweed), appending a single dir to GITHUB_PATH makes the
@@ -363,7 +363,7 @@ jobs:
363363
# (older ones carry minos=15 and refuse to start).
364364
# v0.4.51+: in-process sha256 — this image has no sha256sum
365365
# binary, so pinned fetches failed before it.
366-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.4.1
366+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.5.1
367367
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
368368
369369
- name: Install mcpp and config mirror

.github/workflows/ci-linux-e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ jobs:
123123
124124
- name: Bootstrap xlings + released mcpp
125125
run: |
126-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.4.1
126+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.5.1
127127
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
128128
xlings update
129129
xlings install mcpp -y -g

.github/workflows/cross-build-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ jobs:
118118
# release assets were uploaded in a broken state (records present,
119119
# blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX
120120
# half is handled by the marker-clear below.
121-
XLINGS_VERSION: '2026.8.4.1'
121+
XLINGS_VERSION: '2026.8.5.1'
122122
run: |
123123
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
124124
curl -fsSL -o "/tmp/${tarball}" \
@@ -255,7 +255,7 @@ jobs:
255255
- name: Bootstrap mcpp via xlings
256256
env:
257257
XLINGS_NON_INTERACTIVE: '1'
258-
XLINGS_VERSION: '2026.8.4.1'
258+
XLINGS_VERSION: '2026.8.5.1'
259259
run: |
260260
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
261261
curl -fsSL -o "/tmp/${tarball}" \

.github/workflows/release.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ jobs:
9696
# Pin xlings to a known-good version. The upstream install
9797
# script always grabs `latest` (no version override), so we
9898
# download + self-install manually to avoid broken releases.
99-
XLINGS_VERSION: '2026.8.4.1'
99+
XLINGS_VERSION: '2026.8.5.1'
100100
run: |
101101
if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then
102102
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
@@ -288,7 +288,7 @@ jobs:
288288
- name: Bootstrap mcpp via xlings
289289
env:
290290
XLINGS_NON_INTERACTIVE: '1'
291-
XLINGS_VERSION: '2026.8.4.1'
291+
XLINGS_VERSION: '2026.8.5.1'
292292
run: |
293293
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
294294
curl -fsSL -o "/tmp/${tarball}" \
@@ -358,11 +358,11 @@ jobs:
358358
# below are pinned to the same version as XLINGS_VERSION; they are
359359
# NOT interpolated from it, so check_version_pins.sh scans for them
360360
# explicitly (they were absent from the old lock-step comment).
361-
XLA="xlings-2026.8.4.1-linux-aarch64.tar.gz"
361+
XLA="xlings-2026.8.5.1-linux-aarch64.tar.gz"
362362
if curl -fsSL -o "/tmp/$XLA" \
363-
"https://github.com/openxlings/xlings/releases/download/v2026.8.4.1/$XLA"; then
363+
"https://github.com/openxlings/xlings/releases/download/v2026.8.5.1/$XLA"; then
364364
tar -xzf "/tmp/$XLA" -C /tmp
365-
XLBIN=$(find /tmp/xlings-2026.8.4.1-linux-aarch64 -path '*/bin/xlings' -type f | head -1)
365+
XLBIN=$(find /tmp/xlings-2026.8.5.1-linux-aarch64 -path '*/bin/xlings' -type f | head -1)
366366
if [ -n "$XLBIN" ]; then
367367
mkdir -p "$STAGING/$WRAPPER/registry/bin"
368368
cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings"
@@ -440,7 +440,7 @@ jobs:
440440
- name: Bootstrap mcpp via xlings
441441
env:
442442
XLINGS_NON_INTERACTIVE: '1'
443-
XLINGS_VERSION: '2026.8.4.1'
443+
XLINGS_VERSION: '2026.8.5.1'
444444
run: |
445445
if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then
446446
WORK=$(mktemp -d)
@@ -622,7 +622,7 @@ jobs:
622622
shell: bash
623623
env:
624624
XLINGS_NON_INTERACTIVE: '1'
625-
XLINGS_VERSION: '2026.8.4.1'
625+
XLINGS_VERSION: '2026.8.5.1'
626626
run: |
627627
# Captured before the `cd` below, in POSIX form: this step never
628628
# returns to the workspace, and GITHUB_WORKSPACE is a backslash

.xlings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"workspace": {
3-
"mcpp": "2026.8.3.2"
3+
"mcpp": "2026.8.4.1"
44
}
55
}

0 commit comments

Comments
 (0)