2026-08-26 · 两条用户报告的缺陷 + 两条连带发现 + 优化方案(待 review,尚未实施)
前置:2026-08-26-cross-target-implies-graph.md ·
2026-08-25-the-two-layer-predicate-family.md
两处拒绝,都发生在 mcpp 已经知道正确答案之后。
一处知道图要求 llvm 而手上正装着 llvm@22.1.8,却让用户去改全局默认;
一处知道用户没有写 C 库这一段,却拿自己填进去的那一段去查表并宣布不支持。
⭐ 这不是 2026.8.25.x 那个「谓词回答了更窄的问题」的家族。这次谓词问对了, 答案也算对了,只是那个答案没有参与决定。 前者是判据错,后者是判据没接线。
四条,全部在 mcpp 2026.8.26.1 实测,机器接口取值。
$ cat mcpp.toml
[dependencies]
openkal-llvm-runtime = "0.1.3"
$ mcpp build # 全局默认 gcc@16.1.0
build.mcpp compiling
build.mcpp running
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
compiler gcc (16.1.0, payload)
required llvm (required by openkal-llvm-runtime@0.1.3)
Select that compiler — yours outranks mcpp's own default:
mcpp toolchain default llvm
同一台机器,同一个工程,只把编译器换成图要的那个:
$ MCPP_TOOLCHAIN=llvm@22.1.8 mcpp build
Cached openkal-llvm-runtime v0.1.3 (244 units)
Finished dev [unoptimized + debuginfo] in 1.02s
llvm@22.1.8 本来就装着。 这次拒绝没有换来任何信息:它要求的东西已经
在本机,版本也已经确定,mcpp 只是没有去拿。付出的代价是让用户改一处全局状
态(mcpp toolchain default llvm)去满足一个工程的一条依赖。
$ mcpp build --target aarch64-linux
error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
$ mcpp build --target aarch64-linux-musl
Finished dev [unoptimized + debuginfo] in 0.99s
而同一个短拼写在 x86_64 上是文档教的写法 ——
examples/06-openkal-cross/src/main.cpp:4:
// mcpp build --target x86_64-linux Linux, any host
// mcpp build --target aarch64-macos macOS, any host
// mcpp build --target x86_64-windows Windows, any host
这个示例的主题是「一份源码,三台机器」。缺的第四行正是 aarch64-linux,而它
是四个里唯一写不出来的。
机器接口:
| 请求 | data.status |
data.reason |
|---|---|---|
x86_64-linux |
ok |
none |
x86_64-windows |
ok |
none |
aarch64-macos |
ok |
none |
aarch64-linux |
refused |
tier-planned |
$ mcpp why toolchain --target riscv64-linux --format json | jq -r '.diagnostics[].message'
unknown target 'riscv64-linux'
known targets: `mcpp toolchain list`; a custom triple needs an
explicit [target.riscv64-linux] section in mcpp.toml
riscv64-linux-musl 就在 kKnownTargets 里(tier planned)。这个 arch+os
组合是登记过的,消息说它未知。
而且这条拒绝没有记号:
"reason": "other"
refusal::Code 有十二个具名值,unknown target 这条路径一个都没记,于是
--format json 把一个有名字的拒绝报成 other。
同一份文档,同一次调用:
"cLibrary": { "mode": "payload-first", "origin": "payload",
"path": ".../xim-x-glibc/2.44/lib64" },
"layers": [ { "layer": "c-abi", "interface": "musl",
"impl": "openkal-musl@0.3.5", "origin": "graph" } ]产物是哪一个,由产物回答:
$ file target/x86_64-linux-gnu/*/bin/test4
ELF 64-bit LSB executable, x86-64, statically linked
$ readelf -l … | grep -i interpreter → (空)
$ readelf -d … | grep -i needed → (空)
$ nm -C … | grep -c openkal → 11
静态、无解释器、无 NEEDED。glibc 不在产物里。cLibrary 描述的是 payload 的
链接搜索模型,layers 描述的是产物,两者在图供给 C 库时分叉,而 JSON 没有任何
字段说明哪一个管用。
src/build/prepare.cppm 已经有一个「图存在之后再定工具链」的接缝,它的标题就
是这么写的:
// ─── The toolchain, resolved now that the graph exists ──────────────────
// 5055
for (auto const& pkg : packages)
for (auto const& entry : pkg.manifest.provides) // 5074
… graphSuppliesSystem = true;
…
if (auto r = resolve_target_toolchain(); !r) … // 5165这个循环扫 provides。它不扫 requires。 对称的那一半在一千行之后:
for (auto const& entry : pkg.manifest.requires_) // 6153
requirements.push_back(…);
…
if (auto why = tsd::check_requirements(resolvedTargetSide, requirements)) {
refusal::record(refusal::Code::LayerRequirement); // 6290
return std::unexpected(*why);
}同一个 packages 容器,同一种 manifest 字段,一个用来决定,一个只用来
核对。
resolve_target_toolchain() 在 5165 首次被调用。此前没有任何 tc 消费者:
依赖的 build.mcpp 在 5863 编译,根工程的在 6476。⭐ 在接缝里改 tcSpec
不需要重新规划,也不会浪费任何已完成的工作。
对照 6321 那段注释所说的、在 6328 处做不到的事:
tchas been read and mutated at 39 sites between its resolution and this point — re-resolving it here would redo all of them out of order.
那是 6328。5101 不是。同一份注释还写下了正确的方向:
The structural fix is to defer the pin the way the target side itself was deferred — resolve it after the graph, where the question it answers has an answer.
pin 已经这样延后了(targetPinCandidate,5101–5108)。requires 是同一个决
定的第三个输入,只是还没接进来。
TcOrigin 已经把这个问题回答完了(prepare.cppm:454):
export inline bool tc_origin_is_user_explicit(TcOrigin o) {
return o == TcOrigin::ManifestToolchain || o == TcOrigin::TargetSection;
}GlobalDefault 不在其中,并且那段
GlobalDefault 之上 —— 用户跑过
mcpp toolchain default gcc,而 mcpp toolchain default llvm 打印的
(was: gcc@16.1.0) 就是它的收据。mcpp 拒绝改写一个它自己的谓词判定为可改写
的值。
更能说明问题的是:两种来源产生逐字相同的输出。
$ MCPP_TOOLCHAIN=gcc@16.1.0 mcpp build # ManifestToolchain,用户写下的
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
前者应当拒绝(用户写下了 gcc),后者不应当(那是 mcpp 记住的一个默认值)。
TcOrigin 分出的这个差别,在用户能看到的任何地方都不存在。
src/targetside/model.cppm:600–609,拒绝时给出的全部补救办法:
" Select that compiler — yours outranks mcpp's own default:\n"
" mcpp toolchain default {}\n" // ← 全局,影响本机每一个工程
" or, for one target only:\n"
" [target.<triple>]\n"
" toolchain = \"{}\"" // ← 写进工程 manifest两条都是持久状态变更,而触发它们的是一次构建里的一条依赖。第一条尤其错: 一个工程的依赖要 llvm,代价是这台机器上每一个工程的默认编译器都变了。
requires = ["mcpp:compiler=llvm"] 只给族,不给版本。这个问题已经有答案:
mcpp toolchain default llvm 就只给了族,而它解析出了 llvm@22.1.8。
src/toolchain/lifecycle.cppm:1074:
if (auto picked = resolve_version_match(
pkg.ximVersion, list_installed_versions(pkgsDir, pkg.ximName)))⭐ 复用它,不要新写一条。 一个裸族名在 mcpp 里只应该有一种解析方式;写第二 条就是把同一个决定推导两遍,而那是隐性架构债——两处推导今天一致,加新语义时会 变成构建失败。
src/toolchain/triple.cppm:642:
if (t.os == "linux" && t.env.empty()) t.env = "gnu";紧挨着的注释把这件事说得很清楚:
x86_64-linuxis the canonical identityx86_64-linux-gnu… but it is NOT the requestx86_64-linux-gnu, which names a C library.
envExplicit 这个字段就是为了记住这个区别而存在的,prepare.cppm:1631/1640 两
处都在用它:请求的 C 库只在 envExplicit 时被记下,报告名在 !envExplicit 时
去掉那一段。
prepare.cppm:1550)和 unknown 闸(1540)都不看 envExplicit。
const triple::TargetInfo* known = parsed ? triple::find_known_target(*parsed) : nullptr;
…
if (known && known->tier == "planned" && !hasToolchainOverride) { … }*parsed 是补全后的身份。于是:
| 用户问的 | 引擎答的 |
|---|---|
| aarch64 的 Linux 支持吗 | aarch64-linux-gnu 支持吗 |
| riscv64 的 Linux 支持吗 | riscv64-linux-gnu 支持吗(此行不存在) |
第一行答「planned」,第二行答「unknown」。两个答案都对——对的是它们各自被问的 那个问题,而那不是用户问的问题。
补全是词法的:linux→gnu、windows→gnu、freestanding→elf。它在决定时不知道词表里 有什么。把词表按 (arch, os) 分组之后,唯一的分歧集中在两组:
| arch+os | 词表里的行 | 词法默认 | 结论 |
|---|---|---|---|
| x86_64 + linux | gnu verified / musl verified |
gnu | 命中,不变 |
| x86_64 + windows | gnu verified / musl preview / msvc verified |
gnu | 命中,不变 |
| freestanding | 全部 elf |
elf | 命中,不变 |
| aarch64 + linux | musl verified / gnu planned |
gnu | |
| riscv64 + linux | musl planned |
gnu |
⭐ 改动面就是这两格。 其余每一格的词法默认恰好是一个受支持的行,补全结果不 变。
tests/unit/test_toolchain_triple.cpp:67:
EXPECT_EQ(parse("x86_64-linux")->str(), "x86_64-linux-gnu");parse() 必须保持词法、全量、与宿主无关。triple.cppm 的注释为最后一条给了
理由:从宿主的 env 填会让同一条命令在不同机器上得到不同的输出目录和缓存键,
「a target's identity may not depend on where it was built」。
因此补全要作为请求站点的一个独立步骤,而不是改 parse()。词表是编译期数
据,在每台宿主上相同,所以这个步骤仍然与宿主无关。
改动落在 prepare.cppm:5071–5108 这一个块内。
- 在既有的
packages循环里同时扫requires_,取mcpp:compiler=<family>。 - 出现两个不同的 family ⇒ 拒绝,逐字对齐既有的
「ONE SUPPLIER PER LAYER, AND TWO IS AN ERROR RATHER THAN A PICK」
(
prepare.cppm:6056)那条规则的措辞与形状。 - 恰好一个,且与当前
tcSpec的 family 不同:tc_origin_is_user_explicit(tcOrigin)⇒ 不改,走今天的LayerRequirement拒绝。用户写下的东西不被改写。- 否则 ⇒
tcSpec = <family>(裸族名,由 4.5 的既有路径定版本),tcOrigin = TcOrigin::GraphRequirement。
- 与 pin 的关系不需要新的优先级:图要求编译器的场景里,若图同时供给系统,
graphSuppliesSystem已经取消了 pin;若图不供给系统,那正是ConventionUnreplaced(5142)要拒绝的局面,拒绝先发生。
新增 TcOrigin::GraphRequirement,并且:
tc_origin_is_user_explicit()不含它(它是 mcpp 的推导,不是用户的话)。tc_origin_name()给它一句话。- 状态行按
pinReplacedDefault的先例说明缘由:
Resolved llvm@22.1.8 → …/xim-x-llvm/22.1.8/bin/clang++
required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`),
replacing your default gcc@16.1.0 for this project
mcpp why toolchain --format json在data下给出该来源,使 「为什么是 llvm」可被机器回答。
⭐ check_requirements 的建议文本一并改写(§2.3.1)。 拒绝在方案 A 下只剩
一种局面——工程自己写下了相反的编译器——而那种局面里全局默认与本次构建无
关,建议 mcpp toolchain default llvm 是答非所问:改了它,这条拒绝照旧。剩下
的正确补救只有两条,都在工程内:
This project states its own compiler, and that outranks the graph:
[toolchain] default = "gcc@16.1.0" ← 改成 "llvm",或删掉这一行
Removing it lets mcpp take the compiler the graph asks for.
MCPP_NO_AUTO_INSTALL
下走既有的拒绝并带上安装写法。这一条不引入新的网络效应,只是把选择权从用户手里
移到了图上。
⚠️ 本节的结论对了一半,见 §8.1。 首次运行那条分支确实进不去,但write_default_toolchain有三个调用点,另外 两个的条件不是它。以下保留原文,因为它被推翻的方式本身是这份文档的一条判据。
resolve_target_toolchain 在整个 prepare.cppm 里只有两个调用点:
2255: return resolve_target_toolchain(); // 它自己的一次性递归
5165: if (auto r = resolve_target_toolchain(); !r)
也就是说,它整个函数体——包括那条首次运行安装并持久化的分支——都在图之后
才执行。分支链(1850 / 2010 / 2012 / 2061)的最后一格是首次运行,它的进入条
件是 !tcSpec.has_value();而全部三处 write_default_toolchain
(2157 / 2206 / 2546)都在这一格里面。
于是把图的要求写进 tcSpec 的时机(5101)早于首次运行分支被求值:
| 今天 | 方案 A | |
|---|---|---|
| 已有 gcc 默认的机器 | 拒绝,要求改全局默认 | 装/用 llvm,config.toml 不动 |
| 一台什么都没装的机器 | 装 gcc → 持久化 gcc → 再拒绝 | tcSpec 已是 llvm ⇒ 首次运行分支根本不进 ⇒ 直接装 llvm,什么都不写 |
⭐ 「不改全局配置」不是给方案 A 加的一条约束,而是把决定放对位置后的自然结 果。 不需要新的开关,也没有需要有人记得不去碰的写入点——那些写入点位于一条 不再进入的分支上。
~/.mcpp/config.toml 会让下一个不含该依赖的工程继承一个
没人要求过的编译器。E10 是执行这条承诺的那个判据。
新增 triple::resolve_request(Triple&)(或等价的自由函数),仅在
prepare.cppm 的请求站点、且 !envExplicit 时调用,位置在
[target.X] 段查找之前(1521)——查找键必须是解析后的身份。
规则,按序:
- 词法默认命中一个 tier ≠
planned的行 ⇒ 用它。(x86_64-linux → gnu) - 否则,同 (arch, os) 下恰好一个 tier ≠
planned的行 ⇒ 用它。 (aarch64-linux → musl) - 否则(该组为空,或全
planned)⇒ 保留词法默认,让 1540/1550 的既有诊断照旧 触发,但用该组存在的行改写消息(见 4.3)。 - 多个受支持的兄弟行且词法默认不在其中 ⇒ 拒绝并列出候选。今天词表里没有这一 格;规则先写下,免得第一次出现时靠猜。
⭐ 规则 1 让这件事自己退休。 aarch64-linux-gnu 一旦升到 verified,规则 1
先命中,补全自动回到 gnu,不需要有人记得回来删规则 2。
[target.aarch64-linux-gnu] toolchain = … 想提前用 planned 行,它要写全三段。
一个拼写回答一个问题;让短拼写既表示「给我受支持的那个」又表示「并且照顾我的提
前加入段」,是让它同时回答两个。
unknown target在 (arch, os) 组非空时不再使用。riscv64-linux应得到planned那条消息,主语是riscv64-linux-musl。- 消息里引用的三元组必须是用户写下的那个,或明确写成「你写的 X,它指的是
Y」。今天
--target aarch64-linux的报错主语是aarch64-linux-gnu,用户没有 打过这个字符串。 unknown target路径补refusal::record。新增Code::UnknownTarget,--format json的reason从other变成具名值。
cLibrary 与 layers[].c-abi 在图供给 C 库时说的是两件事。两条路可选,倾向
第二条:
- 图供给 c-abi 时,
cLibrary.mode置为一个表示「不适用」的值。 - 保留两者并改名,让字段名说清它们各自回答什么:
payloadLinkModel(payload 的链接搜索模型)与layers[].c-abi(产物里的 C 库)。二者本就不是同一个问题,今天的字段名让它们看起来是。
判据全部走 --format json,不做字符串搜索。
| # | 判据 | 方式 |
|---|---|---|
| E1 | 图要 llvm、全局默认 gcc、llvm 已装 ⇒ status=ok,compiler.family=clang |
why toolchain --format json |
| E2a | 同上,但工程写了 [toolchain] default = "gcc@16.1.0" ⇒ refused / layer-requirement |
同上 |
| E2b | 同上,但工程写了 [target.<triple>] toolchain = "gcc@16.1.0" ⇒ refused / layer-requirement |
同上 |
| E10 | E1 前后 ~/.mcpp/config.toml 的 sha256 相同 |
sha256sum 对照 |
| E11 | 无任何工具链的机器上跑 E1 ⇒ 只装 llvm,不出现 First run … installing gcc |
构建日志 + config.toml 仍无 default |
| E3 | E1 的状态行含图的选择缘由,且 data 里该来源可读 |
同上 |
| E4 | 两个包要求不同 family ⇒ refused,消息同时点名两个包 |
同上 |
| E5 | --target aarch64-linux ⇒ status=ok,产物目录 target/aarch64-linux-musl/ |
why + 目录存在 |
| E6 | --target x86_64-linux ⇒ 产物目录仍是 x86_64-linux-gnu/ (不回归) |
同上 |
| E7 | --target riscv64-linux ⇒ reason=tier-planned,消息主语含 riscv64-linux-musl |
why toolchain --format json |
| E8 | parse("x86_64-linux")->str() == "x86_64-linux-gnu" 仍然成立 |
既有单测,不改 |
| E9 | 图供给 c-abi 时,C 库在 JSON 里只有一个答案 | why + readelf -d 对照 |
toolchain install 之前)。判据是日志里不出现
installing gcc ——「装了 llvm」是恒真的,两条路径都会装 llvm,区别只在有没有
先装一个没人要的 gcc。
- e2e 编号从
299起(现有最大298)。 - E5/E6 在
tests/matrix/scan.sh里有天然位置:短拼写是请求这一列的一个新 取值,expected.tsv增两格,四台构建机各一次。 ⚠️ E5 的产物目录判据要落到目录名,不要落到构建成功。补全错到 gnu 而工具链 仍然能出 aarch64 产物的世界里,只看「构建成功」是恒绿的。
2026.8.25.x 修的那七条是谓词回答了比它被问的更窄的问题。这两条不是:
| 8.25.x 家族 | 本文两条 | |
|---|---|---|
| 谓词 | 问错了 | 问对了 |
| 答案 | 错的 | 对的 |
| 缺陷位置 | 判据本身 | 答案没有接到决定上 |
requires被完整解析、完整核对,只是没有参与选择。envExplicit被完整记录、在报告里被完整使用,只是没有参与查表。
⭐ 两者都是「多存了一个字段而没有多接一根线」。 这种缺陷不会在读判据时被发 现,因为判据是对的;它只在用户问「你既然已经知道了,为什么还要我说一遍」时暴 露。1.1 的用户原话就是这句。
envExplicit 今天的读者是
两处报告和零处决定。
- 不改
parse()的填充。 身份必须全量、词法、与宿主无关(§3.3)。 - 补全不看
host_can_serve,不看图。 目标身份不得依赖构建它的机器。词表是 编译期数据,这是补全能参照的唯一一张表。 - 不让图要求压过用户写下的工具链。
[toolchain]/[target.X]/MCPP_TOOLCHAIN保持今天的拒绝(E2a/E2b)。这是唯一保留拒绝的局面。 - 不写任何持久状态。 不改
~/.mcpp/config.toml,不改工程的mcpp.toml。 图的要求是这一次构建的性质,不是这台机器的性质,也不是这个工程的声明。 §4.1.1:这不是一条需要有人遵守的约束,而是决定放对位置后的结果——那些写入点 位于一条不再进入的分支上。 - 不建议用户去改全局默认。 拒绝仅剩的那种局面里全局默认与本次构建无关, 建议它是答非所问(§4.1 末)。
- 不给短拼写加「提前加入」语义(§4.2)。
- aarch64 的其余目标仍然延缓,见
2026-08-26-aarch64-linux-ecosystem-closure.md。 §4.2 规则 1 保证aarch64-linux-gnu升级后补全自动跟随。
四项 A/B/C/D 全部实施。实施过程推翻了本文的一处论断,并挖出两处本文没有想到 的缺陷。
原文:
⭐ 「不改全局配置」不是给方案 A 加的一条约束,而是把决定放对位置后的自然结 果。 不需要新的开关,也没有需要有人记得不去碰的写入点 —— 那些写入点位于一条 不再进入的分支上。
对的只有一处。首次运行那条分支的条件是 !tcSpec.has_value(),方案 A 让它有
值,于是确实进不去。但 write_default_toolchain 有三个调用点,另外两个的条
件不是它:
| 调用点 | 条件 | A 之后可达? |
|---|---|---|
| 首次运行安装 | !tcSpec.has_value() |
否 |
| Windows 首次运行改道 | windowsGnuFirstRun && tcSpec.has_value() |
是 |
| MSVC 不可用时的修复 | !tc_origin_is_user_explicit(tcOrigin) |
是 |
一台没装工具链的 Windows 机器,构建一个要求 llvm 的工程,会把 llvm 写成这 台机器的默认值,交给之后每一个什么都没要求的工程。
⭐ 修法不是在两处各写一个条件,而是给这条规则一个名字:
tc_origin_may_persist(TcOrigin),两处都调用它,一条单测陈述它。理由写在函数
上方:今天有两处,第三处会由一个没读过这段注释的人写出来,而一个有名字的谓词是
他能找到的东西。
第一版的 TargetPin 拒绝对两种行给同一段建议:「依赖一个供给该目标系统的包,
这样就不需要行里那个载荷了」。
对约定行成立(graphSuppliesSystem 正是取消它的东西),对能力行不成立 ——
targetPinIsCapability 让 pin 无论图供给什么都保持生效。于是那段文字给出的指
示,被它正上方那句话("no other family emits this target")已经否掉了。
⭐ 这与 §2.3.1 是同一条:一条修不好它所印在其下的那次失败的建议,比没有建议更 糟。我在同一个 PR 里,隔着三屏,把自己刚指出的错误又犯了一遍。
why toolchain --format json新增compiler.chosenBy = {origin, requiredBy, replaced}(§4.1 承诺过)。e2e 301 因此改为断言这个字段而不是状态行的措辞。⚠️ jq 的//把false当成缺席。303 用.suppliesTarget // "MISSING"读一个布尔字段,false是一个合法答案却读成了「字段不存在」。 windows-x86_64 上实测报「字段缺失」,而字段就在那里。判据要用has()。⚠️ 301 第一版在错误的目录里量基线:从 runner 的起始目录问why toolchain, 读到的是 mcpp 自己仓库mcpp.toml里写的 gcc,于是选了 llvm 去要求,而实测工程 的全局默认本来就是 llvm —— 全部断言通过,而那条要求什么都没改变。基线必须取自 同一份 manifest 去掉依赖。⚠️ 281 断言的是旧建议。它 grepmcpp toolchain default,而方案 A 之后唯一 还能走到那条拒绝的局面里,那条建议连问题都解决不了。已改成断言新建议并且 断言旧建议不出现。
| 项 | 依赖 | 触及 | 风险 |
|---|---|---|---|
| C(诊断说真话 + 记号) | 无 | prepare.cppm 1540/1550、refusal.cppm |
低,纯消息与记号 |
| B(补全参照词表) | C 先落更好读 | triple.cppm 新函数、prepare.cppm 请求站点 |
低,改动面两格(§3.2) |
| D(JSON C 库自洽) | 无 | doctor.cppm |
低,但改字段名是接口变更,须进 kindVersion 讨论 |
| A(图声明的编译器) | 无 | prepare.cppm 5071–5108、TcOrigin、状态行、why |
中,它改写决定 —— E2 是闸 |
C、B、D 相互独立;A 独立于三者。四项可并行,合入顺序按上表从上到下最易复查。