Skip to content

fix(target): the triple is a request, and the row's convention waits for the graph - #495

Merged
Sunrisepeak merged 6 commits into
mainfrom
fix/triple-request-and-late-pin
Aug 24, 2026
Merged

fix(target): the triple is a request, and the row's convention waits for the graph#495
Sunrisepeak merged 6 commits into
mainfrom
fix/triple-request-and-late-pin

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Summary

两个症状,一个病根:compiler 被声明为第五层,而它的解析留在了包含它的结构之外。 一个在包含它的结构之前就被决定的层,不是结构的成员,是伪装成成员的输入。

症状 1 — --target x86_64-linux 报告 -gnu

Target x86_64-linux-gnu → x86_64-unknown-linux-gnu
       c-abi   musl   (openkal-musl@0.3.3, graph)

名字与它下面那一行矛盾,而构建成功了

三元组同时充当身份(输出目录、缓存键、cfg() 的主语)和请求。身份必须是全的,请求必须能说「没指定」;parse 用自动填充让身份变全,代价是请求消失。

⚠️ 修法是窄的,而窄是量出来的:读 env 的共 22 处,其中 10 处在 triple.cppm 自己里。保留填充,另记 Triple::envExplicit

⚠️ 请求必须在规范化之前捕获 —— overrides.target_triple = parsed->str() 之后再 parse 就分不出来了。第一版正是在这里丢的:--target x86_64-linux 被当成显式 gnu 而遭拒。

症状 2 — 约定在图之前应用,而它的问题在图之后才有答案

x86_64-linux-musl → gcc@16.1.0 说的不是「偏好 gcc」,是「musl-gcc 载荷供给这个目标的 C 库」。工程的 C 库若来自依赖图,该载荷根本不被使用。

⚠️ 早决定被双向实测否掉:无条件应用会替换用户设下的工具链;不应用会让零依赖的交叉构建从可用变为不可用。两者都错,因为都在猜一个尚不存在的事实。

判据换成它本来就该是的:图供给 kernel-abic-abi 时,约定不适用。

⚠️ 代码不搬,只搬执行时机 —— 原地包成 lambda,在图已知处调用。先前记录的「39 处读写挡着」是没测就写下的:实测依赖解析段读 tc1 处,且那一处要的是三元组不是编译器。逃逸出 lambda 的局部只有 tcSpecIsMsvc 一个。

实测

场景 结果
openkal 工程 + 全局 llvm 默认 + --target x86_64-linux-musl Resolved llvm@22.1.8
无依赖工程 + 同一目标 Resolved gcc@16.1.0(约定生效)✅
无依赖工程 + x86_64-windows-gnu(先前的回归点) Resolved gcc@16.1.0
--target x86_64-linux + 图供 musl 构建成功,报告写 Target x86_64-linux
--target x86_64-linux-gnu + 图供 musl 拒绝,同时指出请求与事实 ✅

Test plan

  • test_targetside 31 passed(新增请求语义三格)
  • unit 93 passed / 0 failed
  • e2e 新增 282_target_is_a_request
  • 全量 e2e:无新增失败(既有 26 条已逐条与基线二进制对照)
  • check_docs_style.sh 通过;SPEC-002 增 §3.4/§3.5;docs/14 中英同步
  • CI

⚠️ v2026.8.24.2 的 GitHub Release 资产已发布但索引未采纳(bump PR 未开),所以用户仍在 24.1。本 PR 发 2026.8.24.3,由它进索引。

…for the graph

两个症状,一个病根:**`compiler` 被声明为第五层,而它的解析留在了包含它的结构
之外。** 一个在包含它的结构之前就被决定的层,不是结构的成员,是伪装成成员的输入。

── 症状 1:`--target x86_64-linux` 报告 `-gnu` ────────────

    Target x86_64-linux-gnu → x86_64-unknown-linux-gnu
           c-abi   musl   (openkal-musl@0.3.3, graph)

名字与它下面那一行矛盾,而构建成功了。

三元组同时充当**身份**(输出目录、缓存键、`cfg()` 的主语)和**请求**。
身份必须是全的,请求必须能说「没指定」。`parse` 用自动填充让身份变全,
代价是请求消失 —— 两态在下游不可区分。

⚠️ 修法是窄的,而窄是量出来的:读 `env` 的共 **22 处,其中 10 处在
triple.cppm 自己里**。保留填充,另记 `Triple::envExplicit`。

⚠️ `operator==` 不能再 `= default`:`envExplicit` 记录的是来源不是身份,
默认比较会让用户写的 `x86_64-linux-gnu` 与 `host_triple()` 推出的不相等,
第一个坏掉的是 `mcpp toolchain list` 的 `host` 标记。

⚠️ 请求**必须在规范化之前捕获**。`str()` 渲染的是填好的身份,
`overrides.target_triple = parsed->str()` 之后再 parse 就分不出来了 ——
第一版正是在这里丢的,`--target x86_64-linux` 被当成显式 gnu 而遭拒。

── 症状 2:约定在图之前应用,而它的问题在图之后才有答案 ──

`x86_64-linux-musl → gcc@16.1.0` 说的不是「偏好 gcc」,是
「musl-gcc 载荷供给这个目标的 C 库」。工程的 C 库若来自依赖图,
该载荷根本不被使用 —— 而这只有解析完图才知道。

⚠️ 早决定被**双向实测**否掉:无条件应用会替换用户用 `mcpp toolchain default`
设下的工具链(上一版的做法);不应用会让一个零依赖的交叉构建从可用变为不可用
(更早那一版被回退的做法)。两者都错,因为都在猜一个尚不存在的事实。

判据换成它本来就该是的那个:**图供给 `kernel-abi` 或 `c-abi` 时,约定不适用。**

⚠️ 代码不搬,只搬执行时机:原地包成 `resolve_target_toolchain` lambda,
在 `packages` 填好之后调用。先前记录的「39 处读写挡着」是**没测就写下的** ——
实测依赖解析段读 `tc` 仅 **1 处**,而那一处要的是**三元组不是编译器**
(`tc->targetTriple` 在检测后几行就被改写成请求的三元组,两者同值)。
逃逸出 lambda 的局部只有 `tcSpecIsMsvc` 一个,提到外层即可。

── 实测 ────────────────────────────────────────────────

| 场景 | 结果 |
|---|---|
| openkal 工程 + 全局 llvm 默认 + `--target x86_64-linux-musl` | `Resolved llvm@22.1.8` ✅ |
| 无依赖工程 + 同一目标 | `Resolved gcc@16.1.0`(约定生效)✅ |
| 无依赖工程 + `x86_64-windows-gnu`(先前的回归点) | `Resolved gcc@16.1.0` ✅ |
| `--target x86_64-linux` + 图供 musl | 构建成功,报告写 `Target x86_64-linux` ✅ |
| `--target x86_64-linux-gnu` + 图供 musl | 拒绝,指出请求与事实 ✅ |

test: test_targetside 31 passed(新增请求语义三格);unit 93 passed;
      e2e 新增 282;全量 e2e 进行中,至此无新增失败。
⚠️ 上一版的拒绝是语义上干净的答案,而它在 mcpp 自己的 openkal 矩阵上第一个开火:

    $ mcpp build --target x86_64-linux-gnu        # openkal 工程
    error: this build requests the `gnu` C ABI, and its dependency graph
           supplies `musl`.

`x86_64-linux-gnu` 正是 `mcpp toolchain list` 打印的宿主目标拼写,
因此也是人们会写的那一个。拒绝它等于打破每一个已经这么写的工程与 CI 配置。

判据是该请求**不改变任何东西**:图两种写法下都供给同一个 C 库,
所以那一段是**被忽略而非被违反**。一个去掉它也完全相同的构建,不是该被拒绝的
构建 —— 它是一个名字与自身不符的构建,而说出来就是全部的补救。

    warning: the target name asks for the `gnu` C ABI and the dependency graph
             supplies `musl`.
           The graph decides, so the build below uses `musl` — the name is what
           is inaccurate, not the artifact. Drop the segment to say what is
           actually meant:
               --target x86_64-linux

