From 630ab816a617e0c2f345f80a7f1701fb46dc3c93 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 21 Sep 2026 04:22:36 +0800 Subject: [PATCH 1/2] a per-target declaration that stays falsifiable `[excluded]` is whole-member, and some members are neither measured-and-broken nor excludable. `cmp-module` RUNS on `x86_64-windows-gnu` and cannot build on `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. Excluding the member outright would discard a result that is true in order to hide one that is also true. `[not-portable.]` declares one target of one member, with the reason and the evidence. THE BAR IS THAT NO MANIFEST KEY REACHES IT. Upstream source asking in the preprocessor qualifies. A generated configuration header this index writes does not: `curl`'s `linux/tcp.h` is `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)` in `pkgs/c/compat.curl.lua`, which reads a correct fact about the kernel as a claim about which userspace headers are installed. That is a recipe defect, the same shape as expat's `arc4random_buf`, and it belongs in the recipe. The two were one item in the plan; they are not one thing. THE DECLARATION STAYS FALSIFIABLE. The cell is measured anyway and `compat.py check` FAILS when a declared cell builds: contradicted declaration: cmp-module on x86_64-linux-gnu: declared not-portable and recorded runs -- remove the declaration Measured both ways before it was committed: red on a synthetic result recording `runs`, green on one recording `fails`. A declaration that removes a cell from the figure on the strength of a sentence, and that nothing can contradict, is a permanent excuse; the cost of keeping it honest is a build that was already being paid for. `selftest` gains three legs pinning that the table is read per (member, target) and not per member --- reading it per member is exactly the mistake that would take `cmp-module`'s working Windows cell out of the figure. --- docs/openkal-compat.md | 21 +++++++++++++ docs/zh/openkal-compat.md | 6 ++++ tests/openkal/compat.py | 61 +++++++++++++++++++++++++++++++++++--- tests/openkal/members.toml | 17 +++++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/docs/openkal-compat.md b/docs/openkal-compat.md index 82eda1dc..47ea1c75 100644 --- a/docs/openkal-compat.md +++ b/docs/openkal-compat.md @@ -101,6 +101,27 @@ question is about a target rather than the package as a whole. that cannot be built in any openkal graph, each with its reason; a member that 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 +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 +that is true in order to hide one that is also true. + +**The bar is that no manifest key reaches it.** Upstream source asking in the +preprocessor qualifies; a generated configuration header this index writes +does not, and belongs in the recipe instead. `curl`'s `linux/tcp.h` is the +second kind — `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)` in +`pkgs/c/compat.curl.lua`, which reads a correct fact about the kernel as a +claim about which userspace headers are installed. + +**The cell is measured anyway, and `compat.py check` fails if it builds.** A +declaration that takes a cell out of the figure on the strength of a sentence +has to stay falsifiable; one that nothing can contradict is a permanent +excuse. The cost is a build that was already being paid for before the +declaration existed. + ## 3. When it runs `.github/workflows/openkal-compat.yml` runs weekly and on demand, measuring every diff --git a/docs/zh/openkal-compat.md b/docs/zh/openkal-compat.md index a459f8b9..2d582e52 100644 --- a/docs/zh/openkal-compat.md +++ b/docs/zh/openkal-compat.md @@ -35,6 +35,12 @@ `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_*` 守卫之外。整个排除掉这个成员,等于为了藏起一个真的结果而丢掉另一个同样真的结果。 + +**门槛是「没有任何清单键伸得进去」。** 上游源码在预处理期发问,算;本索引自己生成的配置头,不算,那属于配方。`curl` 的 `linux/tcp.h` 就是后者——`pkgs/c/compat.curl.lua` 在 `#if defined(__linux__)` 里写了 `#define HAVE_LINUX_TCP_H 1`,把一个关于内核的正确事实读成了关于「装了哪些 userspace 头」的断言。 + +**这个格子照常测量,而 `compat.py check` 在它构建成功时会红。** 一条凭一句话把格子移出统计的声明必须保持**可证伪**;没有任何东西能反驳的声明就是一张永久豁免。代价是一次在声明存在之前本来就要付的构建。 + ## 3. 何时运行 `.github/workflows/openkal-compat.yml` 每周运行、可手动触发,测量全部列出的成员。对 PR,openkal 家族或 `tests/openkal` 变化时测量全部成员,否则测量依赖了被修改描述符的成员。除非启用下文的比较,它不阻止合并。 diff --git a/tests/openkal/compat.py b/tests/openkal/compat.py index efe497ea..77d07cc7 100644 --- a/tests/openkal/compat.py +++ b/tests/openkal/compat.py @@ -45,7 +45,10 @@ `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 -is the regression guard for labels that have been published. +is the regression guard for labels that have been published. It ALSO fails +when a cell declared `[not-portable]` in members.toml built: a declaration +that removes a cell from the figure has to stay falsifiable, or it is a +permanent excuse nothing can contradict. """ from __future__ import annotations @@ -126,6 +129,17 @@ def platform_bound(manifest: dict) -> bool: return False +def declared_not_portable(decls: dict, member: str, target: str) -> str | None: + """The reason `members.toml` gives for this (member, target) being + unbuildable by construction, or None. + + PER TARGET, NOT PER MEMBER. `[excluded]` already covers a member that + cannot be built in any openkal graph; this covers one that builds on some + targets and cannot on another, where excluding the member outright would + discard a result that is true.""" + return ((decls.get(member) or {}).get(target)) or None + + def kind_of(manifest: dict, status: str) -> str | None: """The platform-relation label for one (member, target) result -- see the `posix` / `platform` table in this module's docstring, which this @@ -269,7 +283,12 @@ def cmd_run(args: argparse.Namespace) -> int: "pins": {k: v for k, v in pins.items() if k in ("runtime", "toolchain", "mcpp")}, "members": {}, "excluded": members_file.get("excluded", {}), + # CARRIED INTO THE RESULTS so the consumer that computes the figure + # can take these cells out of the denominator, and so a reader of the + # results file can see WHY without opening another file. + "not_portable": members_file.get("not-portable", {}), } + not_portable = members_file.get("not-portable", {}) for member in members: manifest = load_toml(os.path.join(EXAMPLES, member, "mcpp.toml")) entry = {"packages": packages_of(manifest), @@ -284,6 +303,13 @@ def cmd_run(args: argparse.Namespace) -> int: kind = kind_of(manifest, entry["targets"][target]["status"]) if kind: entry["targets"][target]["kind"] = kind + # MEASURED ANYWAY. Skipping the build would make the declaration + # unfalsifiable, and the cost is a cell that was going to be + # measured before it was declared. + why = declared_not_portable(not_portable, member, target) + if why: + entry["targets"][target]["declared"] = "not-portable" + entry["targets"][target]["declared_reason"] = why print(f" {entry['targets'][target]['status']}" + (f" ({kind})" if kind else "") + (f": {entry['targets'][target].get('diagnostic', '')}" @@ -312,11 +338,29 @@ def cmd_check(args: argparse.Namespace) -> int: if got < was: regressions.append(f"{member} on {target}: {rec['status']} -> " f"{((now or {}).get('targets', {}).get(target) or {}).get('status', 'absent')}") + # A DECLARATION THAT NOTHING CAN CONTRADICT IS A PERMANENT EXCUSE. + # `[not-portable]` takes a cell out of the figure on the strength of a + # sentence, and the only thing that keeps the sentence honest is failing + # here when the cell builds. The cell is measured for this reason alone. + contradicted = [] + decls = load_toml(os.path.join(HERE, "members.toml")).get("not-portable", {}) + for member, targets in decls.items(): + if args.members and member not in args.members: + continue + for target in targets: + rec = ((current.get("members", {}).get(member) or {}) + .get("targets", {}).get(target) or {}) + if RANK.get(rec.get("status"), 0) > 0: + contradicted.append( + f"{member} on {target}: declared not-portable and " + f"recorded {rec.get('status')} -- remove the declaration") for line in regressions: print(f"regression: {line}") - if not regressions: - print("no published label regressed") - return 1 if regressions else 0 + for line in contradicted: + print(f"contradicted declaration: {line}") + if not regressions and not contradicted: + print("no published label regressed, and no declaration was contradicted") + return 1 if (regressions or contradicted) else 0 FAMILY_PREFIXES = ("pkgs/o/openkal", "pkgs/s/std-freestanding-alloc-kal", "tests/openkal/") @@ -381,6 +425,15 @@ def cmd_selftest(_args: argparse.Namespace) -> int: classify_failure(ran, True)["status"], "builds"), ("the same output without a runner is not evidence it built", 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. + ("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", + declared_not_portable({"m": {"t1": "why"}}, "m", "t2"), None), + ("nor for another member", + declared_not_portable({"m": {"t1": "why"}}, "n", "t1"), None), ] bad = 0 for name, got, want in cases: diff --git a/tests/openkal/members.toml b/tests/openkal/members.toml index c2bba69b..1d9172d4 100644 --- a/tests/openkal/members.toml +++ b/tests/openkal/members.toml @@ -36,6 +36,23 @@ curl = "HTTP client, C, sockets" tinyhttps = "HTTPS client, C++ modules, sockets" cmp-module = "coroutines, C++ modules" +# A member that cannot be built FOR ONE TARGET by construction, with the reason +# and the evidence. It is measured anyway, and `compat.py check` FAILS if a +# declared cell builds --- a declaration nothing can contradict is a permanent +# 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 +# order to hide one that is also true. +# +# THE BAR IS "NO MANIFEST KEY REACHES IT". A generated configuration header +# this index writes is a RECIPE DEFECT and belongs in the recipe, not here --- +# `curl`'s `linux/tcp.h` is exactly that, `#define HAVE_LINUX_TCP_H 1` inside +# `#if defined(__linux__)` in `pkgs/c/compat.curl.lua`. What qualifies is +# upstream source that asks in the preprocessor where no key can answer. +[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." + # Members that are not measured, each with the reason. A member belongs here # when it cannot be built in any openkal graph, not when it fails: a failure is # measured and published. From f93d5600c5f92e192e5ff41a56931117e0e6dc54 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 21 Sep 2026 04:24:07 +0800 Subject: [PATCH 2/2] curl's two failures have two different causes, and neither is unportability Both are recipe defects. linux `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)`. The kernel IS Linux, so the predicate is right; reading it as "glibc's userspace headers are installed" is not. The honest test is `__has_include()`. The same block also asserts `HAVE_GLIBC_STRERROR_R`, false over musl. windows the recipe's `windows` branch omits HAVE_CONFIG_H so 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; 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 this index had until openkal presented POSIX on Windows, and mcpp has the predicate for the question actually being asked --- `cfg(c-abi = "musl")`. Recorded rather than fixed here: it also needs this index's OpenSSL over the same environment, so it is a larger change than the first. --- docs/openkal-compat.md | 18 ++++++++++++++++++ docs/zh/openkal-compat.md | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/openkal-compat.md b/docs/openkal-compat.md index 47ea1c75..691a179d 100644 --- a/docs/openkal-compat.md +++ b/docs/openkal-compat.md @@ -122,6 +122,24 @@ has to stay falsifiable; one that nothing can contradict is a permanent excuse. The cost is a build that was already being paid for before the declaration existed. +### curl's two failures have two different causes + +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. | + +**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 +this index had until openkal presented POSIX on Windows, and mcpp has the +predicate for the question actually being asked — `cfg(c-abi = "musl")` +(mcpp docs/22, "Adaptation To The Resolved Target Side"). Selecting the +generated POSIX configuration there, rather than the checked-in Win32 one, is +the shape; it also needs this index's OpenSSL over the same environment, so +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 diff --git a/docs/zh/openkal-compat.md b/docs/zh/openkal-compat.md index 2d582e52..24876d09 100644 --- a/docs/zh/openkal-compat.md +++ b/docs/zh/openkal-compat.md @@ -41,6 +41,17 @@ **这个格子照常测量,而 `compat.py check` 在它构建成功时会红。** 一条凭一句话把格子移出统计的声明必须保持**可证伪**;没有任何东西能反驳的声明就是一张永久豁免。代价是一次在声明存在之前本来就要付的构建。 +### curl 的两条失败是两个不同的真因 + +两条都是配方缺陷,而且不是同一个缺陷: + +| 目标 | 首条诊断 | 真因 | +| --- | --- | --- | +| `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。 | + +**第二条才是有意思的那条:配方按「平台」分支,而问题问的是「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` 变化时测量全部成员,否则测量依赖了被修改描述符的成员。除非启用下文的比较,它不阻止合并。