diff --git a/.agents/docs/2026-09-23-add-yaml-cpp-plan.md b/.agents/docs/2026-09-23-add-yaml-cpp-plan.md new file mode 100644 index 00000000..9fa4f0f4 --- /dev/null +++ b/.agents/docs/2026-09-23-add-yaml-cpp-plan.md @@ -0,0 +1,59 @@ +# compat.yaml-cpp 0.8.0 / 0.9.0 + +## Shape + +C++ source compat (Form B). Upstream's CMake target is `src/*.cpp` plus, with +`YAML_CPP_BUILD_CONTRIB` at its default ON, `src/contrib/*.cpp`. No configure +step, no generated header, no platform code in the library. 0.9.0 adds one TU +(`src/fptostring.cpp`, over the vendored `src/contrib/dragonbox.h`) and one +public header (`yaml-cpp/fptostring.h`); one glob covers both versions. + +Tags are spelled differently upstream (`0.8.0`, `yaml-cpp-0.9.0`), so the wrap +dirs differ (`yaml-cpp-0.8.0/`, `yaml-cpp-yaml-cpp-0.9.0/`); `*` absorbs both. + +## Mirror + +gitcode `mcpp-res/yaml-cpp`, releases `0.8.0` and `0.9.0`, assets byte-identical +to the GitHub tag archives (sha256 checked after download from both sides; each +GitHub archive downloaded twice, digest stable). + +| version | sha256 | +| --- | --- | +| 0.8.0 | `fbe74bbdcee21d656715688706da3c8becfd946d92cd44705cc6098bb23b3a16` | +| 0.9.0 | `25cb043240f828a8c51beb830569634bc7ac603978e0f69d6b63558dadefd49a` | + +## YAML_CPP_STATIC_DEFINE + +Upstream makes it a PUBLIC definition of a static build and reads it only in +`include/yaml-cpp/dll.h`, which every public header reaches. A descriptor's +`defines` are package-private, so `mcpp_generated/yaml-cpp/dll.h` defines it +and `#include_next`s upstream's; `mcpp_generated` is first in `include_dirs`. +The compile database confirms the consumer TU carries no `-D` for it, and the +test's `#error` guard passes: the shim delivered it. + +## Upstream defect found by the test + +`GraphBuilderInterface` declares its destructor pure virtual and defines it +nowhere, in both versions. Any derived class (upstream's own `GraphBuilder` +included) fails to link with `undefined symbol: ~GraphBuilderInterface()` +unless its user defines it. The descriptor does not supply the definition: a +consumer that already writes it would then get a duplicate symbol. The test +writes it, as any consumer of the contrib API must. + +## Features + +None. Contrib is on by default upstream and is two small TUs; there is no +optional component worth a gate. + +## Verification + +- `mcpp test -p yaml-cpp` / `-p yaml-cpp-v080` with the pinned mcpp 2026.9.18.3, + cold: `test result ok`. Objects: 32 + test TU (0.9.0), 31 + test TU (0.8.0), + equal to the source count. +- 0.8.0 member asserts at compile time that `yaml-cpp/fptostring.h` is absent, + so it cannot pass by silently resolving 0.9.0. +- openkal: both members are listed in `tests/openkal/members.toml`. Locally, + `x86_64-linux-gnu`: `runs (posix)` for both. The Windows leg is taken from + the PR's openkal-compat run (a local run with an unpinned mcpp failed inside + openkal-llvm-runtime's libunwind for the cli11 control member as well, so it + says nothing about this package). diff --git a/.github/workflows/openkal-compat.yml b/.github/workflows/openkal-compat.yml index 9f091a0a..49ff23b2 100644 --- a/.github/workflows/openkal-compat.yml +++ b/.github/workflows/openkal-compat.yml @@ -25,6 +25,7 @@ on: paths: - "pkgs/**/*.lua" - "tests/openkal/**" + - "tests/examples/**" - ".xpkgindex/openkal-compat.json" - ".github/workflows/openkal-compat.yml" @@ -68,15 +69,17 @@ jobs: with: fetch-depth: 0 - # A pull request measures the members its change can affect; the schedule - # and a request with no members measure every listed member. + # A pull request measures the members its change can affect (see + # `select_members` in compat.py: adding one member measures that member); + # the schedule and a request with no members measure every listed member. - name: Select the members id: select shell: bash run: | if [ "${{ github.event_name }}" = "pull_request" ]; then - files=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD") - members=$(python3 tests/openkal/compat.py select $files) + base=$(git merge-base "origin/${{ github.base_ref }}" HEAD) + files=$(git diff --name-only "$base" HEAD) + members=$(python3 tests/openkal/compat.py select --base "$base" $files) else members="${{ github.event.inputs.members }}" [ -n "$members" ] || members=$(python3 tests/openkal/compat.py select tests/openkal/) diff --git a/.xpkgindex/openkal-compat.json b/.xpkgindex/openkal-compat.json index 2fabfa59..7b956994 100644 --- a/.xpkgindex/openkal-compat.json +++ b/.xpkgindex/openkal-compat.json @@ -7,7 +7,7 @@ "protobuf-protoc": "runs a host code generator", "sycl-runtime": "a vendor runtime distributed as binaries for the platform C runtime" }, - "measured": "2026-09-20", + "measured": "2026-09-23", "members": { "archive": { "packages": [ @@ -24,9 +24,9 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/archive/.mcpp/.xlings/data/xpkgs/compat-x-xz/5.8.3/xz-5.8.3/src/common/tuklib_physmem.c:21:11: fatal error: 'windows.h' file not found", - "status": "fails" + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" } } }, @@ -40,7 +40,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -56,7 +56,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -72,9 +72,9 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/c-ares/.mcpp/.xlings/data/xpkgs/compat-x-c-ares/1.34.5/c-ares-1.34.5/src/lib/ares_setup.h:81:12: fatal error: 'windows.h' file not found", - "status": "fails" + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" } } }, @@ -88,7 +88,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -104,7 +104,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -120,7 +120,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -136,7 +136,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -152,7 +152,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -165,10 +165,12 @@ "portable": true, "targets": { "x86_64-linux-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/mcpp-2026.9.18.3-linux-x86_64/registry/data/xpkgs/chriskohlhoff-x-asio/1.38.1/asio-asio-1-38-1/include/asio/detail/config.hpp:899:11: fatal error: 'linux/version.h' file not found", + "declared": "not-portable", + "declared_reason": "asio's detail/config.hpp:899 includes whenever __linux__ is defined, OUTSIDE every ASIO_DISABLE_* guard, so no configuration macro prevents it. openkal runs on the Linux kernel, so __linux__ is correct; the uapi headers glibc's userspace installs beside it are not part of what openkal presents.", + "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/mcpp-2026.9.21.3-linux-x86_64/registry/data/xpkgs/chriskohlhoff-x-asio/1.38.1/asio-asio-1-38-1/include/asio/detail/config.hpp:899:11: fatal error: 'linux/version.h' file not found", "status": "fails" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -181,10 +183,14 @@ "portable": true, "targets": { "x86_64-linux-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/curl/.mcpp/.xlings/data/xpkgs/compat-x-curl/8.21.0/curl-curl-8_21_0/lib/setopt.c:31:10: fatal error: 'linux/tcp.h' file not found", + "declared": "not-portable", + "declared_reason": "curl's TLS backend here is compat.openssl, which builds through OpenSSL's own Configure from an install() hook and is therefore a glibc artefact: it declares provides = [\"platform-sdk\"] and fails this graph's link at setcontext and __isoc23_strtol, which musl does not have. Building OpenSSL inside the graph would change this cell.", + "diagnostic": "ld.lld: error: undefined symbol: __memcpy_chk", "status": "fails" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { + "declared": "not-portable", + "declared_reason": "curl's Windows configuration is Schannel plus the checked-in lib/config-win32.h, and both are selected by _WIN32, which a C library presenting POSIX on Windows does not define. With neither HAVE_CONFIG_H nor _WIN32, curl_setup.h reads no configuration at all and stops at curl_setup.h:591 'too small curl_off_t', which names a consequence and not the cause. compat.openssl has no Windows build, so there is no second TLS provider to fall back to.", "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/curl/.mcpp/.xlings/data/xpkgs/compat-x-curl/8.21.0/curl-curl-8_21_0/lib/curl_setup.h:591:2: error: \"too small curl_off_t\"", "status": "fails" } @@ -200,9 +206,9 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { - "diagnostic": "ld.lld: error: undefined symbol: __cxa_thread_atexit", - "status": "fails" + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" } } }, @@ -216,7 +222,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -229,10 +235,10 @@ "portable": false, "targets": { "x86_64-linux-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/expat/.mcpp/.xlings/data/xpkgs/compat-x-expat/2.7.1/lib/xmlparse.c:977:3: error: call to undeclared function 'arc4random_buf'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]", - "status": "fails" + "kind": "platform", + "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "platform", "status": "runs" } @@ -248,7 +254,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -264,7 +270,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -280,7 +286,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -296,7 +302,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -312,7 +318,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -328,7 +334,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -344,9 +350,9 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/mimalloc/.mcpp/.xlings/data/xpkgs/compat-x-mimalloc/3.4.5/mimalloc-3.4.5/include/mimalloc/atomic.h:16:10: fatal error: 'windows.h' file not found", - "status": "fails" + "x86_64-windows-musl": { + "diagnostic": "alloc ... FAIL (exit 1, 2.03s)", + "status": "builds" } } }, @@ -360,7 +366,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -376,7 +382,7 @@ "kind": "platform", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "platform", "status": "runs" } @@ -392,7 +398,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -408,9 +414,9 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { - "diagnostic": "ld.lld: error: undefined symbol: __cxa_thread_atexit", - "status": "fails" + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" } } }, @@ -424,9 +430,9 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { - "diagnostic": "/home/runner/work/mcpp-index/mcpp-index/tests/openkal-work/sqlite3/.mcpp/.xlings/data/xpkgs/compat-x-sqlite3/3.45.3/sqlite-amalgamation-3450300/sqlite3.c:29506:10: fatal error: 'windows.h' file not found", - "status": "fails" + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" } } }, @@ -440,7 +446,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -456,7 +462,39 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" + } + } + }, + "yaml-cpp": { + "packages": [ + "compat.yaml-cpp" + ], + "portable": true, + "targets": { + "x86_64-linux-gnu": { + "kind": "posix", + "status": "runs" + }, + "x86_64-windows-musl": { + "kind": "posix", + "status": "runs" + } + } + }, + "yaml-cpp-v080": { + "packages": [ + "compat.yaml-cpp" + ], + "portable": true, + "targets": { + "x86_64-linux-gnu": { + "kind": "posix", + "status": "runs" + }, + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -472,7 +510,7 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } @@ -488,16 +526,25 @@ "kind": "posix", "status": "runs" }, - "x86_64-windows-gnu": { + "x86_64-windows-musl": { "kind": "posix", "status": "runs" } } } }, + "not_portable": { + "cmp-module": { + "x86_64-linux-gnu": "asio's detail/config.hpp:899 includes whenever __linux__ is defined, OUTSIDE every ASIO_DISABLE_* guard, so no configuration macro prevents it. openkal runs on the Linux kernel, so __linux__ is correct; the uapi headers glibc's userspace installs beside it are not part of what openkal presents." + }, + "curl": { + "x86_64-linux-gnu": "curl's TLS backend here is compat.openssl, which builds through OpenSSL's own Configure from an install() hook and is therefore a glibc artefact: it declares provides = [\"platform-sdk\"] and fails this graph's link at setcontext and __isoc23_strtol, which musl does not have. Building OpenSSL inside the graph would change this cell.", + "x86_64-windows-musl": "curl's Windows configuration is Schannel plus the checked-in lib/config-win32.h, and both are selected by _WIN32, which a C library presenting POSIX on Windows does not define. With neither HAVE_CONFIG_H nor _WIN32, curl_setup.h reads no configuration at all and stops at curl_setup.h:591 'too small curl_off_t', which names a consequence and not the cause. compat.openssl has no Windows build, so there is no second TLS provider to fall back to." + } + }, "pins": { - "mcpp": "2026.9.18.3", - "runtime": "0.12.0", + "mcpp": "2026.9.21.3", + "runtime": "0.15.1", "toolchain": "llvm@22.1.8" }, "schema": 1 diff --git a/.xpkgindex/plugins/mcpp.py b/.xpkgindex/plugins/mcpp.py index 69a90c54..5f6a9769 100644 --- a/.xpkgindex/plugins/mcpp.py +++ b/.xpkgindex/plugins/mcpp.py @@ -46,17 +46,25 @@ def _t(en: str, zh: str, hant: str) -> Dict[str, str]: ("external", _t("upstream mcpp.toml", "上游 mcpp.toml", "上游 mcpp.toml"), "neutral"), ] -# Whether a package's test project builds and runs on openkal, measured by -# tests/openkal/compat.py and recorded in .xpkgindex/openkal-compat.json. The -# label is a measurement, never a declaration: a descriptor carries no field for -# it. Ordered from the strongest statement to the weakest. +# The two values the `openkal` facet files a package under, and the badge each +# puts on the package. They are names, not sentences, and are the same in every +# language: +# +# openkal-ecosystem a package that makes up openkal (OPENKAL_FAMILY below) +# openkal-compat a package whose test project was measured to RUN in an +# openkal graph, by tests/openkal/compat.py, recorded in +# .xpkgindex/openkal-compat.json +# +# `openkal-compat` is a measurement, never a declaration: a descriptor carries +# no field for it. A package measured only to build, or to fail, is filed under +# neither -- the facet lists what a reader can pick, and that package's page +# still shows its per-target measurement with the first diagnostic. OPENKAL_LEVELS = [ - ("family", _t("openkal itself", "openkal 本身", "openkal 本身"), "module"), - ("runs", _t("runs on openkal", "在 openkal 上运行", "在 openkal 上執行"), "module"), - ("builds", _t("builds on openkal", "在 openkal 上构建", "在 openkal 上建置"), "header"), - ("fails", _t("fails on openkal", "在 openkal 上失败", "在 openkal 上失敗"), "neutral"), - ("n/a", _t("not applicable", "不适用", "不適用"), "neutral"), + ("ecosystem", _t("openkal-ecosystem", "openkal-ecosystem", "openkal-ecosystem"), "module"), + ("compat", _t("openkal-compat", "openkal-compat", "openkal-compat"), "module"), ] +# The measured level -> the facet value it is filed under. +_OPENKAL_FACET = {"family": "ecosystem", "runs": "compat"} _OPENKAL_RANK = {"fails": 0, "builds": 1, "runs": 2} # How a package's best-measured target relates to the platform, orthogonal to @@ -514,11 +522,12 @@ def on_package(self, pkg, raw: Dict[str, Any]) -> None: level = self._openkal_level(pkg.identity.slug) if level: - pkg.facets["openkal"] = level ext["openkal"] = {"level": level, **(self.openkal_by_package.get(pkg.identity.slug) or {})} - if level in ("runs", "builds", "family"): - label = {k: lbl for k, lbl, _ in OPENKAL_LEVELS}[level] + facet = _OPENKAL_FACET.get(level) + if facet: + pkg.facets["openkal"] = facet + label = {k: lbl for k, lbl, _ in OPENKAL_LEVELS}[facet] pkg.extensions.setdefault("_badges", []).append(label) # Wired the same way as `level` just above: a measurement, not a diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c885b4..aa536b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,35 @@ ## [Unreleased] +### Added + +- **`compat.yaml-cpp` 0.8.0 与 0.9.0。** YAML 1.2 解析与生成,按上游 CMake 目标原样编译 + (`src/*.cpp` + `src/contrib/*.cpp`),GLOBAL + GitCode CN 镜像字节一致。上游把 + `YAML_CPP_STATIC_DEFINE` 作为静态构建的 PUBLIC 定义;描述符的 `defines` 到不了消费者, + 所以用 `yaml-cpp/dll.h` 的同名遮蔽头送达,测试在编译期断言它到了(MSVC ABI 上缺它就是 + dllimport 链接错误,Linux 上看不出来)。两个版本各有一个测试成员(`yaml-cpp`、 + `yaml-cpp-v080`),都列入 `tests/openkal/members.toml` 在 openkal 上测量。 + +### Changed + +- **站点的 openkal 分面改为两个取值:`openkal-ecosystem` 与 `openkal-compat`。** 原先的 + 「openkal itself / runs on openkal / fails on openkal」是句子而不是名字。构成 openkal 的 + 包归入 `openkal-ecosystem`;测试项目在 openkal 依赖图中测得 `runs` 的包归入 + `openkal-compat`。只测得 `builds` 或 `fails` 的包不再归入分面,其页面仍按目标列出测量结果 + 与第一条诊断。 +- **openkal 测量按改动选成员,不再每次全量。** `compat.py select --base `: + 改动 openkal 家族描述符、`pins.toml`、`compat.py` 才测全部成员;改 `members.toml` 只测 + 新增或改动的条目(`[not-portable]` 计入所指成员,`[excluded]` 不选);改 + `tests/examples/<成员>/` 测该成员;改描述符测依赖它的成员。`selftest` 为每条规则各加一例。 +- **openkal 的 Windows 目标改名为 `x86_64-windows-musl`。** 依赖图在 Windows 上呈现的是 + musl 的 C 环境,不是 MinGW;`x86_64-windows-gnu` 会被读成 mingw-w64 构建。实测同一依赖图: + cli11、zlib、cmp-module 仍 runs,curl 仍停在同一条 `curl_setup.h:591`。2026-09-23 之前的 + 结果仍记在旧名下。 +- **CN 镜像可以由库的维护者自己托管。** `check_mirror_urls.lua` 要求 `CN` 是 gitcode 的 + release 资产(`https://gitcode.com///releases/download/…`),不再限定 + `mcpp-res` 组织;`mcpp-res` 仍是默认。#463 起 `main` 上的 lint 因 `ZheFeng7110.boost` + 的维护者镜像而失败,现在通过。 + ### Fixed - **索引制品按提交只定一次字节,两个托管端提供同一份。** 同一提交重跑发布(夜间 cron) diff --git a/README.md b/README.md index 5d27fed7..0459ca25 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,14 @@ **English** | [简体中文](README.zh-CN.md) -> The default package index repository for the [`mcpp`](https://github.com/mcpp-community/mcpp) build tool. +> The default package index for the [`mcpp`](https://github.com/mcpp-community/mcpp) build tool. > Browse every package online: **https://mcpplibs.github.io/mcpp-index/** -This repository hosts the C++23 packages that `mcpp` can `add` directly — both modular libraries that are ready to -`import`, and third-party C/C++ libraries built from upstream sources or headers in `compat` form. Every package maps -to one `pkgs//.lua` descriptor file. - -> **Engine floor (mcpp 2026.9.18.3):** `openkal-musl 0.15.0` and any future -> `[c-abi]` package require the engine that ships with `mcpp 2026.9.18.3` -> or later — the host-macro strip on Windows hosts and the freestanding -> wchar realisation are what let the c-abi probe verify a declaration -> without host contamination. The `min_mcpp` floor on `[indices]`, -> `MCPP_VERSION`, and the per-package pins under `tests/openkal/pins.toml` -> all gate this together (raising only the index floor would still let -> 30 members E0006 before reading any source line). Older engines -> silently misbuild `[c-abi]` packages: the build succeeds, the probe -> fails, the program compiles against the wrong environment. Upgrade: -> `xlings install mcpp --force`. +The C++23 packages `mcpp` can `add` directly: modular libraries ready to `import`, and third-party C/C++ libraries +built from upstream sources in `compat` form. Each package is one `pkgs//.lua` descriptor. + +> **Requires mcpp 2026.9.18.3 or later** (`min_mcpp` in [`index.toml`](index.toml)). Older engines silently misbuild +> `[c-abi]` packages such as `openkal-musl`. Upgrade: `xlings install mcpp --force`. ## Usage @@ -31,44 +21,36 @@ mcpp search # search and refresh the index mcpp self config --mirror CN # switch to the CN mirror; GLOBAL upstream is the default ``` -For the full package list, see the **[online index site](https://mcpplibs.github.io/mcpp-index/)**. - -## Package ecosystem and contributing +## Package kinds -Two kinds of packages live here: - -- **Native mcpp module libraries**: shipped as C++23 modules and ready to `import` — `mcpplibs.*`, `nlohmann.json`, - `imgui`, `ffmpeg`, `opencv`, plus libraries developed on top of mcpp by users and registered into the index (such as - `tensorvia-cpu` and `huxerui.huxerui`). Their upstream usually carries its own `mcpp.toml`, so the descriptor (Form A) only declares - metadata and a download address. -- **Third-party C/C++ libraries (`compat`)**: upstream offers no mcpp support, so the descriptor (Form B) inlines the - build information. These come in several shapes — header-only, plain C sources, C++23 module wrapper — with optional - components gated behind `features` and a GitCode CN mirror configured. +- **Native mcpp module libraries** (Form A): upstream carries its own `mcpp.toml`; the descriptor only declares + metadata and a download address. `mcpplibs.*`, `nlohmann.json`, `imgui`, `opencv`, `tensorvia-cpu`, … +- **Third-party C/C++ libraries** (`compat`, Form B): upstream has no mcpp support, so the descriptor inlines the build. + Header-only, C/C++ sources, or a C++23 module wrapper; optional parts sit behind `features`; a GitCode CN mirror + serves the same bytes. ### Reference examples -A few descriptors worth opening first, one per common shape: - -| Shape | Example | What it shows | -|------|------|------| -| Native module library (Form A) | [`mcpplibs.cmp`](pkgs/c/cmp.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`gzj-creator.galay`](pkgs/g/gzj-creator.galay.lua) | Upstream carries its own `mcpp.toml`; CMP demonstrates a coroutine runtime, while Galay demonstrates a multi-module package with feature-scoped protocol layers | -| C-source compat | [`compat.cjson`](pkgs/c/compat.cjson.lua) | One `.c` compiled into a lib; the optional extension sits behind a `features` gate | -| Header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) | Nothing to compile — `include_dirs` and an anchor TU | -| Whole-source build + generated config | [`compat.c-ares`](pkgs/c/compat.c-ares.lua) | The config header configure would have produced is snapshotted into `generated_files` | -| Multi-component upstream flattened into one lib | [`compat.recastnavigation`](pkgs/c/compat.recastnavigation.lua) | Recast Navigation 1.6.0 — upstream is five inter-dependent CMake libraries; here the two every consumer uses are the base and the other three are `features`, all compiled into one lib. Their dependency edges have to be rebuilt by hand, which is why `debug-utils` carries `implies = { "tilecache" }`: upstream links DetourTileCache unconditionally, and without the implication a consumer asking only for debug drawing fails at link with missing `dtTileCache*` symbols. Upstream's install puts every header flat under `include/recastnavigation/` **and** keeps both that directory and its parent on the interface include path, so both `` and `` are legal against a real install — 26 generated forwarding headers restore the second spelling for a source-tree build. `RECASTNAVIGATION_DT_POLYREF64` and `RECASTNAVIGATION_DT_VIRTUAL_QUERYFILTER` are deliberately NOT features: they change the ABI of types crossing the library boundary, and a feature's `defines` reach only the package's own TUs | -| C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) | A generated `.cppm` turns a header-only library into `import` | -| C++23 module, upstream's own | [`khronos.vulkan-hpp`](pkgs/k/khronos.vulkan-hpp.lua) | Khronos ships `vulkan.cppm`, so the descriptor just names it — `import vulkan;` with nothing authored here | -| External build system | [`compat.openssl`](pkgs/c/compat.openssl.lua) | An `install()` hook drives upstream's own Perl Configure + Make | -| Form A whose consumer deps must be written by hand | [`huxerui.huxerui`](pkgs/h/huxerui.huxerui.lua) | HuxerUI declares its GTK4 stack on the TARGET axis, which is the form mcpp recommends and which a descriptor structurally cannot carry — three platform blocks, and a cfg selector is not a platform. `mcpp emit xpkg` says so and emits empty `deps`, so the 36-entry closure is transcribed into `xpm.linux.deps` at PLATFORM level (a per-version `deps` is inert). Its `licenses`/`repo` also deliberately disagree with what emit produces | +One descriptor per common shape: -The full catalog — every shape this index has needed, and the reasoning behind each descriptor including what it -deliberately leaves out — is in **[Descriptor examples by shape](docs/descriptor-examples.md)**. +| Shape | Example | +|------|------| +| Native module library (Form A) | [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`gzj-creator.galay`](pkgs/g/gzj-creator.galay.lua) | +| C sources + `features` | [`compat.cjson`](pkgs/c/compat.cjson.lua) | +| C++ sources, several versions | [`compat.yaml-cpp`](pkgs/c/compat.yaml-cpp.lua) | +| Header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) | +| Generated config header | [`compat.c-ares`](pkgs/c/compat.c-ares.lua) | +| C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) | +| C++23 module shipped by upstream | [`khronos.vulkan-hpp`](pkgs/k/khronos.vulkan-hpp.lua) | +| External build system (`install()`) | [`compat.openssl`](pkgs/c/compat.openssl.lua) | + +Every other shape, and why each descriptor is written the way it is, is in +**[Descriptor examples by shape](docs/descriptor-examples.md)**. ### Adding a package -The full procedure is defined in the agent skill -[`add-mcpp-index-package`](.agents/skills/add-mcpp-index-package/SKILL.md). Hand the instruction below to an agent -(Claude Code, for example) and it will invoke that skill to write the descriptor and carry out the whole flow: +The procedure is the agent skill [`add-mcpp-index-package`](.agents/skills/add-mcpp-index-package/SKILL.md). Hand an +agent (Claude Code, for example) this instruction: ```text Following this repo's skill `.agents/skills/add-mcpp-index-package`, add @ to @@ -78,27 +60,18 @@ member; verify locally with the same mcpp version CI pins by running `mcpp test the online index; open a PR and confirm CI is green. ``` -Detailed documentation lives in [`docs/`](docs/), written for humans and agents alike: - -- [Library shapes and descriptor templates](docs/package-types.md): descriptor templates and samples for each shape, - plus how to write the minimal project. -- [Descriptor examples by shape](docs/descriptor-examples.md): the full catalog of what is already in the index, and - why each descriptor is written the way it is. -- [The CN mirror loop](docs/cn-mirror.md): `gtc` and gitcode operations, plus the fallback when you have no - `mcpp-res` access. -- [openkal compatibility](docs/openkal-compat.md): what the `openkal` label on the site means, how it is measured, and - how a package is adapted to an openkal graph. -- [Repository layout, schema and CI](docs/repository-and-schema.md): field cheat-sheet, selective-run mechanics and - local lint. -- The **authoritative** judge of a field is `mcpp xpkg parse` (exactly what CI runs: an unknown mcpp-segment field - fails outright instead of being silently ignored); for semantics and constraints see - [`docs/spec/`](https://github.com/mcpp-community/mcpp/tree/main/docs/spec) in the mcpp repository. - -> Once a PR is open, `validate` runs lint automatically and selects the workspace members affected by the changed -> library (the whole test surface is one mcpp workspace, and the public module packages -> `imgui`/`ffmpeg`/`opencv`/`tinyhttps` are ordinary members too — the `compat` redirect is declared at the workspace -> root and inherited by members, while members that consume another namespace override it themselves, with zero shell -> driving). After the merge, `deploy-site` publishes it to the online browser. +A PR runs lint and tests only the workspace members that depend on the changed descriptors; after the merge, +`deploy-site` publishes the site. + +## Documentation + +- [Library shapes and descriptor templates](docs/package-types.md) +- [Descriptor examples by shape](docs/descriptor-examples.md) +- [The CN mirror loop](docs/cn-mirror.md) +- [openkal compatibility](docs/openkal-compat.md): the `openkal-ecosystem` / `openkal-compat` labels on the site +- [Repository layout, schema and CI](docs/repository-and-schema.md) +- The authoritative check of a field is `mcpp xpkg parse`, which CI runs; semantics are in mcpp's + [`docs/spec/`](https://github.com/mcpp-community/mcpp/tree/main/docs/spec). ## Related links diff --git a/README.zh-CN.md b/README.zh-CN.md index f73531e7..a996bda4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,56 +2,53 @@ [English](README.md) | **简体中文** -> [`mcpp`](https://github.com/mcpp-community/mcpp) 构建工具的默认包索引仓库。 +> [`mcpp`](https://github.com/mcpp-community/mcpp) 构建工具的默认包索引。 > 在线浏览所有包:**https://mcpplibs.github.io/mcpp-index/** -本仓收录可被 `mcpp` 直接 `add` 的 C++23 包,既包含 `import` 即用的模块化库,也包含以 `compat` 形态从上游源码或 -头文件构建的第三方 C/C++ 库。每个包对应一个 `pkgs/<首字母>/<包名>.lua` 描述文件。 +收录 `mcpp` 可直接 `add` 的 C++23 包:`import` 即用的模块化库,以及以 `compat` 形态从上游源码构建的第三方 C/C++ 库。 +每个包对应一个 `pkgs/<首字母>/<包名>.lua` 描述文件。 + +> **需要 mcpp 2026.9.18.3 或更高版本**([`index.toml`](index.toml) 的 `min_mcpp`)。更旧的引擎会静默错误构建 +> `openkal-musl` 等 `[c-abi]` 包。升级:`xlings install mcpp --force`。 ## 使用 ```bash -mcpp add ftxui@6.1.9 # 添加依赖到 mcpp.toml +mcpp add ftxui@6.1.9 # 添加依赖到 mcpp.toml mcpp build # 自动拉取源码并构建,依赖沿链路自动传递 mcpp search # 搜索并刷新索引 mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上游源 ``` -完整包列表见 **[在线索引站](https://mcpplibs.github.io/mcpp-index/)**。 - -## 包生态与贡献 +## 包的种类 -本仓收录两类包: - -- **原生 mcpp 模块库**:以 C++23 模块发布、`import` 即用,包括 `mcpplibs.*`、`nlohmann.json`、`imgui`、`ffmpeg`、`opencv`,以及由 - 用户基于 mcpp 开发并登记进索引的库(如 `tensorvia-cpu`、`huxerui.huxerui`)。其上游通常自带 `mcpp.toml`,描述文件(Form A)只声明 - 元数据与下载地址。 -- **第三方 C/C++ 库(`compat`)**:其上游不提供 mcpp 支持,描述文件(Form B)内联构建信息。该类库存在 - header-only、纯 C 源码、C++23 module wrapper 等形态,可选组件经 `features` 门控,并配备 GitCode CN 镜像。 +- **原生 mcpp 模块库**(Form A):上游自带 `mcpp.toml`,描述文件只声明元数据与下载地址。如 `mcpplibs.*`、 + `nlohmann.json`、`imgui`、`opencv`、`tensorvia-cpu` 等。 +- **第三方 C/C++ 库**(`compat`,Form B):上游不支持 mcpp,描述文件内联构建信息。形态有 header-only、C/C++ 源码、 + C++23 module wrapper;可选组件经 `features` 门控;GitCode CN 镜像提供相同字节。 ### 参考示例 -按常见形态各挑一个,建议先看这几个描述符: +每种常见形态一个描述符: -| 形态 | 示例 | 看点 | -|------|------|------| -| 原生模块库(Form A) | [`mcpplibs.cmp`](pkgs/c/cmp.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`gzj-creator.galay`](pkgs/g/gzj-creator.galay.lua) | 上游自带 `mcpp.toml`;CMP 展示协程运行时,Galay 展示多模块包与按 feature 开启协议层 | -| C 源码 compat | [`compat.cjson`](pkgs/c/compat.cjson.lua) | 单个 `.c` 编成库;可选扩展由 `features` 门控 | -| header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) | 没有可编译内容 —— `include_dirs` 加一个 anchor TU | -| 全源码直编 + 生成 config | [`compat.c-ares`](pkgs/c/compat.c-ares.lua) | 把 configure 本该生成的 config 头快照进 `generated_files` | -| 多组件上游拍平进单个库 | [`compat.recastnavigation`](pkgs/c/compat.recastnavigation.lua) | Recast Navigation 1.6.0 —— 上游是五个互相依赖的 CMake target;这里把「人人都要用」的两个作为核心,其余三个做 `features`,全部编进同一个 lib。依赖边必须手工重建,这正是 `debug-utils` 带 `implies = { "tilecache" }` 的原因:上游无条件链接 DetourTileCache,没有这条 implication,只开调试绘制的消费者会在**链接期**撞上缺失的 `dtTileCache*` 符号。上游安装把头文件拍平进 `include/recastnavigation/`,同时把该目录**及其父目录**都放进 interface include path,于是对着真实安装 `#include ` 与 `#include ` 都合法 —— 26 个生成的转发头把第二种拼写还给源码树构建。`RECASTNAVIGATION_DT_POLYREF64` 与 `RECASTNAVIGATION_DT_VIRTUAL_QUERYFILTER` 刻意**不**做成 feature:它们改变跨库边界类型的 ABI,而 feature 的 `defines` 只作用于本包自己的 TU | -| C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) | 一份生成的 `.cppm` 把 header-only 库变成 `import` 即用 | -| C++23 module,上游自带 | [`khronos.vulkan-hpp`](pkgs/k/khronos.vulkan-hpp.lua) | Khronos 自己发 `vulkan.cppm`,描述符点名即可 —— `import vulkan;`,本仓不写一行包装体 | -| 外部构建系统 | [`compat.openssl`](pkgs/c/compat.openssl.lua) | `install()` 钩子驱动上游自己的 Perl Configure + Make | +| 形态 | 示例 | +|------|------| +| 原生模块库(Form A) | [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`gzj-creator.galay`](pkgs/g/gzj-creator.galay.lua) | +| C 源码 + `features` | [`compat.cjson`](pkgs/c/compat.cjson.lua) | +| C++ 源码,多版本 | [`compat.yaml-cpp`](pkgs/c/compat.yaml-cpp.lua) | +| header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) | +| 生成 config 头 | [`compat.c-ares`](pkgs/c/compat.c-ares.lua) | +| C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) | +| 上游自带 C++23 module | [`khronos.vulkan-hpp`](pkgs/k/khronos.vulkan-hpp.lua) | +| 外部构建系统(`install()`) | [`compat.openssl`](pkgs/c/compat.openssl.lua) | -完整目录 —— 本索引遇到过的全部形态,以及每个描述符背后的取舍(包括它刻意不做什么)—— -见 **[描述符示例总览(按形态)](docs/zh/descriptor-examples.md)**。 +其余形态,以及每个描述符为何这样写,见 **[描述符示例总览(按形态)](docs/zh/descriptor-examples.md)**。 ### 新增一个包 -完整流程定义于 agent skill [`add-mcpp-index-package`](.agents/skills/add-mcpp-index-package/SKILL.md)。可将下列 -指令提供给 agent(如 Claude Code),由其调用该 skill 完成描述文件的编写与全流程: +流程定义于 agent skill [`add-mcpp-index-package`](.agents/skills/add-mcpp-index-package/SKILL.md)。把下面的指令交给 +agent(如 Claude Code)即可: ```text 参考本仓 skill `.agents/skills/add-mcpp-index-package`,将 <库名 / 仓库URL> @<版本> 收录进 mcpp-index: @@ -60,20 +57,17 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 `mcpp test -p <成员>` 进行验证;更新 README 与在线索引;提交 PR 并确认 CI 通过。 ``` -细节文档位于 [`docs/zh/`](docs/zh/),供人工与 agent 共同使用(英文版位于 [`docs/`](docs/)): +提交 PR 后,CI 执行 lint,并只测试依赖了被修改描述符的 workspace 成员;合并后,`deploy-site` 发布站点。 -- [库形态与描述符模板](docs/zh/package-types.md):各类形态的描述符模板与样例,以及最小工程的写法。 -- [描述符示例总览(按形态)](docs/zh/descriptor-examples.md):索引里已有内容的完整目录,以及每个描述符为何这样写。 -- [CN 镜像闭环](docs/zh/cn-mirror.md):`gtc` 与 gitcode 操作,以及无 `mcpp-res` 权限时的回退方案。 -- [openkal 兼容性](docs/zh/openkal-compat.md):站点上 `openkal` 标签的含义、测量方式,以及包如何适配 openkal 依赖图。 -- [仓库结构与 schema 与 CI](docs/zh/repository-and-schema.md):字段速查、选跑机制与本地 lint。 -- 字段的**权威判定**是 `mcpp xpkg parse`(CI 用的就是它:未知的 mcpp 段字段直接失败,而不是被静默忽略); - 语义与约束见 mcpp 仓的 [`docs/spec/`](https://github.com/mcpp-community/mcpp/tree/main/docs/spec)。 +## 文档 -> 提交 PR 后,`validate` 自动执行 lint 并按改动库选跑对应 workspace 成员(整个测试面是一个 mcpp -> workspace,公开模块包 `imgui`/`ffmpeg`/`opencv`/`tinyhttps` 也是普通成员——`compat` 的重定向声明在 -> workspace 根并由成员继承,消费其他命名空间的成员各自覆盖,零 shell 驱动);合并后,`deploy-site` -> 将其发布至在线浏览站。 +- [库形态与描述符模板](docs/zh/package-types.md) +- [描述符示例总览(按形态)](docs/zh/descriptor-examples.md) +- [CN 镜像闭环](docs/zh/cn-mirror.md) +- [openkal 兼容性](docs/zh/openkal-compat.md):站点上的 `openkal-ecosystem` / `openkal-compat` 标签 +- [仓库结构、schema 与 CI](docs/zh/repository-and-schema.md) +- 字段的权威判定是 `mcpp xpkg parse`(CI 用的就是它);语义见 mcpp 仓的 + [`docs/spec/`](https://github.com/mcpp-community/mcpp/tree/main/docs/spec)。 ## 相关链接 diff --git a/docs/cn-mirror.md b/docs/cn-mirror.md index a4c14a7d..13c28d9c 100644 --- a/docs/cn-mirror.md +++ b/docs/cn-mirror.md @@ -14,12 +14,16 @@ effect through the existing engine. for the `nlohmann` family prefer `nlohmann-json`, to avoid the ambiguity of a bare `json`). - The public URL of a CN asset follows this convention: `https://gitcode.com/mcpp-res//releases/download//-.` +- A library's own maintainer may host its CN mirror instead, under their own gitcode account + (`https://gitcode.com///releases/download//`, as `ZheFeng7110.boost` does). They then keep it + in step with the GLOBAL asset: the same bytes, so the same `sha256`. ## Fallback without `mcpp-res` write access Setting up a mirror requires write access (a token) to the gitcode `mcpp-res` organization. Without it, do not force a mirror table into existence: lint (`check_mirror_urls.lua`) mandates that once `url` is written as a table, its `CN` -must be `https://gitcode.com/mcpp-res/…`, so `{ GLOBAL=upstream, CN=upstream }` fails lint outright. The correct +must be a gitcode release asset (`https://gitcode.com///releases/download/…`), so +`{ GLOBAL=upstream, CN=upstream }` fails lint outright. The correct fallback is a plain-string url (upstream release only) — lint imposes no mirror constraint on plain-string urls: ```lua diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 0c42bac5..fc706d9f 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -13,11 +13,13 @@ in the [root README](../README.md#reference-examples). | Shape | Examples | |------|------| | 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.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) | +| Form A whose consumer deps must be written by hand | [`huxerui.huxerui`](../pkgs/h/huxerui.huxerui.lua) (HuxerUI declares its GTK4 stack on the TARGET axis, which is the form mcpp recommends and which a descriptor structurally cannot carry — three platform blocks, and a cfg selector is not a platform. `mcpp emit xpkg` says so and emits empty `deps`, so the 36-entry closure is transcribed into `xpm.linux.deps` at PLATFORM level (a per-version `deps` is inert). Its `licenses`/`repo` also deliberately disagree with what emit produces) | | Native multi-module library with feature-scoped sources | [`gzj-creator.galay`](../pkgs/g/gzj-creator.galay.lua) (Galay 5.0.2 — the upstream Form-A manifest exposes `galay.utils` and `galay.kernel` by default, while SSL, HTTP, database, RPC, MCP, and tracing modules stay behind named features and their corresponding dependencies. The index keeps the upstream manifest intact and tests the default module surface on Unix.) | | C-source compat (with `features`) | [`compat.cjson`](../pkgs/c/compat.cjson.lua) · [`compat.zlib`](../pkgs/c/compat.zlib.lua) · [`compat.hiredis`](../pkgs/c/compat.hiredis.lua) (the classic 1.2.0 — a 7-TU C build whose flat tarball headers get `hiredis/`-prefixed wrapper headers via `generated_files`, so consumers write `#include ` exactly like upstream's install layout) · [`compat.sqlite3`](../pkgs/c/compat.sqlite3.lua) (plain C-source, no features: the single `sqlite3.c` amalgamation; 3.45.3, the final maintenance release of the most widely deployed 3.45.x line) · [`compat.libuv`](../pkgs/c/compat.libuv.lua) (libuv 1.48.0 — the per-OS source sets transcribed from upstream's CMakeLists, because a `src/unix/*.c` glob would compile every OS's backend at once; linux/macos get explicit unix subsets, windows globs `src/win/*.c`) | · [`compat.xxhash`](../pkgs/c/compat.xxhash.lua) (one TU, one header, no features at all — the interesting decision is what is NOT compiled: `xxh_x86dispatch.c` selects an AVX2/AVX512 path at RUNTIME and needs per-file `-mavx2` plus `XXH_X86DISPATCH` at every call site, so the package ships the flagless SSE2 baseline instead. Nor is the header-only `XXH_INLINE_ALL` mode chosen: it re-emits the implementation in every TU that hashes anything, which is the right trade only when there is exactly one such TU — something a package cannot know) | C-source compat whose sources are chosen by ARCHITECTURE | [`compat.wamr`](../pkgs/c/compat.wamr.lua) (WAMR 2.4.5 — the descriptor schema varies `sources` and `cflags` per OS but has no per-architecture hook, and `archs` is package metadata rather than a selector. WAMR needs both: one of `BUILD_TARGET_X86_64`/`BUILD_TARGET_AARCH64` must be defined or `wasm_runtime_common.c` compiles its whole invoke-native section away and the link fails on `invokeNative`, and the implementation of that symbol is a hand-written assembly file per architecture. Both halves are handed to the preprocessor instead: a `generated_files` config header maps `__x86_64__`/`__aarch64__` to the matching `BUILD_TARGET_*` and reaches every TU through `-include`, and a generated `.S` — `.S`, because upstream's `arch/invokeNative_*.s` are lowercase and clang assembles those WITHOUT the preprocessor, so unlike libffi's `.S` files they cannot guard themselves — `#include`s the chosen one as text. Two further notes worth copying: `-std=gnu11` arrives through `cflags` because `c_standard = "c11"` defines `__STRICT_ANSI__`, under which the bare `asm` in `platform_internal.h` is not a keyword; and the POSIX files upstream drops when WASI is off are carried unconditionally rather than `!`-excluded and re-added by the `libc-wasi` feature, because an exclusion glob is global and wins over the feature's own entry for the same file) | | C-source compat where the library IS a kernel ABI | [`compat.libaio`](../pkgs/c/compat.libaio.lua) (libaio 0.3.113 — twelve syscall-wrapper TUs, and the only `xpm` section is `linux`, because there is no port to declare: `struct iocb` is the kernel's and every TU is `syscall(__NR_io_*, …)`. Consumers gate it with `[target.'cfg(linux)'.dependencies]`, the mirror image of compat.wil. Three things it teaches. **One public header out of a source dir**: upstream installs exactly one, `libaio.h`, but the tarball keeps it in `src/` beside the private headers — one of which is named `syscall.h` and would SHADOW glibc's for every consumer TU — so `include_dirs` names a `generated_files` forwarder and nothing else; the package's own sources reach the real header through it while their quote-form `#include "syscall.h"` still resolves next to the including `.c`, so no `-I` into `src/` is needed at all. **A `c_standard` that is a trap**: `-std=c11` sets `__STRICT_ANSI__`, which hides `syscall()` and `sigset_t`, and the public header then fails to parse at `io_pgetevents`; declaring `c_standard = "gnu11"` LOOKS like the fix but mcpp 2026.8.27.2 accepts the string and still emits `-std=c11` (visible in the emitted `compile_commands.json`), so `-D_GNU_SOURCE` in `cflags` is the spelling that takes effect. **Symbol versioning in a static package**: `io_getevents` and `io_cancel` have no ordinary definitions upstream — the functions are `io_getevents_0_4` etc. publishing short names through `.symver … @@LIBAIO_0.4` — which resolves for an executable under both ld.bfd and lld, but not when a consumer builds a `.so` straight out of these objects; that needs upstream's `src/libaio.map`, exactly as upstream's own `libaio.a` does) | | 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`) · [`compat.redis-plus-plus`](../pkgs/c/compat.redis-plus-plus.lua) (redis++ 1.3.13 — the sync client, 17 TUs + `patterns/redlock.cpp`, depends on `compat.hiredis`; the one header CMake would generate, `hiredis_features.h`, is snapshotted via `generated_files`, and the async/TLS TUs are left out so the base build stays a two-package pair. An `async` feature adds the libuv-backed `AsyncRedis` interface (the 9 async TUs + `compat.libuv`; `event_loop.cpp` runs `uv_run` on a background thread, and `` arrives through compat.hiredis' wrapper headers). Two versions, one on each side of the source-structure watershed, share this ONE source list: 1.3.13 (modern 17-TU layout) and 1.3.3 (pre-`redis_uri.cpp`/`redlock` 15-TU layout) — the union works because 1.3.3's TUs are a strict subset, so exactly two globs match nothing there (a warning, not an error; same trick as compat.catch2)) | · [`compat.sqlitecpp`](../pkgs/c/compat.sqlitecpp.lua) (the RAII C++ wrapper over SQLite. Upstream vendors sqlite3 as a GIT SUBMODULE, so a source tarball simply does not contain it and the library cannot link — the dependency edge on `compat.sqlite3` replaces the submodule, and does it better: two consumers of SQLite in one link now share ONE amalgamation instead of each embedding a private copy with its own compile-time options. Its two CMake knobs are deliberately not set — `SQLITECPP_USE_ASSERT_ON_ERRORS` changes the error model from throwing to aborting, and `SQLITE_ENABLE_COLUMN_METADATA` has to agree with how SQLite ITSELF was built; both are the consumer's call, and the headers already guard them with `#ifdef`) +| Multi-component upstream flattened into one lib | [`compat.recastnavigation`](../pkgs/c/compat.recastnavigation.lua) (Recast Navigation 1.6.0 — upstream is five inter-dependent CMake libraries; the two every consumer uses are the base and the other three are `features`, all compiled into one lib. Their dependency edges have to be rebuilt by hand, which is why `debug-utils` carries `implies = { "tilecache" }`: upstream links DetourTileCache unconditionally, and without the implication a consumer asking only for debug drawing fails at link with missing `dtTileCache*` symbols. Upstream's install puts every header flat under `include/recastnavigation/` **and** keeps both that directory and its parent on the interface include path, so both `` and `` are legal against a real install — 26 generated forwarding headers restore the second spelling for a source-tree build. `RECASTNAVIGATION_DT_POLYREF64` and `RECASTNAVIGATION_DT_VIRTUAL_QUERYFILTER` are deliberately NOT features: they change the ABI of types crossing the library boundary, and a feature's `defines` reach only the package's own TUs) | | C transport + the header-only C++ server on top of it | [`compat.usockets`](../pkgs/c/compat.usockets.lua) · [`compat.uwebsockets`](../pkgs/c/compat.uwebsockets.lua) (uSockets picks ONE event loop for all three platforms — libuv, via `compat.libuv` — because the alternative makes `us_loop_t` a different struct per platform for no gain; SSL and QUIC are left out so the base package's only dependency is that loop. The pair's real lesson is that `LIBUS_USE_LIBUV` / `LIBUS_NO_SSL` / `UWS_NO_ZLIB` are INTERFACE facts: `libusockets.h` changes the layout of `us_loop_t` under the first and gates its SSL declarations on the second, and uWS is header-only so its templates are instantiated in the CONSUMER's translation unit. An index descriptor's `cflags` reach only the package's own TUs, so every consumer must declare all three — a mismatch does not fail to build, it corrupts. The usockets test therefore writes to loop-attached extension memory from a timer callback and reads it back, which is exactly the assertion a layout disagreement breaks) | | 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) | | Database client + the driver manager it needs, built from source | [`compat.nanodbc`](../pkgs/c/compat.nanodbc.lua) (nanodbc 2.14.0, frozen upstream — one TU over the platform ODBC driver manager. Two fixes make the four-year-old source compile and RUN here: a force-included `char_traits` shim for libc++ (the standard's own customization point, guarded on `_LIBCPP_VERSION` so libstdc++/MSVC are untouched; and note `-include` reaches C++ TUs only through `cxxflags`, never `cflags`), and a per-platform answer to the manager itself — windows links the SDK's odbc32, macOS the OS's iODBC, while linux takes `compat.unixodbc` because mcpp's runtime closure rejects a NEEDED `libodbc.so.2` that only the host has. The test asserts the manager's own diagnostics surface through the wrapper — including nanodbc's frozen off-by-one that drops the last SQL-state character) · [`compat.unixodbc`](../pkgs/c/compat.unixodbc.lua) (unixODBC 2.3.14, Shape E over A — DM + odbcinst + ini/log/lst + libltdl compiled statically into one `odbc` target, exactly upstream's libodbc.a symbol set, so the consumer carries no `libodbc.so.2` NEEDED at all. The one non-obvious piece is libtool-free ltdl wiring: `-DLTDLOPEN=libltdlc` plus a generated `lt_libltdlc_LTX_preloaded_symbols` table (reconstructed from the libtool object's relocations) registers the dlopen loader. The frozen `config.h` merges ltdl's own configure output into the top-level one — ltdl sources never read the clashing identification macros, and the merge sidesteps a quoted `-DLT_CONFIG_H` that does not survive the pipeline. Verified against the libtool build of the same tarball: identical IM002 error path and identical `lt_dlopen` behaviour) | @@ -37,6 +39,7 @@ in the [root README](../README.md#reference-examples). | Source build that needs a real project (Form A fork) | [`freedesktop.wayland`](../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../pkgs/f/freedesktop.wayland-util.lua) (wayland 1.26.0, four entries out of one tarball — [mcpplibs/wayland](https://github.com/mcpplibs/wayland), a fork that patches no upstream file. **Why a fork and not an inline descriptor**: wayland is mostly GENERATED — `protocol/wayland.xml` describes every interface and wayland-scanner emits ~13,000 lines from it — and the generator is a C program in the same tree, so it must be COMPILED before it can run. An inline descriptor has no build step, and `install()` cannot do it either: mcpp compiles a package's sources at CONSUMER-BUILD time, so no package binary exists while another package installs. `build.mcpp` is the mechanism, and it only exists for a real mcpp project. **Why four packages**: `libwayland-client.so.0` and `libwayland-server.so.0` are distinct SONAMEs that Mesa's libEGL_mesa needs BOTH of, and mcpp links every library target against ALL of a package's sources — measured: a feature-gated second target still receives the feature's objects — so one package cannot emit two libraries with disjoint contents. mcpp's own diagnostic names the remedy: "split into a workspace member". **The module layer ships with the library, not beside it**: each package carries its C library AND its `.cppm`, so there is one package per library rather than a C one and a module one. The wrappers add no API — every exported name is upstream's, and the lists are generated from the public headers so a version bump cannot silently drop one. **A module is named for the organization that owns the INTERFACE**, which is why these are `freedesktop.wayland.{client,server,util}`: freedesktop owns the wayland protocol. The index's older convention was to drop the namespace entirely (`chriskohlhoff.asio` → `import asio`, `compat.opencv` → `import opencv.cv`); this is that rule made specific, because a module name is global and permanent in a way a package name is not. `mcpplibs.openkal` paid for the lesson — its 0.1.0 was withdrawn rather than kept because it "placed the module a consumer imports under the control of the implementation, which contradicts what the specification is for". **Macros are the one thing that cannot cross**: `export` names entities and a macro is not one, so `wayland-util` maps each to what it actually is — `WL_MARSHAL_FLAG_DESTROY` to a constexpr, `wl_container_of` to a function template, the six `*_for_each` loops to ranges — and its test instantiates all of them including the `_safe` removal guarantee) · [`freedesktop.egl`](../pkgs/f/freedesktop.egl.lua) (libglvnd 1.7.0 — [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd), and the entry that replaced the `compat.egl` binding on the row above. **The generated code is checked in rather than generated**: upstream drives ~1000 lines of Python over a 2.7 MB `gl.xml` to emit the dispatch tables, and that output is deterministic from a pinned registry — so it lives in `mcpp/generated/` and NO generator runs at build time, with CI regenerating and diffing it instead. `mcpp build` is the whole toolchain. **The per-architecture choice lives in `build.mcpp`, not in the manifest**: GLdispatch's entry stubs are per-arch *and* per-thread-storage-model, and unlike libffi's they carry no guards of their own, so hard-coding x86_64 in `sources` would make the package x86_64-only and say so nowhere; `build.mcpp` makes upstream's own `gl_dispatch_type` choice from `mcpp::target_arch()`. **The module is `khronos.egl`, not `freedesktop.egl`**, and the package/module mismatch is the point: freedesktop ships this code, but EGL is a *Khronos* specification with several implementations. Naming the module after the implementation would force every consumer to edit its imports the day a different libEGL is used — defeating a wrapper whose whole promise is that it changes nothing but the include line. It also makes two EGL providers in one build a hard conflict rather than silent coexistence, which is what GLVND wants anyway. **One index entry, two libraries**: `libGLdispatch.so.0` is a sibling workspace member reached by a PATH dependency — being the one dispatch point in a process is GLVND's whole purpose, and a second index entry would let a consumer name both and resolve two instances, with soname reuse mapping one and silently discarding the other. **The compiled-in vendor path is deliberately EMPTY**: upstream bakes in `/share/glvnd/egl_vendor.d`, which after relocation means the HOST's directory, so an empty fallback makes a missing ecosystem declaration surface as "no vendor found" rather than as silently loading the host's driver — the same stance compat.libgbm takes with `GBM_BACKENDS_PATH`) | | Source-built library sharing a soname with the payload | [`compat.libdrm`](../pkgs/c/compat.libdrm.lua) (libdrm 2.4.134 — five TUs plus an inlined `generated_static_table_fourcc.h`, and zero dependencies. It carries the same `libdrm.so.2` the ecosystem's Mesa payload does, and that is **not** a reason to bind: a DT_NEEDED soname already in the link map is REUSED, ld.so never searches again, so Mesa's `libgbm.so.1` binds to the copy the consumer linked — measured: one mapping in the process, and Mesa's GBM allocated a buffer through it. It holds only for `kind = "shared"` with the right soname: as the default `kind = "lib"` there is no `.so` to reuse, the payload's loads for Mesa, the consumer keeps its own merged one, and libdrm's file-static state (`drmHashTable`, `nr_fds`, `connection`) exists twice over one set of fds. The test asserts with `dladdr` that the loaded library is this package's. **Two include roots**: the public headers sit at the source root and the uapi headers under `include/drm/`, and `xf86drm.h` line 40 is a bare `#include `) · [`compat.libffi`](../pkgs/c/compat.libffi.lua) (3.4.8. The x86 assembly **gates itself** — `ffi.c`/`ffiw64.c`/`unix64.S`/`sysv.S`/`win64.S` each open with `#ifdef __x86_64__` or `__i386__` — so all of them are listed and the preprocessor selects. `fficonfig.h` is configure's record of what it probed and cannot be derived, so it is inlined; `ffi.h` CAN be derived, so install() performs configure's four substitutions on `ffi.h.in` and stays honest across a version bump) · [`compat.expat`](../pkgs/c/compat.expat.lua) (2.7.1, three TUs — `xmltok_impl.c` and `xmltok_ns.c` are `#include`d BY `xmltok.c`, five times between them, so listing them would produce duplicate symbols) | | Always-on interface define | `CURL_STATICLIB` in [`compat.curl`](../pkgs/c/compat.curl.lua): `cflags` is always on but package-private, while a feature's `defines` reaches consumers yet has to be named — `default = { implies = … }` applies unconditionally and happens to give both | +| `*_STATIC_DEFINE` delivered to consumers through a shim header | [`compat.yaml-cpp`](../pkgs/c/compat.yaml-cpp.lua) (0.8.0 and 0.9.0 off one source list. Upstream makes `YAML_CPP_STATIC_DEFINE` a PUBLIC definition of a static build and reads it only in `yaml-cpp/dll.h`; a descriptor's `defines` stay package-private, so a same-named `mcpp_generated/yaml-cpp/dll.h` defines it and `#include_next`s upstream's. Without it the MSVC ABI turns every `YAML_CPP_API` into `dllimport` and the link fails, while Linux shows nothing — so the test asserts the macro at compile time. Same shape: [`compat.cpptrace`](../pkgs/c/compat.cpptrace.lua)) | | Multiple majors in one package (shape switches with the version) | [`compat.catch2`](../pkgs/c/compat.catch2.lua) (3.x compiles `src/catch2/` into a static library; 2.x goes header-only through `single_include/`) | | External build system (`install()` builds from source) | [`compat.openblas`](../pkgs/c/compat.openblas.lua) (Make) · [`compat.openssl`](../pkgs/c/compat.openssl.lua) (Perl Configure + Make, static libssl/libcrypto) | | Whole-source direct build (config snapshot + source list, no external build system) | [`compat.ffmpeg`](../pkgs/c/compat.ffmpeg.lua) (2281 TUs including NASM assembly, declared through 28 directory globs) | diff --git a/docs/openkal-compat.md b/docs/openkal-compat.md index 2fe7f00f..2fe484ea 100644 --- a/docs/openkal-compat.md +++ b/docs/openkal-compat.md @@ -87,12 +87,22 @@ derivation, stated next to the code that computes it. The results are written to `.xpkgindex/openkal-compat.json` together with the pins and the date. The site gives every package a test project covers the best -result any covering project recorded for each target, and files the package -under the `openkal` facet by its best target and under the `openkal_kind` -facet by `platform` if any measured target recorded it, else `posix` if any -did. The packages of openkal itself are filed as `openkal itself` and carry -neither `kind` nor `openkal_kind`: they answer what openkal is, not what a -package built on it needs. +result any covering project recorded for each target. The `openkal` facet +has two values: + +| Facet value | Packages filed under it | +| --- | --- | +| `openkal-ecosystem` | the packages that make up openkal: the specification, its implementations, and the layers built directly on it | +| `openkal-compat` | packages whose best target recorded `runs` | + +A package whose best target recorded `builds` or `fails` is filed under +neither value. Its page still shows the measurement for each target, with the +first diagnostic. + +A measured package is also filed under the `openkal_kind` facet: `platform` +if any measured target recorded it, otherwise `posix` if any target did. +`openkal-ecosystem` packages carry neither `kind` nor `openkal_kind`: they +answer what openkal is, not what a package built on it needs. The `openkal_kind` facet and the badge it drives are a package-level **summary**: they take the strictest target, the way `platform` above is @@ -112,7 +122,7 @@ fails is measured and published, not excluded. `[not-portable.]` declares one TARGET of one member unbuildable by construction, with the reason. It exists because `[excluded]` is whole-member -and some members are neither: `cmp-module` runs on `x86_64-windows-gnu` and +and some members are neither: `cmp-module` runs on `x86_64-windows-musl` and cannot build on `x86_64-linux-gnu`, because asio's `detail/config.hpp` includes `` whenever `__linux__` is defined, outside every `ASIO_DISABLE_*` guard. Excluding the member outright would discard a result @@ -138,7 +148,7 @@ Both are recipe defects, and neither is the same defect: | target | first diagnostic | cause | | --- | --- | --- | | `x86_64-linux-gnu` | `lib/setopt.c:31: 'linux/tcp.h' file not found` | `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)`. The kernel IS Linux, so the predicate is right; what is wrong is reading it as "glibc's userspace headers are installed". The honest test is `__has_include()`. The same block also asserts `HAVE_GLIBC_STRERROR_R`, which is false over musl. | -| `x86_64-windows-gnu` | `curl_setup.h:591: "too small curl_off_t"` | The recipe's `windows` branch omits `HAVE_CONFIG_H` so that `curl_setup.h` reaches the checked-in `lib/config-win32.h`, and links `-lws2_32` with Schannel. Over openkal that target presents POSIX and is **LP64**, while `config-win32.h` is written for LLP64 and the Win32 API. | +| `x86_64-windows-musl` | `curl_setup.h:591: "too small curl_off_t"` | The recipe's `windows` branch omits `HAVE_CONFIG_H` so that `curl_setup.h` reaches the checked-in `lib/config-win32.h`, and links `-lws2_32` with Schannel. Over openkal that target presents POSIX and is **LP64**, while `config-win32.h` is written for LLP64 and the Win32 API. | **The second is the interesting one: the recipe branches on the PLATFORM where the question is about the C ENVIRONMENT.** Those two agreed on every target @@ -152,10 +162,23 @@ it is a larger change than the first and is not folded into it. ## 3. When it runs `.github/workflows/openkal-compat.yml` runs weekly and on demand, measuring every -listed member. For a pull request it measures every member when the openkal -family or `tests/openkal` changes, and otherwise the members whose test projects -depend on a changed descriptor. It does not block a merge unless the comparison -below is enabled. +listed member. For a pull request, `compat.py select` measures only what the +change can affect: + +| The pull request changes | Members measured | +| --- | --- | +| an openkal family descriptor, `pins.toml`, `compat.py`, or anything else under `tests/openkal/` except `members.toml` | every listed member: the graph or the harness changed | +| `members.toml` | the members whose entry was added or changed, compared with the base branch; a `[not-portable]` declaration counts for the member it names, and `[excluded]` selects nothing | +| `tests/examples//` | that member, if it is listed | +| a descriptor under `pkgs/` | the listed members whose test projects depend on it | + +Adding a member therefore measures that member, not the whole list. The +workflow does not block a merge unless the comparison below is enabled. + +The targets are `x86_64-linux-gnu` and `x86_64-windows-musl`. The Windows +target is named for the C environment the graph presents there, which is +musl's, not MinGW's. Results measured before 2026-09-23 are recorded as +`x86_64-windows-gnu`, the name that target had then. It installs the Windows cross toolchain's host headers on purpose. A build that reached the host's headers would change its result when they are present, so a diff --git a/docs/zh/cn-mirror.md b/docs/zh/cn-mirror.md index 4b68da21..3758826b 100644 --- a/docs/zh/cn-mirror.md +++ b/docs/zh/cn-mirror.md @@ -12,11 +12,15 @@ `nlohmann-json` 以避免裸 `json` 的歧义)。 - CN 资产的公网 URL 约定为: `https://gitcode.com/mcpp-res//releases/download//-.` +- 库的维护者也可以在自己的 gitcode 账号下自行维护 CN 镜像 + (`https://gitcode.com///releases/download//`,如 `ZheFeng7110.boost`)。 + 此时由维护者保证它与 GLOBAL 资产字节一致,即 `sha256` 相同。 ## 无 `mcpp-res` 写权限时的回退 建立镜像需要 gitcode `mcpp-res` 组织的写权限(token)。在不具备该权限时,不应勉强构造镜像表:lint -(`check_mirror_urls.lua`)强制要求,一旦 `url` 写成表形式,其 `CN` 必须为 `https://gitcode.com/mcpp-res/…`, +(`check_mirror_urls.lua`)强制要求,一旦 `url` 写成表形式,其 `CN` 必须为 gitcode 的 release 资产 +(`https://gitcode.com///releases/download/…`), 因此 `{ GLOBAL=上游, CN=上游 }` 会直接导致 lint 失败。正确的回退方式是采用纯字符串 url(仅填上游 release), lint 对纯字符串 url 不施加镜像约束: diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 0c0cf8e8..efa3da25 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -11,11 +11,13 @@ | 形态 | 示例 | |------|------| | 原生模块库(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.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 的消费者链进去的是同一份而非两份)| +| 消费者依赖须手写的 Form A | [`huxerui.huxerui`](../../pkgs/h/huxerui.huxerui.lua)(HuxerUI 把 GTK4 栈声明在 TARGET 轴上 —— 这是 mcpp 推荐的写法,而描述符在结构上承载不了:只有三个平台块,cfg 选择器不是平台。`mcpp emit xpkg` 会如实说明并输出空 `deps`,因此 36 项闭包被手工抄进 PLATFORM 级的 `xpm.linux.deps`(按版本写的 `deps` 不生效)。它的 `licenses`/`repo` 也刻意与 emit 的产出不同) | | 原生多模块库(按 feature 管理源码) | [`gzj-creator.galay`](../../pkgs/g/gzj-creator.galay.lua)(Galay 5.0.2 —— 上游 Form-A manifest 默认提供 `galay.utils` 与 `galay.kernel`,SSL、HTTP、数据库、RPC、MCP、tracing 等模块及其依赖按具名 feature 开启。索引保持上游 manifest 原样,Unix 成员测试默认模块表面)| | C 源码 compat(含 `features`) | [`compat.cjson`](../../pkgs/c/compat.cjson.lua) · [`compat.zlib`](../../pkgs/c/compat.zlib.lua) · [`compat.hiredis`](../../pkgs/c/compat.hiredis.lua)(经典 1.2.0 —— 7 个 C TU;tarball 平铺头经 `generated_files` 补 `hiredis/` 前缀薄包装头,消费者可写 `#include `,与上游安装布局一致) · [`compat.sqlite3`](../../pkgs/c/compat.sqlite3.lua)(纯 C 源码、无 feature:单一 `sqlite3.c` amalgamation;3.45.3,部署最广的 3.45.x 线) · [`compat.libuv`](../../pkgs/c/compat.libuv.lua)(libuv 1.48.0 —— 逐 OS 源清单转录自上游 CMakeLists,因为 `src/unix/*.c` 通配会一次编进所有 OS 的后端;linux/macos 显式列 unix 子集,windows 用 `src/win/*.c` glob) |· [`compat.xxhash`](../../pkgs/c/compat.xxhash.lua)(单 TU、单头、无 feature —— 值得说的是**没有**编译什么:`xxh_x86dispatch.c` 在运行期选择 AVX2/AVX512 路径,需要 per-file `-mavx2` 并要求每个调用点定义 `XXH_X86DISPATCH`,故本包只出无需任何 flag 的 SSE2 基线。也没有选 header-only 的 `XXH_INLINE_ALL` 模式:它会在每个做哈希的 TU 里重新展开整份实现,那只有在「恰好只有一个这样的 TU」时才划算 —— 而这件事包本身无从知道) | C 源码 compat(源码按**架构**选择) | [`compat.wamr`](../../pkgs/c/compat.wamr.lua)(WAMR 2.4.5 —— 描述符能按 OS 分 `sources`/`cflags`,但**没有按架构分**的钩子,`archs` 是包级元数据而非选择器。而 WAMR 两处都要按架构走:不定义 `BUILD_TARGET_X86_64`/`BUILD_TARGET_AARCH64` 之一,`wasm_runtime_common.c` 会把整段 invoke-native 编没,链接期报 `invokeNative` 未定义;而该符号的实现本身就是每架构一份手写汇编。两处都改交给预处理器:`generated_files` 生成的配置头把 `__x86_64__`/`__aarch64__` 映射到对应 `BUILD_TARGET_*`,经 `-include` 到达每个 TU;另一份生成的 `.S` 用 `#include` 把选中的汇编原样拉进来 —— 之所以必须是 `.S`,是因为上游的 `arch/invokeNative_*.s` 是小写后缀,clang 汇编它们**不过预处理器**,因此不像 libffi 的 `.S` 那样能自己加架构守卫。另有两点值得抄:`-std=gnu11` 经 `cflags` 追加,因为 `c_standard = "c11"` 会定义 `__STRICT_ANSI__`,此时 `platform_internal.h` 里裸写的 `asm` 不是关键字;以及上游在关掉 WASI 时会剔除的那几个 POSIX 文件,这里**无条件常含**,而不是 base 用 `!` 排除、再由 `libc-wasi` feature 加回来 —— 排除 glob 是全局的,会盖过 feature 里同名文件的条目) | | C 源码 compat(库本身就是内核 ABI) | [`compat.libaio`](../../pkgs/c/compat.libaio.lua)(libaio 0.3.113 —— 12 个系统调用封装 TU,`xpm` 只有 `linux` 一段,因为根本不存在「移植」可声明:`struct iocb` 就是内核的结构体,每个 TU 都是 `syscall(__NR_io_*, …)`。消费者用 `[target.'cfg(linux)'.dependencies]` 门控,与 compat.wil 互为镜像。它给出三条经验。**把唯一的公开头从源码目录里择出来**:上游只安装 `libaio.h` 一个头,但 tarball 把它放在 `src/` 里、与私有头并列 —— 其中一个恰好叫 `syscall.h`,一旦上了 include 路径就会**遮蔽** glibc 的同名头。故 `include_dirs` 只指向一个 `generated_files` 转发头;包自身的源码经它拿到真头文件,而它们引号形式的 `#include "syscall.h"` 仍按「包含者所在目录优先」解析,于是整包**不需要任何指向 `src/` 的 `-I`**。**一个会骗人的 `c_standard`**:`-std=c11` 会定义 `__STRICT_ANSI__`,从而藏掉 `syscall()` 与 `sigset_t`,连公开头都会在 `io_pgetevents` 处解析失败;写 `c_standard = "gnu11"` **看起来**是解法,但 mcpp 2026.8.27.2 接受这个字符串却依然发 `-std=c11`(在产出的 `compile_commands.json` 里可见),真正生效的写法是 `cflags` 里的 `-D_GNU_SOURCE`。**静态包里的符号版本**:`io_getevents` / `io_cancel` 在上游并没有普通定义 —— 函数名是 `io_getevents_0_4` 之类,短名经 `.symver … @@LIBAIO_0.4` 发布 —— 链接**可执行文件**时 ld.bfd 与 lld 都能解析,但消费者若直接拿这些对象去构建 `.so` 就不行,那需要上游的 `src/libaio.map`,与上游自己的 `libaio.a` 完全同理) | | 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`) · [`compat.redis-plus-plus`](../../pkgs/c/compat.redis-plus-plus.lua)(redis++ 1.3.13 —— 同步客户端,17 TU + `patterns/redlock.cpp`,依赖 `compat.hiredis`;CMake 唯一会生成的头 `hiredis_features.h` 用 `generated_files` 快照,async/TLS TU 不收,基座保持两包成对。`async` feature 补齐 libuv 版 `AsyncRedis` 接口(9 个 async TU + `compat.libuv`;`event_loop.cpp` 在后台线程跑 `uv_run`,`` 经 compat.hiredis 的包装头到达)。两个版本分处源码结构分水岭两侧,共享同一份源列表:1.3.13(现代 17-TU 布局)与 1.3.3(缺 `redis_uri.cpp`/`redlock` 的 15-TU 旧布局)—— 并集之所以成立,是因为 1.3.3 的 TU 是 1.3.13 的严格子集,恰好两个 glob 在 1.3.3 上零命中(仅警告,非错误;与 compat.catch2 同款手法)) · [`compat.sqlitecpp`](../../pkgs/c/compat.sqlitecpp.lua)(SQLite 的 RAII C++ 封装。上游用 **git submodule** 引 sqlite3,源码 tarball 里根本没有它,库因此无法链接 —— 依赖边指向 `compat.sqlite3` 替代了那个 submodule,而且更好:同一次链接里的两个 SQLite 消费者从此共享**一份** amalgamation,而不是各自内嵌一份带各自编译选项的副本。它的两个 CMake 开关有意不设 —— `SQLITECPP_USE_ASSERT_ON_ERRORS` 把错误模型从抛异常改成中止进程,`SQLITE_ENABLE_COLUMN_METADATA` 必须与 SQLite **自身**的构建一致;两者都该由消费者决定,而头文件本来就用 `#ifdef` 守着) | +| 多组件上游拍平进单个库 | [`compat.recastnavigation`](../../pkgs/c/compat.recastnavigation.lua)(Recast Navigation 1.6.0 —— 上游是五个互相依赖的 CMake target;这里把「人人都要用」的两个作为核心,其余三个做 `features`,全部编进同一个 lib。依赖边必须手工重建,这正是 `debug-utils` 带 `implies = { "tilecache" }` 的原因:上游无条件链接 DetourTileCache,没有这条 implication,只开调试绘制的消费者会在**链接期**撞上缺失的 `dtTileCache*` 符号。上游安装把头文件拍平进 `include/recastnavigation/`,同时把该目录**及其父目录**都放进 interface include path,于是对着真实安装 `#include ` 与 `#include ` 都合法 —— 26 个生成的转发头把第二种拼写还给源码树构建。`RECASTNAVIGATION_DT_POLYREF64` 与 `RECASTNAVIGATION_DT_VIRTUAL_QUERYFILTER` 刻意**不**做成 feature:它们改变跨库边界类型的 ABI,而 feature 的 `defines` 只作用于本包自己的 TU) | | C 传输层 + 其上的 header-only C++ 服务端 | [`compat.usockets`](../../pkgs/c/compat.usockets.lua) · [`compat.uwebsockets`](../../pkgs/c/compat.uwebsockets.lua)(uSockets 三平台统一选 libuv 一个事件循环(经 `compat.libuv`),因为按平台各选后端只会让 `us_loop_t` 每个平台一个形状而毫无收益;SSL 与 QUIC 不收,于是基础包的唯一依赖就是那个循环。这一对真正的教训是 `LIBUS_USE_LIBUV` / `LIBUS_NO_SSL` / `UWS_NO_ZLIB` 是**接口级**事实:`libusockets.h` 会因前者改变 `us_loop_t` 的布局、因后者门控 SSL 声明,而 uWS 是 header-only —— 它的模板是在**消费者**的 TU 里实例化的。描述符的 `cflags` 只作用于包自身的 TU,所以每个消费者都必须自己声明这三个;不一致不会构建失败,而是内存损坏。usockets 的测试因此从定时器回调里写loop 附属的扩展内存再读回来 —— 布局一旦不一致,正是这条断言会断) | | 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) | | 数据库客户端 + 源码构建的驱动管理器 | [`compat.nanodbc`](../../pkgs/c/compat.nanodbc.lua)(nanodbc 2.14.0,上游已冻结 —— 单 TU 封装平台 ODBC 驱动管理器。两处修复让这份四年前的源码在此可编译、可运行:一个 force-include 的 `char_traits` 补丁头(标准留给用户的定制点,以 `_LIBCPP_VERSION` 为界,不影响 libstdc++/MSVC;注意 `-include` 只能经 `cxxflags` 到达 C++ TU,`cflags` 够不着),以及对驱动管理器本身的分平台答案 —— windows 链 SDK 的 odbc32、macOS 链系统自带的 iODBC,linux 则依赖 `compat.unixodbc`,因为 mcpp 的运行时闭包检查不接受只有宿主才有的 `libodbc.so.2`。测试断言管理器自身的诊断能穿过封装层 —— 包括 nanodbc 已冻结的、会把 SQL state 末字符截掉的 off-by-one)· [`compat.unixodbc`](../../pkgs/c/compat.unixodbc.lua)(unixODBC 2.3.14,E 叠 A 形态 —— DM + odbcinst + ini/log/lst + libltdl 静态编入单一 `odbc` 目标,与上游 libodbc.a 符号集一致,消费者不带任何 `libodbc.so.2` NEEDED。唯一非常规之处是无 libtool 的 ltdl 接线:`-DLTDLOPEN=libltdlc` 加一张生成的 `lt_libltdlc_LTX_preloaded_symbols` 表(从 libtool 目标文件的重定位记录还原)注册 dlopen loader。冻结的 `config.h` 把 ltdl 自己的 configure 输出合并进顶层(冲突宏 ltdl 源码并不读),绕开了无法在管道中幸存的带引号 `-DLT_CONFIG_H`。已与同 tarball 的 libtool 构建对比验证:IM002 错误路径与 `lt_dlopen` 行为完全一致) | @@ -35,6 +37,7 @@ | 需要真实工程的源码构建(Form A fork) | [`freedesktop.wayland`](../../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../../pkgs/f/freedesktop.wayland-util.lua)(wayland 1.26.0,一个 tarball 出四个条目 —— [mcpplibs/wayland](https://github.com/mcpplibs/wayland),不改上游任何文件的 fork。**为什么要 fork 而不是内联描述符**:wayland 大部分是**生成**出来的 —— `protocol/wayland.xml` 描述了每个接口,wayland-scanner 从它生成约 13000 行 —— 而生成器是同一棵树里的 C 程序,**必须先被编译**才能跑。内联描述符没有构建步骤;`install()` 也做不到,因为 mcpp 是在**消费者构建期**才编译包源码,别的包安装时不存在任何包产物。`build.mcpp` 正是为此而设,而它只存在于真实的 mcpp 工程里。**为什么是四个包**:`libwayland-client.so.0` 与 `libwayland-server.so.0` 是两个不同 soname,Mesa 的 libEGL_mesa 对**两者都**有 DT_NEEDED,所以必须是内容互不相交的两个文件;而 mcpp 把包的源码编一次、让**每个**库 target 都链接全部对象 —— 实测:feature 门控的第二个 target 照样拿到该 feature 的对象 —— 一个包出不了两个互不相交的库。mcpp 自己的告警就点了办法:「split into a workspace member」。**模块层与库同包**:每个包同时带 C 库和它的 `.cppm`,所以是一库一包,而不是 C 一个、模块一个。包装层**不增加 API**:导出名全是上游的,而且列表是从公开头**生成**的,版本一动不会悄悄少一个名字。**模块名跟的是拥有「接口」的组织**,所以这三个叫 `freedesktop.wayland.{client,server,util}` —— wayland 协议确实归 freedesktop。索引早先的惯例是把 namespace 整个丢掉(`chriskohlhoff.asio` → `import asio`、`compat.opencv` → `import opencv.cv`),这一条是把那个惯例说具体:模块名是**全局且长期**的,包名不是。`mcpplibs.openkal` 已经为此付过学费 —— 它的 0.1.0 是被**撤回**而不是保留的,理由是「把消费者 import 的模块放在了实现的控制之下,这与规范存在的意义相悖」。**宏是唯一过不去的东西**:`export` 命名的是实体而宏不是,所以 `wayland-util` 把每个宏映射成它实际是的东西 —— `WL_MARSHAL_FLAG_DESTROY` → constexpr、`wl_container_of` → 函数模板、六个 `*_for_each` → range,它的测试把它们全部实例化,包括 `_safe` 的「循环体内可删除当前元素」保证)· [`freedesktop.egl`](../../pkgs/f/freedesktop.egl.lua)(libglvnd 1.7.0 —— [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd),就是把上一行 `compat.egl` 绑定换掉的那个条目。**生成物是签进仓的,不是构建时生成的**:上游要跑约 1000 行 Python 处理 2.7 MB 的 `gl.xml` 才能得到 dispatch 表,而这份输出在 registry 固定的前提下是确定性的 —— 于是它躺在 `mcpp/generated/`,构建期**不跑任何生成器**,改由 CI 重新生成并 diff。`mcpp build` 就是全部工具链。**按架构选文件这件事放在 `build.mcpp` 里,不在清单里**:GLdispatch 的 entry stub 既分架构又分线程存储模型,而且和 libffi 的不同,它们自身**没有**任何门控 —— 在 `sources` 里写死 x86_64 会让这个包只能在 x86_64 上用、而且哪儿都不写明;`build.mcpp` 用 `mcpp::target_arch()` 做的正是上游 `gl_dispatch_type` 的那个选择。**模块叫 `khronos.egl` 而不是 `freedesktop.egl`**,包名与模块名故意不一致,而这正是重点:freedesktop 确实在发这份代码,但 EGL 是 **Khronos** 的规范、有多个实现。用实现方命名模块,等于将来换一个 libEGL 就要逼所有消费者改 import —— 而这个包装层的全部承诺就是「除了 include 那一行什么都不改」。副作用也是好的:两个 EGL 提供方现在会硬冲突而不是静默共存,这本来就是 GLVND 想要的。**一个索引条目,两个库**:`libGLdispatch.so.0` 由同 workspace 的兄弟成员构建,通过 **path** 依赖引入而不是走索引 —— 「进程里只有一个 dispatch 点」是 GLVND 存在的全部理由,而两个索引条目会让消费者把两个都写上、解析出两个包实例,soname 复用只映射其中一个、另一个被静默丢弃。**编译进去的 vendor 路径刻意是空的**:上游烤进 `/share/glvnd/egl_vendor.d`,重定位之后那就是 **host** 的目录,所以留空能让「生态没声明」表现为「找不到 vendor」,而不是悄悄把宿主驱动装进沙箱进程 —— 与 compat.libgbm 对 `GBM_BACKENDS_PATH` 的立场一致)| | 与 payload 同 soname 的源码库 | [`compat.libdrm`](../../pkgs/c/compat.libdrm.lua)(libdrm 2.4.134,5 个 TU + 内联的 `generated_static_table_fourcc.h`,零依赖。它和生态 payload 里的 `libdrm.so.2` 同名共存,而这**不是**绑定的理由:DT_NEEDED 的 soname 一旦在 link map 中就被复用,ld.so 不再搜索,所以 Mesa 的 `libgbm.so.1` 绑到的是消费者链接的这一份 —— 实测进程里只有一份,且 Mesa 的 GBM 用它分配出了 buffer。前提是 `kind = "shared"` **且 soname 正确**:用默认的 `kind = "lib"` 就没有 `.so` 可复用,payload 那份照常为 Mesa 加载,消费者另有一份合并进来的,libdrm 的 file-static 状态(`drmHashTable`/`nr_fds`/`connection`)在同一批 fd 上分成两套账。测试用 `dladdr` 断言加载的确实是本包所建。**两个 include 根**:公开头在源码根、uapi 头在 `include/drm/`,而 `xf86drm.h` 第 40 行就是裸的 `#include `) · [`compat.libffi`](../../pkgs/c/compat.libffi.lua)(3.4.8。x86 汇编**自带架构门控**,`ffi.c`/`ffiw64.c`/`unix64.S`/`sysv.S`/`win64.S` 各自 `#ifdef __x86_64__`/`__i386__`,所以全部列进 sources 由预处理器筛;`fficonfig.h` 是 configure 的探测结果、无法从 tarball 推导,内联;`ffi.h` **能**推导,于是 install() 照 configure 的四个替换从 `ffi.h.in` 生成,版本一动就跟着动) · [`compat.expat`](../../pkgs/c/compat.expat.lua)(2.7.1,3 个 TU —— `xmltok_impl.c`/`xmltok_ns.c` 是被 `xmltok.c` **include** 五次的,列进 sources 会产生重复符号) | | 恒开的 interface define | [`compat.curl`](../../pkgs/c/compat.curl.lua) 的 `CURL_STATICLIB`:`cflags` 恒开但包私有,feature `defines` 可达消费端但需点名 —— `default = { implies = … }` 无条件生效,恰好两者兼得 | +| 用遮蔽头把 `*_STATIC_DEFINE` 送到消费者 | [`compat.yaml-cpp`](../../pkgs/c/compat.yaml-cpp.lua)(0.8.0 与 0.9.0 共用一份源码列表。上游把 `YAML_CPP_STATIC_DEFINE` 作为静态构建的 PUBLIC 定义,且只在 `yaml-cpp/dll.h` 里读取;描述符的 `defines` 只作用于本包,所以由同名的 `mcpp_generated/yaml-cpp/dll.h` 定义它再 `#include_next` 上游那份。缺了它,MSVC ABI 上每个 `YAML_CPP_API` 都成了 `dllimport`,链接失败,而 Linux 上毫无症状 —— 所以测试在编译期断言这个宏。同一形态:[`compat.cpptrace`](../../pkgs/c/compat.cpptrace.lua)) | | 单包多 major(形态随版本切换) | [`compat.catch2`](../../pkgs/c/compat.catch2.lua)(3.x 编 `src/catch2/` 出静态库;2.x 走 `single_include/` header-only) | | 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](../../pkgs/c/compat.openblas.lua)(Make) · [`compat.openssl`](../../pkgs/c/compat.openssl.lua)(Perl Configure + Make,静态 libssl/libcrypto) | | 全源码直编(config 快照 + 源列表,零外部构建系统) | [`compat.ffmpeg`](../../pkgs/c/compat.ffmpeg.lua)(2281 TU 含 NASM 汇编,28 个目录 glob 声明) | diff --git a/docs/zh/openkal-compat.md b/docs/zh/openkal-compat.md index 24876d09..1564ecd8 100644 --- a/docs/zh/openkal-compat.md +++ b/docs/zh/openkal-compat.md @@ -29,13 +29,20 @@ | `builds` | 成员构建成功,测试未运行或未通过;保留第一条诊断 | | `fails` | 成员构建失败;保留第一条诊断 | -结果连同固定版本与日期写入 `.xpkgindex/openkal-compat.json`。一个包若被多个测试项目覆盖,站点对每个目标取其中最好的结果,并按最好的目标将包归入 `openkal` 分面。openkal 自身的包归为 `openkal 本身`。 +结果连同固定版本与日期写入 `.xpkgindex/openkal-compat.json`。一个包若被多个测试项目覆盖,站点对每个目标取其中最好的结果。`openkal` 分面有两个取值: + +| 分面取值 | 归入的包 | +| --- | --- | +| `openkal-ecosystem` | 构成 openkal 的包:规范、各平台实现,以及直接建立在它之上的各层 | +| `openkal-compat` | 最好的目标记录为 `runs` 的包 | + +最好的目标只记录为 `builds` 或 `fails` 的包不归入任何取值;它的页面仍按目标显示测量结果与第一条诊断。 成员是否自行选择平台依赖也会记录。自行选择平台依赖是允许的:openkal 上的包可以使用平台的系统接口,只要这些依赖来自依赖图。这一区分会显示出来,但不降低标签。 `tests/openkal/members.toml` 列出测量对象。`[excluded]` 列出在任何 openkal 依赖图中都无法构建的成员,并逐条写明原因;失败的成员照常测量并公布,不列入排除。 -`[not-portable.<成员>]` 声明某个成员的**某一个目标**按构造无法构建,并写明理由。它存在是因为 `[excluded]` 是整成员级的,而有些成员两头都不是:`cmp-module` 在 `x86_64-windows-gnu` 上 runs,在 `x86_64-linux-gnu` 上建不起来——asio 的 `detail/config.hpp` 只要 `__linux__` 有定义就 include ``,而那行在所有 `ASIO_DISABLE_*` 守卫之外。整个排除掉这个成员,等于为了藏起一个真的结果而丢掉另一个同样真的结果。 +`[not-portable.<成员>]` 声明某个成员的**某一个目标**按构造无法构建,并写明理由。它存在是因为 `[excluded]` 是整成员级的,而有些成员两头都不是:`cmp-module` 在 `x86_64-windows-musl` 上 runs,在 `x86_64-linux-gnu` 上建不起来——asio 的 `detail/config.hpp` 只要 `__linux__` 有定义就 include ``,而那行在所有 `ASIO_DISABLE_*` 守卫之外。整个排除掉这个成员,等于为了藏起一个真的结果而丢掉另一个同样真的结果。 **门槛是「没有任何清单键伸得进去」。** 上游源码在预处理期发问,算;本索引自己生成的配置头,不算,那属于配方。`curl` 的 `linux/tcp.h` 就是后者——`pkgs/c/compat.curl.lua` 在 `#if defined(__linux__)` 里写了 `#define HAVE_LINUX_TCP_H 1`,把一个关于内核的正确事实读成了关于「装了哪些 userspace 头」的断言。 @@ -48,13 +55,24 @@ | 目标 | 首条诊断 | 真因 | | --- | --- | --- | | `x86_64-linux-gnu` | `lib/setopt.c:31: 'linux/tcp.h' file not found` | `#if defined(__linux__)` 里写死了 `#define HAVE_LINUX_TCP_H 1`。内核**确实**是 Linux,谓词没错;错的是把它读成「glibc 的 userspace 头都装好了」。诚实的判据是 `__has_include()`。同一个块里还有 `HAVE_GLIBC_STRERROR_R`,在 musl 上它是假的。 | -| `x86_64-windows-gnu` | `curl_setup.h:591: "too small curl_off_t"` | 配方的 `windows` 分支**有意**不定义 `HAVE_CONFIG_H`,好让 `curl_setup.h` 去取仓库里checked-in 的 `lib/config-win32.h`,并链 `-lws2_32` 走 Schannel。而在 openkal 上,那个目标呈现的是 POSIX 且是 **LP64**,`config-win32.h` 写的是 LLP64 与 Win32 API。 | +| `x86_64-windows-musl` | `curl_setup.h:591: "too small curl_off_t"` | 配方的 `windows` 分支**有意**不定义 `HAVE_CONFIG_H`,好让 `curl_setup.h` 去取仓库里checked-in 的 `lib/config-win32.h`,并链 `-lws2_32` 走 Schannel。而在 openkal 上,那个目标呈现的是 POSIX 且是 **LP64**,`config-win32.h` 写的是 LLP64 与 Win32 API。 | **第二条才是有意思的那条:配方按「平台」分支,而问题问的是「C 环境」。** 在 openkal 于 Windows 上呈现 POSIX 之前,这两者在本索引的每一个目标上都同答案。mcpp 有那个真正被问的谓词——`cfg(c-abi = "musl")`(mcpp docs/22「按解析出的目标侧适配」)。在那里改选生成的 POSIX 配置而不是 checked-in 的 Win32 配置,就是它的形状;它还需要本索引的 OpenSSL 跑在同一个环境上,所以比第一条大,不与它合并。 ## 3. 何时运行 -`.github/workflows/openkal-compat.yml` 每周运行、可手动触发,测量全部列出的成员。对 PR,openkal 家族或 `tests/openkal` 变化时测量全部成员,否则测量依赖了被修改描述符的成员。除非启用下文的比较,它不阻止合并。 +`.github/workflows/openkal-compat.yml` 每周运行、可手动触发,测量全部列出的成员。对 PR,`compat.py select` 只测量改动可能影响的成员: + +| PR 改动了 | 测量的成员 | +| --- | --- | +| openkal 家族描述符、`pins.toml`、`compat.py`,或 `tests/openkal/` 下除 `members.toml` 以外的文件 | 全部列出的成员:依赖图或测量工具变了 | +| `members.toml` | 与基准分支相比,条目新增或改动的成员;`[not-portable]` 声明计入它所指的成员,`[excluded]` 不选中任何成员 | +| `tests/examples/<成员>/` | 该成员(若已列出) | +| `pkgs/` 下的描述符 | 测试项目依赖该描述符的已列出成员 | + +因此新增一个成员只测量该成员,而不是整张列表。除非启用下文的比较,它不阻止合并。 + +目标为 `x86_64-linux-gnu` 与 `x86_64-windows-musl`。Windows 目标按依赖图在那里呈现的 C 环境命名,即 musl 的,而不是 MinGW 的。2026-09-23 之前测得的结果记为 `x86_64-windows-gnu`,即该目标当时的名字。 它有意安装宿主的 Windows 交叉头文件。若构建触及宿主头文件,结果会因其存在而改变;结果不变即说明依赖图是封闭的。 diff --git a/mcpp.toml b/mcpp.toml index 60f54db2..0494b2b0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -152,6 +152,8 @@ members = [ "tests/examples/websocket-tls", "tests/examples/tray", "tests/examples/xxhash", + "tests/examples/yaml-cpp", + "tests/examples/yaml-cpp-v080", "tests/examples/yyjson", "tests/examples/zlib", "tests/examples/magic_enum", diff --git a/pkgs/c/compat.yaml-cpp.lua b/pkgs/c/compat.yaml-cpp.lua new file mode 100644 index 00000000..67c3a04d --- /dev/null +++ b/pkgs/c/compat.yaml-cpp.lua @@ -0,0 +1,151 @@ +-- compat.yaml-cpp — yaml-cpp, a YAML 1.2 parser and emitter for C++. +-- +-- Shape A (C++ source compat): no configure step, no generated config header, +-- no submodules, no platform code in the library itself, so one source list +-- covers linux/macosx/windows and both versions. +-- +-- TWO VERSIONS, ONE SOURCE LIST. 0.9.0 adds exactly one translation unit +-- (src/fptostring.cpp, shortest round-trip float formatting over the vendored +-- src/contrib/dragonbox.h) and one public header (yaml-cpp/fptostring.h); the +-- `*/src/*.cpp` glob picks it up where it exists. The two tags are spelled +-- differently upstream (`0.8.0` vs `yaml-cpp-0.9.0`), so the archive wrap dirs +-- differ too (`yaml-cpp-0.8.0/` vs `yaml-cpp-yaml-cpp-0.9.0/`); the leading `*` +-- absorbs both. +-- +-- Sources are upstream's CMake target exactly: `src/*.cpp` plus, with +-- YAML_CPP_BUILD_CONTRIB at its default ON, `src/contrib/*.cpp` (the +-- GraphBuilder API declared in the public yaml-cpp/contrib/graphbuilder.h). +-- The src/ headers are all reached by same-directory quoted includes, so the +-- private `src` include root upstream adds is not needed. +-- +-- Upstream defect, left as upstream has it: GraphBuilderInterface's destructor +-- is declared pure virtual and defined nowhere, so a class derived from it +-- links only if its user defines `~GraphBuilderInterface()`. Supplying it here +-- would give every consumer who already writes it a duplicate symbol. +-- +-- YAML_CPP_STATIC_DEFINE. Upstream's CMake makes it a PUBLIC definition of a +-- static build, and include/yaml-cpp/dll.h is the only header that reads it. +-- Without it, on the MSVC ABI (this index's windows leg defines _MSC_VER), +-- every YAML_CPP_API declaration becomes __declspec(dllimport) and a consumer +-- of these statically linked objects fails with `__declspec(dllimport)` +-- undefined symbols; on ELF it is only the difference between +-- visibility("default") and nothing, so Linux never notices. A descriptor's +-- `defines` reach only this package's own TUs, so the define is delivered to +-- consumers by a same-named shim that every public header already funnels +-- through, and tests/examples/yaml-cpp asserts it arrived. +-- +-- openkal: nothing here is conditional on it. yaml-cpp needs only the C++ +-- standard library, and its one compiler-specific branch (dragonbox.h's +-- under _MSC_VER) is taken only where _MSC_VER is defined. +-- tests/openkal/members.toml measures tests/examples/yaml-cpp. +package = { + spec = "1", + namespace = "compat", + name = "yaml-cpp", + description = "yaml-cpp — YAML 1.2 parser and emitter for C++ (static)", + licenses = {"MIT"}, + repo = "https://github.com/jbeder/yaml-cpp", + type = "package", + + xpm = { + linux = { + ["0.8.0"] = { + url = { + GLOBAL = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/yaml-cpp/releases/download/0.8.0/yaml-cpp-0.8.0.tar.gz", + }, + sha256 = "fbe74bbdcee21d656715688706da3c8becfd946d92cd44705cc6098bb23b3a16", + }, + ["0.9.0"] = { + url = { + GLOBAL = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.9.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/yaml-cpp/releases/download/0.9.0/yaml-cpp-0.9.0.tar.gz", + }, + sha256 = "25cb043240f828a8c51beb830569634bc7ac603978e0f69d6b63558dadefd49a", + }, + }, + macosx = { + ["0.8.0"] = { + url = { + GLOBAL = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/yaml-cpp/releases/download/0.8.0/yaml-cpp-0.8.0.tar.gz", + }, + sha256 = "fbe74bbdcee21d656715688706da3c8becfd946d92cd44705cc6098bb23b3a16", + }, + ["0.9.0"] = { + url = { + GLOBAL = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.9.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/yaml-cpp/releases/download/0.9.0/yaml-cpp-0.9.0.tar.gz", + }, + sha256 = "25cb043240f828a8c51beb830569634bc7ac603978e0f69d6b63558dadefd49a", + }, + }, + windows = { + ["0.8.0"] = { + url = { + GLOBAL = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/yaml-cpp/releases/download/0.8.0/yaml-cpp-0.8.0.tar.gz", + }, + sha256 = "fbe74bbdcee21d656715688706da3c8becfd946d92cd44705cc6098bb23b3a16", + }, + ["0.9.0"] = { + url = { + GLOBAL = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.9.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/yaml-cpp/releases/download/0.9.0/yaml-cpp-0.9.0.tar.gz", + }, + sha256 = "25cb043240f828a8c51beb830569634bc7ac603978e0f69d6b63558dadefd49a", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + + -- Consumers write `#include `. ORDER MATTERS: + -- mcpp_generated FIRST, so the dll.h shim below is found before + -- upstream's, which it then reaches with #include_next. + include_dirs = { "mcpp_generated", "*/include" }, + + generated_files = { + -- Delivers YAML_CPP_STATIC_DEFINE to every TU that opens a + -- yaml-cpp header, which no descriptor key can do. See the header + -- note. + ["mcpp_generated/yaml-cpp/dll.h"] = [==[ +// mcpp-index shim: yaml-cpp is built as objects here, not as a shared library, +// so every declaration must be plain rather than dllimport/visibility-default. +// Upstream reads YAML_CPP_STATIC_DEFINE in this header and nowhere else, and +// every public yaml-cpp header reaches this one. +#ifndef MCPP_COMPAT_YAML_CPP_STATIC_SHIM +#define MCPP_COMPAT_YAML_CPP_STATIC_SHIM +#ifndef YAML_CPP_STATIC_DEFINE +# define YAML_CPP_STATIC_DEFINE +#endif +#include_next +#endif +]==], + }, + + sources = { + "*/src/*.cpp", + "*/src/contrib/*.cpp", + }, + + targets = { ["yaml-cpp"] = { kind = "lib" } }, + deps = { }, + + -- Belt and braces for this package's OWN TUs; the shim above is what + -- reaches everyone else's. + defines = { "YAML_CPP_STATIC_DEFINE" }, + + -- 0.8.0's src/emitterutils.cpp names uint16_t/uint32_t unqualified + -- and includes neither nor (0.9.0 added it). It + -- compiles here only because C++20 and later make libstdc++'s + -- reach -> bits/unicode.h -> . Measured + -- with gcc 15.1.0 over glibc 2.44: -std=c++17 fails with "'uint16_t' + -- was not declared in this scope", c++20/c++23 compile. The package + -- builds as c++23, so it holds today, but on a transitive include; this + -- makes it hold on purpose. Harmless for 0.9.0. + cxxflags = { "-include", "cstdint" }, + }, +} diff --git a/tests/check_mirror_urls.lua b/tests/check_mirror_urls.lua index e7cf61f9..cc953176 100644 --- a/tests/check_mirror_urls.lua +++ b/tests/check_mirror_urls.lua @@ -4,7 +4,10 @@ -- urls remain valid and are left to the author): -- * a url table must define BOTH `GLOBAL` and `CN` -- * both values must be non-empty strings --- * `CN` must be a gitcode.com/mcpp-res//releases/download/... URL +-- * `CN` must be a gitcode.com///releases/download/... URL. +-- The owner is usually the index's `mcpp-res` organization, and may be +-- the library's own maintainer, who then keeps that mirror in step with +-- the GLOBAL asset (same bytes, same sha256). -- * `GLOBAL` must not itself point at the mcpp-res CN mirror -- -- Usage: lua5.4 tests/check_mirror_urls.lua @@ -41,8 +44,11 @@ local function check_url(ctx, u) if type(c) ~= "string" or c == "" then err(ctx .. ": url table missing non-empty CN") end - if type(c) == "string" and not c:match("^https://gitcode%.com/mcpp%-res/") then - err(ctx .. ": CN url must be under https://gitcode.com/mcpp-res/ (got " .. tostring(c) .. ")") + if type(c) == "string" + and not c:match("^https://gitcode%.com/[^/]+/[^/]+/releases/download/[^/]+/[^/]+$") then + err(ctx .. ": CN url must be a gitcode.com release asset, " + .. "https://gitcode.com///releases/download// (got " + .. tostring(c) .. ")") end if type(g) == "string" and g:match("^https://gitcode%.com/mcpp%-res/") then err(ctx .. ": GLOBAL url must not point at the CN mirror") diff --git a/tests/examples/yaml-cpp-v080/mcpp.toml b/tests/examples/yaml-cpp-v080/mcpp.toml new file mode 100644 index 00000000..36002c55 --- /dev/null +++ b/tests/examples/yaml-cpp-v080/mcpp.toml @@ -0,0 +1,11 @@ +# yaml-cpp 0.8.0 test project: the same assertions as tests/examples/yaml-cpp +# (0.9.0) minus the float formatting 0.9.0 added, plus a compile-time probe +# that 0.9.0's new header is absent, so a member silently resolving the newer +# version fails instead of passing. +# `compat` redirect inherited from the workspace root. +[package] +name = "yaml-cpp-v080-tests" +version = "0.1.0" + +[dependencies.compat] +yaml-cpp = "0.8.0" diff --git a/tests/examples/yaml-cpp-v080/tests/yaml.cpp b/tests/examples/yaml-cpp-v080/tests/yaml.cpp new file mode 100644 index 00000000..3484f982 --- /dev/null +++ b/tests/examples/yaml-cpp-v080/tests/yaml.cpp @@ -0,0 +1,201 @@ +// Behavioral test for compat.yaml-cpp 0.8.0 -- the same checks as +// tests/examples/yaml-cpp (0.9.0) minus the float formatting 0.9.0 added. +// +// Each check reaches symbols defined in the package's own .cpp files, so a +// package that compiled nothing cannot pass by being header-only by accident: +// +// YAML::Load, node access, conversions -> parse.cpp, node_data.cpp, convert.cpp +// anchors and aliases -> nodebuilder.cpp, singledocparser.cpp +// YAML::LoadAll -> parse.cpp, parser.cpp +// YAML::Emitter, YAML::Dump -> emitter.cpp, emitterstate.cpp, emit.cpp +// malformed input -> scanner.cpp, exceptions.cpp +// YAML::BuildGraphOfNextDocument -> contrib/graphbuilder*.cpp +// +// Returns non-zero on any mismatch. +#include +#include + +#include +#include +#include +#include + +// The package builds objects, not a shared library, so every yaml-cpp +// declaration this TU sees must be plain -- not dllimport. The package +// delivers that with a shim in front of yaml-cpp/dll.h, because no descriptor +// key carries a define to a consumer's TUs. On Linux the difference is only a +// visibility attribute and everything links anyway; on the MSVC ABI it is a +// link failure. Asserting it at compile time fails everywhere the moment the +// shim stops being reached. +#ifndef YAML_CPP_STATIC_DEFINE +# error "YAML_CPP_STATIC_DEFINE did not reach the consumer: the yaml-cpp/dll.h shim was not found first" +#endif + +// Proves this member resolved 0.8.0 and not the newest version: the header +// 0.9.0 added must not exist here. +#if __has_include() +# error "yaml-cpp/fptostring.h is present: this member did not resolve compat.yaml-cpp 0.8.0" +#endif + +namespace { + +const char *const kDoc = R"( +name: mcpp-index +version: 3 +ratio: 0.25 +enabled: true +tags: [yaml, cpp, "quoted, with comma"] +base: &base + host: localhost + port: 8080 +derived: + <<: *base + alias_of_base: *base +text: | + line one + line two +)"; + +bool loads_and_converts() { + YAML::Node root = YAML::Load(kDoc); + if (!root.IsMap()) return false; + if (root["name"].as() != "mcpp-index") return false; + if (root["version"].as() != 3) return false; + if (root["ratio"].as() != 0.25) return false; + if (!root["enabled"].as()) return false; + + const YAML::Node tags = root["tags"]; + if (!tags.IsSequence() || tags.size() != 3) return false; + if (tags[2].as() != "quoted, with comma") return false; + if (tags.as>().front() != "yaml") return false; + + if (root["text"].as() != "line one\nline two\n") return false; + + // A missing key is reported as undefined and a fallback applies; an + // explicit conversion of the wrong kind throws. + if (root["absent"].IsDefined()) return false; + if (root["absent"].as(42) != 42) return false; + try { + (void)root["name"].as(); + return false; + } catch (const YAML::BadConversion &) { + } + return true; +} + +bool resolves_aliases() { + YAML::Node root = YAML::Load(kDoc); + const YAML::Node alias = root["derived"]["alias_of_base"]; + if (alias["port"].as() != 8080) return false; + // An alias is the SAME node as its anchor, not a copy of it. + return alias.is(root["base"]); +} + +bool loads_every_document() { + const std::vector docs = YAML::LoadAll("a: 1\n---\nb: 2\n---\n- 3\n"); + return docs.size() == 3 && docs[0]["a"].as() == 1 + && docs[1]["b"].as() == 2 && docs[2][0].as() == 3; +} + +bool emits_and_round_trips() { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "list" << YAML::Value << YAML::Flow + << YAML::BeginSeq << 1 << 2 << 3 << YAML::EndSeq; + out << YAML::Key << "nested" << YAML::Value << YAML::BeginMap + << YAML::Key << "k" << YAML::Value << "v" << YAML::EndMap; + out << YAML::EndMap; + if (!out.good()) return false; + if (std::string(out.c_str()) != "list: [1, 2, 3]\nnested:\n k: v") return false; + + YAML::Node again = YAML::Load(out.c_str()); + if (again["list"][2].as() != 3) return false; + + // A node built in code and dumped comes back with the same content. + YAML::Node built; + built["x"] = 7; + built["y"].push_back("first"); + built["y"].push_back("second"); + YAML::Node back = YAML::Load(YAML::Dump(built)); + return back["x"].as() == 7 && back["y"].size() == 2 + && back["y"][1].as() == "second"; +} + +bool rejects_malformed_input() { + try { + (void)YAML::Load("key: [unterminated\nother: 1\n"); + } catch (const YAML::ParserException &e) { + // The mark points into the document rather than being left unset. + return e.mark.line >= 0 && !std::string(e.what()).empty(); + } + return false; +} + +} // namespace + +// Upstream declares GraphBuilderInterface's destructor pure virtual and +// defines it nowhere (0.8.0 and 0.9.0 alike), so every class derived from it +// -- upstream's own GraphBuilder included -- needs the definition from +// its user. This is that definition, as any consumer of the contrib API must +// write it. BuildGraphOfNextDocument below still comes only from +// src/contrib/graphbuilder.cpp, so the check still proves contrib was built. +YAML::GraphBuilderInterface::~GraphBuilderInterface() = default; + +namespace { + +// Counts what the contrib GraphBuilder is told, which only the two +// src/contrib/*.cpp translation units implement. +struct Counter : YAML::GraphBuilderInterface { + int scalars = 0, sequences = 0, maps = 0; + void *NewNull(const YAML::Mark &, void *) override { return this; } + void *NewScalar(const YAML::Mark &, const std::string &, void *, + const std::string &) override { + ++scalars; + return this; + } + void *NewSequence(const YAML::Mark &, const std::string &, void *) override { + ++sequences; + return this; + } + void AppendToSequence(void *, void *) override {} + void SequenceComplete(void *) override {} + void *NewMap(const YAML::Mark &, const std::string &, void *) override { + ++maps; + return this; + } + void AssignInMap(void *, void *, void *) override {} + void MapComplete(void *) override {} + void *AnchorReference(const YAML::Mark &, void *node) override { return node; } +}; + +bool builds_a_graph() { + std::istringstream in("a: [1, 2]\nb: {c: 3}\n"); + YAML::Parser parser(in); + Counter counter; + if (YAML::BuildGraphOfNextDocument(parser, counter) == nullptr) return false; + // keys a, b, c and values 1, 2, 3 + return counter.scalars == 6 && counter.sequences == 1 && counter.maps == 2; +} + +} // namespace + +int main() { + struct Check { + const char *name; + bool (*run)(); + } checks[] = { + {"loads_and_converts", loads_and_converts}, + {"resolves_aliases", resolves_aliases}, + {"loads_every_document", loads_every_document}, + {"emits_and_round_trips", emits_and_round_trips}, + {"rejects_malformed_input", rejects_malformed_input}, + {"builds_a_graph", builds_a_graph}, + }; + bool ok = true; + for (const Check &c : checks) { + const bool passed = c.run(); + std::printf("%s %s\n", passed ? "ok " : "FAIL", c.name); + ok = ok && passed; + } + return ok ? 0 : 1; +} diff --git a/tests/examples/yaml-cpp/mcpp.toml b/tests/examples/yaml-cpp/mcpp.toml new file mode 100644 index 00000000..4eadbd7e --- /dev/null +++ b/tests/examples/yaml-cpp/mcpp.toml @@ -0,0 +1,12 @@ +# yaml-cpp test project: consumes compat.yaml-cpp 0.9.0 and asserts behavior +# under `mcpp test` -- parsing, aliases, multi-document loads, emitting, parse +# errors, the contrib GraphBuilder and 0.9.0's shortest float formatting. +# tests/examples/yaml-cpp-v080 covers 0.8.0. Also measured on openkal +# (tests/openkal/members.toml). +# `compat` redirect inherited from the workspace root. +[package] +name = "yaml-cpp-tests" +version = "0.1.0" + +[dependencies.compat] +yaml-cpp = "0.9.0" diff --git a/tests/examples/yaml-cpp/tests/yaml.cpp b/tests/examples/yaml-cpp/tests/yaml.cpp new file mode 100644 index 00000000..cd9ee337 --- /dev/null +++ b/tests/examples/yaml-cpp/tests/yaml.cpp @@ -0,0 +1,208 @@ +// Behavioral test for compat.yaml-cpp 0.9.0. +// +// Each check reaches symbols defined in the package's own .cpp files, so a +// package that compiled nothing cannot pass by being header-only by accident: +// +// YAML::Load, node access, conversions -> parse.cpp, node_data.cpp, convert.cpp +// anchors and aliases -> nodebuilder.cpp, singledocparser.cpp +// YAML::LoadAll -> parse.cpp, parser.cpp +// YAML::Emitter, YAML::Dump -> emitter.cpp, emitterstate.cpp, emit.cpp +// malformed input -> scanner.cpp, exceptions.cpp +// YAML::BuildGraphOfNextDocument -> contrib/graphbuilder*.cpp +// YAML::FpToString -> fptostring.cpp (new in 0.9.0) +// +// Returns non-zero on any mismatch. +#include +#include +#include + +#include +#include +#include +#include + +// The package builds objects, not a shared library, so every yaml-cpp +// declaration this TU sees must be plain -- not dllimport. The package +// delivers that with a shim in front of yaml-cpp/dll.h, because no descriptor +// key carries a define to a consumer's TUs. On Linux the difference is only a +// visibility attribute and everything links anyway; on the MSVC ABI it is a +// link failure. Asserting it at compile time fails everywhere the moment the +// shim stops being reached. +#ifndef YAML_CPP_STATIC_DEFINE +# error "YAML_CPP_STATIC_DEFINE did not reach the consumer: the yaml-cpp/dll.h shim was not found first" +#endif + +namespace { + +const char *const kDoc = R"( +name: mcpp-index +version: 3 +ratio: 0.25 +enabled: true +tags: [yaml, cpp, "quoted, with comma"] +base: &base + host: localhost + port: 8080 +derived: + <<: *base + alias_of_base: *base +text: | + line one + line two +)"; + +bool loads_and_converts() { + YAML::Node root = YAML::Load(kDoc); + if (!root.IsMap()) return false; + if (root["name"].as() != "mcpp-index") return false; + if (root["version"].as() != 3) return false; + if (root["ratio"].as() != 0.25) return false; + if (!root["enabled"].as()) return false; + + const YAML::Node tags = root["tags"]; + if (!tags.IsSequence() || tags.size() != 3) return false; + if (tags[2].as() != "quoted, with comma") return false; + if (tags.as>().front() != "yaml") return false; + + if (root["text"].as() != "line one\nline two\n") return false; + + // A missing key is reported as undefined and a fallback applies; an + // explicit conversion of the wrong kind throws. + if (root["absent"].IsDefined()) return false; + if (root["absent"].as(42) != 42) return false; + try { + (void)root["name"].as(); + return false; + } catch (const YAML::BadConversion &) { + } + return true; +} + +bool resolves_aliases() { + YAML::Node root = YAML::Load(kDoc); + const YAML::Node alias = root["derived"]["alias_of_base"]; + if (alias["port"].as() != 8080) return false; + // An alias is the SAME node as its anchor, not a copy of it. + return alias.is(root["base"]); +} + +bool loads_every_document() { + const std::vector docs = YAML::LoadAll("a: 1\n---\nb: 2\n---\n- 3\n"); + return docs.size() == 3 && docs[0]["a"].as() == 1 + && docs[1]["b"].as() == 2 && docs[2][0].as() == 3; +} + +bool emits_and_round_trips() { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "list" << YAML::Value << YAML::Flow + << YAML::BeginSeq << 1 << 2 << 3 << YAML::EndSeq; + out << YAML::Key << "nested" << YAML::Value << YAML::BeginMap + << YAML::Key << "k" << YAML::Value << "v" << YAML::EndMap; + out << YAML::EndMap; + if (!out.good()) return false; + if (std::string(out.c_str()) != "list: [1, 2, 3]\nnested:\n k: v") return false; + + YAML::Node again = YAML::Load(out.c_str()); + if (again["list"][2].as() != 3) return false; + + // A node built in code and dumped comes back with the same content. + YAML::Node built; + built["x"] = 7; + built["y"].push_back("first"); + built["y"].push_back("second"); + YAML::Node back = YAML::Load(YAML::Dump(built)); + return back["x"].as() == 7 && back["y"].size() == 2 + && back["y"][1].as() == "second"; +} + +bool rejects_malformed_input() { + try { + (void)YAML::Load("key: [unterminated\nother: 1\n"); + } catch (const YAML::ParserException &e) { + // The mark points into the document rather than being left unset. + return e.mark.line >= 0 && !std::string(e.what()).empty(); + } + return false; +} + +} // namespace + +// Upstream declares GraphBuilderInterface's destructor pure virtual and +// defines it nowhere (0.8.0 and 0.9.0 alike), so every class derived from it +// -- upstream's own GraphBuilder included -- needs the definition from +// its user. This is that definition, as any consumer of the contrib API must +// write it. BuildGraphOfNextDocument below still comes only from +// src/contrib/graphbuilder.cpp, so the check still proves contrib was built. +YAML::GraphBuilderInterface::~GraphBuilderInterface() = default; + +namespace { + +// Counts what the contrib GraphBuilder is told, which only the two +// src/contrib/*.cpp translation units implement. +struct Counter : YAML::GraphBuilderInterface { + int scalars = 0, sequences = 0, maps = 0; + void *NewNull(const YAML::Mark &, void *) override { return this; } + void *NewScalar(const YAML::Mark &, const std::string &, void *, + const std::string &) override { + ++scalars; + return this; + } + void *NewSequence(const YAML::Mark &, const std::string &, void *) override { + ++sequences; + return this; + } + void AppendToSequence(void *, void *) override {} + void SequenceComplete(void *) override {} + void *NewMap(const YAML::Mark &, const std::string &, void *) override { + ++maps; + return this; + } + void AssignInMap(void *, void *, void *) override {} + void MapComplete(void *) override {} + void *AnchorReference(const YAML::Mark &, void *node) override { return node; } +}; + +bool builds_a_graph() { + std::istringstream in("a: [1, 2]\nb: {c: 3}\n"); + YAML::Parser parser(in); + Counter counter; + if (YAML::BuildGraphOfNextDocument(parser, counter) == nullptr) return false; + // keys a, b, c and values 1, 2, 3 + return counter.scalars == 6 && counter.sequences == 1 && counter.maps == 2; +} + +// 0.9.0 formats floating point by the shortest representation that reads +// back to the same value; 0.8.0 printed max_digits10 digits +// ("0.10000000000000001"). The emitter uses the same routine. +bool formats_floats_shortest() { + if (YAML::FpToString(0.1) != "0.1") return false; + if (YAML::FpToString(1.5f) != "1.5") return false; + YAML::Emitter out; + out << 0.1; + return std::string(out.c_str()) == "0.1"; +} + +} // namespace + +int main() { + struct Check { + const char *name; + bool (*run)(); + } checks[] = { + {"loads_and_converts", loads_and_converts}, + {"resolves_aliases", resolves_aliases}, + {"loads_every_document", loads_every_document}, + {"emits_and_round_trips", emits_and_round_trips}, + {"rejects_malformed_input", rejects_malformed_input}, + {"builds_a_graph", builds_a_graph}, + {"formats_floats_shortest", formats_floats_shortest}, + }; + bool ok = true; + for (const Check &c : checks) { + const bool passed = c.run(); + std::printf("%s %s\n", passed ? "ok " : "FAIL", c.name); + ok = ok && passed; + } + return ok ? 0 : 1; +} diff --git a/tests/openkal/compat.py b/tests/openkal/compat.py index fbeb0032..10faead7 100644 --- a/tests/openkal/compat.py +++ b/tests/openkal/compat.py @@ -7,7 +7,7 @@ compat.py run [--member NAME ...] [--target TRIPLE ...] --out FILE compat.py check --results FILE [--baseline FILE] [--members NAME ...] - compat.py select FILE ... + compat.py select [--base REF] FILE ... `run` copies each selected member of tests/examples into tests/openkal-work, adds the openkal C++ runtime named by pins.toml, and builds with the pinned @@ -41,8 +41,11 @@ design.md §7, §12 decision 5) -- and is not computed here. `select` reads changed file paths and prints the members to measure: every -listed member when the openkal family or this directory changed, otherwise the -members whose test projects depend on a changed descriptor. +listed member when the graph or the harness changed (an openkal family +descriptor, pins.toml, compat.py), otherwise only the members the change names +-- an entry added or changed in members.toml (compared with `--base`), a +member's own test project, or a descriptor a member's test project depends on. +See `select_members`. `check` compares a results file with a baseline and fails when a member that the baseline records as `runs` or `builds` for a target is recorded lower. It @@ -387,7 +390,62 @@ def cmd_check(args: argparse.Namespace) -> int: return 1 if (regressions or contradicted) else 0 -FAMILY_PREFIXES = ("pkgs/o/openkal", "pkgs/s/std-freestanding-alloc-kal", "tests/openkal/") +# A change to any of these changes the graph every member is measured in, or +# the harness that measures it, so it selects every listed member. +FAMILY_PREFIXES = ("pkgs/o/openkal", "pkgs/s/std-freestanding-alloc-kal") +MEMBERS_FILE = "tests/openkal/members.toml" + + +def members_changed(now: dict, base: dict) -> set[str]: + """The members whose entry in members.toml differs from the base: added, + re-described, or with a `[not-portable]` declaration added, changed or + removed. `[excluded]` selects nothing -- an excluded member is not + measured -- and a removed member has nothing left to measure.""" + changed = set() + now_m, base_m = now.get("members", {}), base.get("members", {}) + for m in now_m: + if m not in base_m or now_m[m] != base_m[m]: + changed.add(m) + now_np, base_np = now.get("not-portable", {}), base.get("not-portable", {}) + for m in set(now_np) | set(base_np): + if now_np.get(m) != base_np.get(m) and m in now_m: + changed.add(m) + return changed + + +def select_members(changed: list[str], listed: list[str], packages: dict, + members_now: dict, members_base: dict | None) -> list[str]: + """What a change can affect, in listed order. + + Every listed member, when the change reaches all of them: an openkal + family descriptor (the graph), `pins.toml` (the graph's versions), + `compat.py` or anything else under tests/openkal/ (the harness), or + members.toml with no base to compare it against. + + Otherwise only the members the change names: a member whose members.toml + entry changed (`members_changed`), a member whose own test project under + tests/examples/ changed, and a member whose test project depends on a + changed descriptor. Adding one member measures that member, not the list. + """ + chosen: set[str] = set() + ids = set() + for p in changed: + if p.startswith(FAMILY_PREFIXES): + return list(listed) + if p == MEMBERS_FILE: + if members_base is None: + return list(listed) + chosen |= members_changed(members_now, members_base) + elif p.startswith("tests/openkal/"): + return list(listed) + elif p.startswith("tests/examples/"): + chosen.add(p[len("tests/examples/"):].split("/", 1)[0]) + elif p.startswith("pkgs/") and p.endswith(".lua"): + ids.add(descriptor_id(p)) + for member in listed: + if ids & set(packages.get(member, [])): + chosen.add(member) + return [m for m in listed if m in chosen] def descriptor_id(path: str) -> str: @@ -403,18 +461,21 @@ def descriptor_id(path: str) -> str: def cmd_select(args: argparse.Namespace) -> int: - listed = list(load_toml(os.path.join(HERE, "members.toml")).get("members", {}).keys()) + members_now = load_toml(os.path.join(HERE, "members.toml")) + listed = list(members_now.get("members", {}).keys()) changed = [p.strip() for p in args.files if p.strip()] - if any(p.startswith(FAMILY_PREFIXES) for p in changed): - print(" ".join(listed)) - return 0 - ids = {descriptor_id(p) for p in changed if p.startswith("pkgs/") and p.endswith(".lua")} - chosen = [] - for member in listed: - manifest = load_toml(os.path.join(EXAMPLES, member, "mcpp.toml")) - if ids & set(packages_of(manifest)): - chosen.append(member) - print(" ".join(chosen)) + # The base's members.toml, so that adding a member measures that member. + # Without --base, or when the base has no such file, a change to it + # selects every member, which is what it did before --base existed. + members_base = None + if args.base: + proc = subprocess.run(["git", "show", f"{args.base}:{MEMBERS_FILE}"], + cwd=ROOT, capture_output=True, text=True) + if proc.returncode == 0 and tomllib is not None: + members_base = tomllib.loads(proc.stdout) + packages = {m: packages_of(load_toml(os.path.join(EXAMPLES, m, "mcpp.toml"))) + for m in listed} + print(" ".join(select_members(changed, listed, packages, members_now, members_base))) return 0 @@ -451,7 +512,7 @@ def cmd_selftest(_args: argparse.Namespace) -> int: classify_failure(ran, False)["status"], "fails"), # `[not-portable]` is per (member, target), and reading it per member # would take a working cell out of the figure along with the broken - # one --- `cmp-module` runs on x86_64-windows-gnu. + # one --- `cmp-module` runs on x86_64-windows-musl. ("a declaration is read for the target it names", declared_not_portable({"m": {"t1": "why"}}, "m", "t1"), "why"), ("and not for a target it does not name", @@ -466,13 +527,49 @@ def cmd_selftest(_args: argparse.Namespace) -> int: ["mcpp", "test", "--toolchain", "llvm@22.1.8", "--target", "aarch64-macos", "--no-run"]), ("a target with a runner runs them", - command_for("x86_64-windows-gnu", "llvm@22.1.8", False, True), + command_for("x86_64-windows-musl", "llvm@22.1.8", False, True), ["mcpp", "test", "--toolchain", "llvm@22.1.8", - "--target", "x86_64-windows-gnu"]), + "--target", "x86_64-windows-musl"]), ("the host names no target and runs them", command_for("x86_64-linux-gnu", "llvm@22.1.8", True, True), ["mcpp", "test", "--toolchain", "llvm@22.1.8"]), ] + + # `select` decides how much of a pull request's four-hour matrix runs. It + # used to measure every member for any change under tests/openkal/, which + # made adding one member cost the whole list. + listed = ["a", "b", "c"] + packages = {"a": ["compat.x"], "b": ["compat.y"], "c": []} + base = {"members": {"a": "A", "b": "B"}} + now = {"members": {"a": "A", "b": "B", "c": "C"}} + sel = lambda changed, n=now, b=base: select_members(changed, listed, packages, n, b) + cases += [ + ("adding a member measures that member", + sel([MEMBERS_FILE]), ["c"]), + ("re-describing a member measures it", + sel([MEMBERS_FILE], {"members": {"a": "A2", "b": "B"}}), ["a"]), + ("a not-portable declaration measures the member it names", + sel([MEMBERS_FILE], {"members": base["members"], + "not-portable": {"b": {"t": "why"}}}), ["b"]), + ("an exclusion alone measures nothing", + sel([MEMBERS_FILE], {"members": base["members"], "excluded": {"z": "why"}}), []), + ("members.toml with no base measures every member", + sel([MEMBERS_FILE], now, None), listed), + ("a pin measures every member", + sel(["tests/openkal/pins.toml"]), listed), + ("the harness measures every member", + sel(["tests/openkal/compat.py"]), listed), + ("the scheduled run's directory measures every member", + sel(["tests/openkal/"]), listed), + ("an openkal family descriptor measures every member", + sel(["pkgs/o/openkal-musl.lua"]), listed), + ("a descriptor measures the members that depend on it", + sel(["pkgs/c/compat.y.lua"]), ["b"]), + ("a member's test project measures that member", + sel(["tests/examples/a/tests/t.cpp"]), ["a"]), + ("an unlisted test project measures nothing", + sel(["tests/examples/zzz/mcpp.toml"]), []), + ] bad = 0 for name, got, want in cases: ok = got == want @@ -496,6 +593,7 @@ def main() -> int: check.add_argument("--baseline", required=True) check.add_argument("--members", nargs="*") select = sub.add_parser("select") + select.add_argument("--base", help="git ref whose members.toml the change is compared with") select.add_argument("files", nargs="*") sub.add_parser("selftest") args = parser.parse_args() diff --git a/tests/openkal/members.toml b/tests/openkal/members.toml index e6dc9b1a..1e073a64 100644 --- a/tests/openkal/members.toml +++ b/tests/openkal/members.toml @@ -8,6 +8,8 @@ cjson = "JSON parsing, C" yyjson = "JSON parsing, C" "nlohmann.json" = "JSON, header-only C++" +yaml-cpp = "YAML parsing and emitting, C++ (0.9.0)" +yaml-cpp-v080 = "YAML parsing and emitting, C++ (0.8.0)" expat = "XML parsing, C" pcre2 = "regular expressions, C" re2 = "regular expressions, C++" @@ -42,7 +44,7 @@ cmp-module = "coroutines, C++ modules" # excuse, and this project has shipped one of those before. # # DISTINCT FROM `[excluded]` BELOW, which is whole-member. `cmp-module` runs on -# `x86_64-windows-gnu`; excluding it would discard a result that is true in +# `x86_64-windows-musl`; excluding it would discard a result that is true in # order to hide one that is also true. # # THE BAR IS "NO MANIFEST KEY REACHES IT". A generated configuration header @@ -56,7 +58,7 @@ cmp-module = "coroutines, C++ modules" # CURL_VERSION_SSL is clear, because "a curl built with no SSL still links and # still runs, it just silently cannot do https". x86_64-linux-gnu = "curl's TLS backend here is compat.openssl, which builds through OpenSSL's own Configure from an install() hook and is therefore a glibc artefact: it declares provides = [\"platform-sdk\"] and fails this graph's link at setcontext and __isoc23_strtol, which musl does not have. Building OpenSSL inside the graph would change this cell." -x86_64-windows-gnu = "curl's Windows configuration is Schannel plus the checked-in lib/config-win32.h, and both are selected by _WIN32, which a C library presenting POSIX on Windows does not define. With neither HAVE_CONFIG_H nor _WIN32, curl_setup.h reads no configuration at all and stops at curl_setup.h:591 'too small curl_off_t', which names a consequence and not the cause. compat.openssl has no Windows build, so there is no second TLS provider to fall back to." +x86_64-windows-musl = "curl's Windows configuration is Schannel plus the checked-in lib/config-win32.h, and both are selected by _WIN32, which a C library presenting POSIX on Windows does not define. With neither HAVE_CONFIG_H nor _WIN32, curl_setup.h reads no configuration at all and stops at curl_setup.h:591 'too small curl_off_t', which names a consequence and not the cause. compat.openssl has no Windows build, so there is no second TLS provider to fall back to." [not-portable.cmp-module] x86_64-linux-gnu = "asio's detail/config.hpp:899 includes whenever __linux__ is defined, OUTSIDE every ASIO_DISABLE_* guard, so no configuration macro prevents it. openkal runs on the Linux kernel, so __linux__ is correct; the uapi headers glibc's userspace installs beside it are not part of what openkal presents." diff --git a/tests/openkal/pins.toml b/tests/openkal/pins.toml index 907af142..4d8ca314 100644 --- a/tests/openkal/pins.toml +++ b/tests/openkal/pins.toml @@ -116,9 +116,14 @@ toolchain = "llvm@22.1.8" # reports it as a compatibility result. mcpp = "2026.9.21.3" -targets = ["x86_64-linux-gnu", "x86_64-windows-gnu"] +# The Windows target is named for the C environment the graph presents there, +# which is musl's (openkal-musl declares `[c-abi] presents = "posix"`), not +# MinGW's: `x86_64-windows-gnu` read as a mingw-w64 build, which this graph +# has never been. Results measured before 2026-09-23 are recorded under that +# older name. +targets = ["x86_64-linux-gnu", "x86_64-windows-musl"] # A target that is not the host is tested through its runner when the runner is # on PATH, and built otherwise. [runners] -x86_64-windows-gnu = ["wine"] +x86_64-windows-musl = ["wine"]