⚠️ 补救必须可执行,而这正是 `x86_64-linux` 必须先能用的原因:
告诉一个人他的目标名字写错了,只有在存在一个对的名字可以给他时才有用。

test: unit 93 passed;e2e 268/280/281/282 全绿。
spec: SPEC-002 §3.4 改为「必须报出、禁止据此失败」,并记下被否掉的那一版。
…model

── examples/06-openkal-cross ──────────────────────────────

一个「询问它落在哪台机器上」的程序,从任何宿主构建到四个目标而不修改一个字符。
`src/main.cpp` 里**没有一条预处理指令,也没有一处按目标名分支**。

实测,同一份源码、同一台 Linux 宿主、两个目标:

| 事实 | x86_64-linux | x86_64-windows-gnu |
|---|---|---|
| 预开目录数 | 2 | 5 |
| 路径区分大小写 | yes | **no** |
| 单调时钟粒度 (ns) | 1 | **100** |

代码路径处处相同,答案不同 —— 这正是「询问而不假定」与「在若干套 `#if` 下
编译」的区别。选的几项查询各自压到目标侧的不同层,而其中展开那一项最严:
展开是 C++ 运行时里**无论是否可用都能链接成功**的那一部分,
一个没有展开器的程序照样构建,只在抛出时失败;析构函数在展开中运行,
才把两者分开。

⚠️ 而这个程序在移植过程中挖出了一条规范缺陷 —— 见 openkal-opensbi#2:
一个**一次文件系统调用都没有**的程序,仅仅因为「询问是否存在文件系统」
就在裸机后端上链接失败,因为查询是属性对象上的 inline 函数。
读规范两轮没发现,写一个程序跑一次就挖出来了。

── docs/15(中英)────────────────────────────────────────

模型、工程书写什么、生态供给什么、以及三条已实测的界限。

⚠️ 顺带修掉一处我自己刚引入的语义错误:请求检查把 env 段一律当作 C 库请求,
而**在 Windows 上那一段命名的是对象 ABI**(`gnu` = PE/GNU,`msvc` = PE/MSVC),
两者都与不止一种 C 库相容。把 Windows 构建报成「请求了 gnu C ABI」描述的是
一条该名字从未涉及的轴,而它建议的更正指向一个不存在的目标。
检查因此限定在该段确实命名 C 库的平台上 —— 这一点我自己的 §4 分析写过,
实现时没照做。

test: unit 93 passed;e2e 268/280/281/282 全绿;
      示例四个宿主目标(host / x86_64-linux / aarch64-macos / x86_64-windows-gnu)
      全部构建通过,Linux 与 wine 下均运行。
UEFI 是其中一条,而不是全部。补上另一条:x86 内核开发。

| 路线 | 目标 | 平台层 | 入口 |
|---|---|---|---|
| UEFI 应用 | `x86_64-windows-gnu` | `openkal-uefi` | 固件,Boot Services 可用 |
| 内核 / 裸机 | `x86_64-none-elf` | 无,或 `openarch` | 复位向量,其下一无所有 |

区别在于**谁加载这个程序**。UEFI 应用有固件服务可调,因此它是 PE/COFF、
经微软 x64 调用约定进入,与 Windows 程序共用三元组;内核没有,
因此它是零 libc 档的 ELF,在自己的 `_start` 被进入并直接触达硬件。

⚠️ `openarch` **不应答 `mcpp:kernel-abi`**,写清楚这一点是必要的:
它不是平台接口,而是架构机制 —— 执行上下文、陷阱、每 CPU 状态、地址空间 ——
跨若干指令集的一个接口,每个指令集一个后端包。一个内核依赖它,
并供给自己的平台层,或者不供给。

── 并记下 x86_64 裸机为何需要引擎侧的工作 ────────────────

`riscv64-none-elf` 与 `aarch64-none-elf` 只是表中的行:clang 对两者都有
BareMetal 工具链。它对 x86_64 没有,于是那个三元组落到通用 GCC 工具链,
而后者的链接器是**宿主的 g++**:

    g++: error: unrecognized command-line option '-fuse-ld=…/ld.lld'

