Commit 3ce6cb5
authored
feat(protobuf): expose protoc as a host tool (#155)
* feat(protobuf): expose protoc as a host tool
compat.protobuf shipped the libprotobuf runtime and nothing else, so a
consumer that needed generated code had to find a protoc somewhere and
guarantee by hand that it matched the runtime being linked. That mismatch is
a RUNTIME failure — the classic protobuf footgun — and it is the whole reason
Conan carries a `protobuf/<host_version>` placeholder and xmake deletes protoc
outright when cross-compiling.
mcpp 2026.8.5.1 makes it inexpressible: a `kind = "bin"` target in the
descriptor, asked for by the consumer as `tools = ["protoc"]`, is built for
the BUILD machine out of the same package the consumer links. The tool's
version IS the dependency's version, and `--target` does not change that
because a code generator has to run here.
- pkgs/c/compat.protobuf.lua: add the `protoc` target
(main = "*/src/google/protobuf/compiler/main.cc") plus a `protoc` feature
carrying the 138 entries of upstream's own `libprotoc_srcs` from
src/file_lists.cmake — zero overlap with the libprotobuf source set, and no
configure step (the tree holds no .h.in/.cmake.in). The target declares
required_features = { "protoc", "upb" }: libprotoc's upb generator links the
upb runtime, and without it the link fails on missing `upb_*` symbols, so
the descriptor states the constraint instead of leaving consumers to
discover it. Consumers who only link the runtime compile none of this.
- tests/examples/protobuf-protoc: the complement of tests/examples/protobuf.
That member deliberately uses NO generated code; this one is generated code
end to end — nested messages, enum, repeated field, map, oneof, a
well-known-type import and reflection over the generated pool, serialized
and parsed back. Verified locally: the four files this toolchain generates
for grpc-m's helloworld.proto are BYTE-IDENTICAL to the officially generated
stubs checked into that repo.
- CI pin -> 2026.8.5.1. This is a floor, not a routine bump: `tools = [...]`
does not parse before it ("tools must be a string, inline dep table, or
nested table"), confirmed against 2026.8.3.3.
- index.toml: min_mcpp deliberately UNCHANGED. It is a gate — an older client
failing it cannot open the index at all (mcpp#349) — and this change does
not make any descriptor unreadable: 2026.8.3.3 still parses compat.protobuf
with an empty unknown_keys. Only the consumer spelling needs the newer mcpp,
and that lives in a consumer's own mcpp.toml. latest_mcpp, an advisory hint
with no gate behaviour, tracks what CI validates against.
- docs: shape H (host tool provider) in package-types.md + zh, README rows.
Includes the one sharp edge: protoc does not embed the well-known types, so
`import "google/protobuf/timestamp.proto"` needs an -I derived from
mcpp::dep_dir("protobuf").
* fix(protobuf): protoc 目标不声明在 windows;并让本 PR 不再触发全量 workspace
CI 暴露了两件独立的事。
── 1. windows:工具子构建失败,所以不在那里声明这个目标 ──────────────────
`workspace (windows)` 里 59 个成员挂了 3 个,其中一个是本 PR 新加的
protobuf-protoc。**不是 protobuf 的问题,也不是 flags 的问题** —— 同一次运行里
tests/examples/protobuf、protobuf-upb、protobuf-gzip 全部通过,用的是同一份
abseil + protobuf 源码,只不过是作为普通依赖构建的。只有**工具子构建**会死:
error: building host tool 'compat.protobuf:protoc' failed
error: cannot read 'obj/compat_abseil/…/absl/time/internal/test_util.cc.ddi'
…/cctz/src/time_zone_posix.cc.ddi、…/cctz/src/zone_info_source.cc.ddi
**不是路径长度。** MAX_PATH 是最顺手的猜测,而且是错的:这三个相对路径分别是
31 / 46 / 47 字符,而同一个子构建里
`absl/container/internal/hashtablez_sampler_force_weak_definition.cc`(67 字符)
编得好好的。子构建内层 ninja 的输出是被汇总过的,真正的 scan 报错没进日志,
**原因未知**,也没有 windows 机器可复现。
在一个建不出来的平台上声明这个目标,等于把一个没有解释的失败甩给用户。所以
windows 平台块里放一份只含 `protobuf` 的 `targets`(平台块的 targets 覆盖顶层,
compat.vulkan 早就在用这个机制),等有 windows 环境查清楚再放开。
成员也随之按 `[target.'cfg(os)']` 分平台:linux/macOS 要 `tools = ["protoc"]`,
windows 只要运行时;build.mcpp 在 windows 上直接返回,tests/codegen.cpp 编成一条
**显式 skip** —— 一个什么都没验证却绿着的测试比红的更糟。
── 2. 还原 index.toml:它让 CI 跑全量 workspace 并超时 ────────────────────
成员选择把 `index.toml` 归到「未分类改动」→ `MEMBERS=__ALL__` → 59 个成员全建。
linux leg 因此**恰好跑满 150 分钟 timeout 被取消**(09:04:17 → 11:34:34),
其间每一条测试都是通过的。macOS 83 分钟侥幸跑完。
而我改它只是把 `latest_mcpp` 从 2026.8.3.3 抬到 2026.8.5.1 —— 一个**引擎里零消费者**
的提示字段(已 grep 确认:只解析、从不使用)。为一个纯装饰的字段换来 2.5 小时全量
构建并超时,不值。还原之后选择器只挑 protobuf 相关成员。
`min_mcpp` 本来就不动,那个判断没变。
* ci: 打开全局包缓存,pin 到 2026.8.5.3
全量运行(改 validate.yml 会强制全量,这是刻意的)跑不完:
linux 2h30m22s -> 跑满 150 分钟 timeout 被取消
windows 2h20m08s -> opencv-module / opencv-module-dnn 链接失败
macos 1h26m45s -> 通过
两个原因,各有各的修法。
── 1. 每个 member 都从零重编所有依赖 ────────────────────────────────────────
这一步一直设着 `MCPP_BUILD_CACHE: local`,把**全局包构建缓存**关掉了。它是
mcpp#344 的临时规避:对象路径消歧按「整个 build dir 的 basename 冲突」触发,
即取决于**消费方**拉了哪些包,而当时的缓存键只覆盖依赖自身,于是一个条目能装下
两种布局,ninja 在图加载阶段就死。
**#344 已在 2026.8.3.4 修掉**(per-package Merkle 键,覆盖随消费方变化的布局),
理由不存在了。而留着它是有代价的:59 个 member 大量共享 abseil / protobuf /
opencv,关掉缓存意味着同一份源码被反复编译 —— opencv 系每个 10~20 分钟。
打开之后,同一个 (包, 版本, features, 工具链) 每次运行只建一次,后面的 member
直接命中。
> 注:CI 的 actions/cache 只缓存 `~/.mcpp/registry`(工具链与包源码),不缓存
> `~/.mcpp/build-cache`。所以本次拿到的是**同一次运行内**的复用 —— 而那正是
> 超时的来源。跨运行缓存是另一件事:GitHub 每仓库 10GB 上限,opencv 的产物有
> 撑爆并引发频繁驱逐的风险,先用数据说话再决定。
── 2. windows 上 opencv 链接失败 ───────────────────────────────────────────
fatal error LNK1170: line in command file contains 135135 or more characters
link.exe 的响应文件**单行**上限 128 KiB,而 mcpp 此前把所有对象写在一行。
mcpp 2026.8.5.3 改成按行分隔(`$in_newline`),上限不再随对象数增长。
编译完 795s / 1166s 之后才在最后一步倒下,这个代价尤其刺眼。
因此 pin 抬到 **2026.8.5.3**。`index.toml` 的 `min_mcpp` 不动 —— 这两条都不改变
任何描述符是否可被旧客户端读取。
* ci: 全量运行按 member 分 8 片并行,每平台 8 个 job
全量运行的墙钟由**一个 job 串行跑完 59 个 member**决定,其中 opencv 系每个
15~20 分钟。linux 因此跑满 150 分钟 timeout 被取消 —— 而它每一条测试都是过的,
只是跑不完。一个验证不完的 workspace 等于没有验证。
改成按 member 轮转分片,每平台 8 个 runner 并行。
── 只在全量时扇出 ─────────────────────────────────────────────────────────
成员选择原本内联在 workspace job 里,三个平台各自重算一遍同一个答案。现在提成
前置 job `select`,因为**矩阵的 shard 维度取决于它**:
shards = [0..7] 全量
shards = [0] 选择性运行
矩阵用 `fromJSON(needs.select.outputs.shards)` 展开,所以选择性运行仍然是每平台
一个 job。给几个 member 分 8 片毫无意义 —— 只会多出 7 份 checkout、mcpp 下载和
缓存恢复。job 名字也只在分片时才带 `0/8`。
── 轮转分片把重的拆开 ──────────────────────────────────────────────────────
按位置取模。opencv-module / -dnn / -unifont 在列表里相邻,`% 8` 必然把它们放到
**三个不同的 runner** 上,这正是要解决的那件事:
shard 3: opencv-module
shard 4: opencv-module-dnn + llamacpp
shard 5: opencv-module-unifont + ffmpeg + llamacpp-metal
**这个分配并不理想,说清楚**:取模不知道每个 member 要跑多久,均衡靠运气。
shard 5 拿了 3 个重量级,shard 0/1/2 一个都没有,而墙钟由最慢的那片决定。要真正
均衡就得维护一张耗时表 —— 本文件别处已经写明「不要再引入手维护的清单」,那种
清单会悄悄过期。先用无信息但零维护的分法,等实测数据说明它不够再谈。
timeout 从 150 降到 90:一片约是 1/8 的活,90 分钟是一道真正的上限,而不是决定
job 能不能跑完的那个数。
── 与全局缓存互补 ─────────────────────────────────────────────────────────
两者管的是不同的重复:片内靠缓存复用同一个 (包,版本,features,工具链),片间靠
并行。跨 runner 不共享 build-cache,所以 abseil 这类共享依赖会在 8 个片里各编
一次 —— 但它们是并行的,墙钟只算一次。
* ci: 手动触发可切换缓存 + 全量跑完给出每个成员的耗时排名
三件事,都先在本地跑通再接进 CI。
── 1. tests/run_members.sh —— 本地与 CI 用同一份 ────────────────────────────
把内联在 workflow 里的循环抽成脚本。理由不是整洁:**只存在于 CI 的耗时表没法用来
决定优化什么**,而与 CI 不一致的本地脚手架量的是另一回事。
bash tests/run_members.sh --all
bash tests/run_members.sh --all --shard 3/8
bash tests/run_members.sh --all --cache local
bash tests/run_members.sh opencv-module protobuf
本地实测发现一个 CI 上才会炸的 bug:`--all` 从 mcpp.toml 里 grep 成员名,而该文件
第 3 行的**散文**也写了 `tests/examples/`(「tests/examples/ — each consumes...」),
去掉前缀后是**空字符串** —— CI 上会变成 `mcpp test -p ""`。现在同时过滤空名和
不存在的目录(注释里被讨论到的 `tests/examples/asio-ssl` 也因此不会变成幽灵成员)。
过滤后 59 个,与目录一致。
失败不吞:任一成员失败则退出码非零,但**耗时表照常打印** —— 跑挂了的那次恰恰最
需要知道时间花在哪。
── 2. workflow_dispatch 可选缓存模式 ───────────────────────────────────────
cache: global(默认)| local
`local` 让每个成员各自重编全部依赖,是耗时表的对照基准。非 dispatch 事件时该
input 为空字符串,而 mcpp 的 `resolve_cache_mode` 只在**非空**时才认这个环境变量,
所以天然回落到默认的 global,不需要额外分支。
── 3. 耗时排名 ────────────────────────────────────────────────────────────
分片把成本藏起来了:八个 runner 各报各的,没人看得见到底谁在吃时间。每片把
`<秒>\t<成员>\t<ok|FAIL>` 传成 artifact,新增 `timings` job 按平台合并,排名写进
run summary(带占比)。`always()` —— 失败的那次正是最该读它的时候。
汇总的合并/排序/占比逻辑已在本地用构造数据验证过。
表里的 total 是**各片之和**,墙钟是**最慢那片** —— 这两个数不是一回事,summary
里写明了,免得下次拿总和当墙钟看。
* ci: pin 抬到 2026.8.5.4 —— windows 改用 lld 才真正修掉 LNK1170
2026.8.5.3 把 mcpp **自己**写的响应文件改成按行分隔,必要但不充分:clang 作为
driver 时会**再生成一个**响应文件转发给链接器,那个是单行的,我们改不到。所以
opencv-module 在 .5.3 上仍然 LNK1170(而且是编译完 795s / 1166s 之后)。
2026.8.5.4 让 windows 的 clang 链接改用 lld —— 它的响应文件解析没有单行上限,
消掉的是一整类而不是把数字调大。同时新增了命令长度预算表与计划期校验,超限会在
还没编译任何东西时报出边名与上限,而不是在构建末尾由别人的程序抛一个没有上下文
的错。
本轮全量将同时验证三件事:lld 修复、分片提速、耗时排名。
* ci: 分片改为按实测耗时装箱,片数按平台并发度 —— 防止「分了但没加速」
上一版分 8 片、位置取模,两处都拍脑袋,实测数据把两处都推翻了。
── 1. 片数必须匹配平台并发度,不是一个整数 ────────────────────────────────
实测(24 个 job 排队时):
macos 1 · linux 3 · windows 2 合计约 6 并行
墙钟 = ceil(片数 / 并发) × 最慢片。**超出并发的片只增加固定开销**:每片各自
checkout + 下载 mcpp + 恢复缓存(实测 18s)。macOS 并发是 1,分 8 片等于**背靠背
串行跑 8 次**,严格慢于不分片 —— 这就是「分了但没加速」。
改成每平台各自的片数(linux 3 / macos 1 / windows 2),全量才扇出,选择性运行
每平台 1 片。注释里写明这是**实测配额**以及怎么重测。
── 2. 分配必须看耗时,不能看位置 ──────────────────────────────────────────
取模不知道谁跑得久。实测:它把 ffmpeg、llamacpp-metal、opencv-module-unifont
三个重量级放进同一片,另三片一个都没有 —— 而墙钟只看最慢那片。
新增 tests/plan_shards.lua:
- **LPT 装箱**(最长优先 + 放进当前最闲的片),用 tests/member-timings.tsv 的
实测秒数;LPT 对这个问题的近似比是 4/3,再优化不值。
- **依赖亲和**作为近似平局的 tie-break:共享大依赖的成员放同一片就只建一次,
分开就建两次(片间不共享 build cache,只有片内共享)。负载差超过 15% 时
仍以均衡优先 —— 均衡才是墙钟。
- 缺耗时的成员取中位数(新成员既不当免费也不当巨物);**完全没有表时回退取模**,
更差但绝不出错。
用构造数据验证:三个 opencv 被分到三个不同片,负载 16422/15900/15500(差 6%)。
── 3. 耗时表从哪来 ────────────────────────────────────────────────────────
timings job 现在除了在 run summary 出排名,还产出可直接落库的
`member-timings.tsv` artifact。**刻意不自动提交**:一个每次运行都改写自己的
数字会让每个 diff 都变噪音,还会把一次偶发的慢 runner 悄悄吸收进去。要更新就
下载 artifact 手动替换 —— 数据是自动测的,采纳是人决定的。
本次不附带表(还没有真实数据),所以先走回退路径;等这轮跑完拿到再落库。
* ci: 落库实测耗时表 —— LPT 比取模省 7.5 分钟墙钟
用真实数据(run 31034885938,24 个分片的 timings artifact)替换掉「无表回退」。
183 行实测,linux 分 3 片的对照:
取模 4158 / 3027 / 2822 最慢 4158s
LPT 3706 / 3152 / 3149 最慢 3706s
墙钟只看最慢片,所以 **省 452s(7.5 分钟,11%)**,极差从 47% 降到 15%。
顺带一个决定继续不继续加片数的数字:**单个最慢成员是硬下界**。grpc-module 一个
就 1701s(28 分钟),linux 再怎么分也快不过它。要再快只能让那个成员本身变快,
不是加 runner。
表由 CI 自动产出,采纳由人做(见 workflow 里的说明):自动提交会让每个 diff 变
噪音,还会把一次偶发的慢 runner 悄悄吸收进去。
* fix(ci): job 级 env 不能引用 env 上下文,导致 workflow 一个 job 都不启动
上一个 commit 里写了:
env:
MCPP_EFFECTIVE: ${{ env.MCPP_VERSION }}
**job 级 `env:` 里不能引用 `env` 上下文**。workflow 在校验阶段就失败,run
创建出来但 `jobs` 是空的、日志 "log not found" —— 和「某个 job 挂了」完全是两回事,
排查方向也完全不同。
MCPP_EFFECTIVE 原本只是 matrix.mcpp_version 的别名;矩阵改由 select 生成之后
这层间接没有存在的理由了,直接全部用 MCPP_VERSION。
顺带记一条判别法:**run 有了但 jobs 为空 = workflow 启动期失败**(YAML schema
或表达式求值),不是任何一个 job 的问题;而 "no checks reported" 则通常是 PR
有冲突、GitHub 算不出 merge ref。两种都表现为「CI 好像没跑」。
* fix(ci): 分片计划挪到 select 算一次 —— runner 上没有 lua5.4
macos / windows 的分片在 16 秒就挂了:
lua5.4: command not found (exit 127)
我把 plan_shards.lua 放在每个 runner 上跑,而 lua5.4 只在 ubuntu 的 lint job
里装过。写的回退也不成立:**windows 没有 apt 也没有 brew**,而 **macOS 的 brew
装的是 `lua`,不是 `lua5.4`**。
改成在 select job(ubuntu,本来就装 lua)里**算一次**,把每片的成员列表作为
数据发给 runner。runner 侧不再需要 lua。
这不只是修 bug —— 决策本来就该集中在一处。让三个 runner 各自重新推导同一个
答案,正是本仓库反复付学费的那个形状。
本地用同样的 jq/lua 管线模拟过:JSON 合法,linux 22/18/21、windows 33/28、
macos 61(并发为 1 故不分片)。1 parent e760383 commit 3ce6cb5
14 files changed
Lines changed: 1380 additions & 137 deletions
File tree
- .github/workflows
- docs
- zh
- pkgs/c
- tests
- examples/protobuf-protoc
- proto
- tests
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
| 42 | + | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
200 | 201 | | |
201 | 202 | | |
202 | 203 | | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
203 | 248 | | |
204 | 249 | | |
205 | 250 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
184 | 185 | | |
185 | 186 | | |
186 | 187 | | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
187 | 230 | | |
188 | 231 | | |
189 | 232 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
| |||
0 commit comments