Skip to content

Resolve the target side once, and let every translation unit read it - #515

Merged
Sunrisepeak merged 20 commits into
mainfrom
feat/target-side-reaches-every-unit
Aug 27, 2026
Merged

Resolve the target side once, and let every translation unit read it#515
Sunrisepeak merged 20 commits into
mainfrom
feat/target-side-reaches-every-unit

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 27, 2026

Copy link
Copy Markdown
Member

Closes #514. Also supplies the engine facility openkal-musl#13 had named in its own source as the one it needed.

Full analysis: .agents/docs/2026-08-27-openkal-native-path-three-issues.md, carried by this pull request.

In one sentence

mcpp had already computed which directories hold the C++ ABI headers this target is built against, and delivered that set accurately — to one translation unit. Every other unit in the graph received it only if it happened to stand on a dependency edge.

This is the next layer of the family 2026.8.25.x and 2026.8.26.1 belong to. Those fixed a predicate that asked the wrong question and an answer that reached no decision. Here the answer reached one consumer and it has five.

Six defects

① The compile-side predicate The twin of the one 2026.8.26.1 fixed on the link side. !crossTargetFlag.empty() becomes plan.targetSide.cAbi.prebuilt()
② A vocabulary mismatch in payload directory names include/<triple>/c++/v1 is spelled in LLVM's vocabulary and looked up in mcpp's, and if (exists) makes the miss silent
③ The target side reached one unit mcpp#514 §A. A graph-supplied layer's publicUsage now merges into every package's privateBuild
④ The cache key described the compiler, not the header set mcpp#514 §B. A new targetHeaderSet axis
⑤ A fourth derivation of the home active_home_xpkgs() omitted self-contained installs; find_sibling_package fell back to ~/.xlings unconditionally
⑥ Two vocabularies for a payload version A binding names the version DECLARED; a directory is named after what that request RESOLVED to
New [build] private_include_dirs — a package may distinguish what it is built from from what it publishes

① measured

Same machine, same compiler, same target, differing only in whether it was spelled out:

ldflags  line:  token for token identical      <- #511 fixed this side
cxxflags line:  the explicit one lost six tokens:
  --no-default-config / -nostdinc++ / payload libc++ x2 / glibc / linux-headers

Headers from one library, objects linked from another.

--no-default-config is split out of that condition and emitted unconditionally. It is not part of the payload's header set, and a cfg file is by post_install.cppm's own words "a per-machine, per-install-path artifact". This also explains why the reporter's hand-written <triple>-clang++.cfg worked around #514: it was read only because this token had gone missing.

e2e 295 states exactly this invariant and compared ^ldflags alone, so the identity failed on the next line and the test could not see it. It now compares both.

② measured

include/<triple>/c++/v1 holds one file: __config_site. Its absence reads as a broken payload, and this repository has misdiagnosed it more than once.

⑥ measured

Downloading xim:glibc@2.44
error: selected RuntimeBinding glibc@2.44.2 requires payload '.../2.44.2', but it is not installed

The criterion is refinement, not a pick by directory order: components are compared component-wise, so 2.4 does not answer 2.44; two payloads both refining one request yield nothing; and the reverse direction is refused, because accepting a payload older than the request would silently use the one the index states is not the one to install.

Criteria

unit 93 passed / 0 failed; 3 added, each confirmed to actually run — the first draft of the hostflags test SKIPPED for want of a real cfg, and now carries a fixture with ASSERT that it took effect
e2e 295 extended to cxxflags; four checks across two toolchains; exit 1 on the unfixed binary, listing the six lost tokens
e2e 304 (new) the target side reaches a sibling dependency package — red on the unfixed binary. Asserts the reverse too: a non-layer package's directory must not spread
e2e 305 (new) built-from and published are separable, globs included — red on the unfixed binary
denominators all three assert CDB row counts ≥ 1, so an empty CDB cannot pass

What the pre-merge review found

Four further defects, all mine, each with an A/B showing the criterion goes red without the fix:

The cache key baked in this machine's home The axis comment promised it would survive a different MCPP_HOME; a <store>-only rule cannot deliver that, because the ordinary toolchain's only compile token is --sysroot=<home>/registry/subos/default. Both existing tests set the axis directly, so neither exercised the relativization at all.
An empty version request returned the package root root / "" is root, and it is a directory. Both callers reject an empty version before arriving — which is exactly why nothing would have caught it.
The two new keys meet in one package and nothing tested it openkal-musl is that shape. Removing the key in the fixture put the overlay onto the sibling package and the root.
The CHANGELOG omitted ⑥ entirely The defect users meet first on a clean machine.

Ecosystem cross-validation

Nine repositories, each carrying implementation rather than a CI change, all building mcpp from this branch:

openkal#13 five interfaces, SPEC clauses 3/6.3/11, SURFACE, conformance, and openkal-kit
openkal-linux#15 all five implemented, plus the three additions to openkal.process
openkal-macos#11 · openkal-windows#13 terminal and channel
openkal-opensbi#11 · openkal-uefi#8 all five declined under clause 6.1, each with a reason
openkal-musl#15 permanent absence becomes a link error; umask; copy_file_range; pipe
openkal-llvm-runtime#7 __config_site reconciliation, and the cross-validation this repository never had

Green is not the criterion; the under review: line is — checked per job.

Compatibility

  • kCacheEpoch is not bumped. An older entry is a miss, not a hazard: inputs_match compares inputs.toolchain as one object.
  • private_include_dirs degrades on an older engine. Measured on 2026.8.26.2: silently accepted in a dependency's manifest, a warning in a root manifest, exit 0 either way. The one place it does not hold is a published xim descriptor's target_cfg, where an unrecognised sub-key is fatal — documented.
  • Toolchain::cAbiPrebuilt defaults to true, so every host-targeting build behaves exactly as before.
  • The xlings pin is deliberately not moved. Bumping it was attempted and reverted with the measurement: the bootstrap engine installs glibc@2.44 while the newly-pinned vendored xlings computes the binding as 2.44.2, and the request then exceeds what is installed. It is self-healing in one order only, so it belongs in the release after this one — the rule the index states in its own source: the consumer ships first.

⭐ 与 2026.8.25.x / 2026.8.26.1 是同一族的下一层。那两批修的是「谓词问错了」
与「答案没接到决定上」;这一批里,答案接上了一个消费者,而它有五个。

## ⭐⭐ 编译侧的谓词是 2026.8.26.1 在链接侧修掉的那条的孪生兄弟

`hostflags.cppm` 问 `!crossTargetFlag.empty()`——「命令行上有没有 `--target=`」——
而它自己的注释写的是「目标侧来自图」。同机、同编译器、同目标,只差写不写
`--target`,编译线少六个 token:

    --no-default-config  -nostdinc++
    -isystem <payload>/include/c++/v1
    -isystem <payload>/include/<triple>/c++/v1
    -isystem <glibc>/include
    -isystem <linux-headers>/include

⇒ 头文件来自一个库,目标文件链自另一个库。两侧现在读同一个
`plan.targetSide.cAbi.prebuilt()`,由 prepare 记在 `Toolchain` 上供三个生产者共用。

⚠️ `--no-default-config` 从该条件里拆出来无条件发:它不是载荷头文件集合的一部分,
而 cfg 按 post_install.cppm 自己的说法是「per-machine, per-install-path artifact」。
这也解释了报告者那个手写 `<triple>-clang++.cfg` 的 workaround 为什么有效。

⚠️ e2e 295 写的就是这条恒等式,而它只比对 `^ldflags` ⇒ 恒等式在下一行不成立而测试
看不见。现在两条都比,并断言两侧非空。

## ⭐⭐ 载荷目录名是 LLVM 词汇,查找用的是 mcpp 词汇,且失配静默

`include/<triple>/c++/v1` 由 LLVM 的构建写下(`x86_64-unknown-linux-gnu`),
`tc.targetTriple` 是 mcpp 的(`x86_64-linux-gnu`)。两者只在三元组是探测来的时候
恰好相同。查找是 `if (exists) push_back`,找不到就什么也不发生 —— 而那个目录里
只有 `__config_site`,它的缺席产生的报错读起来像载荷坏了。两种拼法都试。

## ⭐⭐ 由图供给的目标侧只到达一个编译单元(mcpp#514 §A)

提供 `mcpp:` 层的包发布的是整个目标编译时所依据的头文件集合,而它以 publicUsage
的形态沿依赖边传播 ⇒ 根与 provider 自己的单元拿得到,兄弟依赖包拿不到。结果是
一次构建里两种口味的 BMI(`reference to 'space' is ambiguous`)。

目标侧解析后,`fromGraph()` 的层的 publicUsage 并入每一个包的 privateBuild;
std 模块的命令行改读同一个集合,不再自己推一遍。

## ⭐⭐ 缓存键描述了编译器,没描述它被指向的头文件集合(mcpp#514 §B)

`driverIdentity` 按设计覆盖不了它 —— `normalize_driver_output` 故意抹掉路径,
好让一个条目能被两个 home 共享。新增 `targetHeaderSet` 轴,取自已解析的
linkmodel,store-相对化。不 bump kCacheEpoch:旧条目是 miss 不是不可用。

## ⭐ home 发现有第四份拷贝,而且会伸到别的 home 去

`active_home_xpkgs()` 自己重推一遍 home(漏掉自包含安装那一档);
`find_sibling_package` 无条件回落 `~/.xlings/data/xpkgs` ⇒ 密闭构建可以从另一棵树
取载荷,结果直接进每条编译命令的 `-isystem`。前者改读 `mcpp::home::root()`,
后者删除 —— 找不到会说话,找错了不会。

## 新增 `[build] private_include_dirs`

指出 include_dirs 中在本包边界处停住的条目。publicUsage 此前整份接过
privateBuild,于是内嵌带内部头覆盖层的库(musl 的 src/include 定义
hidden/weak/weak_alias)会把那些宏发给每个消费者。openkal-musl 的
port/include/features.h 自己写下了这个缺口和它的第一优解。

⚠️ 它是 include_dirs 的子集而不是第二个列表:两类目录的相对顺序是承重的,
而两个 TOML 数组表达不了一个顺序。

## 判据

- 单元 93 passed / 0 failed(新增 3 条,均确认真的在跑,非 SKIP)
- e2e 295 扩到 cxxflags:修复后两工具链四条全绿;未修二进制上退出码 1 并列出六个丢失 token
- e2e 304(新):目标侧到达兄弟依赖包 —— 未修二进制上变红
- e2e 305(新):编译用/发布用分开 —— 未修二进制上变红
- 三条 e2e 均带分母(CDB 行数),空 CDB 不会假绿

Closes #514
它在写下它的那台机器上编过了,在 Windows CI 上编不过 —— 而 Windows 是这个项目
一半的面唯一可见的地方。改成固定名 + 先 remove_all:gtest 在一个二进制里串行
跑测试,一个名字就够。
⚠️⚠️ 这条不是本 PR 引入的 —— `mcpp_ref=main` 的对照 run 报同一处错。
它是索引把 `xim:glibc` 在同一系列内移动之后,**任何**从源码构建 mcpp 的
全新 CI 机器都会中的一条,今天开始出现。修在这里,因为它卡住了本 PR 的门。

## 机制

RuntimeBinding 带的是**声明的**身份;xlings 把载荷目录按请求**解析成**的版本命名。
索引把 `xim:glibc@2.44` 解析到 `2.44.2` 的那一刻,两者分开:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '…/xim-x-glibc/2.44', but it is not installed
    $ ls …/xim-x-glibc/   →   2.44.2

⭐ `runtime_binding.cppm` 早一个版本就记下了同一形状(「observed in CI as
runtime=glibc@2.39 with libc.so.6 resolving to the managed 2.44 payload」)
并做了归一 —— 但只在 SubOS 视图存在之后。而这一处跑在**第一次**装工具链时,
那时还没有视图可读。

## ⭐ 是「精化」不是「按目录顺序挑」,而这个区别是那条拒绝还站得住的全部理由

`2.44.2` 是请求 `2.44` 解析出来的东西:它的版本**分量**以请求的分量开头。

⚠️ 逐**分量**比,不是逐字符:`2.4` 不是 `2.44` 回答的那个请求。按字符串前缀比
会说它是,然后把一个错的 C 库交给构建而什么也不说。

⚠️ 若有两个载荷都精化了这个请求,这里**不做选择** —— 原样报拒绝。
「这个请求的解析结果」必须是**一个**载荷才谈得上是个答案。

## 判据(5 条,四个方向)

- 精确版本在场时优先(不会被精化抢走)
- 唯一精化被采纳
- `2.4` 不匹配 `2.44`(逐分量)
- 两个精化 → 仍然拒绝,且消息说明为什么
- 什么都没装 → 仍然拒绝
⚠️⚠️ 这是那条 `glibc@2.44 ... not installed` 的**主因**,而它在索引里被逐字
预告过。`xim-pkgindex` 的 `pkgs/g/glibc.lua` 记着一对提交:

    fb289b8  glibc: withdraw 2.44.2 until the client that can resolve it ships
    557e862  glibc: restore 2.44.2 now that the client resolving it has shipped

以及那次失败的原文:

    -- released xlings pinned `glibc@2.44`, payload directories are named
    -- after the version, and so a clean environment installed 2.44.2 and
    -- then found nothing answering to the binding
    --   error: selected RuntimeBinding glibc@2.44 requires payload
    --          '<home>/.../xpkgs/xim-x-glibc/2.44', but it is not installed
    -- on every NEW environment and on none that already existed.
    -- The index is DATA and the client is a PROGRAM: the consumer ships first.

