From db58387389959c781a22886b3473bbc29f26c32f Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:08:43 +0800 Subject: [PATCH 01/11] =?UTF-8?q?2026.8.24.6=20=E2=80=94=20musl=20on=20Win?= =?UTF-8?q?dows=20has=20a=20name,=20and=20the=20segment=20that=20disagreed?= =?UTF-8?q?=20now=20says=20so?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐ **同一个 `--target x86_64-windows-gnu`,两种体系下的 C 库完全不同**, 而 mcpp 用同一个名字称呼它们。实测同一份源码: MinGW CRT musl / openkal 体积 587,894 9,815,552 (16.7×) 依赖 DLL + msvcrt.dll (无) MinGW 的 C 库不是自足的 —— `printf`/`malloc` 的实现在目标机自带的 `msvcrt.dll` 里,587KB 只是「你的代码 + 胶水」。musl 是自足的。 **两者不该共用一个名字。** ── ⚠️ 这个名字 LLVM 拼不出来,而理由不是我以为的那个 ──────── 我三次说「LLVM 词表没有 windows-musl 这个拼写」。**不准确。** 实测: clang++ --target=x86_64-pc-windows-musl -c t.cpp #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) 三元组解析器**认识**它(`-dumpmachine` 原样返回),代码生成路径 **没有实现**它 —— 崩在 COFF 写出器,是 ICE 不是诊断。Windows 上四个 非 MSVC 环境 `gnu`/`cygnus`/`itanium`/`musl`,前三个能编,只有 musl 死; 预定义宏说明它从未被建模(无 `__MINGW32__`)。 ⭐ 于是 mcpp 的名字与交给 clang 的三元组**必须**是两个字符串 —— 而它们本来就是,报告里那个箭头两侧就是: Target x86_64-windows-musl → x86_64-w64-windows-gnu ^ 回答「C 库是谁」 ^ 回答「遵循哪套对象 ABI」 **我此前用编译器的词表限制了 mcpp 自己的词表**,把两个轴混成一个。 这是同一个问题三轮没改对的根子。 ⚠️ pin 为 `llvm@22.1.8` 不是偏好:全局默认为 gcc 时空 pin 会让它落到 musl-gcc 载荷并报「没有 C++ 前端」—— 一条关于缺前端的消息,而真正的 问题是只有 clang 能发这个目标。 ⚠️ 档为 `preview`:端到端还差一环 —— `openkal-musl@0.3.3` 精确钉死 `openkal-windows = "0.1.3"`,索引里已有的 0.1.4 到不了消费者。 ── 修复:同形的两件事,一个报一个不报 ──────────────────── x86_64-linux-gnu 名字说 gnu,事实是 musl → ⚠️ 警告 x86_64-windows-gnu 名字说 gnu,事实是 musl → ❌ 静默 豁免的理由「Windows 上 gnu 命名对象 ABI 而非 C 库」**只对了一半**: 那一段捆着对象 ABI(被兑现)与 MinGW 的 C 运行时(被图替换)。 判据改为「轴 ∈ {CLibrary, ObjectAbi}」,消息额外说明 ABI 那一半未受影响。 ⚠️ 裸机的 `elf` 继续豁免 —— 它在任何平台上都不命名 C 库。 ── 文档 ────────────────────────────────────────────── docs/16 新增「三套词表」(中英):`x86_64-w64-mingw32` 为何把实现放在 OS 位(autoconf 世界观)、clang 如何重拼为 `windows-gnu`、以及 `gnu` 这个 env 值正是 LLVM 拆开 `mingw32` 后给剩下那一半起的名字。 .agents/docs 另收两份:三轴实测矩阵,以及一份**待 review 的规范设计** —— 提议 mcpp 拥有自己的目标词表(`--`,第三段恒定表示 C 库),与 clang 的分歧限于五处且每处有实测支撑。 单元测试 93/0(targetside 断言随语义反转更新),e2e 284 全绿。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 238 +++++++++ .../2026-08-25-os-toolchain-target-matrix.md | 463 ++++++++++++++++++ CHANGELOG.md | 70 ++- docs/16-the-target-triple.md | 125 +++++ docs/zh/16-the-target-triple.md | 117 +++++ mcpp.toml | 2 +- src/targetside/model.cppm | 43 +- src/toolchain/triple.cppm | 90 ++++ src/version.cppm | 2 +- .../284_env_segment_is_optional_everywhere.sh | 44 ++ tests/unit/test_targetside.cpp | 81 ++- 11 files changed, 1220 insertions(+), 55 deletions(-) create mode 100644 .agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md create mode 100644 .agents/docs/2026-08-25-os-toolchain-target-matrix.md 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..f63b0adc --- /dev/null +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -0,0 +1,238 @@ +# 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` | (无) | libSystem —— 该平台只有一个,不需要名字 | +| `riscv64-none` | (无) | **没有 C 库** | + +⚠️ **`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`(预留) | +| **省略规则** | 省略 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 是否需要 libc 段。** 实测该平台 C 库唯一(libSystem), + 但构建期体系下 `c-abi` 报的是 `musl (graph)` —— 那是 openkal 在 macOS 上 + 的 musl,与 libSystem 并存。⚠️ 这意味着 macOS 也可能需要 + `aarch64-macos-musl` 与 `aarch64-macos-system` 的区分,与 Windows 同形。 + 本轮未展开,应作为下一步实测项。 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..965140c5 --- /dev/null +++ b/.agents/docs/2026-08-25-os-toolchain-target-matrix.md @@ -0,0 +1,463 @@ +# 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 不可再分发 | +| `*-none-*`(裸机) | ✅ | ✅ | ✅ | ⭐ clang/lld 天生交叉,**不需要任何按宿主的载荷** | + +⭐ 裸机那一行是整张表里唯一「三个宿主全绿」的,而理由不是覆盖得好, +是**它根本不需要载荷**。注释把这一点写得很准: + +> ⚠️ 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..c6687eca 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -210,6 +210,96 @@ 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 }, + // ⚠️ musl ON WINDOWS, WHICH THE LLVM TRIPLE VOCABULARY CANNOT SPELL AND + // WHICH EXISTS ANYWAY. + // + // LLVM 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 says otherwise. 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 and the C++ ABI is Itanium. That is musl on + // Windows, and mcpp had no name for it — so its identity, its output + // directory and its packed ABI tag all read `gnu`, which is the one thing + // the C library is not. + // + // ⭐ THE FIX IS A NAME, NOT A MECHANISM. mcpp's canonical form and the + // triple handed to clang are already two different strings — the report + // prints both, either side of an arrow — and `llvm_triple()` already sends + // every non-MSVC Windows target to `…-w64-windows-gnu`. That spelling is + // correct there and stays: it selects the Itanium ABI, which is the ABI + // this C library was built for. What was missing was mcpp's own word for + // the row. + // + // The pin is empty because nothing supplies this target from a payload; a + // project reaches it through the graph, and a project that names no such + // dependency is told so by the layer report rather than handed a payload + // that does not exist. + { "x86_64-windows-musl", "preview", "PE", "", "", 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..ffb8f563 100755 --- a/tests/e2e/284_env_segment_is_optional_everywhere.sh +++ b/tests/e2e/284_env_segment_is_optional_everywhere.sh @@ -96,3 +96,47 @@ 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 toolchain line proves the mapping: mcpp's name goes in, and the +# compiler is asked for the target it actually understands. +case "$out" in + *"→ x86_64-windows-musl →"*) + echo " ok the toolchain line carries mcpp's own name" ;; + *) : ;; # older phrasing; the check above is the load-bearing one +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 From 166193a74dcf1e01288b8e85fddfcb2f5a0b84f4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:24:12 +0800 Subject: [PATCH 02/11] =?UTF-8?q?docs(spec):=20macOS=20=E4=B9=9F=E6=9C=89?= =?UTF-8?q?=20musl,=E8=80=8C=20libSystem=20=E4=B8=8D=E8=AF=A5=E6=9C=89?= =?UTF-8?q?=E5=90=8D=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处我写错了,都是同一种错误的两个方向。 ⚠️ **「苹果 SDK 不可再分发」把源码和 SDK 混成了一件事。** Apple 的 Libc 是开源的(`apple-oss-distributions/Libc`);不可再分发的是 SDK 里的头文件 与 `.tbd` 存根库 —— 那才是交叉链接 Mach-O 所需的。我把「不能交叉」的原因 说成了「libc 闭源」。另外 `libSystem` 不等于 Libc,它是一把伞, 底下含 libc、libm、libpthread、libdyld。 ⚠️ **「macOS 只有一个 C 库,不需要名字」已经不成立** —— openkal 让 macOS 上也有了 musl。而我在同一份文档的 §5 里记下了这个实测, **却没有回头改 §1.1 的表**。测出来了、没把结论传播到该改的地方, 这是本轮第二次。 ── ⭐ 但结论仍是「libSystem 不该有名字」,理由换成实测的 ────── macOS 的三元组形状与另外两个平台不同:OS 段带部署目标,且没有 env 段。 clang 接受任意 env 后缀却不赋予含义: arm64-apple-macos14.0-gnu → 原样返回,未规范化 arm64-apple-macos14.0-musl → 原样返回,未规范化 原样保留而不像 Windows 那样重拼,说明 **Darwin 那一支根本不看 env 段**。 给 libSystem 起名会得到一个**在编译器侧没有对应物**的字符串。 于是规范定:**macOS 第三段只有一个合法值 `musl`,libSystem 由省略表达。** 三平台因此不对称,而不对称来自被映射的那一侧,不是 mcpp 的选择。 ⭐ 连带:openkal 路径压根不碰 libSystem,所以这个命名问题在构建期体系里 从一开始就不存在 —— 只在预构建路径上被问起,而那条路径今天用的正是省略式。 ── §6 新增:已知代价,记录在案 ────────────────────────── 「libSystem 由省略表达」的代价是**说不出它**:一个装了 openkal 依赖、 却想在 macOS 上用 libSystem 的工程无法表达这个意图,而 Linux 上 `-gnu` 恰恰能做到。 不解的理由、**重新打开它的三个触发条件**、以及届时的候选做法都写进 §6。 ⭐ 并把 §5 与 §6 的性质分开:§5 是**尚未测**,§6 是**测过了并决定不做** —— 这两种「待办」看着像,后来者对它们该做的事完全不同。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 108 ++++++++++++++++-- .../2026-08-25-os-toolchain-target-matrix.md | 11 +- 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index f63b0adc..fd5e0ec4 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -80,9 +80,62 @@ mcpp 在映射时补出编译器需要的 vendor,自己不保存它。 | `x86_64-windows-gnu` | C 库 | **MinGW CRT**(封装 `msvcrt.dll`) | | `x86_64-windows-musl` | C 库 | **musl**(openkal 之上) | | `x86_64-windows-msvc` | C 库 | UCRT | -| `aarch64-macos` | (无) | libSystem —— 该平台只有一个,不需要名字 | +| `aarch64-macos` | (省略) | 由图决定:有 openkal 则 musl,否则 libSystem | +| `aarch64-macos-musl` | C 库 | **musl**(openkal 之上) | | `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` 作为别名**并在文档中说明二者等价,新写的工程用哪个都对。 @@ -193,7 +246,7 @@ riscv64-none → (clang 专属:见 §2.3) | **语法** | `-[-]`,段内字符集,大小写 | | **arch 取值** | `x86_64` `aarch64` `riscv64` `riscv32` … | | **os 取值** | `linux` `windows` `macos` `none` | -| **libc 取值** | `gnu`(别名 `glibc`)`musl` `msvc` `ucrt`(预留) | +| **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` | @@ -231,8 +284,49 @@ riscv64-none → (clang 专属:见 §2.3) - **没有断言反向映射的完整规则。** §2.1 的映射不是双射,而诊断路径上 确实需要「从 clang 三元组说出 mcpp 目标」。规则应当是「多对一时报出全部 候选」还是「拒绝反推」,需要看诊断的实际用法再定。 -- **没有断言 macOS 是否需要 libc 段。** 实测该平台 C 库唯一(libSystem), - 但构建期体系下 `c-abi` 报的是 `musl (graph)` —— 那是 openkal 在 macOS 上 - 的 musl,与 libSystem 并存。⚠️ 这意味着 macOS 也可能需要 - `aarch64-macos-musl` 与 `aarch64-macos-system` 的区分,与 Windows 同形。 - 本轮未展开,应作为下一步实测项。 +(macOS 显式钉住 libSystem 的问题已移到 §6,作为一条明确记录在案的代价。) + +--- + +## 6. 已知代价:记录在案,遇到真实用例再解 + +本节记录**明知而未解**的东西。它们不是遗漏,而是判断「现在解的收益不明」 +之后留下的账;每一条都给出**重新打开它的触发条件**,以免后来者需要重新 +把上下文推导一遍。 + +### 6.1 macOS 上无法显式请求 libSystem + +规范定为「libSystem 由省略表达」(§1.1)。代价是**说不出它**: + +``` +aarch64-macos 图里有 openkal → musl 图里没有 → libSystem +aarch64-macos-musl 显式请求 musl +aarch64-macos-??? ← 没有这个写法 +``` + +于是一个**装了 openkal 依赖、却想在 macOS 上用 libSystem** 的工程, +无法表达这个意图。 + +⚠️ **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 的差别是:那些是**尚未测**,这一条是**测过了并决定不做**。 diff --git a/.agents/docs/2026-08-25-os-toolchain-target-matrix.md b/.agents/docs/2026-08-25-os-toolchain-target-matrix.md index 965140c5..143a2dff 100644 --- a/.agents/docs/2026-08-25-os-toolchain-target-matrix.md +++ b/.agents/docs/2026-08-25-os-toolchain-target-matrix.md @@ -68,9 +68,18 @@ | `*-linux-gnu` | ✅ 仅同 arch | ❌ | ❌ | glibc 目标还需要 `xim:glibc` / `xim:linux-headers`,它们只有本 arch 的 | | `*-windows-gnu` | ✅ | ❌ | ✅ | mingw 交叉载荷有 linux-hosted 与 windows-hosted 两份 | | `*-windows-msvc` | ❌ | ❌ | ✅ | MSVC 只在 Windows 上存在 | -| `aarch64-macos` | ❌ | ✅ | ❌ | 苹果 SDK 不可再分发 | +| `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」是就**链接对象**而言, +不是就**实现来源**而言。 + ⭐ 裸机那一行是整张表里唯一「三个宿主全绿」的,而理由不是覆盖得好, 是**它根本不需要载荷**。注释把这一点写得很准: From 53996f81da2e4582c691862d9ea7161f2fa5fb96 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:34:55 +0800 Subject: [PATCH 03/11] test(triple): a duplicate row in the target table is a machine-checkable fact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自查时读 diff 发现 `x86_64-windows-musl` **有两行** —— 改 pin 时插入了修正 后的行,没删掉原来那行。`find_known_target` 取首命中,所以每一处行为都 正确、什么都没失败;表里多的是一行**列值与活着那行不一致的死数据**。 ⭐ 靠通读 diff 发现它,机制是错的:重复是表的性质,机器看得见。 两条测试各四行: TheTargetTableHasNoDuplicateNames 没有两行共用一个规范名 EveryTableRowIsItsOwnCanonicalForm 每行都能 parse 回它自己 第二条守的是另一件事:一个 `parse`/`str` 往返不还原的规范名, 是一行**任何 `--target` 都到不了**的行。 实测:临时插一行重复的,第一条立刻转红。 --- src/toolchain/triple.cppm | 29 ------------------------- tests/unit/test_toolchain_triple.cpp | 32 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm index c6687eca..a3c91110 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -271,35 +271,6 @@ inline constexpr TargetInfo kKnownTargets[] = { // 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 }, - // ⚠️ musl ON WINDOWS, WHICH THE LLVM TRIPLE VOCABULARY CANNOT SPELL AND - // WHICH EXISTS ANYWAY. - // - // LLVM 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 says otherwise. 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 and the C++ ABI is Itanium. That is musl on - // Windows, and mcpp had no name for it — so its identity, its output - // directory and its packed ABI tag all read `gnu`, which is the one thing - // the C library is not. - // - // ⭐ THE FIX IS A NAME, NOT A MECHANISM. mcpp's canonical form and the - // triple handed to clang are already two different strings — the report - // prints both, either side of an arrow — and `llvm_triple()` already sends - // every non-MSVC Windows target to `…-w64-windows-gnu`. That spelling is - // correct there and stays: it selects the Itanium ABI, which is the ABI - // this C library was built for. What was missing was mcpp's own word for - // the row. - // - // The pin is empty because nothing supplies this target from a payload; a - // project reaches it through the graph, and a project that names no such - // dependency is told so by the layer report rather than handed a payload - // that does not exist. - { "x86_64-windows-musl", "preview", "PE", "", "", true }, { "x86_64-windows-msvc", "verified", "PE", "", "", false }, { "aarch64-macos", "verified", "", "", "", false }, { "riscv64-linux-musl", "planned", "", "", "", true }, 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"; + } +} From d5f873baa291ee42f4e844591807244fc3c87873 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:38:23 +0800 Subject: [PATCH 04/11] =?UTF-8?q?docs(spec):=20=C2=A77=20aarch64-linux-mus?= =?UTF-8?q?l=20=E4=BD=9C=E4=B8=BA=E8=A7=84=E8=8C=83=E7=9A=84=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E7=9B=AE=E6=A0=87,=E4=B8=89=E5=A4=84=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=E9=80=90=E5=B1=82=E5=89=A5=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐ **一套目标词表的价值只有在第二个架构上才被检验。** x86_64 上 「按 OS 分」与「按架构分」给出相同答案,判据用错了轴也看不出来。 来源:mcpp-community/mcpp#492 的使用者报告 —— 他要同时出 x86_64 与 aarch64 两份静态二进制。逐层剥出三处: **7.1 ✅ 已修** x87 例程按 OS 排,而它是架构的性质。 `*xf*.c` 的排除挂在 `os = "macos"` 与 `os = "none"` 下,aarch64-linux 落进 `os = "linux"` 那一支去编只有 x86 才有的 `xf_float`。 ⚠️ 两处此前都对 —— 那两个 OS 恰好都蕴含非 x87 架构;linux 段不排除 也对 —— x86 上 `__mulxc3` 会被真实调用。**两个方向都会坏,错的只是轴。** 已提 mcpplibs/openkal-llvm-runtime#5。 **7.2 ⚠️ 未修** `--no-default-config` 本身改变目标特性: clang --target=aarch64-unknown-linux-musl → +outline-atomics clang --target=aarch64-unknown-linux-musl --no-default-config → -fmv ⭐ 根因**不是**三元组不一致(两边逐字核对过都是 `aarch64-unknown-linux-musl`),**不是**缓存串目标(键含 target_triple, 两个 aarch64 条目按工具链正确分开)—— 这两条我都先猜错并被实测否掉。 std 模块带 `--no-default-config` 编(包要求,否则宿主 C 库的头混进来), 普通 TU 不带。**两个都成立的要求相撞**,而 x86_64 上该 flag 不改变特性, 所以从未暴露。三条候选修法按代价列出,均未实测。 **7.3 / 7.4 未查**:`aarch64-linux-gnu` 仍是 planned 而使用者真正要的是 `-musl`(诊断没说「你要的那个另有拼法」);以及使用者报的另两条 —— `std::random_device` 被 `__config_site` 关掉、GMF 里 include 标准头时 宿主头与 openkal 头混合。⭐ 后者与 7.2 是**同一个 cfg** 的两个症状。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index fd5e0ec4..8d38f3d6 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -330,3 +330,105 @@ aarch64-macos-??? ← 没有这个写法 `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 是第一个让它现形的目标 —— 这正是「第二个架构才检验抽象」的又一例。 + +**候选修法**(均未实测,按代价排): + +1. 让普通 TU 也带 `--no-default-config` —— 一致了,但会去掉 cfg 里 + 其它可能承重的东西,影响面未知 +2. 把 std 模块的目标特性显式钉住(`-fno-mv` 或对称地补 `+outline-atomics`) + —— 治标,且要为每个架构维护一张表 +3. ⭐ 让 `stdModuleTargetFlags` 把「驱动配置对特性的影响」也算进去 —— + 即由引擎查询一次 `clang -### --no-default-config` 与不带的差集, + 把差异补给消费侧。治本,但要新增一次编译器探测 + +**验收判据**:`mcpp build --target aarch64-linux-musl` 在一个依赖 +`openkal-llvm-runtime` 的工程上产出 aarch64 静态 ELF,且 `file` 报 +`ELF 64-bit LSB executable, ARM aarch64, statically linked`。 + +### 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 使用者报告里另外两条,未查 + +- **`std::random_device` 不可用** —— payload 的 `__config_site` 两份都写 + `_LIBCPP_HAS_RANDOM_DEVICE 0`,他本地改成 1 即修好 5 个编译错。 + ⚠️ 静态 musl 二进制读 `/dev/urandom` 应当可行,所以 `generic` 那份为何 + 也关着需要单独查。 +- **GMF 里 `#include <标准头>` 的 TU 编不过** —— `clang++.cfg` 把宿主 + libc++ 与 glibc 的 `-isystem` 塞进来,与 openkal 的 musl libc++ 头混合。 + ⭐ 与 7.2 是**同一个 cfg** 引起的两个症状,应一并考虑。 From 40cf1d6941b891473ba6c3094ce23839c5cb5ca6 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:54:07 +0800 Subject: [PATCH 05/11] =?UTF-8?q?docs(spec):=20=C2=A77.2/7.2b/7.5=20?= =?UTF-8?q?=E5=AE=9E=E6=B5=8B=E5=9B=9E=E5=A1=AB,aarch64=20=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E8=BE=BE=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7.2 与 7.2b 从「未修」改为已修并附实测;新增 7.5 —— `std::random_device` 的根在 openkal 规范(SURFACE.txt 全文无 random/entropy),而非运行时包: std::random_device → libc++ 五条后端 → getentropy → musl getrandom() → syscall_cp(SYS_getrandom) → openkal-musl 无替代 → 规范无该接口 ⚠️ `_LIBCPP_HAS_RANDOM_DEVICE 0` **不是保守设置,是事实的准确记录**。 改成 1 只让编译期通过,链接会缺符号。`generic` 那份也关着是对的 —— 它同样跑在 openkal 之上,`generic` 不等于「宿主 Linux」。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 112 +++++++++++++++--- 1 file changed, 95 insertions(+), 17 deletions(-) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index 8d38f3d6..1e17ea51 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -360,7 +360,7 @@ truncxfhf2.c:13:36: error: unknown type name 'xf_float'; did you mean 'tf_float' 修法:`cfg(all(os = "linux", not(arch = "x86_64")))`。 已提 mcpplibs/openkal-llvm-runtime#5。 -### 7.2 ⚠️ 未修:`--no-default-config` 本身改变目标特性 +### 7.2 ✅ 已修:`--no-default-config` 本身改变目标特性 x87 修好之后撞到下一处: @@ -396,19 +396,54 @@ clang --target=aarch64-unknown-linux-musl --no-default-config → -fmv ⚠️ **x86_64 上 `--no-default-config` 不改变特性,所以这对矛盾从未暴露。** aarch64 是第一个让它现形的目标 —— 这正是「第二个架构才检验抽象」的又一例。 -**候选修法**(均未实测,按代价排): +**修法:把 `--rtlib=compiler-rt` 补进 `std-module-flags`。** -1. 让普通 TU 也带 `--no-default-config` —— 一致了,但会去掉 cfg 里 - 其它可能承重的东西,影响面未知 -2. 把 std 模块的目标特性显式钉住(`-fno-mv` 或对称地补 `+outline-atomics`) - —— 治标,且要为每个架构维护一张表 -3. ⭐ 让 `stdModuleTargetFlags` 把「驱动配置对特性的影响」也算进去 —— - 即由引擎查询一次 `clang -### --no-default-config` 与不带的差集, - 把差异补给消费侧。治本,但要新增一次编译器探测 +实测,补上之后两侧逐字相同: -**验收判据**:`mcpp build --target aarch64-linux-musl` 在一个依赖 -`openkal-llvm-runtime` 的工程上产出 aarch64 静态 ELF,且 `file` 报 -`ELF 64-bit LSB executable, ARM aarch64, statically linked`。 +``` +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` @@ -423,12 +458,55 @@ error: target 'aarch64-linux-gnu' is registered but not yet supported (planned) 工具链」,没说「你要的那个目标另有拼法」。`did_you_mean` 今天只对拼错的 名字生效,不对「档位不够但同类目标可用」的情况生效。 -### 7.4 使用者报告里另外两条,未查 +### 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 规范新增随机源接口之后, +`openkal-llvm-runtime` 把两份 `__config_site` 的该宏改为 1, +并在 `[target]` 里为不提供该接口的目标保持 0。 + +### 7.6 GMF 里 include 标准头,未查 -- **`std::random_device` 不可用** —— payload 的 `__config_site` 两份都写 - `_LIBCPP_HAS_RANDOM_DEVICE 0`,他本地改成 1 即修好 5 个编译错。 - ⚠️ 静态 musl 二进制读 `/dev/urandom` 应当可行,所以 `generic` 那份为何 - 也关着需要单独查。 +`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** 引起的两个症状,应一并考虑。 From ee70aa2908d8a41d42eb3b22c6f2da9ffb466634 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:56:21 +0800 Subject: [PATCH 06/11] =?UTF-8?q?docs(spec):=20=C2=A77.5=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E2=80=94=E2=80=94=E7=A7=BB=E6=A4=8D=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E7=8E=B0=E6=88=90,=E7=BC=BA=E7=9A=84=E6=98=AF=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E9=87=8C=E5=8F=AF=E8=BD=AC=E7=9A=84=E7=9B=AE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ 我写「openkal-llvm-runtime 只能开关那个宏」,错了。 `okm_syscall.c` **已经把 69 个系统调用转给了 openkal**,移植机制现成; 真正的约束是规范里没有随机源接口可转。 并补两条:经 fs 打开 `/dev/urandom` 在设计上被挡住(`kal_fs_open` 只能 相对 preopen 目录,而 openkal-linux 只给 2 个)——⭐ 那正是能力模型要挡的 东西,不是缺口是拒绝;以及当下可做的 feature 形状,`openkal-musl` 已有 `[features]` 机制,但名字要说清它假设宿主(`host-entropy` 而非 `random`), 且 `_LIBCPP_HAS_RANDOM_DEVICE` 必须跟随它而非独立开关。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index 1e17ea51..31d114b2 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -498,9 +498,40 @@ openkal 规范里**没有随机源接口**(SURFACE.txt 全文无 random / entr 在这套体系里它同样跑在 openkal 之上,同样没有随机源 —— 名字容易误读, 而设置是对的。 -**重新打开它的触发条件**:openkal 规范新增随机源接口之后, -`openkal-llvm-runtime` 把两份 `__config_site` 的该宏改为 1, -并在 `[target]` 里为不提供该接口的目标保持 0。 +**⚠️ 我先前写「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。⭐ **那正是能力模型要挡的东西** —— 一个程序不能凭一个绝对路径 +够到平台上的任意对象。所以这不是缺口,是拒绝。 + +**落地形状,两段:** + +**长期(根本解)** —— 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.6 GMF 里 include 标准头,未查 From 3a4ec00800a42cc330c93785a0224f50868ea9bf Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 04:01:26 +0800 Subject: [PATCH 07/11] =?UTF-8?q?docs(spec):=20=C2=A77.5.2=20openkal.rando?= =?UTF-8?q?m=20=E6=8E=A5=E5=8F=A3=E8=AE=BE=E8=AE=A1=E8=8D=89=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 §7.5.1 三条判定(不加实现不了 / 不能绕过接口层 / 宿主通用裸机不通用) 给出形状,照 `openkal.time` —— 现有接口里最小的一个,问题结构同型。 ⚠️ **不设 AVAILABLE 能力字**:「有没有」由接口在场与否回答(6.1 条), 这正是 openkal-opensbi@0.1.3 撤回的教训。 ⭐ uefi 那一行最有意思:同一后端在不同固件上可能提供或不提供,而 6.1 条 要求「全有或全无」,所以必须在**构建期**决定 —— 运行期拒绝违反该条。 三处连带改动都写了约束:AT_RANDOM 要保留 fallback(否则裸机上 stack canary 起不来)、`_LIBCPP_HAS_RANDOM_DEVICE` 跟随目标、一致性套件那条会以 2^-256 概率误报而写清楚比省掉好。 ⚠️ 草案未实测,不属于目标词表这一 PR。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index 31d114b2..1484afa1 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -508,6 +508,52 @@ openkal 规范里**没有随机源接口**(SURFACE.txt 全文无 random / entr 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 条, @@ -541,3 +587,76 @@ musl libc++ 头混合。⭐ 与 7.2 是**同一个 cfg** 引起的两个症状, - **GMF 里 `#include <标准头>` 的 TU 编不过** —— `clang++.cfg` 把宿主 libc++ 与 glibc 的 `-isystem` 塞进来,与 openkal 的 musl libc++ 头混合。 ⭐ 与 7.2 是**同一个 cfg** 引起的两个症状,应一并考虑。 + +### 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。 From ad0de5701e8329673484cfa8b593c86f3169b38b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:32:06 +0800 Subject: [PATCH 08/11] test(e2e): a branch that accepts anything is not a check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assertion that the toolchain line carries mcpp's own name for the target had an `*) : ;;` fallback, so it printed on a match and passed on anything else. This file's whole subject is a name that used to be accepted by nothing, and the one line proving the mapping — mcpp's canonical form in, the compiler's spelling out — could not fail. The fallback is gone, and the pin is asserted beside it. The pin is what the row is for: a global default of gcc carried onto this target produces a true sentence about the wrong subject, error: toolchain payload 'xim:musl-gcc@16.1.0' has no known C++ frontend in …/xim-x-musl-gcc/16.1.0/bin so the report showing `llvm@22.1.8` winning is the observable form of the row working. Measured against a binary built from this branch: ok x86_64-windows-musl is known, and the refusal names the real reason ok the toolchain line carries mcpp's own name ok the row's pin decided the toolchain --- .../284_env_segment_is_optional_everywhere.sh | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/e2e/284_env_segment_is_optional_everywhere.sh b/tests/e2e/284_env_segment_is_optional_everywhere.sh index ffb8f563..4c38712c 100755 --- a/tests/e2e/284_env_segment_is_optional_everywhere.sh +++ b/tests/e2e/284_env_segment_is_optional_everywhere.sh @@ -131,12 +131,37 @@ case "$out" in exit 1 ;; esac -# And the toolchain line proves the mapping: mcpp's name goes in, and the -# compiler is asked for the target it actually understands. +# ⭐ 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" ;; - *) : ;; # older phrasing; the check above is the load-bearing one + *) + 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" From f86d19fe31d8c01fb671a21ed011036b4b49ae70 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:42:03 +0800 Subject: [PATCH 09/11] docs(spec): `aarch64-macos-musl` was written as a spelling that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §1.1 listed it as "显式请求 musl" and §6.1 built an argument on it: that macOS can name musl but cannot name libSystem. Measured against a binary built from this branch, neither is true — $ mcpp build --target aarch64-macos-musl error: unknown target 'aarch64-macos-musl' — the table has one macOS row and it carries no env segment, so both directions are unsayable there. Windows can name its C library because this PR adds `x86_64-windows-musl`; macOS is missing the same row rather than missing a policy, which is a smaller and more accurate statement of the gap than the one that was written. Every target name in docs/16 (both languages) and the three analysis documents was then checked against the table. Twenty-three appear; four short spellings are valid by the fill rule, four more are candidate spellings in passages that say they are not adopted, and this was the only one presented as real that is not. --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index 1484afa1..cb4d4867 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -81,7 +81,7 @@ mcpp 在映射时补出编译器需要的 vendor,自己不保存它。 | `x86_64-windows-musl` | C 库 | **musl**(openkal 之上) | | `x86_64-windows-msvc` | C 库 | UCRT | | `aarch64-macos` | (省略) | 由图决定:有 openkal 则 musl,否则 libSystem | -| `aarch64-macos-musl` | C 库 | **musl**(openkal 之上) | +| `aarch64-macos-musl` | — | **词表里没有这一行**,见 §6.1 | | `riscv64-none` | (无) | **没有 C 库** | ⚠️ **macOS 也需要这一段,而我第一稿写的是「该平台只有一个 C 库」。** @@ -300,12 +300,26 @@ riscv64-none → (clang 专属:见 §2.3) ``` aarch64-macos 图里有 openkal → musl 图里没有 → libSystem -aarch64-macos-musl 显式请求 musl -aarch64-macos-??? ← 没有这个写法 +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 也能说出来(今天的行为是报出分歧并以图为准 —— From 74908f34bb7719515abe190ade37fc8f66c919eb Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:45:33 +0800 Subject: [PATCH 10/11] =?UTF-8?q?docs(spec):=20=C2=A78=20=E8=B7=A8?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E8=87=AA=E5=AE=A1=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E5=85=AD=E6=9D=A1=E8=A2=AB=E5=88=A4=E6=8D=AE=E6=8A=93=E4=BD=8F?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 并修正 §7.5.2 排在 §7.6 之后的顺序。 --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 118 ++++++++++++++++-- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index cb4d4867..361b2fbe 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -593,15 +593,6 @@ host-entropy = { } # port 提供 getrandom,经宿主特定通路取熵 命名与映射」,这条讲的是「平台接口该不该多一个能力」,混在一起会让两件事 都说不清。 -### 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** 引起的两个症状,应一并考虑。 - ### 7.5.2 `openkal.random` 接口设计草案 按 §7.5.1 的三条判定,该加。形状照 `openkal.time` —— 它是现有接口里最小的 @@ -674,3 +665,112 @@ kal_status kal_random_fill(void* out, kal_uintptr len); **⚠️ 本草案未实测。** 它是判定之后的下一步,而判定本身(§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 对它来说是散文。这就是一行 +能在若干次发布里一直缺着的原因。 From cefa12a48e5b61f55e27a169fb70bbda8adf639d Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:23:27 +0800 Subject: [PATCH 11/11] =?UTF-8?q?docs(spec):=20=C2=A78.7/=C2=A78.8=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E5=8F=88=E4=B8=A4=E6=9D=A1=E8=A2=AB?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E6=8A=93=E4=BD=8F=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-08-25-mcpp-target-vocabulary-spec.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md index 361b2fbe..d2fe57ac 100644 --- a/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md +++ b/.agents/docs/2026-08-25-mcpp-target-vocabulary-spec.md @@ -774,3 +774,62 @@ tree 0.8.0 → exit 1 1.0 以下,minor 前进即为破坏性 `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 是弱的」会在符号消失或对象没被构建时同样通过,所以旁边那个 +**必需**接口必须仍是未定义强引用。