对裸 x86_64 三元组的每一种拼写都实测过且无法由任何 flag 纠正,
因此该行携带链接器 emulation,由 mcpp 自己调用 `ld.lld`。

示例 README 同步:该目录里的程序在内核路线上跑不起来,
因为它向平台接口提问而那里没有平台接口 —— 说明白比含糊带过有用。
我写了「两种写法下产物相同」,然后去核验这句可证伪的话:

    x86_64-linux       b9bcf550…
    x86_64-linux-musl  481b27d9…      ← 不同
    strip 之后         5a3ed2e3… 两侧一致
    .text 单独取出     ba856ed6… 两侧一致

⭐ 代码是同一份代码,差异整个落在**调试信息**里 —— 它记录了输出目录,
而目录以三元组命名。原话按字面是假的,而它想说的那件事是真的,
于是把测出来的东西写进去,而不是把话说得更满。

── 顺带记下验收结果 ──────────────────────────────

用报告里那两条命令实测(2026.8.24.3,本机):

    --target x86_64-linux        Target x86_64-linux → …-gnu
                                 c-abi musl (graph)          标题不再自相矛盾
    --target x86_64-linux-musl   exit 0                      原先是「未知目标」
    --target x86_64-linux-gnu    warning + 可粘贴的改法       原先静默

第三条的提示给出 `--target x86_64-linux` 这一行可直接粘贴,
因为「说清楚哪里不对」和「说清楚该怎么写」不是同一件事。
…ad it backwards

示例声称「一份源码,四台机器」,其中第四台是 riscv64 裸机。它跑不起来,
而我上一轮把原因定成了后端的缺陷,给 `openkal-opensbi` 加上了

    const kal_uintptr kal_fs_props   = 0;
    const kal_uintptr kal_task_props = 0;

发布为 0.1.3 并合进了索引。⚠️ 这是规范明令禁止的那一种补法。

openkal SPEC 6.1:「实现不提供的接口,作为链接期定义是缺席的,
使用它的消费者链接失败。」6.2 的表把三个时机分开:

    依赖解析 │ 包声明它提供什么 │ 这个程序可否针对这个实现构建
    链接     │ 未定义符号       │ 是否用了它不提供的接口
    运行     │ 能力字           │ 在**它提供的接口内**它如何表现

⭐ 能力字回答的问题比它初看上去窄得多。给一个没有任何操作的接口
定义能力字,是在回答第三个问题而跳过第二个 —— 程序随后越过了
链接器存在的意义,接着撞上未定义的 `kal_fs_open`,或者认为自己
有一个零能力的文件系统。opensbi 只实现 abort/stream/memory/env/time,
一个 `kal_fs_*` 操作都没有。

于是本提交把方向反过来:**程序才是那个不该这么写的东西**。

  示例 mcpp.toml   删掉 `[target.riscv64-none-elf]`,并写明为何没有
  示例 main.cpp    「可以问一个文件系统怎样比较名字,不可以问一台
                   机器有没有文件系统」;两处正文注释原先说反了
  示例 README      新增「本程序不问什么」,把那条链接错误作为机制展示
  docs/15 中英     新增「源码是同一份,程序不是」

「同一份源码」是关于工具链与标准库的断言,它成立;它从来不是
「任何程序都能为任何目标构建」的断言 —— 后者由依赖图回答。

实测(本机 Linux 宿主,2026.8.24.3):
    x86_64-linux        ELF 64-bit LSB executable
    aarch64-macos       Mach-O 64-bit arm64 executable
    x86_64-windows-gnu  PE32+ executable (console) x86-64

上游:mcpplibs/openkal-opensbi#4 撤回 0.1.3,发 0.1.4。
@Sunrisepeak
Sunrisepeak force-pushed the fix/triple-request-and-late-pin branch from 653f5d0 to db8244c Compare August 24, 2026 14:03
@Sunrisepeak
Sunrisepeak merged commit c6ed83c into main Aug 24, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants