Skip to content

Commit 09fc2ad

Browse files
committed
feat(targets): the Android rows build, and the two halves of one goal stop disagreeing
`mcpp build --target aarch64-linux-android` produces a real Android artifact with no project vocabulary beyond `--target`: aarch64-linux-android -> ELF 64-bit LSB pie, ARM aarch64, interpreter /system/bin/linker64 x86_64-linux-android -> ELF 64-bit LSB pie, x86-64, same interpreter Both rows share ONE pin, `android-ndk@30.0.16248370`, and that is where every defect below came from: the NDK names no architecture, `--target` does, so each site that fails to say which target is exposed here and by nothing else. On wasm none of them appear, because `em++` has exactly one target. `preview`, not `verified`: both were BUILT and neither was EXECUTED, which is the difference between the two tiers. ## Five engine gaps, each found by the previous one's failure 1. The std module's PRECOMPILE never received `--target`. `stdModuleTargetFlags` reached only the codegen command, on the reading that step one needs headers and step two needs the machine. Step one needs both: a `--precompile` that does not say the target resolves the standard library's own `#include <__config>` against the building machine. This file already recorded the identical error text from a Windows host in 2026-08 -- same cause, different route. The precompile now takes the machine from whichever source carries it, preferring `stdModuleFlags` because it is a superset. 2. `-D__BIONIC_CTYPE_INLINE=`. bionic declares `isalnum` and its neighbours `static inline`; libc++'s module surface exports them with `using std::isalnum`, and a using-declaration cannot export internal linkage, so the precompile fails on 14 names at once. Scoped to the std module: the rule being satisfied is about exporting from a module, and a unit that includes <ctype.h> directly is entitled to bionic's inline definitions. 3. "A toolchain that ships its own sysroot is told nothing" was one token too strong, and this function already said so further down. The early return stood in front of "THE TRIPLE, SAID OUT LOUD", which states the opposite rule for the same reason. Both are right about their own object: the SYSTEM is the payload's, WHICH TARGET is still mcpp's to say. Reported by the module loader rather than by either compile, then eight cascading "use of undeclared identifier 'std'" lines, which is what a reader sees first. 4. The LINK line had no target either. Both link branches are skipped for such a target and that is correct for what they carry -- C library, C++ runtime, crt objects and loader are all inside the SDK. What the driver cannot do is guess which. Falling through empty linked the target's objects with the host's startup files: six host objects on an aarch64 link. The new third branch carries `crossTarget` and nothing else. 5. `discover_link_runtime_dirs`' gate was evaluated at the wrong time. The gate is right; it runs during DETECTION, before the target is assigned, when `targetTriple` is still the host's. The artifact is what showed it: `-L <ndk>/.../prebuilt/linux-x86_64/lib/x86_64-unknown-linux-gnu`, whose last component is this machine's triple, so the string names the question asked. ## Two artifact checks called a correct artifact a defect Same shape both times: a rule true of a HOST-TARGETED artifact, applied to a cross one. - The hermetic link check counted `/system/bin/linker64` as outside the sandbox. It is the one path this function inspects that is NOT resolved here: it is recorded in the artifact and read by the DEVICE. Android's is fixed by ABI and cannot be inside a payload. The message was accurate about what it saw and wrong about what it meant, and it invited the reader to reinstall an unrelated glibc payload. - Runtime closure rule B compared `PT_INTERP` against the HOST's `RuntimeBinding glibc@2.44` and reported a proven defect -- "one process cannot mix runtime payloads" is a true sentence about a process that will never exist. The linux/glibc guard above it does not cover this: an Android triple's `os` IS `linux`, deliberately. ## host_can_serve stops compiling "Linux" into the engine The predicate returned `mcpp::platform::is_linux` for own-sysroot rows, and its own comment named the expiry: "when a darwin or windows NDK lands in the index -- upstream publishes both -- this is the one line that changes." It landed. Two halves of one goal then disagreed: the index published the payload on three hosts while the engine deleted the row from `toolchain list` on two of them. The symptom was not a wrong answer but an ABSENT one -- on macOS, `mcpp build --target wasm32-emscripten` reported a target this table knows as one it had never heard of, which is what the `planned` tier exists to avoid. Neither half is where it was found. `scan (macos-arm64)` and `scan (windows-x86_64)` failed on a CELL COUNT, 24 measured against 25 declared, and the one missing cell named the row. A per-host job comparing against a checked-in table is the only thing here that can see a row disappear. That predicate's unit test was ALSO true by arithmetic on one host: `EXPECT_EQ(host_can_serve(*wasm), mcpp::platform::is_linux)` passes on Linux because there `is_linux` IS `true`. A criterion whose expected value is the host it runs on cannot report a change on the other two. Now stated unconditionally, with the "this predicate can still say no" half restored. ## The capability-pin reason fell through a second time Android became a capability row while the reason chain still had three arms, so it was explained with the PE+musl sentence -- the same wrong answer recorded for wasm, reached the same way: a fourth case falling into an `else` written as the third's answer. So the last arm now names its own row and the fallthrough is generic; a capability added later gets a sentence that is merely unspecific instead of false. Android's reason is not the other three's. They are refused because the toolchain cannot emit the FORMAT; a stock clang emits aarch64 ELF perfectly well. What it cannot supply is bionic, and no package adds that to another compiler. ## The simulator is a row, not a runner `env = "sim"` gives `aarch64-ios-sim` and `x86_64-ios-sim` -- Rust's pair modulo the vendor segment this table already elides. Apple's own `-simulator` spelling parses to the same row, because clang prints that form and a reader who pastes it back must not be told mcpp has never heard of it. Both arches, because the simulator runs the HOST's architecture. Both `planned`: the blocker is the same licence question as the device row, not a payload. ## The API level's default comes from the payload `min_platform_version` returned an empty string with the comment "the NDK's own default, which clang supplies". Never verified, and false: bionic's <sys/cdefs.h> refuses an unversioned triple outright. So the level is mandatory and a project that never heard of API levels still needs one. It comes from the NDK's own `meta/platforms.json` -- `{"min": 21, "max": 37}` for r30 -- so a newer NDK changes the default by being installed rather than by being edited into this file. 0 when unreadable, which the caller turns into a refusal naming `min_api_level`; a guessed level would be worse, because it selects which bionic symbols exist. The same false claim had a second copy in `llvm_triple()`'s comment and is corrected there too. ## Two vocabulary consequences of the rows leaving `planned` - A bare `aarch64-linux` must never complete to Android. The rows share the `arch-os` prefix because the kernel IS Linux; that does not make bionic a candidate C library for a request that named none. `aarch64-linux` had two supported siblings and resolved as ambiguous, where it had completed to `aarch64-linux-musl` before, and both outcomes of that ambiguity are wrong. Excluded from `siblings` too, since that list is what the diagnostic prints. - `aarch64-unknown-linux-android21` now parses. mcpp PRINTS that string one line above the build it describes, and pasting it back answered `unknown target`, because the env match read `k == "android"` while the API level rides that segment. The msvc branch below carries the identical note for the identical reason; Android was missed. One prefix covers all four spellings. ## Tests - `tests/e2e/641` asserts the vocabulary half, which is what a runner without a 704 MB NDK can assert: both rows name the payload, neither is refused for its tier, the simulator rows say `planned` rather than `unknown`, the effective triple parses back, a bare `aarch64-linux` never mentions android, and `min_api_level` has a floor refused by name. - `tests/e2e/640` case 6 is EXHAUSTIVE, because adding an arm is what keeps failing: it takes every pinned row from `toolchain list`, declares a toolchain that is not its pin, and asserts the PE+musl sentence appears for exactly one. Measured: 17 capability rows of 34 targets, PE+musl printed once. The denominator comes from the engine, so a row added tomorrow is in it without this file being edited -- and it agrees with the unit test's independently written set of 17. - `tests/matrix/expected.tsv`: the Android cells move to `capability-pin` and the wasm cells on macOS/Windows move off `host-cannot-serve`, because the refusal is now the row's rather than the host's. `android-ndk` deliberately does NOT enter the declared-compiler column: the scan installs from that axis. Verified locally, 54/54 payload and 28/28 graph cells for linux-x86_64. - 109 unit test binaries pass. Three tests failed first and each was correct to: one asserted the Android rows were planned, one asserted Android was not a capability pin with the note "it moves when the row does", and one asserted a bare `aarch64-linux` completion that two new supported siblings had made ambiguous.
1 parent c30dc74 commit 09fc2ad

19 files changed

Lines changed: 1252 additions & 108 deletions

.agents/docs/2026-09-11-platform-targets-design-review.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -984,15 +984,31 @@ plan.
984984
| E5 | matrix expectations for 12 wasm cells | **done** |
985985
| E6 | unit tests for E1-E4 | **done** |
986986
| E7 | the EOL debian leg swapped for debian-12 | **done** |
987-
| E8 | R8: `parse()` accepts `wasm32-unknown-emscripten`, `aarch64-apple-ios` | todo |
988-
| E9 | R1: `aarch64-ios-sim` and `x86_64-ios-sim` rows | todo |
989-
| E10 | R3: a payload display identity, so emsdk is not shown as `llvm` | todo |
990-
| E11 | the `.wasm` sibling as an implicit link output | todo |
991-
| E12 | R6: Android and iOS rows to `preview` where the evidence supports it | todo |
992-
| E13 | docs: `20-toolchains`, `21-the-target-triple`, `22-target-side` + zh | todo |
993-
| E14 | CHANGELOG | todo |
994-
995-
| E15 | R2: `min_api_level` under `[target.<triple>]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | todo |
987+
| E8 | R8: `parse()` accepts `wasm32-unknown-emscripten`, `aarch64-apple-ios` | **done** -- vendor segments (`unknown`/`pc`/`w64`/`apple`) skipped, `ios`/`iphoneos` and `emscripten` accepted as OS segments |
988+
| E9 | R1: `aarch64-ios-sim` and `x86_64-ios-sim` rows | **done**, `planned` -- the simulator is a target, so it gets its own identity rather than being folded into `aarch64-ios` |
989+
| E10 | R3: a payload display identity, so emsdk is not shown as `llvm` | **done**, `ToolchainSpec::payloadName` |
990+
| E11 | the `.wasm` sibling as an implicit link output | **not attempted** -- recorded in §11 of the decomposition doc; the artifact is produced and found, only the graph does not name it |
991+
| E12 | R6: Android rows to `preview`; iOS stays `planned` | **done** -- `android-ndk@30.0.16248370` pin, measured below. iOS stays `planned` because its blocker is a licence, not a payload |
992+
| E13 | docs: `04-mcpp-toml` §2.7.3, `20-toolchains`, `21-the-target-triple` + zh | **done** |
993+
| E14 | CHANGELOG | **done** |
994+
| E15 | R2: `min_api_level` under `[target.<triple>]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | **done** |
995+
| E16 | `host_can_serve` stops hardcoding Linux for `has_own_sysroot()` rows | **done** -- see below; this is the line the predicate's own comment named as its expiry |
996+
997+
**E16 was not in the original plan, and the target matrix is what produced
998+
it.** The predicate returned `mcpp::platform::is_linux` for a row whose SDK
999+
ships its own sysroot, because `xim:emsdk` and `xim:android-ndk` declared only
1000+
`xpm.linux` when it was written. X2 made that false, and the two halves of one
1001+
goal then disagreed: the index published the payload on three hosts while the
1002+
engine deleted the row from `toolchain list` on two of them. The symptom was
1003+
not a wrong answer but an ABSENT one -- `mcpp build --target wasm32-emscripten`
1004+
on macOS reported a target this table knows as one it had never heard of, which
1005+
is the same defect the `planned` tier exists to avoid.
1006+
1007+
Neither half is where it was found. `scan (macos-arm64)` and
1008+
`scan (windows-x86_64)` failed on a cell count -- 24 measured against 25
1009+
declared -- and the one missing cell named the row. A per-host job comparing
1010+
against a checked-in table is the only thing in this repository that can see a
1011+
row disappear, because every other check asks about a row it already has.
9961012

9971013
R4 is not in that list because it is **withdrawn** (§12.1a), not deferred: its
9981014
motivating case dissolved once `xim:iphoneos-sdk` covered iOS, and relaxing a
@@ -1005,6 +1021,7 @@ refusal without a case is how `gcc@system` gets in.
10051021
| X1 | Android CN mirrors under clause 3.5 | **done**, #812 |
10061022
| X2 | emsdk, NDK and emulator on all three hosts | **done**, hashes verified |
10071023
| X3 | `xim:python` aarch64 and a GLOBAL url | **done** |
1024+
| X3a | the two host assumptions X2 introduced, found by the per-host install jobs | **done** -- the NDK's release-directory pattern named `-linux` only; emsdk's Windows entry points are `.exe`, not the `.bat` the first version guessed |
10081025
| X4 | R7: `xim:rcodesign` | todo |
10091026
| X5 | R12: `xim:pymobiledevice3` | todo |
10101027
| X6 | R5: the two runner programs | todo, and they are new software rather than packaging |
@@ -1017,8 +1034,9 @@ refusal without a case is how `gcc@system` gets in.
10171034
| P2 | R9: `dist-ipa` | todo |
10181035
| P3 | R7's plugin half: prefer `xim:rcodesign` over the host's codesign | todo |
10191036

1020-
**mcpp-index**: publish `mcpp:plugins@0.6.0`. One task, blocked on the tag's
1021-
sha256.
1037+
**mcpp-index**: publish `mcpp:plugins@0.6.0`. **done** -- tag published, the
1038+
GitCode asset verified by download-back and byte-identical to the GitHub source
1039+
archive, so one sha256 names both hosts; PR open.
10221040

10231041
**Recorded and not attempted**: R10 (`dmg`/`pkg` creators), R11 (Darling),
10241042
R13's implementation (the pattern is documented; the runner programs are X6).

CHANGELOG.md

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,167 @@
3434
wasm 同时加入**能力钉**(capability pin):没有别的东西能发出 WebAssembly,所以
3535
声明一个 `gcc@16.1.0` 是一个无法被满足的请求,说出来比解析出 gcc 再在它内部失败要好。
3636

37+
### 两个 Android 行从 `planned``preview`
38+
39+
`mcpp build --target aarch64-linux-android` 产出真实的 Android 产物,工程侧除
40+
`--target` 之外**一个新词汇都不需要**:
41+
42+
```
43+
aarch64-linux-android -> ELF 64-bit LSB pie, ARM aarch64,
44+
interpreter /system/bin/linker64
45+
x86_64-linux-android -> ELF 64-bit LSB pie, x86-64, 同一个 interpreter
46+
```
47+
48+
两行**共用一个钉** `android-ndk@30.0.16248370`。这不是省事,而是这条路径的全部
49+
性质所系:NDK 不命名架构,`--target` 才命名 —— 于是每一处「谁说出目标」的缺口都
50+
会在这里现形,而在 wasm 上都不会,因为 `em++` 只有一个目标。
51+
52+
`preview` 而不是 `verified`:两者都**构建**过,都没有被**执行**过 —— 跑起来需要一台
53+
设备或一个模拟器,而那正是这两个层级的差别。
54+
55+
五处引擎缺口,每一处都是前一处的失败找出来的,而每一处都**只在多目标载荷上现形**:
56+
57+
1. **std 模块的预编译从来拿不到 `--target`** `stdModuleTargetFlags` 只到达
58+
codegen 那一条命令,依据是「第一步要头文件、第二步要机器」。第一步两者都要:
59+
一个不说目标的 `--precompile` 会把标准库自己的 `#include <__config>` 解析到
60+
**正在构建的那台机器**上。而这个文件里早就记着同一句报错 —— 2026-08 一台
61+
Windows 宿主上的 `'__config' file not found` —— 同一个成因,另一条路径到达。
62+
现在预编译从两个来源里**有机器的那一个**取:`stdModuleFlags` 非空时它是
63+
`stdModuleTargetFlags` 的超集,所以取它就不会让 `--target` 上两次命令行。
64+
2. **`-D__BIONIC_CTYPE_INLINE=`** bionic 把 `isalnum` 一族声明为 `static
65+
inline`,而 libc++ 的模块面用 `using std::isalnum` 导出它们 —— using 声明不能
66+
导出内部链接的名字,于是预编译一次性在 14 个名字上失败。范围限定在 std 模块
67+
上:被满足的规则是「从模块里导出」,而一个直接 `#include <ctype.h>` 的翻译
68+
单元有权拿到 bionic 的 inline 定义。`xim:android-ndk` 自己的安装期自检从另一个
69+
方向得到同一个结论。
70+
3. **「自带 sysroot 就什么都不告诉它」比实际强了一个 token。** 那个提前返回
71+
站在 `host_compile_tokens` 里「THE TRIPLE, SAID OUT LOUD」那一段**之前**,而
72+
那一段讲的是相反的规则、理由相同:一个普通 clang 不被告知就为它自己所在的
73+
机器发码。两句话各自对它自己的对象是对的 —— **体系**是载荷的、不可重建;
74+
**是哪个目标**仍是 mcpp 要说的。报出这件事的是模块加载器而不是任何一次编译:
75+
「AST file 'std.pcm' was compiled for the target
76+
'aarch64-unknown-linux-android21' but the current translation unit is being
77+
compiled for target 'x86_64-unknown-linux-gnu'」,后面跟着八条级联的
78+
「use of undeclared identifier 'std'」,而读者先看到的是后者。
79+
4. **链接行同样没有目标。** 两条链接分支都被跳过,而它们携带的东西被跳过是对的:
80+
C 库、C++ 运行期、crt 对象和加载器全在 SDK 里,驱动自己会找。它**不能**猜的
81+
是找哪一个。落空的结果是目标的对象与宿主的启动文件链在一起:六个宿主对象,
82+
每一个都由一个以为自己在为本机构建的驱动解析出来。新增的第三条分支只放
83+
`crossTarget`,别的什么都不放。
84+
5. **`discover_link_runtime_dirs` 的闸在错误的时刻求值。** 那个闸本身是对的 ——
85+
它拒绝为自带 sysroot 的目标报告这些目录 —— 而它跑在**探测期**,比目标被赋值
86+
更早,那时 `targetTriple` 还是宿主的。产物是揭发它的东西:一条 Android 链接行
87+
上带着 `-L <ndk>/.../prebuilt/linux-x86_64/lib/x86_64-unknown-linux-gnu`,
88+
最后一段是本机的 triple,由 `root / "lib" / targetTriple` 产生 —— 那个字符串
89+
写着被问出来的那个问题。
90+
91+
还有两处**产物检查**把一个正确的产物判成了缺陷,而两处的形状相同:一条对**面向
92+
宿主**的产物为真的规则,施加在一个交叉产物上。
93+
94+
- **hermetic 链接检查**`/system/bin/linker64` 算成「sandbox 之外」。它是这个
95+
函数检查的所有路径里唯一**不在本机解析**的那一个:它被记进产物,由**设备**
96+
加载时读取,而 Android 的这条路径由 ABI 规定,不可能在任何载荷里面。消息对
97+
它看到的东西是准确的、对它的含义是错的 —— 这是更难的那一种,它点名了一条确实
98+
在 sandbox 之外的真实路径,并邀请读者去重装一个与此无关的 glibc 载荷。
99+
- **runtime 闭包校验的 rule B**拿产物的 `PT_INTERP`**宿主的** `RuntimeBinding
100+
glibc@2.44` 相比,报「one process cannot mix runtime payloads」—— 一句关于一个
101+
永不会存在的进程的真话,接着建议一个帮不上忙的 SubOS。上方那个 linux/glibc
102+
闸拦不住它:Android 的 triple 的 `os` **就是** `linux`,这是刻意的。
103+
104+
Android 同时加入**能力钉**,而它的理由与另外三个都不同。那三个被拒绝是因为工具链
105+
发不出那个**格式**;一个普通 clang 发 aarch64 ELF 完全没问题。它拿不出来的是
106+
**体系** —— bionic 的头文件、按 API level 的存根、加载器路径都在 NDK 里,没有任何
107+
包能把它们加到另一个编译器上。
108+
109+
### `host_can_serve` 不再把「Linux」编译进引擎
110+
111+
这一行此前对自带 sysroot 的目标返回 `mcpp::platform::is_linux`,而它自己的注释就
112+
写着这个判断的失效条件:「When a darwin or windows NDK lands in the index —
113+
upstream publishes both — this is the one line that changes.」它落地了,所以这就是
114+
那一行。
115+
116+
同一个 goal 的两半随即互相矛盾:索引把载荷发布到三个宿主,而引擎在其中两个上
117+
**把这一行从 `toolchain list` 里删掉**。症状不是一个错答案而是一个**缺席的**答案 ——
118+
macOS 上 `mcpp build --target wasm32-emscripten` 报了一个这张表认识的目标「未知」,
119+
正是 `planned` 这个层级存在的目的所要避免的事。
120+
121+
两半都不是它被发现的地方。`scan (macos-arm64)``scan (windows-x86_64)`**格数**
122+
上失败 —— 测到 24 格、表里声明 25 格 —— 而那唯一缺的一格点名了这一行。一个按宿主
123+
跑、拿一张入库的表作判据的 job,是这个仓库里唯一能看见**一行消失**的东西,因为其他
124+
每一处检查问的都是它已经拿到的那一行。
125+
126+
那条闸的单测同样是**在一台宿主上由算术为真**:它写的是
127+
`EXPECT_EQ(host_can_serve(*wasm), mcpp::platform::is_linux)`,而在 Linux 上
128+
`is_linux` **就是** `true`。一个期望值等于它所运行的宿主的判据,报不出另两个宿主上
129+
的变化。现在它无条件断言,并且补上了「这个谓词仍然说得出『不』」那一半。
130+
131+
### 模拟器是一行,不是一个 runner
132+
133+
一次 iOS 模拟器构建是**另一个目标**:它自己的 SDK(`iPhoneSimulator.sdk`)、自己的
134+
对象,取 `-mios-simulator-version-min` 而真机取 `-miphoneos-version-min`。此前它
135+
无法被拼写,而设备那一行的注释把反对意见写反了 —— 反对的是**没有**一个单独的行,
136+
而不是有。
137+
138+
`env = "sim"` 给出 `aarch64-ios-sim``x86_64-ios-sim`,也就是 Rust 的
139+
`aarch64-apple-ios-sim` 减去这张表本来就省略的 vendor 段。Apple 自己的
140+
`-simulator` 拼法也解析到同一行 —— clang 打印的 effective triple 带的是那一种,而
141+
一个把它粘回来的读者不该被告知 mcpp 从没听说过它。
142+
143+
两个架构都有,理由与 Android 那一对相同:模拟器跑**宿主的**架构,所以一行会描述
144+
一半机器跑不了的模拟器。
145+
146+
两行都是 `planned`,而阻塞项与设备行是同一个**许可**问题,不是一个载荷问题:NDK
147+
Apache-2.0、Emscripten 是 MIT,而 iPhoneOS 与 iPhoneSimulator 的 SDK 在 Xcode 里,
148+
两者都不可再分发。它们今天买到的是:`mcpp build --target aarch64-ios-sim`
149+
`tier-planned` 并点名那一行,而不是答 `unknown target` —— 后者是假的。
150+
151+
### API level 的默认值取自载荷,而「不写」不是一个合法答案
152+
153+
`min_platform_version` 此前在项目没有声明 `min_api_level` 时返回空串,注释写的是
154+
「the NDK's own default, which clang supplies」。那句话从未被验证,而且是错的:
155+
156+
```
157+
--target=aarch64-unknown-linux-android (无级别)
158+
sys/cdefs.h:365:2: error: Unversioned target triples are not supported!
159+
```
160+
161+
bionic 直接拒绝一个不带版本的 triple,所以级别是**强制**的,一个从没听说过 API
162+
level 的工程也需要一个。于是问题变成这个数字从哪里来。不是一个编译进来的常量:
163+
这个仓库已经记过不止一次,一个写进注释的版本号会变成一个写进诊断的版本号,再变成
164+
某人 install 命令里的版本号,而 NDK 的下限随 NDK 移动。载荷自己回答 ——
165+
`meta/platforms.json` 是上游自己声明的支持区间,r30 是 `{"min": 21, "max": 37}` ——
166+
于是一个更新的 NDK 靠**被安装**改变这个默认值,而不是靠被编辑进这个文件。
167+
168+
读不到时返回 0,由调用方转成一个点名 `min_api_level` 的拒绝。一个****出来的级别
169+
比这个拒绝更坏:它决定哪些 bionic 符号存在,所以猜会产出一个在这里链接得上、在
170+
设备上加载不起来的产物。
171+
172+
同一句错误假设在 `llvm_triple()` 的注释里还有**第二份**,也一并改掉了。
173+
174+
### 一个裸 `aarch64-linux` 永不被补全成 Android
175+
176+
Android 那两行与 `aarch64-linux-musl` 落在同一个 `arch-os` 前缀上,因为它的内核
177+
**就是** Linux —— 那正是树里每一处 Linux 形状的答案对它都成立的原因。这不使 bionic
178+
成为一个没有命名 C 库的请求的候选:它有不同的加载器路径、不同的 SDK 和一个 API
179+
level。
180+
181+
这件事在两行离开 `planned` 的那一刻变得可达:`aarch64-linux` 于是有了**两个**受支持
182+
的兄弟行并解析为 ambiguous,而在此之前它补全成 `aarch64-linux-musl`。这个二义性的
183+
两种结果都是错的 —— 拒绝一个有显然答案的请求,或者用 bionic 回答它。
184+
185+
也从 `siblings` 里排除,不只是从 `supported` 里:那个列表是诊断打印的东西,把
186+
`aarch64-linux-android` 提供给一个输入了 `aarch64-linux` 的人,是在建议他为另一个
187+
平台构建。一个**写出来的** `aarch64-linux-android` 永远到不了这个循环 —— 显式的 env
188+
在上面就返回了,这条规则是「作者自己的拼写是一个请求而不是一个缺口」。
189+
190+
### effective triple 里的 API level 现在能被解析回来
191+
192+
`Target aarch64-linux-android -> aarch64-unknown-linux-android21` 是 mcpp **自己
193+
打印**的一行,而把它粘回去得到的是 `unknown target`:env 段的匹配写的是
194+
`k == "android"`,而 API level 骑在那一段上。下面 msvc 那条分支为同一个理由早就带着
195+
同一条注释(`…-windows-msvc19.44.35211`);Android 是同一个形状,被漏掉了。一个前缀
196+
匹配覆盖全部四种拼法:`android``android21``androideabi``androideabi21`
197+
37198
### 一个按目标付费的机器级扫描,17948ms → 4ms
38199

