Skip to content

import std 的门该问「有没有」;以及三处「由图决定」和一条让改动看起来没生效的指纹缺口 - #486

Open
Sunrisepeak wants to merge 29 commits into
mainfrom
feat/import-std-capability
Open

import std 的门该问「有没有」;以及三处「由图决定」和一条让改动看起来没生效的指纹缺口#486
Sunrisepeak wants to merge 29 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 里。一个「没红」的注入要先确认它真的改到了被测的代码。
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 个单元测试全过。
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