Skip to content

codegen 类库的使用者体验:依赖声明从 4 条降到 1 条、proto 支持通配符(tools 传播 + 目录级 rerun) #359

Description

@speak-agent

#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:283hash_file(),读的是文件内容,目录读不出会变化的东西。

建议

rerun_if_changed 接受目录,哈希其递归文件列表(名字 + mtime/size,不必读内容)。Cargo 的 cargo:rerun-if-changed=<dir> 正是这么做的,是同一个问题的同一个解。

补上之后 glob 从「结构性不安全」变成一等用法,规则包才能提供 generate_all()


相关但应各自单独开 issue

这三条是做 grpc-m 时撞到的,与本 issue 同源(都出现在工具子构建路径上),但不是同一件事:

  1. windows 上工具子构建失败,原因未定位。 同一次 CI 里 tests/examples/protobuf / protobuf-upb / protobuf-gzip 全过(同一份 abseil+protobuf,作为普通依赖构建),只有子构建死在三个 abseil TU 的 .ddi 上。不是路径长度(那三个相对路径 31/46/47 字符,而同一子构建里 67 字符的编得好好的)。子构建内层 ninja 输出被汇总,真正的 scan 报错没进日志。因此 mcpp-index 暂时不在 windows 上声明 protoc 目标。
  2. 工具子构建会透出面向消费者的警告。 warning: src/protobuf.cppm: lib target without conventional lib root —— 子构建把 compat.protobuf 当 root 建,于是它的 lib target 被按 root 校验。子构建只关心那个 kind = "bin" 目标,不该透出这条。凡是写 tools = ["protoc"] 的用户都会看到。
  3. 依赖包自己的 [indices] 不生效。 只有 root 的 [indices] 参与解析,所以一个包无法为自己的依赖指定索引。用本地索引验证时会踩:现象是解析到了线上的版本。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions