diff --git a/.agents/docs/2026-08-25-target-system-analysis.md b/.agents/docs/2026-08-25-target-system-analysis.md new file mode 100644 index 00000000..627add56 --- /dev/null +++ b/.agents/docs/2026-08-25-target-system-analysis.md @@ -0,0 +1,330 @@ +# 目标体系分析:三元组承载了四件事,而它只有三段 + +2026-08-25。本文分析的是 **target 体系** —— 三元组的语法与语义、目标表、 +目标与工具链的关系、以及使用者必须知道多少才能写出一条 `--target`。 + +与 `2026-08-24-target-side-design.md` 的分界:那份讲**目标侧**(五层由谁供给), +本文讲**目标本身**(它是什么、怎么写、被谁读)。两者在 `env` 段相交, +而那个交点正是本文最大的一处缺陷。 + +所有断言取自 2026-08-25 的 HEAD(2026.8.24.5 待发)与实测,不取自记忆。 + +--- + +## 0. 一句话 + +三元组同时是**身份**、**请求**、**表的键**和**目录名**,四种角色对 +「缺席」的要求互相冲突;当前实现只在 Linux 上调和了这个冲突, +其余平台把冲突转嫁给了使用者。 + +## 0.1 分界:第三段在两种体系下做的是两件事 + +⭐ 本文所有关于「第三段」的讨论,真正的分界在这里,而它此前没有被写下来。 + +| | 传统预构建体系 | 构建期体系 | +|---|---|---| +| 目标侧来自 | 工具链**载荷** | 依赖**图**,从源码构建 | +| 第三段的作用 | **挑选载荷** —— 解析期承重 | **不选中任何东西** —— 图已经决定 | +| `x86_64-linux-musl` | 选中 musl-gcc 载荷 | 与 `x86_64-linux` 产出相同 | +| 该不该写 | 该写,它是作出选择的方式 | 不该写,它陈述一个不被查询的请求 | + +于是「第三段能不能省略」不是一个可以按平台一刀切的问题: +**在构建期体系下它应当总是可省,而在预构建体系下写出它才是准确的。** +两者共用同一套语法,所以语法必须允许省略,而由**报告**说明这次解析 +落在哪一种体系里 —— 那正是 `(…, graph)` 与 `(…, payload)` 两个来源标记 +已经在做的事。 + +⚠️ 这条分界解释了 §4 那个不对称为什么代价特别大:被强制写出第三段的两个 +平台(windows、none),恰恰是构建期体系最常用的两个。 + +--- + +## 1. 现状:一条 `--target` 会被读几次 + +``` +--target x86_64-windows-gnu + │ + ├─► parse() 语法 → Triple{arch, os, env, envExplicit} + │ ⚠️ 此处发生「填充」,而填充只对 linux/macos 有规则 + ├─► find_known_target() 查 kKnownTargets(16 行 × 6 列) + ├─► freestanding::resolve() 查第二张表 kTable(6 行 × 6 列) + ├─► effective_sysroot() 表的 sysroot 列 vs 清单的 [target.X].sysroot + ├─► cfg() 求值 os / arch / family / env 四个键 + ├─► 输出目录 target/// + └─► targetside::resolve() env 段作为 C 库**请求**参与五层解析 +``` + +⭐ **同一个字符串在七处被读,而七处对它的期待不同。** 目录名要求它总是完整 +(否则同一工程两种拼法产出两棵树);请求要求它能省略(否则无法表达 +「我不指定 C 库」);表的键要求它精确匹配。 + +## 2. 表:两张,列的含义不齐 + +### 2.1 `kKnownTargets`(`src/toolchain/triple.cppm:207`,16 行) + +| 列 | 谁读 | 决定什么 | +|---|---|---| +| `canonical` | 全部 | 表的键;也是输出目录名 | +| `tier` | `lifecycle.cppm:593` | `planned` 行在 `toolchain list` 里显示为不可装 | +| `note` | 同上 | 列表里的说明文字(`PE`/`bare`) | +| `pin` | `prepare.cppm` | **该目标的 C 库由哪个载荷供给**(不是「首选编译器」) | +| `sysroot` | `effective_sysroot` | 该行默认的 C 库包;空 = 零 libc 档 | +| `defaultStatic` | `lifecycle.cppm:550` + 链接 | 该目标默认静态链接 | + +### 2.2 `freestanding::kTable`(`src/freestanding/target.cppm:135`,6 行) + +裸机行的第二张表:`march` / `mabi` / `mcmodel` / `libdir` / `extra` / +`lldEmulation`。键仍是同一个 canonical 三元组。 + +⚠️ **两张表用同一个键而没有任何东西保证它们一致。** 一个裸机行可以出现在 +`kKnownTargets` 而在 `kTable` 里缺席(反之亦然),后果是构建走到很深处才失败。 +今天没出事,是因为两张表都只有一个人在维护。 + +## 3. 使用者必须知道的东西(实测清点) + +写一条 `--target` 之前,使用者要知道: + +1. **哪些三元组存在** —— `mcpp toolchain list` 的 Targets 段 +2. **第三段写什么** —— 而它在每个平台上是不同的轴 +3. **能不能省略第三段** —— 答案按平台不同,且没有任何文档说明规则 +4. **省略之后身份是什么** —— 影响输出目录与缓存键 +5. **`[target.X]` 里能写什么** —— `toolchain` / `linkage` / `runner` / + `cxxRuntime` / `sysroot` 五个键 + +第 2、3 两条是本文的核心缺陷,下一节。 + +--- + +## 4. 缺陷一:`env` 段的轴按平台变化,而语法不区分 + +| 平台 | 第三段 | 它命名什么 | 可省略? | +|---|---|---|---| +| linux | `gnu` / `musl` | **C 库** | ✅ `x86_64-linux` | +| macos | (无) | — | ✅ 本来就没有 | +| windows | `gnu` / `msvc` | **对象 ABI** | ❌ `x86_64-windows` 被拒 | +| none | `elf` | **对象格式** | ❌ `riscv64-none` 被拒 | + +实测(2026.8.24.5): + +``` +$ mcpp build --target x86_64-linux ✓ +$ mcpp build --target x86_64-windows error: unknown target 'x86_64-windows' +``` + +⭐ **2026.8.24.3 立的规矩是「三元组是请求,而请求必须能什么都不说」, +而它只在 Linux 上实现了。** 其余平台上,使用者被强制写出第三段 —— +包括在 openkal 体系下写一个 `gnu`,而那次构建里**没有任何东西是 GNU 的**: + +| 层 | 实际 | GNU? | +|---|---|---| +| 编译器 | clang | 否 | +| 链接器 | lld | 否 | +| compiler-runtime | compiler-rt | 否 | +| C 库 | musl | 否 | +| C++ 运行时 | libc++ | 否 | +| 平台 | openkal | 否 | + +`gnu` 是 LLVM 词表里的历史标签(源自 MinGW),如今只表示「Windows 上非 +MSVC 的那套 ABI」。clang 靠这个拼写选内部 ToolChain,mcpp 改不了它 —— +但**能不要求使用者写它**。 + +### 4.1 已做的缓解与它的不足 + +2026.8.24.5 在报告里给 `gnu` 加了落脚点: + +``` +Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium C++ ABI, not a C library) +``` + +这解决了「读者把 `gnu` 误映到 `c-abi musl`」,**没有解决「使用者被迫写它」**。 +一句解释一个本不该出现的词,比让那个词消失要差。 + +### 4.2 ✅ 已实现(方案 A) + +`parse()` 补上 windows 与 freestanding 的填充,记为填充。实测: + +``` +--target x86_64-windows Target x86_64-windows → x86_64-w64-windows-gnu 无加注 +--target x86_64-windows-gnu Target x86_64-windows-gnu → … (gnu selects …) 有加注 +--target riscv64-none Target riscv64-none → riscv64-none-elf 无加注 +--target riscv64-none-elf Target riscv64-none-elf (elf selects …) 有加注 +``` + +⭐ **省略即无加注,写出才解释** —— 加注于是退化为「使用者主动提问时的回答」, +而不是对一个本不必出现的词的辩解。 + +⭐ **判据是一个指纹,不是「能构建」。** 同一个二进制、干净的 `target/`, +两种拼法都落进 `target/x86_64-windows-gnu/a9519e720fa9cd0b/`,第二次构建 +0.10s 全缓存命中。若各自产生一个指纹,短拼法就只是「少打四个字符、 +多编译一遍」。e2e `284_env_segment_is_optional_everywhere.sh` 守的正是这一条, +在未修复的 2026.8.24.4 上实测为红。 + +## 5. 缺陷二:`cfg()` 的谓词表与目标表不是同一套词 + +`prepare_inputs.cppm:107` 起的求值器认这些: + +``` +简写 windows | linux | macos | unix +键值 os= | arch= | family= | env= +组合 all(...) | any(...) | not(...) +``` + +⚠️ **没有 `freestanding` / `bare` 谓词。** 一个既要支持宿主又要支持裸机的包 +无法写 `cfg(bare)`,只能逐个列出三元组 —— 而那正是「按宿主分的支」这类 +缺陷的温床。裸机行在表里有 `bare` 这个 `note`,`cfg()` 却读不到它。 + +⚠️ **`env=` 被暴露为一个 cfg 轴**,而第 4 节说明那个轴按平台变化。 +`cfg(env = "gnu")` 在 Linux 上意思是 glibc,在 Windows 上意思是 Itanium ABI。 +同一个谓词,两个语义。 + +## 6. 缺陷三:`toolchain list` 的 TOOLCHAIN 列在 openkal 下误导 + +``` + 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` 无关。 + +⭐ 这不是显示错误,是**列表回答的问题已经不是使用者要问的问题**。 +载荷模型下「目标 → 工具链」是函数;图模型下不是。 + +## 7. 缺陷四:三元组字面量散落在十个文件 + +``` +src/toolchain/triple.cppm 16 处 ← 表本身,应当 +src/freestanding/target.cppm 5 处 ← 第二张表,应当 +src/toolchain/model.cppm 2 处 +src/platform/elf_runtime.cppm 2 处 +src/pack/pipeline.cppm 2 处 +src/pack/manifest_emit.cppm 2 处 +src/toolchain/registry.cppm 1 处 +src/pack/pack.cppm 1 处 +src/pack/abi_tag.cppm 1 处 +src/config.cppm 1 处 +``` + +⚠️ 表外的 **12 处**是加一个新目标时容易漏掉的地方,而漏掉不会有编译错误。 + +--- + +## 8. 优化方案 + +按「改动面 / 收益」排序,每条都给判据。 + +### A. 让第三段在每个平台都可省略 ⭐⭐ ✅ 已实现,见 §4.2 + +`parse()` 中补上 windows 与 freestanding 的填充,并把填充记为填充 +(`envExplicit = false`): + +```cpp +if (t.os == "linux" && t.env.empty()) t.env = "gnu"; +if (t.os == "windows" && t.env.empty()) t.env = "gnu"; // 新增 +if (t.is_freestanding() && t.env.empty()) t.env = "elf"; // 新增 +``` + +⚠️ **填 `gnu` 而不是宿主自己的 env。** `host_triple()` 在 Windows 上答 +`msvc`,按它填会让同一条命令在不同宿主上产生不同的输出目录与缓存键 —— +目标的身份不允许依赖于它在哪里被构建。`gnu` 是每台宿主都能到达的那一行。 + +**判据**: +- `mcpp build --target x86_64-windows` 成功,且产物与 `-gnu` 的 strip 后一致 +- 报告标题显示 `x86_64-windows`(写什么显示什么),**不出现** §4.1 的加注 +- `--target x86_64-windows-msvc` 仍选中 MSVC ABI + +**收益**:openkal 使用者再也不必写 `gnu`。§4.1 的加注退化为只在 +使用者**主动**写出第三段时出现 —— 那时它才是一句有用的解释。 + +### B. `cfg(bare)` 谓词 ⭐ 应做 + +把目标表已有的 `note == "bare"` 暴露给 cfg 求值器: + +```toml +[target."cfg(bare)"] +sysroot = "" +[dependencies."cfg(not(bare))"] +some-hosted-lib = "1" +``` + +**判据**:一个同时支持宿主与三种裸机三元组的包,其清单中三元组字面量 +数量降为零。 + +⚠️ 不要同时引入 `cfg(hosted)`:`not(bare)` 已经表达它,两个名字表达 +同一件事会让人猜它们是否有区别。 + +### C. 两张表的一致性由机器保证 ⭐ 应做 + +加一条单元测试:`kKnownTargets` 中 `note == "bare"` 的每一行,必须在 +`freestanding::kTable` 中有对应行,反之亦然。 + +**判据**:删掉 `kTable` 中任意一行,该测试失败。 + +**成本**:约 15 行。**这是本文投入产出比最高的一条。** + +### D. `toolchain list` 区分「载荷能服务」与「图能供给」 ⭐⭐ 值得做 + +当前的 TOOLCHAIN 列在图模型下答非所问(§6)。方案不是删掉它,而是让 +STATUS 列说明来源: + +``` + TARGET NOTE TOOLCHAIN STATUS + x86_64-windows-gnu PE, static, cross gcc 16.1.0 installed (payload) + x86_64-windows-gnu PE, static, cross llvm 22.1.8 via graph +``` + +⚠️ **这需要解析依赖图**,而 `toolchain list` 目前不解析。若代价过大, +退而求其次:在表头加一行说明「TOOLCHAIN 列是载荷路径;由依赖图供给 +目标侧的工程不使用它」。**一句准确的说明胜过一列精确的谎话。** + +**判据**:在 openkal 工程目录下执行 `mcpp toolchain list`,输出不再暗示 +该工程会使用 `gcc 16.1.0`。 + +### E. 把表外的 12 处三元组字面量收敛 ⭓ 可做可不做 + +逐个分类(§7)。有些是合理的(`elf_runtime.cppm` 判断 ELF 解释器路径), +有些是可以查表的。 + +⚠️ **先分类再决定,不要一次性重构。** 今天没有任何缺陷是由这 12 处引起的; +把它当成「加新目标时的检查清单」比当成技术债更贴近事实。可以先只加一条 +测试:新目标进表时,断言这 12 处不需要跟着改 —— 若需要改,那一处就是 +真正的耦合点。 + +### F. 明确拒绝的事 + +- **不引入 `--target` 的别名机制**(如 `--target windows`)。三元组是身份, + 别名会让同一目标有两个目录名。 +- **不让填充依赖宿主**(见 A 的警告)。 +- **不把 `env` 段的轴写进语法**(如 `x86_64-windows-abi:gnu`)。那是 + LLVM 的词表,mcpp 只能接受它;要减少的是**使用者不得不写它的次数**, + 不是重新发明拼写。 + +--- + +## 9. 依赖关系与落地顺序 + +``` +C(两表一致性测试) ← 独立,先做,15 行 + │ +A(第三段可省略) ← 独立于 C,但两者都动 triple.cppm,顺序做避免冲突 + │ + ├─► §4.1 的加注自动退化为「只在主动写出时出现」 + │ +B(cfg(bare)) ← 依赖 A 吗?不依赖。可并行 + │ +D(toolchain list) ← 独立;若做完整版则依赖图解析,代价最大 + │ +E(字面量收敛) ← 最后,且先分类 +``` + +## 10. 本文刻意没有断言的事 + +- **没有断言 A 的改动面。** `parse()` 的填充影响 `envExplicit`,而后者 + 在 §1 的七个读取点里有三个会读到。⚠️ 实施前必须先跑一次全量 e2e 的 + Windows 分片,而不是只看单元测试 —— 2026.8.24.3 那次同类改动 + (Linux 的填充)是靠 e2e 抓到回归的。 +- **没有断言 D 的完整版是否值得。** 它要求 `toolchain list` 解析依赖图, + 那会让一条只想看看有哪些目标的命令变慢。退化方案(表头说明)的收益 + 可能已经占了八成。 +- **没有断言 §7 的 12 处里有几处是真耦合。** 那是一次分类工作, + 本文只给出计数。 diff --git a/CHANGELOG.md b/CHANGELOG.md index bda290c3..ad23ec93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,49 @@ ### 改进 +- **⭐ `-` 现在在每个平台上都是一个完整的目标。** + + ``` + mcpp build --target x86_64-windows # 此前:error: unknown target + mcpp build --target riscv64-none # 此前:error: unknown target + ``` + + 2026.8.24.3 立的规矩是「三元组既是身份也是请求,而请求必须能什么都不说」, + **而它只在 Linux 与 macOS 上成立**。被强制写出第三段的两个平台 + (windows、none),恰恰是构建期体系最常用的两个。 + + ⭐ **真正的分界是两种体系,而不是平台。** + + | | 传统预构建体系 | 构建期体系 | + |---|---|---| + | 目标侧来自 | 工具链**载荷** | 依赖**图**,从源码构建 | + | 第三段 | **挑选载荷**,解析期承重 | **不选中任何东西**,图已决定 | + | 该不该写 | 该写,它是作出选择的方式 | 不该写,它陈述一个不被查询的请求 | + + 在构建期体系下,`x86_64-windows` 不只是比 `x86_64-windows-gnu` 短 —— + 它**更准确**,因为那次构建里编译器是 clang、链接器是 lld、C 库是 musl、 + C++ 运行时是 libc++,**没有任何东西是 GNU 的**。 + + ⚠️ **填 `gnu` 而不是宿主自己的 env。** `host_triple()` 在 Windows 上答 + `msvc`,按它填会让同一条命令在不同宿主上产生不同的输出目录与缓存键 —— + 目标的身份不允许依赖于它在哪里被构建。 + + ⭐ **判据是一个指纹,不是「能构建」。** 同一个二进制、干净的 `target/`, + 两种拼法都落进 `target/x86_64-windows-gnu/<同一指纹>/`,第二次构建 + 0.10s 全缓存命中。若各自产生一个指纹,短拼法就只是「少打四个字符、 + 多编译一遍」。 + + 连带:下面那条加注退化为**只在使用者主动写出第三段时出现** —— + 那时它才是一句有用的回答,而不是对一个本不必出现的词的辩解。 + +- **两张按三元组索引的表,一致性现在由机器保证。** + + `kKnownTargets` 说某行存在、属于哪一档、C 库由谁供给; + `freestanding::kTable` 说它的 ISA 档与链接如何驱动。此前**没有任何东西 + 检查这两份描述覆盖同一批行**:只在前者里的行会一路走到代码生成才失败, + 且错误信息谈的是 flag 而不是缺行;只在后者里的行是读起来像支持的死数据。 + 实测删掉 `kTable` 任意一行,新增的两条测试立刻转红。 + - **三元组的 env 段在每个平台上命名不同的轴,而报告此前只是对此保持沉默。** ``` @@ -17,7 +60,6 @@ ⭐ 沉默作为**诊断**是对的 —— 在 Windows 上报「名字请求了 `gnu` C ABI」 会在每一次合法的 MinGW 构建上出现,而且说的是错的。作为**报告**则不够: 读者在其中找不到一行叫 `gnu`,于是把它映到最像 C 库名字的那一行。 - 它真正对应的是 `c++-abi`。 于是在该段不命名 C 库的平台上,报告直接说出它选中的是什么: @@ -49,6 +91,12 @@ ⚠️ 该提示**不出现**在 C 库来自载荷时:那种情况下 C 库正是三元组选中的, `gnu → ucrt` 是可见的因果,加注就成了每次普通 Windows 构建上的噪声。 +### 文档 + +- 新增[第 16 章 —— 目标三元组](docs/16-the-target-triple.md)(中英双份): + 三段各是什么、第三段在每个平台上命名的是不同的轴、两种体系的分界、 + 以及该用哪种拼法。 + ## [2026.8.24.4] — 2026-08-24 ### 修复 diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md new file mode 100644 index 00000000..097a279e --- /dev/null +++ b/docs/16-the-target-triple.md @@ -0,0 +1,161 @@ +# The Target Triple + +A target triple is written `-` or `--`. This chapter +states what each segment means, when the third may be declined, and why the +answer differs between two systems that mcpp supports at the same time. + +## Two Systems, One Spelling + +mcpp resolves the target side — the platform interface, the C library, the +compiler runtime and the C++ runtime — in one of two ways, and a project +usually uses one of them without choosing explicitly. + +**The prebuilt system.** A toolchain payload is built for one target and +carries that target's C library with it. Selecting `x86_64-linux-musl` selects +the musl-gcc payload; selecting `x86_64-linux-gnu` selects a glibc one. The +triple's third segment is load-bearing at resolution time, because it is how +the payload is chosen. + +**The build-time system.** The target side arrives as packages in the +dependency graph and is compiled from source by whichever compiler is running. +The third segment selects nothing, because the graph has already decided. This +is what [chapter 15](15-openkal-cross.md) describes. + +The two differ in what the third segment *does*, not in how it is spelled. A +project does not declare which system it is in; the dependency graph decides, +and the build reports what it resolved. + +## The Segments + +| Segment | Content | Example | +|---|---|---| +| `arch` | instruction set | `x86_64`, `aarch64`, `riscv64` | +| `os` | operating system, or `none` | `linux`, `windows`, `macos`, `none` | +| `env` | see below — it is a different axis per platform | `gnu`, `musl`, `msvc`, `elf` | + +The third segment is the one that repays attention, because it does not name +the same kind of thing everywhere: + +| Platform | `env` names | Values | +|---|---|---| +| `linux` | the **C library** | `gnu` (glibc), `musl` | +| `windows` | the **object ABI** | `gnu` (Itanium C++ ABI), `msvc` (Microsoft's) | +| `none` | the **object format** | `elf` | +| `macos` | nothing; the platform carries no segment | — | + +On Windows the segment is frequently misread, because the word `gnu` suggests a +C library that is not there. Measured on an artefact built for +`x86_64-windows-gnu` over the build-time system: + +| Observation | Value | +|---|---| +| imported libraries | `ntdll`, `KERNEL32`, `SHELL32` — no `msvcrt`, no `ucrtbase` | +| Itanium-mangled symbols (`_Z…`) | 4507 | +| MSVC-mangled symbols (`?…`) | 0 | + +Nothing GNU is present: the compiler is clang, the linker lld, the compiler +runtime compiler-rt, the C library musl, the C++ runtime libc++, the platform +openkal. `gnu` is LLVM's label for the non-MSVC ABI, inherited from MinGW, and +clang requires that spelling to select the right internal toolchain. mcpp +cannot rename it. + +## Declining The Third Segment + +`-` is a complete target on every platform: + +```bash +mcpp build --target x86_64-linux # = x86_64-linux-gnu +mcpp build --target x86_64-windows # = x86_64-windows-gnu +mcpp build --target riscv64-none # = riscv64-none-elf +mcpp build --target aarch64-macos # macOS has no segment to decline +``` + +Declining it changes no identity. The output directory, the cache key and the +subject of a `cfg()` predicate are all the canonical form, so the two spellings +share one fingerprint and the second build is a cache hit rather than a second +full build. + +What differs is the record of what was asked for. A triple serves as an +identity, which must be total, and as a request, which must be able to say +nothing; mcpp keeps both, filling the segment for the identity while recording +that the fill was a fill. + +### Which Spelling To Use + +**Under the build-time system, decline it.** The graph supplies the C library +and the runtimes, so the segment states a request that is not consulted. Under +this system `x86_64-windows` is not merely shorter than `x86_64-windows-gnu` — +it is more accurate, because nothing GNU is involved. + +**Under the prebuilt system, write it when the choice matters.** +`x86_64-linux-musl` and `x86_64-linux-gnu` select different payloads and produce +different artefacts. Writing the segment is how that choice is made. + +**On Windows, write `msvc` when Microsoft's ABI is wanted.** `gnu` is the +default fill, and `msvc` is a different object ABI rather than a different C +library, so the segment is meaningful there in both systems. + +## What The Build Reports + +The report heads with the target as written and resolves it to the compiler's +own spelling: + +``` + Target x86_64-windows → x86_64-w64-windows-gnu + kernel-abi openkal (openkal-windows@0.1.3, graph) + c-abi musl (openkal-musl@0.3.3, graph) + c++-abi libc++ (openkal-llvm-runtime@0.1.2, graph) +``` + +Two diagnostics attach to the third segment, and which one applies follows from +the table above. + +**Where the segment names a C library and the graph supplies a different one, +the build reports the disagreement.** The graph decides, so this is a report +rather than a refusal — the artefact is the same either way, and only the name +is inaccurate: + +``` +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 +``` + +**Where the segment names something else, the report says what.** A warning +would be wrong there: it would fire on every legitimate MinGW build and would +describe an axis the name never addressed. The gloss appears only when the +segment was written out, and names the ABI itself rather than any layer: + +``` +Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium C++ ABI, not a C library) +``` + +That correspondence is to no row of the report, and the absence is the point. +The five layers record who **supplies** each layer; the segment names a +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. + +## Custom Targets + +A triple outside mcpp's table needs an explicit section, which is also how a +board declares facts no default can supply: + +```toml +[target.riscv64-none-elf] +sysroot = "" +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +`sysroot = ""` selects the zero-libc tier: no C library on the compile line and +none on the link. An absent `sysroot` key is a different answer — it inherits +the target row's own default. See [chapter 13](13-baremetal.md). + +## Reference + +[chapter 14](14-target-side.md) for the five layers and who supplies them. +[chapter 15](15-openkal-cross.md) for the build-time system in full. +[chapter 03](03-toolchains.md) for the toolchain axis, which is separate: a +target does not determine a compiler. diff --git a/docs/README.md b/docs/README.md index 1d3d3c72..1487c073 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ - [13 - Bare-Metal and Freestanding Targets](13-baremetal.md) - [14 - The Target Side](14-target-side.md) - [15 - Cross-Compilation Over openkal](15-openkal-cross.md) +- [16 - The Target Triple](16-the-target-triple.md) ## Specifications diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md new file mode 100644 index 00000000..4daab979 --- /dev/null +++ b/docs/zh/16-the-target-triple.md @@ -0,0 +1,141 @@ +# 目标三元组 + +目标三元组写作 `-` 或 `--`。本章说明每一段的含义、 +第三段何时可以省略,以及为何这个答案在 mcpp 同时支持的两种体系下并不相同。 + +## 两种体系,同一套拼写 + +mcpp 解析目标侧 —— 平台接口、C 库、编译期运行时与 C++ 运行时 —— 有两条路径, +而一个工程通常在没有显式选择的情况下就落在其中之一。 + +**传统预构建体系。** 一份工具链载荷为一个目标而构建,并随身携带那个目标的 +C 库。选中 `x86_64-linux-musl` 就是选中 musl-gcc 载荷,选中 +`x86_64-linux-gnu` 就是选中一份 glibc 的。三元组的第三段在解析期是承重的, +因为它正是挑选载荷的方式。 + +**构建期体系。** 目标侧以包的形式出现在依赖图中,由正在运行的那个编译器 +从源码构建。第三段不选中任何东西,因为图已经决定了。这是 +[第 15 章](15-openkal-cross.md)所描述的体系。 + +两者的差别在于第三段**做什么**,而不在于它怎么拼。工程不声明自己属于哪一种; +由依赖图决定,构建则报告它解析出了什么。 + +## 各段 + +| 段 | 内容 | 例 | +|---|---|---| +| `arch` | 指令集 | `x86_64`、`aarch64`、`riscv64` | +| `os` | 操作系统,或 `none` | `linux`、`windows`、`macos`、`none` | +| `env` | 见下 —— 它在每个平台上是不同的轴 | `gnu`、`musl`、`msvc`、`elf` | + +第三段值得留意,因为它在各处命名的并不是同一类东西: + +| 平台 | `env` 命名 | 取值 | +|---|---|---| +| `linux` | **C 库** | `gnu`(glibc)、`musl` | +| `windows` | **对象 ABI** | `gnu`(Itanium C++ ABI)、`msvc`(微软的) | +| `none` | **对象格式** | `elf` | +| `macos` | 无;该平台不带这一段 | — | + +⭐ 在 Windows 上这一段经常被读错,因为 `gnu` 这个词暗示了一个并不在场的 C 库。 +对一份按构建期体系为 `x86_64-windows-gnu` 构建的产物实测: + +| 观测 | 值 | +|---|---| +| 导入的库 | `ntdll`、`KERNEL32`、`SHELL32` —— 无 `msvcrt`,无 `ucrtbase` | +| Itanium 修饰符号(`_Z…`) | 4507 | +| MSVC 修饰符号(`?…`) | 0 | + +没有任何 GNU 的东西在场:编译器是 clang,链接器是 lld,编译期运行时是 +compiler-rt,C 库是 musl,C++ 运行时是 libc++,平台是 openkal。`gnu` 是 +LLVM 词表里「非 MSVC 的那套 ABI」的标签,继承自 MinGW,而 clang 需要这个 +拼写来选中正确的内部工具链。mcpp 改不了它。 + +## 省略第三段 + +`-` 在每个平台上都是一个完整的目标: + +```bash +mcpp build --target x86_64-linux # = x86_64-linux-gnu +mcpp build --target x86_64-windows # = x86_64-windows-gnu +mcpp build --target riscv64-none # = riscv64-none-elf +mcpp build --target aarch64-macos # macOS 本来就没有这一段可省 +``` + +省略它不改变任何身份。输出目录、缓存键与 `cfg()` 谓词的主语都取规范形式, +因此两种拼法共用一个指纹,第二次构建是缓存命中而不是又一次完整构建。 + +不同的是**请求被记录成了什么**。三元组既是身份 —— 身份必须完整 —— +也是请求 —— 请求必须能什么都不说;mcpp 两者都保留:为身份填上那一段, +同时记住这次填充是一次填充。 + +### 该用哪种拼法 + +**在构建期体系下,省略它。** 图供给 C 库与各运行时,那一段陈述的是一个 +不会被查询的请求。在这种体系下,`x86_64-windows` 不只是比 +`x86_64-windows-gnu` 短 —— 它更准确,因为并没有任何 GNU 的东西参与。 + +**在传统预构建体系下,当这个选择有意义时写出它。** +`x86_64-linux-musl` 与 `x86_64-linux-gnu` 选中不同的载荷、产出不同的产物。 +写出那一段正是作出这个选择的方式。 + +**在 Windows 上,想要微软那套 ABI 时写 `msvc`。** `gnu` 是默认填充, +而 `msvc` 是不同的对象 ABI 而非不同的 C 库,因此那一段在两种体系下都有意义。 + +## 构建报告了什么 + +报告以写下的目标为标题,并把它解析为编译器自己的拼写: + +``` + Target x86_64-windows → x86_64-w64-windows-gnu + kernel-abi openkal (openkal-windows@0.1.3, graph) + c-abi musl (openkal-musl@0.3.3, graph) + c++-abi libc++ (openkal-llvm-runtime@0.1.2, graph) +``` + +有两条诊断挂在第三段上,适用哪一条由上面那张表决定。 + +**当该段命名 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 +``` + +**当该段命名的是别的东西时,报告说出那是什么。** 在那里发警告是错的: +它会在每一次合法的 MinGW 构建上出现,并且描述了一条名字从未涉及的轴。 +该提示只在使用者主动写出那一段时出现,且命名那套 **ABI 本身**而非任何一层: + +``` +Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium C++ ABI, not a C library) +``` + +⭐ 而它**不对应报告里的任何一行**,这正是要点。五层记录的是每一层 +**由谁供给**;那一段命名的是这些**对象遵循哪套约定**,是若干层必须一致的 +横切事项。把它读成 `c++-abi libc++` 是第二个错误答案,因为 libstdc++ +坐在同一套 ABI 上。 + +## 自定义目标 + +不在 mcpp 表内的三元组需要一个显式段落,而这也是一块板子声明 +「任何默认值都给不出的事实」的方式: + +```toml +[target.riscv64-none-elf] +sysroot = "" +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +`sysroot = ""` 选定零 libc 档:编译行上没有 C 库,链接上也没有。 +⚠️ **缺席 `sysroot` 键是另一个答案** —— 它继承该目标行自己的默认值。 +见[第 13 章](13-baremetal.md)。 + +## 参考 + +[第 14 章](14-target-side.md)讲五层以及每一层由谁供给。 +[第 15 章](15-openkal-cross.md)完整讲构建期体系。 +[第 03 章](03-toolchains.md)讲工具链轴,它是分开的:目标不决定编译器。 diff --git a/docs/zh/README.md b/docs/zh/README.md index ab490139..0a04259f 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -18,6 +18,7 @@ - [13 - 裸机与 freestanding 目标](13-baremetal.md) - [14 - 目标侧](14-target-side.md) - [15 - 基于 openkal 的交叉构建](15-openkal-cross.md) +- [16 - 目标三元组](16-the-target-triple.md) ## 规范文档 diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm index 5b09a0b1..7e9aff10 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -557,6 +557,31 @@ std::optional parse(std::string_view s) { // the request `x86_64-linux-gnu`, which names a C library. See // `Triple::envExplicit`. if (t.os == "linux" && t.env.empty()) t.env = "gnu"; + // ⚠️ AND THE SAME ON WINDOWS AND ON BARE METAL, WHICH WERE MISSING AND MADE + // THE RULE A LIE ON TWO PLATFORMS OUT OF FOUR. + // + // `x86_64-linux` parsed and `x86_64-windows` did not — `unknown target` — + // so "a request must be able to say nothing" held on Linux and macOS and + // not elsewhere. The asymmetry forced every Windows cross build to spell + // `gnu`, and under this ecosystem that word describes nothing present in + // the build: the compiler is clang, the linker lld, the compiler runtime + // compiler-rt, the C library musl, the C++ runtime libc++, the platform + // openkal. It is LLVM's label for the non-MSVC ABI, inherited from MinGW, + // and mcpp cannot rename it — but it can stop requiring it to be typed. + // + // ⚠️ `gnu` AND NOT THE HOST'S OWN ENV. `host_triple()` answers `msvc` on a + // Windows machine, and filling from it would give one command a different + // identity — a different output directory and cache key — on each host. A + // target's identity may not depend on where it was built. `gnu` is the row + // reachable from every host and the one this ecosystem targets; a project + // wanting Microsoft's ABI writes `msvc`, and writing it there is meaningful + // because it selects a different object ABI rather than a different C + // library. + if (t.os == "windows" && t.env.empty()) t.env = "gnu"; + // On a target with no operating system the segment names the object format, + // and every row in the table carries `elf`. Filling it lets `riscv64-none` + // mean what it plainly says. + if (t.is_freestanding() && t.env.empty()) t.env = "elf"; return t; } diff --git a/tests/e2e/284_env_segment_is_optional_everywhere.sh b/tests/e2e/284_env_segment_is_optional_everywhere.sh new file mode 100755 index 00000000..1469ec7c --- /dev/null +++ b/tests/e2e/284_env_segment_is_optional_everywhere.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# requires: gcc +# `arch-os` is a target on every platform, not only on Linux. +# +# ⚠️ WHY THIS WAS ASYMMETRIC AND WHY THE ASYMMETRY COST SOMETHING. +# +# A target triple states a REQUEST, and a request must be able to say nothing. +# `x86_64-linux` was accepted on that basis; `x86_64-windows` was rejected with +# `unknown target`, and `riscv64-none` likewise. The two platforms where the +# segment could not be declined are exactly the two where it names something +# other than a C library — the object ABI on Windows, the object format on bare +# metal — so a project was required to write a word describing nothing it had +# chosen. Under a graph-supplied target side that word is `gnu`, while the +# compiler is clang, the linker lld, the C library musl and the C++ runtime +# libc++: nothing in the build is GNU. +# +# ⭐ THE ASSERTION IS ON THE IDENTITY, NOT ONLY ON THE EXIT STATUS. Accepting the +# short spelling is worth nothing if it produces a second output directory or a +# second fingerprint: the project would then build twice, cache nothing, and the +# two artefacts could drift. The identity must stay total — `x86_64-windows` IS +# `x86_64-windows-gnu` — while only the RECORD of what was asked for differs. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +cd "$work" + +mkdir -p app/src +cat > app/mcpp.toml <<'TOML' +[package] +name = "envopt" +version = "0.1.0" +TOML +cat > app/src/main.cpp <<'CPP' +#include +int main() { std::printf("ok\n"); } +CPP +cd app + +# ── The pair under test, per platform ──────────────────────────────────────── +# Each is "short spelling" and "the canonical form it must resolve to". +check_pair() { + short="$1"; canonical="$2" + + rm -rf target + if ! out="$("$MCPP" build --target "$short" 2>&1)"; then + case "$out" in + *"unknown target"*) + echo "FAIL: \`$short\` was rejected" + printf '%s\n' "$out" | head -3 + return 1 ;; + *) echo "SKIP: $short cannot build here (no payload/toolchain)" + return 0 ;; + esac + fi + + # One directory, named for the CANONICAL form. + if [ ! -d "target/$canonical" ]; then + echo "FAIL: \`$short\` did not build into target/$canonical" + echo " got: $(ls target 2>/dev/null | tr '\n' ' ')" + return 1 + fi + fp_short="$(ls "target/$canonical" | head -1)" + + # ⭐ The load-bearing step: the long spelling must land on the SAME + # fingerprint, which is what makes the second build a cache hit rather than + # a second full build. + "$MCPP" build --target "$canonical" >/dev/null 2>&1 || { + echo "FAIL: canonical \`$canonical\` failed after \`$short\` succeeded" + return 1; } + n="$(ls "target/$canonical" | wc -l)" + if [ "$n" != "1" ]; then + echo "FAIL: the two spellings produced $n fingerprints, not one" + echo " $(ls "target/$canonical" | tr '\n' ' ')" + return 1 + fi + + # And the report heads with what was WRITTEN, so a reader is not told they + # asked for something they declined. + case "$out" in + *"Target $short"*) : ;; + *) echo "FAIL: the report did not head with \`$short\`" + printf '%s\n' "$out" | grep -m1 Target + return 1 ;; + esac + + echo " ok $short → $canonical (one fingerprint: $fp_short)" + return 0 +} + +rc=0 +check_pair x86_64-linux x86_64-linux-gnu || rc=1 +check_pair x86_64-windows x86_64-windows-gnu || rc=1 +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" diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index dd929fbd..2958333f 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -430,3 +430,42 @@ TEST(FreestandingTarget, MultilibDirectoriesMatchTheIndexPayloads) { ASSERT_TRUE(t.has_value()); EXPECT_TRUE(mcpp::toolchain::triple::effective_sysroot(*t, nullptr).empty()); } + +// ── The two tables keyed on the same triple must agree ─────────────────────── +// +// ⚠️ A BARE-METAL TARGET IS DESCRIBED TWICE, IN TWO FILES, AND NOTHING WAS +// CHECKING THAT THE TWO DESCRIPTIONS COVER THE SAME ROWS. +// +// `kKnownTargets` (toolchain/triple.cppm) says a row exists, which tier it is +// in and what supplies its C library. `kTable` (freestanding/target.cppm) says +// what its ISA profile is and how its link is driven. A row present in the +// first and absent from the second parses, resolves a toolchain, and fails +// somewhere inside code generation with a message about flags rather than +// about a missing row. The reverse — present in the second, absent from the +// first — is dead data that reads as support. +// +// ⭐ The check is possible only because both are compile-time tables with a +// single read point each. It costs fifteen lines and removes an entire class +// of "added a target, forgot half of it". +TEST(FreestandingTarget, EveryBareRowInTheTargetTableHasAnIsaProfile) { + for (const auto& info : mcpp::toolchain::triple::known_targets()) { + if (info.note != "bare") continue; + auto t = mcpp::toolchain::triple::parse(info.canonical); + ASSERT_TRUE(t.has_value()) << info.canonical; + EXPECT_TRUE(mcpp::freestanding::resolve(*t).has_value()) + << info.canonical + << " is a bare row in kKnownTargets with no row in freestanding::kTable"; + } +} + +TEST(FreestandingTarget, EveryIsaProfileHasABareRowInTheTargetTable) { + for (const auto& spec : mcpp::freestanding::known()) { + auto t = mcpp::toolchain::triple::parse(spec.triple); + ASSERT_TRUE(t.has_value()) << spec.triple; + const auto* info = mcpp::toolchain::triple::find_known_target(*t); + ASSERT_NE(info, nullptr) + << spec.triple << " has an ISA profile but no row in kKnownTargets"; + EXPECT_EQ(info->note, "bare") + << spec.triple << " has an ISA profile but is not marked bare"; + } +} diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 1143023d..12d41b73 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -341,3 +341,62 @@ TEST(Triple, OsFieldIdentifiesFreestanding) { EXPECT_EQ(t->os, "none"); EXPECT_FALSE(t->is_pe()); } + +// ── The env segment may be declined on every platform ──────────────────────── +// +// ⚠️ `x86_64-linux` parsed and `x86_64-windows` did not. The rule "a target +// triple states a REQUEST, and a request must be able to say nothing" therefore +// held on two platforms out of four, and the two where it did not were exactly +// the ones whose segment names something other than a C library — so a user was +// required to type a word that described nothing they had chosen. +// +// The identity stays total: `x86_64-windows` IS `x86_64-windows-gnu`, one output +// directory and one cache key. What differs is `envExplicit`, which records that +// nothing was asked for. +TEST(Triple, TheEnvSegmentMayBeDeclinedOnWindows) { + auto t = parse("x86_64-windows"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(t->str(), "x86_64-windows-gnu"); + EXPECT_EQ(t->env, "gnu"); + EXPECT_FALSE(t->envExplicit); +} + +TEST(Triple, TheEnvSegmentMayBeDeclinedOnBareMetal) { + auto t = parse("riscv64-none"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(t->str(), "riscv64-none-elf"); + EXPECT_TRUE(t->is_freestanding()); + EXPECT_FALSE(t->envExplicit); +} + +// Writing it out is still a request, and still recorded as one. +TEST(Triple, WritingTheSegmentOutIsStillARequest) { + auto g = parse("x86_64-windows-gnu"); + ASSERT_TRUE(g.has_value()); + EXPECT_TRUE(g->envExplicit); + + auto m = parse("x86_64-windows-msvc"); + ASSERT_TRUE(m.has_value()); + EXPECT_EQ(m->env, "msvc"); + EXPECT_TRUE(m->envExplicit); +} + +// ⚠️ THE FILL IS `gnu` AND NOT THE HOST'S OWN ENV. +// +// `host_triple()` answers `msvc` on a Windows machine. Filling from it would +// give one command a different identity — a different output directory and +// cache key — on each host, and a target's identity may not depend on where it +// was built. The two spellings must therefore agree on every machine. +TEST(Triple, TheFillDoesNotDependOnTheHost) { + EXPECT_EQ(parse("x86_64-windows")->str(), parse("x86_64-windows-gnu")->str()); + EXPECT_EQ(parse("riscv64-none")->str(), parse("riscv64-none-elf")->str()); +} + +// macOS declines the segment by having none at all, which was already true and +// is asserted here so the four platforms are covered in one place. +TEST(Triple, MacosCarriesNoSegmentToDecline) { + auto t = parse("aarch64-macos"); + ASSERT_TRUE(t.has_value()); + EXPECT_TRUE(t->env.empty()); + EXPECT_FALSE(t->envExplicit); +}