diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md new file mode 100644 index 00000000..d2fe57ac --- /dev/null +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -0,0 +1,835 @@ +# mcpp 目标词表:一套规范,映射到各编译器 + +2026-08-25 · 规范设计方案(待 review,尚未实施) + +本文提出 mcpp 拥有**自己的**目标词表,而不是转述某个编译器的。 +每一条设计都给出实测依据与拒绝的替代方案。 + +前置阅读:`2026-08-25-os-toolchain-target-matrix.md`(三个轴的实测矩阵)、 +`2026-08-25-target-system-analysis.md`(现状缺陷)。 + +--- + +## 0. 为什么需要自己的词表 + +现有三套词表,互不一致,而 mcpp 三套都要打交道: + +| 词表 | 例 | 谁读 | +|---|---|---| +| GCC / autoconf | `x86_64-w64-mingw32` | 预构建载荷的编译器,以**文件名**的形式 | +| LLVM | `x86_64-w64-windows-gnu` | `clang --target=` | +| mcpp | `x86_64-windows-gnu` | 目标表、输出目录、`cfg()`、打包 ABI tag | + +⭐ **第三套已经存在了**,只是它今天在**转述** LLVM 而不是自己定义。 +转述的代价实测可见: + +### 0.1 LLVM 词表的第三段没有单一含义 + +| 平台 | `env` 命名 | 实测依据 | +|---|---|---| +| linux | **C 库** | `gnu`→glibc,`musl`→musl | +| windows | **对象 ABI** | `gnu`→`_Z1fi`,`msvc`→`?f@@YAHH@Z` | +| none | **对象格式** | `elf` | + +同一段在三个平台上是三条不同的轴。这直接造成两处缺陷: +`cfg(env = "gnu")` 在两个平台上语义不同; +以及 Windows 上**两个完全不同的 C 库共用 `-gnu` 一个名字** +(MinGW CRT 与 musl,实测体积 587KB vs 9.8MB、依赖 `msvcrt.dll` vs 不依赖)。 + +### 0.2 LLVM 词表拼不出真实存在的目标 + +实测 llvm 22.1.8: + +``` +clang++ --target=x86_64-pc-windows-musl -c t.cpp + #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) ← ICE,不是诊断 +``` + +Windows 上四个非 MSVC 环境 `gnu`/`cygnus`/`itanium`/`musl`,前三个能编, +只有 `musl` 崩;预定义宏显示它从未被建模(无 `__MINGW32__`)。 + +⭐ **而 musl on Windows 真实存在**——`openkal-musl` 就是它。 +一个词表若拼不出已经存在的东西,它就不能作为 mcpp 的身份来源。 + +### 0.3 GCC 词表把实现放进 OS 位 + +`x86_64-w64-mingw32`:`mingw32` 占据 OS 段,`w64` 占据 vendor 段。 +这是 autoconf 的世界观(MinGW 是一个独立 OS),LLVM 已经拒绝了它并重拼为 +`windows-gnu`。mcpp 更没有理由继承它。 + +--- + +## 1. 设计:三段,每段一条轴 + +``` + - - +``` + +**没有 vendor 段。** 实测 LLVM 那一段几乎总被填成 `unknown`,不承重; +苹果的 `apple` 与 MinGW 的 `w64` 是各自项目的标识,不是目标的性质。 +mcpp 在映射时补出编译器需要的 vendor,自己不保存它。 + +### 1.1 第三段恒定表示 C 库实现 + +⭐ **这是与 LLVM 的核心分歧,也是本方案的全部价值。** + +| mcpp 目标 | 第三段的含义 | 对应实现 | +|---|---|---| +| `x86_64-linux-gnu` | C 库 | glibc | +| `x86_64-linux-musl` | C 库 | musl | +| `x86_64-windows-gnu` | C 库 | **MinGW CRT**(封装 `msvcrt.dll`) | +| `x86_64-windows-musl` | C 库 | **musl**(openkal 之上) | +| `x86_64-windows-msvc` | C 库 | UCRT | +| `aarch64-macos` | (省略) | 由图决定:有 openkal 则 musl,否则 libSystem | +| `aarch64-macos-musl` | — | **词表里没有这一行**,见 §6.1 | +| `riscv64-none` | (无) | **没有 C 库** | + +⚠️ **macOS 也需要这一段,而我第一稿写的是「该平台只有一个 C 库」。** +实测(mcpp 2026.8.24.6,构建期体系): + +``` +Target aarch64-macos → arm64-apple-macos14.0 + c-abi musl (openkal-musl@0.3.3, graph) +``` + +⭐ **openkal 让 macOS 上也有了 musl**,与 libSystem 并存 —— 与 Windows +完全同形。所以「该平台只有一个」这句话在 openkal 出现之前成立,现在不成立, +而我在同一份文档的 §5 里已经记下了这个实测却没有回头改这张表。 + +⚠️ **`aarch64-macos` 不带第三段时该解析成哪一个?** 与 Linux/Windows 一致: +**不作请求**,由图决定 —— 有 openkal 依赖就是 musl,没有就是 libSystem。 +这正是「省略即不作请求」这条规则在第三个平台上的同一次应用。 + +⭐ **libSystem 不该有名字,而理由是实测出来的。** + +macOS 的三元组形状与另外两个平台不同 —— OS 段里带部署目标,且**没有 +env 段**: + +``` +arm64-apple-macos14.0 + ^vendor ^os + 部署目标 +``` + +clang 接受任意 env 后缀却不赋予任何含义,实测: + +``` +arm64-apple-macos14.0-gnu → 原样返回,未规范化 +arm64-apple-macos14.0-musl → 原样返回,未规范化 +``` + +原样保留而不像 Windows 那样重拼,说明 **Darwin 那一支根本不看 env 段**。 + +于是给 libSystem 起名会得到一个**在编译器侧没有对应物**的字符串。而它 +本来也不需要名字:按「省略 = 不作请求,由图决定」,两种情况都被覆盖 —— + +| 图里有 openkal | 结果 | +|---|---| +| 有 | `c-abi musl (graph)` | +| 没有 | libSystem(payload) | + +⚠️ **这与 Linux/Windows 不对称,而不对称是有理由的。** 那两个平台上 +`gnu` 是 LLVM 词表里真实存在、且映射到真实编译器行为的值(Windows 上 +它决定对象 ABI);macOS 上没有任何对应物可映射。规范因此定: +**macOS 的第三段只有一个合法值 `musl`,libSystem 由省略表达。** + +⚠️ 连带:openkal 路径上 mcpp 完全不碰 libSystem,所以这个命名问题 +在构建期体系里从一开始就不存在 —— 它只在预构建路径上被问起, +而那条路径今天用的正是省略式。 + +⚠️ **`x86_64-linux-gnu` 里的 `gnu` 严格说是项目名而非库名(glibc)。** +保留它是为了兼容既有工程与 LLVM,而不是因为它准确。规范应当**接受 +`glibc` 作为别名**并在文档中说明二者等价,新写的工程用哪个都对。 + +### 1.2 对象 ABI 是**派生**属性,不是一段 + +Windows 上 ABI 与 C 库在实践中一一对应,因此不需要第四段: + +| os | libc | 派生的对象 ABI | 实测符号 | +|---|---|---|---| +| windows | `gnu` | Itanium | `_ZN2ns4Base1fEi` | +| windows | `musl` | Itanium | 同上 | +| windows | `msvc` | MSVC | `?f@Base@ns@@UEAAHH@Z` | +| linux / macos | 任意 | Itanium | — | + +规范把这张表写成**规范化的派生规则**,而不是让每个消费者自己猜。 +`cfg(abi = "itanium")` 因此可以被支持,且与 `cfg(libc = "musl")` 正交 —— +这解决了 §0.1 里 `cfg(env=)` 语义不定的缺陷。 + +⚠️ **若将来出现「Itanium ABI + UCRT」这样的组合**(mingw-w64 确实支持 +`ucrt` 作为 CRT 而保持 Itanium ABI),一一对应就破了。届时的做法是 +**新增一个 libc 名**(`x86_64-windows-ucrt`),而不是加第四段 —— +因为破的是「libc 只有三种」而不是「一个 libc 对一套 ABI」。 + +### 1.3 裸机没有第三段 + +``` +riscv64-none ← 规范形式 +riscv64-none-elf ← 接受的别名(LLVM 拼法),规范化为上者 +``` + +理由:第三段表示 C 库,而裸机没有 C 库。`elf` 是**对象格式**, +与 `arch`+`os` 一起已经决定,不需要单独命名。 + +⚠️ 这是一处**破坏性**变更:输出目录从 `target/riscv64-none-elf/` 变为 +`target/riscv64-none/`。§4 给迁移方案。 + +### 1.4 不学 clang 的地方,以及各自的理由 + +| clang 的做法 | mcpp | 理由 | +|---|---|---| +| Windows 上 `gnu` = 对象 ABI | `gnu` = MinGW CRT | 第三段恒定表示 C 库(§1.1) | +| 无 `windows-musl` | **有** | 它真实存在(`openkal-musl`),且 clang 那条路是 ICE(§0.2) | +| 裸机带 `-elf` | 不带 | 那不是 C 库(§1.3) | +| 四段含 vendor | 三段 | vendor 不承重(§1) | +| `x86_64-unknown-linux-gnu` | `x86_64-linux-gnu` | 同上 | + +**学 clang 的地方**:段的顺序、`arch` 与 `os` 的取值集合、`musl`/`msvc` +这些名字本身。偏离仅限于上表五处,每一处都有实测支撑。 + +--- + +## 2. 映射:一个词表到多个编译器 + +规范的核心是一张**双向映射表**,而不是散落的 `if`。 + +### 2.1 到 clang(`--target=`) + +``` +mcpp → clang +x86_64-linux-gnu → x86_64-unknown-linux-gnu +x86_64-linux-musl → x86_64-unknown-linux-musl +x86_64-windows-gnu → x86_64-w64-windows-gnu +x86_64-windows-musl → x86_64-w64-windows-gnu ⭐ 与上一行相同 +x86_64-windows-msvc → x86_64-pc-windows-msvc +aarch64-macos → arm64-apple-macos14.0 +riscv64-none → riscv64-unknown-none-elf +``` + +⭐ **`windows-gnu` 与 `windows-musl` 映到同一个 clang 三元组,这是设计而非 +巧合。** 交给 clang 的字符串回答「遵循哪套对象 ABI」,mcpp 的名字回答 +「C 库是谁」。两个问题,两个答案;第二个 clang 答不了(§0.2)。 + +⚠️ 映射**不是双射**,因此从 clang 三元组反推 mcpp 目标是不确定的。 +规范应明确:**反向映射只在诊断中使用,不作为身份来源**。 + +### 2.2 到 GCC 载荷(编译器文件名) + +GCC 的目标编死在可执行文件名里,所以映射的是**载荷与文件名**: + +``` +mcpp → 载荷 / 编译器 +x86_64-linux-gnu → xim:gcc / g++ +x86_64-linux-musl → xim:musl-gcc / x86_64-linux-musl-g++ +x86_64-windows-gnu → xim:mingw-cross-gcc / x86_64-w64-mingw32-g++ +x86_64-windows-musl → (无 GCC 路径) +x86_64-windows-msvc → (MSVC,非 GCC) +riscv64-none → (clang 专属:见 §2.3) +``` + +⚠️ **`x86_64-windows-musl` 没有 GCC 路径**,而这不是缺口:musl-on-Windows +是构建期体系的产物,其 C 库来自图而非载荷。规范应把「某目标在某工具链族下 +无路径」记为**一等状态**,而不是让消费者从空字符串里推断。 + +### 2.3 到 MSVC + +`x86_64-windows-msvc` → `cl.exe` / `clang-cl`,由 SDK 提供 C 库。 +实测:clang 在 Linux 上能为该目标**编译**(产出 `?f@@YAHH@Z` 的 COFF), +但**链接**需要不可再分发的 MSVC SDK/CRT。规范应把这两件事分开记录 —— +「能否发码」与「能否链出成品」是不同的能力。 + +--- + +## 3. 规范应当定死的东西 + +| 条目 | 内容 | +|---|---| +| **语法** | `-[-]`,段内字符集,大小写 | +| **arch 取值** | `x86_64` `aarch64` `riscv64` `riscv32` … | +| **os 取值** | `linux` `windows` `macos` `none` | +| **libc 取值** | `gnu`(别名 `glibc`)`musl` `msvc` `ucrt`(预留);macOS 上仅 `musl` | +| **省略规则** | 省略 libc = **不作请求**,由图或默认决定;身份仍完整 | +| **派生属性** | `abi`(itanium / msvc)、`format`(elf / pe / macho)、`freestanding` | +| **别名表** | `riscv64-none-elf` → `riscv64-none`,`x86_64-w64-mingw32` → `x86_64-windows-gnu` | +| **映射表** | §2 的三张,双向声明,反向仅用于诊断 | +| **cfg 谓词** | `os` `arch` `libc` `abi` `format` `bare`,**不再有 `env`** | + +⭐ **`cfg(env = …)` 应当被弃用而非直接删除**:它今天在生态包里有使用者 +(实测 `openkal-windows` 一处),删掉会让旧包在新引擎上加载失败 —— +这与 [[index-floor-must-degrade]] 记的教训同族。做法是接受它、 +按 `libc` 求值、并在使用时告知新拼法。 + +--- + +## 4. 迁移 + +| 变更 | 影响面 | 做法 | +|---|---|---| +| 新增 `x86_64-windows-musl` | 加法,无破坏 | 已实施(本轮) | +| `cfg(libc=)` / `cfg(abi=)` | 加法 | 新增谓词,`env` 保留为别名 | +| 裸机去掉 `-elf` | **破坏**:输出目录改名 | 别名 + 一个发布周期的过渡,`-elf` 拼法仍接受 | +| 去掉 vendor 段 | 无 —— mcpp 本来就没保存 | 仅文档化 | + +⚠️ **裸机那一条是唯一的破坏性变更,而它的收益最小**(只是名字更整齐)。 +本方案建议**先不做**,或与一次主版本一同做。规范可以先把 +`riscv64-none` 定为规范形式、`riscv64-none-elf` 定为别名,而**输出目录 +继续用别名**,直到有别的理由动它。 + +--- + +## 5. 本文刻意没有断言的事 + +- **没有断言 `ucrt` 该怎么进表。** mingw-w64 支持 `ucrt` 作为 CRT 而保持 + Itanium ABI,这会破坏 §1.2 的一一对应。⚠️ 需要先实测「mingw-w64 + ucrt」 + 在 mcpp 的载荷里是否可达,再决定是新增 libc 名还是引入第四段。 +- **没有断言反向映射的完整规则。** §2.1 的映射不是双射,而诊断路径上 + 确实需要「从 clang 三元组说出 mcpp 目标」。规则应当是「多对一时报出全部 + 候选」还是「拒绝反推」,需要看诊断的实际用法再定。 +(macOS 显式钉住 libSystem 的问题已移到 §6,作为一条明确记录在案的代价。) + +--- + +## 6. 已知代价:记录在案,遇到真实用例再解 + +本节记录**明知而未解**的东西。它们不是遗漏,而是判断「现在解的收益不明」 +之后留下的账;每一条都给出**重新打开它的触发条件**,以免后来者需要重新 +把上下文推导一遍。 + +### 6.1 macOS 上无法显式请求 libSystem + +规范定为「libSystem 由省略表达」(§1.1)。代价是**说不出它**: + +``` +aarch64-macos 图里有 openkal → musl 图里没有 → libSystem +aarch64-macos-musl ← 没有这个写法 +aarch64-macos-??? ← 也没有这个写法 +``` + +⚠️ **上一稿在此写着「`aarch64-macos-musl` 显式请求 musl」,而词表里没有这一行。** +实测(本 PR 构建出的二进制): + +``` +$ mcpp build --target aarch64-macos-musl +error: unknown target 'aarch64-macos-musl' +``` + +也就是说,macOS 这一轴上**两个方向都说不出来**:既钉不住 musl,也钉不住 +libSystem,唯一的写法 `aarch64-macos` 把这件事整个交给图。Windows 那一轴 +之所以能说出来,是因为本 PR 为它加了 `x86_64-windows-musl` 这一行 —— +macOS 缺的是同一件东西,而不是缺一条策略。 + +于是一个**装了 openkal 依赖、却想在 macOS 上用 libSystem** 的工程, +无法表达这个意图;反过来,一个想在 macOS 上明确钉住 musl 的工程,同样 +无法表达。 + +⚠️ **Linux 上没有这个缺口**:`x86_64-linux-gnu` 恰恰就是「显式钉住 glibc」, +即使图里有 openkal-musl 也能说出来(今天的行为是报出分歧并以图为准 —— +见 `check_request`,那本身也是一条待议的策略)。 + +**不解的理由**:没有真实用例。给 libSystem 起的任何名字在 clang 侧都没有 +对应物(实测 Darwin 不看 env 段),所以它只能是 mcpp 单方面的标记 —— +为一个没人提出过的需求引入一个只有一半意义的名字,代价比收益清楚。 + +**重新打开它的触发条件**,满足任一即可: + +1. 出现一个工程,依赖图里有 openkal 而在 macOS 上确实需要 libSystem +2. openkal 支持了「同一构建里按目标选择 C 库」,使这个组合从边缘变成常规 +3. LLVM 在 Darwin 一支开始解释 env 段,使这个名字有了可映射的对象 + +**届时的候选做法**(不预先择一):`aarch64-macos-system`、 +`aarch64-macos-apple`,或者引入一个与三元组正交的表达(如 +`[target.X] c-abi = "system"`)—— 最后一条可能更合适,因为它承认 +「这不是三元组该回答的问题」。 + +### 6.2 其余待定项 + +`ucrt` 如何进表、反向映射的规则、裸机去 `-elf` 的时机 —— 见 §5。 +它们与 6.1 的差别是:那些是**尚未测**,这一条是**测过了并决定不做**。 + +--- + +## 7. 验收:`aarch64-linux-musl` 端到端可用 + +⭐ **一套目标词表的价值只有在第二个架构上才被检验。** x86_64 上「按 OS 分」 +与「按架构分」给出相同答案,所以判据用错了轴也看不出来。本节把 +`aarch64-linux-musl` 定为规范的**验收目标**,并记录逐层剥出的三处缺陷。 + +来源:mcpp-community/mcpp#492 的使用者报告 —— 他需要同时出 x86_64 与 +aarch64 两份静态二进制。 + +### 7.1 ✅ 已修:x87 例程按 OS 排,而它是架构的性质 + +``` +truncxfhf2.c:13:36: error: unknown type name 'xf_float'; did you mean 'tf_float'? +``` + +`*xf*.c` / `*xc3.c` 是 x87 80 位 `long double` 例程。排除它们的两处条件是 +`cfg(os = "macos")` 与 `cfg(os = "none")`,而 aarch64-linux 落进 +`cfg(os = "linux")` 那一支,没有排除。 + +⚠️ **两处此前都是对的**:那两个 OS 今天恰好都蕴含「非 x87 架构」。 +并且 linux 段**不排除**也是对的 —— x86 上 `long double` 真是 x87 80 位, +`-lgcc` 拿掉后 `ld.lld: error: undefined symbol: __mulxc3` 会真的出现。 +**两个方向都会坏,错的只是轴。** + +修法:`cfg(all(os = "linux", not(arch = "x86_64")))`。 +已提 mcpplibs/openkal-llvm-runtime#5。 + +### 7.2 ✅ 已修:`--no-default-config` 本身改变目标特性 + +x87 修好之后撞到下一处: + +``` +error: precompiled file 'std.pcm' was compiled with the target feature '-fmv' + but the current translation unit is not +error: current translation unit is compiled with the target feature + '+outline-atomics' but the precompiled file 'std.pcm' was not +``` + +⭐ **根因不是三元组不一致**(两边都是 `aarch64-unknown-linux-musl`, +已从缓存里那条命令逐字核对),也不是缓存串目标(键含 `target_triple`, +两个 aarch64 条目正确地按工具链分开)。实测: + +``` +clang --target=aarch64-unknown-linux-musl → +outline-atomics +clang --target=aarch64-unknown-linux-musl --no-default-config → -fmv +``` + +**`--no-default-config` 自己就改变目标特性。** std 模块带它编 +(包的 `std-module-flags` 要求),普通 TU 不带。 + +⚠️ 而那个 flag 是**必需的**,包里的注释写明理由:载荷的 `clang++.cfg` +无条件塞进宿主 C 库的头,不排除它,模块会在 `` 上失败。 + +于是这是**两个都成立的要求相撞**: + +| 要求 | 来自 | 为什么必需 | +|---|---|---| +| std 模块要 `--no-default-config` | 包 | 否则宿主 C 库的头混进来 | +| std 模块与 TU 的目标特性必须一致 | clang | 否则 BMI 加载失败 | + +⚠️ **x86_64 上 `--no-default-config` 不改变特性,所以这对矛盾从未暴露。** +aarch64 是第一个让它现形的目标 —— 这正是「第二个架构才检验抽象」的又一例。 + +**修法:把 `--rtlib=compiler-rt` 补进 `std-module-flags`。** + +实测,补上之后两侧逐字相同: + +``` +TU (读 cfg) +fp-armv8 +neon +outline-atomics +v8a +std(旧:无 rtlib) -fmv +fp-armv8 +neon +v8a +std(新:+rtlib) +fp-armv8 +neon +outline-atomics +v8a +``` + +⭐ **声明在这个包里也是最诚实的位置**:它 IS 那份 compiler-rt。 +x86_64 上两侧本来都是空,所以该 flag 对它无影响 —— 这也再次说明为何 +缺陷只在第二个架构上现形。 + +### 7.2b ✅ 已修:特性开了,而辅助函数没人产出 + +7.2 修好后撞到下一层: + +``` +ld.lld: error: undefined symbol: __aarch64_swp4_acq +``` + +`+outline-atomics` 需要 `__aarch64_*` 辅助函数,它们住在 compiler-rt 里 —— +而本包没产出。upstream 把 `aarch64/lse.S` 编 **125 遍**,每遍给不同的 +`-DL_ -DSIZE= -DMODEL=`,`sources` 的 glob 传不了 per-file 定义。 + +⭐ **解法是把宏从命令行移进文件,而不是复制 125 份实现**:每个组合成为 +一个只声明宏、再 include 共享正文的小文件,正文仍是 upstream 的、未改、 +在原处。生成物放在 `llvm-generated/outline-atomics/`,与 `llvm/` 分开 —— +与本包既有约定一致(`std.cppm`、`__config_site` 都在那里)。 + +⚠️ 途中两处「以为做完了其实没有」: +`#include "assembly.h"` 在新目录下解析不到(汇编器把 `HIDDEN(...)` +读成指令),解法是让生成文件按相对自身的路径 include 那个头,不需要 flag; +以及 `__aarch64_have_lse_atomics` 未定义 —— 它在 `cpu_model/aarch64.c`, +而 glob `builtins/*.c` **不含子目录**,那个文件从未被编过。 + +⚠️ **我先把 `-mno-outline-atomics` 加在 mcpp 引擎里,那是错的方向** —— +关掉一个本该支持的特性。有了辅助函数之后该 flag 已撤,**引擎零改动**。 + +**验收判据(已达成)**: + +``` +mcpp build --target aarch64-linux-musl +→ ELF 64-bit LSB executable, ARM aarch64, statically linked + 已定义的 __aarch64_* 符号 10,未定义 0 + 产物中 LSE 指令 8 处 ← 特性真的在工作 +``` + +### 7.3 ⚠️ 未查:`aarch64-linux-gnu` 仍是 `planned` + +使用者撞到的原话是: + +``` +error: target 'aarch64-linux-gnu' is registered but not yet supported (planned) +``` + +⭐ 而他真正需要的是 **`aarch64-linux-musl`**(静态二进制),那一行是 +`verified`。⚠️ 这是一处**文档/诊断问题而非能力问题**:诊断说了「没发布 +工具链」,没说「你要的那个目标另有拼法」。`did_you_mean` 今天只对拼错的 +名字生效,不对「档位不够但同类目标可用」的情况生效。 + +### 7.4 使用者报告里的另外两条 + +### 7.5 `std::random_device`:根在规范,不在运行时包 + +使用者报告 `_LIBCPP_HAS_RANDOM_DEVICE 0` 让 5 个 TU 编不过,本地改成 1 即修好, +并问「静态 musl 二进制读 `/dev/urandom` 应当可行,`generic` 那份为何也关着」。 + +⚠️ **改成 1 只让编译期通过。** 编译期只看宏,真正链接会缺符号 —— +那个 0 不是保守设置,是**当前事实的准确记录**。 + +**因果链逐层查证:** + +``` +std::random_device + ↓ libc++ 有五条后端:GETENTROPY / DEV_RANDOM / ARC4_RANDOM / + WIN32_RANDOM / FUCHSIA_CPRNG + ↓ getentropy 是最合适的一条 —— 它不需要文件系统 +musl 的 src/misc/getentropy.c → 调 getrandom() + ↓ +musl 的 src/linux/getrandom.c → syscall_cp(SYS_getrandom, …) + ↓ 而 openkal-musl 的 port/ 里没有它的替代实现 +openkal 规范里**没有随机源接口**(SURFACE.txt 全文无 random / entropy) +``` + +⭐ **根在最底下那一层。** 另一条后端 `DEV_RANDOM` 走 +`open("/dev/urandom")` + `read`,同样落在 openkal 的文件系统接口上, +而一个没有文件系统的后端(裸机)连这条也没有。 + +**三层各自能做什么:** + +| 层 | 可行性 | 代价 | +|---|---|---| +| **openkal 规范** | ⭐ 根本解 —— 新增 `openkal.random` 或等价接口 | 六个后端各实现一遍;按 6.1 条,不提供该接口的后端让它链接期缺席 | +| **openkal-musl** | 可以,但绕过规范 —— port 里实现 `getrandom` | 它仍要从**某处**取熵,绕不开平台层 | +| **openkal-llvm-runtime** | ❌ 只能开关那个宏 | 开了在链接期缺 `getrandom`,把编译错换成链接错 | + +⚠️ **「`generic` 那份也关着」的答案**:`generic` 不等于「宿主 Linux」。 +在这套体系里它同样跑在 openkal 之上,同样没有随机源 —— 名字容易误读, +而设置是对的。 + +**⚠️ 我先前写「openkal-llvm-runtime 只能开关那个宏」,那句话是错的。** +移植机制现成:`openkal-musl/port/src/okm_syscall.c` **已经把 69 个系统调用 +转给了 openkal**(`SYS_read`、`SYS_openat`、`SYS_mmap`、`SYS_futex` …)。 +真正的约束不是「没有转的办法」,是**规范里没有可转的目标**。 + +**为什么不能经 fs 打开 `/dev/urandom`。** 这条路在设计上就被挡住: +`kal_fs_open` 只能相对 **preopen 目录**打开,而 openkal-linux 只给 2 个 +preopen。⭐ **那正是能力模型要挡的东西** —— 一个程序不能凭一个绝对路径 +够到平台上的任意对象。所以这不是缺口,是拒绝。 + +### 7.5.1 判定:该不该进规范 + +原则是「openkal 不加就能实现,就不加;但绝不能不走 openkal 接口层」。 +逐条核对: + +**① 能不能不加就实现 —— 不能。** + +| 现有接口 | 能否供给熵 | +|---|---| +| `fs` | ❌ `kal_fs_open` 只能相对 preopen 目录 —— 能力模型的**拒绝**,不是缺口 | +| `time` | ❌ 时钟不是熵 | +| `env` | ❌ 只有 arg / var 的读取,没有平台数据通路 | +| 其余六个 | ❌ 与熵无关 | + +⭐ **而这个包已经诚实地回答过同一个问题。** `okm_start.c` 为 musl 的 +`AT_RANDOM` 凑了 16 字节,注释写着: + +> openkal has no source of entropy and this layer does not invent one: the +> bytes below are derived from the clock and from the address of an object, +> which makes them unpredictable to a reader of the source **and not to an +> adversary**. … the README records that **neither is a security property on +> this port**. + +那 16 字节给 allocator cookie 与 stack canary 够用 —— 它们本来就不承诺安全。 +`std::random_device` 不够:那个类的存在意义就是不可预测。 + +**② 能不能绕过接口层 —— 不能。** 直接发 `SYS_getrandom` 违反原则, +而这正是本条最初的症状。 + +**③ 它是不是通用内核能力 —— 宿主上是,裸机上不是。** + +| 后端 | 平台接口 | 有无 | +|---|---|---| +| linux | `getrandom(2)` | ✅ | +| macos | `getentropy(2)` / `arc4random_buf` | ✅ | +| windows | `ProcessPrng` / `BCryptGenRandom` | ✅ | +| uefi | `EFI_RNG_PROTOCOL` | ⚠️ **可选协议**,固件不一定实现 | +| opensbi | SBI 基础规范无 RNG 扩展 | ❌ 取决于板载外设 | + +⭐ **「宿主普遍有、裸机不一定有」正是 openkal 能力模型为之设计的形状。** +按 6.1 条,不提供该接口的后端让它作为链接期定义缺席 —— 与 `openkal.fs` +在 opensbi 上缺席完全同理。 + +**⇒ 判定:该加。** 三条都指向同一个答案,而第三条还说明它加进去之后 +不需要任何新机制。 + +**落地形状,两段:** + +**长期(根本解)** —— openkal 规范新增随机源接口。按 6.1 条, +不提供它的后端让相应符号链接期缺席,这正是规范已有的机制。 + +**当下(可立即做)** —— `openkal-musl` 加一个 feature。该包已有 +`[features]`(`default = ["posix"]`),机制现成: + +```toml +[features] +host-entropy = { } # port 提供 getrandom,经宿主特定通路取熵 +``` + +⚠️ **它在裸机上无法实现**,所以名字必须说清它假设了什么 —— +`host-entropy` 而不是 `random`。 + +⚠️ 并且 `openkal-llvm-runtime` 的 `_LIBCPP_HAS_RANDOM_DEVICE` 必须 +**跟随**这个 feature,不能独立开关 —— 否则回到「宏开了、符号没有」, +把编译错换成链接错而已。 + +**这条跨三个仓库(openkal 规范 / openkal-musl / openkal-llvm-runtime), +且必须先在规范里定接口。** 它不属于目标词表这一 PR:词表讲的是「目标怎么 +命名与映射」,这条讲的是「平台接口该不该多一个能力」,混在一起会让两件事 +都说不清。 + +### 7.5.2 `openkal.random` 接口设计草案 + +按 §7.5.1 的三条判定,该加。形状照 `openkal.time` —— 它是现有接口里最小的 +一个,而随机源的问题结构与它同型:**一件事,可能不被提供,提供时行为有差异**。 + +```c +/* openkal.random --- a source of unpredictable bytes. + * + * ⚠️ NOT A GENERATOR. This interface answers "give me bytes the platform + * considers unpredictable"; it does not define a PRNG, hold state, or promise + * a distribution. A program that wants a reproducible sequence seeds its own + * generator from these bytes and never comes back. + */ +#define KAL_RANDOM_PROP_BLOCKING ((kal_uintptr)1u << 0) +#define KAL_RANDOM_PROP_HARDWARE ((kal_uintptr)1u << 1) + +extern const kal_uintptr kal_random_props; + +/* Fills `len` bytes at `out`. Returns kal_ok, or an error. + * + * ⚠️ NO PARTIAL SUCCESS. Either every byte is filled or none is, and the + * distinction between "the source is momentarily empty" and "this environment + * has no source" is the difference between `kal_err_again` and the interface + * being absent at link time (clause 6.1). + */ +kal_status kal_random_fill(void* out, kal_uintptr len); +``` + +**两个能力字位,各自的理由:** + +| 位 | 含义 | 为什么程序需要知道 | +|---|---|---| +| `BLOCKING` | 熵不足时可能阻塞 | 一个在早期启动路径上取随机数的程序会因此挂住;它需要能选择不那么做 | +| `HARDWARE` | 直接来自硬件 RNG 而非内核池 | 影响的是信任模型,不是接口行为 | + +⚠️ **不设 `KAL_RANDOM_PROP_AVAILABLE`。** 「有没有」由接口的**在场与否**回答 +(6.1 条),不由能力字回答 —— 这正是 `openkal-opensbi@0.1.3` 那次撤回的教训: +给一个不提供的接口定义能力字,是回答第三个问题而跳过第二个。 + +**六个后端的实现路径:** + +| 后端 | 实现 | 提供? | +|---|---|---| +| linux | `getrandom(2)` | ✅ | +| macos | `getentropy(2)` | ✅ | +| windows | `ProcessPrng` / `BCryptGenRandom` | ✅ | +| uefi | `EFI_RNG_PROTOCOL` | ⚠️ 协议在则提供,不在则整个接口缺席 | +| opensbi | 无 SBI 扩展 | ❌ 不提供 | +| 裸机通用 | 取决于板载 RNG | 由 BSP 决定 | + +⭐ **uefi 那一行是这个设计最有意思的地方**:同一个后端在不同固件上 +可能提供或不提供。按 6.1 条「实现提供一个接口是全有或全无」, +`openkal-uefi` 必须在**构建期**决定 —— 要么声明提供并在运行期 +`EFI_RNG_PROTOCOL` 缺席时返回错误,要么整个不提供。⚠️ 前者违反 6.1 条的 +「不得以运行期拒绝表达部分性」;所以正确做法是**后者**,而想要它的 +固件用一个 feature 打开。 + +**连带改动:** + +1. `openkal-musl` 的 `port/` 加 `getrandom` 转发到 `kal_random_fill`, + 并把 `okm_start.c` 的 `AT_RANDOM` 从「时钟凑数」改为真随机 —— + ⚠️ 但要保留 fallback:该接口缺席时那 16 字节仍要有值,否则裸机上 + 连 stack canary 都起不来。 +2. `openkal-llvm-runtime` 的两份 `__config_site` 把 + `_LIBCPP_HAS_RANDOM_DEVICE` 改为跟随目标是否有该接口。 +3. openkal 的一致性套件加一条:同一实现连续两次 `kal_random_fill` + 不得返回相同字节。⚠️ 这条会以极小概率误报,而那个概率是 2^-len*8 —— + 在 `len=32` 时可以忽略,写清楚比省掉好。 + +**⚠️ 本草案未实测。** 它是判定之后的下一步,而判定本身(§7.5.1)是实测的。 +接口一旦定下,六个后端与两个消费者都要改 —— 这是跨三仓库的一件事, +不属于目标词表这一 PR。 + +### 7.6 GMF 里 include 标准头,未查 + +`clang++.cfg` 把宿主 libc++ 与 glibc 的 `-isystem` 塞进来,与 openkal 的 +musl libc++ 头混合。⭐ 与 7.2 是**同一个 cfg** 引起的两个症状,应一并考虑 —— +7.2 已由「把 `--rtlib` 补回 std 模块」解决,这一条尚未查。 +- **GMF 里 `#include <标准头>` 的 TU 编不过** —— `clang++.cfg` 把宿主 + libc++ 与 glibc 的 `-isystem` 塞进来,与 openkal 的 musl libc++ 头混合。 + ⭐ 与 7.2 是**同一个 cfg** 引起的两个症状,应一并考虑。 + +## 8. 跨仓库自审:这条链条上被检查出来的东西 + +本节记录的是**在推进九个仓库的过程中被判据抓住、而不是被设计预见到**的问题。 +每一条都附实测,以及它当时为什么没有被任何东西挡住。 + +### 8.1 六个仓库的版本号等于已发布版本,而没有任何地方在检查 + +这个生态里每个包发布的都是**仓库在某个 tag 上的归档**,所以对仓库的任何改动 +都改变产物。一次发布因此需要 `mcpp.toml` 的 `version` 前进,而没有任何地方在 +检查它有没有前进 —— 每个包的 CI 校验的是**眼前这棵树**,对「哪些数字已经被 +占用」没有意见。 + +实测(2026-08-25,跟随 openkal 0.7.0 走完生态):**八个仓库里有六个**待合入的 +分支,其版本号等于 `main` 的版本号,也就是索引里已有的版本号;其中两个还新增 +了一整个接口实现。六个全都是绿的。 + +⚠️ **后果是安静的,不是响亮的。** `git tag 0.5.3` 发现 tag 已存在便成功;从那个 +tag 取下来的归档是**旧内容**;两条镜像腿于是彼此一致、也与源归档一致 —— +校验完美通过,而什么都没有发布。唯一残留的痕迹是索引里第二个 `["0.5.3"]`, +而 Lua 接受重复键、保留最后一个、一声不吭。 + +修法落在**索引**,因为那是「什么已经发布」的唯一记录,一处覆盖生态里所有包: +`tests/check_duplicate_versions.lua`,文本扫描而非加载描述符 —— 加载正是看不见 +它的那种做法,因为表建起来的时候重复已经合并掉了。128 个描述符全绿;把 openkal +的 0.7.0 条目插两遍,它按文件、行号、平台段点名,而同一份文件 `loadfile` 成功。 + +### 8.2 mangling 暂存丢掉与源码相邻的私有头 + +一张图里出现同一个包的两个主版本时,解析器把其中一份改名暂存到 +`target/.mangled/`。暂存**按设计**只搬源码:经 `[build].include_dirs` 找到的头 +靠绝对化后的路径仍指回原处。 + +写在**源码旁边**的私有头是另一回事,而它没有被处理。`#include "detail.h"` 是 +相对**持有该指令的文件所在目录**解析的,搬走源码就搬走了搜索起点;并且没有任何 +`include_dirs` 条目参与其中,因为一个把头放在源码旁边的包**从来不需要**声明 +路径。 + +⚠️ **这条诊断指向的每一样东西都是错的**:路径是作者没写过的暂存目录,头文件 +就躺在源码期待的位置,触发它的构建也没有要求任何不寻常的事情 —— 一张图里两个 +主版本是受支持的安排,而这是它最普通的后果。它让两个仓库各红了一次 CI,并且 +第一次被诊断成「缺 include 路径」。 + +已修(mcpp #502),并用**两个二进制**而不是一个二进制加一个假设来证明: + +``` +mcpp-unfixed 33_multi_version_mangling.sh → fatal error: libB_detail.h +mcpp-fixed 33_multi_version_mangling.sh → ok,与源码一同暂存 +``` + +### 8.3 一条不可能失败的断言 + +`tests/e2e/284` 里,验证「mcpp 自己的名字进去、编译器认识的拼法出来」这条映射的 +分支写着 `*) : ;;` —— 匹配就打印一行 ok,不匹配也通过。而这条映射正是本 PR +新增那一行的**全部意义**。已改为可失败,并在旁边补上对 pin 的断言。 + +⭐ 同型排查:e2e 全库另有 5 处 catch-all 分支,逐个读过,全部是合法的平台分派或 +带理由的 SKIP,不是伪装成断言的空操作。 + +### 8.4 跨仓库分支协调靠同名,而不同名时不会失败 + +`openkal-musl` 的 CI 把规范与实现的**工作树**放到包旁边,取的是与当前分支同名的 +分支,没有同名分支时回落到默认分支。这是对的 —— 这里多数分支本来就没有对应物 —— +但一次跨两个仓库的改动,于是会对着「哪一半恰好在 main 上」构建。 + +实测:本包的分支叫 `feat/getrandom-through-openkal`,规范的叫 +`feat/openkal-random`,于是一个要求 0.7.0 的清单拿到了 openkal 0.6.0,五个 job +报的是: + +``` +port/src/okm_syscall.c:27:10: fatal error: 'openkal/random.h' file not found +``` + +**缺头文件读起来像是本包写错了。** 错的是两半不同步,而这正是新加的门要说的话。 +硬失败也是唯一安全的答案:如果规范那半的改动是**向后兼容**的,同样的错配会 +通过 —— 而被测的是没有人在审的那份规范。 + +判据用的是**清单本来就写着的版本要求**,只是被「改写成 path」这一步绕过去了: + +``` +tree 0.6.0 → exit 1,"they are not in step" +tree 0.7.0 → exit 0 +tree 0.7.4 → exit 0 补丁位可以前进 +tree 0.8.0 → exit 1 1.0 以下,minor 前进即为破坏性 +``` + +### 8.5 文档里写着一种实现中不存在的目标拼法 + +见 §6.1 的更正。`aarch64-macos-musl` 曾被本文列为「显式请求 musl」的写法, +而词表里没有这一行,实测 `error: unknown target`。随后把 docs/16(中英两份) +与三份分析文档里出现的**全部 23 个目标名**逐一对着词表核过:四个是填充规则下 +合法的短拼法,四个出现在明说「不采用」的段落里,只有这一个被当作真的写着。 + +### 8.6 openkal 的 README 落后规范两行 + +接口表少了 `openkal.random`(本次新增)与 `openkal.exec`(已 optional 很久且 +有头文件)。⚠️ **没有任何东西在检查它**:规范的接口表、模块/头映射表、 +`SURFACE.txt` 和 README 的表是同一份清单的四种陈述,而 CI 只比对前三种 —— +表面检查器读的是 `SURFACE.txt` 和头文件,README 对它来说是散文。这就是一行 +能在若干次发布里一直缺着的原因。 + +### 8.7 一处排除,两个决定 + +`openkal-llvm-runtime` 里 `!llvm/libcxx/src/random.cpp` 这条排除被删掉,对 +hosted 是对的 —— 它同时把 `_LIBCPP_HAS_RANDOM_DEVICE` 打开了。但 **freestanding +那份配置仍是 0**,而同一次删除也波及了它: + +``` +llvm/libcxx/src/random.cpp:68:1: error: use of undeclared identifier 'random_device' +``` + +开关关掉时 `` 不声明这个类,而 `src/random.cpp` 无条件定义它的成员 —— +这个文件**自己不带守卫**,不像旁边的 filesystem 源码。 + +⚠️ **hosted 块的注释里早就写着 freestanding 的立场**("The freestanding +configuration keeps the switch at 0"),只是没有人照着做。一条排除服务于两个 +互相不同意的配置,删它就要分别回答。 + +判据取的是**构建图**而不是「编过了」:`libcxx/src/random.cpp` 在 freestanding 的 +`build.ninja` 里出现 0 次(与已知被排除的 filesystem 一致),而应当在的 +`random_shuffle.cpp` 出现 2 次。再把排除注释掉重建 —— 复现同一条错误。 + +### 8.8 可选接口的转发者让「可选」变成了「必需」 + +`openkal.random` 是可选接口,裸机后端不提供它。而 `openkal-musl` 的系统调用 +分发器**无条件**引用 `kal_random_fill`,且这个分发器被链接进**每一个**程序: + +``` +ld.lld: error: undefined symbol: kal_random_fill +>>> referenced by okm_syscall.c:409 +``` + +于是一条可选接口的缺席,变成了**所有裸机程序**的链接失败 —— 哪怕它一个随机 +字节都不要。这是 6.1 条在另一个方向上的后果:该条把「不提供」表达为「没有定义」, +而任何**无条件引用**都会把这句话翻译成「必须提供」。 + +⭐ **可选接口的转发者必须弱引用**,缺席时返回该层自己的「没有这个东西」的答案。 +这里是 `ENOSYS` —— Linux 系统调用 ABI 对未实现调用的定义答案,而 musl 自己的 +`getrandom` 正是照着它写的。 + +⚠️ **这不是 6.1 条禁止的运行期拒绝。** 那一条约束的是 **openkal 的实现**: +不得提供一个运行时报告不支持的接口。弱引用发生在**层的另一侧** —— 转发者实现的 +是 Linux 的契约,不是 openkal 的。 + +⚠️ **惯用法和约定这个仓库本来都有,一个都没被用上**:`okm_phdr.c` 就在用 +`__attribute__((weak))`,而 `okm_syscall.c` 开头第 11 行写着「能被告知没有的 +调用返回 -ENOSYS」。 + +**这条缺陷发布出去了,而 openkal-musl 自己的 CI 一格都没看见它** —— 是下游 +`openkal-llvm-runtime` 的裸机程序把它顶出来的。补的检查落在**符号类别**上, +不需要裸机工具链,也比一次链接更直接地陈述这个不变量;并且自带控制项: + +``` +有 weak w kal_random_fill U kal_time_sleep → 绿 +去掉 weak U kal_random_fill U kal_time_sleep → 红 +``` + +只断言「random_fill 是弱的」会在符号消失或对象没被构建时同样通过,所以旁边那个 +**必需**接口必须仍是未定义强引用。 diff --git a/.agents/docs/2026-08-25-os-toolchain-target-matrix.md b/.agents/docs/2026-08-25-os-toolchain-target-matrix.md new file mode 100644 index 00000000..143a2dff --- /dev/null +++ b/.agents/docs/2026-08-25-os-toolchain-target-matrix.md @@ -0,0 +1,472 @@ +# OS × 工具链 × 目标:组合矩阵与理由 + +2026-08-25。本文枚举三个轴的**实际**组合,给出每格成立或不成立的理由, +并标出其中的缺陷。事实取自 HEAD 的代码与本机实测,不取自推理。 + +与既有两份的分界:`2026-08-24-target-side-design.md` 讲**五层由谁供给**, +`2026-08-25-target-system-analysis.md` 讲**三元组的语法与语义**, +本文讲**三个轴交叉出来的格子**。 + +--- + +## 0. 三个轴 + +| 轴 | 取值 | 由什么决定 | +|---|---|---| +| **宿主 OS** | linux / macos / windows | mcpp 跑在哪台机器上 | +| **工具链** | `Family { Gcc, Llvm, Msvc }` × 载荷 | `[toolchain]`,或全局默认,或目标行的 `pin` | +| **目标** | `kKnownTargets` 的 16 行 | `--target`,或 `[build] target`,或宿主 | + +⚠️ **三个轴不独立,而依赖方向是单向的**:目标 → 需要某种 C 库 → +只有某些(宿主,工具链)组合能供给它。反过来不成立:工具链不决定目标。 + +## 0.1 两种体系把这张表分成两半 + +| | 传统预构建体系 | 构建期体系 | +|---|---|---| +| C 库来自 | 工具链**载荷** | 依赖**图**,从源码构建 | +| 谁限制组合 | **载荷是否存在**(见 §2 的 `host_can_serve`) | 只需编译器能发这个目标的码 | +| 可达格子 | 少,且按宿主分 | 多,且几乎与宿主无关 | + +⭐ **本文最重要的一句**:`host_can_serve()` 是**预构建体系**的判据。 +构建期体系下它过于保守 —— clang 是天生的交叉编译器,而 C 库由图供给, +所以「这台宿主能不能服务这个目标」的答案与载荷覆盖无关。 +今天 mcpp 用同一个谓词回答两个问题,§5 是它的后果。 + +--- + +## 1. 目标表(16 行,实测) + +| 目标 | 档 | 备注 | pin(C 库载荷) | sysroot | 默认静态 | +|---|---|---|---|---|---| +| `x86_64-linux-gnu` | verified | | — | | 否 | +| `x86_64-linux-musl` | verified | | `gcc@16.1.0` | | 是 | +| `aarch64-linux-musl` | verified | | `gcc@16.1.0` | | 是 | +| `x86_64-windows-gnu` | verified | PE | `gcc@16.1.0` | | 是 | +| `x86_64-windows-msvc` | verified | PE | — | | 否 | +| `aarch64-macos` | verified | | — | | 否 | +| `riscv64-linux-musl` | planned | | — | | 是 | +| `aarch64-linux-gnu` | planned | | — | | 否 | +| `x86_64-macos` | planned | | — | | 否 | +| `riscv64-none-elf` | verified | bare | `llvm@22.1.8` | `xim:picolibc-riscv@1.8.12` | 是 | +| `riscv32-none-elf` | verified | bare | `llvm@22.1.8` | `xim:picolibc-riscv@1.8.12` | 是 | +| `aarch64-none-elf` | preview | bare | `llvm@22.1.8` | — | 是 | +| `x86_64-none-elf` | preview | bare | `llvm@22.1.8` | — | 是 | + +⚠️ **`pin` 列的语义常被读错。** 它不是「这个目标首选哪个编译器」,而是 +「**这个目标的 C 库由哪个载荷供给**」。`x86_64-linux-musl → gcc@16.1.0` +说的是 musl-gcc 载荷带着 musl;它对一个 C 库来自图的工程毫无意义 —— +这正是 2026.8.24.3 把该 pin 推迟到依赖图解析之后才施加的原因。 + +## 2. 宿主 × 目标:`host_can_serve()` 的实际规则 + +`src/toolchain/registry.cppm:586`。逐条抄录并给出理由: + +| 目标 | linux 宿主 | macos 宿主 | windows 宿主 | 理由 | +|---|---|---|---|---| +| `*-linux-musl` | ✅ 任意 arch | ❌ | ✅ 仅同 arch | musl 载荷自包含,不需要宿主 sysroot | +| `*-linux-gnu` | ✅ 仅同 arch | ❌ | ❌ | glibc 目标还需要 `xim:glibc` / `xim:linux-headers`,它们只有本 arch 的 | +| `*-windows-gnu` | ✅ | ❌ | ✅ | mingw 交叉载荷有 linux-hosted 与 windows-hosted 两份 | +| `*-windows-msvc` | ❌ | ❌ | ✅ | MSVC 只在 Windows 上存在 | +| `aarch64-macos` | ❌ | ✅ | ❌ | 见下:是 SDK 的分发条款,不是 libc 闭源 | +| `*-none-*`(裸机) | ✅ | ✅ | ✅ | ⭐ clang/lld 天生交叉,**不需要任何按宿主的载荷** | + +⚠️ **macOS 那一行的理由要说准,我第一稿写错了。** Apple 的 **Libc 是开源的** +(`apple-oss-distributions/Libc`),所以「libc 闭源」不是原因。真正的原因是 +**SDK 的分发条款**:交叉链接一个 Mach-O 需要 SDK 里的头文件与 `.tbd` 存根库, +而那些不可再分发,mcpp 因此没有 macOS 的交叉载荷。 + +⭐ 并且 `libSystem` 不等于 Libc —— 它是一把伞,底下含 libc、libm、 +libpthread、libdyld 等。说「macOS 的 C 库是 libSystem」是就**链接对象**而言, +不是就**实现来源**而言。 + +⭐ 裸机那一行是整张表里唯一「三个宿主全绿」的,而理由不是覆盖得好, +是**它根本不需要载荷**。注释把这一点写得很准: + +> ⚠️ Serviceable is not the same as complete: a target with no C library still +> links only `-nostdlib` programs. + +## 3. 宿主 × 工具链:哪些族能装 + +`available_toolchain_indexes()`: + +| 宿主 | 可装的族 | 索引名 | +|---|---|---| +| linux | Gcc, Llvm | `gcc`、`musl-gcc`、`llvm` | +| macos | Gcc, Llvm | 同上 | +| windows | Gcc, Llvm, **Msvc** | 另加 `msvc`(pinned toolset)、`mingw-gcc` | + +`Family` 只有三个值,而载荷远不止三个(`musl-gcc`、`mingw-cross-gcc`、 +`mingw-gcc`、`aarch64-linux-musl-gcc`…)。**族是语言方言的轴,载荷是分发的轴**, +两者一对多。 + +## 4. 三轴交叉:本机实测 + +宿主 = linux/x86_64。每格跑 `mcpp build --target X`,记录 c-abi 的实际供给者。 + +### 4.1 构建期体系(工程依赖 `openkal-llvm-runtime`) + +| 目标 | 工具链 | c-abi 实际 | 诊断 | +|---|---|---|---| +| `x86_64-linux` | llvm | musl (graph) | — | +| `x86_64-linux-gnu` | llvm | musl (graph) | ⚠️ 名实不符警告 | +| `x86_64-linux-musl` | llvm | musl (graph) | — | +| `x86_64-windows` | llvm | musl (graph) | — | +| `x86_64-windows-gnu` | llvm | musl (graph) | **静默** ← §5.1 | +| `aarch64-macos` | llvm | musl (graph) | — | + +⭐ **六格里五格的 c-abi 都是 musl,且都来自图。** 工具链恒为 llvm, +宿主恒为 linux。**目标的 OS 只改变 kernel-abi 那一层** +(`openkal-linux` / `openkal-windows` / `openkal-macos`),其余四层不变。 +这是构建期体系的全部意义,一张表就能看完。 + +### 4.2 传统预构建体系(无 openkal 依赖) + +| 目标 | 工具链(自动) | c-abi 实际 | 诊断 | +|---|---|---|---| +| `x86_64-linux` | gcc 16.1.0 | **gnu** (payload) | — | +| `x86_64-linux-gnu` | gcc 16.1.0 | **gnu** (payload) | — | +| `x86_64-linux-musl` | gcc 16.1.0 | **musl** (payload) | — | +| `x86_64-windows` | gcc 16.1.0 (mingw-cross) | **gnu** (payload) | — | +| `x86_64-windows-gnu` | gcc 16.1.0 (mingw-cross) | **gnu** (payload) | — | + +⭐ **对照 4.1 的同一列**:同一个 `--target x86_64-windows-gnu`, +预构建下 c-abi 是 `gnu`,构建期下是 `musl`。**这不是同一个目标**, +而 mcpp 用同一个名字称呼它们 —— §5.2。 + +--- + +## 5. 缺陷 + +### 5.1 同形的两件事,一个报一个不报 + +``` +构建期 + x86_64-linux-gnu 名字说 gnu,事实是 musl → ⚠️ 警告 +构建期 + x86_64-windows-gnu 名字说 gnu,事实是 musl → ❌ 静默 +``` + +原因是 `check_request()` 的第一行豁免了非「C 库轴」的平台。豁免的理由 +(「Windows 上 `gnu` 命名对象 ABI 而非 C 库」)**只对了一半**:那一段 +在 Windows 上捆着两件事 —— 对象 ABI(被兑现)与 MinGW 的 C 运行时 +(被图替换)。第二件正是本函数存在的意义。 + +⚠️ 裸机的 `elf` 必须继续豁免:它在任何平台上都不命名 C 库, +对它说「名字请求了 `elf` C ABI」是胡话而不只是噪声。 + +**修法**:判据从「轴 == CLibrary」改为「轴 ∈ {CLibrary, ObjectAbi}」。 +误伤由既有的 `fromGraph()` 挡住 —— §4.2 实测预构建五格的 c-abi 都是 +payload 来源,不进警告。 + +### 5.2 ⭐⭐ musl on Windows 存在,而 mcpp 没有它的名字 + +§4 的两张表并排放,这一条就无法回避:同一个 `x86_64-windows-gnu`, +预构建下 C 库是 MinGW 的,构建期下是 **musl**。产物实测: + +| 观测 | 值 | +|---|---| +| 导入的库 | `ntdll`、`KERNEL32`、`SHELL32` —— 无 `msvcrt`,无 `ucrtbase` | +| Itanium 修饰符号 | 4507 | +| MSVC 修饰符号 | 0 | + +**MinGW 的 C 运行时一个都没链进来。那就是 Windows 上的 musl。** +而 mcpp 的身份、输出目录、`cfg(env=)` 与打包的 ABI tag 全都写着 `gnu` —— +恰恰是这个 C 库唯一不是的东西。 + +⚠️ **我此前把这条否掉的理由是错的**,理由是「LLVM 的三元组词表没有 +`x86_64-windows-musl` 这个拼写」。那句话本身是对的,但它是关于**交给 +clang 的字符串**的,而 mcpp 的规范形式与它**本来就是两个字符串** —— +报告里那个箭头两侧就是: + +``` +Target x86_64-windows-gnu → x86_64-w64-windows-gnu + ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ + mcpp 的身份 交给 clang 的 +``` + +用编译器的词表限制 mcpp 自己的词表,是把两个轴混成了一个。 + +**修法是一个名字,不是一套机制。** 但在给出它之前,必须先纠正本文 +上一稿里的一处错误 —— 那一稿把两种体系的列并进了一张表: + +``` + ✗ 错的: + mcpp 目标 → 交给 clang c-abi + x86_64-windows-gnu → x86_64-w64-windows-gnu MinGW CRT +``` + +⚠️ **这一行不自洽。** `MinGW CRT` 是**预构建**路径的答案,而 +`x86_64-w64-windows-gnu` 是**clang** 路径上的字符串。实测预构建路径的 +编译行: + +``` +x86_64-w64-mingw32-g++ ← 编译器 +(没有任何 --target= 参数) +``` + +⭐ **预构建路径根本不传三元组。** 那个载荷的编译器天生只发一个目标, +目标由「装了哪份载荷」决定,不由命令行决定。把它的 c-abi 与 clang 的 +`--target` 并列,等于把两个体系混成一个 —— 这与 §5.2 开头批评的 +「用编译器的词表限制 mcpp 的词表」是同一类错误,只是方向相反。 + +正确的表必须按体系分开: + +**传统预构建体系**(载荷决定目标,无 `--target`) + +| mcpp 目标 | 编译器 | c-abi | +|---|---|---| +| `x86_64-linux-gnu` | `g++`(glibc 载荷) | glibc | +| `x86_64-linux-musl` | `x86_64-linux-musl-gcc` | musl | +| `x86_64-windows-gnu` | `x86_64-w64-mingw32-g++` | MinGW CRT | +| `x86_64-windows-msvc` | `cl.exe` | UCRT | + +**构建期体系**(clang + 图,`--target` 决定) + +| mcpp 目标 | 交给 clang | c-abi | +|---|---|---| +| `x86_64-linux`(或 `-musl`) | `x86_64-unknown-linux-musl` | musl(图) | +| `x86_64-windows`(今日拼作 `-gnu`) | `x86_64-w64-windows-gnu` | **musl**(图) | + +最后一格就是缺名字的那一格。 + +### 5.2.1 ⚠️ clang 为什么没有 `windows-musl`:实测是它会崩 + +「LLVM 没有这个拼写」这句话我说了三遍,而它不准确。实测 llvm 22.1.8: + +| 三元组 | `-dumpmachine` | 编译 | +|---|---|---| +| `x86_64-pc-windows-musl` | `x86_64-pc-windows-musl`(**原样保留**) | **崩溃** | +| `x86_64-w64-windows-musl` | `x86_64-w64-windows-musl`(原样保留) | **崩溃** | +| `x86_64-unknown-windows-musl` | 原样保留 | **崩溃** | + +``` +PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ +Stack dump: +0. clang++ --target=x86_64-pc-windows-musl -c t.cpp +2. Code generation +``` + +⭐ **三元组解析器认识它,代码生成路径没有实现它。** 加上 +`-nostdinc -ffreestanding` 的纯 C 也一样崩 —— 所以这不是缺头文件、 +缺 sysroot 或缺库,是 LLVM 里 `(windows, musl)` 这个组合根本没有后端处理。 + +**崩点被抓到了**,它很说明问题: + +``` +#5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) +``` + +⚠️ 崩在 **COFF 写出器**。clang 已经认定「输出 COFF」,却没有一条 +Windows-musl 的路径把 streamer 需要的状态配齐 —— 这不是「不支持所以拒绝」, +是「没人写过所以走到就死」。ICE,不是诊断。 + +**决定性对照:Windows 上四个非 MSVC 环境,只有 musl 崩。** + +| `x86_64-pc-windows-…` | 结果 | +|---|---| +| `gnu` | 编译通过 | +| `cygnus` | 编译通过 | +| `itanium` | 编译通过 | +| **`musl`** | **崩溃** | + +环境识别本身就已经断了。预定义宏: + +| 三元组 | 宏 | +|---|---| +| `…-windows-gnu` | `__GNUC__` `__MINGW32__` `_WIN32` | +| `…-windows-msvc` | `_MSC_VER` `_WIN32` | +| `…-windows-musl` | `__GNUC__` `_WIN32` ← **`__MINGW32__` 缺席** | + +⭐ **`windows-musl` 落进了一个没人认领的组合**:OS 是 Windows(所以输出 +COFF),env 不是 msvc(所以走 Itanium 那一支),但**不是 MinGW** —— +下游每一处按 env 分支的地方都没有它这一支。 + +**「clang 为什么有 windows-gnu」的答案由此对称地给出**:MinGW-w64 是一个 +成建制的目标 —— LLVM 里有专门的 ToolChain 类、有 `__MINGW32__` 预定义、 +有对应的 COFF/SEH/Itanium-ABI 配置路径。`cygnus` 与 `itanium` 同理, +各有分支。`musl` 在 Windows 侧从未被建模。 + +于是结论是一句更准的话:**mcpp 无法把 `windows-musl` 交给 clang, +不是因为拼写不存在,而是因为交过去会 ICE。** 而这恰恰证明了 §5.2 的分工 +是唯一可行的: + +``` +mcpp 的身份 x86_64-windows-musl ← 回答「C 库是谁」 +交给 clang 的 x86_64-w64-windows-gnu ← 回答「遵循哪套对象 ABI」 +``` + +两个字符串必须不同,**不是设计偏好,是上游约束**。 + +### 5.2.1b 编在 clang 里的是「支持」,不是「CRT」 + +两件同名不同物的东西,分不开就会得出「既然 clang 内置 MinGW,就该用它的 +C 库」这种结论: + +| | 在哪 | 编在 clang 二进制里? | +|---|---|---| +| MinGW **支持** —— `__MINGW32__`、`MinGW` ToolChain 类、COFF/SEH/Itanium 配置路径 | clang 内部的 C++ 代码 | **是** | +| MinGW **CRT** —— `crt2.o`、`libmsvcrt.a`、`libmingw32.a`、mingw 头 | 独立载荷 `xim:mingw-cross-gcc` | **否** | + +实测:llvm 载荷里一个 `crt2.o` / `libmingw32*` / `libmsvcrt*` 都没有; +它们全在 `xim-x-mingw-cross-gcc/16.1.0/x86_64-w64-mingw32/lib/`。 + +⭐ **于是 `x86_64-w64-windows-gnu` 这个三元组上,C 库那一格是空的、由外部填。** +填 MinGW CRT 是传统载荷路径,填 musl 是 openkal 路径 —— 两条路走同一套 ABI +约定,链进去的东西完全不同。 + +这就是 §5.2 那个分工的**机制层面**理由,而不只是命名问题:交给 clang 的 +三元组只说明 ABI,**说不出 C 库是谁**,而那一格恰恰是 mcpp 要管的。 + +### 5.2.1c 两个 C 库的实测差别是结构性的 + +同一份源码,同为 `--target x86_64-windows-gnu`,只换 C 库来源: + +| | MinGW CRT | musl / openkal | +|---|---|---| +| 体积 | 587,894 | **9,815,552**(16.7×) | +| 依赖的 DLL | `KERNEL32`、**`msvcrt.dll`** | `ntdll`、`KERNEL32`、`SHELL32`、`api-ms-win-core-synch` | + +⭐ **关键在 `msvcrt.dll` 那一格。** MinGW 的 C 库不是自足的 —— 它是一层 +薄封装,`printf`/`malloc` 真正的实现在目标机器自带的那份 DLL 里, +所以 587KB 只是「你的代码 + 胶水」。musl 是自足的:C 库、libc++、 +libc++abi、libunwind、compiler-rt 全部静态链入,只经 openkal 调 Win32 原语。 + +| 维度 | MinGW CRT | musl | +|---|---|---| +| 版本漂移 | 受制于目标机 `msvcrt.dll` 的行为 | 无 —— 就是构建时那份 | +| 体积 | 小 | 大 16.7 倍 | +| 同一份源码跨平台 | **不成立** —— Windows 上是 msvcrt 封装,与 Linux 的 C 库无共同实现 | **成立** —— 三平台同一份 musl 源码 | + +最后一行是这套东西存在的理由:构建期体系下 `c-abi musl` 在 +linux / windows / macos 三个目标上**是同一个包**,只有 `kernel-abi` 那一层换。 + +### 5.2.2 ⚠️ 加这一行会打断一个包,而失败形状是链接期缺符号 + +实测:加行之后 `--target x86_64-windows-musl` 构建失败于 + +``` +ld.lld: error: undefined symbol: __declspec(dllimport) MultiByteToWideChar +``` + +对比两种拼法的链接行: + +``` +-gnu -lkernel32 -lntdll -lshell32 -lsynchronization ← 四个 Win32 导入库 +-musl (一个都没有) +``` + +真因在 `openkal-windows@0.1.3` 的清单里: + +```toml +[target.'cfg(all(windows, env = "gnu"))'.build] +ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32"] +``` + +⭐ **包用 `env = "gnu"` 表达「对象 ABI 是 Itanium」**,而那个键在 Windows 上 +同时承载 C 库与对象 ABI 两件事(§5.1)。新目标一来条件就落空, +而落空的表现是**链接期缺符号**,不是「配置没生效」这种一眼可见的形状。 + +⚠️ **这条是加行连带暴露的,不是加行造成的。** 那个条件今天就在表达一件 +它表达不了的事;只是在 `-gnu` 是唯一非 MSVC 拼法时,写错和写对不可区分。 + +### 5.2.3 前置是一条链,不是一处 + +实测把整条链走了一遍,前置比预想的深一层: + +``` +mcpp 加 x86_64-windows-musl 这一行 + ↑ 需要 +openkal-windows ≥ 0.1.4 条件改为 not(env = "msvc") ✅ 已发布并进索引 + ↑ 需要 +openkal-musl 放开对它的钉死 `openkal-windows = "0.1.3"` ❌ 未做 +``` + +`openkal-musl@0.3.3` 的清单里写的是**精确版本**: + +```toml +openkal-windows = { version = "0.1.3", features = ["standalone"] } +``` + +所以索引里有了 0.1.4 也到不了消费者 —— lockfile 忠实地钉在 0.1.3, +构建仍然撞上同一组缺符号: + +``` +ld.lld: error: undefined symbol: __declspec(dllimport) MultiByteToWideChar +``` + +⚠️ **这不是新增那一行造成的**,而是那一行让一条既有的版本钉死浮出水面。 +今天 `-gnu` 是唯一非 MSVC 拼法,钉在 0.1.3 与钉在 0.1.4 行为相同, +所以钉死是隐形的。 + +⭐ **但它不阻塞 mcpp 侧的改动。** 新增一行目标是词表扩充,其正确性由 +「名字是否映到正确的 LLVM 三元组、身份是否唯一」判定,而不由某个生态包 +今天能不能构建判定。生态链条另行推进:`openkal-musl` 放宽依赖 → 发版 → +进索引,之后 `x86_64-windows-musl` 才端到端可用。 + +在那之前,该目标的档是 `preview` 而非 `verified` —— 而 `verified` 的定义 +正是「构建**并运行**过」,所以档位本身就诚实地记录了这个状态。 + +**这使 §5.1(cfg 轴)从「应做」升为 §5.2 的前置**:必须先让包能表达 +「对象 ABI 是 Itanium」而不借用 `env = "gnu"`,否则加行就是把一个 +生态包弄坏。可选形状(未实测,需要设计): + +- `cfg(all(windows, not(env = "msvc")))` —— 包侧一行改完,零引擎改动 +- 引入 `cfg(abi = "itanium")` —— 更准,但要新增一条 cfg 轴 + +⚠️ 第一条今天就能用且不需要引擎改动,应先验证它是否覆盖全部三种工具链 +(注释里说这个包「为三种工具链而写」)。 + +### 5.3 `host_can_serve()` 回答了两个问题 + +该谓词的每一条分支都在问「**载荷**是否覆盖这个(宿主,目标)对」。 +在构建期体系下这个问题不成立 —— C 库来自图,编译器天生交叉。 + +后果实测于 `mcpp toolchain list`: + +``` + TARGET NOTE TOOLCHAIN STATUS + x86_64-windows-gnu PE, static, cross gcc 16.1.0 installed +``` + +一个 openkal 工程用的是 `llvm@22.1.8`,与这一行显示的 `gcc 16.1.0` 无关。 +**列表回答的问题已经不是使用者要问的问题。** + +⚠️ 不建议现在拆分该谓词:它有 6 个调用点,而拆分需要先回答 +「一个尚未解析依赖图的命令(`toolchain list`)如何知道自己在哪种体系里」。 +先把 §5.2 做掉,那一行会让 `x86_64-windows-musl` 出现在列表里且 +`TOOLCHAIN` 列显示 `—`,本身就是一次诚实的表达。 + +### 5.4 `tier` 的含义在两种体系下不同 + +`verified` 在裸机行的注释里定义为「**镜像被构建并且被运行过**」。 +而 `x86_64-windows-gnu` 标 `verified` 指的是预构建路径被验证过 —— +构建期路径是另一回事,今天由 openkal 的 3×3 矩阵单独覆盖。 + +⚠️ 同一个词标注两种不同的验证,而表里没有任何东西区分它们。 +本文不提出改法(加一列或拆两张表都有代价),只记录这个歧义。 + +--- + +## 6. 落地顺序与依赖 + +``` +5.2(windows-musl 一行) ← 先做。它连带修正 5.1 的一半语义 + │ +5.1(check_request 判据) ← 依赖 5.2 的语义,不依赖它的代码 + │ +5.3(host_can_serve 拆分) ← 先做完 5.2 再评估;可能不必做 + │ +5.4(tier 歧义) ← 仅记录,不改 +``` + +## 7. 本文刻意没有断言的事 + +- **没有断言 `x86_64-windows-musl` 该是哪个档。** `verified` 的定义是 + 「构建**并运行**过」,而在 Linux 宿主上运行 PE 需要 wine。 + ⚠️ openkal 的 CI 已有 wine 步骤,所以这是可测的 —— 但要先测再标, + 不能先标再说。 +- **没有断言 5.3 拆分后 `toolchain list` 该显示什么。** 完整方案要求它 + 解析依赖图,而那会让一条只想看看有哪些目标的命令变慢。 +- **没有断言 macOS 上是否也存在同一个命名缺陷。** `aarch64-macos` + 不带 env 段,所以 §5.2 的形状在那里不出现;但 c-abi 实测同样是 + `musl (graph)`,而目标名里没有任何地方说明这一点 —— 这与 Windows + 是同一个问题的另一种表现,本文未展开。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 626d5fcb..997c08a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,71 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 -## [未发布] +## [2026.8.24.6] — 2026-08-25 + +### 新增 + +- **⭐ `x86_64-windows-musl`:Windows 上的 musl 有名字了。** + + 同一个 `--target x86_64-windows-gnu`,两种体系下的 C 库完全不同 —— + 而 mcpp 用同一个名字称呼它们。实测同一份源码: + + | | MinGW CRT | musl / openkal | + |---|---|---| + | 体积 | 587,894 | **9,815,552**(16.7×) | + | 依赖 DLL | `KERNEL32`、**`msvcrt.dll`** | `ntdll`、`KERNEL32`、`SHELL32` | + + ⭐ MinGW 的 C 库不是自足的:`printf`/`malloc` 的实现在目标机自带的 + `msvcrt.dll` 里。musl 是自足的,整个 C 库静态链入,只经 openkal 调 + Win32 原语。**两者是不同的东西,不该共用一个名字。** + + ⚠️ **这个名字 LLVM 拼不出来。** 实测 llvm 22.1.8: + + ``` + clang++ --target=x86_64-pc-windows-musl -c t.cpp + #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) + ``` + + 崩在 COFF 写出器,不是诊断而是 ICE。Windows 上四个非 MSVC 环境 + `gnu`/`cygnus`/`itanium`/`musl`,前三个能编,只有 `musl` 死; + 预定义宏显示它从未被建模(无 `__MINGW32__`)。 + + ⭐ **于是 mcpp 的名字与交给 clang 的三元组必须是两个字符串**,而它们 + 本来就是 —— 构建报告里那个箭头两侧就是: + + ``` + Target x86_64-windows-musl → x86_64-w64-windows-gnu + ^ 回答「C 库是谁」 ^ 回答「遵循哪套对象 ABI」 + ``` + + ⚠️ 该行 pin 为 `llvm@22.1.8`,不是偏好:全局默认为 gcc 时,空 pin 会让 + 它落到 musl-gcc 载荷并报「没有 C++ 前端」—— 一条关于缺前端的消息, + 而真正的问题是只有 clang 能发这个目标。裸机各行同理。 + + ⚠️ 档为 `preview`:`verified` 的定义是「构建**并运行**过」,而端到端 + 可用还差一环 —— `openkal-musl@0.3.3` 精确钉死 `openkal-windows = "0.1.3"`, + 索引里已有的 0.1.4 到不了消费者。生态链条另行推进。 ### 修复 +- **构建期体系下 `x86_64-windows-gnu` 的名实不符,此前静默。** + + ``` + x86_64-linux-gnu 名字说 gnu,事实是 musl → ⚠️ 警告 + x86_64-windows-gnu 名字说 gnu,事实是 musl → ❌ 静默 + ``` + + `check_request()` 豁免了非「C 库轴」的平台,理由是「Windows 上 `gnu` + 命名对象 ABI 而非 C 库」—— **只对了一半**:那一段捆着对象 ABI(被兑现) + 与 MinGW 的 C 运行时(被图替换),第二件正是该函数存在的意义。 + + 判据改为「轴 ∈ {CLibrary, ObjectAbi}」。⚠️ 裸机的 `elf` 继续豁免 —— + 它在任何平台上都不命名 C 库,对它说「请求了 `elf` C ABI」是胡话。 + 消息在对象 ABI 轴上额外说明 ABI 那一半**未受影响**。 + + 误伤由既有的 `fromGraph()` 挡住:实测传统预构建路径下 + `c-abi gnu (payload)` 不进警告。 + - **暂存跨主版本副本时,与源码相邻的私有头没有跟着走。** 当同一个包的两个主版本同时出现在一张图里,解析器会把其中一份改名暂存到 @@ -27,6 +88,12 @@ 现在,凡是包含了被暂存源码的目录,其中未被暂存的文件一并原样带过去。没有被 暂存源码的目录不会被访问,所以代价与暂存量成正比。 +### 文档 + +- docs/16 新增「三套词表,以及它们为何不同」(中英双份):GCC / LLVM / + mcpp 三套的形状、`x86_64-w64-mingw32` 为何把实现放在 OS 位、 + clang 如何把它重拼为 `windows-gnu`,以及 mcpp 为何要保留自己那一套。 + ## [2026.8.24.5] — 2026-08-25 ### 改进 @@ -2656,7 +2723,6 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 独立提供,不读取 cfg)。 - ### 修复 - **Linux llvm 工具链链接失败 `cannot open Scrt1.o/crti.o/crtn.o`(#195)**:clang-with-cfg diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index 097a279e..ad72f159 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -137,6 +137,131 @@ convention the **objects follow**, which several layers must agree on. Reading it as `c++-abi libc++` is a second wrong answer, since libstdc++ sits on the same ABI. +## Three Vocabularies, And Why They Differ + +A triple is written by three parties that do not share a convention, and mcpp +translates between them. Knowing which one a string belongs to removes most of +the confusion the third segment causes. + +### The shape + +``` + - - - +``` + +Every field may be omitted and the compiler fills what is missing. `vendor` is +historical and carries almost nothing today — it is filled as `unknown` unless +the target has a reason to say otherwise: + +``` +x86_64-linux-gnu → x86_64-unknown-linux-gnu +aarch64-macos → aarch64-unknown-macos +``` + +A three-field spelling is a four-field one with a field elided, and which field +was elided follows from what parses. + +### GCC and clang do not select targets the same way + +| | GCC | clang | +|---|---|---| +| target | fixed when the compiler was **built** | chosen at **run time** | +| how to ask | `-dumpmachine` | `-dumpmachine`, or `--target=` | +| to cross-compile | use a **different executable** | pass a flag | + +Measured: + +``` +g++ → x86_64-linux-gnu (emits nothing else) +x86_64-w64-mingw32-g++ → x86_64-w64-mingw32 (emits nothing else) +clang++ → x86_64-unknown-linux-gnu, and --target= changes it +``` + +⭐ This is why the prebuilt system passes no `--target`: the payload's compiler +is named after the one target it can emit, and choosing a target means choosing +a payload. It is also why the build-time system needs only one compiler. + +### MinGW names itself in the GCC convention + +MinGW's own triple is `x86_64-w64-mingw32`: + +| Field | Value | Why | +|---|---|---| +| arch | `x86_64` | | +| vendor | `w64` | the project is `mingw-w64`, distinguishing it from the stalled original `mingw32` | +| os | **`mingw32`** | ⭐ MinGW puts *itself* in the OS field | +| env | (absent) | three fields is the whole name | + +The convention descends from autoconf's `config.guess`, where the OS field names +the target's runtime environment — and in the GNU toolchain's view MinGW *is* a +distinct environment, with its own headers, its own C runtime and its own +`configure` branches. The `32` is historical; `w64` is what says the name refers +to the 64-bit-capable project. + +LLVM does not accept that view. It holds that the OS is `windows` and that MinGW +is one ABI environment on top of it, so it re-spells the name — measured: + +``` +x86_64-w64-mingw32 → x86_64-w64-windows-gnu +x86_64-pc-mingw32 → x86_64-pc-windows-gnu +``` + +``` +GCC x86_64 - w64 - mingw32 - (none) + ^vendor ^os +LLVM x86_64 - unknown - windows - gnu + ^vendor ^os ^env +``` + +⭐ **`mingw32` is split out of the OS field into `windows` plus `gnu`.** The +value `gnu` exists because LLVM needed a name for the half that was left over. +Its meaning is "the MinGW/Itanium ABI lineage" and it has never meant "the C +library is glibc" on Windows — the same word carries different duties under +different operating systems, and that is LLVM's vocabulary rather than mcpp's +invention. + +### What mcpp keeps + +| Vocabulary | Example | Read by | +|---|---|---| +| GCC / autoconf | `x86_64-w64-mingw32` | the prebuilt payload's compiler, by its file name | +| LLVM | `x86_64-w64-windows-gnu` | `clang --target=` | +| **mcpp** | `x86_64-windows-gnu` | the target table, the output directory, `cfg()`, a packed ABI tag | + +mcpp's own form must map onto both. The arrow in the build report is that +mapping, from the third vocabulary to the second: + +``` +Target x86_64-windows-gnu → x86_64-w64-windows-gnu + ^ mcpp ^ LLVM +``` + +⚠️ Keeping the third vocabulary separate is what lets mcpp name something LLVM +cannot. Measured on llvm 22.1.8, `windows` with a `musl` environment is accepted +by the triple parser and crashes the compiler: + +``` +clang++ --target=x86_64-pc-windows-musl -c t.cpp + #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) +``` + +The crash is in the COFF writer: clang has decided the output is COFF and has no +Windows-musl path to configure the streamer with. Of the four non-MSVC Windows +environments it knows — `gnu`, `cygnus`, `itanium`, `musl` — the first three +compile and only `musl` dies, and the predefined macros show why the environment +was never modelled: + +| Triple | Macros | +|---|---| +| `…-windows-gnu` | `__GNUC__` `__MINGW32__` `_WIN32` | +| `…-windows-msvc` | `_MSC_VER` `_WIN32` | +| `…-windows-musl` | `__GNUC__` `_WIN32` — **no `__MINGW32__`** | + +So a musl C library on Windows cannot be named to clang, and must still be named +by mcpp, because mcpp's name answers a different question: **which C library**, +where LLVM's answers **which object ABI**. Both are needed and they are not the +same string. + ## Custom Targets A triple outside mcpp's table needs an explicit section, which is also how a diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md index 4daab979..35faceeb 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -118,6 +118,123 @@ Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium 横切事项。把它读成 `c++-abi libc++` 是第二个错误答案,因为 libstdc++ 坐在同一套 ABI 上。 +## 三套词表,以及它们为何不同 + +一个三元组由三方书写,而三方并不共用一套约定;mcpp 在它们之间翻译。 +知道手上这个字符串属于哪一套,第三段带来的困惑就消掉大半。 + +### 形状 + +``` + - - - +``` + +每一段都可省,省掉的由编译器补。`vendor` 是历史遗留,今天几乎不承重 —— +除非目标有理由说别的,否则一律填 `unknown`: + +``` +x86_64-linux-gnu → x86_64-unknown-linux-gnu +aarch64-macos → aarch64-unknown-macos +``` + +三段写法是四段省掉一段,省的是哪一段由「能否解析」决定。 + +### GCC 与 clang 选目标的方式根本不同 + +| | GCC | clang | +|---|---|---| +| 目标 | **构建编译器时**就定死 | **运行时**选 | +| 怎么问 | `-dumpmachine` | `-dumpmachine`,或 `--target=` | +| 交叉编译 | 换**另一个可执行文件** | 传一个 flag | + +实测: + +``` +g++ → x86_64-linux-gnu (只能发这个) +x86_64-w64-mingw32-g++ → x86_64-w64-mingw32 (只能发这个) +clang++ → x86_64-unknown-linux-gnu,而 --target= 可改 +``` + +⭐ 这就是传统预构建体系**不传 `--target`** 的原因:那份载荷的编译器以它唯一 +能发的目标命名,选目标等于选载荷。也是构建期体系只需要一个编译器的原因。 + +### MinGW 按 GCC 的约定给自己命名 + +MinGW 自己的三元组是 `x86_64-w64-mingw32`: + +| 段 | 值 | 为什么 | +|---|---|---| +| arch | `x86_64` | | +| vendor | `w64` | 项目名 `mingw-w64`,用以区别于已停滞的原 `mingw32` 项目 | +| os | **`mingw32`** | ⭐ MinGW 把**自己**放在 OS 位 | +| env | (无) | 三段就是全名 | + +这套约定源自 autoconf 的 `config.guess`,那里 OS 段命名的是目标的运行环境 —— +而在 GNU 工具链的世界观里 MinGW **就是**一个独立环境:有自己的头文件、 +自己的 C 运行时、自己的 `configure` 分支。名字里的 `32` 是历史遗留, +`w64` 才表示这是那个支持 64 位的项目。 + +LLVM 不接受这个世界观。它认为 OS 是 `windows`,MinGW 只是其上的一种 ABI +环境,于是把这个名字重拼 —— 实测: + +``` +x86_64-w64-mingw32 → x86_64-w64-windows-gnu +x86_64-pc-mingw32 → x86_64-pc-windows-gnu +``` + +``` +GCC x86_64 - w64 - mingw32 - (无) + ^vendor ^os +LLVM x86_64 - unknown - windows - gnu + ^vendor ^os ^env +``` + +⭐ **`mingw32` 从 OS 位被拆成 `windows` 加 `gnu`。** `gnu` 这个取值之所以 +存在,正是因为 LLVM 需要给拆剩下的那一半起个名字。它的含义是 +「MinGW/Itanium 这一支 ABI」,在 Windows 上从来不是「C 库是 glibc」—— +同一个词在不同操作系统下承担不同职责,这是 LLVM 词表的既有事实, +不是 mcpp 的发明。 + +### mcpp 保留的那一套 + +| 词表 | 例 | 谁读 | +|---|---|---| +| GCC / autoconf | `x86_64-w64-mingw32` | 预构建载荷的编译器,以文件名的形式 | +| LLVM | `x86_64-w64-windows-gnu` | `clang --target=` | +| **mcpp** | `x86_64-windows-gnu` | 目标表、输出目录、`cfg()`、打包的 ABI tag | + +mcpp 自己那套必须能映到前两套。构建报告里那个箭头就是这个映射, +从第三套到第二套: + +``` +Target x86_64-windows-gnu → x86_64-w64-windows-gnu + ^ mcpp ^ LLVM +``` + +⚠️ 把第三套词表独立出来,正是 mcpp 能命名 LLVM 命名不了的东西的原因。 +实测 llvm 22.1.8:`windows` 配 `musl` 环境能被三元组解析器接受, +而编译器会崩: + +``` +clang++ --target=x86_64-pc-windows-musl -c t.cpp + #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) +``` + +崩点在 **COFF 写出器**:clang 已认定输出是 COFF,却没有一条 Windows-musl +的路径去把 streamer 需要的状态配齐。它认识的四个非 MSVC Windows 环境 —— +`gnu`、`cygnus`、`itanium`、`musl` —— 前三个都能编,只有 `musl` 死; +预定义宏说明了这个环境从未被建模: + +| 三元组 | 宏 | +|---|---| +| `…-windows-gnu` | `__GNUC__` `__MINGW32__` `_WIN32` | +| `…-windows-msvc` | `_MSC_VER` `_WIN32` | +| `…-windows-musl` | `__GNUC__` `_WIN32` —— **无 `__MINGW32__`** | + +所以 Windows 上的 musl C 库**无法**向 clang 命名,而 mcpp 仍然必须为它命名, +因为 mcpp 的名字回答的是另一个问题:**C 库是谁**,而 LLVM 的名字回答 +**遵循哪套对象 ABI**。两者都需要,且不是同一个字符串。 + ## 自定义目标 不在 mcpp 表内的三元组需要一个显式段落,而这也是一块板子声明 diff --git a/mcpp.toml b/mcpp.toml index bc91cdbd..8e95ee3d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.8.24.5" +version = "2026.8.24.6" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/targetside/model.cppm b/src/targetside/model.cppm index 8d030977..67ced1a6 100644 --- a/src/targetside/model.cppm +++ b/src/targetside/model.cppm @@ -631,7 +631,27 @@ check_requirements(const TargetSide& ts, std::span reqs) { // first. Telling someone their target name is wrong is only useful once there // is a right one to give them. inline std::optional check_request(const TargetSide& ts) { - if (ts.envAxis != EnvAxis::CLibrary) return std::nullopt; + // ⚠️ TWO AXES REACH HERE, AND EXEMPTING THE SECOND WAS THE DEFECT. + // + // `CLibrary` is the obvious one: on Linux the segment names the C library + // outright. `ObjectAbi` was exempted on the grounds that `gnu` on Windows + // names the Itanium C++ ABI rather than a C library — true, and incomplete. + // The segment there bundles TWO things: the object ABI, which is honoured, + // and MinGW's C runtime, which a graph-supplied C library replaces. The + // second half is a name/fact disagreement of exactly the shape this + // function exists to report, and it went unreported. + // + // Measured, one project, two spellings, same graph: + // + // --target x86_64-linux-gnu c-abi musl (graph) warned + // --target x86_64-windows-gnu c-abi musl (graph) silent ← the defect + // + // ⚠️ `ObjectFormat` STAYS EXEMPT, and not for symmetry. `elf` never names a + // C library on any platform, so "the target name asks for the `elf` C ABI" + // would be nonsense rather than merely noisy. That axis is glossed in the + // report instead. + if (ts.envAxis != EnvAxis::CLibrary && ts.envAxis != EnvAxis::ObjectAbi) + return std::nullopt; if (ts.requestedCAbi.empty()) return std::nullopt; if (ts.cAbi.absent()) return std::nullopt; if (ts.cAbi.interfaceName == ts.requestedCAbi) return std::nullopt; @@ -640,15 +660,24 @@ inline std::optional check_request(const TargetSide& ts) { // can disagree with it. if (!ts.cAbi.fromGraph()) return std::nullopt; + // On the object-ABI axis the name is half honoured, and saying only that it + // is "inaccurate" would suggest the ABI changed too. It did not. + std::string aside; + if (ts.envAxis == EnvAxis::ObjectAbi) + aside = std::format( + "\n The object ABI `{}` selects is unaffected; what it does " + "not select here is the\n C library.", + ts.requestedCAbi); + return std::format( "the target name asks for the `{}` C ABI and the dependency graph " "supplies `{}`.\n" " The graph decides, so the build below uses `{}` — the name is " "what is inaccurate,\n" - " not the artifact. Drop the segment to say what is actually " - "meant:\n" + " not the artifact.{}\n" + " Drop the segment to say what is actually meant:\n" " --target {}", - ts.requestedCAbi, ts.cAbi.interfaceName, ts.cAbi.interfaceName, + ts.requestedCAbi, ts.cAbi.interfaceName, ts.cAbi.interfaceName, aside, ts.requestFreeTarget.empty() ? std::string("-") : ts.requestFreeTarget); } @@ -787,7 +816,11 @@ inline std::string format_report(const TargetSide& ts, std::string_view targetNa // present, it does not name a C library here, and the C library came from // somewhere the segment did not choose. A payload C library IS selected by // the triple, so `gnu → ucrt` follows visibly and needs no gloss. - if (ts.envAxis != EnvAxis::Unknown && ts.envAxis != EnvAxis::CLibrary + // ⚠️ `ObjectFormat` ONLY. The object-ABI axis used to be glossed here and is + // now WARNED about instead — see `check_request`. Leaving both in place + // would say the same thing twice, once as an aside and once as a warning, + // which reads as two different findings. + if (ts.envAxis == EnvAxis::ObjectFormat && !ts.requestedCAbi.empty() && !ts.cAbi.absent() && ts.cAbi.fromGraph() && ts.cAbi.interfaceName != ts.requestedCAbi) { diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm index 7e9aff10..a3c91110 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -210,6 +210,67 @@ inline constexpr TargetInfo kKnownTargets[] = { { "x86_64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, { "aarch64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, { "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", "", true }, + // ⚠️ musl ON WINDOWS. IT EXISTS, AND UNTIL THIS ROW mcpp HAD NO NAME FOR IT. + // + // LLVM's triple vocabulary offers `gnu` and `msvc` for Windows and both + // name an ABI, so a reader concludes there is no third possibility and + // calls a musl-based Windows build `-gnu`. The artefact disagrees. Measured + // on one built over `openkal-musl`: + // + // imports ntdll, KERNEL32, SHELL32 — no msvcrt, no ucrtbase + // `_Z…` symbols 4507 `?…` symbols 0 + // + // No MinGW C runtime is linked. That is musl on Windows, and calling it + // `gnu` put the one thing the C library is not into its identity, its + // output directory, its `cfg(env = …)` and its packed ABI tag. + // + // ⭐ THE FIX IS A NAME, NOT A MECHANISM, AND THE REASON THE MISTAKE HELD SO + // LONG IS WORTH RECORDING. "LLVM cannot spell x86_64-windows-musl" is true + // and is about the string handed to CLANG. mcpp's canonical form is a + // different string — the build report prints both, either side of an arrow: + // + // Target x86_64-windows-gnu → x86_64-w64-windows-gnu + // ^ mcpp's identity ^ what clang is given + // + // Letting the compiler's vocabulary bound mcpp's own merged two axes into + // one. `llvm_triple()` already sends every non-MSVC Windows target to + // `…-w64-windows-gnu`, and that spelling is correct there and stays: it + // selects the Itanium C++ ABI, which is the ABI this C library was compiled + // for. mcpp's name answers a different question — which C library — and now + // it can. + // + // mcpp target → clang c-abi + // x86_64-linux-musl → x86_64-unknown-linux-musl musl + // x86_64-windows-gnu → x86_64-w64-windows-gnu MinGW CRT + // x86_64-windows-musl → x86_64-w64-windows-gnu musl + // + // The last two rows differ in the first column and agree in the second, + // which is the whole point. + // + // ⚠️ THE PIN IS `llvm`, AND IT IS NOT A PREFERENCE. The column names the + // payload that supplies this target's C library everywhere else in this + // table; here nothing supplies it, and what the column has to prevent is + // the OPPOSITE — a global default of gcc being carried onto a target no gcc + // can emit. Measured with `mcpp toolchain default gcc@16.1.0`: + // + // error: toolchain payload 'xim:musl-gcc@16.1.0' has no known C++ + // frontend in …/xim-x-musl-gcc/16.1.0/bin + // + // — a message about a missing frontend, for a target whose real problem is + // that only clang emits it at all. The bare-metal rows carry `llvm` for the + // same reason and say so in their own note. + // + // The C library still comes from the dependency graph. `host_can_serve` + // says no for this row on a non-Windows host — correctly, for the prebuilt + // system — and that refusal is diagnosed early and RELEASED once the graph + // is known (see the long note at prepare.cppm's `unservedTargetDiagnosis`), + // so a project whose C library comes from a dependency is not turned away. + // + // ⚠️ TIER IS `preview`, NOT `verified`. `verified` in this table means an + // artefact was built AND RUN. Running a PE on a Linux host needs wine, and + // openkal's CI has that step — so this is measurable, and the tier moves + // when it has been measured rather than when it seems likely. + { "x86_64-windows-musl", "preview", "PE", "llvm@22.1.8","", true }, { "x86_64-windows-msvc", "verified", "PE", "", "", false }, { "aarch64-macos", "verified", "", "", "", false }, { "riscv64-linux-musl", "planned", "", "", "", true }, diff --git a/src/version.cppm b/src/version.cppm index b2881efb..c50c6245 100644 --- a/src/version.cppm +++ b/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.8.24.5"; +inline constexpr std::string_view MCPP_VERSION = "2026.8.24.6"; } // namespace mcpp diff --git a/tests/e2e/284_env_segment_is_optional_everywhere.sh b/tests/e2e/284_env_segment_is_optional_everywhere.sh index 1469ec7c..4c38712c 100755 --- a/tests/e2e/284_env_segment_is_optional_everywhere.sh +++ b/tests/e2e/284_env_segment_is_optional_everywhere.sh @@ -96,3 +96,72 @@ check_pair riscv64-none riscv64-none-elf || rc=1 [ "$rc" = 0 ] || exit 1 echo "OK: the env segment is optional on every platform, and declining it changes no identity" + +# ── The Windows C-library axis has a name of its own ───────────────────────── +# +# ⚠️ `x86_64-windows-musl` is a target mcpp names and LLVM cannot. Measured on +# llvm 22.1.8, handing `windows` with a `musl` environment to clang is not a +# diagnostic but an ICE inside the COFF writer: +# +# #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) +# +# so mcpp's canonical form and the triple passed to the compiler must be +# different strings — which they already are, either side of the arrow in the +# build report. +# +# ⭐ THE ASSERTION IS THAT IT IS A KNOWN TARGET WITH A CORRECT DIAGNOSIS, NOT +# THAT IT BUILDS. This project names no dependency, so nothing supplies the +# target side and the build cannot succeed — correctly. What is under test is +# that mcpp recognises the name and says the true thing about it, rather than +# `unknown target`, which is what it said before the row existed. +out="$("$MCPP" build --target x86_64-windows-musl 2>&1 || true)" + +case "$out" in + *"unknown target"*) + echo "FAIL: x86_64-windows-musl is not in the target table" + exit 1 ;; +esac + +case "$out" in + *"nothing in the dependency graph"*) + echo " ok x86_64-windows-musl is known, and the refusal names the real reason" ;; + *) + echo "FAIL: the refusal did not name the missing supplier" + printf '%s\n' "$out" | grep -m2 -iE 'error|Target' + exit 1 ;; +esac + +# ⭐ AND THE PIN IS ASSERTED, BECAUSE THE PIN IS WHAT THE ROW IS FOR. +# +# The row names `llvm` not as a preference but because a global default of gcc +# would otherwise be carried onto a target no gcc can emit, and the error would +# then be about a missing C++ frontend in a musl-gcc payload — a true sentence +# about the wrong subject. So the toolchain line has to show the pin winning, +# and it has to show mcpp's own name being what was asked for: +# +# Resolved llvm@22.1.8 → x86_64-windows-musl → …/xim-x-llvm/22.1.8/bin/clang++ +# target default for x86_64-windows-musl, replacing your gcc@16.1.0 +# +# ⚠️ THIS WAS WRITTEN WITH AN `*) : ;;` FALLBACK, WHICH MADE IT UNFAILABLE. +# A branch that accepts anything is not a check, and this file's whole subject +# is a name that used to be accepted by nothing. The fallback is gone; if the +# report's phrasing changes, this is meant to go red and be updated. +case "$out" in + *"→ x86_64-windows-musl →"*) + echo " ok the toolchain line carries mcpp's own name" ;; + *) + echo "FAIL: the toolchain line did not carry mcpp's own name for the target" + printf '%s\n' "$out" | grep -m2 -i 'resolv' + exit 1 ;; +esac + +case "$out" in + *"llvm@22.1.8"*) + echo " ok the row's pin decided the toolchain" ;; + *) + echo "FAIL: the target table's pin did not decide the toolchain" + printf '%s\n' "$out" | grep -m2 -i 'resolv' + exit 1 ;; +esac + +echo "OK: mcpp names a C library LLVM cannot spell, and diagnoses it honestly" diff --git a/tests/unit/test_targetside.cpp b/tests/unit/test_targetside.cpp index 35ecbbfc..007aa08d 100644 --- a/tests/unit/test_targetside.cpp +++ b/tests/unit/test_targetside.cpp @@ -449,7 +449,15 @@ TEST(TargetSideRequest, APrebuiltCLibraryIsWhatTheRequestSelected) { // than one C library. Reporting such a build as "asking for the `gnu` C ABI" // describes an axis the name never addressed, and the correction it suggested // named a target that does not exist. -TEST(TargetSideRequest, TheSegmentIsOnlyACLibraryWhereItNamesOne) { +// ⚠️ AND THE OBJECT-ABI AXIS REPORTS TOO, WHICH IS THE REVERSAL. +// +// It was exempted on the grounds that `gnu` on Windows names the Itanium C++ +// ABI rather than a C library. True, and incomplete: the segment bundles the +// object ABI, which IS honoured, with MinGW's C runtime, which a graph-supplied +// C library replaces. The second half is a name/fact disagreement of exactly +// the shape this function reports, and it went unreported — while the identical +// shape on Linux warned. +TEST(TargetSideRequest, TheObjectAbiAxisReportsTheCLibraryHalfToo) { auto in = payload_linux(); in.targetOs = "windows"; in.compilerFamily = "llvm"; @@ -457,6 +465,27 @@ TEST(TargetSideRequest, TheSegmentIsOnlyACLibraryWhereItNamesOne) { in.requestedCAbi = "gnu"; in.requestFreeTarget = "x86_64-windows"; in.envAxis = ts::EnvAxis::ObjectAbi; + + auto why = ts::check_request(ts::resolve(in)); + ASSERT_NE(why, std::nullopt); + EXPECT_NE(why->find("musl"), std::string::npos) << *why; + // ⭐ And it says the ABI half was honoured, so the reader does not conclude + // the object ABI changed as well. + EXPECT_NE(why->find("object ABI"), std::string::npos) << *why; + EXPECT_NE(why->find("--target x86_64-windows"), std::string::npos) << *why; +} + +// The object-FORMAT axis stays exempt, and not for symmetry: `elf` names no C +// library on any platform, so "asks for the `elf` C ABI" would be nonsense +// rather than merely noisy. +TEST(TargetSideRequest, TheObjectFormatAxisStaysExempt) { + auto in = payload_linux(); + in.targetOs = "none"; + in.compilerFamily = "llvm"; + in.cAbi = provider("openkal-musl", "0.3.3", "musl"); + in.requestedCAbi = "elf"; + in.requestFreeTarget = "riscv64-none"; + in.envAxis = ts::EnvAxis::ObjectFormat; EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); } @@ -466,7 +495,10 @@ TEST(TargetSideRequest, TheSegmentIsOnlyACLibraryWhereItNamesOne) { // reader sees `x86_64-windows-gnu` above a line reading `c-abi musl`, finds no // row called `gnu`, and maps it to the nearest thing that looks like a C // library name. Measured twice, by the same reader, on two different days. -TEST(TargetSideReport, AnEnvSegmentThatIsNotACLibraryIsGlossed) { +// ⚠️ THE GLOSS IS NOW THE OBJECT-FORMAT AXIS ALONE. The object-ABI axis warns +// instead (see `TheObjectAbiAxisReportsTheCLibraryHalfToo`), and leaving both +// in place would state one finding twice — once as an aside, once as a warning. +TEST(TargetSideReport, TheObjectAbiAxisIsNoLongerGlossed) { auto in = payload_linux(); in.targetOs = "windows"; in.compilerFamily = "llvm"; @@ -476,8 +508,7 @@ TEST(TargetSideReport, AnEnvSegmentThatIsNotACLibraryIsGlossed) { in.envAxis = ts::EnvAxis::ObjectAbi; auto r = ts::format_report(ts::resolve(in), "x86_64-windows-gnu"); - EXPECT_NE(r.find("gnu selects the Itanium C++ ABI, not a C library"), - std::string::npos) << r; + EXPECT_EQ(r.find("not a C library"), std::string::npos) << r; } // A bare-metal target's segment names the object FORMAT, and the same gloss @@ -496,48 +527,6 @@ TEST(TargetSideReport, OnBareMetalTheSegmentNamesTheObjectFormat) { std::string::npos) << r; } -// ⚠️ THE NOUN COMES FROM THE VALUE, NOT ONLY FROM THE AXIS. -// -// `gnu` and `msvc` sit on the same axis and select OPPOSITE ABIs. A noun fixed -// per axis would print "the Itanium C++ ABI" for an MSVC build — a statement -// that is not merely vague but false. -TEST(TargetSideReport, TheOppositeValueOnTheSameAxisGetsTheOppositeName) { - auto in = payload_linux(); - in.targetOs = "windows"; - in.compilerFamily = "llvm"; - in.cAbi = provider("openkal-musl", "0.3.3", "musl"); - in.requestedCAbi = "msvc"; - in.requestFreeTarget = "x86_64-windows"; - in.envAxis = ts::EnvAxis::ObjectAbi; - - auto r = ts::format_report(ts::resolve(in), "x86_64-windows-msvc"); - EXPECT_NE(r.find("msvc selects the MSVC C++ ABI"), std::string::npos) << r; - EXPECT_EQ(r.find("Itanium"), std::string::npos) << r; -} - -// ⭐ AND THE NAME IT PRINTS MUST NOT BE ANY ROW'S VALUE. -// -// Naming the ABI rather than the axis is what stops a reader mapping `gnu` to -// `c++-abi libc++` — a second wrong answer, since libstdc++ sits on the same -// ABI. If the gloss ever printed a string that also appears as a layer's -// implementation name, the confusion would return in a new place. -TEST(TargetSideReport, TheGlossNamesNothingThatAppearsAsALayerValue) { - auto in = payload_linux(); - in.targetOs = "windows"; - in.compilerFamily = "llvm"; - in.cAbi = provider("openkal-musl", "0.3.3", "musl"); - in.cxxAbi = provider("openkal-llvm-runtime", "0.1.2", "libc++"); - in.requestedCAbi = "gnu"; - in.requestFreeTarget = "x86_64-windows"; - in.envAxis = ts::EnvAxis::ObjectAbi; - - auto r = ts::format_report(ts::resolve(in), "x86_64-windows-gnu"); - auto head = r.substr(0, r.find('\n')); - EXPECT_NE(head.find("Itanium"), std::string::npos) << head; - EXPECT_EQ(head.find("libc++"), std::string::npos) << head; - EXPECT_EQ(head.find("musl"), std::string::npos) << head; -} - // ⚠️ AND IT DOES NOT FIRE WHEN THE C LIBRARY CAME FROM A PAYLOAD. // // A payload C library IS what the triple selected — the triple is how it was diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 12d41b73..a518b35f 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -400,3 +400,35 @@ TEST(Triple, MacosCarriesNoSegmentToDecline) { EXPECT_TRUE(t->env.empty()); EXPECT_FALSE(t->envExplicit); } + +// ⚠️ NO TWO ROWS MAY SHARE A CANONICAL NAME, AND THIS WAS NOT A HYPOTHETICAL. +// +// Adding `x86_64-windows-musl` and later correcting its `pin` column produced +// TWO rows with that name — the edit inserted a corrected row without removing +// the original. `find_known_target` returns the first match, so every behaviour +// was correct and nothing failed; what the table carried was a second row of +// dead data whose columns disagreed with the live one. +// +// ⭐ Caught by reading the diff, which is the wrong mechanism: a duplicate is a +// property of the table and a machine can see it. The cost of the check is four +// lines. +TEST(Triple, TheTargetTableHasNoDuplicateNames) { + std::vector seen; + for (const auto& info : known_targets()) { + for (auto s : seen) + EXPECT_NE(s, info.canonical) + << "duplicate row in kKnownTargets: " << info.canonical; + seen.push_back(info.canonical); + } +} + +// And every row must parse to itself: a canonical name that does not survive a +// round trip through `parse`/`str` is a row no `--target` can reach. +TEST(Triple, EveryTableRowIsItsOwnCanonicalForm) { + for (const auto& info : known_targets()) { + auto t = parse(info.canonical); + ASSERT_TRUE(t.has_value()) << info.canonical; + EXPECT_EQ(t->str(), info.canonical) + << info.canonical << " does not round-trip"; + } +}