39200
`mcpp test` 在这台机器上从约 3 分钟变成投影 33 分钟,而根因不是回归而是**一直
@@ -94,9 +255,21 @@ payload emits a PE with a musl C library」—— 一句关于另一行的、本
94255
它发不出来的目标解析了通用 llvm 载荷。判据现在是**那一行自己的钉**,也就是这一行
95256
一直在回答的那个问题。
96257

97-
`tests/e2e/640` 把五条钉住:三行各自的句子、那个「声明 llvm 也必须被拒绝」的缺口,
98-
以及收尾那句话要点名**本行的**钉而不是一个固定的词。拿已发布的 2026.9.10.2 对照:
99-
三条与 wasm 有关的变红,两条既有的保持绿。
258+
**然后同一件事发生了第二次。** Android 这一行拿到钉、成为能力钉行,而理由链仍是
259+
三条臂,于是它被解释成了 PE+musl 那一句 —— 与上一段记的一模一样的错答案,经由
260+
一模一样的路径:第四个情形落进了一个按第三个情形写的 `else`
261+
262+
所以修法不是再加一条臂。最后那条臂现在**点名它自己那一行**(`is_pe() && is_musl()`),
263+
而兜底句是通用的:「This row's toolchain is the only one that can emit the target at
264+
all.」以后新增的能力行拿到的是一句**不够具体**的话,而不是一句****话。
265+
266+
`tests/e2e/640` 现在钉住九条:四行各自的句子(裸机、PE+musl、wasm、Android)、
267+
「声明 llvm 也必须被拒绝」这个缺口在 wasm 与 Android 两处、收尾那句要点名本行的钉,
268+
以及**第六条是穷举的** —— 前面每一条都是有人想到了那一行才写下的,而两次缺陷都是
269+
**没人想到的那一行**掉进了兜底,任何按行写的测试都抓不到。第六条从引擎自己的词汇
270+
里取出每一个带钉的行,声明一个不是它的钉的工具链,并断言 PE+musl 那句话恰好出现
271+
在一行上。实测:34 个目标里 17 个是能力钉行,PE+musl 出现 1 次。分母取自
272+
`toolchain list`,所以明天新增的一行不需要编辑这个文件就已经在里面。
100273

101274
### 解析行里说出**是哪个载荷**回答的
102275

docs/20-toolchains.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,10 @@ and refuses.
776776

777777
**Scope.** The contract governs the C++ runtime only. Static **libc** is a separate
778778
axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment
779-
floor is a third (`macos_deployment_target`). Also, `host-coupled` means mcpp adds
779+
floor is a third — `macos_deployment_target` in `[package]` for Apple targets,
780+
`min_api_level` under `[target.<triple>]` for Android. Both feed one parameter
781+
of `llvm_triple()` and one slot in the build fingerprint, because a target is
782+
either Apple or Android and the two answer the same question. Also, `host-coupled` means mcpp adds
780783
nothing to embed a C++ runtime; it does not strip the toolchain rpath the link
781784
carries for other reasons, so on ELF such an artifact may still find the
782785
toolchain's libraries first.

0 commit comments

Comments
 (0)