⭐ 那个消费者是 **xlings 2026.8.27.1**(openxlings/xlings#567):它改为从索引文件
读默认 runtime binding,而不是读一个编译进客户端的常量。mcpp 钉的还是 8.17.2,
所以每一台冷缓存的 CI 机器都停在上面 —— **在 main 上和在任何分支上一样**。

## ⭐ 与前一条提交的关系(两条,不是一条的两半)

| | 覆盖谁 |
|---|---|
| 本条(pin) | **新环境**:xlings 从索引读绑定,直接绑到 2.44.2 |
| 前一条(`select_glibc_payload_lib`) | **已存在的环境升级**:subos 记下的绑定还是 `2.44`,而载荷已经是 `2.44.2`,且 SubOS 视图尚未建立 —— `runtime_binding.cppm:224` 早一个版本就记过这个形状 |

两者在不同的层,谁也替代不了谁;去掉后者,一台已经跑过一次的机器在升级后仍会停住。

## 判据

`.github/tools/check_version_pins.sh` 是这件事的唯一真源检查器,它一次列出了
全部 7 个不同步的点(3 个 workflow + 2 个 composite action + 2 个 release 段),
同步后退出码 0。**这正是它存在的理由** —— 注释里说过,手工维护的清单曾漏掉
两个 composite action,让 CI 的沙箱在 0.4.30 上停了很久没人发现。
ninja 通道会给需要引号的路径加引号,于是同一个 flag 到达时是
`"-fprebuilt-module-path=C$:\Users\..."` —— 锚定的模式匹配不到它,
比对随后就栽在这个过滤器本来要去掉的那个 token 上。

在 windows-x86_64 上实测:它把一条本该绿的恒等式变成红的,而原因与被编译的
东西毫无关系。改成子串匹配。⚠️ Linux 侧仍然能在未修二进制上变红(已复验)。
第一版把解析写在了工具链 post-install 里。工具链于是装得上了,而**编译线**的载荷
发现(`probe.cppm` 的 `payload_root_for_binding`)自己拼了一遍同样的查找,继续找不到
—— 而它的失败**连版本都不提**:

    bits/os_defines.h:39: fatal error: features.h: No such file or directory

因为 glibc 的 include 目录压根没被加上。在 openkal-musl 的 CI 上实测,就在
post-install 那一处修好之后。

⭐ ⇒ 抽成 `mcpp::xlings::paths::payload_dir_for_version`,两个调用方都读它。
这正是这个仓库反复付过代价的那个形状(#233/#240/#242/#344):**同一个决定推导两遍,
修好一处会让另一处更难被发现**,因为症状换了一副样子。

## 判据

单测从断言「那个包装函数」改为断言**共享的解析器**本身,四个方向:
精确优先 · 唯一精化被采纳 · 两个精化不是答案 · `2.4` 不匹配 `2.44`(逐分量)。
包装函数那五条保留,它们覆盖的是「拿不到 lib 目录时仍然拒绝」这一层。
`build_program.cppm` 的开篇写着这条不变量 ——「`tc` 永远是一个宿主目标的工具链」
—— 而原生分支直接返回主 `tc`。对除此之外的每个字段它都成立(原生构建里编译器
就是宿主编译器);`cAbiPrebuilt` 是「同一个编译器」与「同一个目标侧」第一次分开的
那个字段。

⚠️ **这是一条不变量,不是任何已测失败的修复。** 它是在追 openkal-musl CI 上那条
`features.h: No such file` 时写下的,而**它不是那条的成因** —— 在 origin/main 和
本分支上实测,gcc 的 std 模块两边都是零个 `-isystem`/`-idirafter`:那条工具链是
通过 post-install 重写的 specs 到达它的 C 库的,真正的缺陷在于**那些 specs 指名的是
哪一个 glibc 载荷**。

保留它,是因为这条不变量值得为真:一个在构建机上编译并运行的助手程序不该继承目标的
C 库出处,而下一个分开的字段将在这里找不到规则。
⚠️⚠️ **bump 是对的方向,但在这次过渡里它制造了一个新的失败模态,而且比原来那个更糟。**

## 实测出来的因果

`acquire_xlings_binary` 会在 pin 前移时**更新沙箱里那份 xlings**
(`fallback/xlings_binary.cppm:55`)。而沙箱那份才是解析 runtime binding 的那个
—— `.github/actions/bootstrap-mcpp/action.yml` 自己的注释就写着这件事:

    The SANDBOX xlings is a separate copy: mcpp vendors it into
    ~/.mcpp/registry/bin at `self init` and never revisits it … and it —
    not the system binary — is what resolves dependencies.

于是在一次 CI 里发生的是:

  1. 引导用的已发布 mcpp(沙箱 xlings 8.17.2)绑 `glibc@2.44`,装出 `2.44.2`
  2. 我的构建把沙箱 xlings 升到 2026.8.27.1,绑定**前移**到 `glibc@2.44.2`
  3. 而已经装好的是 `2.44` ⇒ 请求比已装的**更新**

⭐ 第 3 步正是 `payload_dir_for_version` **正确地不覆盖**的方向:接受一个更旧的载荷
去回答一个更新的请求,等于静默地用上索引明说「IT IS NOT THE ONE TO INSTALL」的那份
—— 它带着 `mcpp#484` 的 preload 缺陷。**拒绝是对的。**

## 保留的是哪一半

`payload_dir_for_version` 保留:它覆盖的是**请求比已装的更旧**那一侧
(绑定说 `2.44`,装好的是 `2.44.2`),那是安全的方向,也是这次事件最初的形状。

pin 的前移留给一次**独立的**变更:它需要与「谁来装那个新绑定指名的载荷」一起做,
而那不是本 PR 的题目。
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的
版本命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进
自己沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住。

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
在 `main` 上和在任何分支上一样(已用 `mcpp_ref=main` 的对照 run 证实)。

⭐ 待一个能解析它的 mcpp 发布之后移除:2026.8.27.1 接受「版本精化了请求」的已装载荷
(`payload_dir_for_version`),从它引导就不需要这一步了。在那之前,缺的那个载荷
直接装上。

共享的 `bootstrap-mcpp` action 覆盖 6 个 workflow;另外两个自己引导的
(`ci-fresh-install`、`ci-aarch64-fresh-install`)单独插入。
Sunrisepeak pushed a commit to mcpplibs/openkal that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
Sunrisepeak pushed a commit to mcpplibs/openkal-linux that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
Sunrisepeak pushed a commit to mcpplibs/openkal-macos that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
Sunrisepeak pushed a commit to mcpplibs/openkal-windows that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
Sunrisepeak pushed a commit to mcpplibs/openkal-opensbi that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
Sunrisepeak pushed a commit to mcpplibs/openkal-uefi that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
Sunrisepeak pushed a commit to mcpplibs/openkal-musl that referenced this pull request Aug 27, 2026
`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。
新轴的注释承诺「键要能跨 MCPP_HOME 存活」,而只做 `<store>` 一档的规则兑现不了它。
本机自己的工具链走 `CLibMode::Sysroot`,它唯一的编译 token 是

    --sysroot=/home/<user>/.mcpp/registry/subos/default

—— 在 HOME 底下而不在 `<store>` 底下。⇒ 每个条目都带上了这台机器的 home,两台装着
同样载荷的机器互相命不中。⚠️ 不是错的答案,是**不可共享**的答案 —— 而
`normalize_driver_output` 正是为了保住这条性质才故意丢掉路径信息的。

补第二档 `<home>`,和 `fill_package_config` 已有的 `<store>`/`<pkg>` 同形。
从 storeRoot 推导而不再传一个参数:两者必须是同一个 home,多一个参数就多一种
它们描述两台机器的写法。

⭐ 顺带把这个轴分不分得开什么写进注释,免得下一个读者重新推一遍:
载荷带版本号 ⇒ 两个载荷两个键(#514 §B,本轴存在的理由);一个 home 下的两个 subos
⇒ 两个键(此前没有);两个 home 下同名的 subos ⇒ 仍是一个键(与此前一致,由整工程
指纹的第 11 项区分)。

⚠️ 已有的两个单测**直接给轴赋值**,所以都没跑到相对化那段 —— 只做 `<store>` 也全绿。
新增 `TheHeaderSetCarriesNoAbsoluteHome`,判据是**home 不出现**而不是「有 `<home>`
前缀」(后者在前缀后面还拖着绝对路径时照样通过)。A/B 实测:移掉这一档后它失败,
逐字打印上面那行。

另:候选去重时 `index` 改为**跟着 `p` 一起动**。两者描述同一个包,今天只有同一个
`pkgIndex` 能走到那个分支,所以还差不出来;绑在一起是为了将来也差不出来。
`payload_dir_for_version(root, "")` 里 `root / ""` 就是 `root`,而它**是**一个
目录 ⇒ 精确匹配那一支把**容器**交了出去,调用者会把它当载荷用。

⚠️ 今天两个调用者都在到达这里之前拒了空版本(`probe` 靠 `at + 1 >= size()`,
`post_install` 有显式的 `version.empty()` 判断)—— 这恰恰是没有任何东西会撞上它的
原因。函数是导出的,第三个调用者不会知道这条约束写在别处。⇒ 闸放在性质成立的地方,
而不是放在每个调用者那里。

A/B 实测:移掉这一行后 `AnEmptyRequestIsNotTheContainer` 失败;测试自带分母
(同一棵树上真实的请求仍要解析得出来),空结果才不会和「目录读不了」同读数。
## 旧引擎怎么读这个键 —— 实测,不是推断

在 **2026.8.26.2** 上跑:
  * 出现在**依赖**清单里 ⇒ 静默接受,退 0
  * 出现在**根**清单里 ⇒ `warning: [build] has unsupported key
    'private_include_dirs' (ignored)`,退 0

⇒ 包可以先用上它,不必等消费者升级。这条写进中英两份文档,因为它决定了
openkal-musl 现在能不能发 —— 能。

⚠️ **唯一不成立的地方**:已发布 `xim` 描述符的 `target_cfg` 块里,不认识的子键是
**硬错误**(那段代码自己的注释就写着 "Unknown sub-keys stay a HARD ERROR here"),
会让整份清单加载失败。文档明说:索引下限指向认识它的引擎之前别写进那里。

## 消息是承诺,而我把它改得不准了

`target_cfg` 的未知键报错列出「期望哪些键」,我加了 `private_include_dirs` 却没有
把它加进那份列表 —— 于是错误信息会把一个**已经被接受**的键说成不存在。补上。
⚠️ **载荷版本解析整条没写进去** —— 而它是这一版里用户最先撞上的一条(干净机器
上引导直接停住)。补上,含判据:精化是逐**分量**比而不是逐字符,两个都精化时返回
空而不是按目录序挑,反方向不接受。

订正:`targetHeaderSet` 写的是「store-相对化」,自我 review 时实测发现只做这一档
盖不住最常见的那台机器(`--sysroot=<home>/registry/subos/default` 在 HOME 底下),
已改成 `<store>`/`<home>` 两档。顺带把这个轴分得开什么写清楚。
## e2e 304:提供层的包同时带私有目录

`openkal-musl` 就是这个形状 —— `provides = ["mcpp:c-abi=musl"]` **加上**内部头
覆盖层。层的目录到达**每一个**单元,所以在这里误发布覆盖层比在别处都宽:musl 的
`hidden`/`weak` 宏不只落到它的消费者,而是落到整个构建的每一个翻译单元。

两者按构造是相容的(目标侧取的是提供者的 `publicUsage`,而那已经是过滤后的集合)
—— 「按构造」正是需要一条断言的地方。反向对照实测:把 fixture 里的
`private_include_dirs` 拿掉,判据当场检出

    /tmp/.../dep/src/sibling.cppm      ← 兄弟依赖包
    /tmp/.../src/main.cpp              ← 根

也就是把那句「爆炸半径」从注释变成了读数。⭐ 另加控制:提供者自己的单元必须仍然
带着它,否则「没泄漏」和「整个丢掉了」同读数。

## e2e 305:glob 在展开之后比对

文档承诺「条目支持 `*` glob 并在展开之后比对」,而没有判据。加 `gen/*` 两条边:
提供者两个目录都要有,消费者一个都不能有。⭐ 判据落到 `gen/one`/`gen/two` **按名**
而不是 `gen`—— 只查 `gen` 的话,一个拿未展开拼写去比的实现照样通过。

反向对照实测:把 glob 从 private 列表里拿掉,两条都变红并各自点名。
`target/` 是**恢复出来的缓存**,键里含 `src/**` 的哈希。源码一改就 miss,
`restore-keys` 照样把最近的旧树递回来(分层恢复本来就是为此),构建再把产物写进
一个**新的** `<triple>/<fingerprint>/` 目录,和已有的并排。于是
`find … | head -1` 返回的是遍历先到的那个 —— 不是最新的,也不是这次构建的。

⚠️ 本机与 CI 双侧实测。本机这棵树上该命令挑到 `007bc0f2b78f7fa5`,而构建刚写的是
`148448dec6c4a7a7`。CI 上挑到的陈旧二进制,其 ELF 解释器指向 runner 上已经不存在的
glibc 载荷:

    …/bin/mcpp: cannot execute: required file not found     (exit 127)

—— 读起来像**这个 commit 构建坏了**,而完全不是。连着七次绿之后两次红、同一个陈旧
指纹:触发条件只是「源码改动大到让恢复出来的树里留下两个目录」。

⭐ **mtime 不能当判据。** 缓存恢复会把所有文件的时间戳写成解压时刻,所以恰恰在出问题
的那棵树上「最新」是没有意义的 —— `ls -t | head -1` 在本仓库挑到过陈旧指纹。
**缺席伪造不了**:先删掉,构建之后还在的就是刚链出来的。对象和 BMI 仍在缓存里,
代价是一次链接。

⭐ 判据是**恰好一个**而不是「至少一个」:两个就说明删漏了一棵树,这一步要消除的
歧义又回来了。⚠️ 不用 `mapfile` —— 这个 job 也在 macos 上跑,那里的 /bin/bash 是
3.2,没有这个 builtin。

本机实测:删前 4 个候选 → 删后 0 → 重建后 COUNT=1,且路径正是新指纹
`31ec1ef4ce2afcd5`,`--version` 正常。

⚠️ 同型写法在别的 workflow 里还有(ci-macos / ci-macos-e2e / ci-target-matrix 的
`| head -1`,以及若干 `-printf|sort -rn` 的 mtime 形态)。它们今天是绿的,这次改动
只碰变红的这一处;规则记在这里。
⚠️⚠️ **它一次都没有执行过,而且它的前提本身做不到。**

## 没执行

我先前把日志里的 `glibc payloads present: ` 读成了它的输出。那是**回显的脚本正文**
—— 展开后的输出一次都没出现过。守卫 `[ -x "$HOME/.mcpp/registry/bin/xlings" ]` 在
那个位置为假:mcpp 的 registry 要到第一次运行 mcpp 才建出来。

⚠️ 同一个坑今天第三次:**判据是子串匹配,而被匹配到的是脚本自己。**

## 前提做不到

`xlings install glibc@2.44` 会把请求**解析成** `2.44.2`,建出来的目录就叫
`2.44.2` —— 它**不可能**造出旧绑定要找的那个 `2.44` 目录。这一步的整个想法
(「把绑定指名的那个载荷装上」)对这个包管理器无法成立。

## 真正修好它的是上一个 commit

`openkal cross-build` 在桥**之前**就已经绿了:02:10 / 02:41 / 02:57 三次,而桥是
03:15 的 6e310aa。⇒ 止血的是 `b3735fc`(撤回 xlings pin 前移),不是这座桥。

⇒ 留着它 = 留一段永不执行的 CI 代码,配一条声称自己承重、并让下一个人去「等发布后
移除」的注释。删掉。

⚠️ 生态那七个仓库里也有同一段,同样是空转;它们下一轮改用正式版时一并删。
The released 2026.8.27.2 fixes, upstream, the defect this release works around
from the client side. Its own commit message states it:

    a stale bootstrap pin is a claim about the ecosystem, not neutral setup.
    The aarch64 job bootstrapped v2026.8.10.1. That client has `glibc@2.44`
    compiled in as the default runtime binding, so once the index's `latest`
    moved past it the job installed the newer payload and then refused to use
    it.

It also fixes a defect in 2026.8.27.1, which is why that version is skipped: the
subos code asked the index for `glibc` by bare name, which is ambiguous on an
ordinary home, and the resolver's error was read as "the index cannot answer" and
turned into the pinned fallback. The mechanism shipped dead and the binding still
came out right, because the pinned value and the index agreed that day. Two
constants now answer the two questions: `DEFAULT_RUNTIME_PACKAGE` names the
runtime and cannot carry a namespace because it is the payload directory's name;
`DEFAULT_RUNTIME_QUERY` asks the index and must carry one.

The two halves are complementary rather than redundant. A binding that follows
the index stops going stale; `payload_dir_for_version` in this release accepts an
installed payload whose version refines a request an OLDER client already
compiled in, and older clients continue to exist.

⚠️ I attempted this bump earlier and reverted it, correctly at the time: the
version then available moved the binding FORWARD, to `glibc@2.44.2` against an
installed `2.44`, which is the direction `payload_dir_for_version` refuses --- and
must refuse, since accepting it would silently use the payload the index states is
not the one to install.

The comments in `bootstrap-mcpp/action.yml` that record measurements taken on the
2026.8.17.2 assets keep that version, because that is the version they were taken
on. A comment stating a measurement states which build it came from.

    check_version_pins.sh   OK: xlings pins all at 2026.8.27.2
    unit test_xlings_version_pin   5 passed
The bump is correct and this release cannot carry it. Measured on CI rather than
predicted:

    Downloading xim:glibc@2.44
    error: toolchain post-install fixup: selected RuntimeBinding glibc@2.44.2
           requires payload '…/xpkgs/xim-x-glibc/2.44.2', but it is not installed

Two versions of one decision meet inside a single run. The mcpp that BUILDS this
one is the released 2026.8.25.2, whose compiled-in pin is 2026.8.17.2, and it
installs the payload its own binding names: 2.44. The vendored xlings that
COMPUTES the binding is the one this branch pins, which asks the index and gets
2.44.2. The request is then newer than what is installed.

That is the direction `payload_dir_for_version` refuses, and it must: accepting a
payload older than the request would silently use the one the index states is not
the one to install --- the preload defect of mcpp#484.

⚠️ THIS IS NOT A STALE CACHE. The sandbox cache key includes the xlings version
for exactly this reason, so the bump is a cache miss and the sandbox was fresh.
The mismatch is between the engine doing the building and the xlings it was told
to vendor.

⭐ IT IS SELF-HEALING, AND ONLY IN ONE ORDER. Once 2026.8.27.1 is released, a
bootstrap from it carries the new pin, computes 2.44.2, and installs 2.44.2. The
bump therefore belongs in the release AFTER this one, which is the rule the index
states in its own source: "The index is DATA and the client is a PROGRAM: the
consumer ships first."

The engine-side half of the same problem stays in this release:
`payload_dir_for_version` accepts an installed payload whose version REFINES a
request an older client already compiled in, so older clients stop being stranded
by an index move in the safe direction.
§7.5 是落地之后回填的。§2.12 的头文件草案里有两处,读的时候没看出问题,写第一份实现
时第一次运行就暴露了,而且两处都是**改规范而不是改实现**。

⓵ `openkal.space` 从两个操作收敛成一个。本规范面向的环境里没有一个把
「克隆地址空间」和「在其中启动上下文」当作一对原语:Linux 的 clone 是一个动作。要求
实现分开,它只能启动一个上下文、把它停住、再自建一条信令通道 —— clause 7.1 的判词是
**形状有错,不是实现有错**。分开的形式还有一个答不上来的问题:clone 之后 start 之前
改了内存,子上下文看见哪一份。

⓶ `openkal.net` 交出的是被拥有的连接,流从它借。草案直接交出 `kal_stream` 并称其
被拥有,而那在 clause 7.2 下无法实现:流句柄就是环境传输操作接受的东西,在描述符系统
上是一个关掉就被复用的数字,没有地方放代际。`openkal.fs` 早就回答过同一个问题。

⓷ 顺带更正 §4.1:`_LIBCPP_HAS_TERMINAL` 的结论与那里相反。设成 0 只是「对一个坏掉
的 port 的正确描述」;它门控的是 isatty,而 isatty 不工作是因为端口层用 TCGETS 回答
musl 用 TIOCGWINSZ 问的问题。修端口层才是修法。

§2.12 的两份草案加了取代说明而不是改写 —— 草案是「当时提出了什么」的记录。
@Sunrisepeak Sunrisepeak changed the title 2026.8.27.1 —— 目标侧被解析出来了,只发给了一个编译单元 Resolve the target side once, and let every translation unit read it Aug 27, 2026
§3.6 说这条「零设计成本,回报最大」。实施之后两句都要更正。

**前提只在四个 target 里的一个成立。** 机制靠「没被引用的定义会被丢掉」,那要
`-ffunction-sections` + `-Wl,--gc-sections`,而只有 `cfg(os = "linux")` 设了;
macOS 与 Windows 那两节根本没有 ldflags。⚠️ 我当时只跑了宿主一行,并据此在清单
注释里写下「这两个 flag 已经在位」—— 对我测的那个 target 为真,对其余三个为假。

**闭包应当一次问清,而不是跟着失败一条条加。** 逐族查目录之外的引用者:五个 fd 族
为 0;`network` 经 syslog/nscd_query/mq_notify 通到 `getpwnam`;`fork` 经
forkpty/wordexp/daemon/clone.c 通到 **`pthread_create`**。⇒ fork 的闭包终点是线程
实现,network 的终点是口令函数,两个都不能排。

⭐ **不是拒绝,是还不可能**:排 network 族要等每个 target 都收集未引用 section,
那是三条链接线的改动,可以单独测。代价照直说 —— 今天用 socket 的程序仍然构建成功、
仍然运行期失败。

另加一条:`pipe` 落到 `kal_process_channel` 上之后,裸机后端上每个程序都链接失败
(不管要不要 pipe)。同一条规则四行之外就写在 `kal_random_fill` 上 ⇒ 判据改成
符号类别 `w` 而不是 `U`。

§3.6 加了取代说明而不是改写 —— 它是「当时是怎么想的」的记录。
@Sunrisepeak
Sunrisepeak merged commit e187d3f into main Aug 27, 2026
37 checks passed
Sunrisepeak added a commit to mcpplibs/openkal-windows that referenced this pull request Aug 27, 2026
* ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)

`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。

* Implement openkal.terminal, and follow openkal 0.8

The console operations this environment offers map onto the interface directly:
ENABLE_LINE_INPUT is what openkal.terminal calls line editing and
ENABLE_ECHO_INPUT is what it calls echo, which is the same division the interface
draws. GetConsoleMode is already what kal_stream_props asks, so a stream that is
not a console is reported as unsupported by the same test.

The mode is read, modified and written rather than written alone. The console
mode carries processed input, mouse input, window input and virtual terminal
processing, none of which the interface names; a word composed from the two
positions alone would turn all of them off, and a program that asked only to stop
echoing would find its console changed in ways it did not ask for.

kal_terminal_size reports the WINDOW and not the buffer. This environment's
screen buffer may be taller than the window that shows it, and a program drawing
a full screen against the buffer's height would scroll its own output away.

The other four interfaces version 0.8 adds are not provided, which clause 6.1
makes an absence at the link rather than a deviation.

* Provide the three operations openkal 0.8 adds to openkal.process

ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, and adding a
new interface obliges none. Clause 6.1 makes an interface a backend does not
provide absent at the link and not a deviation; it makes one provided IN PART a
deviation. The five interfaces version 0.8 adds are therefore free to decline,
and the three names added to openkal.process are not.

The specification's own surface checker said so before anything else noticed:

    openkal.process is provided in part: 3 of 8 names are not exported --
        kal_process_channel
        kal_process_channel_close
        kal_process_spawn_with

kal_process_channel is CreatePipe. THIS ENVIRONMENT DECIDES INHERITANCE PER
HANDLE AND NOT PER EXEC, which is the opposite of the other two: there every
handle is inherited unless marked otherwise, so those implementations mark both
ends close-on-exec and let the spawn place the far one. Here the default is not
to inherit, so the far end is created inheritable and the near end is withdrawn
afterwards --- otherwise the started program would hold both ends and the writer
would never observe the end of input.

kal_process_spawn_with refuses a non-empty set of grants, and the refusal is the
honest answer rather than a gap. A preopened directory is a handle a started
program reads back by NUMBER, and this environment has no numbering: a handle
crosses a spawn by being inheritable, and the started program learns of it
through a mechanism the parent arranges. There is no correspondence to descriptor
three.

Clause 6.2 is what makes that conforming: the operation exists, reports
kal_err_not_supported, and the property word does not claim
KAL_PROCESS_PROP_GRANT_DIR --- so a caller learns from the word what it would
otherwise learn from a failed call. A count of zero is still answered, by the
ordinary spawn, because a program with no preopens is what this environment
starts anyway.

* Export the three names the new declarations call

`port/*.def` is an explicit list, and the import library generated from it
contains exactly those names. I added three declarations to `src/win32.h` for
openkal 0.8 and did not add the names, so the import library this package
generates for its consumers was missing them.

⚠️ AND THIS PACKAGE'S OWN CI STAYED GREEN, because this package does not link.
The failure appeared one repository away, in openkal-llvm-runtime's cross-build:

    ld.lld: error: undefined symbol: __declspec(dllimport) CreatePipe
    ld.lld: error: undefined symbol: __declspec(dllimport) GetConsoleScreenBufferInfo
    ld.lld: error: undefined symbol: __declspec(dllimport) SetConsoleMode

which reads as a defect in the consumer and is nothing of the kind.

⭐ IT WAS FOUND BY COMPARING IMPORT LIBRARIES, NOT BY READING THE ERROR. Every
copy of a vendor `libkernel32.a` on the machine had all three; the one generated
here had `GetConsoleMode` and not `SetConsoleMode`, which is not a shape a
vendor's library takes and pointed straight at the generated list.

The check added here is the one that would have caught it: every name
`src/win32.h` declares must be exported by some `.def`. It belongs in this
repository, where the two lists are, rather than in the consumer that trips over
the difference.

Measured both ways: removing `CreatePipe` from the list makes it red and names
the symbol; the lists as committed are 42 declared, 49 exported, none missing.
A denominator is asserted on both sides, since two empty lists have an empty
difference too.

* ci: name the encoding the check reads its sources in

Python opens a file with the platform's default encoding, which on the Windows
runner is cp1252, and these sources are UTF-8:

    UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 in position 90

A check that reads source files must say what they are encoded in, or it reports
on the runner's locale rather than on the sources. The byte it stopped at is in a
comment marker, so the check would have passed on a repository whose comments
happened to be ASCII and failed on this one --- which is the least useful place
for a difference to appear.

Both reads name utf-8. Rehearsed locally under cp1252: 42 declared, 49 exported,
none missing.

* Name the released specification and the released engine

openkal 0.8.0 is published, so the manifest names it by version rather than by
branch. A branch is how a change spanning these repositories is developed and is
not a form a published package may carry: a consumer resolving from the index
would be handed a reference that moves.

The engine pin moves to mcpp 2026.8.27.1 and the repository variable
MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user
installs.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Sunrisepeak added a commit to mcpplibs/openkal-opensbi that referenced this pull request Aug 27, 2026
…#11)

* ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)

`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。

* Follow openkal 0.8, and record why the five new interfaces are absent

This machine provides none of them, and each absence has a reason rather than
being an omission: no network for net and datagram, no memory management unit to
copy an address space with for space, no second execution context to bound a wait
against for timeout, and a console whose settings the firmware does not expose for
terminal.

Clause 6.1 makes an interface an implementation does not provide absent at the
link, so a program requiring one is refused when it is built rather than when it
runs. The README states this beside the reasons already recorded for fs, process
and task, so that a reader asking why finds the answer where the other answers
are.

* examples: name openkal the way this repository does

mcpp refuses a graph in which one package reaches a dependency by version and
another by git:

    dependency 'mcpplibs.openkal' is requested as both a version dep (by 'hello')
    and a git dep (by 'mcpplibs.openkal-<this>@path'). Pick one.

That is the right refusal --- two forms can name two different trees --- and it
means an example inside a repository must follow that repository's own
declaration while a change spanning repositories is in flight. The example named
a released version while the package beside it followed a branch.

* Name the released specification and the released engine

openkal 0.8.0 is published and resolvable from the index, so the manifest names
it by version rather than by branch --- including the example's, where a branch
would hand a reader a reference that moves.

The engine pin moves to mcpp 2026.8.27.1 and the repository variable
MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user
installs.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Sunrisepeak added a commit to mcpplibs/openkal-uefi that referenced this pull request Aug 27, 2026
…#8)

* ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)

`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。

* Follow openkal 0.8, and record why the five new interfaces are absent

This machine provides none of them, and each absence has a reason rather than
being an omission: no network for net and datagram, no memory management unit to
copy an address space with for space, no second execution context to bound a wait
against for timeout, and a console whose settings the firmware does not expose for
terminal.

Clause 6.1 makes an interface an implementation does not provide absent at the
link, so a program requiring one is refused when it is built rather than when it
runs. The README states this beside the reasons already recorded for fs, process
and task, so that a reader asking why finds the answer where the other answers
are.

* examples: name openkal the way this repository does

mcpp refuses a graph in which one package reaches a dependency by version and
another by git:

    dependency 'mcpplibs.openkal' is requested as both a version dep (by 'hello')
    and a git dep (by 'mcpplibs.openkal-<this>@path'). Pick one.

That is the right refusal --- two forms can name two different trees --- and it
means an example inside a repository must follow that repository's own
declaration while a change spanning repositories is in flight. The example named
a released version while the package beside it followed a branch.

* Name the released specification and the released engine

openkal 0.8.0 is published and resolvable from the index, so the manifest names
it by version rather than by branch --- including the example's, where a branch
would hand a reader a reference that moves.

The engine pin moves to mcpp 2026.8.27.1 and the repository variable
MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user
installs.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Sunrisepeak added a commit to mcpplibs/openkal-macos that referenced this pull request Aug 27, 2026
* ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)

`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。

* Implement openkal.terminal, and follow openkal 0.8

The terminal ioctls this kernel offers are the ones kal_stream_props already
uses, so the interface is a natural extension rather than a new mechanism:
TIOCGETA and TIOCSETA for the mode, TIOCGWINSZ for the size.

The request numbers encode the size of the structure they carry, so the layout is
declared here and asserted rather than named from a header. A header stating it
would belong to a C library, and this implementation has none.

The mode is read, modified and written rather than written alone. The structure
carries a baud rate and twenty control characters that the interface does not
name; composing one from the mode word would discard them, and the terminal a
program returned to would not be the one it found.

kal_terminal_props asks TIOCGWINSZ rather than deriving the answer from TIOCGETA.
A pseudo terminal answers both and a serial line answers only the first, so a
word derived from one would claim a facility the next call refuses.

The other four interfaces version 0.8 adds are not provided, which clause 6.1
makes an absence at the link rather than a deviation.

* Provide the three operations openkal 0.8 adds to openkal.process

ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, and adding a
new interface obliges none. Clause 6.1 makes an interface a backend does not
provide absent at the link and not a deviation; it makes one provided IN PART a
deviation. The five interfaces version 0.8 adds are therefore free to decline,
and the three names added to openkal.process are not.

The specification's own surface checker said so before anything else noticed:

    openkal.process is provided in part: 3 of 8 names are not exported --
        kal_process_channel
        kal_process_channel_close
        kal_process_spawn_with

kal_process_channel is this kernel's `pipe', which reports BOTH descriptors as
return values rather than through a buffer. That is a property of the calling
convention, and src/sys.h already meets the same difference for the duplication
primitive; a second wrapper is added beside it rather than the difference being
hidden.

There is no pipe2 here, so close-on-exec is set afterwards with fcntl. The two
steps are not equivalent to one under a concurrent spawn, and that is stated
rather than concealed: a caller that spawns from one context, which is what a
program using this operation does, is unaffected.

dup2 onto the same number succeeds and does nothing, where the other kernel's
dup3 refuses. Either is right for the placement loop; only the reason differs,
and it is written down so that a reader comparing the two implementations does
not take one for an oversight.

* Do not redeclare nr_fcntl

The number was already in the table. I added it again beside nr_pipe rather than
looking, and this kernel's table is one enumeration, so the second declaration is
an error rather than a duplicate:

    src/sys.h:178:5: error: redefinition of enumerator 'nr_fcntl'

Not visible from a machine that cannot build for this system, which is why the
check that found it is the one that runs on it. The whole table was then examined
for the same shape rather than this one line corrected: no other name is declared
twice, in either architecture's block, in this repository or in the other
backend that has such a table.

* Name the released specification and the released engine

openkal 0.8.0 is published, so the manifest names it by version rather than by
branch. A branch is how a change spanning these repositories is developed and is
not a form a published package may carry: a consumer resolving from the index
would be handed a reference that moves.

The engine pin moves to mcpp 2026.8.27.1 and the repository variable
MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user
installs.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Sunrisepeak added a commit to mcpplibs/openkal-musl that referenced this pull request Aug 27, 2026
* feat: 本包从哪儿编,与本包发布什么,现在是两件事

`port/include/features.h` 从 2026-08-22 起记着这个缺口和它的第一优解:

  ⓘ THIS IS THE SECOND-BEST REMEDY. The first would be for a package to
  distinguish the directories it is built from from the directories it
  publishes. Measured: mcpp cannot express it — publicUsage takes
  privateBuild's include directories entire.

mcpp 2026.8.27.1 增了 `[build] private_include_dirs`。本包改用它:
musl 的三个内部目录(`src/include`、`src/internal`、`musl-generated/internal`)
只到达本包自己的源码,不再进入任何消费者的命令行。

⇒ openkal-musl#13(把 `hidden` 当普通标识符的消费者编不过)在**目录**这一层
解决,而不是在**宏**这一层再打一个补丁。

## ⚠️ 为什么 features.h 那段还在

两条轴回答的是不同的问题,只有一条是关于可见性的:

  private_include_dirs  谁看得见 —— 消费者的命令行上根本没有这些目录
  OKM_MUSL_INTERNAL     谁在编译 —— 一个确实带着这些目录、但不是 musl 的 C 单元
                        (板子在这里构建 compiler-rt 时)仍然需要那些宏是惰性的

第二种情形已被实测过:`int_util.c:49: use of undeclared identifier __weak__`。
去掉任何一条都会让一个已经付过代价的缺陷回来。

## ⚠️ 为什么是 include_dirs 的子集而不是第二个列表

顺序承重:`port/include` 必须排在架构目录之前,内部覆盖层必须排在
`musl/include` 之前 —— 把这三个挪到末尾,musl 自己的构建会先找到公共
`<features.h>` 并以 `unknown type name hidden` 失败(同一份文件里记着这次实测)。
两个 TOML 数组表达不了一个顺序,所以那一个有序列表仍然是那一个有序列表。

## 判据

CI 增一步,断言的是**目录**不是症状 —— 断言某个宏不再冲突,会在包侧再打一个
宏补丁时变绿而泄漏还在。并带两个控制项:

- 分母:compile_commands.json 至少有一行,否则 grep 恒真
- 反向:`port/include` 必须仍在消费者命令行上,否则「什么都没发布」也会通过

本机实测(mcpp feat/target-side-reaches-every-unit + examples/cross-hello):
消费者单元 公共目录=有、三个内部目录=无。

* fix: 问「这是不是终端」的那个请求,不是端口层认得的那一个

musl 的 `isatty` 用 **TIOCGWINSZ** 问,而端口层只答 **TCGETS** ——
TCGETS 是**读**终端设置用的请求,不是**问它是不是终端**用的那个:

    int isatty(int fd) {
        struct winsize wsz;
        unsigned long r = syscall(SYS_ioctl, fd, TIOCGWINSZ, &wsz);
        if (r == 0) return 1;
        ...
    }

⇒ 这个端口之上的每一次 `isatty` 都返回 0 —— 对一个真终端和对一个管道一样。

## ⚠️ 什么都没有失败,这正是它的形状

`std::print` 只是从不走终端那一支;任何靠「问一下」来决定用不用颜色、
用什么缓冲策略的程序,都在无声地决定错。

## 判据(实测,2026-08-27,同一套 harness 下带原生对照)

                  管道   伪终端
    原生 glibc      0        1
    本端口(修前)    0        0
    本端口(修后)    0        1     ← 与原生逐字一致

⭐ **判据是一个关系,不是一个值。** 只断言「管道下为 0」在整个缺陷期间都会通过;
必须断言管道与伪终端**不同**,且与系统自己的 C 库**同样地**不同。CI 里带了原生
对照:若 `script` 分配不出伪终端,这条检查会说自己什么也证明不了,而不是把端口
判成坏的。

## ⭐ 尺寸报「不知道」,不编一个出来

openkal 没有回答窗口尺寸的操作,而 `winsize` 已被调用方清零。
编一个 80x24 出来会是这份文件唯一明令禁止的形状 ——「报成功而什么也没做」。
一个要尺寸的调用方读到零,而一条串口线报的也是零。

* ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)

`xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本
命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己
沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要
`2.44`,构建在编译任何东西之前就停住:

    error: selected RuntimeBinding glibc@2.44 requires payload
           '.../xpkgs/xim-x-glibc/2.44', but it is not installed

⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。
索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则:
「The index is DATA and the client is a PROGRAM: the consumer ships first。」

## ⚠️ 我先试过 bump xlings,撤回了

把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉
(mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到
`glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧:
接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说
「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。

⇒ 保持 xlings 不动,把缺的那个载荷直接装上。

⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受
「版本精化了请求」的已装载荷,从它引导就不需要这一步。

* ci: 两个新断言步骤从来没跑过,而其中一个跑起来会误报

## 1. `$MCPP` 这个变量不存在

本 workflow 把构建出来的 mcpp 目录写进 `$GITHUB_PATH`,后续步骤直接叫 `mcpp`。
我新加的两个步骤自己发明了 `"$MCPP"`,展开成空 ⇒ 四个 job 全部

    line 19: : command not found        exit 127

⚠️ 也就是说这两条断言**一次都没有真正执行过**。它是硬失败而不是静默跳过,所以被
逮住了 —— 但判据本身的「否」和「没测成」曾经同读数,这正是要避免的形状。

## 2. 内部覆盖层的判据是**整文件 grep**,而提供者的行就在同一个文件里

`examples/cross-hello` 通过 `path = "../.."` 依赖本包,所以 openkal-musl **自己的**
编译行也在这份 compile_commands.json 里,并且**理应**带着 `musl/src/include` ——
那正是「私有,而不是没用」的意思。整文件 grep 分不开两者,会把本包自己的构建
判成泄漏。⇒ 改成按行取:jq 先按 `.file` 分出消费者行和提供者行。

⚠️ 顺带归一化分隔符:Windows runner 写的是 `…\musl\src\include`,不归一化的判据
在那台机器上会**悄悄不再匹配**。

⭐ 同一个键的另一半也补上:三个目录必须**出现在提供者的行里**。只断言「消费者没有」
的话,把这三个目录整个丢掉也一样绿。

⭐ 双侧分母(consumer=N provider=M 打印出来),任一为 0 直接报「什么都没检查」。

实测(本地合成 CDB,两种分隔符各一行):正向 bad=0;把覆盖层塞进消费者行后
bad=1 并逐条打印。另:错误信息里的反引号在双引号串里是命令替换,已去掉。

* A permanent absence is a link error; umask stops reporting a fiction

Three changes, of which the second is the hardest defect this port had.

## What openkal will never have is now absent at the link

The port answered two different facts with one ENOSYS. A backend that does not
provide openkal.fs makes `open` fail at run time, and that is correct: another
backend does provide it, so the condition is a property of the deployment.
`socket` is not like that. No openkal implementation provides one, because the
specification has no interface that would carry it; the fact is permanent and
independent of the backend.

Reporting a permanent fact as a run-time condition made it discoverable only by
running. mcpplibs/tinyhttps --- a package in mcpp's own index --- built
successfully against this port and failed at its twenty-seventh test.

Clause 6.2 states the remedy: the absence of an operation is expressed by the
absence of its definition. The sources are withheld from the build, and the
granularity is exact because of two flags this package already sets:
-ffunction-sections with -Wl,--gc-sections means a definition nobody references
is discarded rather than demanded.

Measured, in both directions, because neither half is sufficient alone:

    absent at the link: socket fork poll pipe chmod symlink
    still linked:       printf malloc open strtod qsort

Asserting only the first would pass for a port that had stopped linking anything;
asserting only the second would pass for the defect this replaces. The CI step
added here asserts both.

## umask reported a success it could not deliver

`g_umask` was written by the SYS_umask case and read by the SYS_umask case, and
by nothing else. openkal's kal_fs_open takes no mode argument, so there was no
creation for a mask to apply to. A caller set a mask, was told the previous one,
and watched the next file it created ignore both.

That is the one outcome okm_opt.h forbids in terms: nothing there reports success
having done nothing. It is a harder defect than a wrong permission bit, because a
wrong bit can be argued about and an effect that does not exist cannot.

The case now falls to the default and umask() reports ENOSYS. Most callers do not
check it, which is correct: the success they were getting was false.

    measured   umask -> -1 errno=38

## copy_file_range, and only that one

libc++'s std::filesystem::copy_file tries copy_file_range first and falls back on
a list of errors that includes ENOSYS; its fallback from sendfile accepts only
EINVAL. So providing this one is both necessary and sufficient, and providing
sendfile as well would be unreachable code.

It is a read-and-write loop, because openkal has no operation that moves bytes
between two files without passing them through the caller. The offsets are
optional and, when given, are advanced by what was copied while the descriptors'
own positions are put back --- which is the form std::filesystem uses.

    measured   copy_file_range -> 55 bytes, and the destination holds the
               source's bytes rather than merely the count
               with offsets -> 10, in 8->18, out 0->10, source position unmoved

## Also

The dead glibc bridge is removed. It never executed: the guard tested a path mcpp
had not yet created, and what read as its output in the logs was the workflow
echoing its own script text. Its premise was also unreachable, since
`xlings install glibc@2.44` resolves to 2.44.2 and creates a directory of that
name. mcpp-community/mcpp#515 carries the same removal with the run history that
shows what actually fixed the failure.

* Move the declared versions in step with openkal 0.8

tools/working-trees.sh substitutes a working tree for each dependency and then
asserts that what was fetched satisfies what the manifest asked for. Below 1.0 a
minor bump is a breaking change, so a manifest asking for openkal 0.7.0 does not
accept the 0.8.0 tree this branch is built against, and the failure would have
named a missing header rather than the two halves being out of step.

That guard exists because the fallback once produced exactly that: a branch of
this package was built against openkal 0.6.0 while asking for 0.7.0, and five
jobs reported a missing 'openkal/random.h'.

* pipe is supplied from kal_process_channel, and its ends are owned

`pipe` was on the list of facilities this port withholds, because openkal had no
way to express one. openkal 0.8 does: kal_process_channel is a pair of streams of
which one end is meant to cross a spawn, which is what a pipe is. So it moves off
that list and is supplied like any other facility.

⚠️ THE CLOSURE SAID SO BEFORE THE REASONING DID. Withholding it broke faccessat,
which forks and reports its answer back through a pipe:

    ld64.lld: error: undefined symbol: pipe2
    >>> referenced by faccessat.c:45

An exclusion that takes an ordinary function with it is the wrong exclusion, and
that was the first evidence this one had become so.

## A channel end is owned; a stream is not

The port had one kind for a stream descriptor, and it closed nothing on release
--- correct, because the only streams it held were the program's own three, which
openkal states are borrowed. A channel end is obtained and must be given back.

Binding a pipe end as a borrowed stream therefore produced a program that hung:

    write(fds[1], …)  ->  16
    read (fds[0], …)  ->  16
    close(fds[1])
    read (fds[0], …)  ->  waits for ever

The write end was still open, so the read end never saw the end of input.

OKM_CHANNEL is that distinction, named after what it is rather than expressed as
a flag on the existing kind. openkal draws the same division and for the same
reason, so the port now draws it too. Every place that branches on the kind was
enumerated rather than sampled: the stream lookup, fstat, and ioctl each say what
a channel end is.

  measured   the read end holds the 16 bytes the write end was given
             closing the write end is observed as end of input
             pipe2 applies O_CLOEXEC
             two thousand pipes opened and released without exhausting
             faccessat answers, so it linked

The last two are the ones a release that did nothing would fail: the table would
run out, and the function would not have linked at all.

* The committed manifest names no directory of anybody's machine

I committed one that did. tools/working-trees.sh rewrites this manifest to name
working trees and deliberately does not restore it --- the rewrite must survive
for the rest of a CI job, in a checkout that is thrown away. I ran it by hand,
committed with `git add -A`, and published

    openkal = { path = "/home/<user>/workspace/github/mcpplibs/openkal" }
    openkal-linux = { path = "/home/<user>/workspace/github/mcpplibs/openkal-linux" }

A consumer resolving this package from the index would be handed a manifest
pointing at a directory that exists on one machine and nowhere else.

⚠️ FOUND BY A REVIEW THAT LISTED WHAT EVERY PACKAGE DECLARES, not by CI. CI had
already failed on it, and the failure named a build error further down where the
substitution had produced a graph that could not resolve --- which reads as a
problem with the change rather than with the manifest.

The remedy is a criterion and not a note. The step added here runs BEFORE the
rewrite, so what it examines is what the commit contains rather than what the job
has done to it, and it names the remedy in its own message. Measured both ways: a
manifest with an absolute path is caught, and the manifest as committed passes.

The script now also says what it leaves behind, because the next person to run it
by hand will be reading the script rather than the workflow.

* Narrow the exclusion list to what can be withheld without taking a neighbour

Two failures, both from the same cause and both invisible on the host.

## The list took ordinary functions with it

musl reaches the withheld facilities from functions that are not withheld:

    lchmod.c   -> fchmodat        undefined reference to `fchmodat'
    sem_open.c -> link            undefined reference to `link'

⚠️ AND THE BREAKAGE IS PER TARGET, WHICH IS WHY THE HOST DID NOT SHOW IT. With
--gc-sections a definition nobody references is discarded, so on a target where
nothing reaches lchmod the exclusion is invisible; on one whose link model keeps
it, the build stops. A criterion that holds on one of four rows is not a
criterion, and the earlier measurement --- taken on the host alone --- is what
made this look settled.

⭐ THE VALUE WAS NEVER EVENLY SPREAD ACROSS THE LIST. What motivated it was
mcpplibs/tinyhttps building against this port and failing at its twenty-seventh
test, and what it used was sockets. The network and process families carry that
value. A program is not misled for long about chmod, which fails at its first
call and says so.

So the list is now what openkal will never have AND what can be withheld without
taking an ordinary function with it. The second half is a property of musl rather
than of openkal, which is why the list is shorter than the argument for it would
suggest.

## SYS_pipe does not exist everywhere

    error: use of undeclared identifier 'SYS_pipe'

The architectures numbered after pipe2 have only the later call. The guard is on
the NUMBER being defined rather than on the architecture, because what varies is
the kernel's table and not the machine.

## The assertion followed the list

The CI step asserted that `pipe` fails at the link, and reported honestly that it
no longer does --- pipe is now supplied from kal_process_channel. It moves to the
other half of the check, where it must link, and `chmod` moves with it. `_Fork`
takes its place among the absent, since it is excluded and nothing reaches it.

    absent at the link:  socket  fork  poll  _Fork
    still linked:        pipe  chmod  printf  malloc  open  faccessat

Measured on the host for both halves. The other three rows are what CI is for.

* Withhold only what can be withheld on every target

The list is now five sources rather than eighteen, and the reduction is measured
rather than cautious.

## Withholding a family only works when nothing else in musl reaches it

Each candidate was checked for consumers outside its own directory:

    timerfd eventfd signalfd inotify epoll    0 consumers          withheld
    select                                    network/res_msend.c
    network      syslog.c nscd_query.c mq_notify.c    -> getpwnam
    fork         forkpty.c wordexp.c daemon.c
                 linux/clone.c                        -> pthread_create

The fork closure terminates at the THREAD implementation and the network closure
at the password functions. Withholding either takes an essential or an ordinary
facility with it, so neither is withheld. The failures said so one at a time ---
lchmod wanting fchmodat, sem_open wanting link, daemon and wordexp and forkpty
wanting fork --- and following them one at a time was the wrong method; the
question is answerable in one pass over the sources.

## And the mechanism has a precondition this package meets on one target of four

`-ffunction-sections` with `-Wl,--gc-sections` is what makes an unreferenced
definition cost nothing, and only `cfg(os = "linux")` sets those ldflags. macOS
and Windows set no ldflags at all. So the objects are kept there, and a list that
passed here took the mingw build down:

    musl/src/legacy/daemon.c:18: undefined reference to `fork'

⚠️ I HAD MEASURED ONE ROW OF FOUR AND CALLED IT SETTLED. The manifest comment I
wrote said the flags were already in place, which is true of the target I tested
and of no other.

⭐ SO WITHHOLDING THE NETWORK FAMILY IS NOT REFUSED, IT IS NOT YET POSSIBLE. It
becomes possible once every target collects unreferenced sections ---
`--gc-sections` for the ELF and PE linkers, `-dead_strip` for this system's ---
and that is a change to three link lines, testable on its own, rather than
something to attach here.

What that costs today is stated plainly: a program using sockets still builds
against this port and still fails when it runs, which is what mcpplibs/tinyhttps
met at its twenty-seventh test. The five that remain withheld carry their own
reason --- a descriptor created and never ready is worse than one that cannot be
created --- and their closure is empty on every target.

  measured   withheld, absent at the link:  epoll_create1 eventfd timerfd_create
             still linked:                  printf pipe chmod socket faccessat

* Reference the channel weakly, or an interface a backend may decline becomes one it must provide

Clause 6.1 expresses an interface an implementation does not provide as the
absence of a definition, and a bare-metal backend provides no `openkal.process'
at all --- it has no second image to start. A strong reference from this port
therefore made every program above such a backend fail to link, whether or not it
ever asked for a pipe:

    ld.lld: error: undefined symbol: kal_process_channel
    >>> referenced by okm_syscall.c:407

Measured on openkal-opensbi through openkal-llvm-runtime's bare-metal row, which
is the row that has nothing to fall back on and is why the acceptance environment
is bare metal.

⚠️ THE SAME RULE IS ALREADY APPLIED TO kal_random_fill IN THIS FILE, four lines
away, with a comment stating it. This is the second time this port has had to
learn that a forwarder to an optional interface must be a weak reference, so the
criterion is now the symbol class rather than a sentence:

    w kal_process_channel
    w kal_process_channel_close
    w kal_random_fill
    U kal_stream_write        <- a required interface, and still strong

`pipe` reports ENOSYS where the channel is absent, which is the same answer the
default branch gives for everything else openkal does not have. The release in
okm_fd.c tests the reference too, although a descriptor of that kind cannot exist
without the operation that made it.

* The criterion is this package's own objects, not a program's link

It was a program's link, and that measured the wrong thing. Under the llvm
toolchain the probe built successfully for symbols this package does not define
at all, so the check reported

    epoll_create1 did not fail at the link; got: a successful build

for a facility that had in fact been withheld. Something else on that link line
supplied them. A program's link is closed over this port only when nothing else
answers, and whether that holds is a property of the toolchain rather than of
this package --- so an assertion resting on it reports on the toolchain.

A definition either is in these objects or is not. That holds under every
toolchain and cannot be satisfied by a host C library.

⚠️ AND THE OBJECTS ARE SCOPED TO ONE FINGERPRINT DIRECTORY. `target/` accumulates
one per configuration, so a search across all of them reads definitions from a
build made before the change. That is how my first reading of this reported every
withheld symbol as still present, and it is the same shape as the CI step that
picked a stale mcpp out of a restored cache. The step asserts there is exactly
one directory rather than trusting that there is.

    withheld, not defined: epoll_create1 eventfd timerfd_create inotify_init
                           signalfd
    defined here:          printf malloc open pipe faccessat chmod socket fork

The second list is not decoration. Without it the first would pass for a package
that had stopped compiling anything, and `socket` and `fork` appear in it because
their closures reach the password functions and the thread implementation --- so
they are deliberately not withheld, and the manifest says why.

* Test against the released engine

mcpp 2026.8.27.1 is published and the index's latest points at it, so the pin
moves to it and the repository variable MCPP_SOURCE_REF is cleared. What this
repository tests is now the engine a user installs rather than a branch of it.

The manifests are unchanged: they already name the versions this round
publishes, and this repository's continuous integration substitutes working
trees for them, so the change spanning these repositories is still tested as
written.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native musl path: dependency-package C++ module units miss the target C++-ABI include set; global build cache reuses BMIs across targets

2 participants