Skip to content

Resolve the target side once, per layer, after the dependency graph is known - #486

Merged
Sunrisepeak merged 33 commits into
mainfrom
feat/import-std-capability
Aug 23, 2026
Merged

Resolve the target side once, per layer, after the dependency graph is known#486
Sunrisepeak merged 33 commits into
mainfrom
feat/import-std-capability

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 22, 2026

Copy link
Copy Markdown
Member

一个目标能不能 import std;,现在由依赖图里有没有包为它提供标准库决定,而不是由「这个目标是不是 freestanding」决定。顺着这条线,另外三处对目标的猜测也换成了图里已知的事实,外加一条独立的指纹缺陷。

1. 门问的是「有没有」

prepare.cppm 的门从 ft->is_freestanding() 改为 ft->is_freestanding() && !hostedStdProvided,hostedStdProvided 来自 capability hosted-standard-library。配套:[package] std-module / std-module-flags 让一个包带自己的 std 模块源。

⚠️ 取的是 publicUsage 而不是 privateBuild —— 后者会让 unexpected 变成有歧义。

2. ⭐ 三处「由图决定」,而它们的注释各自预言了自己

flag 触发的现象
-fno-exceptions / -fno-rtti exception handling was enabled in precompiled file ... but is currently disabled
-ffreestanding ⚠️ 它的作用之一是 no main special-casing ⇒ C++ 的 main 被修饰成 _Z4mainv,启动对象报 undefined symbol: main
-fasynchronous-unwind-tables(反向:要加上) 见下

freestanding/target.cppm-fno-exceptions 上方的注释逐字写着:「a board that ships a target-built libc++abi and unwinder has a real case for turning these back on, and that is the point at which this becomes a manifest key。那个点到了。

⚠️⚠️ 一套不完整的展开表不会降级,它会让走栈停住

宿主 ELF 目标上 clang 默认给每个函数发 .eh_frame,裸机 ELF 目标上不发。凡是 -fexceptions 编的都有表,其余全都没有 —— 在这套栈上就是 C 库和 libunwind 自己的 C 源码。实测 riscv64,而中间每一步读数都指向别处:

__unw_get_proc_info  -> 0  start=80200148 end=8020068c lsda=80447190
__unw_step           -> 0   (UNW_STEP_END)
after step           -> 8021d55e

帧找到了、personality 找到了、这一步确实算出了返回地址 —— 而 step 仍报「栈底」,因为 libunwind 为调用者重新求信息,而调用者是 __libc_start_main:一个没有表的 C 函数。

3. ⭐⭐ 依赖的 [build] 编译输入从来没进过指纹

canonical_package_build_metadata 折进去的是身份、runtime 需求、链接意图 —— 不包括 buildConfig。只有根的编译输入经 canonical_compile_flags 进指纹。⇒ 改一个依赖的 cflags/defines/sources,指纹不变,消费者留在同一个输出目录,快路径重放改动之前生成的 build.ninja

⚠️ 显形方式是「这条改动像是没生效」:重建没用、touch 源码也没用,删掉 target/ 立刻就对。前两次观察正是用来推出「[build] defines 到不了路径依赖」的依据 —— 我推了、写下了,直到第三次测量推翻它。

prepare.cppm 里那段注释在这条修复之前就已经写着「canonical_package_build_metadata folds packages[].manifest.buildConfig」。现在它真的这么做了。

验证

宿主 import std 在 openkal 之上 ✅ 输出未变
openkal-llvm-runtime 的 C++ 例子 ✅ 8 项断言(含跨帧 throw + 展开期析构)
裸机 riscv64 import std sorted: 2 4 7 / caught: 42 / unwound: true / baremetal import std: ok
裸机 std-freestanding(回归面) ✅ 未受影响
openkal-musl posix 探针 ✅ 24 项断言 0 failures
openkal-opensbi hello

设计出处:mcpplibs/openkal 的 .agents/docs/2026-08-22-ecosystem-closure-design.md。生态侧的九个 PR 与本 PR 是同一条线。

这两个问题在没有人为这种目标建过 hosted 标准库之前是同一个,而有人建了之后
就不是了:mcpplibs/openkal-llvm-runtime 为一台没有操作系统的机器配置了
libc++、libc++abi 与 libunwind(实测 riscv64-none-elf 1431 个对象,
__cxa_throw 有定义),而它上面的程序拥有这条拒绝说它不可能拥有的东西。

拒绝本身保留 —— 在没有任何东西提供标准库时它是对的,而那仍是通常情况,
下面那段建议也仍然成立。变的是:一个包现在可以说不是这样,而且它用声明
其它 capability 的同一种方式说:

    provides = ["hosted-standard-library"]

用 capability 而不是三元组,因为这是依赖图的性质而不是目标的性质,
而依赖解析是这件事最早可知的时刻。

已验证三侧:
  宿主 import std           仍然可用(跑通,输出未变)
  freestanding 无提供者      仍然拒绝,消息逐字未变
  freestanding 有提供者      放行,错误前进到下一格

⚠️ 下一格是 mcpp 向编译器问 std.cppm 的位置(-print-library-module-manifest-path),
而那份是包 configure 出来的、编译器不知道的。让提供 capability 的包一并声明
它的 std 模块源,是一件独立的改动 —— 它还要让那个模块用包自己的 include 路径
去编,不是加一个字段就完。本 PR 不做,只把前提改对。
Sunrisepeak pushed a commit to mcpplibs/openkal-llvm-runtime that referenced this pull request Aug 22, 2026
构建工具在 configure 期拒绝没有操作系统的目标上的 import std,理由是「那种目标
没有 hosted 标准库」—— 而这个包正是一份。这件事是依赖图的性质,不是三元组的
性质,所以在这里声明、在那里读。

配套改动:mcpp-community/mcpp#486
上一条提交把 import std 的门改成问 capability,而放行之后立刻卡在下一格:

  error: source imports std but toolchain 'clang 22.1.8 (riscv64-none-elf)'
         provides no std module source

mcpp 用 -print-library-module-manifest-path 向编译器问 std.cppm 在哪。当标准库
是编译器自己的时候,那是对的问题;当它是一个包配置出来的时候,那是错的问题 ——
那份源码是为一个编译器毫不知情的目标配置的,而它的 include 路径和它自己的
__config_site 都在包里。

于是包自己说:

    [package]
    provides = ["hosted-standard-library"]
    std-module = "llvm-generated/std.cppm"
    std-module-flags = ["--no-default-config", "-nostdinc", "-nostdinc++", ...]

两个字段只从「同时提供那条 capability」的包读。一个包声明了 std 模块却不供给
标准库,是在描述一件它没有的东西。

## 包声明不了的那一半,由解析补上

std 模块源在任何要紧的意义上都是这个包的一个翻译单元,它够到 C 库的头的方式
和这个包其余的翻译单元一样 —— 通过它下面那些包发布的使用要求。包在自己的清单
里写不出那些:它们属于它的依赖,而它们的路径只有解析之后才知道。

用 publicUsage 而不是 privateBuild:模块编一次、被消费者 import,所以它该看见的
是这个包**发布**的头,不是它自己碰巧拿来构建的那些。两者不同,而且差别不是
装饰性的 —— 实测:用 privateBuild 时 libcxx/src 进了路径,std/expected.inc 报
「reference to 'unexpected' is ambiguous」;换成 publicUsage 就消失了。

定义也一样带上:一个 C 库的头会因为特性宏不同而显示成不同的库。实测:不带的话
模块编到 musl 的 <time.h> 就停在 clockid_t 上。

## 而工具链那份 sysroot flag 要被替换,不是被追加

那些 flag 描述的是编译器自带的标准库和宿主有的 C 库,而且它们以 -isystem 开头
—— 追加就意味着宿主的头排在包的前面,而后面任何 flag 都撤销不了。实测:模块
于是去编宿主 C 库的 <wchar.h>,停在那个库指望宿主编译器提供的名字上。

三元组因此要重述:它本来在被替换掉的那串里,不重述的话模块会为「正在构建的
这台机器」编。

已验证:宿主 import std 走包提供的源码跑通,输出未变;cxx 例子 8 项断言全过;
缓存键无需另行处理 —— 它由构建命令导出,而这些 flag 就在命令里。
Sunrisepeak pushed a commit to mcpplibs/openkal-llvm-runtime that referenced this pull request Aug 22, 2026
构建工具否则会向编译器问 std.cppm 在哪,而这一份是这个包为一个编译器毫不知情的
目标配置出来的:它的 __config_site 在 llvm-generated/,它的片段在
llvm/libcxx/modules/std/。

三个 flag 都是实测逼出来的,不是预防性的:
  --no-default-config  载荷的 clang++.cfg 无条件加一个宿主 C 库的头,
                       模块于是编那个库的 <wchar.h> 并停在它指望宿主编译器
                       提供的名字上
  -nostdinc/-nostdinc++ 同上
  -D_GNU_SOURCE        libc++ 的 locale 层去够 musl 只在这个宏下声明的名字
                       (vasprintf、strtof_l 等),LLVM 自己的 runtimes 构建
                       出于同样理由定义它
  -I llvm/libcxx/modules  模块源按相对名 include 九十来个片段,它们是上游的、
                       未改动,所以留在上游放的地方,由搜索路径点名那个目录

配套:mcpp-community/mcpp#486
freestanding/target.cppm 里 -fno-exceptions/-fno-rtti 上方的注释自己写着:

  a board that ships a target-built libc++abi and unwinder has a real case for
  turning these back on, and that is the point at which this becomes a
  manifest key.

那个点到了。一个为这个目标提供 hosted-standard-library 的包,就是那段描述的
「board」:它带着为这个目标编译的 libc++abi 与 libunwind。有它在场时,强制关掉
这一对反而是弄坏构建的那件事 —— 那份运行时是带异常编的,因为它「实现」异常,
而与它意见不同的图会报出上面那段注释引用的同一条:

  error: exception handling was enabled in precompiled file ... but is
         currently disabled

答案由图给,不是对目标的猜测:capability 在依赖解析期就已知。

## 而 std 模块要拿到目标自己的编译前缀

同一次运行里的第二条:

  error: precompiled file ... was compiled for the target ABI 'lp64'
         but the current translation unit is being compiled for target 'lp64d'

模块少了 --target/-march/-mabi/-mcmodel。它们本来在被替换掉的那串 sysroot flag
里,所以三元组连同整个 ISA 档位一起在解析处重述 —— 而 clang.cppm 不再自己拼
--target,那个职责归设置 stdModuleFlags 的一方,一处而不是两处。

已验证:
  宿主 import std        跑通,输出未变
  cxx 例子               8 项断言全过
  openkal-musl 裸机构建   通过
  std-freestanding 裸机   通过(这条改动的回归面)
  裸机 import std         std 模块与程序都编过了,链接停在 kal_* ——
                         而那不是缺陷,见下

⚠️ 裸机 import std 仍然链不成,原因不在这条链路上:openkal-opensbi 只实现
core(11 个名字:abort + stream + memory),而 musl 要 env/fs/process/task/time。
6.1 说消费者用了实现不提供的接口就该链接失败 —— 它正是这么做的。要在裸机上
跑 import std,需要一个提供 hosted 集的实现,那是另一件事。
## 1. -ffreestanding 也归图决定

target.cppm 这个函数上方的段落自己写着这个 flag 改变了什么:「no `main`
special-casing, no builtin-to-libcall rewrites it cannot back up」。两条都是
关于「有没有一个库在那里」的陈述 —— 而当图里有一个包为这个目标提供
hosted-standard-library 时,就是有。hosted 是语言自己对「不是 freestanding」
的叫法,提供这个 capability 的包断言的正是这个 flag 否认的那件事。

⚠️ 实测 2026-08-23,而它显形的方式不是关于这个 flag 的诊断。一个 main 写成
普通 C++ `int main()` 的裸机程序链接失败于 `undefined symbol: main`,而 nm 看
它自己的对象里是 `_Z4mainv` —— 在 -ffreestanding 下 C++ 的 main 不是保留的
入口点,于是像别的函数一样被修饰。启动对象引用 main,没有人定义它。

另一条路是让每个这样的程序写 `extern "C" int main()`,那是在给一个「构建替
程序作出、而且已经不再成立」的主张打补丁。

## 2. ⭐⭐ 依赖的 [build] 编译输入从来没进过指纹

canonical_package_build_metadata 折进去的是每个包的身份、runtime 需求和链接
意图 —— 不包括 buildConfig。只有根的编译输入经由 canonical_compile_flags 进
指纹。于是**改一个依赖的 cflags / defines / sources / per-glob flags,指纹不
变**,消费者留在同一个输出目录,快路径重放改动之前生成的 build.ninja。

