From 63a4a5448bb869ce4e9ef8f6b0f47b8a0c0b0525 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 4 Aug 2026 22:38:04 +0800 Subject: [PATCH 1/2] feat: add compat.abseil 20250512.1 + compat.protobuf 35.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groundwork for gRPC: its two largest prerequisites, as ordinary compat packages. gRPC itself still needs a dedicated adaptation repo (see .agents/docs/2026-08-04-grpc-feasibility-analysis.md) — these two do not, and the analysis says why: * both publish a real self-contained release ASSET (not the tag archive): abseil-cpp-20250512.1.tar.gz 2.2MB, protobuf-35.1.tar.gz 7.1MB. No submodules; protobuf's third_party/ carries utf8_range as actual source. * no configure step — platform handling is in-source #ifdef, so all three xpm blocks share one tarball and one sha256. * no protoc bootstrap — the 14 .pb.cc for the well-known types and descriptor.pb.cc are CHECKED IN upstream. By contrast gRPC's tag archive has abseil/protobuf/re2/boringssl/zlib as EMPTY submodule placeholders (1 entry each), which is what would force a self-hosted repack. Versions track upstream verbatim, which here is a correctness requirement rather than a style choice: 20250512.1 IS the Abseil LTS tag, protobuf 35.1's MODULE.bazel names that same LTS, and gRPC 1.83.0 pins protobuf at commit 35cd01f (== tag v35.1) and abseil at 76bb243 ("Abseil LTS Branch, May 2025, Patch 1" == 20250512.1). So a later gRPC package links ONE abseil instead of colliding with a second vendored copy. Source lists are taken the way each upstream affords: * protobuf transcribes libprotobuf_srcs from upstream's own src/file_lists.cmake (79 TUs). A directory glob cannot substitute: src/google/protobuf/**/*.cc would also sweep in libprotoc (157 more TUs) and the tests, while two runtime TUs live under compiler/ (importer.cc, parser.cc — the .proto text parser is part of the runtime), so nothing separates the two libraries by directory. * abseil globs absl/**/*.cc and trims by upstream's own test/benchmark naming conventions plus 14 named stragglers. Four of the excluded TUs define their own main() — a dependency's objects all enter the consumer's link, so any one of them would collide with the consumer's main() and break every dependent build. compat.protobuf declares compat.abseil as a dependency because protobuf's public headers include "absl/…" directly, and gates GzipInputStream / GzipOutputStream behind a `gzip` feature: io/gzip_stream.cc is wrapped head-to-toe in #if HAVE_ZLIB, so the default build compiles it to an empty TU and pulls no zlib at all. Verified cold with the pinned mcpp 2026.8.3.3, gcc@16.1.0, MCPP_INDEX_MIRROR=GLOBAL and MCPP_BUILD_CACHE=local (the CI configuration), target/ and .mcpp/ removed first: abseil test result ok. 1 passed; 0 failed (18.57s) protobuf test result ok. 1 passed; 0 failed (63.35s) protobuf-gzip test result ok. 1 passed; 0 failed (54.48s) Object counts checked rather than assumed: 151 .o linked for compat.abseil, 80 for compat.protobuf. The protobuf test uses NO protoc output anywhere — .proto text through the runtime's own parser, then DynamicMessage, wire/TextFormat/JSON round-trips, well-known types and UTF-8 validation (which reaches utf8_range.c); a truncated payload must be rejected, so a no-op parser cannot pass. The gzip feature was verified negatively too: reverting the member to a bare `protobuf = "35.1"` fails the link with undefined GzipOutputStream/GzipInputStream symbols. CN mirrors published and closed-loop checked: mcpp-res/abseil@20250512.1 and mcpp-res/protobuf@35.1 both return http=200 and are byte-identical to GLOBAL. Two known mcpp-side issues are documented in the plan doc, neither a defect in these descriptors and neither affecting CI: a false `module 'binascii' imported but not provided` warning from a Python snippet inside a block comment in randen_round_keys.cc (mcpp's text scan does not strip block comments, and scan_overrides cannot express "no module edges"), and a new instance of mcpp#344 — abseil's str_format/parser.cc and protobuf's compiler/parser.cc share a basename, which the package build cache mishandles. CI already bypasses that cache via MCPP_BUILD_CACHE: local, and upstream fixed it in 2026.8.3.4, so MCPP_VERSION is deliberately left alone. --- ...2026-08-04-add-abseil-and-protobuf-plan.md | 146 ++++++++++++ .../2026-08-04-grpc-feasibility-analysis.md | 168 ++++++++++++++ README.md | 1 + README.zh-CN.md | 1 + mcpp.toml | 3 + pkgs/c/compat.abseil.lua | 160 +++++++++++++ pkgs/c/compat.protobuf.lua | 211 ++++++++++++++++++ tests/examples/abseil/mcpp.toml | 16 ++ tests/examples/abseil/tests/basics.cpp | 120 ++++++++++ tests/examples/protobuf-gzip/mcpp.toml | 13 ++ tests/examples/protobuf-gzip/tests/gzip.cpp | 79 +++++++ tests/examples/protobuf/mcpp.toml | 17 ++ tests/examples/protobuf/tests/runtime.cpp | 177 +++++++++++++++ 13 files changed, 1112 insertions(+) create mode 100644 .agents/docs/2026-08-04-add-abseil-and-protobuf-plan.md create mode 100644 .agents/docs/2026-08-04-grpc-feasibility-analysis.md create mode 100644 pkgs/c/compat.abseil.lua create mode 100644 pkgs/c/compat.protobuf.lua create mode 100644 tests/examples/abseil/mcpp.toml create mode 100644 tests/examples/abseil/tests/basics.cpp create mode 100644 tests/examples/protobuf-gzip/mcpp.toml create mode 100644 tests/examples/protobuf-gzip/tests/gzip.cpp create mode 100644 tests/examples/protobuf/mcpp.toml create mode 100644 tests/examples/protobuf/tests/runtime.cpp diff --git a/.agents/docs/2026-08-04-add-abseil-and-protobuf-plan.md b/.agents/docs/2026-08-04-add-abseil-and-protobuf-plan.md new file mode 100644 index 0000000..69a5efb --- /dev/null +++ b/.agents/docs/2026-08-04-add-abseil-and-protobuf-plan.md @@ -0,0 +1,146 @@ +# 新增 compat.abseil 20250512.1 与 compat.protobuf 35.1(2026-08-04) + +本次变更为 [gRPC 收录可行性分析](2026-08-04-grpc-feasibility-analysis.md) 的 **P0**:先把 gRPC 最大的两个前置 +依赖以普通 compat 包的形态收进索引。gRPC 本身仍走独立适配仓路线,不在本次范围内。 + +产出: + +| 包 | 版本 | TU 数 | 形态 | +|---|---|---|---| +| `compat.abseil` | `20250512.1` | 151 | Shape A(C++ 源码 compat) | +| `compat.protobuf` | `35.1` | 80(79 C++ + 1 C) | Shape A,依赖 `compat.abseil`,含 `gzip` feature | + +workspace 成员:`tests/examples/abseil`、`tests/examples/protobuf`、`tests/examples/protobuf-gzip`。 + +## 1. 形态判定:为什么不需要独立仓 + +最初的 gRPC 分析曾建议 protobuf 也做成 `protobuf-m` 独立仓。核实上游发布物后推翻了这一判断——三条性质 +让它们正好落进索引标准模板: + +1. **有正式的自包含 release asset**。不是 GitHub tag 归档,而是真实发布产物: + `abseil-cpp-20250512.1.tar.gz`(2.2MB)、`protobuf-35.1.tar.gz`(7.1MB)。均无 submodule; + protobuf 的 `third_party/` 只含 utf8_range,且是实体源码。sha256 两次下载一致。 +2. **无需 configure**。两者的平台差异全在源码内 `#ifdef`(`absl/base/config.h` / `port_def.inc`), + 所以三平台共用同一个 tarball、同一份 sha256、同一份源码清单。 +3. **无需 protoc 自举**。protobuf 把 14 个 `.pb.cc`(well-known types + `descriptor.pb.cc`)**check-in** + 在源码树里,构建运行时完全不需要先有一个 protoc。 + +这与 gRPC 形成鲜明对比:gRPC 的 tag 归档里 abseil/protobuf/re2/boringssl/zlib 全是空 submodule 占位, +必须自行打包并双镜像自托管——那才是需要独立仓的理由。 + +## 2. 版本对齐:一份 abseil,不是两份 + +版本号与上游逐字对应,这不是风格问题而是正确性要求: + +- `compat.abseil` 的 `20250512.1` **就是** Abseil 的 LTS tag(`ABSL_LTS_RELEASE_VERSION 20250512` / + `ABSL_LTS_RELEASE_PATCH_LEVEL 1`)。 +- protobuf 35.1 的 `MODULE.bazel` 声明 `abseil-cpp 20250512.1`;gRPC 1.83.0 的 abseil submodule 固定在 + commit `76bb243`,即 "Abseil LTS Branch, May 2025, Patch 1" —— **同一个发布**。 +- gRPC 1.83.0 的 protobuf submodule 固定在 commit `35cd01f`,与上游 tag `v35.1` **完全相等**(已核对)。 + +因此后续 gRPC 包可以直接依赖这两个包,而不是各自 vendor 一份 abseil 在链接期撞车。 + +## 3. 源码清单的取法 + +两个包取清单的方式不同,各自取决于上游给了什么: + +- **protobuf —— 逐条转录上游权威清单**。`src/file_lists.cmake` 里的 `libprotobuf_srcs`(79 条)是上游从 + Bazel 规则自动生成的。**不能用目录 glob 代替**:`src/google/protobuf/**/*.cc` 会同时扫进 libprotoc + (另外 157 个 TU)和单元测试,而运行时自己有两个 TU 恰恰住在 `compiler/` 下 + (`importer.cc`、`parser.cc`——`.proto` 文本解析器属于运行时),没有任何目录层面的 glob 能干净切开两者。 + `libprotobuf_lite_srcs` 是该清单的真子集,故 lite 无需单独处理。 +- **abseil —— 通配 + 按上游命名约定裁剪**。abseil 没有等价的清单文件,但它的测试/基准 TU 命名规律极强, + 5 条否定 glob(`*_test.cc` / `*_test_common.cc` / `*_testing.cc` / `*_benchmark.cc` / `*_benchmarks.cc`) + 就吃掉了绝大部分,再逐个排除 14 个漏网的。分四类,每一类都有理由,不是凑数: + + 1. 仍会 include gtest/gmock/benchmark 的(本包无测试框架依赖,根本编不过); + 2. 能编过但不属于任何上游 library target 的测试支撑 TU; + 3. **自带 `main()` 的**——这一类是硬约束:依赖的 `.o` 会全量进入消费者的链接(不是从 archive 里惰性挑选), + 任何一个都会和消费者自己的 `main()` 撞车,让每一个依赖此包的构建都失败。 + 实测确有 4 个:`print_hash_of.cc`、`gaussian_distribution_gentables.cc`、 + `randen_benchmarks.cc`、`raw_hash_set_probe_benchmark.cc`(后两个已被 glob 覆盖)。 + + 第一版描述符漏掉了通配的 `*_benchmark.cc`,直接表现为一大片 + `undefined symbol: benchmark::internal::Benchmark::…` 链接错误——这也是"漏了就一定会炸"的证据。 + +## 4. 几个刻意的取舍 + +- **`randen_hwaes.cc` 不加 `-maes -msse4.1`**(上游把它放在独立 CMake target 上)。这是正确而非退化: + 没有加速 AES 时该文件走 `!ABSL_RANDEN_HWAES_IMPL` 分支,`HasRandenHwAesImplementation()` 返回 false, + 而 `randen.cc` 的判断是 `HasRandenHwAesImplementation() && CPUSupportsRandenHwAes()` —— 短路, + 桩函数不可达,`absl::BitGen` 走 RandenSlow。若改为全包加这两个 flag,等于抬高每个 TU 的基线 ISA, + 对一个要求可移植的索引包是更差的交易。 +- **utf8_range 只取 `utf8_range.c` 一个文件**。上游 `third_party/utf8_range/CMakeLists.txt` 里 + `utf8_range` 和 `utf8_validity` 两个 target 都只由这一个文件构成;同目录其余 `.c` + (naive/lookup/lemire-\*/range-\*)是基准实现,而 `main.c` 是基准驱动,其 `main()` 会与消费者撞车。 +- **`gzip` 做成 feature 而非默认**。`io/gzip_stream.cc` 整个被 `#if HAVE_ZLIB` 包住,默认编成空 TU, + 于是默认构建**完全不带 zlib 依赖**;开启 feature 才定义宏并拉入 `compat.zlib`。 + `defines` 只作用于本包 TU,而这个开关恰恰只被 gzip_stream.cc 读取,作用域正好。 + +## 5. 验证结论 + +全部使用与 CI 完全一致的配置:mcpp **2026.8.3.3**(`validate.yml` 的 `MCPP_VERSION`)、 +工具链 gcc@16.1.0、`MCPP_INDEX_MIRROR=GLOBAL`、`MCPP_BUILD_CACHE=local`,三个成员均先清空 +`target/` 与 `.mcpp/` 冷构建: + +``` +::: abseil ::: Compiling compat.abseil v20250512.1 test result ok. 1 passed; 0 failed (18.57s) +::: protobuf ::: Compiling compat.protobuf v35.1 test result ok. 1 passed; 0 failed (63.35s) +::: protobuf-gzip ::: Compiling compat.protobuf v35.1 test result ok. 1 passed; 0 failed (54.48s) +``` + +- **实际编译数已核对**,不是"绿了就算":`compat.abseil` 151 个 `.o` 入链,`compat.protobuf` 80 个。 +- **测试用例只打真符号**。abseil 侧覆盖 StrCat/StrFormat/StrSplit、Cord(撑到 btree 表示)、Status/StatusOr、 + FormatTime/ParseTime(走 vendored cctz)、Mutex+Notification(双线程计数)、flat_hash_map; + protobuf 侧**全程不用任何 protoc 产物**,走 `.proto` 文本 → 描述符 → DynamicMessage → wire/TextFormat/JSON + 往返 + well-known types + UTF-8 校验(打到 utf8_range.c)。截断报文必须被拒,以证明解析确实在做事。 +- **`gzip` feature 做了负向验证**:把成员依赖改回裸 `protobuf = "35.1"` 后,链接给出 + `undefined symbol: google::protobuf::io::GzipOutputStream::…` 等 10+ 条,证明门控真实生效而非默认即编入。 +- **CN 镜像已闭环**:`mcpp-res/abseil@20250512.1`、`mcpp-res/protobuf@35.1` 均 `http=200` 且与 GLOBAL + **字节一致**(sha256 逐一比对)。 + +## 6. 两个已知问题(均为 mcpp 侧,非本描述符缺陷) + +### 6.1 `randen_round_keys.cc` 的假 module import 警告 + +每次构建 `compat.abseil` 都会打印: + +``` +randen_round_keys.cc: module 'binascii' imported but not provided in this build +``` + +该文件把生成常量用的 Python 脚本贴在 `/* … */` 块注释里,其中 `import binascii` 位于第 0 列。mcpp 的 M1 +文本扫描会剥掉 `//` 行注释和裸字符串体(`src/modgraph/scanner.cppm` 的 `strip_raw_strings`),但**不剥块注释**, +于是这一行被当成 C++23 module import。 + +**无法从描述符侧消除**:`scan_overrides` 正是为此设计的,但 mcpp 拒绝"既无 provides 也无 imports"的条目 +(`src/manifest/toml.cppm`),而"无 provides、无 imports"恰恰是这个文件的真实答案;编造一条假边则会 +在与编译器 P1689 输出对账时炸掉。警告是纯装饰性的,TU 编译与链接均正常。修复应在 mcpp 侧(扫描时剥块注释, +或允许空的 scan_overrides 条目)。 + +### 6.2 本组包新增一对 mcpp#344 的撞名样本 + +`compat.abseil` 的 `absl/strings/internal/str_format/parser.cc` 与 `compat.protobuf` 的 +`google/protobuf/compiler/parser.cc` **basename 相同**(经比对,这是两包间唯一的一处)。 + +mcpp 的目标文件消歧依赖**整个构建图**(只有撞名时才用嵌套路径),而包级构建缓存键**不含**这个上下文, +于是同一个缓存条目可能持有两种不兼容的布局。本地用默认全局缓存可稳定复现: + +``` +1) 单跑 abseil -> 以扁平 obj/parser.o 建缓存,通过 +2) 跑 protobuf -> 拉入 abseil,此时撞名,改要嵌套路径;缓存键未变而命中旧条目 + ninja: error: '.../obj/compat_abseil/.../parser.o' missing and no known rule to make it +3) 再单跑 abseil -> 又通过 +``` + +这正是已知的 [mcpp#344](https://github.com/mcpp-community/mcpp/issues/344)(仓库此前由 `tests/examples/archive` +的 zlib + bzip2 双 `compress.c` 撞出),**CI 不受影响**:`validate.yml` 的测试步骤已设 +`MCPP_BUILD_CACHE: local` 绕开包级缓存。上游已在 **2026.8.3.4** 修复 +(`702c1c8 fix(cache): a package's object layout must not depend on the consumer`),已用本地 2026.8.3.5 验证 +其目标文件布局改为无条件嵌套(197/197)。**结论:本次不动 `MCPP_VERSION`**;等 CI 版本自然推进到 ≥2026.8.3.4 +时,可连同 `MCPP_BUILD_CACHE: local` 这条绕行一起移除。 + +## 7. 下一步 + +P1 起进入 gRPC 本体,走独立适配仓(`grpc-m`)路线;本次两个包将作为它的依赖被直接复用, +不再重复 vendor。 diff --git a/.agents/docs/2026-08-04-grpc-feasibility-analysis.md b/.agents/docs/2026-08-04-grpc-feasibility-analysis.md new file mode 100644 index 0000000..3763ebe --- /dev/null +++ b/.agents/docs/2026-08-04-grpc-feasibility-analysis.md @@ -0,0 +1,168 @@ +# gRPC 收录可行性分析(2026-08-04) + +目标:评估 能否以 +`pkgs/c/compat.grpc.lua` 的形态进入 mcpp-index。 + +**结论:不能。gRPC 属于 opencv/ffmpeg 那一档的重型库,应走 `opencv-m` 已经验证过的 +「独立适配仓库 + 索引侧一条 Form-A 描述符」路线,而不是索引内的 compat 描述符。** + +以下每一条判断都附了核实方式,评估基于上游 **v1.83.0**(`git ls-remote --tags` 取到的最新 +release tag)。 + +--- + +## 1. 上游没有自包含 tarball —— 索引的 `url + sha256` 模型直接失配 + +mcpp-index 的每个版本条目都是「一个 tarball + 一个 sha256」。gRPC 没有这样的产物: + +- v1.83.0 **没有任何 release asset**(`GET /repos/grpc/grpc/releases/tags/v1.83.0` → `assets: []`), + 只有 GitHub 自动生成的 tag 归档。 +- 该归档里的 third_party 全是**空的 submodule 占位目录**。实测 + `tar -tzf grpc-1.83.0.tar.gz | grep -c third_party//`: + + | submodule | 归档内条目数 | + |---|---| + | abseil-cpp | 1(空) | + | protobuf | 1(空) | + | re2 | 1(空) | + | boringssl-with-bazel | 1(空) | + | zlib | 1(空) | + | upb / utf8_range / address_sorting | 389 / 40 / 11(有内容) | + +要拿到可构建的源码,必须自己 `git clone --recurse-submodules` 后重新打包,并把这个 +**我们自己造的 tarball** 托管到 mcpp-res/xlings-res(GLOBAL 与 CN 两侧)。这条路本身有先例 +(见记忆 `windows-symlink-archives`),但它意味着索引不再指向可被上游校验的产物,而是长期承担 +一个 ~200MB 级别的自托管资产。这已经超出 `compat.*` 描述符的定位。 + +## 2. 构建形态:CMake 路线不可行,可行的是「整源码直编」 + +### 2.1 `install()` + CMake 会撞上 C++ ABI + +`compat.openssl` / `compat.openblas` 用 `install()` 钩子跑上游自己的构建系统。对 gRPC 复用这个 +模式会失败,原因是 **openssl 是 C,gRPC 是 C++**: + +- `install()` 跑在 mcpp 的编译规则之外,只能用 PATH 上的 `cc`/`c++`(`compat.openssl.lua` 的注释 + 已经写明这一点,并因此在 macOS 上被迫 `CC=/usr/bin/cc`)。 +- 而 mcpp 链接消费者时用的是自己的工具链:linux x86_64 默认 `gcc@16.1.0`,macOS/Windows 默认 + `llvm@20.1.7`(`mcpp/docs/03-toolchains.md`);并且 `[build] cxx_runtime` 默认 + `self-contained` —— macOS 静态链 LLVM 自带的 libc++,linux clang 工具链显式链 + libc++.a/libc++abi.a(`mcpp/docs/05-mcpp-toml.md`)。 +- gRPC 的 C++ 接口(`grpc::Status`、`ServerBuilder`、`ClientContext`)满是 `std::string`。 + 外部 `c++` 编译出的 `libgrpc++.a` 与 mcpp 链接侧的标准库不一致时,是 std 类型布局层面的 + ABI 冲突,不是加个 `-l` 能补的。macOS(Apple clang + 系统 libc++ vs 静态 llvm libc++) + 风险最高。 + +C 语言的 `compat.openssl` 能绕过去,恰恰因为 C 没有这个问题。 + +### 2.2 整源码直编是可行的 —— 而且比预期干净 + +`compat.ffmpeg`(2281 TU / 28 个目录 glob)和 `opencv-m` 已经证明这条路走得通。gRPC 的体量在 +同一量级,并且**上游已经把绝大多数生成物 check-in 了**,实测: + +| 组件 | 非测试源文件数 | 备注 | +|---|---|---| +| grpc `src/core` | 994 | 已含 `ext/upb-gen` 182 + `ext/upbdefs-gen` 166 的 check-in 生成码 | +| grpc `src/cpp` | 91 | C++ 同步/异步 API | +| abseil-cpp | 206 | Apache-2.0 | +| protobuf `src` | 287 | 含 14 个 check-in 的 `.pb.cc`(well-known types) | +| re2 | 28 | BSD-3-Clause | +| c-ares | 94 | MIT | +| **合计** | **≈1700** | 全部为重型 C++17 TU | + +关键有利事实: + +- **不需要跑 protoc 来构建库本身** —— upb 生成码与 protobuf 的 `.pb.cc` 都已在源码树内。 +- **不需要跑 configure** —— c-ares 的平台配置 grpc 上游已经按 OS 冻结好了 + (`third_party/cares/config_linux` / `config_darwin` / `config_windows`),形态与 + `opencv-m` 的 `gen//` 快照完全一致;absl/protobuf/re2 无 configure。 +- **SSL 可复用本仓已有的 `compat.openssl` 3.5.1**,不必再 vendor boringssl(451 个文件 + 汇编)。 + gRPC 官方支持 OpenSSL(`gRPC_SSL_PROVIDER=package`,`cmake/ssl.cmake`),整个 core 里只有 + 9 个文件带 `OPENSSL_IS_BORINGSSL` 分支,且 OpenSSL 是非-boringssl 的默认分支。 + +代价:~1700 个重型 C++ TU 的编译时间远超 ffmpeg 的 C 代码。CI 的 `workspace` job +`timeout-minutes: 150` 有余量,但每晚的 full regression 要在三平台各付一次这个成本。 + +## 3. helloworld 需要 codegen —— 消费侧机制存在,但工具链分发是缺口 + +`examples/cpp/helloworld` 必须先用 `protoc` + `grpc_cpp_plugin` 把 `helloworld.proto` 生成 +`helloworld.pb.cc` / `helloworld.grpc.pb.cc` 才能编译。 + +**好消息:mcpp 有 `build.mcpp`**(`mcpp/docs/07-build-mcpp.md`),这是 Cargo `build.rs` 的对应物, +足以承载 codegen:消费者的 `build.mcpp` 调用 protoc,然后 `mcpp::generated(...)` 把产物加进构建, +`mcpp::include_dir(...)` 加进搜索路径。`opencv-m` 已经在用这个机制。 + +**缺口:插件二进制怎么到消费者手上。** + +- `mcpp::dep_dir("grpc")` 给的是依赖的 **install 目录(源码)**,不是构建产物目录;mcpp + **没有**把依赖的 `kind = "bin"` target 暴露给消费者的机制(已在 mcpp 源码与文档中确认)。 +- 因此 protoc(144 个 `.cc`)与 grpc_cpp_plugin(15 个 `.cc`)不能靠包依赖图交付。 + +可行解是走 xim 构建环境(`[xlings] deps`,与 `xim:make` / `xim:cmake` / `xim:nasm` 同一层): +把两个插件做成一个 `xim:grpc-tools` 预编译包托管到 xlings-res,消费者 +`[xlings] deps = ["grpc-tools@1.83.0"]`,`build.mcpp` 里直接调。protoc 上游本身就发布各平台 +预编译产物;grpc_cpp_plugin 需要我们自行构建并托管。 + +## 4. 建议路线:`grpc-m` 独立适配仓库 + +与 `opencv-m` 同构(该仓库的 `mcpp.toml` 注释明确记录了它是「从 mcpp-index 的 +`compat.opencv.lua` 迁出来的单仓形态」—— gRPC 应当直接从终点开始,跳过中间那一步): + +``` +grpc-m/ + third_party/grpc-1.83.0/ # 剪枝后的上游 vendor(含 absl/protobuf/re2/cares 子模块) + gen/{common,linux,macosx,windows}/ # 平台配置快照(c-ares 的可直接取上游冻结版) + build.mcpp # 平台条件的 include dirs / 源码选择 + src/*.cppm # 可选:C++23 module 层(import grpc;) + tools/vendor/port_descriptor.py # 对标 opencv-m 的 vendoring 脚本,产物可复现 + examples/helloworld/ # 端到端验证 + mcpp.toml +``` + +索引侧最终只增加**一条 Form-A 描述符** `pkgs/g/grpc.lua`(形态同 `pkgs/o/opencv.lua`), +`mcpp` 字段指向该仓自带的 `mcpp.toml`,索引不承载任何构建信息。 + +建议分期: + +- **P0 —— protobuf + abseil(已完成,见下方修订)**:**不需要独立仓**,已作为普通 compat 包直接进索引。 +- **P1 —— grpc core + C++ 同步 API**:加 re2 / c-ares / grpc `src/core` + `src/cpp`, + SSL 复用 `compat.openssl`,跑通 helloworld(unary,同步)。 +- **P2 —— 工具链分发**:`xim:grpc-tools`(protoc + grpc_cpp_plugin 预编译,三平台)。 + 注:protobuf 上游已发布全平台 protoc 预编译包(`protoc-35.1-{linux-x86_64,osx-aarch_64,win64,…}.zip`), + 只有 `grpc_cpp_plugin` 需要我们自建托管。 +- **P3 —— 索引登记**:`pkgs/g/grpc.lua` Form-A + CN 镜像 + workspace 成员。 + +## 4.1 修订(2026-08-04,P0 落地后) + +上文第 4 节最初提议把 protobuf 也做成 `protobuf-m` 独立仓。**这一判断在核实上游发布物后被推翻**, +实际结论更简单: + +- abseil 与 protobuf **都发布了正式的自包含 release asset** + (`abseil-cpp-20250512.1.tar.gz` 2.2MB、`protobuf-35.1.tar.gz` 7.1MB),无 submodule、无 configure、 + 生成码(14 个 `.pb.cc`)已 check-in,因此**不存在自举 protoc 的问题**;protobuf 还自带权威源码清单 + `src/file_lists.cmake`。 +- 三条性质合起来正好落在索引标准的 Shape A 模板里,于是 P0 以两个普通 compat 包落地: + `compat.abseil@20250512.1`(151 TU)与 `compat.protobuf@35.1`(79 TU + utf8_range), + 详见 [2026-08-04-add-abseil-and-protobuf-plan.md](2026-08-04-add-abseil-and-protobuf-plan.md)。 +- 需要独立仓的**只有 gRPC 本身** —— §1 的"上游无自包含 tarball"与 §2 的构建规模问题都只出现在 gRPC, + protobuf/abseil 一条都不占。 + +## 5. 不建议的做法 + +| 做法 | 为什么不行 | +|---|---| +| `pkgs/c/compat.grpc.lua` + `install()` 跑 CMake | C++ ABI 与 mcpp 工具链不一致(§2.1);CI 每次冷构建 | +| 上传各平台预编译静态库 | C++ 静态库跨标准库/glibc 版本不可移植,比 openblas 的 C 场景脆弱得多 | +| 只收录 grpc、让用户自备 protobuf | 上游无自包含 tarball,protobuf 版本必须与 grpc 严格配对 | +| 在索引里放 compat.grpc 先跑通再迁出 | opencv 走过这条路并已迁走;重复一遍只是多一次迁移 | + +--- + +## 附:核实命令 + +```bash +git ls-remote --tags https://github.com/grpc/grpc.git # → v1.83.0 +curl -sL https://api.github.com/repos/grpc/grpc/releases/tags/v1.83.0 | jq .assets # → [] +tar -tzf grpc-1.83.0.tar.gz | grep -c '^grpc-1.83.0/third_party/abseil-cpp/' # → 1 +git clone --depth 1 -b v1.83.0 ... && git submodule update --init --depth 1 ... # → 550MB +grep -rl OPENSSL_IS_BORINGSSL src/core src/cpp include | wc -l # → 9 +``` diff --git a/README.md b/README.md index 5140234..4bf63e7 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Two kinds of packages live here: |------|------| | 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) | | C-source compat (with `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) | +| 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`) | | header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | 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) | | Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) | diff --git a/README.zh-CN.md b/README.zh-CN.md index d6c4f10..fcfb694 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -36,6 +36,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 |------|------| | 原生模块库(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 全源码构建同在包内,索引侧只留本描述符) | | C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) | +| 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`) | | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | 运行时 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) | | 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) | diff --git a/mcpp.toml b/mcpp.toml index 023726e..064ac62 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -7,6 +7,7 @@ # mcpp .agents/docs/2026-06-30-workspace-test-and-zero-shell-index-design.md. [workspace] members = [ + "tests/examples/abseil", "tests/examples/archive", "tests/examples/asio-module", "tests/examples/asio-ssl", @@ -41,6 +42,8 @@ members = [ "tests/examples/nlohmann.json", "tests/examples/openblas", "tests/examples/openssl", + "tests/examples/protobuf", + "tests/examples/protobuf-gzip", "tests/examples/opencv-module", "tests/examples/opencv-module-dnn", "tests/examples/opencv-module-unifont", diff --git a/pkgs/c/compat.abseil.lua b/pkgs/c/compat.abseil.lua new file mode 100644 index 0000000..f669cbe --- /dev/null +++ b/pkgs/c/compat.abseil.lua @@ -0,0 +1,160 @@ +-- compat.abseil — Abseil C++ LTS 20250512.1, built straight from the upstream +-- release tarball as one static archive. +-- +-- Shape A (C++ source compat): no configure step, no code generation, no +-- submodules. Abseil's platform handling is entirely in-source `#ifdef` +-- (`absl/base/config.h`), so one source list covers linux/macosx/windows and +-- the three xpm blocks share a single tarball and sha256. +-- +-- Version numbering follows upstream verbatim: `20250512.1` IS the Abseil LTS +-- tag (`ABSL_LTS_RELEASE_VERSION 20250512` / `ABSL_LTS_RELEASE_PATCH_LEVEL 1` +-- in absl/base/config.h). Not a coincidence worth losing: protobuf 35.1 +-- (MODULE.bazel) and gRPC 1.83.0 (its third_party/abseil-cpp submodule pin, +-- commit 76bb243 = "Abseil LTS Branch, May 2025, Patch 1") BOTH resolve to +-- exactly this release. Keeping the package at the upstream spelling is what +-- lets compat.protobuf — and later gRPC — depend on one shared Abseil instead +-- of each vendoring its own copy and colliding at link time. +-- +-- The upstream release ASSET is used, not the GitHub tag archive: +-- abseil-cpp-20250512.1.tar.gz is a real published artifact with a stable +-- digest (verified by downloading twice). +package = { + spec = "1", + namespace = "compat", + name = "abseil", + description = "Abseil — Google's C++ common libraries (LTS 20250512.1, static)", + licenses = {"Apache-2.0"}, + repo = "https://github.com/abseil/abseil-cpp", + type = "package", + + xpm = { + linux = { + ["20250512.1"] = { + url = { + GLOBAL = "https://github.com/abseil/abseil-cpp/releases/download/20250512.1/abseil-cpp-20250512.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/abseil/releases/download/20250512.1/abseil-20250512.1.tar.gz", + }, + sha256 = "9b7a064305e9fd94d124ffa6cc358592eb42b5da588fb4e07d09254aa40086db", + }, + }, + macosx = { + ["20250512.1"] = { + url = { + GLOBAL = "https://github.com/abseil/abseil-cpp/releases/download/20250512.1/abseil-cpp-20250512.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/abseil/releases/download/20250512.1/abseil-20250512.1.tar.gz", + }, + sha256 = "9b7a064305e9fd94d124ffa6cc358592eb42b5da588fb4e07d09254aa40086db", + }, + }, + windows = { + ["20250512.1"] = { + url = { + GLOBAL = "https://github.com/abseil/abseil-cpp/releases/download/20250512.1/abseil-cpp-20250512.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/abseil/releases/download/20250512.1/abseil-20250512.1.tar.gz", + }, + sha256 = "9b7a064305e9fd94d124ffa6cc358592eb42b5da588fb4e07d09254aa40086db", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + -- Abseil headers are addressed as , so the include root is + -- the tarball wrap dir itself. `*` absorbs `abseil-cpp-20250512.1/`. + include_dirs = { "*" }, + + -- Upstream keeps tests, benchmarks and small generator tools in the + -- SAME directories as the library sources, so the wildcard has to be + -- trimmed. Each exclusion below is one of three kinds; none is + -- cosmetic. + sources = { + "*/absl/**/*.cc", + + -- (1) The naming conventions upstream uses for its own test and + -- benchmark TUs. These carry the bulk of the exclusions (~250 + -- files) and must come first — `*_benchmark.cc` alone accounts for + -- every google/benchmark reference in the tree. + "!*/absl/**/*_test.cc", + "!*/absl/**/*_test_common.cc", + "!*/absl/**/*_testing.cc", + "!*/absl/**/*_benchmark.cc", + "!*/absl/**/*_benchmarks.cc", + + -- (2) The stragglers the conventions above miss: TUs that still + -- pull in gtest/gmock/benchmark, which this package has no + -- dependency on and therefore cannot link. + "!*/absl/log/internal/test_actions.cc", + "!*/absl/log/internal/test_helpers.cc", + "!*/absl/log/internal/test_matchers.cc", + "!*/absl/log/scoped_mock_log.cc", + "!*/absl/status/internal/status_matchers.cc", + "!*/absl/random/benchmarks.cc", + + -- (3) Test-support TUs that compile cleanly but belong to no + -- upstream library target (they exist only for absl's own test and + -- benchmark binaries). + "!*/absl/base/internal/atomic_hook_test_helper.cc", + "!*/absl/base/internal/scoped_set_env.cc", + "!*/absl/container/internal/test_instance_tracker.cc", + "!*/absl/debugging/internal/stack_consumption.cc", + "!*/absl/flags/flag_test_defs.cc", + "!*/absl/random/internal/chi_square.cc", + "!*/absl/random/internal/nanobenchmark.cc", + "!*/absl/strings/internal/pow10_helper.cc", + + -- (4) TUs that define their own main(). These MUST stay out: a + -- dependency's objects all enter the consumer's link (they are not + -- lazily selected out of an archive), so either one would collide + -- with the consumer's own main() and break every build that + -- depends on this package. + "!*/absl/hash/internal/print_hash_of.cc", + "!*/absl/random/internal/gaussian_distribution_gentables.cc", + }, + + -- KNOWN WARNING, not a defect in this descriptor: every build of this + -- package prints + -- randen_round_keys.cc: module 'binascii' imported but not provided + -- randen_round_keys.cc documents how its constants were produced by + -- pasting the generator INSIDE a `/* … */` block comment, and that + -- Python starts with `import binascii` at column 0. mcpp's M1 text + -- scan strips `//` comments and raw-string bodies + -- (src/modgraph/scanner.cppm strip_raw_strings) but NOT block + -- comments, so the line reads as a C++23 module import. + -- + -- It cannot be silenced from here: `scan_overrides` is the mechanism + -- for exactly this, but mcpp rejects an entry that "declares neither + -- provides nor imports" (src/manifest/toml.cppm), and an all-empty + -- scan result — no provides, no imports — is precisely the true + -- answer for this file. Declaring a fake edge instead would be + -- reconciled against the compiler's P1689 output and fail the build. + -- The warning is cosmetic; the TU compiles and links correctly. Fixing + -- it belongs upstream in mcpp (strip block comments in the text scan, + -- or allow an empty scan_overrides entry). + + targets = { ["abseil"] = { kind = "lib" } }, + deps = { }, + + -- randen_hwaes.cc is compiled WITHOUT `-maes -msse4.1` (upstream puts + -- them on a dedicated CMake target). That is correct, not a + -- regression: without accelerated AES the file takes its + -- `!ABSL_RANDEN_HWAES_IMPL` branch, where + -- `HasRandenHwAesImplementation()` returns false — and randen.cc gates + -- on `HasRandenHwAesImplementation() && CPUSupportsRandenHwAes()`, so + -- the stubs are unreachable and absl::BitGen takes RandenSlow. Adding + -- the flags package-wide instead would raise the baseline ISA for + -- every TU, which is a worse trade for a portable index package. + + linux = { + -- Abseil's synchronization/time layers need pthread; -lrt covers + -- clock_gettime on pre-2.17 glibc (folded into libc since). + ldflags = { "-lpthread", "-lrt" }, + }, + -- macOS: libSystem carries pthread and the clock APIs. + windows = { + -- absl/base/CMakeLists.txt links -ladvapi32 for the Windows + -- entropy/thread-identity paths. + ldflags = { "-ladvapi32" }, + }, + }, +} diff --git a/pkgs/c/compat.protobuf.lua b/pkgs/c/compat.protobuf.lua new file mode 100644 index 0000000..3b7e079 --- /dev/null +++ b/pkgs/c/compat.protobuf.lua @@ -0,0 +1,211 @@ +-- compat.protobuf — Protocol Buffers 35.1 C++ RUNTIME (libprotobuf), built +-- straight from the upstream release tarball as one static archive. +-- +-- Shape A (C++ source compat). Three properties of the upstream release make +-- this possible without a configure step or an external build system: +-- +-- 1. protobuf-35.1.tar.gz is a real published release asset (not the GitHub +-- tag archive) and is self-contained: no submodules, and third_party/ +-- carries utf8_range as actual source. +-- 2. The 14 .pb.cc files for the well-known types AND descriptor.pb.cc are +-- CHECKED IN upstream, so building the runtime needs no protoc — there is +-- no bootstrap problem to solve here. +-- 3. Platform handling is entirely in-source `#ifdef` (port_def.inc), so one +-- source list covers linux/macosx/windows and the three xpm blocks share +-- a single tarball and sha256. +-- +-- SCOPE — runtime only. This package builds upstream's `libprotobuf` target +-- (79 TUs), i.e. what a program that *uses* generated code needs: messages, +-- reflection, descriptors, text/JSON formats, the well-known types. It does +-- NOT build `libprotoc` (a further 157 TUs) and ships no protoc binary, so it +-- does not generate .pb.cc from .proto. Consumers either check in +-- protoc-generated sources or build them with the official upstream protoc +-- release (protoc-35.1-.zip); wiring that into an mcpp build belongs +-- to a build.mcpp step, not to this descriptor. +-- +-- Version numbering follows upstream verbatim: `35.1` is the protobuf release, +-- and it is what gRPC 1.83.0 pins (its third_party/protobuf submodule is +-- exactly tag v35.1, commit 35cd01f). The Abseil dependency is pinned to the +-- same LTS both agree on — protobuf's MODULE.bazel says `abseil-cpp +-- 20250512.1` and gRPC's submodule resolves to the same release — so a future +-- gRPC package links ONE Abseil rather than colliding with a second copy. +package = { + spec = "1", + namespace = "compat", + name = "protobuf", + description = "Protocol Buffers — Google's data interchange format, C++ runtime (libprotobuf 35.1, static)", + licenses = {"BSD-3-Clause"}, + repo = "https://github.com/protocolbuffers/protobuf", + type = "package", + + xpm = { + linux = { + ["35.1"] = { + url = { + GLOBAL = "https://github.com/protocolbuffers/protobuf/releases/download/v35.1/protobuf-35.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/protobuf/releases/download/35.1/protobuf-35.1.tar.gz", + }, + sha256 = "f0b6838e7522a8da96126d487068c959bc624926368f3024ac8fd03abd0a1ac4", + }, + }, + macosx = { + ["35.1"] = { + url = { + GLOBAL = "https://github.com/protocolbuffers/protobuf/releases/download/v35.1/protobuf-35.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/protobuf/releases/download/35.1/protobuf-35.1.tar.gz", + }, + sha256 = "f0b6838e7522a8da96126d487068c959bc624926368f3024ac8fd03abd0a1ac4", + }, + }, + windows = { + ["35.1"] = { + url = { + GLOBAL = "https://github.com/protocolbuffers/protobuf/releases/download/v35.1/protobuf-35.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/protobuf/releases/download/35.1/protobuf-35.1.tar.gz", + }, + sha256 = "f0b6838e7522a8da96126d487068c959bc624926368f3024ac8fd03abd0a1ac4", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", -- for third_party/utf8_range/utf8_range.c + + -- `*/src` carries the public headers; + -- `*/third_party/utf8_range` carries utf8_range.h + utf8_validity.h, + -- which protobuf's own TUs include unqualified. + include_dirs = { "*/src", "*/third_party/utf8_range" }, + + -- Transcribed from upstream's own authoritative list — the + -- `libprotobuf_srcs` set in src/file_lists.cmake, which upstream + -- auto-generates from its Bazel rules. Listed file-by-file rather than + -- globbed on purpose: `src/google/protobuf/**/*.cc` would also sweep in + -- libprotoc and the unit tests, and two of the runtime's own TUs live + -- under compiler/ (importer.cc, parser.cc — the .proto text parser is + -- part of the runtime), so no directory-level glob separates the two + -- libraries cleanly. libprotobuf_lite_srcs is a strict subset of this + -- list, so the lite variant needs no separate handling. + sources = { + -- well-known types + descriptor: generated, but CHECKED IN upstream + "*/src/google/protobuf/any.pb.cc", + "*/src/google/protobuf/api.pb.cc", + "*/src/google/protobuf/duration.pb.cc", + "*/src/google/protobuf/empty.pb.cc", + "*/src/google/protobuf/field_mask.pb.cc", + "*/src/google/protobuf/source_context.pb.cc", + "*/src/google/protobuf/struct.pb.cc", + "*/src/google/protobuf/timestamp.pb.cc", + "*/src/google/protobuf/type.pb.cc", + "*/src/google/protobuf/wrappers.pb.cc", + "*/src/google/protobuf/cpp_features.pb.cc", + "*/src/google/protobuf/descriptor.pb.cc", + -- runtime core + "*/src/google/protobuf/any.cc", + "*/src/google/protobuf/any_lite.cc", + "*/src/google/protobuf/arena.cc", + "*/src/google/protobuf/arena_align.cc", + "*/src/google/protobuf/arenastring.cc", + "*/src/google/protobuf/arenaz_sampler.cc", + "*/src/google/protobuf/compiler/importer.cc", + "*/src/google/protobuf/compiler/parser.cc", + "*/src/google/protobuf/descriptor.cc", + "*/src/google/protobuf/descriptor_database.cc", + "*/src/google/protobuf/dynamic_message.cc", + "*/src/google/protobuf/extension_set.cc", + "*/src/google/protobuf/extension_set_heavy.cc", + "*/src/google/protobuf/feature_resolver.cc", + "*/src/google/protobuf/generated_enum_util.cc", + "*/src/google/protobuf/generated_message_bases.cc", + "*/src/google/protobuf/generated_message_reflection.cc", + "*/src/google/protobuf/generated_message_tctable_full.cc", + "*/src/google/protobuf/generated_message_tctable_gen.cc", + "*/src/google/protobuf/generated_message_tctable_lite.cc", + "*/src/google/protobuf/generated_message_util.cc", + "*/src/google/protobuf/implicit_weak_message.cc", + "*/src/google/protobuf/inlined_string_field.cc", + "*/src/google/protobuf/internal_feature_helper.cc", + "*/src/google/protobuf/map.cc", + "*/src/google/protobuf/map_field.cc", + "*/src/google/protobuf/message.cc", + "*/src/google/protobuf/message_lite.cc", + "*/src/google/protobuf/micro_string.cc", + "*/src/google/protobuf/parse_context.cc", + "*/src/google/protobuf/port.cc", + "*/src/google/protobuf/raw_ptr.cc", + "*/src/google/protobuf/reflection_mode.cc", + "*/src/google/protobuf/reflection_ops.cc", + "*/src/google/protobuf/repeated_field.cc", + "*/src/google/protobuf/repeated_ptr_field.cc", + "*/src/google/protobuf/service.cc", + "*/src/google/protobuf/stubs/common.cc", + "*/src/google/protobuf/symbol_checker.cc", + "*/src/google/protobuf/text_format.cc", + "*/src/google/protobuf/unknown_field_set.cc", + "*/src/google/protobuf/wire_format.cc", + "*/src/google/protobuf/wire_format_lite.cc", + -- io + "*/src/google/protobuf/io/coded_stream.cc", + "*/src/google/protobuf/io/gzip_stream.cc", -- empty TU unless the `gzip` feature is on + "*/src/google/protobuf/io/io_win32.cc", + "*/src/google/protobuf/io/printer.cc", + "*/src/google/protobuf/io/strtod.cc", + "*/src/google/protobuf/io/tokenizer.cc", + "*/src/google/protobuf/io/zero_copy_sink.cc", + "*/src/google/protobuf/io/zero_copy_stream.cc", + "*/src/google/protobuf/io/zero_copy_stream_impl.cc", + "*/src/google/protobuf/io/zero_copy_stream_impl_lite.cc", + -- json + "*/src/google/protobuf/json/json.cc", + "*/src/google/protobuf/json/internal/lexer.cc", + "*/src/google/protobuf/json/internal/message_path.cc", + "*/src/google/protobuf/json/internal/parser.cc", + "*/src/google/protobuf/json/internal/unparser.cc", + "*/src/google/protobuf/json/internal/untyped_message.cc", + "*/src/google/protobuf/json/internal/writer.cc", + "*/src/google/protobuf/json/internal/zero_copy_buffered_stream.cc", + -- util + "*/src/google/protobuf/util/delimited_message_util.cc", + "*/src/google/protobuf/util/field_comparator.cc", + "*/src/google/protobuf/util/field_mask_util.cc", + "*/src/google/protobuf/util/message_differencer.cc", + "*/src/google/protobuf/util/time_util.cc", + "*/src/google/protobuf/util/type_resolver_util.cc", + + -- utf8_range: upstream's CMakeLists builds BOTH the `utf8_range` + -- and `utf8_validity` targets from this one file. Every other .c + -- in that directory (naive/lookup/lemire-*/range-*) is a + -- benchmark alternative, and main.c is a benchmark driver whose + -- main() would collide with the consumer's. + "*/third_party/utf8_range/utf8_range.c", + }, + + targets = { ["protobuf"] = { kind = "lib" } }, + + -- protobuf's public headers #include "absl/…" directly, so Abseil is + -- part of this package's interface, not an implementation detail. + deps = { ["compat.abseil"] = "20250512.1" }, + + features = { + -- GzipInputStream / GzipOutputStream. io/gzip_stream.cc is wrapped + -- head-to-toe in `#if HAVE_ZLIB`, so by default it compiles to an + -- empty TU and the package carries no zlib dependency at all; + -- turning the feature on defines the macro for protobuf's own TUs + -- and pulls the provider. (`defines` reaches only this package's + -- TUs, which is exactly the scope needed — the switch is read by + -- gzip_stream.cc, never by consumer code.) + ["gzip"] = { + defines = { "HAVE_ZLIB=1" }, + deps = { ["compat.zlib"] = "1.3.2" }, + }, + }, + + linux = { + ldflags = { "-lpthread" }, + }, + -- macOS: libSystem carries pthread. + -- windows: protobuf's own TUs need no extra system import libs; the + -- Abseil dependency brings -ladvapi32. + }, +} diff --git a/tests/examples/abseil/mcpp.toml b/tests/examples/abseil/mcpp.toml new file mode 100644 index 0000000..99131ee --- /dev/null +++ b/tests/examples/abseil/mcpp.toml @@ -0,0 +1,16 @@ +# abseil test project: consumes compat.abseil and asserts behavior under +# `mcpp test`. Part of the mcpp-index self-referential workspace — the +# workspace-root `[indices]` redirect points at this repo, so the dependency +# resolves to the checked-in recipe (pkgs/c/compat.abseil.lua) rather than the +# published remote index. +# +# The test deliberately exercises symbols that live in COMPILED TUs +# (absl::StrCat, absl::Status, absl::Mutex, absl::Cord, absl::FormatTime), +# not just header inlines: a green build that linked zero Abseil objects would +# otherwise look identical to a working one. +[package] +name = "abseil-tests" +version = "0.1.0" + +[dependencies.compat] +abseil = "20250512.1" diff --git a/tests/examples/abseil/tests/basics.cpp b/tests/examples/abseil/tests/basics.cpp new file mode 100644 index 0000000..08582c0 --- /dev/null +++ b/tests/examples/abseil/tests/basics.cpp @@ -0,0 +1,120 @@ +// Behavioral test for compat.abseil. +// +// Every assertion below crosses into a COMPILED Abseil translation unit, on +// purpose — header-only usage would link zero objects from the package and +// still go green, which is exactly the failure this suite has to catch: +// +// absl::StrCat / StrSplit / StrJoin -> absl/strings/*.cc +// absl::Status / StatusOr -> absl/status/status.cc, statusor.cc +// absl::Mutex / Notification -> absl/synchronization/mutex.cc +// absl::Cord -> absl/strings/cord.cc + cord_rep_*.cc +// absl::FormatTime / ParseTime -> absl/time/*.cc + internal/cctz/src/*.cc +// absl::flat_hash_map -> absl/container/internal/raw_hash_set.cc +// absl::StrFormat -> absl/strings/internal/str_format/*.cc +// +// Returns non-zero on any mismatch. +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/cord.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "absl/synchronization/mutex.h" +#include "absl/synchronization/notification.h" +#include "absl/time/civil_time.h" +#include "absl/time/time.h" + +namespace { + +bool strings_ok() { + if (absl::StrCat("answer=", 42, "/", 1.5) != "answer=42/1.5") return false; + if (absl::StrFormat("%s:%04d:%#x", "id", 7, 255) != "id:0007:0xff") return false; + + const std::vector parts = absl::StrSplit("a,b,,c", ','); + if (parts.size() != 4 || parts[2] != "") return false; + if (absl::StrJoin(parts, "|") != "a|b||c") return false; + return true; +} + +bool cord_ok() { + // Build a Cord large enough to leave the inline representation and go + // through the btree reps in absl/strings/internal/cord_rep_btree*.cc. + absl::Cord cord; + for (int i = 0; i < 512; ++i) cord.Append(absl::StrCat("chunk", i, ";")); + const std::string flat(cord); + return cord.size() == flat.size() && flat.rfind("chunk511;") != std::string::npos; +} + +bool status_ok() { + const absl::Status ok = absl::OkStatus(); + const absl::Status err = absl::InvalidArgumentError("bad input"); + if (!ok.ok() || err.ok()) return false; + if (err.code() != absl::StatusCode::kInvalidArgument) return false; + if (err.message() != "bad input") return false; + // ToString() formats through absl/status/status.cc. + if (err.ToString().find("INVALID_ARGUMENT") == std::string::npos) return false; + + const absl::StatusOr good = 11; + const absl::StatusOr bad = absl::NotFoundError("missing"); + return good.ok() && *good == 11 && !bad.ok(); +} + +bool time_ok() { + // A fixed instant, formatted in UTC: exercises the vendored cctz sources. + const absl::TimeZone utc = absl::UTCTimeZone(); + const absl::Time t = absl::FromCivil(absl::CivilSecond(2026, 8, 4, 12, 34, 56), utc); + if (absl::FormatTime("%Y-%m-%dT%H:%M:%S", t, utc) != "2026-08-04T12:34:56") return false; + + absl::Time parsed; + std::string err; + if (!absl::ParseTime("%Y-%m-%d", "2026-08-04", utc, &parsed, &err)) return false; + return absl::ToUnixSeconds(t) - absl::ToUnixSeconds(parsed) == 12 * 3600 + 34 * 60 + 56; +} + +bool concurrency_ok() { + // absl::Mutex + Notification pull in the real synchronization objects + // (mutex.cc, waiter backends, thread identity). + absl::Mutex mu; + int counter = 0; + absl::Notification started; + + std::thread worker([&] { + started.WaitForNotification(); + for (int i = 0; i < 1000; ++i) { + absl::MutexLock lock(&mu); + ++counter; + } + }); + + started.Notify(); + for (int i = 0; i < 1000; ++i) { + absl::MutexLock lock(&mu); + ++counter; + } + worker.join(); + + absl::MutexLock lock(&mu); + return counter == 2000; +} + +bool container_ok() { + absl::flat_hash_map m; + for (int i = 0; i < 256; ++i) m.emplace(absl::StrCat("k", i), i); + if (m.size() != 256) return false; + const auto it = m.find("k255"); + return it != m.end() && it->second == 255; +} + +} // namespace + +int main() { + const bool ok = strings_ok() && cord_ok() && status_ok() && time_ok() + && concurrency_ok() && container_ok(); + return ok ? 0 : 1; +} diff --git a/tests/examples/protobuf-gzip/mcpp.toml b/tests/examples/protobuf-gzip/mcpp.toml new file mode 100644 index 0000000..884cc3b --- /dev/null +++ b/tests/examples/protobuf-gzip/mcpp.toml @@ -0,0 +1,13 @@ +# protobuf `gzip` feature member. The sibling tests/examples/protobuf covers +# the DEFAULT build, where io/gzip_stream.cc compiles to an empty TU and no +# zlib dependency is pulled at all. +# +# Here the feature is requested, which defines HAVE_ZLIB=1 for protobuf's own +# TUs and pulls compat.zlib as the provider. The long-form dependency spelling +# is what carries `features`. +[package] +name = "protobuf-gzip-tests" +version = "0.1.0" + +[dependencies.compat] +protobuf = { version = "35.1", features = ["gzip"] } diff --git a/tests/examples/protobuf-gzip/tests/gzip.cpp b/tests/examples/protobuf-gzip/tests/gzip.cpp new file mode 100644 index 0000000..94c4c55 --- /dev/null +++ b/tests/examples/protobuf-gzip/tests/gzip.cpp @@ -0,0 +1,79 @@ +// Behavioral test for compat.protobuf's `gzip` feature. +// +// io/gzip_stream.cc is wrapped head-to-toe in `#if HAVE_ZLIB`, so with the +// feature OFF the TU is empty and GzipOutputStream / GzipInputStream have no +// definitions — this member therefore fails to LINK without the feature, +// which is the negative half of the verification (see the design doc). +// +// With it on: compress a payload, assert the compressed form is actually +// smaller and is a real gzip stream (magic 0x1f 0x8b), then decompress and +// assert byte equality. +#include + +#include "google/protobuf/io/coded_stream.h" +#include "google/protobuf/io/gzip_stream.h" +#include "google/protobuf/io/zero_copy_stream_impl_lite.h" + +namespace gpb = google::protobuf; + +namespace { + +std::string make_payload() { + // Highly repetitive, so a working deflate must shrink it a lot. Random + // data would leave "compressed" ~= "original" and make the size assertion + // meaningless. + std::string s; + for (int i = 0; i < 2000; ++i) s += "the quick brown fox jumps over the lazy dog\n"; + return s; +} + +bool compress(const std::string& in, std::string* out) { + gpb::io::StringOutputStream sink(out); + gpb::io::GzipOutputStream::Options opts; + opts.format = gpb::io::GzipOutputStream::GZIP; + gpb::io::GzipOutputStream gz(&sink, opts); + { + // GzipOutputStream is a ZeroCopyOutputStream (Next/BackUp); CodedOutputStream + // is the adapter that turns that into a plain buffer write. It must be + // destroyed — i.e. flushed back into gz — before gz.Close(). + gpb::io::CodedOutputStream coded(&gz); + coded.WriteRaw(in.data(), static_cast(in.size())); + if (coded.HadError()) return false; + } + return gz.Close(); +} + +bool decompress(const std::string& in, std::string* out) { + gpb::io::ArrayInputStream source(in.data(), static_cast(in.size())); + gpb::io::GzipInputStream gz(&source, gpb::io::GzipInputStream::GZIP); + + const void* chunk = nullptr; + int size = 0; + while (gz.Next(&chunk, &size)) { + if (size > 0) out->append(static_cast(chunk), static_cast(size)); + } + return gz.ZlibErrorMessage() == nullptr; +} + +} // namespace + +int main() { + const std::string original = make_payload(); + + std::string compressed; + if (!compress(original, &compressed)) return 1; + + // A real gzip member starts with the 0x1f 0x8b magic. + if (compressed.size() < 2) return 1; + if (static_cast(compressed[0]) != 0x1f) return 1; + if (static_cast(compressed[1]) != 0x8b) return 1; + + // Repetitive input must compress substantially; this also proves deflate + // ran rather than the stream passing bytes through. + if (compressed.size() >= original.size() / 10) return 1; + + std::string restored; + if (!decompress(compressed, &restored)) return 1; + + return restored == original ? 0 : 1; +} diff --git a/tests/examples/protobuf/mcpp.toml b/tests/examples/protobuf/mcpp.toml new file mode 100644 index 0000000..b90ce98 --- /dev/null +++ b/tests/examples/protobuf/mcpp.toml @@ -0,0 +1,17 @@ +# protobuf test project: consumes compat.protobuf (which in turn pulls +# compat.abseil) and asserts behavior under `mcpp test`. Part of the +# mcpp-index self-referential workspace — the workspace-root `[indices]` +# redirect points at this repo, so both dependencies resolve to the checked-in +# recipes (pkgs/c/compat.protobuf.lua, pkgs/c/compat.abseil.lua) rather than +# the published remote index. +# +# No .proto codegen anywhere in this member, by design: compat.protobuf ships +# the RUNTIME, not protoc. The test drives the runtime through the descriptor +# / DynamicMessage path plus the checked-in well-known types, which is exactly +# the surface a consumer gets without a compiler. +[package] +name = "protobuf-tests" +version = "0.1.0" + +[dependencies.compat] +protobuf = "35.1" diff --git a/tests/examples/protobuf/tests/runtime.cpp b/tests/examples/protobuf/tests/runtime.cpp new file mode 100644 index 0000000..b1217cb --- /dev/null +++ b/tests/examples/protobuf/tests/runtime.cpp @@ -0,0 +1,177 @@ +// Behavioral test for compat.protobuf (the libprotobuf runtime). +// +// Deliberately uses NO protoc-generated code: the package ships the runtime, +// not the compiler, so this drives the surface a consumer actually gets — +// +// .proto text -> descriptor compiler/parser.cc, io/tokenizer.cc +// descriptor -> message descriptor.cc, dynamic_message.cc +// set/get fields generated_message_reflection.cc, map_field.cc +// serialize / parse wire_format.cc, parse_context.cc, coded_stream.cc +// TextFormat text_format.cc +// JSON json/json.cc + json/internal/*.cc +// well-known types timestamp.pb.cc, struct.pb.cc, util/time_util.cc +// MessageDifferencer util/message_differencer.cc +// UTF-8 validation third_party/utf8_range/utf8_range.c +// +// Returns non-zero on any mismatch. +#include +#include + +#include "google/protobuf/compiler/parser.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/descriptor.pb.h" +#include "google/protobuf/dynamic_message.h" +#include "google/protobuf/io/tokenizer.h" +#include "google/protobuf/io/zero_copy_stream_impl_lite.h" +#include "google/protobuf/json/json.h" +#include "google/protobuf/struct.pb.h" +#include "google/protobuf/text_format.h" +#include "google/protobuf/timestamp.pb.h" +#include "google/protobuf/util/message_differencer.h" +#include "google/protobuf/util/time_util.h" + +namespace gpb = google::protobuf; + +namespace { + +// Collects parse errors instead of printing them, so a broken schema shows up +// as a failed assertion rather than noise on stderr. +class SilentErrorCollector : public gpb::io::ErrorCollector { +public: + void RecordError(int line, gpb::io::ColumnNumber col, absl::string_view msg) override { + ++errors; + (void)line; + (void)col; + (void)msg; + } + int errors = 0; +}; + +constexpr const char* kSchema = R"( + syntax = "proto3"; + package mcpp.test; + message Person { + string name = 1; + int32 id = 2; + repeated string tags = 3; + map scores = 4; + } +)"; + +// Parse the .proto TEXT above into a FileDescriptorProto. This is the runtime's +// own parser (compiler/parser.cc), not protoc. +bool build_pool(gpb::DescriptorPool& pool, const gpb::Descriptor** out) { + gpb::io::ArrayInputStream raw(kSchema, static_cast(std::string(kSchema).size())); + SilentErrorCollector collector; + gpb::io::Tokenizer tokenizer(&raw, &collector); + + gpb::FileDescriptorProto file; + gpb::compiler::Parser parser; + parser.RecordErrorsTo(&collector); + if (!parser.Parse(&tokenizer, &file) || collector.errors != 0) return false; + file.set_name("mcpp_test.proto"); + + const gpb::FileDescriptor* fd = pool.BuildFile(file); + if (fd == nullptr) return false; + *out = pool.FindMessageTypeByName("mcpp.test.Person"); + return *out != nullptr; +} + +bool dynamic_message_roundtrip(const gpb::Descriptor* desc) { + gpb::DynamicMessageFactory factory; + const gpb::Message* prototype = factory.GetPrototype(desc); + if (prototype == nullptr) return false; + + std::unique_ptr msg(prototype->New()); + const gpb::Reflection* ref = msg->GetReflection(); + + ref->SetString(msg.get(), desc->FindFieldByName("name"), "Ada"); + ref->SetInt32(msg.get(), desc->FindFieldByName("id"), 1815); + const gpb::FieldDescriptor* tags = desc->FindFieldByName("tags"); + ref->AddString(msg.get(), tags, "math"); + ref->AddString(msg.get(), tags, "engine"); + + // Wire round-trip. + std::string wire; + if (!msg->SerializeToString(&wire)) return false; + std::unique_ptr back(prototype->New()); + if (!back->ParseFromString(wire)) return false; + if (!gpb::util::MessageDifferencer::Equals(*msg, *back)) return false; + + // TextFormat round-trip. + std::string text; + if (!gpb::TextFormat::PrintToString(*back, &text)) return false; + if (text.find("name: \"Ada\"") == std::string::npos) return false; + if (text.find("id: 1815") == std::string::npos) return false; + std::unique_ptr from_text(prototype->New()); + if (!gpb::TextFormat::ParseFromString(text, from_text.get())) return false; + if (!gpb::util::MessageDifferencer::Equals(*msg, *from_text)) return false; + + // A truncated payload must be REJECTED — proves parsing is real and not a + // no-op that accepts anything. + std::unique_ptr truncated(prototype->New()); + if (!wire.empty() && truncated->ParseFromString(wire.substr(0, wire.size() - 1))) return false; + + return ref->GetRepeatedString(*back, tags, 1) == "engine"; +} + +bool json_ok(const gpb::Descriptor* desc) { + gpb::DynamicMessageFactory factory; + std::unique_ptr msg(factory.GetPrototype(desc)->New()); + const gpb::Reflection* ref = msg->GetReflection(); + ref->SetString(msg.get(), desc->FindFieldByName("name"), "Grace"); + ref->SetInt32(msg.get(), desc->FindFieldByName("id"), 1906); + + std::string json; + if (!gpb::json::MessageToJsonString(*msg, &json).ok()) return false; + if (json.find("\"name\":\"Grace\"") == std::string::npos) return false; + + std::unique_ptr back(factory.GetPrototype(desc)->New()); + if (!gpb::json::JsonStringToMessage(json, back.get()).ok()) return false; + return gpb::util::MessageDifferencer::Equals(*msg, *back); +} + +// The well-known types are generated code that upstream CHECKS IN, so they are +// compiled into this package and usable with no protoc anywhere. +bool well_known_types_ok() { + gpb::Timestamp ts; + if (!gpb::util::TimeUtil::FromString("2026-08-04T12:34:56Z", &ts)) return false; + if (gpb::util::TimeUtil::ToString(ts) != "2026-08-04T12:34:56Z") return false; + + gpb::Struct s; + (*s.mutable_fields())["pi"].set_number_value(3.5); + (*s.mutable_fields())["name"].set_string_value("mcpp"); + if (s.fields().size() != 2) return false; + if (s.fields().at("pi").number_value() != 3.5) return false; + + std::string wire; + if (!s.SerializeToString(&wire)) return false; + gpb::Struct back; + return back.ParseFromString(wire) && back.fields().at("name").string_value() == "mcpp"; +} + +// proto3 string fields are UTF-8 validated, which routes into +// third_party/utf8_range/utf8_range.c — the one C TU in this package. +bool utf8_validation_ok(const gpb::Descriptor* desc) { + gpb::DynamicMessageFactory factory; + std::unique_ptr msg(factory.GetPrototype(desc)->New()); + msg->GetReflection()->SetString(msg.get(), desc->FindFieldByName("name"), "héllo-世界"); + + std::string wire; + if (!msg->SerializeToString(&wire)) return false; + std::unique_ptr back(factory.GetPrototype(desc)->New()); + if (!back->ParseFromString(wire)) return false; + return back->GetReflection()->GetString(*back, desc->FindFieldByName("name")) == "héllo-世界"; +} + +} // namespace + +int main() { + gpb::DescriptorPool pool; + const gpb::Descriptor* person = nullptr; + if (!build_pool(pool, &person)) return 1; + + const bool ok = dynamic_message_roundtrip(person) && json_ok(person) + && well_known_types_ok() && utf8_validation_ok(person); + return ok ? 0 : 1; +} From f1657cce44099b9416f63a81f4683d421d51dc52 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 4 Aug 2026 22:54:06 +0800 Subject: [PATCH 2/2] fix(abseil,protobuf): supply the per-OS flags upstream's build system does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's linux leg passed; macOS and Windows did not, both for the same reason — each upstream's own build system carries platform settings that the descriptors did not restate. macOS, compat.abseil. cctz's time_zone_lookup.cc reads the system time zone through CFTimeZoneCopyDefault / CFStringGet* / CFRelease on Apple platforms, so CoreFoundation belongs on the link line; upstream's absl/time/CMakeLists.txt carries `$<$:-Wl,-framework,CoreFoundation>`. Without it the package compiles fine and the failure lands in the CONSUMER's link: ld64.lld: error: undefined symbol: CFTimeZoneCopyDefault ld64.lld: error: undefined symbol: CFStringGetCString (+4 more) Windows, both packages. defines min/max as function-like MACROS, which turns absl/time/time.cc's `std::numeric_limits::max()` into error: too few arguments provided to function-like macro invocation note: macro 'max' defined here (Windows Kits …/minwindef.h) NOMINMAX is therefore load-bearing rather than hygiene. It comes from upstream's ABSL_MSVC_FLAGS (absl/copts/GENERATED_AbseilCopts.cmake), along with WIN32_LEAN_AND_MEAN and _CRT_SECURE_NO_WARNINGS. compat.protobuf gets the same three: it reaches through io/io_win32.cc and port.h while using std::min/std::max throughout, and upstream's protobuf CMake only gets away with staying silent because its Abseil dependency's copts supply them — here each package carries its own compile flags. Only the macosx/windows blocks changed. Re-verified on linux with the pinned mcpp 2026.8.3.3 + MCPP_BUILD_CACHE=local, target/ removed first: abseil, protobuf and protobuf-gzip all `test result ok`. Lint and `mcpp xpkg parse` still pass. The other two platforms are verifiable only in CI. --- pkgs/c/compat.abseil.lua | 22 ++++++++++++++++++++-- pkgs/c/compat.protobuf.lua | 14 +++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pkgs/c/compat.abseil.lua b/pkgs/c/compat.abseil.lua index f669cbe..a96ff9b 100644 --- a/pkgs/c/compat.abseil.lua +++ b/pkgs/c/compat.abseil.lua @@ -150,11 +150,29 @@ package = { -- clock_gettime on pre-2.17 glibc (folded into libc since). ldflags = { "-lpthread", "-lrt" }, }, - -- macOS: libSystem carries pthread and the clock APIs. + macosx = { + -- libSystem carries pthread and the clock APIs, but NOT the time + -- zone lookup: on Apple platforms cctz's time_zone_lookup.cc reads + -- the system zone through CFTimeZoneCopyDefault / CFStringGet* / + -- CFRelease, so CoreFoundation has to be on the link line. + -- Upstream does the same — absl/time/CMakeLists.txt carries + -- `$<$:-Wl,-framework,CoreFoundation>`. + -- Without it the package builds and only fails at LINK time in the + -- consumer, with six undefined CF* symbols. + ldflags = { "-framework", "CoreFoundation" }, + }, windows = { + -- Upstream's own MSVC copts (absl/copts/GENERATED_AbseilCopts.cmake + -- ABSL_MSVC_FLAGS). NOMINMAX is load-bearing, not hygiene: + -- defines min/max as function-like MACROS, which turns + -- every `std::numeric_limits::max()` in absl/time/time.cc + -- into "too few arguments provided to function-like macro + -- invocation". WIN32_LEAN_AND_MEAN trims the same header down, and + -- _CRT_SECURE_NO_WARNINGS silences the CRT deprecation noise. + cxxflags = { "-DNOMINMAX", "-DWIN32_LEAN_AND_MEAN", "-D_CRT_SECURE_NO_WARNINGS" }, -- absl/base/CMakeLists.txt links -ladvapi32 for the Windows -- entropy/thread-identity paths. - ldflags = { "-ladvapi32" }, + ldflags = { "-ladvapi32" }, }, }, } diff --git a/pkgs/c/compat.protobuf.lua b/pkgs/c/compat.protobuf.lua index 3b7e079..294ebe8 100644 --- a/pkgs/c/compat.protobuf.lua +++ b/pkgs/c/compat.protobuf.lua @@ -204,8 +204,16 @@ package = { linux = { ldflags = { "-lpthread" }, }, - -- macOS: libSystem carries pthread. - -- windows: protobuf's own TUs need no extra system import libs; the - -- Abseil dependency brings -ladvapi32. + -- macOS: libSystem carries pthread, and the CoreFoundation framework + -- the time zone lookup needs comes in through compat.abseil. + windows = { + -- Same min/max macro hazard as compat.abseil: protobuf + -- reaches through io/io_win32.cc and port.h while using + -- std::min/std::max throughout. Upstream's CMake does not spell + -- NOMINMAX out because its Abseil dependency's copts already do; + -- here each package carries its own compile flags, so it has to be + -- stated. No extra import libs: -ladvapi32 arrives with abseil. + cxxflags = { "-DNOMINMAX", "-DWIN32_LEAN_AND_MEAN", "-D_CRT_SECURE_NO_WARNINGS" }, + }, }, }