#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")
结果:
原因
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] 参与解析,所以一个包无法为自己的依赖指定索引。用本地索引验证时会踩:现象是解析到了线上的版本。
#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今天 grpc-m 的用户要写:
后三条依赖全是为了 codegen,而且用户得知道 gRPC 的代码生成需要 protobuf 的 protoc。这是本该由库承担的知识。
目标形态:
1 条依赖 + 1 行 build.mcpp,且仍然保留上面那两个正确性性质 —— 严格优于 xmake。
两个缺口,都在引擎侧;描述符怎么写都绕不过去。
缺口 A:
tools/host-module不向消费者传播grpc的描述符里写[feature-deps.codegen]是合法的 ——featureDeps的值类型就是DependencySpec,和dependencies共用load_deps,所以tools/host-module都能解析。问题是解析了也传不到消费者。实测
结果:
原因
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走同一条通道是最贴合既有模型的做法:MCPP_DEP_<PKG>_BIN_<TOOL>同时发给该边的消费者;host-module同样沿 public 边传播,而不是只认 root 的dependencies;这样「库代用户拉起整条 codegen 工具链」就成立了,而这正是 vcpkg 的
"host": true、Conan 的tool_requires想表达的东西。缺口 B:
rerun_if_changed不支持目录要让
.proto用通配符,规则必须能扫目录。今天不能安全地扫:实测
build.mcpp 里 glob
proto/**,新增一个.proto之后: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 同源(都出现在工具子构建路径上),但不是同一件事:
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]参与解析,所以一个包无法为自己的依赖指定索引。用本地索引验证时会踩:现象是解析到了线上的版本。