⚠️ 它显形的方式是「这条改动像是没生效」。实测:给一个 path 依赖的
[build] cflags 加一个 flag,重建后生成的 unit_cflags 里没有它;touch 源码,
还是没有;删掉 target/ 立刻就有。前两次观察正是一个读者用来得出「这个 flag
被过滤了」的依据 —— 而我确实得出了这个结论并写了下来,直到第三次测量。

prepare.cppm 里根 flag 尾部合并旁边的注释,在这条修复之前就已经写着
「canonical_package_build_metadata folds packages[].manifest.buildConfig」。
现在它真的这么做了。
宿主 ELF 目标上 clang 默认给每个函数发 .eh_frame。裸机 ELF 目标上不发 ——
它假设没有人会展开。当目标侧**有**一个 C++ 运行时时这个假设是错的,而且错的
方式很具体:凡是 -fexceptions 编的都有表(libc++abi、libunwind 的 C++ 那半、
程序自己),其余全都没有 —— 在这套栈上就是 C 库和 libunwind 自己的 C 源码。

⚠️ 而**一套不完整的展开表不会降级,它会让走栈直接停住**。实测
riscv64-none-elf,这组读数值得留下,因为中间每一步都指向别处:

  __unw_get_proc_info  -> 0  start=80200148 end=8020068c lsda=80447190
  __unw_step           -> 0   (UNW_STEP_END)
  after step           -> 8021d55e

帧找到了、personality 数据找到了、这一步**确实算出了返回地址** —— 而 step
仍然报「栈底」,因为 libunwind 会为调用者重新求一次信息,而调用者是
__libc_start_main:一个没有表的 C 函数。_Unwind_RaiseException 本身住在
libunwind 的 UnwindLevel1.c 里,同样没有表,所以一次 throw 在第一步就结束在
`terminating due to uncaught exception`。

在此之前查过、并逐条排除的:__ehdr_start 为 0(是真缺陷,已由链接脚本改用
libunwind 的 _LIBUNWIND_IS_BAREMETAL 符号约定绕开)、.eh_frame 边界符号、
.eh_frame_hdr 的编码、FDE 覆盖范围与真实函数边界是否一致、线程指针。
**每一条都是对的。**

用异步形式而不是 -funwind-tables:那是宿主 ELF 目标本来就默认拿到的,而这整套
栈是照着那个行为开发出来的。

已验证:裸机 riscv64 上 `import std;` + vector + ranges::sort + throw/catch +
展开期析构 全部通过 ——

  sorted: 2 4 7
  caught: 42
  unwound: true
  baremetal import std: ok

代价:text 2.33 MB → 2.49 MB(+7%)。
Sunrisepeak pushed a commit to mcpplibs/openkal-llvm-runtime that referenced this pull request Aug 22, 2026
这一步需要四个还没有任何已发布 mcpp 做出的决定,而它们都属于工具而不是本包:
import std 的门问「有没有包为这个目标提供标准库」而不是「目标是不是
freestanding」;包可以带自己的 std 模块源;图里有为目标编译的 C++ 运行时时
-fno-exceptions/-fno-rtti/-ffreestanding 要摘掉;以及展开表要打开 ——
编译器为这类目标默认关掉它,而一套不完整的展开表不会降级,它会让走栈停住。

都在 mcpp-community/mcpp#486 上。在它发布之前,这个 job 用和这里每一个依赖
同样的方式把工具拿过来 —— 从分支 —— 这样下面那条判据是**真的在被执行**,
而不是一句「在某台笔记本上验过一次」的注释。

⇒ #486 发布后,删掉这一步并抬高 MCPP_VERSION。
@Sunrisepeak Sunrisepeak changed the title import std 的门该问「有没有」,不该问「是不是 freestanding」 import std 的门该问「有没有」;以及三处「由图决定」和一条让改动看起来没生效的指纹缺口 Aug 22, 2026
mcpp 从来没有把目标三元组告诉过编译器本身。每一个它能做的 hosted 交叉,都由一个
**driver 只有一个目标**的载荷伺候 —— x86_64-w64-mingw32-g++ 不需要 --target,
因为它没得选。所以 freestanding 之外没有任何地方发过 --target,而「driver 知道」
这个假设是成立的。

⭐ 目标侧改由**依赖图**供给的那一刻,它不再成立:C 库、C++ 运行时、那个平台自己的
openkal 实现,都是从源码建的包,而编译器是一个普通 clang —— 它会为**这台机器**
发码,除非有人告诉它别这样。

## 四处,都是实测出来的

1. `Triple::llvm_triple()` —— mcpp 的词汇(`aarch64-macos`)不是编译器吃的拼写
   (`arm64-apple-macos14.0`)。以前不需要这个函数,因为以前没人要说出这个三元组。

2. `Toolchain::crossTargetFlag` —— 在**同时知道请求和编译器**的地方定下来。
   ⚠️ 它不能从「targetTriple 非空」推出来:原生构建也有 targetTriple(探测来的),
   照那样判断会给每个工程的每次编译加上 `--target=<宿主>`。实测过,而它产出的
   不是关于目标的诊断,是生成的构建文件里的
   `/bin/sh: 1: Syntax error: word unexpected`。

3. prepare.cppm 那条「可重定向的 driver 必须被告知」的规则,原来限定在 freestanding,
   理由写着「hosted 交叉本来就解析到一个按目标的二进制」。openkal 让这句话不成立。
   ⚠️ 实测:aarch64-macos 的构建里,清单的 cfg 用了**请求的**目标(所以命令行上是
   C 库的 aarch64 头),而工具链的三元组还是宿主的(所以代码生成是 x86_64)。
   一条命令里两个答案,抓住它的是 C 库自己的断言:

     okm_float_assert.c: the C library and the compiler disagree about
     LDBL_DIG ('33 == 18')       33 = aarch64 binary128,18 = x87

4. `std.compat` 的**源**和**flag** 都还从工具链拿,而 `std` 已经来自包。
   ⚠️ 混用不在选择处失败,在工具链那份的头文件里失败:

     …/openkal-llvm-runtime/llvm-generated/std.compat.cppm:16
     …/xim-x-llvm/22.1.8/include/c++/v1/__config:13
         fatal error: '__config_site' file not found

   报的源是包的,打开的头是工具链的 —— 读这条消息,混用是看不见的。
   新增 `[package] std-compat-module`:提供一个模块的包提供两个,否则这一对不提供。

## 效果

`mcpp build --target aarch64-macos`(在 Linux 上)从「mcpp 拒绝:没有能在这台机器上
跑并产出它的工具链载荷」推进到「正在编译 libc++abi 自己的源码」。

⚠️ 还没到。剩下的是 libc++abi 的 guard 实现与 libc++ 的 locale 后端各自**按平台宏
选实现**,而「Mach-O 上的 musl」既不是 __APPLE__ 那份也不是 __linux__ 那份。

已验证无回归:same-source 宿主 ✅ / 裸机 ✅ / openkal-musl posix 24 条断言 0 failures /
普通程序 Linux→Windows ✅。
⭐⭐ `openkal-llvm` 不是第四个编译器,是**同一份 llvm 载荷**被问了一个不同的问题:
目标侧从哪来。

Gcc / Llvm / Msvc 回答「我能产出哪个目标」的方式都是「我的载荷是为哪个目标建的」
—— 一份 gcc 载荷**就是**它的目标;msvc 载荷只面向它运行的那台机器;连 llvm 载荷
也是这么用的,因为 mcpp 一直从一份按 (宿主, 目标) 的载荷里取目标的头文件和 C 库。

openkal 把后半句拿掉了。目标侧 —— 头文件、C 库、C++ 运行时、以及那个平台自己的
48 个函数的实现 —— 是**依赖图里的一组包**,由正在运行的那个编译器从源码建出来。
留给编译器的只剩代码生成,而 clang 一个二进制发它被建进去的每一种格式。

⇒ 所以这个族的目标覆盖不是一张载荷矩阵,是「clang 能发的每一个三元组」。

## 而「那个目标的实现到底存不存在」不归这一层问

openkal 6.1 在**链接期**回答它,并且报出解析不了的那个 `kal_*`。那比下面这条
拒绝好:前者说出缺的是什么,后者说的是另一回事。

## 三处改动

1. `Family::OpenkalLlvm` + `family_serves_every_target()`。载荷解析复用 llvm 那条
   —— 装了一个就有两个,因为本来就是一份。

2. 载荷门:项目说了自己的目标侧来自 openkal 时不拒绝。⚠️ 这个判断放在**依赖解析
   之前**,因为拒绝发生在那里 —— 而它答得出来,因为这是项目**关于自己**的陈述,
   不是关于图的事实。图不被查询,这是刻意的。

3. ⚠️ 目标表的 pin 不再覆盖它。实测:`--target x86_64-windows-gnu` 在 openkal 栈
   上解析到 `x86_64-w64-mingw32-g++`,**即使显式 `--toolchain llvm@22.1.8`** ——
   因为 `pinWouldOverruleUser` 只守着「来自记忆的默认目标」,而这个来自命令行。
   gcc 编不了 libc++ 的 std 模块。

## 效果:一句话,而不是每个目标一条 [target.X]

  [toolchain] default = "openkal-llvm@22.1.8"

  Resolved openkal-llvm@22.1.8 → aarch64-macos      → …/clang++
  Resolved openkal-llvm@22.1.8 → x86_64-windows-gnu → …/clang++

⚠️ 两边接下来都失败在 libc++/libunwind 自己的平台选择上(macOS 的 `Dl_info` /
`_dyld_register_func_for_remove_image`,Windows 的 `_locale_t`)—— **同一个形状,
两种目标格式**,而那属于运行时包的 port/ 覆盖,不属于工具链。

无回归:same-source 宿主 ✅ / 裸机 ✅。
⭐ `--target` 上了链接线之后,macOS 交叉从「链接器不对」推进到「链接器对了,
但被喂了宿主的 flag」。而宿主那套 C 运行时是从**三条**通道到链接线上的,
堵住一条会看到一模一样的报错。

## 1. 链接线上的三元组

编译时缺它产出的是给错机器的对象;**链接**时缺它选出的是错的**链接器**:
`-fuse-ld=lld` 点的是一个家族,而驱动按目标挑形态 —— ELF 的 ld.lld、
Mach-O 的 ld64.lld、PE 的 lld-link。没有目标就挑宿主的。

  ld.lld: error: obj/main.o: unknown file type

—— ELF 的链接器拿到 Mach-O 对象,描述得完全准确,而它一个字都没说自己为什么
是跑起来的那一个。

## 2. 三条通道

| 通道 | 它是什么 | 症状 |
|---|---|---|
| `link_toolchain_flags` | `lm.link_flags()` + 驱动的 C++ 运行时选择 | `unknown argument '--as-needed'` |
| `payload_ld` | 同一组,第二次 | **一模一样的报错** |
| `atomic_ld` | 宿主的 libatomic + GNU ld 的 push-state 拼法 | 同上 |

⚠️ **第二条是这里的发现**:只堵第一条的人会看到完全相同的错误,并合理地
得出「这条修复没生效」的结论。

## 3. 判据

在 Linux 上 `mcpp build --target aarch64-macos` 产出:

  Mach-O 64-bit arm64 executable,唯一依赖 /usr/lib/libSystem.B.dylib,
  外部符号三个(两个借的 + 一个属于格式的)

无回归:same-source 宿主 ✅ / 裸机 ✅ / openkal-musl posix 24 条 0 failures。
两条都由「Linux 宿主 → x86_64-windows-gnu,目标侧来自图」暴露,而两条都是同一
句话的第三次和第四次出现:「每个交叉都由载荷伺候」时成立的假设,在目标侧来自
依赖图时不再成立。

── 1. mingw 分支提前 return,整条 link_toolchain_flags 被跳过 ──

flags.cppm:1204 的注释写明了理由:「MinGW PE link … No rpath/loader/payload
model」。那在载荷的 driver 本身就是目标时是对的 —— x86_64-w64-mingw32-g++ 没有
第二个目标可选,不需要被告知。openkal 下编译器是一个普通的可重定向 clang。

⚠️ 实测:每个对象都编过了,然后

    ld.lld: error: obj/…/types.m.o: unknown file type      (× 30)

COFF 对象递给了 lld 的 ELF driver,因为编译行有 --target= 而链接行没有。三十条
准确的报错,没有一条提到缺的那个 flag。

── 2. graph_runtime_compile_flags:一个函数,不是两个 if ──

-fdwarf-exceptions(PE)和 -femulated-tls(PE + Mach-O)不是普通 flag:它们改变
一个翻译单元为 throw 和 thread_local 发出什么。两个不一致的对象能链接,然后
不一致本身就是 bug。

