Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/openkal-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,45 @@ 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.<member>]` 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 `<linux/version.h>` 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.

### 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(<linux/tcp.h>)`. 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
Expand Down
17 changes: 17 additions & 0 deletions docs/zh/openkal-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@

`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 `<linux/version.h>`,而那行在所有 `ASIO_DISABLE_*` 守卫之外。整个排除掉这个成员,等于为了藏起一个真的结果而丢掉另一个同样真的结果。

**门槛是「没有任何清单键伸得进去」。** 上游源码在预处理期发问,算;本索引自己生成的配置头,不算,那属于配方。`curl` 的 `linux/tcp.h` 就是后者——`pkgs/c/compat.curl.lua` 在 `#if defined(__linux__)` 里写了 `#define HAVE_LINUX_TCP_H 1`,把一个关于内核的正确事实读成了关于「装了哪些 userspace 头」的断言。

**这个格子照常测量,而 `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(<linux/tcp.h>)`。同一个块里还有 `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` 变化时测量全部成员,否则测量依赖了被修改描述符的成员。除非启用下文的比较,它不阻止合并。
Expand Down
61 changes: 57 additions & 4 deletions tests/openkal/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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', '')}"
Expand Down Expand Up @@ -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/")
Expand Down Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions tests/openkal/members.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <linux/version.h> 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.
Expand Down
Loading