Skip to content

Commit 3ce6cb5

Browse files
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/validate.yml

Lines changed: 341 additions & 126 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Two kinds of packages live here:
3939
|------|------|
4040
| Native module library (Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua) (module layer; sources compiled directly through `compat.ffmpeg`) · [`opencv`](pkgs/o/opencv.lua) (single repository: the module layer and the full OpenCV 5 source build both live in the package, and only this descriptor stays on the index side) · [`mcpplibs.grpc`](pkgs/g/grpc.lua) (gRPC 1.83.0 — the one library here that CANNOT be a compat descriptor: upstream publishes no self-contained source artifact, its tag archive carrying abseil/protobuf/re2/boringssl/zlib as empty submodule placeholders, so [grpc-m](https://github.com/mcpplibs/grpc-m)'s release tarball IS that artifact. It vendors only gRPC's own source and takes the five dependencies from this index, so a consumer that also uses protobuf links one copy rather than two) |
4141
| C-source compat (with `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
42-
| C++-source compat, one depending on the other | [`compat.abseil`](pkgs/c/compat.abseil.lua) (151 TUs; a wildcard over `absl/**` trimmed by upstream's test/benchmark naming conventions) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua) (the libprotobuf runtime, 79 TUs transcribed from upstream's own `src/file_lists.cmake`; declares `compat.abseil` as a dependency because protobuf's public headers include `absl/…`, and its `gzip` feature defines `HAVE_ZLIB` and pulls `compat.zlib`, while `upb` adds protobuf's 64-TU C runtime out of the same tarball) · [`compat.re2`](pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) |
42+
| C++-source compat, one depending on the other | [`compat.abseil`](pkgs/c/compat.abseil.lua) (151 TUs; a wildcard over `absl/**` trimmed by upstream's test/benchmark naming conventions) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua) (the libprotobuf runtime, 79 TUs transcribed from upstream's own `src/file_lists.cmake`; declares `compat.abseil` as a dependency because protobuf's public headers include `absl/…`, and its `gzip` feature defines `HAVE_ZLIB` and pulls `compat.zlib`, while `upb` adds protobuf's 64-TU C runtime out of the same tarball. It also exposes **`protoc`** as a `kind = "bin"` target, so a consumer writing `tools = ["protoc"]` gets the compiler built for its own machine out of the same package it links — making a generator/runtime version mismatch inexpressible) · [`compat.re2`](pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) |
4343
| C++-source compat, zero-dep client + optional components | [`compat.websocket`](pkgs/c/compat.websocket.lua) (IXWebSocket 12.0.1 — a pure RFC 6455 client compiled from upstream's `IXWEBSOCKET_SOURCES` minus the four server TUs, so the **base build has zero external dependencies**: TLS off (the OpenSSL/MbedTLS/AppleSSL TUs aren't built) and `IXWEBSOCKET_USE_ZLIB` unset, so the gzip codec compiles to a no-op. Two optional features add on top: `server` (the four server TUs — `IXWebSocketServer`, `IXSocketServer`, `IXHttpServer`, `IXWebSocketProxyServer` — needing nothing external, and it **implies `zlib`** because upstream's server advertises permessage-deflate by default, which the transport negotiates regardless of the define) and `zlib` (deps `compat.zlib` and turns the codec into real per-message-deflate compression). The default-feature test brings its own minimal RFC 6455 echo server on loopback sockets (handshake, masking, fragmentation and close all exercised offline); a second member, `websocket-features`, runs a real `ix::WebSocketServer` and asserts the compression is observable on the wire — a 64 KiB repeated payload round-trips with `wireSize` = 80) |
4444
| header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
4545
| Runtime loader compat (pure sources, sidestepping upstream codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua) (the Khronos loader: `loader/generated/` is checked in, and the assembly path degrades to plain C through `UNKNOWN_FUNCTIONS_SUPPORTED`, so no CMake/Python/assembler is needed; windows deferred) · [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) |

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上
3636
|------|------|
3737
| 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) · [`mcpplibs.grpc`](pkgs/g/grpc.lua)(gRPC 1.83.0 —— 本索引里唯一**无法**做成 compat 描述符的库:上游不发布任何自包含源码产物,其 tag 归档里 abseil/protobuf/re2/boringssl/zlib 全是空 submodule 占位,因此 [grpc-m](https://github.com/mcpplibs/grpc-m) 的 release tarball 才是那个产物。它只 vendor gRPC 自己的源码,五个依赖全取自本索引,故同时直接使用 protobuf 的消费者链进去的是同一份而非两份)|
3838
| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
39-
| C++ 源码 compat(彼此依赖) | [`compat.abseil`](pkgs/c/compat.abseil.lua)(151 TU;对 `absl/**` 取通配后,按上游自身的 test/benchmark 命名约定裁剪) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua)(libprotobuf 运行时,79 TU 逐条转录自上游 `src/file_lists.cmake`;因 protobuf 公开头文件 include 了 `absl/…`,故显式依赖 `compat.abseil`;`gzip` feature 定义 `HAVE_ZLIB` 并拉入 `compat.zlib`,`upb` feature 则从同一个 tarball 里再编出 protobuf 的 64 TU C 运行时) · [`compat.re2`](pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) |
39+
| C++ 源码 compat(彼此依赖) | [`compat.abseil`](pkgs/c/compat.abseil.lua)(151 TU;对 `absl/**` 取通配后,按上游自身的 test/benchmark 命名约定裁剪) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua)(libprotobuf 运行时,79 TU 逐条转录自上游 `src/file_lists.cmake`;因 protobuf 公开头文件 include 了 `absl/…`,故显式依赖 `compat.abseil`;`gzip` feature 定义 `HAVE_ZLIB` 并拉入 `compat.zlib`,`upb` feature 则从同一个 tarball 里再编出 protobuf 的 64 TU C 运行时;还以 `kind = "bin"` target 暴露 **`protoc`**,消费者写 `tools = ["protoc"]` 即可从「自己链接的那个包」拿到为本机构建的编译器,使生成器与运行时的版本错配无法表达) · [`compat.re2`](pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) |
4040
| C++ 源码 compat(零依赖客户端 + 可选组件) | [`compat.websocket`](pkgs/c/compat.websocket.lua)(IXWebSocket 12.0.1 —— 从上游 `IXWEBSOCKET_SOURCES` 剔掉 4 个 server TU 后直编的纯 RFC 6455 客户端,**基座零外部依赖**:TLS 关闭(OpenSSL/MbedTLS/AppleSSL 三组 TU 均不编),`IXWEBSOCKET_USE_ZLIB` 不定义(gzip codec 编译为 no-op)。两个可选 feature 在基座上叠加:`server`(4 个 server TU —— `IXWebSocketServer`/`IXSocketServer`/`IXHttpServer`/`IXWebSocketProxyServer`,零新增外部依赖,且 **implies `zlib`** —— 因为上游 server 默认就宣称 permessage-deflate,而 transport 的协商不受宏门控)与 `zlib`(依赖 `compat.zlib`,把 codec 变成真正的 permessage-deflate 压缩)。默认构建的测试自带基于 loopback 原始 socket 的最小 RFC 6455 echo server(握手/掩码/分片/关闭全部离线实测);第二个成员 `websocket-features` 跑真实的 `ix::WebSocketServer`,并断言压缩在线路上可观测 —— 64 KiB 重复载荷往返,`wireSize` = 80) |
4141
| header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
4242
| 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua)(Khronos loader:`loader/generated/` 已签入,汇编路径经 `UNKNOWN_FUNCTIONS_SUPPORTED` 降级为纯 C,故无需 CMake/Python/汇编器;windows 延后)· [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) |

docs/package-types.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ combined as needed.
1818
| **E. Whole-source direct build with a generated config** | upstream generates its config header through configure/CMake; here a snapshot of it lands in `generated_files` | `pkgs/c/compat.libpng.lua`, `compat.curl.lua`, `compat.sdl2.lua`, `compat.ffmpeg.lua` | `generated_files` + `include_dirs` |
1919
| **F. Shared-library compat** | has to be the **only** copy of that `.so` in the process (third parties `dlopen` it) | the X11 family such as `pkgs/c/compat.x11.lua`, and `compat.vulkan.lua` (linux) | `targets = { kind = "shared", soname = … }` |
2020
| **G. Host runtime adaptation** | things that cannot be vendored, such as drivers — only a symlink farm plus metadata | `pkgs/c/compat.glx-runtime.lua`, `compat.vulkan-runtime.lua` | `runtime.library_dirs` / `capabilities` |
21+
| **H. Host tool provider** | the upstream tarball also holds a **code generator** consumers run at build time | `pkgs/c/compat.protobuf.lua` (`protoc`) | a `targets` entry with `kind = "bin"` + `main`, plus `required_features` |
2122

2223
For the complete sample index, see the
2324
[Reference examples table in the root README](../README.md#reference-examples-lua-descriptors).
@@ -200,6 +201,50 @@ Two details that keep biting:
200201
- **The closure has to be complete.** A farm holding `libxcb.so.1` but not the `libXau.so.6` it depends on shadows the
201202
host copy that would otherwise have resolved, and the executable simply fails to start.
202203

204+
## H. Host tool provider (`compat.protobuf`'s `protoc`)
205+
206+
Some tarballs hold both a library and the code generator that emits code against it. Declare the generator as a second
207+
target, and consumers ask for it with `tools = [...]` (mcpp 2026.8.5.1+):
208+
209+
```lua
210+
targets = {
211+
["protobuf"] = { kind = "lib" },
212+
["protoc"] = { kind = "bin",
213+
main = "*/src/google/protobuf/compiler/main.cc",
214+
required_features = { "protoc", "upb" } },
215+
},
216+
features = {
217+
["protoc"] = { sources = { … the compiler's own sources … } },
218+
},
219+
```
220+
221+
```toml
222+
# consumer side — one dependency, two roles
223+
compat.protobuf = { version = "35.1", tools = ["protoc"] }
224+
```
225+
226+
mcpp then builds that target **for the build machine** in a nested sub-build and hands the path to the consumer's
227+
`build.mcpp` through `mcpp::dep_bin("protobuf", "protoc")`. This is the whole reason the shape is worth naming: the
228+
tool's version **is** the dependency's version, so a generator/runtime mismatch — a *runtime* failure everywhere else,
229+
and the classic protobuf footgun — is not expressible. Under `mcpp build --target <triple>` the tool is still built for
230+
the host, because a code generator has to run here.
231+
232+
Four things to get right:
233+
234+
- **Gate the compiler's sources behind a feature**, and name it in the target's `required_features`. Consumers who only
235+
link the library must not pay for the generator's TUs; consumers who ask for the tool must not have to know which
236+
features it needs. `compat.protobuf`'s `protoc` also requires `upb`, because libprotoc's upb generator links the upb
237+
runtime — get that wrong and it fails at **link** time with missing `upb_*` symbols.
238+
- **Transcribe the source list from upstream**, exactly as for a library — protobuf's 138 entries come from
239+
`libprotoc_srcs` in its own `src/file_lists.cmake`.
240+
- **`main` needs the same `*/` wrap glob as `sources`**; it is expanded the same way.
241+
- **A generator that reads data files at runtime still needs a path to them.** protoc does not embed the well-known
242+
types: `import "google/protobuf/timestamp.proto"` is read from disk. Consumers derive that directory from
243+
`mcpp::dep_dir("protobuf")` — see `tests/examples/protobuf-protoc/build.mcpp`.
244+
245+
The matching member is `tests/examples/protobuf-protoc`, and it is the complement of `tests/examples/protobuf`: that
246+
one deliberately uses no generated code, this one is generated code end to end.
247+
203248
---
204249

205250
## The minimal project (`tests/examples/<short>/`)

docs/zh/package-types.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A–D 是四种**基础**形态,先按它们判定;E–G 是在基础形态之
1616
| **E. 生成 config 的全源码直编** | 上游用 configure/CMake 生成配置头,此处以 `generated_files` 落一份快照 | `pkgs/c/compat.libpng.lua``compat.curl.lua``compat.sdl2.lua``compat.ffmpeg.lua` | `generated_files` + `include_dirs` |
1717
| **F. 共享库 compat** | 必须是**唯一**的那个 `.so`(会被第三方 `dlopen`) | `pkgs/c/compat.x11.lua` 等 X11 家族、`compat.vulkan.lua`(linux) | `targets = { kind = "shared", soname = … }` |
1818
| **G. 宿主运行时适配** | 驱动之类无法 vendor 的东西,只做符号链接农场 + 元数据 | `pkgs/c/compat.glx-runtime.lua``compat.vulkan-runtime.lua` | `runtime.library_dirs` / `capabilities` |
19+
| **H. 宿主工具提供方** | 上游 tarball 里除了库,还带着消费者在构建期要跑的**代码生成器** | `pkgs/c/compat.protobuf.lua`(`protoc`) | `targets` 里一条 `kind = "bin"` + `main`,配 `required_features` |
1920

2021
完整的样例索引见[根 README 的「参考示例」表](../../README.zh-CN.md#参考示例lua-描述符)
2122

@@ -184,6 +185,48 @@ runtime = {
184185
- **闭包必须完整**。农场里有 `libxcb.so.1` 却没有它依赖的 `libXau.so.6`,会遮蔽掉本来能解析的宿主副本,可执行
185186
文件直接起不来。
186187

188+
## H. 宿主工具提供方(`compat.protobuf``protoc`)
189+
190+
有些 tarball 里同时装着一个库,和「针对这个库生成代码」的那个生成器。把生成器声明成第二个 target,
191+
消费者用 `tools = [...]` 索取(mcpp 2026.8.5.1 起):
192+
193+
```lua
194+
targets = {
195+
["protobuf"] = { kind = "lib" },
196+
["protoc"] = { kind = "bin",
197+
main = "*/src/google/protobuf/compiler/main.cc",
198+
required_features = { "protoc", "upb" } },
199+
},
200+
features = {
201+
["protoc"] = { sources = { … 编译器自身的源码 … } },
202+
},
203+
```
204+
205+
```toml
206+
# 消费者侧 —— 一条依赖,两种角色
207+
compat.protobuf = { version = "35.1", tools = ["protoc"] }
208+
```
209+
210+
mcpp 会在一次嵌套子构建里把这个 target 编成**构建机**的二进制,并把路径经
211+
`mcpp::dep_bin("protobuf", "protoc")` 交给消费者的 `build.mcpp`。这个形态值得单列的全部理由在于:
212+
工具的版本**就是**那条依赖的版本,于是「生成器与运行时版本错配」——在别处是**运行期**才炸、也正是
213+
protobuf 最经典的坑——在这里**语法上无法表达**`mcpp build --target <triple>` 下工具仍为宿主构建,
214+
因为代码生成器必须在本机跑。
215+
216+
四个要点:
217+
218+
- **把编译器的源码关进一个 feature**,并在 target 的 `required_features` 里写明。只链库的消费者不该为
219+
生成器的 TU 买单;索取工具的消费者也不该需要知道它要哪些 feature。`compat.protobuf``protoc`
220+
必须要 `upb`,因为 libprotoc 的 upb 生成器要链 upb 运行时——搞错了会在**链接期**缺一批 `upb_*` 符号。
221+
- **源码列表照旧逐条转录自上游**:protobuf 这 138 项来自它自己的 `src/file_lists.cmake``libprotoc_srcs`
222+
- **`main``sources` 一样需要 `*/` 那层 wrap glob**,展开方式相同。
223+
- **运行期还要读数据文件的生成器,仍然需要一个路径**。protoc 并不内嵌 well-known types:
224+
`import "google/protobuf/timestamp.proto"` 是从磁盘读的。消费者用 `mcpp::dep_dir("protobuf")` 推出那个
225+
目录——见 `tests/examples/protobuf-protoc/build.mcpp`
226+
227+
对应的成员是 `tests/examples/protobuf-protoc`,它与 `tests/examples/protobuf` 互为补集:那个刻意**不用**
228+
任何生成代码,这个从头到尾都是生成代码。
229+
187230
---
188231

189232
## 最小工程(`tests/examples/<short>/`)

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ members = [
4747
"tests/examples/protobuf",
4848
"tests/examples/protobuf-upb",
4949
"tests/examples/protobuf-gzip",
50+
"tests/examples/protobuf-protoc",
5051
"tests/examples/opencv-module",
5152
"tests/examples/opencv-module-dnn",
5253
"tests/examples/opencv-module-unifont",

0 commit comments

Comments
 (0)