两者都由一个事实推出 —— Toolchain::targetCxxRuntime,即 C++ 运行时/展开器/C 库
来自图而不是编译器载荷。编译器对这两项的默认值是为平台自己的运行时选的,而那
正是没在用的东西。

  -fdwarf-exceptions  PE 上 clang 默认 SEH,人格例程 __gxx_personality_seh0 和
                      .pdata/.xdata 来自操作系统的展开器;图里带的是 libunwind,
                      它读 .eh_frame。
  -femulated-tls      PE 的 _tls_index 和 Mach-O 的 _tlv_bootstrap 都由动态
                      加载器 bootstrap,自包含镜像没有加载器。

⚠️ ELF 故意不在第二项里,不是遗漏:那里 thread_local 是相对线程指针的固定偏移,
C 库自己就建立了它。加上去能用,代价是每次访问一次间接 —— 而且会让 ELF 成为
唯一一个 thread local 布局与同目标其它构建不同的目标。

⚠️ 写成函数而不是两处 if,因为编译命令在两个地方拼:hostflags.cppm 拼普通翻译
单元,prepare.cppm 拼 std 模块。一个用 SEH 建的 std.pcm 被用 DWARF 建的单元
import,两个文件都看不见。「一个事实两条通道」在这套生态里已经制造过三次相同的
缺陷,这次是把第二条通道去掉,而不是叮嘱它记得。

── 实测结果 ─────────────────────────────────────────

一台 Linux 宿主,一份 src/main.cpp,四个目标:

  x86_64-linux-gnu    静态 ELF        跑通
  x86_64-windows-gnu  PE32+ 15 段     跑通(wine),依赖只有 KERNEL32/ntdll/
                                      SHELL32/api-ms-win-core-synch,没有任何 CRT
  aarch64-macos       Mach-O arm64    产出
  riscv64-none-elf    RISC-V ELF      产出
    if constexpr (is_windows)          { … }   // 这台机器是 Windows
    else if constexpr (needs_explicit_libcxx) { … }   // 这台机器是 macOS
    else                               { … }   // 这台机器是 Linux

三个答案各自描述「在那台机器上怎么链接」:一个 SDK 路径、一个部署目标、一条
加载器搜索路径、这台机器的 libatomic。目标是宿主自己、或由载荷伺候时,它们都对。

⚠️ 而**只有第三支消费 link_toolchain_flags**,`--target=` 就在里面。⇒ 从 Linux
宿主交叉编译 openkal 目标链接正确,从 macOS 或 Windows 宿主则会把一个 Mach-O 或
ELF 递给一个没被告知目标的链接器。那种失败的样子已经有记录 —— PE 那条从另一个
方向撞到了同一堵墙:

    ld.lld: error: obj/…/types.m.o: unknown file type      (× 30)

⭐ 这一条不是靠 CI 发现的,是**改完 PE 之后主动去找第二条通道**找到的。风险表
R4 写着「这个形状已经出现三次,应当在改动后主动找第二条」。

── 修法:整条替换,而不是编织进去 ──────────────────────

文件自己给了先例 —— freestanding 那块的注释:「Applied LAST and by REPLACEMENT
rather than woven in above … 先前做的每个 hosted 链接决定不只是多余,而是错的,
往一条已经带着它们的命令行后面追加 -nostdlib,会让结果取决于驱动的 flag 顺序而
不是取决于任何人做的决定。」目标侧来自图是同一种情况。

⭐ 而且它**替换掉了一个特例而不是新增一个**:PE 分支里我先前为一种格式加的那份
拷贝去掉了,现在一条规则覆盖 PE / Mach-O / ELF,在每一种宿主上。

留下的,以及每一条为什么不是宿主的:
  full_static           契约表的,按目标的**格式**取
  link_toolchain_flags  --target= / --no-default-config / -fuse-ld=lld
  link_intent_ld        用户要建的是什么(exe/shared/static)
  user_ldflags          清单自己的话
  link_extra            -flto / -s

去掉的:b_flag(这台机器的 binutils)、runtime_dirs 和随它的 -rpath、payload_ld、
atomic_ld。

⭐ 实测收益不止于宿主维度 —— macOS 产物的链接行上原本有

    -L…/xim-x-llvm/22.1.8/lib/x86_64-unknown-linux-gnu
    -Wl,-rpath,…/lib/x86_64-unknown-linux-gnu

ld64 接受 -rpath 并把它写进镜像。修完之后 LC_RPATH 为空。

── ⚠️ 谓词收窄:两个条件,不是一个 ───────────────────────

第一版写成 !crossTargetFlag.empty(),太宽:它对**每一个**被指向 hosted 目标的
可重定向 clang 都成立,包括由载荷伺候的 musl / glibc 交叉 —— 那些仍然需要这台
机器的 -B、runtime 目录和 C 运行时 flag,因为对它们来说载荷就是目标侧。

targetCxxRuntime 单独用则朝另一个方向太宽:它只说「有个包提供 C++ 运行时」,
而那对该包的**本机**构建同样成立,那里载荷的链接模型是对的。

两个一起才是这次替换所依赖的事实:C 库 / C++ 运行时 / 平台都是包,**并且**我们
把编译器指向了一个不是这台机器的目标。

实测(收窄后重跑):四个目标全部产出,三个能在本机跑的输出逐字相同;另造一个
非 openkal 的 --target x86_64-linux-musl 最小工程,走的仍是旧路径,产物能跑。
「编译器发出调用而 C 库不定义的那些例程在哪个库里」和「把 .def 变成导入库的工具
叫什么」都是构建程序真正需要问的问题,而在这个字段之前唯一的问法是去看
toolchain_dir() 的目录名并认出它。

⚠️ 同一天的两次实测,都出自这一个缺口:

  openkal-musl    在编译器是 clang 的链接上写了 -lgcc
                  → lld: error: unable to find library -lgcc
  openkal-windows 在 GCC 工具链下跑 llvm-dlltool
                  → sh: 1: llvm-dlltool: not found

两个包各自把「作者那台机器的工具链」当成了普遍事实。

MCPP_COMPILER = "gcc" | "clang" | "msvc" | ""(与 CompilerId 一一对应),
mcpp::compiler() 读它。
    clang++.exe -std=c++23 --precompile "…/std.cppm" -o "…/std.pcm"
    …/llvm-generated/std.cppm:16:10: fatal error: '__config' file not found

五个 token。同一份构建从 Linux 宿主出发时带的是 --target= / --no-default-config /
-nostdinc / -nostdinc++ 和八个 -I。报错指着一个头文件,而原因是一条按「哪台机器在
构建」分的分支。

⚠️ 这条分支写在「Windows 宿主只为自己构建、对着 MSVC STL」的年代,那时
stdModuleFlags 还不存在 —— 所以遗漏当时不可见:没有东西可漏。它在「包可以自带 std
模块」之后才变成缺陷,因为那个字符串正是包自己的头、-nostdinc 和目标三元组所在。

⚠️ 按规矩找了第二条通道:std_compat_build_commands 没有 Windows 分支,一直带着
extraFlags。

⇒ 这是 host-dimension 那个新 job 挖出来的第二处 —— 第一处是链接行按宿主分的三支。
⚠️⚠️ 这一条是被「在真机上跑一次」挖出来的 —— 程序链接成功、ad-hoc 签名合法,
然后在 arm64 Mac 上:

    stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000

没有输出,没有栈帧:在第一次间接调用处跳到了地址 0。

⭐ 镜像里有 1238 个 __stubs 项和 1335 个 __got 槽,而未定义符号只有**三个**。
stub 的名字是它自己的 —— std::vector<int>::__init_with_size、operator new,以及
一千多个 libc++ 内部符号。而 main 的第一条语句正是构造 std::vector<int>。

真因:Mach-O 上「默认可见 + 弱(linkonce_odr)链接」的符号 —— 也就是每一个模板
实例和内联函数 —— 是**由动态加载器合并**的,所以链接器把对它们的调用绕经 stub 和
一个由加载器填充的 GOT 槽。那是「一个定义在多个 dylib 间胜出」的机制,而自包含的
镜像用不上它。

包用 _LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS 建 libc++(对静态构建是对的),于是每个
实例都停在默认可见性。⚠️ 在 ELF 上这是惰性的 —— 静态链接在**链接期**解析弱定义,
没有东西活到运行期。在 Mach-O 上它产出上面那套机制。

⇒ 实测加上两个 flag 之后:__stubs 0x3a08 → 0x6c,__got 0x29b8 → 0x58。九个 stub、
十一个槽,正是一个有三个导入的程序该有的规模。

⚠️ 只对 Mach-O。ELF 和 PE 上加它也不会错,但那两条是绿的,而这里要修的是「弱定义
在这个格式上是运行期机制」这一件具体的事。
── 1. Mach-O:自包含镜像不该把内部符号交给加载器合并 ──

⚠️⚠️ 由「在真机上跑一次」挖出:程序链接成功、ad-hoc 签名合法,然后在 arm64 Mac 上
EXC_BAD_ACCESS,PC=0,一个栈帧都没有。

镜像里 1238 个 __stubs、1335 个 __got 槽,而未定义符号只有三个;stub 的名字是它
自己的 libc++ 内部符号,而 main 的第一条语句正是构造 std::vector<int>。

真因:Mach-O 上「默认可见 + 弱(linkonce_odr)链接」由动态加载器合并,链接器把
调用绕经 stub 和加载器填充的 GOT 槽。ELF 上这是惰性的(静态链接在链接期就解析弱
定义),Mach-O 上它是运行期机制。

⇒ -fvisibility=hidden -fvisibility-inlines-hidden(仅 Mach-O)。
实测:__stubs 0x3a08 → 0x6c,__got 0x29b8 → 0x58。

⇒ CI:the artefact built on Linux runs on macOS —— **pass**。

── 2. std.compat 的命令用相对路径,而 cmd.exe 的 cd 不换盘符 ──

    std.compat.cppm:84:8: fatal error: module file 'pcm.cache\std.pcm' not found

而 std.pcm 在上一条命令里刚刚构建成功。构建缓存在用户目录(C:)、检出在 runner 给
的位置(D:),`cd X && …` 成功而盘符不动,于是每个相对路径都相对错了根。

⚠️ 显然的修法是加一条 #if defined(_WIN32) 分支(std 那个构建器就有一条)。写了又
撤回:**一个只在一种平台上编译的分支是这台机器无法检查的分支**,而这一轮在宿主维度
找到的每一处缺陷都正是这个形状 —— 按「哪台机器在构建」分的代码。绝对路径到处都
对,所以只留一种形式。
    if (t.find("apple")) return MachO;

apple / darwin 是 LLVM 的词。mcpp 的规范形式是 aarch64-macos,两个都不含 —— 于是
这个判断在**每一种宿主上**都落到了「问宿主」,而它产生的两种失败方向相反:

    Linux 宿主 + macOS 目标   → 给 Mach-O 用了 Elf 的契约
    macOS 宿主 + Linux 目标   → 给 ELF 用了 MachO 的契约

⚠️ 实测第二种(macOS runner 交叉到 x86_64-linux-gnu):

    ld.lld: error: unable to find library -load_hidden
    …/xim-x-llvm/22.1.8/lib/libc++.a: archive member 'system_error.cpp.o'
      is neither ET_REL nor LLVM bitcode

-load_hidden 是 Mach-O 链接器的词,那个归档是**宿主的** —— 两个都是因为「格式」
这个问题被用「哪台机器在构建」回答了。

⇒ 先用已解析的 triple 回答(is_pe / os == macos / linux / none);字符串判断保留
在后面,作为词表解析不了的三元组([target.X] 逃生口)的兜底,那里只有拼法可依。
    ld64.lld: error: library not found for -lc++

这个块能给出的每一个答案都是「链哪一个运行时」——系统的(-lc++)、工具链的
(-load_hidden …/libc++.a),或者其中之一的静态形式。三个在「运行时是产物必须
被接上的东西」时都对,在「图里已经有一个包为这个目标编好了它、而它的对象就在链接
线上」时都错。

⚠️ 实测就发生在上一处(格式判据改成按目标)修好之后:aarch64-macos 此前一直落到
Elf,而这个块在那里什么都不贡献 —— **一个问题的错答案在遮蔽另一个问题的错答案**。

⇒ 没有东西要找,也没有东西要去找。

从全清缓存复验:四个目标全部产出;linux / windows(wine)/ riscv64(qemu)三个
跑通且输出逐字相同;macOS 产物是 Mach-O arm64,唯一依赖 /usr/lib/libSystem.B.dylib。
    ld64.lld: error: library not found for -lc++

⚠️ 我的第一版修法把整块跳过了,那说错了话,而且丢东西:那一块还负责产物格式、
MinGW 判定、macOS 下限这些下游要用的事实,跳过它们一起没了。

⭐⭐ 而这一块里本来就有正确形状的先例 —— mi.freestanding:

    // 裸机短路整张表:下面找到的归档是**宿主的**
    if (in.freestanding) { m.effective = SelfContained; m.unitFlags = " -nostdlib++"; }

图供给运行时是同一件事的 hosted 形态。表里三个答案全都在「点名一个要链的运行时」
(系统的 / 工具链的 / 其中之一的静态形式),三个在「运行时是产物必须被接上的东西」
时都对,在「它已经在里面」时都错;而它会找到的归档是宿主的。

⇒ 加 mi.graphCxxRuntime,与 freestanding 走同一条短路。

⭐ 并且它比「跳过」更强:-nostdlib++ 是**主动**告诉驱动别加它自己那套,而不是
沉默。答案不是「在这里改挑 openkal 的那个」—— openkal 的那个**就是那些对象**,
没有库可点名,诚实的 flag 是阻止驱动自作主张的那一个。

实测:unit_ldflags = -nostdlib++;四个目标全部产出,三个能在本机跑的输出逐字相同。
dc6eb34(#436)误提交,在 main 上待了一周。名字读起来像一个本该被展开而没有展开
的变量(`> $binDir`),内容是空的。

顺手写进 .gitignore,让同样的手滑下次被挡住而不是再被 review 一遍。
    mcpp-linux-musl: ELF 64-bit LSB executable, x86-64, …
      dynamically linked, interpreter /lib/ld-musl-x86_64.so.1

而其它每一种宿主都产出静态的。

⚠️ 这条是上一处修复(产物格式改成按目标判)暴露出来的:Windows 宿主上给 ELF 目标
的 -static 一直是从 **C++ 运行时契约**里来的,而那个契约选了 PE 那一格 —— 因为格式
这个问题原先是用「哪台机器在构建」回答的。把答案改对,那条 flag 就没了。

⇒ 三支宿主分支里,另外两支都带 full_static,只有 Windows 那支没有。整条静态链接是
目标的性质(target_supports_full_static + 清单的 linkage),所以它属于每一条链接行。
@Sunrisepeak
Sunrisepeak force-pushed the feat/import-std-capability branch from 594b519 to 97ba75a Compare August 23, 2026 01:48
    lld-link: error: obj/mcpplibs_openkal-linux/src/env.o: unknown file type

ELF 对象递给了 lld 的 MSVC 驱动。

⚠️ 图替换取的是 link_toolchain_flags,而那个字符串只在 isClangWithCfg(载荷旁边有
clang++.cfg)时才被填。Linux 载荷带一份,Windows 载荷不带 —— 于是在 Windows 宿主上
整条替换一个 --target= 都没发出来,clang 用了它自己的默认。

⇒ --target= 不是「有没有配置文件」的函数,它是「这是给哪台机器的」的全部内容。
在替换处就地拼:crossTarget + (有 cfg 才加 --no-default-config) + -fuse-ld=lld。
── 1. MCPP_TARGET_REQUESTED —— 「有没有被指名一个目标」 ────────

MCPP_TARGET 回答「这是给哪台机器的」,没给 --target 时用宿主填上,那对那个问题是
对的。但它回答不了平台包必须问的另一个问题:**这次构建有没有被指向一个目标**。

⚠️ 两者不同,即使三元组相等:在 arm64 Mac 上跑 mcpp build --target aarch64-macos
指的就是宿主那台机器,而目标侧仍然来自图 —— 于是本工具不往链接线上放任何系统
SDK,而知道这个系统的那个包是唯一能点名一个的东西。同一台机器上的本机构建拿得到
SDK,不需要包供给任何东西。

实测(openkal-macos 试图用手头能拿到的东西判断这件事):
· 用宿主判 → 交叉对,而在 Mac 上 --target aarch64-macos 错
              (library not found for -lSystem)
· 用 MCPP_TARGET 判 → 交叉对,本机构建错,因为它**从不为空**
              (undefined symbol: wcslen / strtoul / __error —— 包里三个名字的
               stub 遮蔽了厂商那份完整的)

⭐ 旧版 mcpp 两个都不设,而那对它是**正确**的答案:它没有「目标侧来自图」这回事,
系统永远在链接线上,包不该供给任何东西。

── 2. .github/workflows/openkal-cross.yml —— 3 宿主 × 3 目标 ──

cross-build-test.yml 验证的是由**载荷**伺候的交叉:驱动只有一个目标,宿主和目标是
绑在一起的,所以一行一种组合是诚实的形状。

openkal 把问题的形状改了:目标侧是依赖图里的一组包,编译器是普通的可重定向 clang。
由此得出的断言是 N 宿主 × N 目标塌缩成 N 个实现加一个工具 —— **构建的那台机器不再
是一个变量**。

⚠️ 而这个形状的断言在本仓库错过。从 Linux 宿主到达 PE 需要四处分别的修复,加上另外
两台宿主又找到七处,每一处都是「按哪台机器在构建」而不是「按输出给哪台机器」分的
决定 —— 链接行的三支、std 模块命令的 Windows 分支、cmd.exe 的 cd 不换盘符、格式
判据匹配 LLVM 的 apple 而不是 mcpp 的 macos、契约在运行时已在对象里时仍去点名一个
库、缺 -nostdinc、-lgcc 在 clang 的链接上。没有一处是从一台宿主看得见的。

⇒ 三个构建 job(每个产三个产物,共九个);三个运行 job(每个执行**三个宿主**为它
产出的那一个)。对角线是普通的本机构建,六个非对角格才是那个断言。

⚠️ 运行的三个 job 什么都不装 —— 不装 mcpp、不装编译器、不装 C 运行时。判据是
unwound: true:链接骗不出「析构函数在异常被带出栈帧时跑到了」。
首跑就红:

    xlings: version '2026.8.17.1' not found for 'mcpp'
      available: 2026.8.19.4

仓库根的 .xlings.json 钉的是「构建 mcpp 的那个 mcpp」,而这个 pin 不随 mcpp 发布
移动 —— 于是它指向一个索引里已经没有的版本,而在检出目录里裸装会**听 pin 的而不是
听参数的**。

⭐ .github/actions/bootstrap-mcpp 早就知道这件事(它跑 install_pinned_mcpp.sh),
三个系统都支持,而且落在其它每个 job 都落的那条缓存血统上。

⇒ 两套引导就是两处要保持正确的东西,而第二套写出来不到一天就错了。
    clang++: warning: argument unused during compilation: '-nostdinc++'
    clang++: warning: argument unused during compilation: '-isystem …'
      (共十九条,每个 include 目录一条)

stdModuleFlags 同时携带「给哪台机器」和「头文件在哪」。构建这个模块有两步,只有
第一步两样都要:第二步编译的是 BMI,而 BMI 已经包含头文件贡献的一切。

把前半单独记为 stdModuleTargetFlags,第二步只用它。

⭐ 核验方式是产物而不是「编过了」:同一个 codegen 步骤,用拆分后的 flag 和用完整
flag 各跑一次,std.o **逐字节相同**(sha256 a6d837241e7f02b2,736 字节),而后者
产生十九条警告。⇒ 被丢掉的 flag 确实无用。

⚠️ 这些警告在每个平台上都存在,而在每个平台上都看不见:非 Windows 的命令以 2>&1
结尾,mcpp 又丢弃成功命令的输出 —— Windows 那条没有重定向,所以它是第一次被看见的
地方。⇒ 噪声本身不是缺陷,「十九条正确而无意义的警告排在任何有意义的警告之前」
才是。
本 PR 改了 16 个 src/ 模块而新增测试为零,同时**三个新增或改动的纯函数各自都有一个
现成的测试文件就在旁边**,覆盖为零:

  Triple::llvm_triple()             test_toolchain_triple.cpp (258 行)  0
  graph_runtime_compile_flags()     test_hostflags.cpp        (218 行)  0
  distribution 的两条短路           test_distribution.cpp     (559 行)  0

⚠️ 而本轮在三台宿主上修的九处缺陷里,至少三处是**纯函数的错误答案**:产物格式判据
匹配不到 aarch64-macos、契约在运行时已在对象里时仍点名一个库、llvm_triple 的拼法。
它们各自可以用一个不到十行、不需要编译器也不需要网络的断言判定,而实际是用三台
runner 的完整交叉构建发现的。

⇒ 一个在一秒内失败的断言和一个在四十分钟后失败的断言,发现的是同一个缺陷,而前者
会被更早地跑到。

── 新增 15 个断言 ────────────────────────────────────

Triple:llvm_triple 的六种拼法(含 aarch64→arm64 改名与 macOS 版本号后缀、
freestanding 原样返回);以及「os 字段能认出 macos 而三元组里没有 apple/darwin」——
那正是格式判据用子串匹配时失效的原因。

GraphRuntimeFlags:PE / Mach-O / ELF / 载荷伺候 / 三元组无法解析 五态。⚠️ ELF 取零
是决定不是遗漏,注释写明理由。

Distribution:freestanding 与 graphCxxRuntime 两条短路,跨三种格式、跨三种被请求的
契约。⚠️ 前者是既有行为,此前也没有测试。

── 每个测试都以注入缺陷核验过它会红 ─────────────────

  "arm64" → "aarch64"                    ⇒ LlvmTripleMacos* 红
  if (freestanding || graphCxxRuntime)
    → if (freestanding)                  ⇒ GraphSuppliedRuntime* 三条红
  if (os == "macos") → if (false)        ⇒ MachOTakes* 红

⚠️ 第一次注入没红,而那不是缓存问题:replace(…, 1) 只换了第一处出现,而那处不在
llvm_triple 里。一个「没红」的注入要先确认它真的改到了被测的代码。
speak-agent and others added 6 commits August 23, 2026 10:55
MCPP_TARGET 在没人指名目标时用宿主填上,对「这是给哪台机器的」是对的,而对平台包
必须问的另一个问题无用:**这次构建有没有被指向一个目标**。

⚠️ 这个问题的两种读法都已经在 openkal-macos 上被实测判错过:
· 用宿主读 → 交叉对,而 Mac 上 --target aarch64-macos 得到 library not found for -lSystem
· 用 MCPP_TARGET 读 → 交叉对,本机构建得到 undefined symbol: wcslen(它从不为空,
  于是包里三个名字的 stub 遮蔽了厂商完整的那份)

断言:本机构建为空;指名目标时携带被指名的三元组。

⚠️ 探针以**非零退出**汇报 —— mcpp 只打印失败的构建程序的输出,返回零的探针输出会被
丢弃,那样这个测试什么都不断言。
⚠️ 同一行上带阳性对照(MCPP_TARGET 必须被填上):没有它,mcpp 若停止设置全部变量,
上面那条断言同样会通过。

注入核验:把 MCPP_TARGET_REQUESTED 也改成「空则填宿主」⇒ 测试报
'should be empty for a native build' 并退非零。
    error: target 'x86_64-linux-musl' cannot be built on this host —
           no toolchain payload exists that runs here and produces it

我写的注释说「每个宿主都能解析它」,而那是一句假设。

⇒ 改用**宿主自己的三元组**,而这恰好是更强的那一格:--target <host> 让
MCPP_TARGET 与宿主三元组相等,而 MCPP_TARGET_REQUESTED 非空 —— 因为确实指名了一个
目标。用一个异己三元组的测试,会被一个只是回显 MCPP_TARGET 的变量骗过去。

而且它不需要任何载荷:宿主自己的目标是每个宿主按构造都有的那一个。
⚠️ 它此前是一个一千五百行函数里的 lambda —— **那正是它没有测试的原因**。而它答错的
那件事(用 LLVM 的 apple/darwin 匹配 mcpp 自己的 aarch64-macos)是靠三台宿主对三个
目标跑出来发现的,四行断言本可以在一秒内发现。

⭐ 提出来之后,「宿主兜底」变成一个参数而不是编译期常量 —— 于是这个函数可以被检查,
而不必成为它所描述的那台机器。

新增三组断言:
· 词表内的六个三元组,对**每一种兜底**都必须给出同一个答案(目标决定格式,而不是
  构建的那台机器);
· [target.X] 逃生口:词表解析不了的拼法上,LLVM 的词被认出来;
· 兜底只在三元组什么都没说时才被用到。

注入 `os == "macos"` 那一行删掉 ⇒ 第一组变红。
    std.cppm:167:15: warning: 'std' is a reserved name for a module
      [-Wreserved-module-identifier]

export module std; 是保留标识符,每一个自带 std 模块的标准库都会触发它;非 Windows
那条命令从写下的那天起就带着抑制。这条分支把它绑在 .ixx 上,而那在「Windows 宿主
见到的唯一 std 模块是 MSVC STL 的」时是对的 —— 在包可以自带一个之后就不对了。

一条正确的、不可避免的、每次构建都打印的警告,是会遮蔽下一条的噪声。

⚠️ 这是同一个函数里的第三处不对称(前两处:不带 extraFlags、cd 不换盘符)。
    error: git clone of 'https://github.com/…' failed:
    Cloning into '/home/runner/.mcpp/git/63269d80b47f71e6'...

git 一个字都没说,那正是连接在传输中途断掉的样子。2026-08-23 实测两次:一次在 CI,
一次在本机是 TLS connect error: … unexpected eof while reading。

⚠️⚠️ 而第一版只给 clone 加了重试 —— 用一个不存在的仓库做探针,它在**一秒**内就失败
了,因为先跑的那一步是 git ls-remote,而它仍然是裸的。**在一条路径的一半上加重试,
是一个「报告自己已被加上」的重试。**

⇒ 提成一个共用的 run_with_network_retry,两处都用它。

⚠️ 三次尝试,最后一次的失败**原样上报**:错的 URL 和不存在的分支与瞬时故障失败方式
完全相同,所以它分辨不了、也不去分辨 —— 一次永久性失败的代价是三秒,而报告与从前
一字不差。把真错误藏在重试后面是更坏的交换。

⚠️ 回调在失败的一次之后运行:clone 需要把残留目录删掉,否则 git 下一次会报
「already exists and is not an empty directory」—— 第二个、不同的错误,而它对第一个
只字不提。

实测:永久性失败仍然打印 remote: Repository not found,耗时 6s(两次退避);正常
路径无额外开销;92 个单元测试全过。
… is known

A build must answer where the target's platform interface, C library and C++
runtime come from before it can emit a command line. Three places answered it
with three different criteria:

    prepare  openkalTargetSide  the toolchain family name is "openkal-llvm"
    flags    graphTargetSide    targetCxxRuntime && !crossTargetFlag.empty()
    dist     graphCxxRuntime    targetCxxRuntime

They disagreed on the case none of them was written for. Measured, a pure C
program crossed to macOS over the openkal stack:

    ld64.lld: error: .../lib/x86_64-unknown-linux-gnu/libc++.so:
                     unhandled file type

The first criterion admitted the build; the second rejected it, because a C
program has no C++ runtime in its graph, so the payload's own libc++ stayed on
the link line and a Linux shared object reached a Mach-O linker.

The three did not disagree by accident. mcpp serves two ways of supplying a
target side and the moment each is knowable is opposite: a prebuilt directory
before dependency resolution, a set of packages only after it. All three ran at
the earlier moment and guessed the later answer. The fix is not a better guess.

mcpp.targetside resolves it once, after the graph is known, for three layers
separately -- the kernel ABI (the triple's OS field), the C ABI (its
environment field), and the C++ runtime (which has no field of the triple
because it sits above the ABI). Each layer records where it came from, which
interface it answers to and which package supplies it. It is a pure function of
plain data with no I/O, so its whole table is asserted from unit tests; the
capability it replaces drove seven behaviours from inside a 7000-line
translation unit and had zero test coverage.

The layering rule is structure rather than a later check: the payload's C++
runtime is selected only when the C library is also the payload's, because that
runtime was configured against that library and never against another one. The
default path therefore cannot construct the combination that produced the
measured failure.

Packages name the layer they supply through a reserved capability namespace:

    provides = ["mcpp:kernel-abi=openkal"]

mcpp hardcodes the three layer names and no implementation. Names outside the
prefix pass through untouched, so the feature system's own capabilities keep
working; names inside it are a closed set, so a misspelling is an error rather
than a silently disabled behaviour. The older `hosted-standard-library`
spelling continues to be recognised.

Also:
  * the host-serviceability refusal is decided where it was and released after
    the graph is known, since a dependency can supply a target's system and the
    old message asserted the machine could not build the target at all;
  * a target row's pin no longer overrules an explicitly stated toolchain,
    which removes the openkal-specific exception that guarded it;
  * the build reports the resolution, so what reaches the link line is
    observable instead of being re-derived by a reader from three manifests;
  * `linkage = "dynamic"` says so when the graph supplies the system and the
    artifact is therefore static;
  * `sysroot = ""` is documented as "no prebuilt C library directory", which is
    what it selects, rather than "no C library", which it does not.

Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD
@Sunrisepeak Sunrisepeak changed the title import std 的门该问「有没有」;以及三处「由图决定」和一条让改动看起来没生效的指纹缺口 Resolve the target side once, per layer, after the dependency graph is known Aug 23, 2026
…eep working

268 covers the wiring the unit tests cannot: that a package's `provides` line
reaches the resolver, that the resolution reaches the report, that a
misspelling inside mcpp's reserved namespace stops the build before anything is
compiled, and that a name outside it passes through untouched. It asserts on
the report rather than on a successful link, because the resolution is printed
during planning and a link additionally needs a working host C runtime, which
is a different thing to test.

269 covers the compatibility promise. `openkal-llvm` used to carry the fact
that a project's target side comes from packages; it now carries nothing, and a
manifest written against it must still resolve to the same driver and must NOT
still decide anything. The second half is the load-bearing one: both projects
in that test have an empty dependency graph, so both must report a target side
supplied entirely by the payload.

Both were checked against an engine without the feature and go red there.

Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD
BSD sed reads the argument after -i as a backup suffix, so the in-place edits
written for GNU sed failed on the macOS leg:

    sed: 1: "sys/mcpp.toml": unterminated substitute pattern

The provider manifest is now written from scratch for each case by a helper,
which is also clearer about what each case declares. A case asserting that a
name outside the reserved namespace passes through now additionally asserts
that the layer declared beside it still resolves, so the case cannot pass by
the manifest having been rejected wholesale.

Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD
…up files

A hosted target whose C-ABI layer is absent is a real shape: a program that
calls the platform interface directly depends on the platform implementation
and nothing above it. The driver does not know that. Told to emit for a hosted
triple it supplies crt1.o, crti.o, the gcc startup objects and a dynamic
linker, all of them the host machine's, and the hermetic link check reports
them one by one:

    /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o (outside the sandbox)
    /lib64/ld-linux-x86-64.so.2 (outside the sandbox)

`-nostdlib` removes the startup objects; `-static` removes the interpreter,
which is not a policy choice either -- a dynamic executable names an
interpreter and lets a loader resolve its imports, and with no C library there
is nothing to resolve.

Measured end to end afterwards: openkal-linux with the standalone feature plus
the five-function C surface builds a static ELF that runs and prints.

Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD
Sunrisepeak added a commit to mcpplibs/openkal-llvm-runtime that referenced this pull request Aug 23, 2026
* openkal-llvm-runtime 0.1.0:C++ 运行时在 openkal 之上

libc++、libc++abi、libunwind,为 openkal-musl 配置,而不是为某个宿主 C 库。

一份 C++ 标准库不像程序那样可移植:它是为某一个 C 库配置、并对着那个库的头
编译的,而「能找到头」和「为这个目标配置过」不是一回事。

vendored 的是四个 runtimes 子树,revision 与工具链自身构建所用的一致;
编译器和链接器不 vendored、不改 —— 它们按三元组生成代码,而 openkal 不改变
任何架构或目标格式。

配置只决定两位:
  _LIBCPP_HAS_MUSL_LIBC     1    底下的 C 库是 musl 的
  _LIBCPP_HAS_RANDOM_DEVICE 0    openkal 没有熵源

第一位是实测出来的:置 0(工具链自带的值)时,一个 include <vector> 的翻译
单元报 20 个错,头一个点名原因「unknown rune table for this platform」;置 1
则一个不报。

判据:examples/cxx 五项断言,其中两项是这个包的全部理由 —— 异常跨三层栈帧
抛出并接住,以及栈展开时析构函数运行。其余的,一个链接进去但从没工作过的
运行时同样满足。examples/import-std 断言另一半:import std 本身。

⚠️ 这不是预防性措辞。写这个包的过程中,示例打印完第一行就
  libc++abi: terminating due to uncaught exception
而 _Unwind_Backtrace 走 0 帧,同时编译、链接、以及所有不抛的路径全绿。
真因在两层之下:openkal-musl 替换掉的 __libc_start_main 不读辅助向量,于是
dl_iterate_phdr 报了一个「没有程序头」的对象,unwinder 在那里找
PT_GNU_EH_FRAME,断定这个程序没有帧描述,而不是断定它没被告知。

一个只被构建过的包会把那个发出去。

* docs: README —— rebase 的冲突解决把它清空了

* feat: 为没有操作系统的机器构建 C++ 运行时

libc++ / libc++abi / libunwind 现在也为 riscv64-none-elf 建得出来。实测:
1431 个对象,ELF 64-bit LSB relocatable UCB RISC-V,__cxa_throw 与
__cxa_begin_catch 有定义。

llvm-generated/ 变成一个配置一个目录 —— 那正是 musl-generated/ 已有的形状,
理由相同:它们是 configure 产物,而一个没有 configure 步骤的包把它们提交进来。

freestanding 与 generic 的差别只有三位加两个开关:

  线程     hosted 目标下 libc++ 自己认出系统并找到 pthread;不被认出的系统上
           它停在 "No thread API" 而不是猜 —— 正确,因为不猜正是 configure
           存在的理由。所以 freestanding 把它写出来。
  文件系统 关掉,并且实现它的源文件随之不编 —— 与 random.cpp 同一条规则,
           也是 libc++ 自己的构建用同一个开关做的事。
  终端     关掉。
  dladdr   关掉:没有动态加载器,它接口里那个类型不存在。
  _GNU_SOURCE  libc++abi 为线程标识去够 syscall,而 musl 只在这个宏下声明
           那个名字。LLVM 自己的 runtimes 构建出于同样理由定义它 ——
           而宿主构建从别处拿到了它,这个差别直到这个目标来问才可见。

⚠️ 一并暴露的一处不完整:__assertion_handler 也是 configure 产物,而宿主那次
构建悄悄用了工具链载荷里的那份。现在它在 llvm-generated/ 里(与模板逐字相同,
所以是收进来而不是改写)。裸机这一试才让它现形 —— 宿主上它一直是绿的。

⚠️ import std 在这种目标上仍被拒绝,而拒绝它的不是这个包:

  error: `import std;` is not available on 'riscv64-none-elf'
         --- a freestanding target has no hosted standard library.

那句话陈述的前提,这个包让它不成立了。解除它是构建工具的改动 —— 它问的是
「目标是不是 freestanding」,而该问的是「hosted 标准库在不在」。

* feat: 声明 hosted-standard-library

构建工具在 configure 期拒绝没有操作系统的目标上的 import std,理由是「那种目标
没有 hosted 标准库」—— 而这个包正是一份。这件事是依赖图的性质,不是三元组的
性质,所以在这里声明、在那里读。

配套改动:mcpp-community/mcpp#486。

* feat: 声明自己的 std 模块源

构建工具否则会向编译器问 std.cppm 在哪,而这一份是这个包为一个编译器毫不知情的
目标配置出来的:它的 __config_site 在 llvm-generated/,它的片段在
llvm/libcxx/modules/std/。

三个 flag 都是实测逼出来的,不是预防性的:
  --no-default-config  载荷的 clang++.cfg 无条件加一个宿主 C 库的头,
                       模块于是编那个库的 <wchar.h> 并停在它指望宿主编译器
                       提供的名字上
  -nostdinc/-nostdinc++ 同上
  -D_GNU_SOURCE        libc++ 的 locale 层去够 musl 只在这个宏下声明的名字
                       (vasprintf、strtof_l 等),LLVM 自己的 runtimes 构建
                       出于同样理由定义它
  -I llvm/libcxx/modules  模块源按相对名 include 九十来个片段,它们是上游的、
                       未改动,所以留在上游放的地方,由搜索路径点名那个目录

配套:mcpp-community/mcpp#486。

* feat: 编译器自己的运行时,裸机的展开约定,以及那个不会假绿的判据

## compiler-rt 早就 vendored,只是没被构建

宿主目标链接工具链的 libclang_rt.builtins,没人注意到它存在。这个目标没有那份
归档可链,而它要的东西并不冷僻:这个架构上 long double 是 IEEE binary128 而且
没有硬件,于是 musl 的每一个 long double 函数都调用一个编译器发出调用、却不
定义的软浮点例程。

实测:C 库自己的链接错误清干净之后,二十个未定义名字,全是 __addtf3、__letf2、
__fixtfdi、__udivti3 这种形状。⚠️ 它们**一个都不出现在本仓库的任何源码里** ——
它们是编译器决定要调的,所以读源码永远找不到它们,而第一份证据是一次链接。

选择用的是 compiler-rt 自己的:它的 CMakeLists 为这个架构列出 GENERIC_SOURCES
与 GENERIC_TF_SOURCES 共 146 个文件,清单里排除的正好是目录里剩下的部分 ——
取自那份列表,而不是猜哪些能编过。

## 裸机的展开约定走符号,不走程序头

_LIBUNWIND_IS_BAREMETAL:unwinder 通过链接脚本定义的符号找它的表,而不是通过
程序头。两条路上游都有,而这个目标必须走第二条 —— 走程序头要 __ehdr_start,
那要求 ELF 头落在已加载段里,那要求它在最低加载地址上,而固件跳的就是最低加载
地址。实测:镜像链接成功,入口移到 0x80200270,OpenSBI 仍然宣告
`Next Address 0x80200000`,机器停在把 \x7fELF 当指令执行上。

--eh-frame-hdr 也归这个包:选了裸机 unwinder 的包才知道那个节是被需要的。
没有它两个 _hdr_ 符号都是 0,libunwind 读成「没有索引」,于是每一帧都从头线性
扫 .eh_frame —— 一个保持正确性的减速,而**没有任何东西会报告它**。

## examples/baremetal —— 判据本身

这上面每一步都跑在宿主上,而宿主本来就装着 C 库、C++ 运行时和 unwinder。
一个不小心够到了它们的程序照样能跑 —— 所以那些步骤可以在**根本没用到本包**的
情况下通过。

这里没有东西可以够到。C 库是 openkal-musl,标准库和 unwinder 是本包的,底下是
一个全部接口就是 ecall 的固件。三条断言各要一层:容器与算法要分配器,格式化与
输出要流,throw/catch **加上展开期跑掉的析构函数**要 unwinder 与 libc++abi ——
最后那个是「unwinder 能用」与「unwinder 只是链上了」的分界。

  sorted: 2 4 7
  caught: 42
  unwound: true
  baremetal import std: ok

* ci: 裸机那一步要的四个决定还没发布,所以从分支把工具建出来

这一步需要四个还没有任何已发布 mcpp 做出的决定,而它们都属于工具而不是本包:
import std 的门问「有没有包为这个目标提供标准库」而不是「目标是不是
freestanding」;包可以带自己的 std 模块源;图里有为目标编译的 C++ 运行时时
-fno-exceptions/-fno-rtti/-ffreestanding 要摘掉;以及展开表要打开 ——
编译器为这类目标默认关掉它,而一套不完整的展开表不会降级,它会让走栈停住。

都在 mcpp-community/mcpp#486 上。在它发布之前,这个 job 用和这里每一个依赖
同样的方式把工具拿过来 —— 从分支 —— 这样下面那条判据是**真的在被执行**,
而不是一句「在某台笔记本上验过一次」的注释。

⇒ #486 发布后,删掉这一步并抬高 MCPP_VERSION。

* ci: 克隆带过来的 workspace pin 指向索引里已经没有的版本

  xlings: version '2026.8.17.1' not found for 'mcpp'
    available: 2026.8.19.4

仓库根的 .xlings.json 决定那棵树里的构建用哪个 mcpp,而 mcpp 自己的 bootstrap
pin 不随发布移动 —— 它是上次动这个文件时的当前版本。把分支克隆过来,就把一个
只在那个仓库自己的 CI 里成立的 pin 一起带了过来。

改写成本 job 已经装好的那个版本,两边就一致了。它不改变被测的东西:pin 选的是
**构建 mcpp 的**工具,而被测的是构建出来的那个 mcpp。

* fix: 本机绝对路径又一次被提交进清单,而这次 CI 在「运行」那一步才说

程序建出来了(text 2494992,和本机一致),失败在运行:runner 指向
/home/speak/.mcpp/registry/... —— 一台机器上的绝对路径进了每台机器都读的文件。

⚠️ 这是本生态里第二次,第一次在 conformance/mcpp.toml,两次的显形方式都一样:
别的仓库的 CI 挂在一条只存在于一台笔记本上的路径上。成因也一样:本机要跑就得
把名字换成装好的模拟器,而最顺手的做法就是改这一行。

清单改回裸名,并在旁边写下为什么它必须保持裸名。CI 那一步加了一条守卫:先断言
清单里还是裸名,再替换 —— 于是一个已经带着路径的 checkout 会**被告知**,而不是
得到一个路径套路径。

* examples: 一份源码,两台机器,中间什么都没改

原来这个目录叫 baremetal 并且 [build] target 钉死在 riscv64-none-elf —— 那让它
成了一个「裸机的例子」,而不是设计文档那条主张的**演示**。

主张是:openkal 之上的程序,构建时不关心底下是哪个实现。所以现在:

  mcpp run                             这台机器,openkal-linux 之上
  mcpp run --target riscv64-none-elf   riscv64,OpenSBI 之上,没有操作系统

同一个 src/main.cpp,没有 #if,没有第二个目录,两边打印同样的四行。

唯一不同的是内存布局,而 build.mcpp 把它放在一个对 target_os 的条件后面 ——
因为那是一句关于**机器**的陈述,不是关于程序的。

CI 两步:第二步跑宿主那条,并且  两边的四行 —— 断言的是「输出相同」而不是
「两边都有输出」。

已验证两个方向都通过。目录改名为 same-source,因为它现在说的是这件事。

* ci: 自建工具那一步用 --dev,并把 job 上限抬到 90 分钟

被测的是工具对编译 flag 做出的一组决定,优化级别改变不了其中任何一条。实测:
release 自建在双核 runner 上花掉了六十分钟里的半个多小时 —— 预算的大头花在了
测试观察不到的东西上。

⚠️ 两处都是临时的:#486 发布后,那一步删掉,这里也跟着回去。

* feat(port): 差异放一个目录 —— libc++ 按 OS 宏选后端,而 openkal 按「配置的是哪个 C 库」

⭐ 形态先说:`port/include` 是**覆盖目录**,不是对 vendored 树的修改。
openkal-musl 的 `port/` 把与上游 musl 的每一处差异收在一个目录里、靠 include
顺序遮蔽它;这里是 libc++ 的同一套安排 —— vendored 树与上游逐字节相同,
`git diff` 对一份新 checkout 为空,而全部差异就是 port/include 下的这几个文件。

## locale 后端

upstream 的选择链问的是「这是哪个操作系统」,并在每个答案上假定那个系统的 C 库:

    #if defined(__APPLE__)    → support/apple.h
    #elif defined(__linux__)  → support/linux.h

这在 libc++ 正常被构建的每个地方都成立,因为 OS 和它的 C 库是一起来的。

⚠️ 在这里它们不是一起来的。openkal 的安排是 C 库是一个**包**,而下面的平台是一个
48 个函数的接口的实现。为 Apple 目标格式构建的程序定义了 `__APPLE__`,因为那是
关于**格式和 ABI** 的陈述 —— 而底下是 musl。upstream 的第一问于是对着错的问题
给出了正确答案:

    bsd_like.h:203: no member named 'asprintf_l' in the global namespace

⚠️ 这条**不能**用 `__config_site` 表达,而那是先试的:libc++ 那条链是写死的
`#if defined(__APPLE__)`,没有覆盖点;唯一相关的旋钮 `_LIBCPP_HAS_LOCALIZATION 0`
是**整个去掉 `<locale>`**,不是换后端。砍掉一个能用的设施去绕过一个口味问题,
比遮蔽一个头文件更差。

## 三个 Apple SDK 桩

libunwind 与 libc++ 的源码在 `__APPLE__` 下 include Apple SDK 的头。整棵树里
一共 5 处、4 个头 —— 数出来的,不是估的。桩里只有**被读到**的那些名字,和
openkal-macos 那份只有两个名字的 `libSystem.tbd` 同一种做法。

⚠️ `mach-o/dyld.h` 不是编译期的桩:refstring 与 UnwindCursor 在**运行期**调它。
openkal 没有动态加载器,一个这样构建的程序就是一个镜像,没有东西可枚举 —— 所以
诚实的答案是零个镜像,而两个调用者本来就处理这个情况。⚠️ 答零个和让符号未定义
不是一回事:后者是一个从没想要加载器的程序链接失败在 Apple 的加载器上。

## std.compat

`[package] std-compat-module`:提供一个模块的包提供两个。理由见 mcpp#486。

⚠️ 未完成:libc++abi 的 guard 实现(`mach_port_t` / `syscall`)与 libc++ 的
locale 后端(`vasprintf` / `strtof_l`)各自按平台宏选实现,而「Mach-O 上的 musl」
两份都不是。CI 不跑这条,所以没有回归面。

* fix(port): 判据是「哪个 C 库」,不是「哪个 C 库,在 Apple 上」

覆盖里原来写的是 `_LIBCPP_HAS_MUSL_LIBC && defined(__APPLE__)`,因为 Apple 是
第一个暴露它的目标。实测 2026-08-23 的第二个:x86_64-windows-gnu 在 openkal 上走
_LIBCPP_MSVCRT_LIKE → support/windows.h,停在 `_locale_t` —— 那是微软的 C 运行时,
而这个程序的 C 库是 musl。同一个形状,第二种目标格式。

⇒ 把规则收窄到一个平台,就意味着每个平台再写一遍。upstream 问的是「底下是哪个
C 库」;这里回答它,于是每一个答案是 musl 的目标都到达同一个后端 —— 而在 ELF 上
那本来就是它到达的那个。

* feat(port): C++ 运行时不再关心后端是谁 —— macOS 交叉构建打通

⭐⭐ 在 Linux 上,`mcpp build --target aarch64-macos` 现在产出:

  Mach-O 64-bit arm64 executable
  /usr/lib/libSystem.B.dylib          ← 唯一依赖
  _clock_gettime_nsec_np              ← 借的名字,仍然是那两个
  _pthread_create_from_mach_thread
  dyld_stub_binder                    ← 第三个属于格式,不属于本包

带着 import std、容器、算法、异常与展开期析构 —— 全部来自 openkal 生态,
没有一个字节来自 Apple 的 SDK。

## 一个谓词,而不是每个平台一遍

`_LIBCPP_HAS_MUSL_LIBC` 现在是 locale 后端的第一问。⚠️ 第一版写成
「musl **且在 Apple 上**」,第二个目标 Windows 立刻以同样形状失败(`_locale_t`)
—— 收窄到一个平台就意味着每个平台再写一遍。

## 三条搬出了「按目标」的块,因为它们是关于 openkal 的事实

- `_LIBUNWIND_USE_DLADDR=0` —— openkal 在**任何**目标上都没有动态加载器。
  ⚠️ 它原来在 cfg(os="none") 里,macOS 目标于是以 `unknown type name 'Dl_info'`
  重新发现了同一件事。
- `_GNU_SOURCE` —— musl 的非标准名字住在它后面(vasprintf/strtof_l/syscall)。
  同样的搬迁,同样的理由。
- compiler-rt builtins —— ⚠️ 这条**不能**搬成包级:宿主链接里驱动仍然链载荷自己的
  归档,实测重复定义 __muloti4。改为按目标格式各一份,并记下清单表达不了
  「驱动的运行时选择被替换过」这件事。

## 覆盖目录里的新增

`port/include/__thread/support.h` —— libc++abi 的 guard 问 `__APPLE__` 要 Mach 的
线程标识。⚠️ 三个分支里只有第一个不适合 openkal,而它正是命中的那个;第二个
(SYS_gettid)会经 musl 的系统调用垫片走到 kal_task_*,第三个是合法答案。
⚠️ 而它关不掉:_LIBCPP_HAS_THREAD_API_PTHREAD 是我们的,清掉它是为了绕开一个
标识函数而拿掉整个线程层。

`mach-o/dyld.h` 加上卸载钩子 —— 这里没有东西会被卸载。

## Mach-O 的 thread_local 走模拟 TLS

⚠️ `undefined symbol: _tlv_bootstrap`:这个格式经加载器 bootstrap 的描述符访问
thread_local,而这里没有加载器。`-femulated-tls` 让每次访问改走
`__emutls_get_address`。⭐ 机制不是新的 —— 生态设计的实验 A 验证过这条路
(32 项断言 0 failures),而那次也记下了这个 flag 是 LLVM 专有的,
这正是它能写在这里的原因:这个目标的编译器按构造就是 clang。

⚠️ 而 `emutls.c` 要从排除表里拿掉,并且**顺序不决定这件事** —— 列表里任何位置的
排除都压过任何位置的包含。实测:加了包含而排除还在,产物没有那个对象。

* feat(port): 撤回 libc++ 从 _WIN32 draws 出来的那个结论

libc++ 的 __config 在 `#if defined(_WIN32)` 里写着:

  // Both MinGW and native MSVC provide a "MSVC"-like environment
  #define _LIBCPP_MSVCRT_LIKE

对它见过的两种 Windows 环境都成立。对这一种不成立:`_WIN32` 是关于**目标格式和
调用约定**的陈述,而底下的 C 库是 musl —— 和 ELF 上、Mach-O 上同一个 musl。

⭐ 为什么在 __config 而不在 __config_site:__config_site 是构建陈述**答案**的
地方,这个包生成一份(_LIBCPP_HAS_MUSL_LIBC、线程 API、文件系统开关都在那里)。
而这条不是 libc++ 问的问题,是它自己**推**出来的结论 —— 在我们的答案被读到之前。

⚠️ 实测 x86_64-windows-gnu 交叉:

  fstream:1004: use of undeclared identifier '_ftelli64'
  fstream:741:  use of undeclared identifier '_wfopen'

⚠️ 三个名字要一起撤,不是一个:_LIBCPP_MSVCRT_LIKE 选 C 运行时,
_LIBCPP_WIN32API 选操作系统 API,而这个目标下面两者都不是那个。只撤第一个会让
libc++ 一边调 fopen 一边去够 CreateFileW。

⚠️ 而**从结论里再推出来的那个也要撤**:_LIBCPP_HAS_OPEN_WITH_WCHAR 在同一个
_WIN32 块里被 `#define` 成一个**值**,撤掉上面两个名字它仍然是 1,`<fstream>`
仍然去够 _wfopen。实测:撤前两个只消掉了 _ftelli64。

已验证无回归:same-source 宿主 ✅;macOS 交叉仍产出 Mach-O arm64 ✅。

* feat(port): PE 上的异常机制跟随我们带的 unwinder

clang 面向这个三元组会定义 __SEH__,因为那是 mingw 在这个架构上用的东西 ——
而 libunwind 自己的公开头于是去够 <windows.h> 和 <ntverp.h>:

  unwind.h:22 → …/mingw32/include/windows.h → crtdefs.h
              → corecrt.h:98 typedef redefinition

⚠️ 和这个目标上其它每一条失败同一类:一个平台宏被读成了「底下是什么」的陈述。
这里答案是一个 flag 而不是一份覆盖,因为问题确实是一个构建决定:这个程序的
unwinder 就是本包里的这个,而它读 DWARF 表 —— 和它在 ELF 上、Mach-O 上读的
是同一批。

⚠️ 它必须到达图里每一个翻译单元(docs/13 记着 -fno-exceptions 的同一条理由:
模型记在 BMI 里)。写在这里覆盖本包;消费者也要写一遍,而由 capability 决定
才是能消掉这份重复的形状。

⚠️ 我第一次把它写在了 block header **之前**,于是它落进了上一个块 —— 实测
compile_commands 里根本没有这个 flag,而报错一字未变。

* feat(port): 平台相关的换成 openkal 接口 —— 立规矩,并做掉 std::atomic 的等待

⭐ 规矩三条,写在 llvm/PATCHES.md 里,与 openkal-musl 的一致:

1. **可以动源码。** 移植就是动源码;假装不动只会把差异藏进别处。
2. **动过的地方要标注清楚。** 每一处夹在 `// ─── openkal ─── BEGIN/END` 之间,
   `grep -rn "openkal ─── BEGIN" llvm/` 一次数完。
3. **能不动的就不动。** 上游能自己走对的路,让它走。

判据:换的是「平台」不是「库」。每一处替换必须能填进这句话 ——「upstream 在这里
问『这是哪个 OS』,而这个问题真正的答案是 openkal 的 <接口>」。⚠️ 填不进去的不要
动:sizeof(long)、int64_t 的拼法、目标格式不是「下面是谁」,是目标自己的定义。

## 已换:std::atomic 的等待与唤醒

上游一条链,五个分支,四个操作系统(SYS_futex / _umtx_op / futex / WaitOnAddress
/ 完全不等待)。openkal 的答案是 kal_task_wait / kal_task_wake —— 规范管它叫挂起
原语,而它在每一个目标上是同样的两个调用。

平台面**恰好两个函数**,其余 495 行全可移植 ⇒ 原地打标记的补丁,漂移面从 495 行
缩到约 50 行,而不是整文件拷贝。

⚠️ 它本来就已经到 openkal 了,只是绕了一圈:Linux 上发 SYS_futex,musl 的 port
拦下来,__okm_futex 调 kal_task_wait。直接走去掉那一圈,并让另外两种目标格式也
能用 —— 在那里没有系统调用可拦。

## 三处配套,而每一处都是「同一个事实的第二个说法」

- port/include/__atomic/contention_t.h —— 竞争计数器的宽度跟随挂起原语(4 字节)
- port/include/__atomic/atomic_waitable_traits.h —— ⚠️ **只改前者不够**:开了
  _LIBCPP_ABI_ATOMIC_WAIT_NATIVE_BY_SIZE 之后实例化清单来自这个宏而不是
  sizeof(__cxx_contention_t)。两个头陈述一个事实,改了读者先找到的那个,报错一字
  不变。(这是本移植第二次遇到「一个事实走两条通道」,链接线是第一次。)
- OPENKAL_HAS_TASK —— ⚠️ 只提供 core 的实现没有挂起原语,实测裸机链接停在
  undefined symbol: kal_task_wait。那是 6.1 在工作,不是缺陷;答案是落回上游那条
  本来就正确的分支(单执行上下文的机器上 atomic::wait 没有东西可等,轮询是准确的
  实现而不是替代品)。⚠️ 这与 openkal-musl 的 OKM_HAS_TASK 是同一个事实,说了
  第二遍 —— 因为现在有第二个消费者。

## PATCHES.md 里比「已换」更重要的一节:不需要动的

整棵树 #include <windows.h> 共 19 处。12 处守在 _LIBCPP_WIN32API 上,已由
port/include/__config 撤回 ⇒ **自己落到 POSIX 分支,而那条本来就在 openkal 上**
(它调 fopen/clock_gettime/pthread_*,那些是 musl)。2 处守在 __SEH__ 上,已由
-fdwarf-exceptions 绕开。

⇒ 这就是为什么整份移植是几十行而不是把 libc++ 重做一遍。**先问「上游有没有一条
路已经通向 openkal」,再考虑换。**

已验证三个目标:宿主 ✅ / 裸机 riscv64 ✅ / macOS 交叉产出 Mach-O arm64 ✅
(外部符号仍是那三个)。

* feat(port): PE 目标打通 —— 展开表、锁、TLS 三处都问了「哪个 OS」

同一份 src/main.cpp 现在从一台 Linux 宿主构建出四个目标,并且两个能在本机跑的
输出逐字相同:

  x86_64-linux-gnu    静态 ELF        ✅ 跑通
  x86_64-windows-gnu  PE32+           ✅ 跑通(wine 冒烟,⚠️ 不等于 Windows)
  aarch64-macos       Mach-O arm64    ✅ 产出
  riscv64-none-elf    RISC-V ELF      ✅ 产出

    sorted: 2 4 7 / caught: 42 / unwound: true / import std over openkal: ok

⭐ unwound: true 是判据 —— 析构函数在展开中跑到了,说明 libunwind 靠自读镜像
找到了 .eh_frame,而不是靠 EnumProcessModules。

── 三处 <windows.h>,一个形状 ──────────────────────────────

⭐⭐ AddressSpace.hpp 问「这个镜像的展开表在哪」,上游答「让操作系统枚举模块」。
而这份文件已经答过这个问题三遍,没有一遍问 OS:裸机读链接器符号,Darwin 读
_dyld_find_unwind_sections,ELF 读自己的 program headers。openkal 的答案和它们
同一句话 —— 镜像自己知道:__ImageBase 是链接器定义的符号(不是调用),段表在离
它固定的偏移上。

⚠️ 而 PATCHES.md 原先写着这一支「已由 DWARF 路线绕开」—— 凭读守卫写的,实测
否掉了:_WIN32 && DWARF 正是它的守卫。修好它,同样的错误挪到 UnwindCursor.hpp
(一处纯粹没被用到的 include),再挪到 RWMutex.hpp。

⚠️ RWMutex 里同一个事实说了两遍(include 一处、class 一处),两处一起改。

⚠️ 走过一条错路并记进台账:先试 COFF 分组段 .eh_frame$a/$z 夹住 .eh_frame,
实测链接器把不带 $ 的段排在最前 ⇒ start 落在数据之后,end-start=1 字节。那不是
链接失败,是静默的错答案。

── emutls:第三种格式,同一堵墙 ────────────────────────────

PE 的 _tls_index 和 Mach-O 的 _tlv_bootstrap 都由动态加载器 bootstrap,而自包含
的 openkal 镜像没有加载器。macOS 上已经用 -femulated-tls + 编 emutls.c 解过一次;
Windows 是它在第三种格式里的同一件事。

⚠️ emutls.c 自己又问了一次「哪个 OS」,于是被编译到 PE 上的原因(平台的 TLS 用
不了)和它的行为(去要平台的 C 运行时)自相矛盾。走 POSIX 分支。

── 一个名字,不是五个 ──────────────────────────────────

五处补丁统一守卫在 OPENKAL 上,cflags/cxxflags 各给一次(compiler-rt 是 C)。
⚠️ 一度写成 _LIBUNWIND_OPENKAL,随后 emutls.c 需要同一个事实 —— 同一个事实两个
名字正是这套代码反复出问题的形状。

── 另外三处不是「移植」而是「配置」 ────────────────────────

1. __config_site 的线程 API:四个全写 0 不是「没有线程 API」,是「没人回答」,
   libc++ 于是按 OS 自选 —— ELF/Mach-O 上碰巧选对 pthread,PE 上选了 WIN32,
   报 undefined symbol: std::__1::__libcpp_mutex_lock(void**)(void** 是 Win32
   的形状)。⚠️ port/include/__config 撤回 _LIBCPP_WIN32API 修不了它,因为撤回
   发生在 __config 已经据此下了结论之后 —— 该文件记录的「结论的结论」第三例。

2. operator new/delete 定义了两遍。libcxx/src/new.cpp 自己写着「以下代码原样
   拷贝进 libcxxabi/src/stdlib_new_delete.cpp,本文件这份是权威的」。上游把两者
   建成两个库,本包建成一个 ⇒ 同一条链接线上各来一份。
   ⚠️ 三个目标看不见:两份都是弱符号,ELF/Mach-O 上重复的弱定义正是「弱」的
   含义,链接器挑一个并且不说话。COFF 报了 30 条。⇒ 包级排除,不是 cfg(windows)
   排除 —— 第三种格式没有制造这个重复,它只是报告了它。

3. -fdwarf-exceptions / -femulated-tls 从本包 [build] 里撤走,改由 mcpp 全图
   推导 —— 见 mcpp 的 graph_runtime_compile_flags。它们决定 throw 和
   thread_local 编译成什么,是整张图的性质。写在包里时只覆盖了本包的对象,
   ⚠️ 实测:全部编过之后链接报 undefined symbol: __gxx_personality_seh0,
   引用它的是使用者的 main.o。

── 例子清单 ───────────────────────────────────────────

examples/same-source 的 [toolchain] default 从 llvm@22.1.8 改成
openkal-llvm@22.1.8。⚠️ 同一个载荷、同一个 clang、同一个版本 —— 差别在 mcpp
据此相信目标侧从哪来。写成 llvm 时四个目标只成两个,而两条报错都没提到工具链族。

* ci: 在 Linux 上构建,在真机上跑 —— 而那两个 job 什么都不装

原先 CI 证明的是「同一份源码不知道自己在哪台机器上」(裸机 + 本机,四行输出
diff 相等)。现在加证「构建也不必在那台机器上」:一台 Linux 宿主产出 PE 和
Mach-O,两个 job 把它们下载下来在真的 Windows 和真的 macOS 上跑。

⭐⭐ 那两个 job 故意没有任何工具链步骤 —— 不装 mcpp、不装编译器、不装 C 运行时。
程序自带 C 库、C++ 运行时和展开器,剩下的只有它被构建给的那个操作系统。如果哪
天有人因为「程序需要」而往里加一步安装,那一步就是发现,不是修复。

⚠️ 交叉构建产出一个格式正确的文件,只证明编译器被告知了正确的目标。它不证明程
序能跑 —— 而这套生态在这两个平台上找到的每一处差异(加载器 bootstrap 的
thread-local、展开器找自己的表、人格例程)都是**链接成功、运行时失败**。

判据是 unwound: true —— 析构函数在展开中跑到了,链接骗不出这一行。

两处断言写在运行之前,因为它们的修法不同:
· 格式(PE32+ / Mach-O arm64)在构建 job 里断言 —— 失败意味着目标选错了;
· ad-hoc 签名在 macOS job 里断言 —— arm64 macOS 拒绝未签名镜像,失败意味着
  链接器没签,而不是程序崩了。
⚠️ 产物上传会丢执行位,所以 chmod 在前。

── 本机已验(⚠️ wine 不等于 Windows,所以才要上面那个 job)──────

  x86_64-linux-gnu    ELF   跑通
  x86_64-windows-gnu  PE    跑通(wine)
  riscv64-none-elf    ELF   跑通(qemu + OpenSBI)
  aarch64-macos       Mach-O 产出,LC_CODE_SIGNATURE 在

* ci: 宿主维度 —— 两台不是 Linux 的机器,各自到达全部四个目标

上面所有 job 都在 Linux 上构建。那证明的是「一台宿主能到达每个目标」,而 N×N
矩阵真正要问的另一半没被碰:**宿主重不重要**。方案的答案是不重要 —— 目标侧是
一组包、编译器是可重定向的 clang,所以 N 宿主 × N 目标塌缩成 N 个实现加一个工具。

⚠️ 那是一个断言,而这个形状的断言在这里错过:Linux 宿主到达 PE 之前需要四处
分别的修复,每一处在真正跑一次构建之前都看不见。两台宿主的代价是两个 job;不跑
就断言塌缩成立的代价是一段话,而它什么都不证明。

每台宿主构建全部四个目标,并**运行它自己是的那一个** —— 与 Linux job 对自己用的
是同一条判据。

⚠️ 写这个 job 时预测到并已在 mcpp 侧修掉一处真缺陷:链接行的三支是按宿主分的,
而只有 Linux 那一支消费 --target=。见 mcpp#486 的对应提交。

* fix(windows): 栈探针从 compiler-rt 取,它是 .S 所以被 *.c 的 glob 漏掉了

编译器在足够大的帧之前发 ___chkstk_ms 的调用,而在这个格式上那是**编译器运行时**
的例程,不是 C 库的。compiler-rt 有它,在架构自己的目录里:x86_64/chkstk.S。

⚠️ 实测 2026-08-22,openkal-musl 那侧把 -lgcc 从这个格式的链接线上拿掉之后 ——
那个归档是 GCC 的,它一直在悄悄供给这个符号给一条编译器是 clang 的链接:

    ld.lld: error: undefined symbol: ___chkstk_ms

⚠️ 只取这一个文件而不是整个目录:x86_64/ 里还有 floatdidf.c 等,上面的通用列表
已经供给了它们,整目录会让每个都定义两遍。

* fix(windows): x87 的 long double 例程被一条从 macOS 抄来的排除挡掉了

    ld.lld: error: undefined symbol: __mulxc3

*xf*.c 和 *xc3.c 是 x87 80 位 long double 的例程。在 Apple 的 arm64 上
long double 就是 double,没有东西能调用它们,编了是死重量 —— 那个块排除它们是对的。
而在 x86_64-w64-windows-gnu 上 long double **就是** x87 80 位,调用是真的
(libc++ 的 <complex> 发出来的)。两行是从 macOS 块整份抄过来的。

GCC 的归档一直在供给它,而排除项让 compiler-rt 供给不了。-lgcc 拿掉之后现形。

⚠️ 一处方法教训:第一次改的时候我按行号定位,而断言通过了 —— 因为
cfg(os = "none") 块里碰巧有一模一样的两行。**按位置的断言匹配到了错误的那一份**,
于是我从裸机块里删掉了正确的排除,同时把 Windows 那两条留在原处。改成按块定位。

── 从全清缓存复验(rm -rf ~/.mcpp/git/*)────────────────

  x86_64-linux-gnu    ELF     跑通
  x86_64-windows-gnu  PE32+   跑通(wine),unwound: true
  aarch64-macos       Mach-O  产出
  riscv64-none-elf    ELF     跑通(qemu + OpenSBI)

⭐ 而这一轮的意义不只是绿:Windows 那条链接线现在**不引用宿主的 mingw 任何东西** ——
导入库由 openkal-windows 从自己的 .def 生成,builtins / emutls / 栈探针由
compiler-rt 供给,__main 由 openkal-musl 的 port 供给。

* ci: 宿主维度少了 toolchain install;以及让 macOS 上的崩溃当场给出回溯

⭐⭐ 而这一轮 CI 已经证明了它存在的理由:

    the artefact built on Linux runs on Windows    ✅ pass
    the artefact built on Linux runs on macOS      ✗ Segmentation fault: 11

在 Linux 上构建的 Mach-O 链接成功、ad-hoc 签名合法、在**真机**上崩。这是这套栈
第一次在这个系统上真的跑 —— 而 R6 写的就是「平台实现的 stub 与真实系统不一致,
只有真机能验」。

⚠️ 有意思的失败在外面看起来都一样(入口对内核交给它什么的假设、线程指针、借来的
两个名字),所以崩溃时当场跑一次 lldb 打回溯、otool -L 打依赖、LC_MAIN 打入口 ——
一个能打出回溯的 CI 轮次抵得上好几个打不出的。

── 另一条 ─────────────────────────────────────────

两个 host-dimension job 报 `llvm@22.1.8 is not installed`:toolchain default 只是
点名,不会去取。补 install。

* ci: 跳到 0 的时候要问的是 lr,不是 bt

    stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000

⚠️ bt 只能说出「PC 是 0」,那是把症状重述一遍 —— 跳到地址 0 没有留下可展开的帧。
而链接寄存器里还是发出那次调用的人的返回地址,image lookup 能把它变成名字。

本机已排除的:
· LC_MAIN entryoff 124032 = 0x1e480,正是 _okm_start(不是入口错);
· __init_offsets 的十七项里前十个是 openkal 的模块初始化器,各一条 ret(合法);
· __init_array_start == __init_array_end,__libc_start_init 的循环不跑;
· _init / _fini 各一条 ret;
· 未定义符号只有三个,没有弱未定义。

⇒ 剩下的是「谁调用了 0」,那要 lr。顺带跑一次 DYLD_PRINT_INITIALIZERS 看加载器
走到哪一步。

* ci: lldb 的 -o 在第一个错误后就不往下走了 —— 崩溃诊断要用 -k

上一轮加的三条命令(register read / image lookup $lr / bt all)一条都没执行:
--batch 模式下 lldb 在第一个报错的 -o 之后放弃余下的,而在 PC=0 处读寄存器就是
报错。-k 是「进程崩溃时执行」的那一组,正对这个场景。

⚠️ 同时把 DYLD_PRINT_INITIALIZERS 的输出写进文件再读:管进 tail 会和 shell 自己
对信号的报告交错,而最后几行 —— 说明当时在跑哪个初始化器的那几行 —— 正是被冲掉
的那些。

* fix: -nostdinc —— 这个包不该从构建它的那台机器上拿到任何头文件

    libcxx/src/chrono.cpp:59 → …/MacOSX.sdk/usr/include/mach/mach_time.h:55:
      error: unknown type name '__MAC_10_10'                        (× 19)

⚠️ 这个 flag 一直只在 std-module-flags 上,包自己的源码编译没有它。在 Linux 上无害
(宿主没有 mach/ 头,C 库的头从 -I 来);在 macOS 上 SDK 就在手边。

chrono.cpp 用 __has_include 守卫那个 include —— 而那是**一个关于机器的问题**。
Linux 上答案是否,文件编过;macOS 上 SDK 在那里,答案是是,而头文件到来时它自己的
SDK 本该提供的前提没有到位。⇒ 那个 include 对 libc++ 是可选的、这次构建走的路径也
不用它;缺陷在于这个问题**能被问出来**。

本包在 include_dirs 里陈述了它需要的每一个头;编译器自己找到的任何东西都是「正在
构建它的那个系统」的头。

从全清缓存复验四个目标:linux / windows(wine)/ riscv64(qemu)三个跑通并输出
逐字相同,aarch64-macos 产出 Mach-O。

* feat(linux): compiler-rt 的 builtins 也为这个目标编 —— 它是最后一个还在借的

macOS / Windows / 裸机都从本包的 compiler-rt 取「编译器发出调用而 C 库不定义」的
那些例程。Linux 不:openkal-musl 点名 -lgcc,而在 Linux 宿主上那会解析到一个碰巧
装着的载荷。

⚠️ macOS 宿主交叉到 x86_64-linux-gnu 时:ld.lld: error: unable to find library -lgcc

⚠️ 与 -lgcc 并存不是重复定义的危险:归档只贡献仍未定义的东西,我们的先链上去,
归档就不会为它们被查询。

* chore: 删掉误提交的 .mcpp/,并补上这个仓唯一缺的那条 .gitignore

.mcpp/.xlings.json 是裸机那次工作里的实验残留 —— 它声明 xim:picolibc-riscv,而本包
没有任何地方用它。

⚠️ 而它能进来的原因是可查的:这套生态里七个仓,**只有这一个**的 .gitignore 没有
.mcpp。补上。

⭐ 顺手把七个 PR 的改动文件全扫了一遍(对象/归档/日志/构建目录/临时文件),其余六个
干净。

* chore: 第二个误提交的 .mcpp/,以及上一次扫描为什么没看见它

examples/import-std/.mcpp/.xlings.json —— 与根下那个同源的实验残留(同样声明
xim:picolibc-riscv,本包同样没有任何地方用它)。

⚠️ 上一轮已经做过一次这个检查并报告「六个仓干净」。它漏掉这一个的原因是判据写成了
^(\.mcpp|target/…) —— **锚定在路径开头**,所以只看得见仓库根下的那一个。

⇒ 一个检查报告「干净」的时候,要先问它看得见多少。

* refactor(example): 板子的内存布局不再由这个例子陈述

它此前带着一份链接脚本 —— OpenSBI 把控制权交到哪里、多少栈、堆在哪 —— 挂在一个
关于目标的条件后面。那个条件是诚实的部分:它们是关于**一台机器**的陈述,不是关于
这个程序的。

⭐ 而这恰恰说明它们也不属于这里。openkal-opensbi 是知道那台机器的包,它现在用自己的
构建程序供给这份地图,到达每个使用者的链接线 —— 与它供给 kal_* 定义的方式相同。
于是这个目录所主张的「同一份源码、不同的机器、什么都不用改」对构建文件也成立。

⚠️ 两份并存时实测(板级事实**可传递**地到达使用者,而这个例子是使用者):

    ld.lld: error: section .eh_frame file range overlaps with .debug_str_offsets
    ld.lld: error: section .debug_str file range overlaps with .eh_frame_hdr

两份脚本都被应用了,而诊断一个字都没提「有两份」。

复验四个目标全部产出;裸机(qemu+OpenSBI)、Linux、Windows(wine)三个跑通,
输出逐字相同。

* docs: README 的授权段说 vendored 源码「unchanged」,而那句话现在是假的

⚠️ llvm/PATCHES.md 记录着五处原地标记补丁,而 README 的授权段写着 vendored 源码
「are unchanged」。一句与授权相邻的陈述不该与台账矛盾。

改为「几乎未改动」,并把例外**枚举**出来而不是描述:四个文件五处区域,各自夹在
BEGIN/END 之间,一条 grep 数得完。同时写明判据(upstream 在这里问哪个 OS,而真正
的答案是 openkal 的某个接口)以及那份文档更重要的另一半 —— 十九处 <windows.h> 里
有十二处**不需要动**,谓词答对之后 libc++ 的 POSIX 分支本来就已经经 musl 到达
openkal。

并补一节「四个目标,一份源码」:四种目标格式、各自怎么被运行,以及为什么裸机那个
不会侥幸通过;⚠️ 并写明产物是在**真机上被运行**而不是被检查 —— 这个包在 Mach-O 和
PE 上要找的每一处差异都是链接成功、运行时失败。

* feat: name the c++-abi layer in the current vocabulary

`hosted-standard-library` is kept beside `mcpp:c++-abi=libc++` so that an
engine predating the layer vocabulary still recognises this package. Both name
the same layer; a newer engine reads the second, which additionally carries the
interface name, and ignores the first.

Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD

* refactor(example): name an ordinary toolchain

The target side no longer follows from the toolchain family name. It follows
from the one dependency this example declares, and mcpp resolves it after the
dependency graph exists. `llvm@22.1.8` therefore says all this example needs to
say about a compiler.

Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
@Sunrisepeak
Sunrisepeak merged commit 01d6cef into main Aug 23, 2026
27 checks passed
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.

2 participants