…s known (#486)
* fix(build): import std 的门该问「有没有」,不该问「是不是 freestanding」
这两个问题在没有人为这种目标建过 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 不做,只把前提改对。
* feat(build): 一个包可以带自己的 std 模块源
上一条提交把 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 就在命令里。
* feat(build): 那个「不是永久的」的点到了 —— 异常与 RTTI 由图决定
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 集的实现,那是另一件事。
* fix(build): 两处「由图决定」的续篇,和一条让改动看起来没生效的指纹缺陷
## 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」。
现在它真的这么做了。
* fix(build): 裸机上异常抛不出去,真因是编译器为这类目标默认关掉了展开表
宿主 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%)。
* feat(build): 交叉一直靠「载荷的 driver 本身就是交叉 driver」,而 openkal 不是那样
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 ✅。
* feat(toolchain): openkal-llvm —— 一个工具链,交叉到每一个实现了 openkal 的平台
⭐⭐ `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 宿主 ✅ / 裸机 ✅。
* fix(build): 目标侧来自图时,宿主的链接模型要一处不剩地让开
⭐ `--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。
* fix(build): PE 的链接行少了 --target;以及两个 flag 决定「编译成什么」却写在包里
两条都由「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 产出
* fix(build): 链接行的三支是按「宿主」分的,而目标侧来自图时三支都不对
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 最小工程,走的仍是旧路径,产物能跑。
* feat(build-program): mcpp::compiler() —— 构建程序不该靠猜工具链是谁
「编译器发出调用而 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() 读它。
* fix(std-module): Windows 宿主那条分支不带 extraFlags —— 包自己的头全在里面
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 挖出来的第二处 —— 第一处是链接行按宿主分的三支。
* fix(macos): 自包含镜像不该把一千多个 libc++ 内部符号交给加载器去合并
⚠️⚠️ 这一条是被「在真机上跑一次」挖出来的 —— 程序链接成功、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 上加它也不会错,但那两条是绿的,而这里要修的是「弱定义
在这个格式上是运行期机制」这一件具体的事。
* fix(macos+windows-host): 两条只在「构建发生在哪台机器上」时才成立的假设
── 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 那个构建器就有一条)。写了又
撤回:**一个只在一种平台上编译的分支是这台机器无法检查的分支**,而这一轮在宿主维度
找到的每一处缺陷都正是这个形状 —— 按「哪台机器在构建」分的代码。绝对路径到处都
对,所以只留一种形式。
* fix(dist): 产物格式的判据匹配不到 mcpp 自己用的三元组拼法
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] 逃生口)的兜底,那里只有拼法可依。
* fix(dist): C++ 运行时已经在对象里时,不该再去找一个来链
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。
* fix(dist): 图供给 C++ 运行时时,契约是 SelfContained,由对象兑现而不是由点名一个库
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++;四个目标全部产出,三个能在本机跑的输出逐字相同。
* chore: 删掉 main 上的 binDir —— 一个 shell 重定向留下的空文件
dc6eb34(#436)误提交,在 main 上待了一周。名字读起来像一个本该被展开而没有展开
的变量(`> $binDir`),内容是空的。
顺手写进 .gitignore,让同样的手滑下次被挡住而不是再被 review 一遍。
* fix(windows-host): 整条 -static 是目标的性质,该出现在每一种宿主的链接行上
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),所以它属于每一条链接行。
* fix(windows-host): --target= 挂在「载荷有没有 cfg 文件」上,而那不是它的性质
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。
* feat: MCPP_TARGET_REQUESTED,以及 openkal 的 3×3 交叉验证工作流
── 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:链接骗不出「析构函数在异常被带出栈帧时跑到了」。
* ci(openkal-cross): 用仓库自己的 bootstrap-mcpp,而不是再写一套
首跑就红:
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 都落的那条缓存血统上。
⇒ 两套引导就是两处要保持正确的东西,而第二套写出来不到一天就错了。
* fix(std-module): 一个字符串承载了两个事实,于是第二条命令收到十九个用不上的 flag
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 那条没有重定向,所以它是第一次被看见的
地方。⇒ 噪声本身不是缺陷,「十九条正确而无意义的警告排在任何有意义的警告之前」
才是。
* test: 三个纯函数的单元测试 —— 判据本来在四十分钟外,现在在一秒内
本 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 里。一个「没红」的注入要先确认它真的改到了被测的代码。
* test(e2e): 267 —— MCPP_TARGET_REQUESTED 的契约,以及它为什么不是 MCPP_TARGET
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' 并退非零。
* fix(e2e 267): 我选的目标在 macOS 上没有载荷 —— 那是假设不是实测
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 的变量骗过去。
而且它不需要任何载荷:宿主自己的目标是每个宿主按构造都有的那一个。
* refactor(dist): 产物格式的判据从一个 lambda 提成 format_for,并给它断言
⚠️ 它此前是一个一千五百行函数里的 lambda —— **那正是它没有测试的原因**。而它答错的
那件事(用 LLVM 的 apple/darwin 匹配 mcpp 自己的 aarch64-macos)是靠三台宿主对三个
目标跑出来发现的,四行断言本可以在一秒内发现。
⭐ 提出来之后,「宿主兜底」变成一个参数而不是编译期常量 —— 于是这个函数可以被检查,
而不必成为它所描述的那台机器。
新增三组断言:
· 词表内的六个三元组,对**每一种兜底**都必须给出同一个答案(目标决定格式,而不是
构建的那台机器);
· [target.X] 逃生口:词表解析不了的拼法上,LLVM 的词被认出来;
· 兜底只在三元组什么都没说时才被用到。
注入 `os == "macos"` 那一行删掉 ⇒ 第一组变红。
* fix(std-module): Windows 分支把「保留模块名」的抑制挂在 .ixx 上,而它不该有条件
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 不换盘符)。
* fix(resolve): git 依赖的两个网络步骤都没有重试 —— 一次瞬时故障让整个构建红
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 个单元测试全过。
* feat(build): resolve the target side once, per layer, after the graph 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
* test(e2e): target-side resolution, and the older spelling that must keep 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
* fix(e2e): 268 rewrote its fixture with sed -i, which is not portable
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
* fix(build): a target side with no C library must not link one's startup 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
---------
Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Summary
The enumeration asked toolchain_frontend(root / "bin", pkg), got nothing, and continued. cl.exe is four levels deeper — under VC/Tools/MSVC/<ver>/bin/Host<h>/<arch>/ — so every msvc payload installed correctly and was then invisible.
$ mcpp toolchain list # before Toolchains: * gcc 16.1.0 (default) gcc 15.1.0 llvm 22.1.8 llvm 20.1.7 <- the installed msvc 14.44.35207 is simply absent $ mcpp toolchain list # after msvc 14.44.35207The shape of it
Three places have to know that layout. Install and build knew; the listing did not — which is what a third inline copy of a rule tends to produce. They now share payload_frontend(payloadRoot, pkg, family), delegating to msvc::installation_at() for msvc and the bin/-shaped lookup for everything else, so the three cannot disagree about where a payload keeps its compiler.
prepare.cppm's default-toolchain path also resolves through payload->binDir and is deliberately left alone: the Windows first-run pin is llvm@20.1.7, so that site is unreachable for msvc.
How it was found
By running the released 2026.8.16.1 binary against a payload-shaped fixture — not by a test. The unit test in #434 pinned identify_xim_payload("msvc"), which was already correct: the identity mapping and the enumeration are different questions, and only one of them was being asked.
tests/e2e/239_msvc_managed_toolset.sh step 1b would also have caught it on the next Windows e2e run, now that xim:msvc is published — which is the other half of the lesson: the e2e was written at the right unit, it just had not run yet.
Test plan