Repository navigation
codegen 类库的使用者体验:依赖声明从 4 条降到 1 条、proto 支持通配符(tools 传播 + 目录级 rerun) #359
Description
#355 让「依赖产出的 host 工具」可用之后,grpc-m 成了第一个真实使用者:protoc 与 grpc_cpp_plugin 由 mcpp 自建,生成产物不再签入仓库,产物与官方 protoc 35.1 + 官方插件逐字节相同。
在正确性上这已经超过业界:工具的版本 ≡ 依赖的版本,所以「protoc 与 protobuf 运行时错配」——别处是运行期才炸、也是 protobuf 最经典的坑——在这里语法上无法表达;--target 下工具仍为构建机构建,交叉编译构造上就对。Conan 要专门引入 protobuf/<host_version> 占位符去逼近前者,xmake 在交叉时直接删掉 protoc。
但在简洁度上还输给 xmake。这个 issue 说的是这一半。
现状对比
| 依赖声明 | 加一个 .proto |
版本错配 | 交叉编译 | |
|---|---|---|---|---|
| CMake + vcpkg/Conan | 1 条 | 通配符 | 用户自己保证,运行期炸 | 自己找 host protoc |
| xmake | 1 条 | 通配符 | 同上 | 交叉时删掉 protoc |
| Bazel | 3 个 rule | 显式列 | 同上 | exec-config 正确 |
| mcpp 今天 | 4 条 | 要改 build.mcpp | 不可表达 ✅ | 构造正确 ✅ |
今天 grpc-m 的用户要写:
[dependencies.mcpplibs]
grpc = "1.83.0"
grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"] }
grpcgen = { version = "1.83.0", host-module = true }
[dependencies.compat]
protobuf = { version = "35.1", tools = ["protoc"] }// build.mcpp
import mcpp;
import grpcgen;
int main() { return grpcgen::generate({"helloworld"}) ? 0 : 1; }后三条依赖全是为了 codegen,而且用户得知道 gRPC 的代码生成需要 protobuf 的 protoc。这是本该由库承担的知识。
目标形态:
grpc = { version = "1.83.0", features = ["codegen"] }import mcpp;
import grpcgen;
int main() { return grpcgen::generate_all() ? 0 : 1; } // 扫 proto/**1 条依赖 + 1 行 build.mcpp,且仍然保留上面那两个正确性性质 —— 严格优于 xmake。
两个缺口,都在引擎侧;描述符怎么写都绕不过去。
缺口 A:tools / host-module 不向消费者传播
grpc 的描述符里写 [feature-deps.codegen] 是合法的 —— featureDeps 的值类型就是 DependencySpec,和 dependencies 共用 load_deps,所以 tools / host-module 都能解析。问题是解析了也传不到消费者。
实测
lib/mcpp.toml compat.protobuf = { version = "35.1", tools = ["protoc"] }
app/mcpp.toml mylib = { path = "../lib" }
app/build.mcpp mcpp::dep_bin("protobuf", "protoc")
结果:
protoc=[]
depdir=[]
原因
prepare.cppm:4113 —— 工具的环境变量只发给发出请求的那条边的消费者:
auto& v = toolEnvByConsumer[edge.consumerPackageIndex];lib → protobuf 这条边的消费者是 lib,不是 app。工具被构建了,但 app 的 build.mcpp 看不见它。
host-module 同理,而且更硬:prepare.cppm:4016 只遍历 m->dependencies,即 root 的 manifest。依赖包提供的规则模块根本不进入注册流程。
建议
边上已经有 DependencyVisibility::Public / Private(prepare.cppm:2662),public include dirs 就是靠它传播的。让 tools 走同一条通道是最贴合既有模型的做法:
- 一条 Public 边请求的工具,其
MCPP_DEP_<PKG>_BIN_<TOOL>同时发给该边的消费者; host-module同样沿 public 边传播,而不是只认 root 的dependencies;- 传播是加法,不改变「谁构建了这个工具」和 store 的键。
这样「库代用户拉起整条 codegen 工具链」就成立了,而这正是 vcpkg 的 "host": true、Conan 的 tool_requires 想表达的东西。
注:
dep_dir()目前也只覆盖直接依赖,所以传递依赖的 well-known types 目录同样取不到(见缺口 B 的注)。这条一并考虑。
缺口 B:rerun_if_changed 不支持目录
要让 .proto 用通配符,规则必须能扫目录。今天不能安全地扫:
实测
build.mcpp 里 glob proto/**,新增一个 .proto 之后:
=== 新增一个 .proto:无需改 build.mcpp ===
Finished dev in 0.01s ← 什么都没发生
fresh.pb.h 生成数量 = 0
build.mcpp 的缓存键是声明过的文件的哈希,新文件不在其中,于是 build.mcpp 不重跑、不产生新 action、新 .proto 静默不生成。这比「要求用户列名字」更坏,所以 grpc-m 最后选择了显式列表。
对目录调用 rerun_if_changed 也无效:build_program.cppm:283 走 hash_file(),读的是文件内容,目录读不出会变化的东西。
建议
让 rerun_if_changed 接受目录,哈希其递归文件列表(名字 + mtime/size,不必读内容)。Cargo 的 cargo:rerun-if-changed=<dir> 正是这么做的,是同一个问题的同一个解。
补上之后 glob 从「结构性不安全」变成一等用法,规则包才能提供 generate_all()。
相关但应各自单独开 issue
这三条是做 grpc-m 时撞到的,与本 issue 同源(都出现在工具子构建路径上),但不是同一件事:
- windows 上工具子构建失败,原因未定位。 同一次 CI 里
tests/examples/protobuf/protobuf-upb/protobuf-gzip全过(同一份 abseil+protobuf,作为普通依赖构建),只有子构建死在三个 abseil TU 的.ddi上。不是路径长度(那三个相对路径 31/46/47 字符,而同一子构建里 67 字符的编得好好的)。子构建内层 ninja 输出被汇总,真正的 scan 报错没进日志。因此 mcpp-index 暂时不在 windows 上声明 protoc 目标。 - 工具子构建会透出面向消费者的警告。
warning: src/protobuf.cppm: lib target without conventional lib root—— 子构建把 compat.protobuf 当 root 建,于是它的 lib target 被按 root 校验。子构建只关心那个kind = "bin"目标,不该透出这条。凡是写tools = ["protoc"]的用户都会看到。 - 依赖包自己的
[indices]不生效。 只有 root 的[indices]参与解析,所以一个包无法为自己的依赖指定索引。用本地索引验证时会踩:现象是解析到了线上的版本。
Activity
docs: #359 的架构设计 —— 两个缺口是同一个形状
fix(link): 命令长度上限从架构上消掉,不再补第八个洞 (2026.8.5.4) (#361)
feat(build): 库可以替用户拉起工具链,构建程序可以声明「一组文件」(#359) (2026.8.6.2) (#366)
fix(manifest): the per-target unknown-key check must not see the cond…
fix: 六处「答案已记录、做决定的代码不读它」+ 明确拒绝 system 工具链 (#538)