2026-08-25。本文分析的是 target 体系 —— 三元组的语法与语义、目标表、
目标与工具链的关系、以及使用者必须知道多少才能写出一条 --target。
与 2026-08-24-target-side-design.md 的分界:那份讲目标侧(五层由谁供给),
本文讲目标本身(它是什么、怎么写、被谁读)。两者在 env 段相交,
而那个交点正是本文最大的一处缺陷。
所有断言取自 2026-08-25 的 HEAD(2026.8.24.5 待发)与实测,不取自记忆。
三元组同时是身份、请求、表的键和目录名,四种角色对 「缺席」的要求互相冲突;当前实现只在 Linux 上调和了这个冲突, 其余平台把冲突转嫁给了使用者。
⭐ 本文所有关于「第三段」的讨论,真正的分界在这里,而它此前没有被写下来。
| 传统预构建体系 | 构建期体系 | |
|---|---|---|
| 目标侧来自 | 工具链载荷 | 依赖图,从源码构建 |
| 第三段的作用 | 挑选载荷 —— 解析期承重 | 不选中任何东西 —— 图已经决定 |
x86_64-linux-musl |
选中 musl-gcc 载荷 | 与 x86_64-linux 产出相同 |
| 该不该写 | 该写,它是作出选择的方式 | 不该写,它陈述一个不被查询的请求 |
于是「第三段能不能省略」不是一个可以按平台一刀切的问题:
在构建期体系下它应当总是可省,而在预构建体系下写出它才是准确的。
两者共用同一套语法,所以语法必须允许省略,而由报告说明这次解析
落在哪一种体系里 —— 那正是 (…, graph) 与 (…, payload) 两个来源标记
已经在做的事。
--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/<canonical>/<fingerprint>/
└─► targetside::resolve() env 段作为 C 库**请求**参与五层解析
⭐ 同一个字符串在七处被读,而七处对它的期待不同。 目录名要求它总是完整 (否则同一工程两种拼法产出两棵树);请求要求它能省略(否则无法表达 「我不指定 C 库」);表的键要求它精确匹配。
| 列 | 谁读 | 决定什么 |
|---|---|---|
canonical |
全部 | 表的键;也是输出目录名 |
tier |
lifecycle.cppm:593 |
planned 行在 toolchain list 里显示为不可装 |
note |
同上 | 列表里的说明文字(PE/bare) |
pin |
prepare.cppm |
该目标的 C 库由哪个载荷供给(不是「首选编译器」) |
sysroot |
effective_sysroot |
该行默认的 C 库包;空 = 零 libc 档 |
defaultStatic |
lifecycle.cppm:550 + 链接 |
该目标默认静态链接 |
裸机行的第二张表:march / mabi / mcmodel / libdir / extra /
lldEmulation。键仍是同一个 canonical 三元组。
kKnownTargets 而在 kTable 里缺席(反之亦然),后果是构建走到很深处才失败。
今天没出事,是因为两张表都只有一个人在维护。
写一条 --target 之前,使用者要知道:
- 哪些三元组存在 ——
mcpp toolchain list的 Targets 段 - 第三段写什么 —— 而它在每个平台上是不同的轴
- 能不能省略第三段 —— 答案按平台不同,且没有任何文档说明规则
- 省略之后身份是什么 —— 影响输出目录与缓存键
[target.X]里能写什么 ——toolchain/linkage/runner/cxxRuntime/sysroot五个键
第 2、3 两条是本文的核心缺陷,下一节。
| 平台 | 第三段 | 它命名什么 | 可省略? |
|---|---|---|---|
| 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 改不了它 ——
但能不要求使用者写它。
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」,没有解决「使用者被迫写它」。
一句解释一个本不该出现的词,比让那个词消失要差。
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 上实测为红。
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。
同一个谓词,两个语义。
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 无关。
⭐ 这不是显示错误,是列表回答的问题已经不是使用者要问的问题。 载荷模型下「目标 → 工具链」是函数;图模型下不是。
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 处
按「改动面 / 收益」排序,每条都给判据。
parse() 中补上 windows 与 freestanding 的填充,并把填充记为填充
(envExplicit = false):
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 的加注退化为只在
使用者主动写出第三段时出现 —— 那时它才是一句有用的解释。
把目标表已有的 note == "bare" 暴露给 cfg 求值器:
[target."cfg(bare)"]
sysroot = ""
[dependencies."cfg(not(bare))"]
some-hosted-lib = "1"判据:一个同时支持宿主与三种裸机三元组的包,其清单中三元组字面量 数量降为零。
cfg(hosted):not(bare) 已经表达它,两个名字表达
同一件事会让人猜它们是否有区别。
加一条单元测试:kKnownTargets 中 note == "bare" 的每一行,必须在
freestanding::kTable 中有对应行,反之亦然。
判据:删掉 kTable 中任意一行,该测试失败。
成本:约 15 行。这是本文投入产出比最高的一条。
当前的 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。
逐个分类(§7)。有些是合理的(elf_runtime.cppm 判断 ELF 解释器路径),
有些是可以查表的。
- 不引入
--target的别名机制(如--target windows)。三元组是身份, 别名会让同一目标有两个目录名。 - 不让填充依赖宿主(见 A 的警告)。
- 不把
env段的轴写进语法(如x86_64-windows-abi:gnu)。那是 LLVM 的词表,mcpp 只能接受它;要减少的是使用者不得不写它的次数, 不是重新发明拼写。
C(两表一致性测试) ← 独立,先做,15 行
│
A(第三段可省略) ← 独立于 C,但两者都动 triple.cppm,顺序做避免冲突
│
├─► §4.1 的加注自动退化为「只在主动写出时出现」
│
B(cfg(bare)) ← 依赖 A 吗?不依赖。可并行
│
D(toolchain list) ← 独立;若做完整版则依赖图解析,代价最大
│
E(字面量收敛) ← 最后,且先分类
- 没有断言 A 的改动面。
parse()的填充影响envExplicit,而后者 在 §1 的七个读取点里有三个会读到。⚠️ 实施前必须先跑一次全量 e2e 的 Windows 分片,而不是只看单元测试 —— 2026.8.24.3 那次同类改动 (Linux 的填充)是靠 e2e 抓到回归的。 - 没有断言 D 的完整版是否值得。 它要求
toolchain list解析依赖图, 那会让一条只想看看有哪些目标的命令变慢。退化方案(表头说明)的收益 可能已经占了八成。 - 没有断言 §7 的 12 处里有几处是真耦合。 那是一次分类工作, 本文只给出计数。