Uh oh!
There was an error while loading. Please reload this page.
feat(protobuf): expose protoc as a host tool - #155
Conversation
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").Sunrisepeak
commented
Aug 5, 2026
macOS 全量 workspace 已通过 —— 新成员实测链路
一处已知的外观问题(不阻塞,但会被每个用户看到)工具子构建里会打一条: 它在整个 59 成员的运行里只出现一次,只在这个成员下 —— 时间戳落在 原因:子构建把 不阻塞本 PR,产物与测试都正确。但它是噪声,而且出现在一个主打功能的路径上 —— |
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` 本来就不动,那个判断没变。
全量运行(改 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` 不动 —— 这两条都不改变 任何描述符是否可被旧客户端读取。
全量运行的墙钟由**一个 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。 ── 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 里写明了,免得下次拿总和当墙钟看。
2026.8.5.3 把 mcpp **自己**写的响应文件改成按行分隔,必要但不充分:clang 作为 driver 时会**再生成一个**响应文件转发给链接器,那个是单行的,我们改不到。所以 opencv-module 在 .5.3 上仍然 LNK1170(而且是编译完 795s / 1166s 之后)。 2026.8.5.4 让 windows 的 clang 链接改用 lld —— 它的响应文件解析没有单行上限, 消掉的是一整类而不是把数字调大。同时新增了命令长度预算表与计划期校验,超限会在 还没编译任何东西时报出边名与上限,而不是在构建末尾由别人的程序抛一个没有上下文 的错。 本轮全量将同时验证三件事:lld 修复、分片提速、耗时排名。
main 上合入了 #159(compat.websocket)与 #160(修 xpkg.lua 里那个把 0.0.47/0.0.48 一起吞掉的畸形 0.0.49 条目),分支落后并冲突。 **冲突让 CI 一个 run 都不建** —— GitHub 在 PR 有冲突时算不出 merge ref,于是 `pull_request` 触发的 workflow 完全不启动。表现是 "no checks reported",极易被 误读成 CI 挂了或 push 没生效。 冲突只在两个 README 的同一张表:main 新增了 websocket 那一行,而我改的是同表的 protobuf 那一行。两边都保留。 合并后核验:74 个描述符全部解析通过;members 同时含 protobuf-protoc 与 websocket / websocket-features。
上一版分 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 手动替换 —— 数据是自动测的,采纳是人决定的。 本次不附带表(还没有真实数据),所以先走回退路径;等这轮跑完拿到再落库。
用真实数据(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 悄悄吸收进去。
上一个 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 好像没跑」。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 故不分片)。
Sunrisepeak
commented
Aug 5, 2026
CI 结论:9 绿 1 红,唯一的红与本 PR 无关
本 PR 达成的三件事都有证据
那个红是什么
根因在 openxlings/xim-pkgindex#463,它自己的描述就是这个症状:gcc 的 xvm alias 把 时间对照支持这个判断:
跨过的正是 subos 那批重构。 为什么不在本 PR 里修
证据已完整评论在 #463 上,包括「DO NOT MERGE 的前置条件(xlings#460)已于 2026-07-31 满足」。 因此本 PR 的内容( |
Uh oh!
There was an error while loading. Please reload this page.
…one thing (#162) The CI section still described a three-platform matrix and a single `mcpp test --workspace` per platform. #155 replaced that with a `select` job that decides the whole plan once, per-platform shard counts taken from measured runner concurrency, measured-time bin packing, and a `timings` job that ranks members. None of it was written down. Also documented: the `cache` dispatch input, why the global package build cache came back (mcpp#344's reason was removed in 2026.8.3.4, and the bypass made the full linux run exceed its timeout), and the pre-test index refresh. `run_members.sh --shard` computed its own split by round-robin while CI used `plan_shards.lua`'s measured packing — two algorithms answering one question, in a script whose header claims local and CI measure the same thing. --shard now delegates to plan_shards.lua, so shard N locally holds the members shard N holds in CI; round-robin stays as the fallback where lua is absent and says so in its output. --platform selects which column of member-timings.tsv to read and defaults to the host. Both language versions updated.
What
compat.protobufnow exposesprotocas akind = "bin"target, so a consumer can write:and get the compiler built for its own machine, out of the same package it links.
Why this shape
A protoc that disagrees with the linked libprotobuf fails at runtime, not at build time. Every other ecosystem carries machinery to paper over this — Conan has a
protobuf/<host_version>placeholder, xmake deletes protoc outright when cross-compiling, and protobuf's own CMake has an open issue (#14576) whereProtobuf_PROTOC_EXECUTABLEis ignored in CONFIG mode.Here it needs no machinery, because there is only one version axis: the tool's version is the dependency's version. The mismatch is not expressible.
mcpp build --target <triple>does not change it either — the tool is still built for the build machine, because a code generator has to run here.Contents
pkgs/c/compat.protobuf.luaprotoctarget +protocfeature (138 TUs)tests/examples/protobuf-protoc.github/workflows/validate.ymlindex.tomlmin_mcppunchanged;latest_mcpp-> 2026.8.5.1docs/package-types.md+ zh, both READMEsThe source list
138 entries transcribed from upstream's own
libprotoc_srcsinsrc/file_lists.cmake— not hand-picked. Zero overlap with the libprotobuf set (importer.cc/parser.ccwere already there), and no configure step is needed: the tree holds no.h.in/.cmake.in.The target declares
required_features = { "protoc", "upb" }.upbis not optional — libprotoc's upb generator links the upb runtime, and without it the link fails on missingupb_*symbols. Stating it in the descriptor means a consumer asking for the tool does not have to know that. Consumers who only link the runtime compile none of the 138.The new member
It is the deliberate complement of
tests/examples/protobuf, whose header comment says it "deliberately uses NO protoc-generated code". This one is generated code end to end: nested messages, an enum, a repeated message field, a map, a oneof, a well-known-type import, and reflection over the generated pool — serialized and parsed back, asserting on every one.Verification
mcpp xpkg parse --json), 0 failures.mcpp test -p protobuf-protocpasses locally (18.6s cold, 1.6s warm)..protore-runs only that edge (1.38s vs 0.02s no-op) — the codegen is a build-graph node, not work done during prepare.grpc-m'shelloworld.protoare byte-identical to the officially-generated stubs checked into that repo. Self-built protoc == official protoc 35.1.check_mirror_urls,check_package_name,check_cross_package_refs) pass.Two version decisions, made in opposite directions
CI pin -> 2026.8.5.1 (required). Confirmed empirically against 2026.8.3.3:
min_mcppstays at 2026.8.3.3 (deliberate). The floor is a gate — an older client that fails it cannot open this index at all, which is exactly the failure mode of mcpp#349. Raising it is only justified when a descriptor genuinely stops being readable, and that has not happened: 2026.8.3.3 still parses the newcompat.protobufwith an emptyunknown_keys. What needs the newer mcpp is the consumer spellingtools = [...], which lives in a consumer's ownmcpp.toml— including this repo's new member, hence the CI pin and nothing more.latest_mcppis an advisory hint with no gate behaviour anywhere in the engine, so it tracks what CI validates against.Notes for review
index.toml, which the member-selection step classifies as an unclassified change — so CI runs the full workspace on all three platforms against the new pin. That is the intended validation for a pin bump.MCPP_BUILD_CACHE: localbypass for mcpp#344 is now droppable (that fix landed in 2026.8.3.4). It is kept, with its comment updated to record why: re-enabling the global package cache changes how every member builds and deserves its own PR, where a failure is unambiguous.import "google/protobuf/timestamp.proto"needs an-Iderived frommcpp::dep_dir("protobuf"). The new member'sbuild.mcppshows the robust form (probe for the directory holdingdescriptor.protorather than hardcoding the tarball's wrap-directory name).Follow-up
mcpplibs.grpc-plugin— the gRPC C++ codegen plugin, a package of its own because a code generator needs a.protoparser and a C++ emitter, not TLS, DNS and a regex engine. It lands after grpc-m tags a release; this PR is its prerequisite.