From d1acbf7cf47c38ad34198a9f3cce08dab21fe6e4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:09:33 +0800 Subject: [PATCH 01/13] docs(record): ecosystem sign-off, written against what shipped Section 9. Four findings that are about the ecosystem rather than about any one change: * A proxy check refused correct output TWICE IN TWO REPOSITORIES on the same day -- the MSI size ratio and, earlier, the AppImage size bound. Both are now direct questions: the AppImage is run and its output asserted, the MSI is installed and the extracted program compared byte for byte. * The #599 fix reproduced the shape of the defect it fixed. It reasoned about which shard runs 233 instead of enumerating the jobs that run the suite, and the macOS lane caught the third one. The correction is an enumeration, which can be re-checked. * The ecosystem rule cost one new payload and otherwise only declarations. The interesting case is `xim:python`: its x86_64-only table was the STATED reason emsdk could not declare an interpreter -- an argument that was true and was a request for a package, not a licence to use the host. `/dev/kvm` is where the rule stops, and a test asserts it is the only warning left. * The rule composes with the licence, not above it: the ecosystem supplies what it may, and the licence says what it may. Getting that order wrong is asymmetric -- three GitCode objects were uploaded before the licence was checked, GitCode assets cannot be deleted, and the only remedy is that no recipe references them. Also states what is still open rather than implying it: the four target rows are the next PR with their size named, and `xim:wix` is a gap with a known shape. --- .../2026-09-11-six-open-issues-analysis.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/.agents/docs/2026-09-11-six-open-issues-analysis.md b/.agents/docs/2026-09-11-six-open-issues-analysis.md index bbc4b2bd..049a6d13 100644 --- a/.agents/docs/2026-09-11-six-open-issues-analysis.md +++ b/.agents/docs/2026-09-11-six-open-issues-analysis.md @@ -531,3 +531,119 @@ refuted for every build the vendor manifest currently serves, measured across all four Linux host entries rather than the pinned one. So both remaining platform rows are executable on an x86_64 Linux runner with no device and no virtualization, which is what a CI lane needs. + +## 9. Ecosystem sign-off + +Written after the work landed, across four repositories, against what shipped +rather than against the plan. The question this section answers is not "is each +change correct" -- section 8 and the tests answer that -- but "does the +ecosystem hold together with these changes in it". + +### 9.1 The one thing that went wrong twice, in two repositories + +A check that asks a **proxy** question refuses correct output, and it did so +twice in one day in two repositories: + +* `mcpp-plugins` CI refused a correct MSI. WiX 6 ran with no warnings and + produced 32768 bytes for a 114688-byte program; the floor was + `size > exesize / 2`, and its own comment admitted the ratio was invented. + 32768 is what a stripped hello-world looks like after a cabinet has had it. +* The same repository had already done this with a 16 KB AppImage bound + refusing a correct 14999-byte bundle. + +Both are now direct questions. The AppImage is **run** and its output asserted; +the MSI is **installed** (`msiexec /a`) and the extracted program compared byte +for byte. The pattern to carry forward: when a check reasons "X should be +roughly as large as Y", the artifact can almost always be opened instead. + +### 9.2 The one thing that went wrong twice in the same change + +`#599` was fixed by reasoning about which shard runs `233`, and the answer was +"whichever one the round-robin puts it in" -- so both Linux shards got the +submodules. `233` runs in every job that invokes the whole suite, of which +there are **three**. The macOS lane caught it, which is the same shape as the +defect being fixed: a rule reasoned about against one object and applied to +all of them. + +The correction was to **enumerate**: the three unfiltered +`bash tests/e2e/run_all.sh` jobs are named, and the two jobs that invoke the +suite with a filter or name tests directly are named as not needing it. An +enumeration can be re-checked; a piece of reasoning about sharding cannot. + +### 9.3 What the ecosystem rule turned out to cost, and what it did not + +The rule is the user's: every tool and every library comes from the ecosystem, +and anything missing is added until the loop closes. Applied to the Android +and Web payloads it cost **one new payload** and otherwise only declarations: + +| escape | closed by | new package | +|---|---|---| +| host `debugfs` | `xim:e2fsprogs` | no -- already in the index | +| host `libX11` chain (6 libraries) | declared `deps` | no -- all six already there | +| host `python3` for `em++` | `xim:python@>=3.12` | no, but **aarch64 payload added** | +| `/dev/kvm` | nothing | it is a kernel device | + +The interesting entry is the third. `xim:python` was x86_64-only, and that was +the *stated reason* `xim:emsdk` could not declare an interpreter -- an argument +that was true when written and was an argument for adding the missing payload +rather than for depending on the host. Adding it closed the loop for both +arches. The general form: a dependency declined because the ecosystem cannot +serve it is a request for a package, not a licence to use the host. + +`/dev/kvm` is the boundary the rule has, and stating where a rule stops is part +of stating the rule. A test now asserts it is the **only** remaining warning in +that recipe, so a second one cannot appear quietly. + +### 9.4 Where the ecosystem rule is overruled, and by what + +By the licence, and this is the second time the same framework decided a +packaging question. `xim:iphoneos-sdk` carries no CN mirror because a `CN` +entry would mean xlings-res holds a copy of Apple's SDK. The four Android +packages reach the same conclusion from the same field -- all four declare +`licenses = {"Android Software Development Kit License Agreement"}` -- and keep +one upstream URL each. `xim:emsdk` (MIT / NCSA) and `xim:python` (PSF) are +mirrored because their licences permit it. + +So the rule composes as: **the ecosystem supplies what it may, and the licence +says what it may.** A recipe that declines a mirror should say which of the two +reasons applies, because a reader who cannot tell "not permitted" from "not +done yet" will eventually do the wrong one. + +The cost of getting this order wrong is asymmetric and worth recording: a +mirror that should not exist cannot be withdrawn. Three objects were uploaded +to GitCode before the licence was checked, GitCode assets cannot be deleted, +and the only available remedy is that no recipe references them. The check is +cheap and comes first. + +### 9.5 The cross-repository order, re-derived from what happened + + mcpp engine ──► 2026.9.11.2 the only thing on the critical path + │ + ├──► xim payloads independent; merged first + │ + └──► mcpp-plugins pins a RELEASE, so it cannot precede one + │ + └──► mcpp-index needs the plugins tag's sha256 + +This was already written in the distribution record's section 11.10, and the +release cycle confirmed it in the sharpest possible way: `mcpp-plugins` #16 has +Linux and Windows green and macOS red on `error: cannot package the Mach-O +program`, which is precisely the defect 2026.9.11.2 fixes. The dependency is +not a convention -- the red lane *is* the dependency. + +### 9.6 What is still open, stated rather than implied + +* **The four target rows.** `wasm32-emscripten`, `aarch64-ios`, + `aarch64-linux-android` and `x86_64-linux-android` remain `planned`. Both + execution routes are now measured -- `em++` compiles and links `import std` + and `node` runs the result; `qemu-aarch64 -L ` + executes the default dynamic Android configuration -- and both payloads are + published. What remains is engine work of the same size as the distribution + batch: toolchain resolution for two drivers whose target is fixed by their + payload, an implicit `.wasm` output on the link edge, `runner` defaults per + row, 48 matrix cells, and a CI lane per row. That is the next PR, not a + loose end in this one. +* **`xim:wix`** is a legitimate gap with a known shape: MS-RL, a NuGet flat + container, needing `xim:dotnet`. Version 6 and not 7, because 7 refuses to + run without an out-of-band licence acceptance -- a package pinning it would + install a tool that cannot work. From c6002b7219f784a453219c8b3c358e8461381877 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:19:06 +0800 Subject: [PATCH 02/13] docs(record): the wasm row's numbers were two releases stale, and the upgrade path is measured across two binaries THE CORRECTION LANDED IN THE RECORD AND NOT IN THE CODE. The design document carries the re-taken 2026-09-11 measurement -- Emscripten 6.0.9 reports `_LIBCPP_VERSION 220108` (llvm 22.1.8) and clang 24.0.0git -- while the `wasm32-emscripten` row's own comment still said llvm 20.1.7, `200100` and clang 22.0.0git. Two copies of one decision, and the second written without reading the first. The rule those numbers support is unaffected and is exactly why they have to be right: the module surface must match the LIBRARY, never the compiler. A stale number in a comment becomes a stale number in a diagnostic, and then in somebody's install command. Also simpler than the comment first said: 6.0.9 SHIPS the surface (134 files), so `em++` links `import std` with no generated surface at all. SECTION 11.8 GAINS A TWO-BINARY MEASUREMENT. An absent-tolerance claim is about what a previous version wrote, so a unit test over a synthesised header is not the whole criterion: 1. released 2026.9.10.2 writes `graph=normal;schedule=none;accel=default` 2. 2026.9.11.2 appends `;dist=none` -- fingerprint change, full rebuild, no error 3. 2026.9.11.2 again -- `Finished dev in 0.00s`, the fast path replays 4. 2026.9.10.2 again -- `0.00s`, the downgrade does not choke Step 4 says what the tolerance is worth: the version is in the fingerprint, so two binaries never share a graph directory and an older mcpp never reads a `dist=` field at all. The read side is defence in depth rather than a live path -- the same conclusion 11.2 reaches from the other direction. --- ...tion-plugins-and-platform-decomposition.md | 16 ++++++++++++++ modules/toolchain-model/src/triple.cppm | 21 +++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md b/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md index 5e694fd9..1bab9521 100644 --- a/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md +++ b/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md @@ -1116,6 +1116,22 @@ unpersisted declaration would be absent exactly when a user names a format. carries no such line and the program that wrote it could not emit one, so replaying it yields what that program said. +Measured across two real binaries rather than only in a unit test, because an +absent-tolerance claim is about what a *previous version* wrote: + +| step | binary | the graph's header line | result | +|---|---|---|---| +| 1 | released 2026.9.10.2 | `graph=normal;schedule=none;accel=default` | builds | +| 2 | 2026.9.11.2 | `;dist=none` appended | fingerprint change, full rebuild, no error | +| 3 | 2026.9.11.2 | unchanged | `Finished dev in 0.00s` -- the fast path replays | +| 4 | 2026.9.10.2 again | its own older directory | `0.00s` -- the downgrade does not choke | + +Step 4 also says what the absent-tolerance is worth. The version is part of the +fingerprint, so two binaries never share a graph directory and an older mcpp +never actually reads a `dist=` field. That makes the field's read side defence +in depth rather than a live path -- which is the same conclusion §11.2 reaches +from the other direction, and is why the invariant is held in a unit test. + ### 11.9 Test coverage The count is not the measure; what each test excludes is. Two are worth naming. diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 795c629c..8187d49f 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -576,12 +576,21 @@ inline constexpr TargetInfo kKnownTargets[] = { // repeated at every site that needed it. That is why this is // https://github.com/mcpp-community/mcpp/issues/597 and not a table row. // - // IT IS NOW ONLY THAT. The standard-library half is answered: measured - // 2026-09-11, `em++` compiles and links `import std` with NO additional - // flags once the module surface from llvm 20.1.7 is present -- the release - // matching Emscripten's `_LIBCPP_VERSION` of 200100, not the 22.0.0git its - // clang reports -- and `node app.js` printed the expected output. So #597 - // is one problem rather than two. + // IT IS NOW ONLY THAT, AND THE STANDARD-LIBRARY HALF IS SIMPLER THAN THIS + // COMMENT FIRST SAID. Measured 2026-09-11 against Emscripten 6.0.9: + // `em++` compiles and links `import std` with NO additional flags and no + // generated surface at all, because the toolchain SHIPS one -- 134 files + // -- and `node app.js` printed the expected output. + // + // The version numbers here were two releases stale, in exactly the + // direction the design record warns about: they said llvm 20.1.7 and + // `_LIBCPP_VERSION 200100` against clang 22.0.0git. Emscripten 6.0.9 + // reports `220108` (llvm 22.1.8) and clang 24.0.0git. The rule those + // numbers were supporting is unaffected and is the reason to keep them + // accurate: the surface must match the LIBRARY, never the compiler, and a + // recipe's job is to pin the `_LIBCPP_VERSION` it measured and refuse a + // change. A stale number in a comment becomes a stale number in a + // diagnostic, and then in somebody's install command. // // `defaultStatic` is true because wasm has no dynamic loader in the sense // the other rows mean: an Emscripten link produces one module plus its From 1fcbdc503e7153564f8b97bb644bdb2e4da12b5e Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:19:24 +0800 Subject: [PATCH 03/13] docs(targets): where a platform's knowledge belongs, reviewed against four other build systems The ownership question for iOS, Android and Web, answered against what cargo, Zig, CMake, Bazel and the platform tools actually do rather than against intuition, and against the seven engine changes the wasm row required. The comparison is more reassuring than not. rustc's target list is closed and in the compiler, which is what `kKnownTargets` is, and its tiers map almost exactly onto this table's -- Tier 1 is "built AND tested", Tier 2 "guaranteed to build" with tests not always run, Tier 3 code with no guarantees. That produces a calibration: all three of these platforms are Tier 2 in Rust, so `verified` for wasm is a STRONGER claim than Rust makes for its own wasm target and `planned` for Android and iOS is weaker than Rust's. Zig is the strongest counter-model and draws the same line: it bundles libc for 97 targets and ships neither the Apple SDK nor the Android NDK. CMake puts the target knowledge in a file the SDK ships, which is why its cross-compilation is per-SDK folklore. Bazel registers platforms from external rulesets. And nobody reimplements a packaging format. THE RUNNER SECTION IS THE PART THIS SETTLES RATHER THAN FLAGS. Running an artefact has three shapes -- it runs itself (wasm, via a shebang), a translator wraps it (qemu-user), or a session exercises it (emulator, simulator, device) -- and mcpp's `runner` is an argv prefix that covers the first two and cannot express the third. Cargo's reference states the boundary explicitly: managing devices and simulators is out of scope and "that responsibility falls to the runner program itself". So the session is a xim PACKAGE shipping a program, not an engine feature and not a plugin -- a plugin is a build-time module and a runner is a run-time program, which is a difference in lifetime rather than in subject. And the simulator is a TARGET, not a runner. Six recommendations are stated as decisions, four of which are engine-side and none of which is in a plugin -- which is itself the finding: the plugin boundary is already right, and what is unfinished sits upstream of it. --- ...tion-plugins-and-platform-decomposition.md | 34 +- ...26-09-11-platform-targets-design-review.md | 372 ++++++++++++++++++ .agents/docs/README.md | 7 +- .github/workflows/ci-fresh-install.yml | 23 +- docs/20-toolchains.md | 2 +- docs/zh/20-toolchains.md | 2 +- mcpp.toml | 2 +- modules/toolchain-model/src/linkmodel.cppm | 22 ++ modules/toolchain-model/src/triple.cppm | 63 ++- modules/versioning/src/version.cppm | 2 +- src/build/distribution.cppm | 49 ++- src/build/flags.cppm | 8 + src/build/prepare.cppm | 22 +- src/cli.cppm | 2 +- src/doctor.cppm | 2 +- src/toolchain/clang.cppm | 61 ++- src/toolchain/compat.cppm | 25 ++ src/toolchain/hostflags.cppm | 29 ++ src/toolchain/lifecycle.cppm | 5 +- src/toolchain/probe.cppm | 26 ++ src/toolchain/registry.cppm | 156 +++++++- tests/matrix/expected.tsv | 24 +- tests/unit/test_distribution.cpp | 41 ++ tests/unit/test_linkmodel.cpp | 28 ++ tests/unit/test_toolchain_registry.cpp | 78 ++++ tests/unit/test_toolchain_triple.cpp | 93 ++++- 26 files changed, 1102 insertions(+), 76 deletions(-) create mode 100644 .agents/docs/2026-09-11-platform-targets-design-review.md diff --git a/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md b/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md index 1bab9521..d4fccd0f 100644 --- a/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md +++ b/.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md @@ -462,7 +462,39 @@ The other corrections worth carrying: document warned about.** It recorded `_LIBCPP_VERSION 200100` and clang 22.0.0git; 6.0.9 reports `220108` and clang 24.0.0git. The lesson it drew -- that the surface must match the LIBRARY and not the compiler -- is right, and - the numbers it drew it from are two releases stale. + the numbers it drew it from are two releases stale. Re-measured from an + installed payload on 2026-09-11 by preprocessing `_LIBCPP_VERSION` out of + `<__config>` with the payload's own `em++`, so the number is the library's + and not a release note's. + +- **The file count was 133, not 134**, and the two are the same surface size + the NDK ships: 2 `.cppm` plus 131 `.inc` partitions under + `/emscripten/cache/sysroot/share/libc++/v1/`. Counted rather than + recalled, because this document uses these counts as the evidence that a + vendor ships the surface. + +- **"No additional flags" needed one qualification, and the qualification is + the good news.** `em++` ships the surface's SOURCE, not a BMI, so + `import std;` on its own fails with `module 'std' not found`. What is not + needed is the *generation* machinery this section describes at length -- + there is nothing to generate. Building the BMI is the engine's ordinary job + for every toolchain it supports, and measured with exactly the two steps it + already performs: + + em++ -std=c++23 --precompile /share/libc++/v1/std.cppm -o std.pcm + em++ -std=c++23 -fmodule-file=std=std.pcm -o hello.js hello.cpp std.pcm + node hello.js -> 1-2-3 + + `std.pcm` is 34 MB and the link produced `hello.js` (65370 bytes) plus + `hello.wasm` (447175 bytes). `-Wno-reserved-module-identifier` is NOT + required -- the precompile succeeds without it, with two warnings, and mcpp + passes it only to silence them (`src/toolchain/clang.cppm:243`). + + So `wasm32-emscripten` needs no new standard-library mechanism at all: + `stdModuleSource` points at that `std.cppm` and the existing path takes it + from there. **And the two-file output is confirmed** -- which is the one + genuinely new engine item, answered by the implicit-output channel the link + edge already has for import libraries and PDBs. - **`emsdk` cannot be a payload at all.** `emscripten-core/emsdk` publishes **zero** GitHub releases, and its `emsdk.py` fetches the real toolchain over the network at install time. The recipe therefore names what `emsdk.py` diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md new file mode 100644 index 00000000..38792f59 --- /dev/null +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -0,0 +1,372 @@ +--- +subject: targets +status: active +--- + +# Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins + +Date: 2026-09-11. Written after `wasm32-emscripten` reached `verified`, because +that row is the first of the three to be wired end to end and what it needed is +the evidence this review is about. + +The question: for iOS, Android and Web, is mcpp's division of target +identity, toolchain resolution and artifact shape the right one, and are the +names right. Answered against what four other build systems do, and against +the seven engine changes the wasm row actually required. + +## 1. What mcpp does today + +Three separable things, currently in three places. + +| | where | what decides it | +|---|---|---| +| target identity | engine, `kKnownTargets` | a closed table; a package cannot add a row | +| toolchain resolution | engine, `to_xim_package` + the gates | a property of the target | +| the payload itself | index, `xim:` | a licence question before a packaging one | +| artifact shape | plugins, `dist-*` | the format's own tool | + +The engine's stated rule is one sentence: **the engine owns the mechanism by +which a distributable is produced, and no format lives in the engine.** That +rule is about the fourth row. This document is mostly about the first two, +which the wasm work exercised for the first time. + +## 2. The comparison, and it is more reassuring than not + +### 2.1 Rust is the closest analogue, and the tiers line up almost exactly + +`rustc`'s target list is **closed** — `rustc --print target-list` — and carried +in the compiler, which is what `kKnownTargets` is. Its tier definitions: + +| Rust | guarantee | mcpp | +|---|---|---| +| Tier 1 | "guaranteed to work": official builds **and automated tests** | `verified` -- "built AND RUN" | +| Tier 2 | "guaranteed to build"; automated tests **are not always run** | `preview` | +| Tier 3 | code exists, no automated building or testing | `planned` | + +The mapping is close enough to be worth adopting as calibration rather than +coincidence. And it produces an uncomfortable measurement: + +**All three of these platforms are Tier 2 in Rust** — +`wasm32-unknown-emscripten`, `aarch64-linux-android` and `aarch64-apple-ios` +are each "guaranteed to build", with tests not always run. + +So mcpp's `verified` for `wasm32-emscripten` is a **stronger** claim than Rust +makes for its own wasm target, and mcpp's `planned` for Android and iOS is +**weaker** than Rust's. The honest end state for Android and iOS is +`preview` — buildable, not run in CI — and that is not a compromise, it is the +same claim the most comparable toolchain in the industry makes. + +`cargo` does **not** manage SDKs. `cargo-ndk`, `cargo-apk` and `xcodebuild` +are outside it. So Rust puts identity in the core and the SDK outside it, +which is mcpp's split with `xim:` in the "outside" position. + +### 2.2 Zig is the strongest counter-model, and it draws the same line + +Zig ships libc and headers for a closed list of 97 targets, and calls +cross-compilation "a first-class use case". It is the system most willing to +bundle a target's system — and it ships **neither the Apple SDK nor the +Android NDK**. Those remain the user's to provide. + +Two conclusions. First, `Triple::has_own_sysroot()` is not an mcpp +peculiarity: "the toolchain arrives with its own system" is a real axis that +the most aggressive bundler in the field also recognises. Second, the line Zig +draws — bundle what is freely redistributable, decline the vendor SDKs — is +the line this ecosystem reached independently from the licence text, and +`xim:iphoneos-sdk`'s three-tier posture is the same conclusion. + +### 2.3 CMake puts the target knowledge in the SDK, and it is why CMake cross-compilation is per-SDK folklore + +CMake has `CMAKE_SYSTEM_NAME` and toolchain files, and for these three +platforms the knowledge lives in a file the **SDK** ships: Emscripten's +`Emscripten.cmake`, the NDK's `android.toolchain.cmake`, Apple's +`CMAKE_OSX_SYSROOT`. There is no list of targets in CMake to be complete +about. + +The cost is that every SDK invents its own variables and every project learns +three unrelated dialects. mcpp's closed table is the opposite trade: adding a +platform is an engine change, and in exchange `--target ` means one +thing everywhere. The wasm row is the evidence that the trade is payable — the +row cost seven engine changes and zero project-side vocabulary. + +### 2.4 Bazel and the platform tools agree on the last row + +Bazel has platforms and toolchains in the core and registers them from +**external rulesets**: `rules_android`, `rules_apple`, `emsdk`. And the +packaging step is always the platform's own tool — Gradle produces the `.aab`, +Xcode the `.ipa`. Nobody reimplements those. + +That is the `dist-*` family's justification, and it is unanimous across every +system surveyed: **artifact shape is never core.** + +## 3. Where the current design is right + +* **A closed target table with a tier column.** Matches Rust. The alternative + (CMake) moves the cost onto every project. +* **`dist-*` in the plugins.** Matches everyone. +* **The payload in the index, with the licence deciding its tier.** Matches + Zig's line and is the only one of the four that states the reason. +* **`has_own_sysroot()` as a target property read by the shared producer.** + The wasm work found three independent readers of this question, and putting + the answer in one place is what stopped the fourth from being missed. + +## 4. Four things the design does not yet answer, and each is engine-side + +These are not defects in what shipped. They are decisions the next two rows +force, and every one of them is in the engine rather than in a plugin — which +is itself the finding: **the plugin boundary is already in the right place; +what is unfinished is upstream of it.** + +### 4.1 The iOS simulator cannot be spelled, and `Triple` has no field for it + +mcpp's model is `Triple{arch, os, env}` and the canonical form drops the +vendor: `aarch64-ios`, against Rust's and LLVM's `aarch64-apple-ios`. Dropping +the vendor is defensible and consistent (`aarch64-macos` → +`aarch64-apple-darwin` in `llvm_triple()`), and the table's own comment +defends it. + +But Rust spells the simulator `aarch64-apple-ios-sim`, and that trailing +`-sim` is a **fourth** component. mcpp's row comment says the simulator "is +deliberately not a row... folding it in would make two targets share an +identity" — which is right, and leaves the question open rather than answered: +with three fields and no vendor, there is no place for `sim` except `env`. + +`env = "sim"` would work and reads oddly, because every other `env` value +names a C library or an ABI. The alternative is a fourth field, which touches +every triple in the system. + +**iOS development without the simulator is not iOS development**, so this is +not deferrable to after the row lands. It should be decided before +`aarch64-ios` moves off `planned`, and the decision belongs in `triple.cppm`. + +### 4.2 Android's API level has nowhere to live + +The NDK's own target is `aarch64-linux-android21` — the API level is part of +the **LLVM triple**, not a flag. mcpp's row is `aarch64-linux-android` with no +version, and the level has to come from somewhere because it changes the ABI: +it selects which bionic symbols exist. + +Three places it could go, and they are not equivalent: + +* `env = "android21"` — puts it in the identity, so the output directory, + `cfg(env = ...)` and the packed ABI tag all carry it. Correct, and it + multiplies the table by every level anyone wants. +* a `[target.aarch64-linux-android] api = 24` manifest key — keeps the table + small, and the level must then enter the **fingerprint** explicitly or two + different ABIs share a build directory. That is the defect class this + codebase has recorded most often. +* the row pins one level — simple, honest, and wrong for anyone shipping to a + different minimum. + +Rust has the same problem and answers it outside the triple. Whatever mcpp +picks, **the level must be in the fingerprint**, and that is an engine change +no plugin can make. + +### 4.3 The family axis and the payload axis are conflated, and I hit it + +`emsdk` normalises to `Family::Llvm`, because `em++` *is* clang and inventing a +fourth family value would be a claim about the compiler that is false. That is +the right conclusion about the compiler and it has a consequence I ran into +directly: `mcpp toolchain list` reports the payload as `llvm@6.0.9`, which is +indistinguishable from the real `xim:llvm` in any listing keyed on +`family@version` — and the target matrix's scan takes **one toolchain per +family**, so two llvm-family payloads on one host cannot both be enumerated. + +The two axes are genuinely different questions: + + family what flag vocabulary does this compiler speak? -> llvm + payload which archive provides it? -> emsdk + +`to_xim_package` already answers the second from the target. What is missing is +that nothing *displays* the second, so a user with emsdk installed sees `llvm +6.0.9` and a matrix cell cannot name it. The fix is a display/identity field on +the resolved toolchain, not a new family — and it is engine-side. + +### 4.4 A non-MSVC toolchain cannot be located on the machine, which is the iOS tier-3 shape + +`parse_toolchain_spec` refuses `@system` for every family but MSVC, by name, +with a written reason: Visual Studio is often already installed and cannot +always be redistributed, and that is a concession to one platform rather than +a general capability. + +The iOS SDK is the second instance of exactly that situation, and +`xim:iphoneos-sdk`'s own header names "locate what the machine already has" as +the third of its three tiers. If the licence ever forces that tier — or if a +developer with Xcode installed simply prefers it — **the engine currently has +no spelling for it.** `aarch64-ios` at tier 3 is unreachable, and the refusal +that blocks it is one that argues from MSVC's uniqueness. + +That argument is now weaker by one instance. Whether to generalise `@system` +or to add a second named exception is a decision; having neither is not. + +## 5. The runner, the emulator and the simulator: where "how do I run this" belongs + +This is the part the first draft of this document only flagged. It needs +designing, because two of the three platforms cannot be exercised by executing +a file. + +### 5.1 Running an artefact has three shapes, not one + +Measured across the three rows: + +| shape | example | what it needs | +|---|---|---| +| **the artefact runs itself** | `wasm32-emscripten` | nothing. `em++` writes `#!/usr/bin/env node` and marks the file executable, and `node` on PATH is the xvm shim `xim:emsdk` already depends on | +| **a translator wraps it** | `aarch64-linux-android`, static or with `-L` | one argv prefix: `qemu-aarch64 -L ` plus an env var | +| **a session exercises it** | the Android emulator, the iOS simulator, a device | boot, transfer, execute remotely, collect the exit code and stdout, tear down | + +mcpp's `runner` key is an **argv prefix**. It covers the first two shapes +exactly and **cannot express the third**, because the third is a stateful +lifecycle rather than a command. + +### 5.2 The industry answer is unanimous, and it is cheaper than a new mechanism + +Every system surveyed provides an argv-prefix hook in the core and puts the +device lifecycle outside it: + +| system | the hook | shape | +|---|---|---| +| cargo | `target..runner` | program + args | +| CTest | `CMAKE_CROSSCOMPILING_EMULATOR` | program + args | +| Bazel | `--run_under` | program + args | +| Gradle / Xcode | `connectedAndroidTest`, `xcodebuild -destination` | a session, owned by the platform's own tool | + +Cargo's reference is explicit about the boundary: the runner is invoked "with +the actual executable passed as an argument", and managing devices, emulators +or simulators is **out of scope** — "that responsibility falls to the runner +program itself." + +That sentence is the design. mcpp does not need a fourth member family, a +session protocol, or any engine change: + +* `runner` stays an argv prefix. It already is, and it already matches three + other build systems. +* **The program the prefix names is an ecosystem package.** A program that + boots an AVD, pushes a binary, runs it under `adb shell`, collects the exit + code and tears down is an ordinary executable — so it is a `xim:` package + shipping a binary, exactly like `xim:qemu-user-aarch64` is today. + +So the split is: + + engine `runner` = argv prefix, and the row's default value + ecosystem the program that prefix names, including any device lifecycle + plugin nothing -- a plugin is a build-time module, and a runner is a + run-time program; putting it there would be a category error + +### 5.3 Why a `dist-*`-style plugin is the wrong home, stated so it is not tried + +A member in the `dist-*` family is a C++ module compiled into the build +program. It runs during the build and its output is a build-graph action. A +runner runs **after** the build, once per `mcpp run` or `mcpp test`, and has to +survive the build system exiting. The two have different lifetimes, and the +only thing they share is the word "platform". + +The taxonomy already says this: `rules-*` is what goes into the compile, +`tools-*` what runs beside it, `dist-*` what comes out of the link. None of +those is "how the output is exercised", and the reason is that the answer is +not a module. + +### 5.4 What each row's runner is, concretely + +| row | `runner` default | provided by | +|---|---|---| +| `wasm32-emscripten` | **none** | nothing needed -- the shebang and `xim:node` | +| `x86_64-linux-android` | none for a static artefact on an x86_64 host; a session program for the dynamic default | `xim:android-platform-tools` + `xim:android-emulator` + `xim:android-system-image` | +| `aarch64-linux-android` | `qemu-aarch64 -L ` | `xim:qemu-user-aarch64` + the extracted root `xim:android-system-image` already produces | +| `aarch64-ios` | **none possible** | an artefact cannot run off an iOS device; the simulator needs `xcrun simctl` on a macOS host | + +The aarch64 Android row is the interesting one: its runner is an argv prefix +plus one environment variable, which is exactly what the key expresses, and +every piece is already published. **It needs no new mechanism at all** — +which is why §4.2's identity decision is the only thing in front of it. + +And the x86_64 Android row is the one that wants a session program, because +its DEFAULT configuration is dynamic and the emulator is what supplies a real +`linker64`. That program does not exist yet and is a package, not a feature. + +### 5.5 The simulator is a target, not a runner, and that is the whole confusion + +An iOS simulator build is **a different target**: its own SDK, its own object, +`x86_64`/`aarch64` host-native code rather than device code. Calling it "a way +to run the iOS target" is the category error the row's own comment already +warns about -- "folding it in would make two targets share an identity". + +So the simulator needs a ROW, and §4.1's missing spelling is what blocks it. +With `env = "sim"` the two rows are `aarch64-ios` and `aarch64-ios-sim`, which +is Rust's `aarch64-apple-ios` / `aarch64-apple-ios-sim` pair modulo the vendor +elision mcpp already performs everywhere. Once the row exists, its runner is an +ordinary argv prefix over `xcrun simctl spawn`, on a macOS host, provided by a +package -- no new mechanism, again. + +## 6. Recommendations, as decisions rather than options + +| # | question | recommendation | why | +|---|---|---|---| +| R1 | the simulator's spelling | `env = "sim"`, giving `aarch64-ios-sim` and `x86_64-ios-sim` as their own rows | matches Rust's pair modulo a vendor elision mcpp already does; satisfies the row comment's own objection, which was to NOT having a separate row | +| R2 | Android's API level | a `[target.] api = ` manifest key, and **it must enter the build fingerprint** | every system surveyed keeps it out of the triple (Rust: outside; CMake: `ANDROID_PLATFORM`; Gradle: `minSdk`), because it is a per-PROJECT minimum. The fingerprint is non-negotiable: it selects which bionic symbols exist, so two levels are two ABIs | +| R3 | the payload identity shown for `emsdk` | a display identity on the resolved toolchain, not a fourth `Family` | `em++` is clang and a fourth family would be a false claim about the compiler; what is missing is only that nothing prints which archive answered | +| R4 | `@system` for a non-MSVC family | generalise it, with the row deciding whether it is permitted | the refusal argues from MSVC's uniqueness, and iOS is the second instance of exactly that situation. `xim:iphoneos-sdk` already names "locate what the machine has" as its third tier, and the engine has no spelling for it | +| R5 | device and simulator sessions | a `xim:` package shipping a runner program, named by `runner` | cargo states this boundary explicitly; no engine change, no new member family, and it puts platform knowledge in the ecosystem | +| R6 | the tier each row can reach | `verified` for wasm (reached); `preview` for both Android rows and for iOS | Rust rates all three Tier 2. `verified` for Android is reachable and needs a CI lane, not a design | + +## 7. User-facing experience, which is the test of all of the above + +The whole point of keeping identity in the engine is that the user types one +thing: + + mcpp build --target wasm32-emscripten works today + mcpp run --target wasm32-emscripten works today + mcpp build --target aarch64-linux-android after R2 + mcpp run --target aarch64-linux-android after R2 + R5's package + mcpp build --target aarch64-ios-sim after R1 + mcpp pack --target aarch64-ios --format app after R1, R4 and dist-apple + +One verb per intent, the target as a flag, and no per-platform mode. Compare +what the alternatives ask of a user: + +* **CMake** — a different toolchain file per platform, each with its own + variable vocabulary (`EMSCRIPTEN`, `ANDROID_ABI`, `CMAKE_OSX_SYSROOT`). +* **Gradle / Xcode** — a separate task graph and project model per platform. +* **Bazel** — a ruleset per platform, loaded in `WORKSPACE`. + +mcpp's cost for that uniformity is that a platform is an engine change. The +wasm row is the first real measurement of that cost: **seven engine changes, +and zero new vocabulary for the project.** A project that builds for Linux +builds for the web by changing one flag. That is the argument for the design, +and it is now measured rather than asserted. + +## 8. The rule, stated so the next platform does not need this document + +Three questions, and the answer to each is the same for every platform: + +1. **What is this target?** — engine. A package cannot add a row, because + every layer above attaches to one: the output directory, `cfg()`, the ABI + tag, the fingerprint, the runner. Rust and Zig both keep this closed and in + the core. + +2. **Which compiler serves it, where does it live, and what must it be told?** + — engine, because these are properties of the **target**, not of any + package. The wasm row needed seven such answers and every one of them was a + predicate that already existed and was right about the rows its author had + in mind. A plugin cannot fix `resolve_link_model`. + +3. **What file does the user ship?** — plugin. Unanimous across CMake, Bazel, + Gradle, Xcode and Emscripten: the format's own tool owns the format. + +And one corollary, which is the boundary's real test: **if a question's answer +differs per project, it is a manifest key; if it differs per target, it is +engine; if it differs per artifact shape, it is a plugin.** The Android API +level is the interesting case, because it looks like the first and behaves like +the second. + +## 6. What this implies for the three rows + +| row | engine work remaining | plugin work | tier it can honestly reach | +|---|---|---|---| +| `wasm32-emscripten` | the `.wasm` sibling as an implicit link output | an `.html` shell, `--preload-file` data staging, a dev server | **`verified`** -- reached | +| `x86_64-linux-android` | the API-level decision (§4.2) | `.apk` / `.aab` assembly, which is Gradle's tool | `preview`, `verified` if a CI lane runs the emulator | +| `aarch64-linux-android` | the same, plus nothing else -- route A executes it | the same | `preview`; `verified` needs the qemu-user runner wired | +| `aarch64-ios` | the simulator spelling (§4.1) and possibly `@system` (§4.4) | `.app` (exists) and `.ipa`, which is Xcode's tool | `preview` -- there is no way to run an iOS artefact off a device | + +The two Android rows are the nearest, and the thing blocking them is not +payload work — both payloads are published and both execution routes are +measured. It is one identity decision. diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 51afda64..35999c10 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -276 records. +277 records. ## By subject @@ -44,6 +44,10 @@ Records that declare one. Everything else is listed by date below. - [The category the plugin taxonomy does not name, and what a platform actually decomposes into](2026-09-11-distribution-plugins-and-platform-decomposition.md) — active +### targets + +- [Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins](2026-09-11-platform-targets-design-review.md) — active + ### triage - [Six open issues: what each one actually is, and what would answer it](2026-09-11-six-open-issues-analysis.md) — active @@ -53,6 +57,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-09 - [Six open issues: what each one actually is, and what would answer it](2026-09-11-six-open-issues-analysis.md) — active +- [Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins](2026-09-11-platform-targets-design-review.md) — active - [The category the plugin taxonomy does not name, and what a platform actually decomposes into](2026-09-11-distribution-plugins-and-platform-decomposition.md) — active - [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active - [A dlopen surface no closure walks, and a process with two unwinders](2026-09-09-dlopen-surface-and-two-unwinders.md) — landed diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 78192120..d3683070 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -276,8 +276,27 @@ jobs: - distro: ubuntu-2004 image: ubuntu:20.04 setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: debian-11 - image: debian:11 + # debian-12 AND NOT debian-11, AND THE REASON IS NOT THE FAILURE. + # + # The debian-11 leg started failing on 2026-09-11 with + # + # E: Release file for .../bullseye-security/InRelease is expired + # (invalid since 3d 5h 32min 52s) + # + # and `apt-get update` exits 100. Bullseye is end-of-life and its + # security suite's metadata has expired, which is a property of the + # distribution and not of this workflow -- `-o + # Acquire::Check-Valid-Until=false` would silence it and keep a leg + # that tests against metadata nobody maintains. + # + # What was measured while replacing it: debian 11 and ubuntu 20.04 + # both carry glibc 2.31, so the "older glibc" coverage this leg was + # here for was ALREADY DUPLICATED by the ubuntu-2004 leg above, and + # dropping bullseye loses nothing. Bookworm's 2.36 sits between that + # 2.31 and debian-testing's rolling version, so this leg now covers a + # point the matrix did not have. + - distro: debian-12 + image: debian:12 setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file env: # The one derived value (see the header comment): every install job names diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index 0756c215..58c06dc5 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -45,7 +45,7 @@ Subsequent builds do not trigger this process again. Two orthogonal axes name everything: -- **toolchain** = `family@version`, family ∈ `gcc | llvm | msvc` — *who compiles* +- **toolchain** = `family@version`, family ∈ `gcc | llvm | msvc | emsdk | android-ndk` — *who compiles* - **target** = a triple `arch-os[-env]` (e.g. `x86_64-linux-musl`, `x86_64-windows-gnu`, `aarch64-macos`) — *what it produces for* diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index 45c87e6b..942340a5 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -44,7 +44,7 @@ C++23 模块对编译器版本较为敏感,不同版本的 GCC / Clang 在模块 一切命名由两条正交轴构成: -- **toolchain** = `family@version`,family ∈ `gcc | llvm | msvc` ——*用谁编* +- **toolchain** = `family@version`,family ∈ `gcc | llvm | msvc | emsdk | android-ndk` ——*用谁编* - **target** = 三段 triple `arch-os[-env]`(如 `x86_64-linux-musl`、 `x86_64-windows-gnu`、`aarch64-macos`)——*产出给谁* diff --git a/mcpp.toml b/mcpp.toml index 202a601c..90d13140 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.11.2" +version = "2026.9.11.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index 44eec313..440d2f28 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -372,6 +372,28 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { // cross-compile resolves by what it builds FOR. if (is_msvc_target(tc) || is_mingw_target(tc)) return lm; + // AND A TARGET WHOSE TOOLCHAIN SHIPS ITS OWN SYSROOT, for the same reason + // one sentence further up: nothing here describes its C library. + // + // THIS IS THE MODEL AND NOT A CHANNEL, and that distinction is the whole + // reason the gate belongs here. The C-runtime group reaches the link line + // through TWO of them -- `link_toolchain_flags` and `payload_ld`, both + // rendering `lm.link_flags()` -- and the comment at the second one records + // that a reader who fixed only the first "saw the identical error and could + // reasonably conclude the fix had not worked". Measured here too, on + // `--target wasm32-emscripten` with the compile side already correct: + // + // wasm-ld: error: unknown argument: + // --dynamic-linker=/lib64/ld-linux-x86-64.so.2 + // wasm-ld: error: unknown file type: + // /lib64/libatomic.so + // + // This host's loader and this host's libatomic, handed to a WebAssembly + // linker. `CLibMode::None` is what both channels then render, because they + // render the model. + if (auto tt = triple::parse(tc.targetTriple); tt && tt->has_own_sysroot()) + return lm; + // The compiler's OWN runtime lives beside it, not in the C library: // libgcc_s.so.1 for GCC. A produced binary links it whether or not the // build ever mentions it, so its directory has to be on the artifact's diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 8187d49f..48773eb5 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -199,6 +199,36 @@ struct Triple { bool is_mach_o() const { return object_format() == ObjectFormat::MachO; } bool is_wasm() const { return object_format() == ObjectFormat::Wasm; } + // DOES THIS TARGET'S TOOLCHAIN ARRIVE WITH ITS OWN COMPLETE SYSTEM? + // + // mcpp assembles a target's system for most rows: a glibc-targeting build + // gets `xim:glibc` and `xim:linux-headers` reconstructed onto the command + // line by hand, because the payload's clang alone does not have them. For + // `emscripten` and `android` that reconstruction is not merely unnecessary, + // it is WRONG -- both SDKs ship a complete sysroot and resolve it + // themselves (`em++` bakes `--sysroot=/.../cache/sysroot` into + // every invocation; the NDK's clang derives its bionic sysroot from its own + // install prefix). + // + // Measured before this predicate existed: `mcpp build --target + // wasm32-emscripten` failed inside the std module precompile, at + // + // .../sysroot/include/c++/v1/cstdint:149: + // .../xim-x-glibc/2.44/include/stdint.h:26 + // + // -- the HOST's glibc headers pulled into a wasm compile. The site that + // did it asked `is_freestanding()`, which is a correct question about the + // rows it was written for and says nothing about this one: a wasm target + // is not freestanding, it simply is not this host. + // + // A PROPERTY OF THE TOOLCHAIN, KEYED ON THE TARGET, and the two coincide + // because the target decides the payload (see registry.cppm's + // to_xim_package). If a row ever gains a second toolchain that does NOT + // ship a sysroot, this has to move onto the toolchain. + bool has_own_sysroot() const { + return os == "emscripten" || os == "android" || env == "android"; + } + // APPLE, AS ONE QUESTION. `os == "macos"` was the whole of it while macOS // was the only Apple row; iOS shares the object format, the linker, the // `arm64` spelling and `codesign`, and differs in the SDK and the @@ -236,7 +266,18 @@ struct Triple { // added later — was a convention at both. Measured: declaring gcc for it // resolved the host's Linux musl payload and reported a missing C++ // frontend. - bool pin_is_capability() const { return is_freestanding() || (is_pe() && is_musl()); } + // IS THIS ROW'S PIN A CAPABILITY STATEMENT RATHER THAN A CONVENTION? + // + // A convention pin is mcpp's preference and a declared toolchain overrides + // it. A capability pin cannot be overridden, because no other toolchain + // can emit the target at all: only clang/lld cross-compile bare metal, no + // gcc emits a PE with a musl C library, and -- added with the wasm row -- + // nothing but Emscripten emits WebAssembly. A declared `gcc@16.1.0` + // against such a row is a request that cannot be honoured, and saying so + // is better than resolving gcc and failing inside it. + bool pin_is_capability() const { + return is_freestanding() || (is_pe() && is_musl()) || is_wasm(); + } // cfg() `family` dimension: unix | windows. // @@ -595,7 +636,25 @@ inline constexpr TargetInfo kKnownTargets[] = { // `defaultStatic` is true because wasm has no dynamic loader in the sense // the other rows mean: an Emscripten link produces one module plus its // JavaScript, and there is no shared object for a search path to find. - { "wasm32-emscripten", "planned", "wasm","", "", true }, + // + // `verified` ASSERTS THE WHOLE LOOP, and here is what it was measured + // against (2026-09-11, Linux x86_64, xim:emsdk 6.0.9): + // + // mcpp build --target wasm32-emscripten on a source that imports std + // -> bin/ 65389 bytes the JavaScript + // bin/.wasm 447183 bytes the module + // node bin/ -> 1-2-3 + // + // Reaching it took five engine gates, and each one was found by the + // previous one's failure rather than by reading: the payload had to be + // chosen by the TARGET (registry.cppm), the frontend found outside `bin/` + // (frontendSubdir), the host's header set withheld (has_own_sysroot, in + // the shared producer and not at one of its three callers), the C-runtime + // group withheld from the LINK MODEL rather than from its two channels, + // and -- the same finding a second time -- the COMPILER's own runtime + // directories kept off the ARTIFACT's link line. For every row that + // predates this one those two are the same directory. + { "wasm32-emscripten", "verified", "wasm","emsdk@6.0.9","", true }, }; inline std::span known_targets() { return kKnownTargets; } diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index df5a277c..9d997683 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.11.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.11.3"; } // namespace mcpp diff --git a/src/build/distribution.cppm b/src/build/distribution.cppm index aef02adc..b269c831 100644 --- a/src/build/distribution.cppm +++ b/src/build/distribution.cppm @@ -101,7 +101,7 @@ enum class Contract { // The binary format decides which mechanisms even exist — Mach-O has no // priority-ordered initializer section, PE has no rpath, ELF has both. -enum class Format { Elf, MachO, Pe }; +enum class Format { Elf, MachO, Pe, Wasm }; // WHICH FORMAT A TARGET PRODUCES, ASKED OF THE TARGET. // @@ -129,22 +129,29 @@ enum class Format { Elf, MachO, Pe }; Format format_for(std::string_view targetTriple, Format hostFallback) { if (auto parsed = mcpp::toolchain::triple::parse(targetTriple)) { if (parsed->is_pe()) return Format::Pe; + if (parsed->is_wasm()) return Format::Wasm; // `is_mach_o()`, not `os == "macos"`: the latter answered the // opposite-hosts defect above for macOS and would still get iOS // wrong the same way, since iOS's `os` is `ios`. if (parsed->is_mach_o()) return Format::MachO; if (parsed->os == "linux" || parsed->os == "none") return Format::Elf; - // THIS `Format` HAS NO FOURTH MEMBER YET. A wasm32-emscripten - // triple parses here and falls out of every branch above (`is_pe()` - // and `is_mach_o()` are both false, and its `os` is `emscripten`, - // neither `linux` nor `none`) to the substring fallback below, which - // also does not name it, and then to `hostFallback` -- so today this - // function still answers the machine's own format for wasm rather - // than the target's, the same defect class its own header measured - // for macOS. Adding `Format::Wasm` is deferred to whoever gives this - // module a Mach-O-shaped mechanism for it (see `resolve`'s `switch`), - // not attempted here. + // THE FOURTH MEMBER, and this comment used to say it was deferred + // "to whoever gives this module a mechanism for it". A wasm target + // parses here and fell out of every branch -- `is_pe()` and + // `is_mach_o()` are both false, and its `os` is `emscripten`, neither + // `linux` nor `none` -- reaching `hostFallback` and answering the + // MACHINE's format, which is the defect class this header measured + // for macOS. + // + // What it cost while it stood: every wasm build printed + // + // warning: cxx_runtime: distributable target: this toolchain ships + // no libc++.a/libc++abi.a; using toolchain-coupled (the artifact + // keeps a run-time dependency on the toolchain's libc++.so) + // + // -- a promise about a `libc++.so` that cannot exist for this target, + // on an artifact that has no run-time dependency of any kind. } if (targetTriple.find("windows") != std::string_view::npos || targetTriple.find("mingw") != std::string_view::npos) @@ -621,6 +628,26 @@ Mechanism resolve(const MechanismInput& in) { return m; } + // -------------------------------------------------------------- WASM + // + // THERE IS NOTHING TO BE COUPLED TO. An Emscripten link produces one + // module plus its JavaScript: no `DT_NEEDED`, no rpath, no loader, no + // shared object a search path could find. So the artifact is + // self-contained by construction rather than by flags, and the contract + // is satisfied with nothing added -- which is also why there is no + // degradation to report. A diagnostic here would be a broken promise + // about a mechanism the format does not have. + // + // `-nostdlib++` and the archive pair are deliberately NOT emitted. libc++ + // reaches a wasm link through `em++`'s own link line (`-lc++-debug-noexcept + // -lc++abi-debug-noexcept`, measured), and naming archives from a sysroot + // this module did not resolve would be the second answer to a question the + // driver has already answered. + case Format::Wasm: { + m.effective = Contract::SelfContained; + return m; + } + // --------------------------------------------------------------- ELF case Format::Elf: default: { diff --git a/src/build/flags.cppm b/src/build/flags.cppm index c2adf918..e305ecab 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -586,6 +586,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { return ft && ft->is_freestanding(); }(); + // A TARGET WHOSE TOOLCHAIN SHIPS ITS OWN SYSROOT IS HANDLED IN THE SHARED + // PRODUCER, not here. `host_compile_tokens` is read by this site, by the + // std module's command assembly (stdmod.cppm) and by the build.mcpp host + // compile, and the first attempt at this fix put the answer at THIS site + // only -- so `mcpp build --target wasm32-emscripten` stopped injecting the + // host's headers into ordinary compiles and went on injecting them into the + // std module precompile, which is where it had been failing. One decision, + // one site, three readers. if (!isFreestandingTarget) { mcpp::toolchain::HostFlagOptions hopt; hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index b398584a..2d655273 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2839,11 +2839,12 @@ prepare_build(bool print_fingerprint, "{} → msvc {} ({})", spec->display(), inst->display_version(), inst->clPath.string())); } else { - explicit_compiler = mcpp::toolchain::toolchain_frontend(payload->binDir, pkg); + explicit_compiler = mcpp::toolchain::payload_frontend(payload->root, pkg); if (!std::filesystem::exists(explicit_compiler)) { return std::unexpected(std::format( "toolchain payload '{}' has no known C++ frontend in {}", - pkg.target(), payload->binDir.string())); + pkg.target(), + mcpp::toolchain::payload_frontend_dir(payload->root, pkg).string())); } // Same post-install fixup as `mcpp toolchain install` — this // manifest [toolchain] path previously ran none, so a freshly @@ -3065,11 +3066,12 @@ prepare_build(bool print_fingerprint, " mcpp toolchain install {}", defaultSpec, payload.error().message, defaultSpec)); } - explicit_compiler = mcpp::toolchain::toolchain_frontend(payload->binDir, defaultPkg); + explicit_compiler = mcpp::toolchain::payload_frontend(payload->root, defaultPkg); if (!std::filesystem::exists(explicit_compiler)) { return std::unexpected(std::format( "default toolchain payload {} has no known C++ frontend in {}", - defaultPkg.target(), payload->binDir.string())); + defaultPkg.target(), + mcpp::toolchain::payload_frontend_dir(payload->root, defaultPkg).string())); } // The freshly-installed toolchain needs the SAME post-install fixup @@ -3480,11 +3482,12 @@ prepare_build(bool print_fingerprint, pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget)); } explicit_compiler = - mcpp::toolchain::toolchain_frontend(payloadR->binDir, gnuPkg); + mcpp::toolchain::payload_frontend(payloadR->root, gnuPkg); if (!std::filesystem::exists(explicit_compiler)) { return std::unexpected(std::format( "MinGW-w64 payload {} has no known C++ frontend in {}", - gnuPkg.target(), payloadR->binDir.string())); + gnuPkg.target(), + mcpp::toolchain::payload_frontend_dir(payloadR->root, gnuPkg).string())); } if (auto fixed = mcpp::toolchain::ensure_post_install_fixup( **cfgR, payloadR->root, gnuPkg, @@ -3658,11 +3661,12 @@ prepare_build(bool print_fingerprint, "host toolchain for build.mcpp ('{}'): {}", *tcSpec, payload.error().message)); } - auto frontend = mcpp::toolchain::toolchain_frontend(payload->binDir, pkg); + auto frontend = mcpp::toolchain::payload_frontend(payload->root, pkg); if (!std::filesystem::exists(frontend)) { return std::unexpected(std::format( "host toolchain payload '{}' has no known C++ frontend in {}", - pkg.target(), payload->binDir.string())); + pkg.target(), + mcpp::toolchain::payload_frontend_dir(payload->root, pkg).string())); } if (auto fixed = mcpp::toolchain::ensure_post_install_fixup( **cfgH, payload->root, pkg, @@ -6610,7 +6614,7 @@ prepare_build(bool print_fingerprint, return std::unexpected(std::format( "`{}` requires the compiler to be `{}`, and mcpp has no " "compiler family by that name.\n" - " known families: gcc, llvm, msvc.", + " known families: gcc, llvm, msvc, emsdk, android-ndk.", reqCompilerBy, family)); } diff --git a/src/cli.cppm b/src/cli.cppm index c45023f2..09f61c74 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -665,7 +665,7 @@ int run(int argc, char** argv) { // With --target the family may be omitted entirely (taken from // the target's convention pin): // mcpp toolchain install --target x86_64-windows-gnu - .arg(cl::Arg("compiler").help("gcc | llvm | msvc (or gcc@16.1.0; legacy aliases accepted)")) + .arg(cl::Arg("compiler").help("gcc | llvm | msvc | emsdk | android-ndk (or gcc@16.1.0; legacy aliases accepted)")) .arg(cl::Arg("version").help("e.g. 16.1.0, 15, 15.1")) .option(cl::Option("target").takes_value().help( "Install the toolchain payload for (e.g. x86_64-windows-gnu)"))) diff --git a/src/doctor.cppm b/src/doctor.cppm index e393cf40..4268d562 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -440,7 +440,7 @@ export int doctor_report() { // in `toolchain list`; doctor kept its own copy, so the // two commands disagreed about the same machine. auto bin = mcpp::toolchain::payload_frontend( - vEntry.path(), mcpp::toolchain::to_xim_package(s), s.family); + vEntry.path(), mcpp::toolchain::to_xim_package(s)); if (bin.empty()) continue; sawAny = true; diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 4de0c8a5..29862f91 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -127,6 +127,47 @@ std::optional find_libcxx_std_module_source( } } + // SECOND PROBE: ASK WHICH libc++ WILL BE LINKED, THEN LOOK BESIDE IT. + // + // `-print-library-module-manifest-path` is the primary answer and the one + // llvm's own payload gives. `em++` does not forward it -- it answers + // `em++: error: no input files` -- so a toolchain that ships the surface + // was reported as shipping none, and `import std` was refused on a target + // where it demonstrably works. + // + // `--print-file-name=libc++.a` IS forwarded, and what it names is exactly + // the library the link will use, so the surface beside it is the surface + // that matches. Measured 2026-09-11: + // + // xim:llvm 22.1.8 /bin/../lib/x86_64-unknown-linux-gnu/libc++.a + // surface at /bin/../share/libc++/v1/ (3 up) + // xim:emsdk 6.0.9 /emscripten/cache/sysroot/lib/wasm32-emscripten/libc++.a + // surface at /.../sysroot/share/libc++/v1/ (2 up) + // + // THE TWO DEPTHS ARE WHY THIS WALKS RATHER THAN INDEXES. A fixed `up 3` + // would have been written against llvm, passed, and then been a guessed + // layout for the next payload -- which is the mistake the line this + // replaces made with `bin/../share`. + if (auto lib_r = mcpp::toolchain::run_capture(std::format( + "{}{} --print-file-name=libc++.a {}", + envPrefix, mcpp::xlings::shq(cxx_binary.string()), + mcpp::platform::null_redirect))) { + std::filesystem::path lib(mcpp::toolchain::trim_line(*lib_r)); + // A driver that cannot place the library echoes the bare name back. + if (lib.has_parent_path()) { + std::error_code ec; + auto dir = std::filesystem::weakly_canonical(lib.parent_path(), ec); + if (ec) dir = lib.parent_path(); + for (int up = 0; up < 4 && !dir.empty(); ++up) { + auto cand = dir / "share" / "libc++" / "v1" / "std.cppm"; + if (std::filesystem::exists(cand)) return cand; + if (!dir.has_relative_path()) break; + dir = dir.parent_path(); + } + } + } + + // THIRD: the layout guess, kept for a driver that answers neither probe. auto root = cxx_binary.parent_path().parent_path(); auto fallback = root / "share" / "libc++" / "v1" / "std.cppm"; if (std::filesystem::exists(fallback)) return fallback; @@ -318,10 +359,22 @@ std::optional find_libcxx_std_compat_source( const std::filesystem::path& cxx_binary, const std::string& envPrefix) { - // Same search strategy as find_libcxx_std_module_source but for std.compat - auto root = cxx_binary.parent_path().parent_path(); - auto p = root / "share" / "libc++" / "v1" / "std.compat.cppm"; - if (std::filesystem::exists(p)) return p; + // DERIVED FROM THE SIBLING, NOT SEARCHED FOR SEPARATELY. + // + // The comment here used to say "same search strategy as + // find_libcxx_std_module_source", and it was not: that function has three + // probes and this one had the last of them, so on any payload the layout + // guess does not reach -- emsdk, for one -- `std` was found and + // `std.compat` was not, from one directory. + // + // `std.compat.cppm` sits beside `std.cppm` in every libc++ layout, because + // the same install rule places both. Deriving it makes the two answers + // structurally consistent rather than two searches that can disagree, + // which is what the comment claimed all along. + if (auto std_src = find_libcxx_std_module_source(cxx_binary, envPrefix)) { + auto p = std_src->parent_path() / "std.compat.cppm"; + if (std::filesystem::exists(p)) return p; + } return std::nullopt; } diff --git a/src/toolchain/compat.cppm b/src/toolchain/compat.cppm index acf165e4..0cd51728 100644 --- a/src/toolchain/compat.cppm +++ b/src/toolchain/compat.cppm @@ -126,6 +126,31 @@ std::optional normalize_spec(std::string_view compilerIn, return out; } + // ── canonical families whose payload carries its own target ───────────── + // + // NOT ALIASES AND NOT LEGACY. `em++` and the NDK's `clang++` are clang, so + // the FAMILY is llvm and there is no fourth value to invent; what these + // spellings add is which payload answers, and for emsdk also which target + // -- the payload compiles for exactly one, so a spec that names it has + // already named the target. `with_hint` is deliberately not used: a hint + // says "this spelling is old, here is the current one", and these are the + // current ones. + if (compiler == "emsdk" || compiler == "emscripten") { + out.family = "llvm"; + if (auto t = triple::parse("wasm32-emscripten")) out.target = *t; + if (muslVersionSuffix) return std::nullopt; + return out; + } + // The NDK serves BOTH Android arches from one payload, so it must NOT set + // a target: the arch arrives from `--target` or `[target.]`, and + // pinning one here would make `android-ndk@` mean aarch64 to a reader + // who typed it for x86_64. + if (compiler == "android-ndk" || compiler == "ndk") { + out.family = "llvm"; + if (muslVersionSuffix) return std::nullopt; + return out; + } + // ── legacy spellings ───────────────────────────────────────────────────── if (compiler == "clang") { // alias family → llvm out.family = "llvm"; diff --git a/src/toolchain/hostflags.cppm b/src/toolchain/hostflags.cppm index dea0b85a..6bb6e956 100644 --- a/src/toolchain/hostflags.cppm +++ b/src/toolchain/hostflags.cppm @@ -184,6 +184,35 @@ std::vector host_compile_tokens(const Toolchain& tc, const PathEscape& esc) { std::vector out; + // A TOOLCHAIN THAT SHIPS ITS OWN SYSROOT IS TOLD NOTHING. + // + // What this function emits is a target's system reconstructed onto the + // command line: libc++'s headers, glibc's, the Linux UAPI headers, the + // cfg bypass, the C-runtime prefix. Every one of those is an answer mcpp + // supplies because the payload's clang does not have one. An Emscripten or + // Android SDK does: `em++` bakes `--sysroot=/.../cache/sysroot` + // into every invocation and the NDK's clang derives its bionic sysroot + // from its own install prefix. + // + // Measured before this gate, on the std module precompile for + // `wasm32-emscripten`: + // + // em++ ... -isystem'/include' -isystem'/include' + // --precompile /share/libc++/v1/std.cppm + // /include/gnu/stubs.h:7: fatal error: + // 'gnu/stubs-32.h' file not found + // + // This host's glibc headers, handed to a wasm compile. The error names a + // missing 32-bit stub, so it reads as a broken glibc payload rather than + // as a C library that has no business being there. + // + // The cfg bypass is withheld too, and deliberately: it exists to stop + // clang reading a per-install `clang++.cfg`, while `em++` is a wrapper + // whose entire job is to supply configuration. Suppressing it would be + // suppressing the toolchain. + if (auto tt = triple::parse(tc.targetTriple); tt && tt->has_own_sysroot()) + return out; + // MSVC carries none of this on the command line: cl.exe and link.exe find // headers and import libraries through INCLUDE / LIB, which detection // synthesizes into tc.envOverrides. Emitting the GNU shapes below would diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 638014f6..e9342146 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -490,8 +490,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // From the payload ROOT, not `root/bin`: msvc keeps cl.exe // four levels deeper, and asking for `root/bin` skipped every // installed toolset silently. - auto bin = mcpp::toolchain::payload_frontend(vEntry.path(), pkg, - id->family); + auto bin = mcpp::toolchain::payload_frontend(vEntry.path(), pkg); if (bin.empty()) continue; payloads.push_back({ *id, s.version, bin }); } @@ -1103,7 +1102,7 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, // // Same rule as everywhere else in this round: installed means usable, // not present. - if (mcpp::toolchain::payload_frontend(installDir, pkg, spec->family).empty()) { + if (mcpp::toolchain::payload_frontend(installDir, pkg).empty()) { // Before "not installed", check whether this is the retired // `msvc@` spelling — otherwise the advice is to // install a toolset that does not exist and never will. diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index dff4b0a6..44ab9de6 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -14,6 +14,7 @@ export module mcpp.toolchain.probe; import std; import mcpp.toolchain.model; +import mcpp.toolchain.triple; import mcpp.xlings; import mcpp.platform; import mcpp.log; @@ -212,6 +213,31 @@ std::vector discover_link_runtime_dirs(const std::filesystem::path& compilerBin, std::string_view targetTriple) { std::vector dirs; + + // THE COMPILER'S OWN RUNTIME IS NOT THE ARTIFACT'S, AND FOR EVERY ROW + // BEFORE THESE TWO THE DISTINCTION DID NOT MATTER. + // + // What this returns goes on the ARTIFACT's link line as `-L` and `-rpath` + // (flags.cppm's runtime_dirs). For a payload that targets its own host + // that is right: the libstdc++ beside the compiler is the libstdc++ the + // artifact links. An SDK that cross-compiles breaks the coincidence -- + // `/lib` holds the host x86-64 libraries its own clang needs to + // RUN, and its sibling `xim:gcc-runtime` is a runtime dependency of the + // toolchain, not of a wasm module. + // + // Measured on `--target wasm32-emscripten`, after the sysroot and link + // model were already gated: + // + // wasm-ld: error: unknown file type: + // /lib64/libatomic.so + // + // and `atomic_link_flag` had found that libatomic precisely because this + // function had put its directory on the link dirs. The compiler's own + // needs are `compilerRuntimeDirs`, which this does not touch and which + // still gets them. + if (auto tt = triple::parse(targetTriple); tt && tt->has_own_sysroot()) + return dirs; + auto root = compilerBin.parent_path().parent_path(); if (!targetTriple.empty()) append_existing_unique(dirs, root / "lib" / std::string(targetTriple)); diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 609bcc34..68a32f62 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -2,7 +2,8 @@ // payload mapping. // // Identity (design §4.1–§4.3): a toolchain is `family@version` (family ∈ -// gcc | llvm | msvc), a target is a canonical Triple (triple.cppm). The two +// gcc | llvm | msvc | emsdk | android-ndk), a target is a canonical Triple +// (triple.cppm). The two // axes are orthogonal: "cross", "musl" and "mingw" are NOT names — the // variant lives in the target's env segment, and cross is the host≠target // relation. Which xim PACKAGE serves a (family, version, target, host) @@ -96,6 +97,27 @@ struct XimToolchainPackage { } std::string display_spec() const { return displaySpec; } + // WHERE THE FRONTEND LIVES, RELATIVE TO THE PAYLOAD ROOT. + // + // `bin` for every payload that grew up here, and that was hardcoded at the + // one place which composed a root with a bin directory -- with MSVC as a + // named exception four levels deeper. A third and fourth shape make the + // exception list the wrong structure: emsdk keeps `em++` in + // `emscripten/`, and the NDK keeps `clang++` in + // `toolchains/llvm/prebuilt//bin/`. Neither is unusual; what was + // unusual was asking the FAMILY where a PAYLOAD keeps its compiler. + // + // MSVC stays a branch rather than a subdirectory because its path carries + // the toolset version and the host/target arch pair, which is a lookup and + // not a constant. + std::string frontendSubdir = "bin"; + // THE FAMILY, CARRIED RATHER THAN PASSED ALONGSIDE. `payload_frontend` + // took it as a second argument, which every caller had to source from a + // differently-named local; five of them composed `payload->binDir` + // themselves instead and so could not see `frontendSubdir` at all. The + // package is built from a spec that has a family, so carrying it is free + // and removes an argument that could disagree with `pkg`. + Family family = Family::Gcc; }; std::expected @@ -143,8 +165,17 @@ std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, // here, which is the caller's cue to skip; a wrong LAYOUT and a missing // PAYLOAD had been reporting the same way. std::filesystem::path payload_frontend(const std::filesystem::path& payloadRoot, - const XimToolchainPackage& pkg, - Family family); + const XimToolchainPackage& pkg); + +// The DIRECTORY `payload_frontend` searched, for a message that has to name it. +// +// The five "has no known C++ frontend in " refusals printed +// `payload->binDir`, which was the directory they had composed themselves. Once +// the package decides where its frontend lives, a message naming `bin` would +// be naming a directory nothing looked in -- and that is the failure this +// codebase records most often: the lookup is fixed and the message is not. +std::filesystem::path payload_frontend_dir(const std::filesystem::path& payloadRoot, + const XimToolchainPackage& pkg); // Reverse mapping: an installed `xim-x-` payload directory back to its // (family, target) identity. nullopt for non-toolchain xpkgs (ninja, glibc, @@ -309,8 +340,9 @@ parse_toolchain_spec(std::string compilerArg, auto norm = compat::normalize_spec(compilerArg, versionArg); if (!norm) { return std::unexpected(std::format( - "unknown toolchain '{}' (expected gcc | llvm | msvc, or a " - "supported alias like mingw / musl-gcc)", compilerArg)); + "unknown toolchain '{}' (expected gcc | llvm | msvc | emsdk | " + "android-ndk, or a supported alias like mingw / musl-gcc)", + compilerArg)); } ToolchainSpec spec; @@ -390,10 +422,22 @@ bool gcc_native_payload_is_musl(std::string_view hostArch, bool isLinux, || (target.os == "linux" && target.arch == hostArch); } +// The NDK's own name for the HOST it runs on, which is the directory component +// under `toolchains/llvm/prebuilt/`. Upstream ships `linux-x86_64`, +// `darwin-x86_64` (a universal binary, so Apple silicon reads it too) and +// `windows-x86_64`. Not the target -- a Linux x86_64 machine building for +// aarch64 still reads `linux-x86_64`. +std::string ndk_host_tag() { + if constexpr (mcpp::platform::is_windows) return "windows-x86_64"; + else if constexpr (mcpp::platform::is_macos) return "darwin-x86_64"; + else return "linux-x86_64"; +} + XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { XimToolchainPackage pkg; pkg.displaySpec = spec.display(); pkg.ximVersion = spec.version; + pkg.family = spec.family; if (spec.family == Family::Msvc) { // `xim:msvc@`. Only reached for a VERSIONED spec — @@ -413,10 +457,53 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { return pkg; } if (spec.family == Family::Llvm) { - // ONE PAYLOAD. The `openkal-llvm` spelling normalises to this family and - // installs nothing of its own — it is a statement about where the - // TARGET SIDE comes from, and the compiler is the llvm payload either - // way. A user who has one has both. + // THE TARGET DECIDES THE PAYLOAD HERE TOO, and it did not used to. + // + // This returned the generic llvm payload unconditionally, which is + // right for every target llvm itself serves and wrong for the two that + // arrive with their own clang. `em++` and the NDK's `clang++` ARE + // clang -- same family, same flag vocabulary, same `import std` path + // -- and each is a clang whose target is fixed by its payload, which + // is the shape `src/toolchain/hostflags.cppm` already describes: + // "every hosted cross this build tool could do was served by a payload + // whose driver had exactly one target". So the family stays `Llvm` and + // no fourth value is invented; what changes is which package answers. + const auto& lt = spec.target; + + if (lt.os == "emscripten") { + // `em++` is a `#!/bin/sh` wrapper beside the Python it execs, in + // `emscripten/` rather than `bin/` -- `bin/` holds the raw clang, + // which would compile for wasm and then link like an ordinary + // clang, producing a `.wasm` with no JavaScript and none of + // Emscripten's own glue. Naming the wrapper is the whole point. + pkg.ximName = "emsdk"; + pkg.frontendSubdir = "emscripten"; + pkg.frontendCandidates = { "em++", "emcc" }; + return pkg; + } + + if (lt.is_android()) { + // One NDK payload serves every Android arch and API level: the + // arch arrives as `--target=-linux-android` on the + // command line, not as a different package. The host tuple in the + // path is the HOST's, not the target's -- a Linux x86_64 machine + // cross-compiling for aarch64 still reads + // `prebuilt/linux-x86_64/`. + pkg.ximName = "android-ndk"; + pkg.frontendSubdir = std::format("toolchains/llvm/prebuilt/{}/bin", + ndk_host_tag()); + if constexpr (mcpp::platform::is_windows) { + pkg.frontendCandidates = { "clang++.exe", "clang++" }; + } else { + pkg.frontendCandidates = { "clang++", "clang" }; + } + return pkg; + } + + // ONE PAYLOAD for everything else. The `openkal-llvm` spelling + // normalises to this family and installs nothing of its own — it is a + // statement about where the TARGET SIDE comes from, and the compiler is + // the llvm payload either way. A user who has one has both. pkg.ximName = mcpp::toolchain::llvm::package_name(); pkg.frontendCandidates = mcpp::toolchain::llvm::frontend_candidates(); return pkg; @@ -530,10 +617,22 @@ std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, return {}; } +std::filesystem::path payload_frontend_dir(const std::filesystem::path& payloadRoot, + const XimToolchainPackage& pkg) { + if (pkg.family == Family::Msvc) { + // The lookup's own answer, so the message names the toolset directory + // rather than a path this code would have guessed. + if (auto inst = mcpp::toolchain::msvc::installation_at(payloadRoot, + pkg.ximVersion)) + return inst->clPath.parent_path(); + return payloadRoot / "VC" / "Tools" / "MSVC" / pkg.ximVersion; + } + return payloadRoot / pkg.frontendSubdir; +} + std::filesystem::path payload_frontend(const std::filesystem::path& payloadRoot, - const XimToolchainPackage& pkg, - Family family) { - if (family == Family::Msvc) { + const XimToolchainPackage& pkg) { + if (pkg.family == Family::Msvc) { // Same resolution the install and build paths use, so the three // cannot disagree about where an msvc payload keeps its compiler. // below, which is the llvm payload's shape.) @@ -542,7 +641,7 @@ std::filesystem::path payload_frontend(const std::filesystem::path& payloadRoot, return inst->clPath; return {}; } - return toolchain_frontend(payloadRoot / "bin", pkg); + return toolchain_frontend(payloadRoot / pkg.frontendSubdir, pkg); } std::optional identify_xim_payload(std::string_view ximDirName) { @@ -602,6 +701,37 @@ bool needs_linux_sysroot_payloads(const triple::Triple& target) { bool host_can_serve(const triple::Triple& target) { if (target.empty()) return true; // host target + // AN SDK THAT SHIPS ITS OWN SYSROOT IS SERVED WHERE THE SDK IS PUBLISHED, + // AND THE ARCH IN THE TRIPLE IS THE GUEST'S. + // + // Every branch below reasons about a cross payload per host arch, because + // that is how a compiler targeting another Linux or another Windows is + // published here. An Emscripten or Android SDK is published per HOST and + // serves every guest arch from one archive: one `xim:emsdk` compiles for + // wasm32 regardless of the machine's arch, and one NDK serves both Android + // arches from a single `--target=-linux-android`. + // + // Without this the wasm row's own pin was not enough. Measured: with + // `emsdk@6.0.9` in the table, `mcpp build --target wasm32-emscripten` + // still answered "No toolchain payload here produces it" and listed + // seventeen servable targets -- because `target.os` is neither "linux" nor + // a Windows form, so control reached a `return false` written for triples + // nobody publishes a payload for. + // + // BUT "WHEREVER" WAS TOO BROAD, AND THE TARGET MATRIX IS WHAT CAUGHT IT. + // The first version of this returned true unconditionally, which is the + // same mistake as the branches it sits above: a predicate correct about + // the objects its author had in mind. `xim:emsdk` and `xim:android-ndk` + // both declare ONLY an `xpm.linux` table today, so on macOS or Windows + // there is no payload to install and the honest answer is the same + // `host-cannot-serve` every other unpublished combination gets. + // + // Keyed on the HOST and not on the arch, because these payloads are + // published per host OS and carry every guest arch. When a darwin or + // windows NDK lands in the index -- upstream publishes both -- this is the + // one line that changes. + if (target.has_own_sysroot()) return mcpp::platform::is_linux; + if (target.os == "linux") { if constexpr (mcpp::platform::is_linux) { // "SELF-CONTAINED" IS ABOUT THE PAYLOAD'S CONTENTS, NOT ABOUT diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 0badcac4..edbd76e9 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -274,15 +274,15 @@ payload linux-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 aarch64-ios msvc@system - - - - - unsupported tier-planned -graph linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported tier-planned -payload linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported tier-planned -payload macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported tier-planned +graph linux-aarch64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +graph linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +graph linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +graph macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve +graph windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve +graph windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported host-cannot-serve +payload linux-aarch64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +payload linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +payload linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +payload macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve +payload windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve +payload windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported host-cannot-serve diff --git a/tests/unit/test_distribution.cpp b/tests/unit/test_distribution.cpp index db33297a..a2b2f44d 100644 --- a/tests/unit/test_distribution.cpp +++ b/tests/unit/test_distribution.cpp @@ -746,3 +746,44 @@ TEST(Distribution, ASharedLibraryStillHidesTheArchivesWithoutASecondRuntime) { " -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a" " /tc/libunwind.a -Wl,--exclude-libs,libunwind.a"); } + +// ─── Format::Wasm, the member the module predicted and deferred ──────────── +// +// `format_for`'s own comment said a wasm triple "falls out of every branch" +// and reaches `hostFallback`, answering the MACHINE's format -- "the same +// defect class its own header measured for macOS" -- and deferred the fourth +// member "to whoever gives this module a mechanism for it". +TEST(Distribution, FormatAnswersWasmForAWasmTargetAndNotTheHosts) { + using mcpp::build::dist::Format; + using mcpp::build::dist::format_for; + // Every host fallback, so this cannot pass by agreeing with the machine. + for (auto fallback : {Format::Elf, Format::MachO, Format::Pe}) + EXPECT_EQ(format_for("wasm32-emscripten", fallback), Format::Wasm); + // The three it already answered stay unchanged. + EXPECT_EQ(format_for("x86_64-linux-gnu", Format::Pe), Format::Elf); + EXPECT_EQ(format_for("aarch64-macos", Format::Elf), Format::MachO); + EXPECT_EQ(format_for("x86_64-windows-gnu", Format::Elf), Format::Pe); +} + +// THERE IS NOTHING TO BE COUPLED TO, so the contract is satisfied with +// nothing added and there is no degradation to report. +// +// While the member was missing, every wasm build printed a warning about a +// `libc++.so` that cannot exist for the target, on an artefact that has no +// run-time dependency of any kind. A diagnostic is for a BROKEN PROMISE; that +// one was a promise about a mechanism the format does not have. +TEST(Distribution, WasmIsSelfContainedByConstructionAndSaysNothing) { + mcpp::build::dist::MechanismInput in; + in.format = mcpp::build::dist::Format::Wasm; + in.stdlibId = "libc++"; + in.requested = mcpp::build::dist::Contract::SelfContained; + in.role = mcpp::build::dist::Role::Distributable; + auto m = mcpp::build::dist::resolve(in); + EXPECT_EQ(m.effective, mcpp::build::dist::Contract::SelfContained); + EXPECT_FALSE(m.degraded); + EXPECT_TRUE(m.diagnostic.empty()) << m.diagnostic; + // And no flags: libc++ reaches a wasm link through `em++`'s own link line, + // so naming archives from a sysroot this module did not resolve would be a + // second answer to a question the driver has already answered. + EXPECT_TRUE(m.unitFlags.empty()) << m.unitFlags; +} diff --git a/tests/unit/test_linkmodel.cpp b/tests/unit/test_linkmodel.cpp index a4f3b5c2..76354229 100644 --- a/tests/unit/test_linkmodel.cpp +++ b/tests/unit/test_linkmodel.cpp @@ -237,3 +237,31 @@ TEST(LinkModel, NothingUsableYieldsNoneAndEmptyFlags) { // 设计:.agents/docs/2026-08-08-payload-version-and-contract-drift-design.md §3.2 } // namespace + +// ─── An SDK target gets CLibMode::None, and the gate is in the MODEL ─────── +// +// The C-runtime group reaches the link line through TWO channels -- flags.cppm's +// `link_toolchain_flags` and its `payload_ld` -- both rendering +// `lm.link_flags()`. The comment at the second one records that a reader who +// fixed only the first "saw the identical error and could reasonably conclude +// the fix had not worked". So the gate belongs here, where both read it. +// +// Measured on `--target wasm32-emscripten` with the compile side already +// correct: `wasm-ld: error: unknown argument: +// --dynamic-linker=/lib64/ld-linux-x86-64.so.2` -- this host's loader +// handed to a WebAssembly linker. +TEST(LinkModel, AnSdkTargetDescribesNoCLibrary) { + for (auto target : {"wasm32-emscripten", "aarch64-linux-android", + "x86_64-linux-android"}) { + auto tc = mcpp::toolchain::Toolchain{}; + tc.compiler = mcpp::toolchain::CompilerId::Clang; + tc.targetTriple = target; + auto lm = mcpp::toolchain::resolve_link_model(tc); + EXPECT_EQ(lm.mode, mcpp::toolchain::CLibMode::None) << target; + EXPECT_TRUE(lm.libDirs.empty()) << target; + EXPECT_TRUE(lm.crtDir.empty()) << target; + // And the rendered flags are empty, which is what both channels emit. + EXPECT_TRUE(lm.link_flags([](const std::filesystem::path& p) { + return p.string(); }).empty()) << target; + } +} diff --git a/tests/unit/test_toolchain_registry.cpp b/tests/unit/test_toolchain_registry.cpp index 2437d968..a69f41ec 100644 --- a/tests/unit/test_toolchain_registry.cpp +++ b/tests/unit/test_toolchain_registry.cpp @@ -295,3 +295,81 @@ TEST(ToolchainSysrootDeps, OneDerivationForTheGlibcSysrootPayloads) { EXPECT_FALSE(needs_linux_sysroot_payloads(t)); } } + +// ─── An SDK payload is chosen by the TARGET, and knows its own layout ────── +// +// `to_xim_package` returned the generic llvm payload for every `Family::Llvm` +// spec, which is right for the targets llvm itself serves and wrong for the two +// that arrive with their own clang. `em++` and the NDK's `clang++` ARE clang -- +// same family, same flag vocabulary -- so no fourth `Family` value exists; +// what changes is which package answers and where its driver lives. +TEST(SdkPayloads, TheTargetChoosesThePackageAndThePackageKnowsItsLayout) { + auto pkg_for = [](std::string_view target) { + auto spec = mcpp::toolchain::parse_toolchain_spec("emsdk@6.0.9"); + // The spec's own target is replaced, because the NDK case must be + // reachable from the same family with a different triple. + auto s = *spec; + if (auto t = mcpp::toolchain::triple::parse(target)) s.target = *t; + return mcpp::toolchain::to_xim_package(s); + }; + + { // Emscripten: `em++` is a wrapper in `emscripten/`, NOT the raw clang + // in `bin/`. Naming the wrapper is the whole point -- `bin/clang` + // compiles for wasm and then links like an ordinary clang, producing a + // module with none of Emscripten's JavaScript glue. + auto pkg = pkg_for("wasm32-emscripten"); + EXPECT_EQ(pkg.ximName, "emsdk"); + EXPECT_EQ(pkg.frontendSubdir, "emscripten"); + ASSERT_FALSE(pkg.frontendCandidates.empty()); + EXPECT_EQ(pkg.frontendCandidates.front(), "em++"); + } + { // Android: ONE payload for both arches -- the arch arrives as + // `--target=-linux-android`, not as a different package -- + // and the host tuple in the path is the HOST's, not the target's. + for (auto target : {"aarch64-linux-android", "x86_64-linux-android"}) { + auto pkg = pkg_for(target); + EXPECT_EQ(pkg.ximName, "android-ndk") << target; + EXPECT_NE(pkg.frontendSubdir.find("toolchains/llvm/prebuilt/"), + std::string::npos) << target; + // The HOST, so an aarch64 Linux machine cross-compiling still + // reads `linux-x86_64`. Asserted as "not the target's arch" rather + // than against a literal, so this test says the same thing on + // every runner. + EXPECT_EQ(pkg.frontendSubdir.find("aarch64-linux-android"), + std::string::npos) << target; + } + } + { // And every other target still gets the generic llvm payload in bin/. + auto pkg = pkg_for("x86_64-linux-gnu"); + EXPECT_NE(pkg.ximName, "emsdk"); + EXPECT_NE(pkg.ximName, "android-ndk"); + EXPECT_EQ(pkg.frontendSubdir, "bin"); + } +} + +// THE MESSAGE MUST NAME THE DIRECTORY THAT WAS SEARCHED. +// +// Five refusals printed `payload->binDir`, the directory they had composed +// themselves. Once the package decides where its frontend lives, `bin` is a +// directory nothing looked in -- and this codebase's most frequent defect is a +// fixed lookup with an unfixed message. +TEST(SdkPayloads, TheSearchedDirectoryIsAvailableForTheDiagnostic) { + auto spec = mcpp::toolchain::parse_toolchain_spec("emsdk@6.0.9"); + ASSERT_TRUE(spec.has_value()); + auto pkg = mcpp::toolchain::to_xim_package(*spec); + auto dir = mcpp::toolchain::payload_frontend_dir("/p/xim-x-emsdk/6.0.9", pkg); + EXPECT_EQ(dir, std::filesystem::path("/p/xim-x-emsdk/6.0.9/emscripten")); +} + +// AN SDK IS SERVED WHERE THE SDK IS PUBLISHED, and the first version of this +// said "wherever" -- the same over-broad shape as the branches it sits above. +// The target matrix caught it: declaring the row servable on macOS and Windows +// would have claimed a payload that does not exist there. +TEST(SdkPayloads, ServedOnTheHostsTheSdkIsPublishedFor) { + auto wasm = mcpp::toolchain::triple::parse("wasm32-emscripten"); + ASSERT_TRUE(wasm.has_value()); + EXPECT_EQ(mcpp::toolchain::host_can_serve(*wasm), mcpp::platform::is_linux); + auto droid = mcpp::toolchain::triple::parse("aarch64-linux-android"); + ASSERT_TRUE(droid.has_value()); + EXPECT_EQ(mcpp::toolchain::host_can_serve(*droid), mcpp::platform::is_linux); +} diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 3302b3a8..e41c6b43 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -628,30 +628,99 @@ TEST(Triple, EmscriptenIsAnOsSegmentAndNotAnEnv) { EXPECT_FALSE(t->nasm_format().has_value()); } -TEST(Triple, TheThreePlatformsAreRegisteredAndPlanned) { - // A row here is what every layer above waits on: the `.apk` step, the - // `.app` step, the `.html`+`.wasm` step, the runner and the signing all - // attach to a triple, and a package cannot add one. - // - // `planned` is a REFUSAL and not a gap -- the tier gate answers - // `tier-planned` naming the row, rather than `unknown target`, which would - // be false, or a build that resolves and produces nothing, which is worse. +TEST(Triple, TheWasmRowIsWiredAndTheOtherThreeAreStillPlanned) { + // WHAT A TIER ASSERTS. `verified` in this table means an artefact was + // built AND RUN, and `wasm32-emscripten` now is: measured 2026-09-11 on + // linux-x86_64 with xim:emsdk 6.0.9, `mcpp run --target wasm32-emscripten` + // on a source that imports std printed `1-2-3`. + { + auto t = parse("wasm32-emscripten"); + ASSERT_TRUE(t.has_value()); + auto* info = find_known_target(*t); + ASSERT_NE(info, nullptr); + EXPECT_EQ(info->tier, "verified"); + // A ROW THAT IS WIRED NAMES ITS PAYLOAD. Without the pin the row's + // tier was reachable only through an explicit + // `[target.wasm32-emscripten] toolchain = "..."` override, which is + // the escape hatch and not the support claim. + EXPECT_EQ(info->pin, "emsdk@6.0.9"); + // No `sysroot` column, and that is a statement: the SDK ships one, so + // there is no separate C library for the row to name. + EXPECT_TRUE(info->sysroot.empty()); + } + + // The other three stay `planned`, with no pin and no sysroot, because what + // each still needs is execution evidence rather than vocabulary. A tier + // that moved on expectation would be the one thing this column cannot be. for (auto name : {"aarch64-linux-android", "x86_64-linux-android", - "aarch64-ios", "wasm32-emscripten"}) { + "aarch64-ios"}) { auto t = parse(name); ASSERT_TRUE(t.has_value()) << name; EXPECT_EQ(t->str(), name); auto* info = find_known_target(*t); ASSERT_NE(info, nullptr) << name; EXPECT_EQ(info->tier, "planned") << name; - // No pin and no sysroot: what each row still needs is a PAYLOAD, and - // naming a compiler that cannot serve the target would be a claim the - // row cannot keep. EXPECT_TRUE(info->pin.empty()) << name; EXPECT_TRUE(info->sysroot.empty()) << name; } } +// DOES THIS TARGET'S TOOLCHAIN ARRIVE WITH ITS OWN COMPLETE SYSTEM? +// +// The predicate exists because mcpp reconstructs a target's system by hand -- +// libc++'s headers, glibc's, the Linux UAPI headers, the C-runtime prefix, the +// loader -- and for a target whose SDK ships a sysroot every one of those is an +// answer competing with one the driver already has. Three independent sites +// read it, and each was found by the previous one's failure: +// +// host_compile_tokens this host's stdint.h reached a wasm compile +// resolve_link_model --dynamic-linker=...ld-linux-x86-64.so.2 reached wasm-ld +// discover_link_runtime_dirs the COMPILER's libatomic reached the ARTIFACT's link line +// +// Asserted as an exhaustive statement over the table rather than on examples, +// so a new row cannot join the set by accident or be left out of it. +TEST(Triple, OnlyTheSdkTargetsShipTheirOwnSysroot) { + std::set shipsOwn; + for (auto& row : known_targets()) { + auto t = parse(row.canonical); + ASSERT_TRUE(t.has_value()) << row.canonical; + if (t->has_own_sysroot()) shipsOwn.insert(std::string(row.canonical)); + } + EXPECT_EQ(shipsOwn, (std::set{ + "aarch64-linux-android", "x86_64-linux-android", "wasm32-emscripten"})); + + // `aarch64-ios` is NOT in the set, and that is the interesting exclusion. + // The iPhoneOS SDK does ship a sysroot -- but mcpp reaches it with + // `-isysroot`, which this predicate is not about: the question here is + // whether the DRIVER resolves the system without being told, and an + // ordinary clang pointed at an SDK does not. + auto ios = parse("aarch64-ios"); + ASSERT_TRUE(ios.has_value()); + EXPECT_FALSE(ios->has_own_sysroot()); +} + +// A CAPABILITY PIN CANNOT BE OVERRIDDEN, BECAUSE NOTHING ELSE CAN EMIT THE +// TARGET. A convention pin is a preference; this is a fact about the world. +TEST(Triple, WasmJoinsTheCapabilityPinsBecauseNothingElseEmitsIt) { + auto wasm = parse("wasm32-emscripten"); + ASSERT_TRUE(wasm.has_value()); + EXPECT_TRUE(wasm->pin_is_capability()) + << "a declared gcc@16.1.0 would otherwise override emsdk@6.0.9 and " + "fail inside a compiler that cannot emit WebAssembly"; + + // The two that were there before, unchanged. + EXPECT_TRUE(parse("riscv64-none-elf")->pin_is_capability()); + EXPECT_TRUE(parse("x86_64-windows-musl")->pin_is_capability()); + // And an ordinary hosted row is still a convention: a project may name + // whichever compiler it likes for its own Linux. + EXPECT_FALSE(parse("x86_64-linux-musl")->pin_is_capability()); + // ANDROID IS NOT ONE EITHER, deliberately. The NDK's clang is the only + // thing that serves it today, but the row carries no pin yet, so calling + // the absent pin a capability statement would assert something the table + // does not say. It moves when the row does. + EXPECT_FALSE(parse("aarch64-linux-android")->pin_is_capability()); +} + TEST(Triple, TheCanonicalSpellingIsNotSEARCHABLEForAVENDORNAME) { // WHY A SUBSTRING TEST ON THE CANONICAL TRIPLE IS WRONG, stated as a fact // about the vocabulary rather than as a comment somewhere else. From cb6d6ba2c2a7ead018ef7ffed5f7158a6d2875e0 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:27:25 +0800 Subject: [PATCH 04/13] docs(targets): the target name is a normalisation, the SDK binding differs by platform, and codesign was never a host dependency Three questions answered by measurement rather than by recall. IS `wasm32-emscripten` STANDARD. No -- the industry name is the four-field `wasm32-unknown-emscripten`, which is what `em++ -v` passes to its own clang on this machine and what rustc's table lists. mcpp's short form is its own canonical spelling, the same vendor elision it already performs for `aarch64-macos` and `x86_64-windows-gnu`, with `llvm_triple()` restoring the field. That is defensible and should be stated as what it is rather than implied to be the common name -- hence R8: `parse()` should accept the four-field spelling every other toolchain prints. And the wasm family is nine targets in rustc, which settles the simulator question from an unexpected direction: Rust appends a FOURTH component for a variant (`-threads` there, `-sim` for iOS), and mcpp's `env` is exactly that slot, already used that way by `gnu`, `musl` and `eabihf`. `env = "sim"` is the field doing its job, not a workaround. IS THE TOOLCHAIN BOUND TO THE SDK. Two answers, not one. Emscripten and the NDK each ship compiler and sysroot as ONE archive, which is precisely the set `has_own_sysroot()` names -- the predicate is not an arbitrary pair. iOS does not: any sufficiently new clang plus `-isysroot`, which makes the row structurally the SAME SHAPE as bare metal (a generic llvm pin plus a separately-versioned sysroot package), so the `sysroot` column already exists for it and the row needs no new table machinery. Also worth stating because the naming misleads: "the Android SDK" names two unrelated things, and the four xim packages split on what Google actually ships rather than on a decomposition choice. WHAT MUST GO THROUGH THE HOST. Exactly three things, and a falsifiable test for them: a kernel facility, a proprietary RUNTIME that exists only on its own OS, or a credential. `/dev/kvm`, the Simulator or a device, and notarization. Everything that is a PROGRAM can be packaged -- and the survey found the third instance of this ecosystem mistaking a program for a host dependency: `rcodesign` (crate apple-codesign, MPL-2.0) signs Mach-O binaries, `.app` bundles, `.pkg` and `.dmg` with no Apple hardware or OS, and ships prebuilt static binaries for linux-musl on both arches, macOS universal and Windows. So `codesign` is not a host dependency; it is an unpackaged one (R7). The test also narrows R4: `msvc@system` is the (b) case, and the existing refusal is right about the general shape and wrong only about believing MSVC is its sole instance. --- ...26-09-11-platform-targets-design-review.md | 177 +++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index 38792f59..27e70524 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -307,6 +307,8 @@ package -- no new mechanism, again. | R4 | `@system` for a non-MSVC family | generalise it, with the row deciding whether it is permitted | the refusal argues from MSVC's uniqueness, and iOS is the second instance of exactly that situation. `xim:iphoneos-sdk` already names "locate what the machine has" as its third tier, and the engine has no spelling for it | | R5 | device and simulator sessions | a `xim:` package shipping a runner program, named by `runner` | cargo states this boundary explicitly; no engine change, no new member family, and it puts platform knowledge in the ecosystem | | R6 | the tier each row can reach | `verified` for wasm (reached); `preview` for both Android rows and for iOS | Rust rates all three Tier 2. `verified` for Android is reachable and needs a CI lane, not a design | +| R7 | signing a Mach-O or a `.app` | package `rcodesign` as `xim:rcodesign` and have `dist-apple` prefer it | MPL-2.0 with prebuilt static binaries for linux-musl (both arches), macOS universal and Windows. It removes the last host dependency from the iOS BUILD path, leaving only a device, the Simulator runtime and a notarization credential -- none of which is a program | +| R8 | the four-field spelling | `parse()` should accept `wasm32-unknown-emscripten` and `aarch64-apple-ios` and canonicalise them | the industry writes four fields; refusing the spelling every other toolchain prints is a UX cost with no design benefit, and `parse()` already normalises several aliases | ## 7. User-facing experience, which is the test of all of the above @@ -334,7 +336,180 @@ and zero new vocabulary for the project.** A project that builds for Linux builds for the web by changing one flag. That is the argument for the design, and it is now measured rather than asserted. -## 8. The rule, stated so the next platform does not need this document +## 8. Is `wasm32-emscripten` a standard name, and a common one? + +Two different questions, and the answers differ. + +### 8.1 The industry name is the four-field one, and mcpp's is a normalisation + +Measured, not recalled -- `em++ -v` on this machine passes to its own clang: + + -target wasm32-unknown-emscripten + +and `rustc`'s platform table lists the same spelling at Tier 2 with host +tools. That is the industry name. + +mcpp writes `wasm32-emscripten`, and `llvm_triple()` restores the vendor +(`triple.cppm:172` emits `arch + "-unknown-emscripten"`). So the short form is +**mcpp's canonical spelling, not a spelling anyone else uses** -- and it is the +same elision mcpp already performs everywhere: `aarch64-macos` becomes +`aarch64-apple-darwin`, `x86_64-windows-gnu` becomes `x86_64-w64-windows-gnu`. + +That is defensible and should be stated as what it is. mcpp's `Triple` has +three fields and no vendor, deliberately, and `unknown` is a placeholder that +carries no information for any target in the table. A user who types the +four-field form should still be understood, which is a `parse()` question +rather than a naming one. + +### 8.2 The wasm family is nine targets, and the three-field model holds + +`rustc`'s table lists nine: + + wasm32-unknown-emscripten Tier 2 with host tools + wasm32-unknown-unknown Tier 2 with host tools + wasm32-wasip1 Tier 2 with host tools + wasm32-wasip1-threads Tier 2 with host tools + wasm32-wasip2 Tier 2 with host tools + wasm32v1-none Tier 2 without host tools + wasm64-unknown-unknown Tier 3 + wasm32-wali-linux-musl Tier 3 + wasm32-wasip3 Tier 3 + +Mapped onto `Triple{arch, os, env}`: + +| Rust | mcpp | field use | +|---|---|---| +| `wasm32-unknown-emscripten` | `wasm32-emscripten` | os = emscripten | +| `wasm32-unknown-unknown` | `wasm32-none` | os = none, i.e. `is_freestanding()` | +| `wasm32-wasip1` | `wasm32-wasi` + env | os = wasi | +| `wasm32-wasip1-threads` | env = `p1-threads` | **env absorbs Rust's fourth component** | +| `wasm64-unknown-unknown` | `wasm64-none` | arch = wasm64 | + +The last row of that table is the important one, and it settles §4.1 from an +unexpected direction. Rust appends a fourth component for a *variant*: +`-threads` here, `-sim` for the iOS simulator. mcpp has exactly one slot for +it, `env`, and the wasm family shows that slot is adequate and already used +that way by every other row (`gnu`, `musl`, `eabihf`). So **`env = "sim"` is +not a workaround; it is the field doing its job**, and R1 is a use of the model +rather than a stretch of it. + +## 9. Is the toolchain bound to the SDK? Three platforms, two answers + +This is the question that most changes how a row is written, and the three +platforms do not agree. + +| platform | compiler | its system | one archive? | mcpp's columns | +|---|---|---|---|---| +| Emscripten | `em++` (clang) | `/emscripten/cache/sysroot` | **yes** | `pin = emsdk@6.0.9`, `sysroot` empty | +| Android | NDK's `clang++` | bionic, in `toolchains/llvm/prebuilt//sysroot` | **yes** | `pin = android-ndk@V`, `sysroot` empty | +| iOS | **any sufficiently new clang** | the iPhoneOS SDK, reached with `-isysroot` | **no** | `pin = llvm@V`, `sysroot = xim:iphoneos-sdk@V` | + +So `has_own_sysroot()` is not an arbitrary set of two: it is exactly the +platforms whose compiler and system arrive as one payload, and that is why the +predicate reads the way it does. + +**And iOS is structurally the same shape as bare metal.** `riscv64-none-elf` +pins `llvm@22.1.8` and names `xim:picolibc-riscv@1.8.12` in the `sysroot` +column -- a generic clang plus a separately-versioned system. iOS is that +shape with a different sysroot package. The `sysroot` column already exists +for precisely this, which means the iOS row needs **no new table machinery**, +only the two columns filled. + +The consequence for the other two is the opposite: a `sysroot` entry for +Emscripten or Android would be wrong, because the driver resolves its own and a +second answer competes with it -- which is the defect the wasm row hit three +times. + +### 9.1 "The Android SDK" names two unrelated things, and the packages split on that + +Worth stating because the naming misleads: + +* the **NDK** is the compiler and bionic -- one archive, bound, used at BUILD + time. `xim:android-ndk`. +* the **SDK** proper is `platform-tools` (adb, fastboot), the emulator, system + images and build-tools -- used at RUN and PACKAGE time, and unbound both from + each other and from the compiler. `xim:android-platform-tools`, + `xim:android-emulator`, `xim:android-system-image`. + +Four packages rather than one is therefore not a decomposition choice; it is +what upstream actually ships, and each is independently versioned by Google. + +## 10. What can close inside the ecosystem, and what cannot + +The preference is stated: close the loop inside xlings, and reach the host only +where nothing else is possible. Enumerated per platform rather than argued. + +### 10.1 Closed today, measured + +| need | package | evidence | +|---|---|---| +| `em++`, the wasm sysroot and its module surface | `xim:emsdk` | `mcpp run --target wasm32-emscripten` printed `1-2-3` | +| the interpreter `em++` execs | `xim:python` | needed an aarch64 payload added; declared as a dep | +| the JS engine the artefact needs | `xim:node` | the artefact's own `#!/usr/bin/env node` resolves the xvm shim | +| the Android compiler and bionic | `xim:android-ndk` | `import std` built for both Android arches | +| an aarch64 loader and a real bionic | `xim:android-system-image` | `debugfs` extraction, then `qemu-aarch64 -L` ran the DYNAMIC artefact | +| the user-mode translator | `xim:qemu-user-aarch64` | same measurement | +| the ext4 reader that extraction needs | `xim:e2fsprogs` | declared; was a host probe | +| `adb` / `fastboot` | `xim:android-platform-tools` | installs on all three hosts | +| the emulator and its X11 chain | `xim:android-emulator` + six existing libs | declared; was a host probe | + +### 10.2 Closeable, and one of them is a finding + +| need | how | status | +|---|---|---| +| a clang that targets iOS | `xim:llvm` plus `-isysroot` | the payload exists; the row is unfilled | +| the iPhoneOS SDK | `xim:iphoneos-sdk`, at whichever of three licence tiers applies | exists | +| **signing a Mach-O, a `.app`, a `.dmg` or a `.pkg`** | **`rcodesign`** (crate `apple-codesign`, MPL-2.0) | **not yet packaged, and it should be** | +| finding the SDK path | nothing -- `xcrun` is a path-finder and `-isysroot ` needs none | no dependency | + +The third row changes the iOS picture. `apple-codesign` states its goal as +being "a stand-in replacement for Apple's `codesign` ... without a dependency +on an Apple hardware device or operating system", covering Mach-O binaries, +`.app` bundles, `.pkg` installers and `.dmg` images. Release 0.29.0 ships +**prebuilt static binaries for `x86_64-unknown-linux-musl`, +`aarch64-unknown-linux-musl`, macOS universal and Windows**, under MPL-2.0. + +So signing -- which `dist/apple.cppm` currently reaches through the host's +`codesign` -- **is not a host dependency at all**. It is an unpackaged one. + +Sources: [apple-codesign on crates.io](https://crates.io/crates/apple-codesign) +and its [documentation](https://gregoryszorc.com/docs/apple-codesign/stable/). + +### 10.3 Genuinely host-bound, and there are exactly three + +1. **`/dev/kvm`** -- a kernel facility. No package ships a kernel feature, and + group membership is a machine's configuration. This is why it is the only + `log.warn` left in `android-emulator.lua`. +2. **A real device, or Apple's Simulator runtime.** The simulator is a + proprietary runtime that exists only on macOS and is not redistributable; + a device is hardware. Both are *environments* rather than tools. +3. **Notarization.** Apple's servers plus a developer credential. A credential + is never a package, and `rcodesign` can drive the submission but cannot + supply the account. + +### 10.4 The rule that falls out + + A host dependency is legitimate only when the thing needed is + (a) a kernel facility, + (b) a proprietary RUNTIME that exists only on its own OS, or + (c) a credential. + Anything that is a PROGRAM can be packaged, and the survey found that + even Apple's signing tool has a redistributable replacement. + +That test is worth having because it is falsifiable, and it immediately +reclassifies two things this ecosystem had treated as host dependencies: +`debugfs` and the libX11 chain were (a)-shaped in the recipes' prose and were +in fact just programs. `codesign` is the third instance of the same mistake, +and this document is the first place it is named. + +It also narrows R4. `msvc@system` is a (b): Visual Studio is a proprietary +toolchain that exists only where it is installed. Generalising `@system` +should therefore mean "a row may declare that its system is host-located +because it is (b)", not "any family may be located on the host" -- the +existing refusal is right about the general case and wrong only about +believing MSVC is the sole instance. + +## 11. The rule, stated so the next platform does not need this document Three questions, and the answer to each is the same for every platform: From 47b3c3ac6afad4762154d7e3d5d6909dbce73a42 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:34:42 +0800 Subject: [PATCH 05/13] docs(targets): signing closes in the ecosystem, and the iOS runtime is blocked by an image licence rather than by tooling SIGNING BELONGS IN THREE LAYERS THAT ALREADY EXIST, in this order: `rcodesign` is a program so it is a xim package; what invokes it is a `dist-*` member; what the user types is `mcpp pack --format `. Release 0.29.0 signs BUNDLES and not only flat Mach-O binaries -- its changelog discusses `--shallow` bundle mode and child-bundle signing "compatible with the behavior of Apple's codesign" -- which is exactly what a `.app` inside an `.ipa` needs. `--format ipa` needs NO NEW TOOL AT ALL: an `.ipa` is a zip containing `Payload/.app/`, so clang plus the iPhoneOS SDK produce the Mach-O (both xim), `dist-apple` assembles the bundle, `rcodesign` signs it, and a zip step finishes. So iOS PACKAGING closes entirely inside the ecosystem. `--format dmg` and `--format pkg` each need a creator as well as a signer (`libdmg-hfsplus`, `xar` -- both open source, neither measured), and are recorded as gaps with a known shape rather than claimed. THE iOS RUNTIME BLOCKER IS A LICENSED IMAGE, NOT A MISSING EMULATOR, and the obvious answer is wrong in an instructive way. QEMU can emulate ARM iOS hardware and community projects boot iOS on it; what none can supply is the iOS kernel and root filesystem, because distributing iOS images is against Apple's terms. The emulator is not the scarce thing. That is the SAME SHAPE as the Android question with the opposite answer, and the comparison is the point: Android's emulator is Apache-2.0 and its `default` images are AOSP, so the loop closes -- and `qemu-aarch64 -L` needs no emulator at all. iOS cannot close, for a reason that is not about mcpp, xlings or effort. It is the one row whose execution is blocked by a licence rather than by work. On macOS the Simulator is the host's and that is correct: it is a (b)-category proprietary runtime and `xcrun simctl spawn` is an argv prefix, so section 5's model already covers it once R1 gives the simulator a row. And Darling is recorded as an UNMEASURED candidate for the macOS rows -- GPL-3.0, actively developed, and it reimplements Darwin's libraries rather than redistributing them, so unlike the iOS image it carries no licence blocker. A row does not move on a plausible mechanism. --- ...26-09-11-platform-targets-design-review.md | 96 ++++++++++++++++++- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index 27e70524..1439f062 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -309,6 +309,9 @@ package -- no new mechanism, again. | R6 | the tier each row can reach | `verified` for wasm (reached); `preview` for both Android rows and for iOS | Rust rates all three Tier 2. `verified` for Android is reachable and needs a CI lane, not a design | | R7 | signing a Mach-O or a `.app` | package `rcodesign` as `xim:rcodesign` and have `dist-apple` prefer it | MPL-2.0 with prebuilt static binaries for linux-musl (both arches), macOS universal and Windows. It removes the last host dependency from the iOS BUILD path, leaving only a device, the Simulator runtime and a notarization credential -- none of which is a program | | R8 | the four-field spelling | `parse()` should accept `wasm32-unknown-emscripten` and `aarch64-apple-ios` and canonicalise them | the industry writes four fields; refusing the spelling every other toolchain prints is a UX cost with no design benefit, and `parse()` already normalises several aliases | +| R9 | `mcpp pack --format ipa` | a `dist-ipa` member: zip `Payload/.app/`, after `dist-apple` and `rcodesign` | it needs NO new tool. Every other link is already in the ecosystem or one member away, so iOS PACKAGING closes entirely -- what does not close is the credential and the runtime | +| R10 | `--format dmg` and `--format pkg` | recorded as gaps with a known shape, not attempted | each needs a *creator* as well as a signer (`libdmg-hfsplus`; `xar`), both open source and neither measured here | +| R11 | the macOS rows' runner | Darling recorded as an unmeasured candidate | GPL-3.0, active, and it REIMPLEMENTS Darwin's libraries rather than redistributing them, so unlike the iOS image it carries no licence blocker. A row does not move on a plausible mechanism, so this is a candidate and not a plan | ## 7. User-facing experience, which is the test of all of the above @@ -475,19 +478,102 @@ So signing -- which `dist/apple.cppm` currently reaches through the host's Sources: [apple-codesign on crates.io](https://crates.io/crates/apple-codesign) and its [documentation](https://gregoryszorc.com/docs/apple-codesign/stable/). -### 10.3 Genuinely host-bound, and there are exactly three +### 10.3 Signing belongs in the ecosystem AND in the plugin system, in that order + +`rcodesign` is a program, so it is a `xim:` package; what it is invoked BY is a +`dist-*` member; and what the user types is `mcpp pack --format `. All +three layers already exist, which is why this needs no new mechanism -- only +the package and the members. + + xim:rcodesign the program (MPL-2.0, prebuilt static) + dist-apple --format app the bundle exists, uses the host's codesign today + dist-ipa --format ipa the shippable file does not exist + dist-dmg --format dmg a disk image does not exist + dist-pkg --format pkg an installer does not exist + +Release 0.29.0 signs **bundles**, not only flat Mach-O binaries -- its +changelog discusses `--shallow` bundle mode and child-bundle signing "compatible +with the behavior of Apple's `codesign`" -- which is exactly what a `.app` +inside an `.ipa` needs. + +**`--format ipa` is the one that needs no new tool at all.** An `.ipa` is a zip +containing `Payload/.app/`, so the chain is: clang plus the iPhoneOS SDK +produce the Mach-O (both `xim:`), `dist-apple` assembles the bundle, `rcodesign` +signs it, and a zip step produces the file. Every link is already in the +ecosystem or is one member away. + +`--format dmg` and `--format pkg` each need a *creator* as well as a signer, +and neither creator is packaged: a `.dmg` is an HFS+/APFS image (Apple's +`hdiutil`, or `libdmg-hfsplus` off macOS) and a `.pkg` is an XAR archive +(Apple's `pkgbuild`, or `xar`). Both alternatives are open source and neither +has been measured here, so they are named as gaps with a known shape rather +than claimed. + +### 10.4 The iOS runtime: the blocker is a licensed IMAGE, not a missing emulator + +This is the question worth getting exactly right, because the obvious answer is +wrong in an instructive way. + +QEMU can emulate ARM iOS hardware, and community projects exist that boot iOS +on it. What none of them can supply is the **iOS kernel and root filesystem**: +distributing iOS images is against Apple's terms, so an image must be one the +user already legally owns. The emulator is not the scarce thing. + +That is the same shape as the Android question, with the opposite answer, and +the comparison is the point: + +| | the emulator | the OS image | can the loop close? | +|---|---|---|---| +| Android | Apache-2.0, and `emulator/LICENSE` says so | AOSP `default` builds, OSS notices throughout | **yes** -- both are packaged, and `qemu-aarch64 -L` needs no emulator at all | +| iOS | QEMU, GPL, packageable | **not redistributable** | **no** -- and no amount of tooling changes it | + +So `aarch64-ios` cannot reach `verified` for a reason that is not about mcpp, +xlings, or effort. It is the one row in the table whose execution is blocked by +a licence rather than by work, and saying so precisely is better than leaving +it as "needs a device". + +Sources: [iOS emulators, Emulation General Wiki](https://emulation.gametechwiki.com/index.php/IOS_emulators) +and [Emulating iOS on Linux](https://linuxvox.com/blog/emulate-ios-on-linux/). + +**On macOS, the Simulator is the host's and that is fine.** It ships with Xcode, +it is a (b)-category proprietary runtime, and `xcrun simctl spawn` is an argv +prefix -- so §5's model covers it with no new mechanism, on a macOS host, once +R1 gives the simulator a row. + +### 10.5 Darling is a candidate for the macOS rows, and is recorded as unmeasured + +[Darling](https://github.com/darlinghq/darling) is a macOS compatibility layer +for Linux -- GPL-3.0, actively developed (last push 2026-09-06). It reimplements +Darwin's system libraries rather than redistributing them, so it carries **no +Apple licence blocker**, which makes it categorically different from the iOS +image problem above. + +It runs macOS binaries, not iOS ones, so it is irrelevant to `aarch64-ios` and +potentially relevant to `x86_64-macos` and `aarch64-macos` -- the first of which +is `planned` in this table with no host able to serve it off an Apple machine. + +Recorded as a candidate and explicitly **not** as a plan: nothing in this +ecosystem has run it, its coverage is partial by construction, and a row does +not move on a plausible mechanism. What it would be, if it worked, is an +ordinary `runner` argv prefix supplied by a `xim:` package -- the same shape as +`qemu-user-aarch64`. + +### 10.6 Genuinely host-bound, and there are exactly three 1. **`/dev/kvm`** -- a kernel facility. No package ships a kernel feature, and group membership is a machine's configuration. This is why it is the only `log.warn` left in `android-emulator.lua`. -2. **A real device, or Apple's Simulator runtime.** The simulator is a - proprietary runtime that exists only on macOS and is not redistributable; - a device is hardware. Both are *environments* rather than tools. +2. **A real device, Apple's Simulator runtime, or a licensed OS image.** The + simulator is a proprietary runtime that exists only on macOS; a device is + hardware; and an iOS kernel plus root filesystem cannot be redistributed at + all. Note which of the three is the actual blocker for emulation: QEMU is + packageable and the IMAGE is not, which is precisely why the same mechanism + closes for Android -- where the image is AOSP -- and cannot for iOS. 3. **Notarization.** Apple's servers plus a developer credential. A credential is never a package, and `rcodesign` can drive the submission but cannot supply the account. -### 10.4 The rule that falls out +### 10.7 The rule that falls out A host dependency is legitimate only when the thing needed is (a) a kernel facility, From 59d37e7918e2f9f6951e18552f6948837b178379 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:40:27 +0800 Subject: [PATCH 06/13] docs(targets): real devices close for both platforms, which supersedes the simulator route ASKED DIRECTLY AND IT IS THE STRONGEST RESULT IN THE SECTION: running on real hardware needs no Apple or Google software on either platform. Android adb push + adb shell Apache-2.0, already packaged iOS pymobiledevice3 GPL-3.0, pure Python 3, no compiled extensions, Linux/Windows/macOS, 2736 stars, pushed 2026-09-10 iOS libimobiledevice LGPL-2.1, the C library it models, 8177 stars, pushed 2026-06-10 pymobiledevice3 requires no Xcode, works with the system usbmuxd, and covers app management plus iOS 17+ tooling over a tunnel; libimobiledevice needs no jailbreak. So the iOS row's execution story is not "needs a device on a Mac": build, bundle, sign, package, deploy and run all close, and the ONLY line that does not is a provisioning profile and signing identity -- a credential, which is category (c) and which no package manager closes. That is the same boundary a developer already lives with under Xcode. It is materially better than the simulator route and the reason is worth stating: the simulator is blocked by a licensed IMAGE that cannot be redistributed, while a real device supplies its own OS and the only thing crossing the boundary is a signature the developer already owns. WHERE A DEVICE SESSION LIVES: the runner program absorbs deployment, so no fourth verb is needed -- "adb push" followed by "adb shell" is one operation from mcpp's side. Both mechanisms are used for what each is: a PLUGIN produces the artefact (dist-ipa, build time), a PACKAGE deploys and runs it (the runner program, run time), and the ENGINE names the runner (the existing "runner" key). The ordering is a real dependency, not a convention: the iOS device runner has nothing to install until dist-ipa has produced a signed file, so R9 precedes R12. THE iOS IMAGE IS A LOCATOR AND NEVER A RE-HOST. An image in a public index is redistribution of Apple's operating system whatever it is labelled, and this differs from the Android decision earlier in this document in a way worth stating precisely: there, Apache-2.0 licence files were verified INSIDE the archives and clause 3.5 genuinely applies; here there is no open-source component to invoke. The legitimate form is the locator tier iphoneos-sdk.lua already documents -- the index carries a path and a probe, not bytes -- and R4 is its engine half. With R12 in place no image is on the critical path at all. --- ...26-09-11-platform-targets-design-review.md | 96 ++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index 1439f062..a35513c5 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -312,6 +312,8 @@ package -- no new mechanism, again. | R9 | `mcpp pack --format ipa` | a `dist-ipa` member: zip `Payload/.app/`, after `dist-apple` and `rcodesign` | it needs NO new tool. Every other link is already in the ecosystem or one member away, so iOS PACKAGING closes entirely -- what does not close is the credential and the runtime | | R10 | `--format dmg` and `--format pkg` | recorded as gaps with a known shape, not attempted | each needs a *creator* as well as a signer (`libdmg-hfsplus`; `xar`), both open source and neither measured here | | R11 | the macOS rows' runner | Darling recorded as an unmeasured candidate | GPL-3.0, active, and it REIMPLEMENTS Darwin's libraries rather than redistributing them, so unlike the iOS image it carries no licence blocker. A row does not move on a plausible mechanism, so this is a candidate and not a plan | +| R12 | real-device run for both platforms | `xim:android-platform-tools` (have) and a new `xim:pymobiledevice3`, each named by a `runner` program | neither needs Apple or Google software. It supersedes the simulator route rather than complementing it: a device brings its own OS, so the only thing crossing the boundary is a signature the developer already owns | +| R13 | the iOS image | a LOCATOR package, never a re-host, gated on R4 | an image in a public index is redistribution of Apple's OS whatever it is labelled. The locator is the tier `iphoneos-sdk.lua` already documents, and with R12 in place no image is on the critical path at all | ## 7. User-facing experience, which is the test of all of the above @@ -540,7 +542,95 @@ it is a (b)-category proprietary runtime, and `xcrun simctl spawn` is an argv prefix -- so §5's model covers it with no new mechanism, on a macOS host, once R1 gives the simulator a row. -### 10.5 Darling is a candidate for the macOS rows, and is recorded as unmeasured +### 10.5 Real devices close for BOTH platforms, and that is a better answer than an emulator + +Asked directly, and it turns out to be the strongest result in this section: +**running on real hardware needs no Apple or Google software on either +platform.** + +| platform | what installs and launches | licence | state | +|---|---|---|---| +| Android | `adb push` + `adb shell` | Apache-2.0 | **already packaged** -- `xim:android-platform-tools`, installs on all three hosts | +| iOS | [`pymobiledevice3`](https://github.com/doronz88/pymobiledevice3) | GPL-3.0 | pure Python 3, no compiled extensions, Linux / Windows / macOS; 2736 stars, last push 2026-09-10 | +| iOS | [`libimobiledevice`](https://github.com/libimobiledevice/libimobiledevice) | LGPL-2.1 | the C library it was modelled on; 8177 stars, last push 2026-06-10 | + +`pymobiledevice3` describes itself as requiring no Xcode, working with the +system `usbmuxd`, and covering app management plus iOS 17+ developer tooling +over a tunnel. `libimobiledevice` states it needs no jailbreak. Neither +requires a Mac. + +So the iOS row's execution story is not "needs a device on a Mac". It is: + + build xim:llvm + xim:iphoneos-sdk closed + bundle dist-apple (--format app) closed + sign xim:rcodesign closeable, R7 + package dist-ipa (--format ipa) closeable, R9 + deploy+run xim:pymobiledevice3 closeable, R12 + ------------------------------------------------------------------------ + entitlement a provisioning profile and a signing identity NOT closeable + +**Only the last line does not close, and it is a credential rather than a +tool.** Installing on a non-jailbroken device requires an Apple Developer +provisioning profile; `pymobiledevice3` can install a signed `.ipa` and cannot +conjure the entitlement. That is category (c), and no package manager closes a +credential -- which is the same boundary a developer already lives with when +using Xcode. + +That is a materially better position than the simulator route, and it is worth +stating why: the simulator is blocked by a **licensed image** that cannot be +redistributed, while a real device supplies its own OS and the only thing +crossing the boundary is a signature the developer already owns. + +### 10.6 Where a device session lives: the runner program absorbs deployment + +§5 concluded that a device session is a runner PROGRAM in a `xim:` package, +named by an argv-prefix `runner`. Deployment does not need a fourth verb, and +the reason is that `adb push && adb shell` is one operation from mcpp's side: + + mcpp run --target aarch64-linux-android + -> runner = ["mcpp-android-device-run"] a xim package's program + which pushes, executes, collects the exit code and stdout, tears down + + mcpp run --target aarch64-ios + -> runner = ["mcpp-ios-device-run"] a xim package's program + which installs the signed .ipa, launches it, streams the log, collects + +So both mechanisms are used, each for what it is: + + plugin produces the artefact dist-ipa, dist-apple (build time) + package deploys and runs it the runner program (run time) + engine names the runner the `runner` key (already exists) + +And the ordering is a real dependency rather than a convention: the iOS device +runner has nothing to install until `dist-ipa` has produced a signed file, so +R9 precedes R12. + +### 10.7 The iOS image: a locator, not a re-host + +Adding an iOS kernel and root filesystem to a public index would be +redistributing Apple's operating system, and a "temporary, test-only, disabled +later" label does not change that -- anyone resolving the index would install +it. This differs from the Android decision earlier in this document in a way +worth stating precisely: there, Apache-2.0 licence files were verified INSIDE +the archives and clause 3.5 genuinely applies; here there is no +open-source component to invoke. + +What serves the same purpose legitimately is the third tier +`pkgs/i/iphoneos-sdk.lua` already documents, and R4's `@system` +generalisation is the engine half of it: + + a LOCATOR package records where an image the user already owns lives. + Nothing is re-hosted; the index carries a path and a probe, not bytes. + +That is the `msvc@system` shape, and it is why R4 matters beyond iOS: the +engine currently has no spelling for "this row's system is host-located", +so the locator tier is unreachable even though the recipe describes it. + +And it is worth noting what the locator would be FOR. With R12 in place, a +simulator or an emulated image is not on the critical path at all -- a real +device is the supported route, and it needs no image from anyone. + +### 10.8 Darling is a candidate for the macOS rows, and is recorded as unmeasured [Darling](https://github.com/darlinghq/darling) is a macOS compatibility layer for Linux -- GPL-3.0, actively developed (last push 2026-09-06). It reimplements @@ -558,7 +648,7 @@ not move on a plausible mechanism. What it would be, if it worked, is an ordinary `runner` argv prefix supplied by a `xim:` package -- the same shape as `qemu-user-aarch64`. -### 10.6 Genuinely host-bound, and there are exactly three +### 10.9 Genuinely host-bound, and there are exactly three 1. **`/dev/kvm`** -- a kernel facility. No package ships a kernel feature, and group membership is a machine's configuration. This is why it is the only @@ -573,7 +663,7 @@ ordinary `runner` argv prefix supplied by a `xim:` package -- the same shape as is never a package, and `rcodesign` can drive the submission but cannot supply the account. -### 10.7 The rule that falls out +### 10.10 The rule that falls out A host dependency is legitimate only when the thing needed is (a) a kernel facility, From d62d46ed4cb126cd1cc540d69edcb1d0d83f0bce Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:45:34 +0800 Subject: [PATCH 07/13] docs(targets): the runner program owns the image path, because a path is a machine fact and a declaration is not Three homes for an iOS image path were considered, and two are wrong for reasons worth recording because each looked right first. A LOCATOR PACKAGE has nothing to probe. Locators work where the thing has a conventional location -- vswhere, /Applications/Xcode.app -- and an image a user legally owns is wherever they put it. Its version axis would be meaningless too: a locator for 18.0 cannot verify that what it found is 18.0. THE PATH IN build.mcpp FAILS ON THIS REPOSITORY'S OWN ANALYSIS. A build program is per-package, committed, and its declarations are persisted in the cache record; a path is per-MACHINE, not committable, and must not be a build input or identical sources produce different build directories. That is exactly the precedence #564 established for `[build] default_jobs` -- invocation > project > machine -- and an image path sits on the machine level as a job count does. And a sharper failure: a variable read through `env_or` is NOT in the build program's contract environment, so it is not in its re-run key. Change the path and the program does not re-run, while the cache record persists the old runner (tag "runner" is non-empty). A path that was changed and appears not to have been is the defect class recorded most often here, and this would introduce it deliberately. THE DESIGN: the runner PROGRAM owns the path, resolved at run time. The engine names a runner and does not know what an image is; the index ships the emulator and no image; the plugin declares WHICH runner and produces the artefact; the program reads its own configuration each invocation. The path enters mcpp at no point -- not the index, the build program, the cache record or the fingerprint. That is the boundary cargo states for itself and which section 5.2 already quotes, applied one level further: "that responsibility falls to the runner program itself." It dissolves the staleness rather than mitigating it, because a run-time resolution has no record to go stale. The distinction the section arrives at is one sentence: declaring a NAME is cache-safe, baking a PATH is not. --- ...26-09-11-platform-targets-design-review.md | 123 ++++++++++++++---- 1 file changed, 98 insertions(+), 25 deletions(-) diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index a35513c5..6b194941 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -313,7 +313,7 @@ package -- no new mechanism, again. | R10 | `--format dmg` and `--format pkg` | recorded as gaps with a known shape, not attempted | each needs a *creator* as well as a signer (`libdmg-hfsplus`; `xar`), both open source and neither measured here | | R11 | the macOS rows' runner | Darling recorded as an unmeasured candidate | GPL-3.0, active, and it REIMPLEMENTS Darwin's libraries rather than redistributing them, so unlike the iOS image it carries no licence blocker. A row does not move on a plausible mechanism, so this is a candidate and not a plan | | R12 | real-device run for both platforms | `xim:android-platform-tools` (have) and a new `xim:pymobiledevice3`, each named by a `runner` program | neither needs Apple or Google software. It supersedes the simulator route rather than complementing it: a device brings its own OS, so the only thing crossing the boundary is a signature the developer already owns | -| R13 | the iOS image | a LOCATOR package, never a re-host, gated on R4 | an image in a public index is redistribution of Apple's OS whatever it is labelled. The locator is the tier `iphoneos-sdk.lua` already documents, and with R12 in place no image is on the critical path at all | +| R13 | the iOS image | **the RUNNER PROGRAM owns the path, resolved at run time.** The plugin declares a runner by NAME; the index ships the emulator and no image; the user configures the program | three homes were considered. A locator has nothing to probe. `build.mcpp` is per-package and committed, while a path is per-MACHINE -- the same analysis #564's `default_jobs` needed -- and a path read through `env_or` is not in the program's re-run key, so changing it would appear not to change anything while the cache record persists the old one. Declaring a NAME is cache-safe; baking a PATH is not. Run-time resolution has no record to go stale | ## 7. User-facing experience, which is the test of all of the above @@ -605,30 +605,103 @@ And the ordering is a real dependency rather than a convention: the iOS device runner has nothing to install until `dist-ipa` has produced a signed file, so R9 precedes R12. -### 10.7 The iOS image: a locator, not a re-host - -Adding an iOS kernel and root filesystem to a public index would be -redistributing Apple's operating system, and a "temporary, test-only, disabled -later" label does not change that -- anyone resolving the index would install -it. This differs from the Android decision earlier in this document in a way -worth stating precisely: there, Apache-2.0 licence files were verified INSIDE -the archives and clause 3.5 genuinely applies; here there is no -open-source component to invoke. - -What serves the same purpose legitimately is the third tier -`pkgs/i/iphoneos-sdk.lua` already documents, and R4's `@system` -generalisation is the engine half of it: - - a LOCATOR package records where an image the user already owns lives. - Nothing is re-hosted; the index carries a path and a probe, not bytes. - -That is the `msvc@system` shape, and it is why R4 matters beyond iOS: the -engine currently has no spelling for "this row's system is host-located", -so the locator tier is unreachable even though the recipe describes it. - -And it is worth noting what the locator would be FOR. With R12 in place, a -simulator or an emulated image is not on the critical path at all -- a real -device is the supported route, and it needs no image from anyone. +### 10.7 The iOS image: the ecosystem supplies the PROGRAM, the user supplies the BYTES + +The principle is not in question: an image in a public index is redistribution +of Apple's operating system whatever it is labelled, and a "temporary, disabled +later" flag does not change it -- anyone resolving the index installs it. This +differs from the Android decision earlier in this document in a way worth +stating precisely: there, Apache-2.0 licence files were verified INSIDE the +archives and clause 3.5 genuinely applies; here there is no open-source +component to invoke. + +What took analysis is WHERE the path lives. Three candidates were considered +and two are wrong for reasons worth recording, because each looked right first. + +#### Rejected: a locator package + +A locator works when the thing has a CONVENTIONAL location to probe -- +`vswhere` for Visual Studio, `/Applications/Xcode.app` for Xcode. An image a +user legally owns is wherever they put it, so a locator has nothing to probe +and would be a package whose entire content is a question. Its version axis +would be meaningless too: a locator for `iphoneos-image@18.0` cannot verify +that what it found is 18.0. + +#### Rejected: the path in `build.mcpp`, and the reason is this repository's own + +A build program is per-package, committed to a repository, and its declarations +are persisted in the build cache record. An image path is none of those things: + + it is per-MACHINE two developers keep it in different places + it is not committable an absolute path in someone's home directory + it must not be a build input identical sources must not produce different + build directories because a path differs + +That is precisely the analysis `[build] default_jobs` needed (#564): the +precedence is invocation > project > **machine**, and an image path sits on the +machine level exactly as a job count does. Putting a machine fact in a +per-package file is the shape that key was fixed for. + +And there is a sharper failure. If a build program reads +`MCPP_IOS_IMAGE_ROOT` through `env_or`, that variable is **not** part of +mcpp's contract environment, so it is not in the program's re-run key. Change +the path and the program does not re-run; the stale runner replays from the +cache record, which persists it (tag `"runner"`). The result is a path that was +changed and appears not to have been -- the defect class this repository has +recorded most often, and here it would be introduced deliberately. + +#### The design: the runner PROGRAM owns the path, and nothing above it knows + + engine names a runner. Does not know what an image is. + index ships the emulator (QEMU is packageable) and NO image. + plugin declares WHICH runner, and produces the artefact to run. + runner a program in a xim package. Owns the path, at RUN time. + user keeps the bytes, and tells the runner program where they are. + +The path enters mcpp at no point: not the index, not the build program, not the +cache record, not the fingerprint. What is published is a program that takes a +path, and the one thing crossing the boundary is the user's own configuration +of that program. + +This is the boundary cargo states for itself, quoted earlier in §5.2: managing +devices and simulators is out of scope, and "that responsibility falls to the +runner program itself." A runner program that owns its own configuration is the +same sentence applied one level further. + +It also dissolves the staleness problem rather than mitigating it. A run-time +resolution cannot be stale, because there is no record to go stale -- the +program reads its configuration each time it is invoked, which is what a +machine fact wants. + +#### What each layer actually writes + +The plugin side declares the runner by name, so it is available and not +imposed: + + // a dist/run member, or the project's own build.mcpp + mcpp::runner("device", "mcpp-ios-device-run"); // needs no image at all + mcpp::runner("qemu", "mcpp-ios-qemu-run"); // reads its own config + +reached as `mcpp run --runner device` or `--runner qemu`. Two properties of the +existing machinery make this work unmodified: the `runner` directive's cache +tag is non-empty so a declaration survives a cache hit, and its +`Scope::RunGlobal` is correct because a runner is a property of the invocation +rather than of one package in the graph. **Declaring a NAME is cache-safe; +baking a PATH is not** -- which is the whole distinction this section arrived +at. + +#### This is a pattern, and naming it is worth more than the iOS instance + +The same contract serves every "you have it, we cannot ship it" case: a vendor +BSP under NDA, a licensed board-support blob, proprietary firmware, a paid SDK. +In each, the ecosystem packages the TOOL that consumes the bytes and never the +bytes, and the tool owns its own configuration. + +Stating it as a pattern matters because the alternative -- deciding case by +case -- is how a "temporary" entry becomes permanent. And it composes with R12 +in the direction that counts: **with a real device supported, no image is on +the critical path at all.** The image route serves a developer who has one and +prefers it; the row does not depend on it. ### 10.8 Darling is a candidate for the macOS rows, and is recorded as unmeasured From 2e348e347d8afe81e7a5ca21f524d0b684439c91 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:10:50 +0800 Subject: [PATCH 08/13] Target rows become executable, and a machine-scoped scan stops being paid per target (2026.9.11.3) WASM32-EMSCRIPTEN IS `verified`. `mcpp run --target wasm32-emscripten` prints `1-2-3` on a source that imports std, from the row alone -- and the project side needed NO new vocabulary. That is the first real measurement of what this design costs: seven engine changes, and a project that builds for Linux builds for the web by changing one flag. Each of the seven was found by the previous one's failure, and each was a predicate that was CORRECT about the rows its author had in mind: 1. the payload chosen by the TARGET in `to_xim_package`, or `xim:llvm` answers for a wasm target 2. `XimToolchainPackage::frontendSubdir`, or `em++` is looked for in `bin/` 3. `Triple::has_own_sysroot()` in the SHARED PRODUCER `host_compile_tokens`. The first attempt put it at one of its three callers, so ordinary compiles stopped taking this host's headers and the std module precompile went on taking them -- which is where it had been failing. One decision, one site, three readers. 4. the same predicate in `resolve_link_model` -- the MODEL, not its two channels; the comment at the second channel already recorded that a reader who fixed only the first "saw the identical error" 5. the same finding twice: `discover_link_runtime_dirs` put the COMPILER's own runtime directories on the ARTEFACT's link line. For every row that predates these two, those are the same directory. 6. `host_can_serve` -- the first version returned true unconditionally, the same over-broad shape as the branches above it. The TARGET MATRIX caught it: `xim:emsdk` publishes linux only. 7. `Format::Wasm`, which the module PREDICTED AND DEFERRED in its own comment. While it was missing, every wasm build warned about a `libc++.so` that cannot exist for the target. Wasm also joins the capability pins: nothing else emits WebAssembly, so a declared `gcc@16.1.0` is a request that cannot be honoured, and saying so beats resolving gcc and failing inside it. A MACHINE-SCOPED SCAN WAS PAID PER TARGET: 17948ms -> 4ms. mcpp build loader-tags stage 170ms one 21 MB binary mcpp test loader-tags stage 17948ms 108 binaries, 2.4 GB and flat at ~17.9s across every target measured, so the cost is the whole artefact set rather than the one being built. `mcpp test` drives the backend once per target, so a 110-target suite paid it 110 times -- 3 minutes became a projected 33. `check_dlopen_surface` ran a full `inspect_elf_runtime` over EVERY linked artefact to collect SONAMEs, before it discovered the surface was empty. Its record said `members=0, walked=0` every time, and the artefacts are executables, which cannot carry `DT_SONAME` at all. Both halves fixed, and the shape is worth naming: the expensive work ran BEFORE the cheap test that makes it unnecessary. The record is still published -- a field that disappears is worse than one that says why it is empty. `min_api_level`, REUSING THE MECHANISM `macos_deployment_target` ALREADY HAS. Measured: `clang -target aarch64-linux-android21 -print-effective-triple` answers `aarch64-unknown-linux-android21`, so the level belongs on the effective triple -- and NOT on the canonical one, because mcpp keeps its own target vocabulary and maps it. macOS is already this shape. So `[target.] min_api_level = 24`, fused by the parameter `llvm_triple()` already takes, and in the FINGERPRINT because the level selects which bionic symbols are visible. The key's slot is renamed `minPlatformVersion`: a target is either Apple or Android, so one slot cannot hold both, and both answer one question. The name comes from Android's own words -- the NDK's CMake toolchain documents `ANDROID_PLATFORM` as "the minimum API level supported by the application or library". `ndk_api_version` was rejected twice over: "version" is not Android's word, and `ndk_` names the TOOLCHAIN while one NDK serves a RANGE of levels. THE FOUR-FIELD SPELLING IS ACCEPTED. `em++ -v` passes `-target wasm32-unknown-emscripten` and rustc lists that spelling; refusing what every other toolchain prints is a cost with no benefit. `str()` still returns the three-field form, which is what keeps the output directory, `cfg()` and the ABI tag single-valued. AND THE EOL DISTRO LEG IS SWAPPED. debian-11 fails with `E: Release file ... is expired` -- bullseye is end-of-life and its security metadata has expired, which is a property of the distribution. Measured while replacing it: debian 11 and ubuntu 20.04 both carry glibc 2.31, so the "older glibc" coverage was ALREADY duplicated by the ubuntu-2004 leg. Bookworm's 2.36 sits between that and debian-testing, so the leg now covers a point the matrix did not have. --- ...26-09-11-platform-targets-design-review.md | 244 +++++++++++++++++- CHANGELOG.md | 94 +++++++ docs/04-mcpp-toml.md | 38 +++ docs/README.md | 1 + docs/zh/04-mcpp-toml.md | 34 +++ docs/zh/README.md | 1 + modules/manifest/src/toml.cppm | 17 ++ modules/manifest/src/types.cppm | 24 ++ modules/toolchain-model/src/triple.cppm | 50 +++- src/build/cache_key.cppm | 23 +- src/build/prepare.cppm | 37 ++- src/build/runtime_validation.cppm | 44 ++++ tests/unit/test_cache_key.cpp | 15 +- tests/unit/test_toolchain_msvc.cpp | 19 +- tests/unit/test_toolchain_triple.cpp | 71 +++++ 15 files changed, 684 insertions(+), 28 deletions(-) diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index 6b194941..e0000a58 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -302,9 +302,9 @@ package -- no new mechanism, again. | # | question | recommendation | why | |---|---|---|---| | R1 | the simulator's spelling | `env = "sim"`, giving `aarch64-ios-sim` and `x86_64-ios-sim` as their own rows | matches Rust's pair modulo a vendor elision mcpp already does; satisfies the row comment's own objection, which was to NOT having a separate row | -| R2 | Android's API level | a `[target.] api = ` manifest key, and **it must enter the build fingerprint** | every system surveyed keeps it out of the triple (Rust: outside; CMake: `ANDROID_PLATFORM`; Gradle: `minSdk`), because it is a per-PROJECT minimum. The fingerprint is non-negotiable: it selects which bionic symbols exist, so two levels are two ABIs | +| R2 | Android's API level | **`min_api_level` under `[target.]`, reusing the `macos_deployment_target` design** -- see 12.1 | mcpp maintains its own vocabulary and MAPS to a compiler target, so where LLVM carries the level says nothing about where mcpp stores it. macOS already does this exactly: a manifest key, a clean canonical triple, the level appended by `llvm_triple(param)` -- which already takes a version -- and the value in the fingerprint. One NDK serves a range of levels, so it is a project decision and not a toolchain property. No new rows | | R3 | the payload identity shown for `emsdk` | a display identity on the resolved toolchain, not a fourth `Family` | `em++` is clang and a fourth family would be a false claim about the compiler; what is missing is only that nothing prints which archive answered | -| R4 | `@system` for a non-MSVC family | generalise it, with the row deciding whether it is permitted | the refusal argues from MSVC's uniqueness, and iOS is the second instance of exactly that situation. `xim:iphoneos-sdk` already names "locate what the machine has" as its third tier, and the engine has no spelling for it | +| R4 | `@system` for a non-MSVC family | **withdrawn** -- see 12.1a | the second instance dissolved: `xim:iphoneos-sdk` serves iOS as a package, so no host locator is required. Generalising would admit `gcc@system`, which the existing refusal names by name and which costs hermeticity. A refusal should not be relaxed without a case | | R5 | device and simulator sessions | a `xim:` package shipping a runner program, named by `runner` | cargo states this boundary explicitly; no engine change, no new member family, and it puts platform knowledge in the ecosystem | | R6 | the tier each row can reach | `verified` for wasm (reached); `preview` for both Android rows and for iOS | Rust rates all three Tier 2. `verified` for Android is reachable and needs a CI lane, not a design | | R7 | signing a Mach-O or a `.app` | package `rcodesign` as `xim:rcodesign` and have `dist-apple` prefer it | MPL-2.0 with prebuilt static binaries for linux-musl (both arches), macOS universal and Windows. It removes the last host dependency from the iOS BUILD path, leaving only a device, the Simulator runtime and a notarization credential -- none of which is a program | @@ -794,3 +794,243 @@ the second. The two Android rows are the nearest, and the thing blocking them is not payload work — both payloads are published and both execution routes are measured. It is one identity decision. + +## 12. Self-review of this proposal, before implementing any of it + +### 12.1 R2, three times, and the design mcpp already has + +This recommendation was written one way, reversed on a measurement, and then +reversed back when the measurement turned out to answer a different question. +The sequence is recorded because the mistake in the middle is instructive. + +**First answer: a manifest key.** Because the API level is a per-project +minimum -- what Gradle calls `minSdk` -- and because every system surveyed +keeps it out of the triple. + +**The reversal, and why it was wrong.** A real clang was asked: + + clang -target aarch64-linux-android21 -print-effective-triple + -> aarch64-unknown-linux-android21 + +LLVM puts the level in the **env** field, so `env = "android"` looked like +LLVM's own model rather than an invention, with `llvm_triple()` staying pure. + +That measurement is correct and it settles nothing here, because **mcpp +maintains its own target vocabulary and MAPS it to a compiler target.** Where +LLVM carries the level is a fact about the EFFECTIVE triple. Where mcpp carries +it is a question about the CANONICAL one, and the two are deliberately +different -- as `prepare.cppm` says in as many words: "The triple is mcpp's +vocabulary (`aarch64-macos`); the flag carries the spelling a compiler takes +(`arm64-apple-macos14.0`)." + +The general lesson: **a measurement of another tool's model does not settle a +question about ours.** It told me where LLVM writes the level, and I read it as +telling me where mcpp should store it. + +**The design mcpp already has, and which R2 should reuse.** macOS solved this +exact problem and the machinery is complete on all three counts: + +| | macOS, today | Android, proposed | +|---|---|---| +| manifest key | `macos_deployment_target = "14.0"` in `[package]` | `api = 24` in `[target.]` | +| canonical triple | `aarch64-macos` -- clean | `aarch64-linux-android` -- clean | +| effective target | `arm64-apple-macos14.0`, composed by `llvm_triple(param)` | `aarch64-unknown-linux-android24`, same call | +| fingerprint | `put(s, "macos", b.macosDeploymentTarget)` | the same, one line | + +`llvm_triple()` **already takes a version parameter** -- it is called as +`want->llvm_triple(macos::deployment_target(...))` -- so the objection that a +manifest key would give it a second input was already false when I raised it. +The function is not pure of versions today; it is pure of the *manifest*, which +is the property that matters, and the caller supplies the value. + +**And one NDK serves a range of levels**, so the level is not a property of the +toolchain either: naming `android-ndk@30.0.16248370` does not pin API 24. It is +a project decision, which is what a manifest key is for. + +So R2 is: **`api` under `[target.]`, appended to the effective triple +by the existing parameter, and entered into the fingerprint the way +`macos_deployment_target` already is.** No new rows, no new mechanism, and the +table does not multiply as levels are added. + +#### The field name, chosen against Android's own vocabulary + +`api = 24` was the first spelling and it is too vague: it says nothing about +WHICH property of the API is meant, and mcpp has no other `api` key to anchor +the reading. The naming convention to follow is `macos_deployment_target`'s -- +**named in the platform's own words** -- so the question is what Android calls +this. + +Read from the NDK's own documentation rather than recalled: + +| source | spelling | what the docs say | +|---|---|---| +| NDK CMake toolchain | `ANDROID_PLATFORM` | "specifies the **minimum API level** supported by the application or library" | +| the same, alias | `ANDROID_NATIVE_API_LEVEL` | "Alias for `ANDROID_PLATFORM`" | +| Android.mk | `TARGET_PLATFORM` | "The Android **API level** number the build system is targeting" | +| Gradle | `minSdk` | the NDK docs state `ANDROID_PLATFORM` "corresponds to the application's `minSdkVersion`" | + +So Android's concept name is **"API level"** -- the term its documentation uses +most -- and the specific quantity here is the **minimum**. + +Judged against that: + +| candidate | verdict | +|---|---| +| `api` | rejected. Says nothing about which property, and anchors to nothing | +| `ndk_api_version` | rejected on two counts. "version" is not Android's word, which is "level"; and `ndk_` names the TOOLCHAIN, while one NDK serves a RANGE of levels -- so naming it after the NDK reintroduces exactly the confusion 12.1 resolved | +| `platform` | rejected. It is the NDK's own variable name, and `platform` is badly overloaded in mcpp -- a module, and the `xpm` platform tables | +| `min_sdk_version` | rejected. Gradle's `minSdk` is an application-manifest concept for the Java side; for native code the NDK's word is API level, and mcpp is not building an app | +| **`min_api_level`** | **chosen.** "API level" is Android's own term; "min" states the semantics the NDK docs state themselves; no platform prefix, because `[target.aarch64-linux-android]` already supplies it | + +The kinship with `macos_deployment_target` is worth stating: both answer "the +oldest OS release this artifact must run on", and both are named in their +platform's vocabulary rather than in a shared abstraction. A single +`min_os_version` for both would be more uniform and would cost the existing +key a rename and both platforms their own words -- which is the trade this +codebase has consistently declined. + +#### The usage model + + # mcpp.toml + [package] + name = "app" + version = "0.1.0" + + # The minimum Android API level this project supports -- the same decision + # Gradle spells `minSdk`. One NDK serves a range, so this is the project's + # to make and not the toolchain's. + [target.aarch64-linux-android] + min_api_level = 24 + +and what each layer then sees: + + mcpp build --target aarch64-linux-android + + canonical triple aarch64-linux-android identity: output directory, + cfg(env = "android"), ABI tag + effective target aarch64-unknown-linux-android24 what clang is given + fingerprint includes 24 so 21 and 24 are two build + directories, never one + + # unset is legal and means the NDK's own default, which is what + # `clang -target aarch64-linux-android` normalises to. + +The parallel with the macOS key is exact, down to `[package]` versus +`[target.]` being the only difference -- and that difference is right: +a deployment target applies to every Apple artifact a project produces, while +an API level applies to one target row. + +### 12.1a R4 is withdrawn, because its second instance dissolved + +R4 proposed generalising `@system` beyond MSVC, on the grounds that the iOS SDK +is a second instance of "a proprietary thing that only exists where it is +installed". + +After the rest of this document, that is no longer true. `xim:iphoneos-sdk` +exists and the licence permits at least the fetch-upstream tier, so iOS is +served by a PACKAGE and needs no host locator. The locator tier its header +documents is a fallback that nothing currently requires. + +And the risk is concrete rather than theoretical: generalising the spelling +admits `gcc@system`, which the existing refusal names and refuses by name, and +which would let a build use the host's compiler and silently lose hermeticity +-- the property the whole payload model exists for. + +So the honest conclusion is not "defer until the narrow form is designed". It +is that **the motivating case evaporated, and a refusal should not be relaxed +without one.** If a real instance appears, the narrow form -- a per-row +permission defaulting to denied -- is the shape to design then. + +### 12.2 Two recommendations should be split by what they cost to be wrong about + +R1 (`env = "sim"`) and R8 (accept four-field spellings) are both cheap and +reversible: a new row is additive, and widening a parser is additive. They can +go in without further argument. + +R4 (generalise `@system`) is neither. It removes a refusal whose comment argues +at length for why it exists, and a wrong generalisation admits +`gcc@system` -- the exact spelling that comment refuses by name. The safe form +is narrow: a per-row permission, defaulting to denied, so the refusal's +reasoning stays true for every row that has not opted in. + +### 12.3 What this proposal does not measure, stated plainly + +* `rcodesign` has not been run. Its capabilities are quoted from its own + changelog and documentation. Signing a real `.app` and having macOS accept + it is the criterion, and no macOS machine has been involved. +* `pymobiledevice3` has not been run, and there is no iOS device here. +* Darling has not been run. +* The macOS and Windows legs of every payload completed today are declared + from verified hashes and have not been executed. + +Every one of those is a claim about somebody else's software, and this session's +record is that **every wrong guess in it was about what a vendor had done, and +every one was cheap to check and was not checked.** The four above are the +places that pattern would recur. + +### 12.4 The task list, so nothing is left half-done + +Grouped by repository, because the one-PR-per-repo rule makes the grouping the +plan. + +**mcpp (one PR, 2026.9.11.3)** -- the general capability, all of it: + +| # | task | state | +|---|---|---| +| E1 | `wasm32-emscripten` resolves, builds and runs | **done**, measured `1-2-3` | +| E2 | the seven gates the wasm row needed | **done** | +| E3 | `Format::Wasm` and its mechanism | **done** | +| E4 | wasm is a capability pin | **done** | +| E5 | matrix expectations for 12 wasm cells | **done** | +| E6 | unit tests for E1-E4 | **done** | +| E7 | the EOL debian leg swapped for debian-12 | **done** | +| E8 | R8: `parse()` accepts `wasm32-unknown-emscripten`, `aarch64-apple-ios` | todo | +| E9 | R1: `aarch64-ios-sim` and `x86_64-ios-sim` rows | todo | +| E10 | R3: a payload display identity, so emsdk is not shown as `llvm` | todo | +| E11 | the `.wasm` sibling as an implicit link output | todo | +| E12 | R6: Android and iOS rows to `preview` where the evidence supports it | todo | +| E13 | docs: `20-toolchains`, `21-the-target-triple`, `22-target-side` + zh | todo | +| E14 | CHANGELOG | todo | + +| E15 | R2: `min_api_level` under `[target.]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | todo | + +R4 is not in that list because it is **withdrawn** (§12.1a), not deferred: its +motivating case dissolved once `xim:iphoneos-sdk` covered iOS, and relaxing a +refusal without a case is how `gcc@system` gets in. + +**xim-pkgindex** -- one PR, already open as #812 plus the platform completion: + +| # | task | state | +|---|---|---| +| X1 | Android CN mirrors under clause 3.5 | **done**, #812 | +| X2 | emsdk, NDK and emulator on all three hosts | **done**, hashes verified | +| X3 | `xim:python` aarch64 and a GLOBAL url | **done** | +| X4 | R7: `xim:rcodesign` | todo | +| X5 | R12: `xim:pymobiledevice3` | todo | +| X6 | R5: the two runner programs | todo, and they are new software rather than packaging | + +**mcpp-plugins** -- one PR after the engine release: + +| # | task | state | +|---|---|---| +| P1 | `dist-*` family, three members | **done**, 0.6.0 tagged | +| P2 | R9: `dist-ipa` | todo | +| P3 | R7's plugin half: prefer `xim:rcodesign` over the host's codesign | todo | + +**mcpp-index**: publish `mcpp:plugins@0.6.0`. One task, blocked on the tag's +sha256. + +**Recorded and not attempted**: R10 (`dmg`/`pkg` creators), R11 (Darling), +R13's implementation (the pattern is documented; the runner programs are X6). + +### 12.5 The dependency order, and the one place it is not obvious + + mcpp engine ──► release ──► mcpp-plugins ──► mcpp-index + │ │ + └──► xim payloads ──────────┘ + (independent, merge first) + +The non-obvious edge is **X6 before P2 is wrong**. A device runner has nothing +to install until `dist-ipa` exists, so P2 precedes X6 -- the reverse of the +usual "payloads first" rule, and the reason is that here the payload consumes +the plugin's output rather than feeding it. diff --git a/CHANGELOG.md b/CHANGELOG.md index 641be546..22cce3df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,100 @@ ## [Unreleased] +## [2026.9.11.3] - 2026-09-11 + +### `wasm32-emscripten` 从 `planned` 到 `verified` + +`mcpp run --target wasm32-emscripten` 在一个 `import std` 的源码上打印 `1-2-3`, +而工程侧**一个新词汇都不需要** —— 改一个 flag,一个为 Linux 构建的工程就能为 Web +构建。这是「加一个平台等于一次引擎改动」这个代价的第一次实测:七处引擎改动。 + +七处里每一处都是**前一处的失败**找出来的,而每一处都是一个**对它作者心里那些行 +正确**的谓词: + + 1. `to_xim_package` 里由**目标**决定载荷 —— 否则 `xim:llvm` 会回答一个 wasm 目标 + 2. `XimToolchainPackage::frontendSubdir` —— 否则去 `bin/` 里找 `em++` + 3. `Triple::has_own_sysroot()`,放在**共享生产者** `host_compile_tokens` 里 —— + 第一次尝试只改了 `flags.cppm` 一个调用点,于是普通编译不再注入宿主头文件, + 而 std 模块预编译照旧注入,正是它原本失败的地方。一个决定、一个站点、三个读者。 + 4. 同一个谓词放进 `resolve_link_model` —— **模型**而不是它的两条通道;那里的注释 + 早就记着「只修第一条的人会看到一模一样的报错」 + 5. 同一个发现的第二次:`discover_link_runtime_dirs` 把**编译器自己的**运行期目录 + 放上了**产物的**链接行。这条之前的每一行里,两者是同一个目录。 + 6. `host_can_serve`:第一版无条件返回 true,与它上方那些分支是同一种过宽 —— + **目标矩阵抓到的**:`xim:emsdk` 只发布 linux + 7. `Format::Wasm` 及其机制 —— 而这一处**模块自己预言并推迟过**:「Adding + `Format::Wasm` is deferred to whoever gives this module a mechanism for it」。 + 它缺席期间,每一次 wasm 构建都会警告一个这个目标上不可能存在的 `libc++.so`。 + +wasm 同时加入**能力钉**(capability pin):没有别的东西能发出 WebAssembly,所以 +声明一个 `gcc@16.1.0` 是一个无法被满足的请求,说出来比解析出 gcc 再在它内部失败要好。 + +### 一个按目标付费的机器级扫描,17948ms → 4ms + +`mcpp test` 在这台机器上从约 3 分钟变成投影 33 分钟,而根因不是回归而是**一直 +存在的形状**被一次大载荷安装放大了: + +``` +mcpp build loader-tags 阶段 170ms 一个 21 MB 二进制 +mcpp test loader-tags 阶段 17948ms 108 个二进制、2.4 GB +``` + +而且在测到的每一个目标上都**平的** 17.9 秒 —— 所以代价是**整个产物集合**而不是 +正在构建的那一个。`mcpp test` 每个目标驱动一次后端,于是 110 个目标付了 110 次。 + +`check_dlopen_surface` 会对**每一个已链接产物**做完整 `inspect_elf_runtime` 去收集 +SONAME,而这发生在它发现「surface 是空的」**之前**。它每次写下的记录都说 +`members=0, walked=0`。而那些产物全是**可执行文件**,按构造不可能有 `DT_SONAME`。 + +两处都修了,而值得命名的是形状:**昂贵的工作跑在了那个使它变得不必要的便宜判据 +之前。** 记录照旧发布 —— 一个会消失的字段比一个说明自己为何为空的字段更坏。 + +### `min_api_level`,复用 `macos_deployment_target` 已有的那套机制 + +Android 的 API level 在**交给编译器的** triple 里(实测: +`clang -target aarch64-linux-android21 -print-effective-triple` 答 +`aarch64-unknown-linux-android21`),而它**不该进规范 triple**:mcpp 维护自己的目标 +词汇并映射到编译器目标,而 macOS 早就是这个形状。 + +``` +[target.aarch64-linux-android] +min_api_level = 24 +``` + +规范 triple 保持 `aarch64-linux-android`(它命名输出目录、`cfg(env=)`、ABI tag); +级别由 `llvm_triple()` **已有的那个参数**拼进 effective triple;而它进**指纹** —— +级别决定哪些 bionic 符号可见,所以两个级别是两个 ABI,绝不可共用一个构建目录。 + +字段名取自 Android 自己的词汇:NDK 的 CMake toolchain 把 `ANDROID_PLATFORM` 记载为 +「the minimum API level supported by the application or library」。`ndk_api_version` +被否掉有两条理由:「version」不是 Android 的用词(是 **level**),而 `ndk_` 命名的是 +**工具链**,可一个 NDK 服务一个级别**区间** —— 用 NDK 命名会把这个区分重新弄混。 + +指纹里那个槽因此从 `macosDeploymentTarget` 改名为 `minPlatformVersion`:一个目标 +要么是 Apple 要么是 Android,所以一个槽装不下两者,而两者回答的是同一个问题。改名 +不额外增加任何重建 —— mcpp 版本本来就在这个键里。 + +### 四段式拼法被接受 + +`em++ -v` 传给它自己 clang 的是 `-target wasm32-unknown-emscripten`,rustc 的表里 +也是这个拼法。拒绝每个别的工具链都会打印的那个形式是纯 UX 代价。`parse()` 现在接受 +它并规范化,而 `str()` 两边都返回三段式 —— 这才是让输出目录、`cfg()` 和 ABI tag +保持单值的东西。 + +### 那条 EOL 的 distro 腿换掉了 + +`debian-11` 从 2026-09-11 起以 `E: Release file ... is expired` 失败 —— bullseye +已经 EOL,而它 security suite 的元数据过期了。这是发行版的属性而不是本工作流的, +而 `-o Acquire::Check-Valid-Until=false` 只会让它安静下来并留着一条「对着没人维护的 +元数据测试」的腿。 + +换掉时顺手测了一件事:debian 11 与 ubuntu 20.04 **都是 glibc 2.31**,所以这条腿 +原本要覆盖的「更老的 glibc」**早就被上面那条 ubuntu-2004 腿覆盖了**。bookworm 的 +2.36 落在那个 2.31 与 debian-testing 的滚动版本之间,所以这条腿现在盖住了矩阵原本 +没有的一个点。 + + ## [2026.9.11.2] - 2026-09-11 ### 扫描器读到了注释里面,而且是双向的 diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index c742e43f..6062fcc5 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -685,6 +685,44 @@ linkage = "static" Moved to [22 — The Target Side](22-target-side.md). +### 2.7.3 `min_api_level` — the oldest OS release the artifact must run on + +```toml +[target.aarch64-linux-android] +min_api_level = 24 +``` + +Android's own term is **API level**, and this is the minimum — the quantity the +NDK's CMake toolchain documents `ANDROID_PLATFORM` as carrying ("the minimum +API level supported by the application or library"), and which corresponds to +Gradle's `minSdk`. + +**It is a project decision and not a property of the toolchain.** One NDK +serves a range of levels, so naming `android-ndk@` does not pin one. + +**It reaches the compiler and not the identity.** The canonical triple stays +`aarch64-linux-android`, which is what names the output directory, `cfg(env = +"android")` and the packed ABI tag; the level is fused onto the triple the +compiler is given: + +| | | +|---|---| +| canonical triple | `aarch64-linux-android` | +| what clang receives | `aarch64-unknown-linux-android24` | +| build fingerprint | carries the level | + +The fingerprint is not optional: the level selects which bionic symbols are +visible, so two levels are two ABIs and must never share a build directory. + +Unset is legal and means the NDK's own default, which is what +`clang -target aarch64-linux-android` normalises to. + +This is the same mechanism `macos_deployment_target` (§ above) uses, and the +two are named in their own platforms' words rather than in a shared +abstraction. Both answer one question: the oldest OS release the artifact has +to run on. + + ### 2.7.2 Bare metal (`os = none`) — freestanding targets `riscv64-none-elf` and `riscv32-none-elf` are targets with no operating system diff --git a/docs/README.md b/docs/README.md index 09eaffcf..cb1ecc7f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -125,6 +125,7 @@ token in front of a reader to the chapter that owns it. | `[build] accel`, `[package] accelerators`, `device_extensions` | [42](42-heterogeneous-builds.md) | `[hooks]` | [09](09-commands-by-scenario.md) | | `[package] platforms`, `[build] cache` | [04](04-mcpp-toml.md) | `[targets.]`, `[profile.]` | [04](04-mcpp-toml.md) | | `runner`, `[target..runners]` | [41](41-devices.md) | `rule_module` | [31](31-authoring-a-rule-package.md) | +| `min_api_level` | [04](04-mcpp-toml.md) | `macos_deployment_target` | [04](04-mcpp-toml.md) | **Commands** diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index 1e11beb8..47acb367 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -594,6 +594,40 @@ linkage = "static" 已移入 [22 —— 目标侧](22-target-side.md)。 +### 2.7.3 `min_api_level` —— 产物必须能跑在多老的 OS 上 + +```toml +[target.aarch64-linux-android] +min_api_level = 24 +``` + +Android 自己的用词是 **API level**,而这里要的是它的**最小值** —— NDK 的 CMake +toolchain 把 `ANDROID_PLATFORM` 记载为「the minimum API level supported by the +application or library」,并说明它对应 Gradle 的 `minSdk`。 + +**这是工程的决定,不是工具链的属性。** 一个 NDK 服务一个级别区间,所以写 +`android-ndk@` 并不钉住某一个级别。 + +**它到达编译器,不进入身份。** 规范 triple 仍然是 `aarch64-linux-android` —— +输出目录、`cfg(env = "android")` 和打包的 ABI tag 都由它命名;级别只拼进交给 +编译器的那个 triple: + +| | | +|---|---| +| 规范 triple | `aarch64-linux-android` | +| clang 实际收到 | `aarch64-unknown-linux-android24` | +| 构建指纹 | 含级别 | + +指纹不是可选项:级别决定哪些 bionic 符号可见,所以两个级别就是两个 ABI,绝不可 +共用一个构建目录。 + +不设也合法,含义是 NDK 自己的默认级别 —— 那正是 +`clang -target aarch64-linux-android` 规范化出的形式。 + +这与 `macos_deployment_target`(见上文)是同一套机制,而两者都用各自平台的词汇 +命名,而不是一个共享抽象。它们回答同一个问题:产物必须能跑在多老的 OS 发布版上。 + + ### 2.7.2 裸机(`os = none`)—— freestanding target `riscv64-none-elf` 与 `riscv32-none-elf` 是底下没有操作系统的 target。它们不需要 diff --git a/docs/zh/README.md b/docs/zh/README.md index f3b7dc06..f44cb1d4 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -121,6 +121,7 @@ | `[build] accel`、`[package] accelerators`、`device_extensions` | [42](42-heterogeneous-builds.md) | `[hooks]` | [09](09-commands-by-scenario.md) | | `[package] platforms`、`[build] cache` | [04](04-mcpp-toml.md) | `[targets.]`、`[profile.]` | [04](04-mcpp-toml.md) | | `runner`、`[target..runners]` | [41](41-devices.md) | `rule_module` | [31](31-authoring-a-rule-package.md) | +| `min_api_level` | [04](04-mcpp-toml.md) | `macos_deployment_target` | [04](04-mcpp-toml.md) | **命令** diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 9e567f5b..2a5bb867 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -2501,6 +2501,23 @@ std::expected parse_string(std::string_view content, e.sysrootDeclared = true; } + // `min_api_level` — the oldest OS release the artifact must run + // on, for a target whose compiler takes it inside the triple. + // Refused rather than coerced when it is not a positive integer: + // a level is a number, and a string that looks like one would be + // a second spelling of the same key. + if (auto it = body.find("min_api_level"); it != body.end()) { + auto n = it->second.is_int() + ? std::optional(it->second.as_int()) + : std::nullopt; + if (!n || *n <= 0) { + return std::unexpected(error(origin, std::format( + "[target.{}].min_api_level must be a positive integer, " + "e.g. min_api_level = 24", triple))); + } + e.minApiLevel = static_cast(*n); + } + // `runner` — the argv template `mcpp run` uses for a target whose // artifact cannot execute here. An ARRAY, so it is neither a // scalar (the unknown-key sweep below skips it by type) nor part diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d39f7c79..8ea21761 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1092,6 +1092,30 @@ struct TargetEntry { // Two plain members carry the same information and instantiate nothing. std::string sysroot; bool sysrootDeclared = false; + // THE OLDEST OS RELEASE THIS ARTIFACT MUST RUN ON, for a target whose + // compiler takes it as part of the triple. + // + // `min_api_level = 24` under `[target.aarch64-linux-android]`. Android's + // own term is "API level" -- the NDK's CMake toolchain documents + // `ANDROID_PLATFORM` as "the minimum API level supported by the + // application or library" -- and the quantity here is that minimum. + // + // A PROJECT DECISION AND NOT A TOOLCHAIN PROPERTY, which is why it is a + // manifest key. One NDK serves a RANGE of levels: naming + // `android-ndk@30.0.16248370` does not pin API 24, so the level cannot be + // read off the toolchain. + // + // AND NOT PART OF THE CANONICAL TRIPLE, which is the other half. mcpp + // keeps its own target vocabulary and maps it to a compiler target, and + // this is the same shape `macos_deployment_target` already has: + // + // canonical aarch64-linux-android identity: output dir, cfg(), ABI tag + // effective aarch64-unknown-linux-android24 what clang is given + // fingerprint carries the level so 21 and 24 are two directories + // + // Zero means unset, which is legal and means the NDK's own default -- what + // `clang -target aarch64-linux-android` normalises to. + int minApiLevel = 0; // NO per-role field here. There used to be a `cxxRuntimeTests`, and it was // parsed nowhere and applied nowhere — a configuration key that looked // available and did nothing (#418). The per-target channel carries the diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 48773eb5..a9bf9968 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -130,7 +130,25 @@ struct Triple { // LDBL_DIG ('33 == 18') // // 33 is aarch64's binary128; 18 is x87. Two machines in one command line. - std::string llvm_triple(std::string_view macosVersion = {}) const { + // THE EFFECTIVE TRIPLE, WHICH IS NOT THE CANONICAL ONE. + // + // `str()` is mcpp's vocabulary and is the identity: the output directory, + // `cfg()`, the packed ABI tag and the fingerprint all derive from it. This + // is the spelling a COMPILER takes, and the two are deliberately different + // -- `aarch64-macos` against `arm64-apple-macos14.0`. + // + // `minPlatformVersion` IS THE PROJECT'S STATEMENT, passed in rather than + // stored, because it is a manifest value and this function must stay pure + // of the manifest. Two platforms fuse it into the triple and each names it + // in its own words: + // + // macOS the deployment target `[build] macos_deployment_target` + // Android the minimum API level `[target.] min_api_level` + // + // Keeping it one parameter rather than two is the point: both answer "the + // oldest OS release this artefact must run on", and a second parameter + // would let a caller supply one platform's answer for the other's. + std::string llvm_triple(std::string_view minPlatformVersion = {}) const { if (empty()) return {}; if (os == "macos") { // Apple spells the 64-bit ARM architecture `arm64`, and the OS @@ -139,8 +157,8 @@ struct Triple { // decision belonging to the project rather than to the compiler. const std::string a = (arch == "aarch64") ? "arm64" : arch; std::string t = a + "-apple-macos"; - t += macosVersion.empty() ? std::string("14.0") - : std::string(macosVersion); + t += minPlatformVersion.empty() ? std::string("14.0") + : std::string(minPlatformVersion); return t; } if (os == "windows") { @@ -159,13 +177,25 @@ struct Triple { const std::string a = (arch == "aarch64") ? "arm64" : arch; return a + "-apple-ios"; } - // ANDROID IS LINUX, AND THE ENV SEGMENT IS WHERE IT SAYS SO. clang also - // accepts an API level fused onto the OS segment - // (`aarch64-linux-android24`), which selects which bionic symbols are - // visible; it is omitted here for the reason the iOS version is -- - // the minimum platform version is the project's statement, and clang - // has a default. - if (os == "linux") return arch + "-unknown-linux-" + (env.empty() ? "gnu" : env); + // ANDROID IS LINUX, AND THE ENV SEGMENT IS WHERE IT SAYS SO -- with the + // API level fused onto it when the project stated one. + // + // Measured: `clang -target aarch64-linux-android21 -print-effective-triple` + // answers `aarch64-unknown-linux-android21`, so the level belongs on + // the ENV segment of the effective triple and nowhere else. It selects + // which bionic symbols are visible, which is why it is in the + // fingerprint; and it is the project's statement rather than the + // toolchain's, because one NDK serves a range of levels. + // + // Absent, clang's own default applies -- `aarch64-unknown-linux-android` + // is what it normalises with no level, so omitting the key is a legal + // answer and not a gap. + if (os == "linux") { + std::string e = env.empty() ? std::string("gnu") : env; + if (env == "android" && !minPlatformVersion.empty()) + e += std::string(minPlatformVersion); + return arch + "-unknown-linux-" + e; + } // Emscripten's own effective triple. The vendor segment is `unknown` // and the OS segment is the platform layer rather than a kernel, which // is why `object_format()` reads the ARCH for this row. diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index 3222086e..1bc42321 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -135,7 +135,18 @@ struct BuildAxes { std::string cppStandardFlag; std::vector dialectFlags; std::string cStandard; - std::string macosDeploymentTarget; + // THE OLDEST OS RELEASE THIS ARTEFACT MUST RUN ON, one slot for both + // platforms that have such a thing. + // + // It was `macosDeploymentTarget`. Android's minimum API level is the same + // quantity -- and it is in this key for the same reason: it selects which + // bionic symbols are visible, so two levels are two ABIs and must never + // share a build directory. A target is either Apple or Android, so one + // slot cannot be asked to hold both at once. + // + // Renaming the hashed label costs no extra rebuild: the mcpp version is + // already part of this key, so every release invalidates it anyway. + std::string minPlatformVersion; // C std::string optLevel; bool debug = false; @@ -208,7 +219,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, const mcpp::manifest::Manifest& rootManifest, std::string_view cppStandardFlag, const std::vector& dialectFlags, - std::string_view macosDeploymentTarget, + std::string_view minPlatformVersion, const std::filesystem::path& storeRoot = {}, bool needsPic = false); @@ -273,7 +284,7 @@ nlohmann::json to_json(const BuildAxes& b, const PackageAxes& p) { {"cpp_standard_flag", b.cppStandardFlag}, {"dialect_flags", b.dialectFlags}, {"c_standard", b.cStandard}, - {"macos_deployment_target", b.macosDeploymentTarget}, + {"min_platform_version", b.minPlatformVersion}, }; j["profile"] = { {"opt_level", b.optLevel}, @@ -321,7 +332,7 @@ std::string key_hex(const BuildAxes& b, const PackageAxes& p) { put(s, "stdflag", b.cppStandardFlag); put_list(s, "dialect", b.dialectFlags); put(s, "cstd", b.cStandard); - put(s, "macos", b.macosDeploymentTarget); + put(s, "minplat", b.minPlatformVersion); // C put(s, "opt", b.optLevel); put(s, "debug", b.debug ? "1" : "0"); @@ -353,7 +364,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, const mcpp::manifest::Manifest& rootManifest, std::string_view cppStandardFlag, const std::vector& dialectFlags, - std::string_view macosDeploymentTarget, + std::string_view minPlatformVersion, const std::filesystem::path& storeRoot, bool needsPic) { @@ -468,7 +479,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, b.cppStandardFlag = std::string(cppStandardFlag); b.dialectFlags = dialectFlags; b.cStandard = rootManifest.buildConfig.cStandard; - b.macosDeploymentTarget = std::string(macosDeploymentTarget); + b.minPlatformVersion = std::string(minPlatformVersion); b.optLevel = rootManifest.buildConfig.optLevel; b.debug = rootManifest.buildConfig.debug; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 2d655273..5b8c760b 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -1559,6 +1559,27 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, return {}; } +// THE PROJECT'S MINIMUM PLATFORM VERSION FOR THIS TARGET, in one place. +// +// Two platforms fuse it into the effective triple and each names it in its own +// words: macOS's deployment target lives in `[build]` because it applies to +// every Apple artefact a project produces, and Android's API level lives in +// `[target.]` because it applies to one row. `llvm_triple` takes one +// parameter for both, so the choice between them is made here rather than at +// each of its call sites -- there are two, and a decision made twice is the +// shape this codebase records most often. +std::string min_platform_version(const mcpp::manifest::Manifest& m, + const mcpp::toolchain::triple::Triple& t) { + if (t.is_android()) { + if (auto it = m.targetOverrides.find(t.str()); it != m.targetOverrides.end()) + if (it->second.minApiLevel > 0) + return std::to_string(it->second.minApiLevel); + return {}; // the NDK's own default, which clang supplies + } + return mcpp::platform::macos::deployment_target( + m.buildConfig.macosDeploymentTarget); +} + std::string with_index_cause(std::string msg) { if (auto hint = mcpp::pm::unusable_index_hint(); !hint.empty()) msg += "\n" + hint; @@ -3278,8 +3299,7 @@ prepare_build(bool print_fingerprint, && tc->compiler == mcpp::toolchain::CompilerId::Clang) { tc->crossTargetFlag = "--target=" + want->llvm_triple( - mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget)); + min_platform_version(*m, *want)); } } if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); @@ -8697,8 +8717,7 @@ prepare_build(bool print_fingerprint, if (tc) { if (auto tt = mcpp::toolchain::triple::parse(tc->targetTriple)) { in.llvmTriple = tt->llvm_triple( - mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget)); + min_platform_version(*m, *tt)); in.targetOs = tt->os; in.targetEnv = tt->env; in.freestandingTarget = tt->is_freestanding(); @@ -11001,8 +11020,14 @@ prepare_build(bool print_fingerprint, mcpp::toolchain::cppfly::effective_dialect_flags( *tc, m->cppStandard.experimental, mcpp::manifest::dialect_flags(m->buildConfig)), - mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget), + // ONE SLOT, BOTH PLATFORMS. See `min_platform_version`: a target + // is either Apple or Android, and the level selects which bionic + // symbols are visible, so two levels must be two build + // directories. + [&] { + auto tt = mcpp::toolchain::triple::parse(tc->targetTriple); + return tt ? min_platform_version(*m, *tt) : std::string{}; + }(), // The GLOBAL registry root — the same one `fill_package_config` // relativizes against below, so both halves of the key describe // payload paths the same way. diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index cc2cd1d1..d9f833a8 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -1248,6 +1248,33 @@ check_dlopen_surface(const mcpp::build::BuildPlan& plan) { return report; } + // AND NO SURFACE MEANS NOTHING TO READ. THE CHEAP TEST COMES FIRST. + // + // `inspect_dlopen_surface` walks `plan.depRuntimeLibraryDirs`. With that + // list empty there are no members, so every reading below is taken to + // answer a question with no subject -- and the readings are not cheap: the + // SONAME scan that follows parses EVERY linked artifact in full. + // + // Measured on this repository, 2026-09-11, before this guard existed: + // + // mcpp build loader-tags stage 170ms one 21 MB binary + // mcpp test loader-tags stage 17948ms 108 binaries, 2.4 GB + // + // and flat at ~17.9s across every target measured, because the cost is the + // whole artifact set rather than the one being built. `mcpp test` drives + // the backend once per target, so a 110-target suite paid it 110 times -- + // turning a 3-minute run into 33. The record it produced every time said + // `members=0, walked=0`. + // + // This is the shape worth naming: the expensive work ran BEFORE the cheap + // test that makes it unnecessary. The record is still published, because a + // field that disappears is worse than a field that says why it is empty. + if (plan.depRuntimeLibraryDirs.empty()) { + publish_reason("no dependency published a runtime library directory; " + "there is no dlopen surface to judge"); + return report; + } + for (auto const& [artifact, stamp] : artifacts) { auto dir = artifact.parent_path(); if (dir.empty() || std::ranges::find(searchDirs, dir) != searchDirs.end()) @@ -1258,8 +1285,25 @@ check_dlopen_surface(const mcpp::build::BuildPlan& plan) { // The SONAMEs this build produces, read from the objects rather than from // their filenames. See `inspect_dlopen_surface` for why a filename search // is not enough while the build is still running. + // ONLY A SHARED LIBRARY CAN HAVE ONE, so only a shared library is read. + // + // This parsed every artifact, including executables, which have no + // `DT_SONAME` by construction -- an ELF that is not a shared object cannot + // carry one. On a test suite that is the entire cost of the loop spent to + // append nothing: 108 executables, 2.4 GB, one empty vector. + // + // The kind comes from `plan.linkUnits` rather than from the file, because + // that is the answer the plan already computed and reading it back out of + // the ELF is the same parse this avoids. + std::vector sharedOutputs; + for (auto const& unit : plan.linkUnits) { + if (unit.kind != mcpp::build::LinkUnit::SharedLibrary) continue; + sharedOutputs.push_back(plan.outputDir / unit.output); + } std::vector produced; for (auto const& [artifact, stamp] : artifacts) { + if (std::ranges::find(sharedOutputs, artifact) == sharedOutputs.end()) + continue; auto facts = mcpp::platform::elf::inspect_elf_runtime(artifact); if (facts && !facts->soname.empty()) produced.push_back(facts->soname); } diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index d0b9c273..adc50041 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -132,8 +132,21 @@ TEST(CacheKey, LanguageAndDialectChangeTheKey) { { auto b = axes(); b.dialectFlags = {"-freflection"}; EXPECT_NE(ck::key_hex(b, pkg()), base); } { auto b = axes(); b.cStandard = "c17"; EXPECT_NE(ck::key_hex(b, pkg()), base); } - { auto b = axes(); b.macosDeploymentTarget = "14.0"; + // ONE SLOT, BOTH PLATFORMS. It was `macosDeploymentTarget`; Android's + // minimum API level is the same quantity and is in this key for the same + // reason -- it selects which bionic symbols are visible, so two levels are + // two ABIs. A target is either Apple or Android, so one slot cannot be + // asked to hold both at once. + { auto b = axes(); b.minPlatformVersion = "14.0"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + { auto b = axes(); b.minPlatformVersion = "24"; + EXPECT_NE(ck::key_hex(b, pkg()), base); } + // AND TWO LEVELS ARE TWO KEYS, which is the property the Android row + // depends on: without it `min_api_level = 21` and `= 24` would share a + // build directory and the second build would serve the first's objects. + { auto b21 = axes(); b21.minPlatformVersion = "21"; + auto b24 = axes(); b24.minPlatformVersion = "24"; + EXPECT_NE(ck::key_hex(b21, pkg()), ck::key_hex(b24, pkg())); } } // ── D axis: package identity ───────────────────────────────────────────────── diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index 7804158d..b6ca5a85 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -302,7 +302,12 @@ TEST(MsvcManaged, PayloadFrontendFindsClWhereMsvcActuallyKeepsIt) { ASSERT_TRUE(spec.has_value()); auto pkg = to_xim_package(*spec); - auto found = payload_frontend(t.root, pkg, Family::Msvc); + // The family comes from the PACKAGE now, not from a second argument every + // caller had to source from a differently-named local -- five of them + // composed `payload->binDir` themselves instead and so could not see + // `frontendSubdir` at all. + EXPECT_EQ(pkg.family, Family::Msvc); + auto found = payload_frontend(t.root, pkg); ASSERT_FALSE(found.empty()) << "payload_frontend found no cl.exe under " << t.root; EXPECT_EQ(found.filename(), "cl.exe"); @@ -314,8 +319,16 @@ TEST(MsvcManaged, PayloadFrontendFindsClWhereMsvcActuallyKeepsIt) { // A root with no toolset at that version stays empty rather than // returning a path that does not exist. EXPECT_TRUE(payload_frontend(t.root, - to_xim_package(*parse_toolchain_spec("msvc@14.52.36629")), - Family::Msvc).empty()); + to_xim_package(*parse_toolchain_spec("msvc@14.52.36629"))) + .empty()); + + // AND THE DIRECTORY THE SEARCH USED IS AVAILABLE FOR THE MESSAGE. MSVC is + // the case that proves it is a lookup rather than a constant: the path + // carries the toolset version, so a refusal naming `bin` would name a + // directory nothing looked in. + auto dir = payload_frontend_dir(t.root, pkg); + EXPECT_FALSE(dir.empty()); + EXPECT_NE(dir.string().find("14.44.35207"), std::string::npos) << dir; } // ─── Windows SDK discovery ─────────────────────────────────────────────── diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index e41c6b43..97c9c61b 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -701,6 +701,77 @@ TEST(Triple, OnlyTheSdkTargetsShipTheirOwnSysroot) { // A CAPABILITY PIN CANNOT BE OVERRIDDEN, BECAUSE NOTHING ELSE CAN EMIT THE // TARGET. A convention pin is a preference; this is a fact about the world. +// THE EFFECTIVE TRIPLE CARRIES THE PROJECT'S MINIMUM PLATFORM VERSION, AND THE +// CANONICAL ONE NEVER DOES. +// +// Two platforms fuse it and each names it in its own words: macOS's deployment +// target, Android's minimum API level. Measured on a real clang -- +// `-target aarch64-linux-android21 -print-effective-triple` answers +// `aarch64-unknown-linux-android21` -- so the level belongs on the ENV segment +// of the effective triple. +// +// Asserted as the PAIR, because the whole design is that the two differ: if +// `str()` ever carried the version, the output directory and `cfg()` would +// multiply per level and the table would need a row for each. +TEST(Triple, TheMinimumPlatformVersionReachesTheEffectiveTripleAndNotTheCanonicalOne) { + auto droid = parse("aarch64-linux-android"); + ASSERT_TRUE(droid.has_value()); + EXPECT_EQ(droid->str(), "aarch64-linux-android"); + EXPECT_EQ(droid->llvm_triple("24"), "aarch64-unknown-linux-android24"); + EXPECT_EQ(droid->llvm_triple("21"), "aarch64-unknown-linux-android21"); + // Unset is legal and means the NDK's own default -- what clang normalises + // when no level is given. + EXPECT_EQ(droid->llvm_triple(""), "aarch64-unknown-linux-android"); + // And the canonical form is unmoved by any of it. + EXPECT_EQ(droid->str(), "aarch64-linux-android"); + + // macOS, the platform this parameter already served, is unchanged. + auto mac = parse("aarch64-macos"); + ASSERT_TRUE(mac.has_value()); + EXPECT_EQ(mac->llvm_triple("15.2"), "arm64-apple-macos15.2"); + EXPECT_EQ(mac->str(), "aarch64-macos"); + + // AND NO OTHER ROW TAKES IT. One parameter serves both platforms, so the + // risk is a caller handing one platform's answer to another's row -- an + // ordinary Linux target must ignore it rather than fuse it. + auto lin = parse("x86_64-linux-gnu"); + ASSERT_TRUE(lin.has_value()); + EXPECT_EQ(lin->llvm_triple("24"), "x86_64-unknown-linux-gnu"); + auto musl = parse("aarch64-linux-musl"); + ASSERT_TRUE(musl.has_value()); + EXPECT_EQ(musl->llvm_triple("24"), "aarch64-unknown-linux-musl"); +} + +// THE FOUR-FIELD SPELLING IS WHAT EVERY OTHER TOOLCHAIN PRINTS, so refusing it +// is a cost with no design benefit. `em++ -v` passes +// `-target wasm32-unknown-emscripten`, rustc's table lists that spelling, and a +// user copying either into a manifest should be understood. +// +// mcpp's canonical form elides the vendor -- `unknown`, `pc` and `w64` carry no +// information for any row in the table -- so this is a normalisation and not a +// second vocabulary: `str()` returns the three-field form either way, which is +// what keeps the output directory, `cfg()` and the ABI tag single-valued. +TEST(Triple, TheFourFieldSpellingParsesToTheSameCanonicalTriple) { + struct Case { const char* spelled; const char* canonical; }; + for (auto [spelled, canonical] : { + Case{"wasm32-unknown-emscripten", "wasm32-emscripten"}, + Case{"aarch64-apple-ios", "aarch64-ios"}, + Case{"aarch64-unknown-linux-android", "aarch64-linux-android"}, + Case{"x86_64-unknown-linux-gnu", "x86_64-linux-gnu"}, + Case{"x86_64-pc-windows-msvc", "x86_64-windows-msvc"}, + Case{"aarch64-unknown-linux-musl","aarch64-linux-musl"}, + }) { + auto t = parse(spelled); + ASSERT_TRUE(t.has_value()) << spelled; + EXPECT_EQ(t->str(), canonical) << spelled; + // And the three-field form still parses to itself, so accepting the + // longer spelling did not make the canonical one a second dialect. + auto c = parse(canonical); + ASSERT_TRUE(c.has_value()) << canonical; + EXPECT_EQ(c->str(), canonical) << canonical; + } +} + TEST(Triple, WasmJoinsTheCapabilityPinsBecauseNothingElseEmitsIt) { auto wasm = parse("wasm32-emscripten"); ASSERT_TRUE(wasm.has_value()); From c30dc747d2e28c9275835efe7c0f639356cc10d8 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:43:39 +0800 Subject: [PATCH 09/13] fix(targets): a capability pin explains its own row, and the resolved line names the payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE RULE IS SHARED AND THE REASON IS NOT, which `prepare.cppm` already said: "one sentence covering both would be wrong about one of them: a PE+musl target is not bare metal, and a reader told it is stops reading." A third row was then added without a third reason, so the sentence became wrong about the new one. Measured: `--target wasm32-emscripten` with a declared gcc was refused CORRECTLY and explained with "No gcc payload emits a PE with a musl C library" -- a true sentence about a different row. AND THE GATE ASKED THE WRONG QUESTION, which is the defect a reader would not think to look for. It tested `family != Llvm`, right while every capability-pinned row pinned llvm. `wasm32-emscripten` pins `emsdk@6.0.9`, and emsdk normalises to the LLVM FAMILY because `em++` is clang -- so a declared `llvm@22.1.8` passed the gate, was never refused, and resolved the generic llvm payload for a target it cannot emit. The condition is now the row's own pin, which is the question it was always answering. The closing line names that pin too, instead of the fixed word "llvm". `tests/e2e/640` holds five properties: each row's own sentence, the declared-llvm gap, and the closing line naming this row's pin. Against released 2026.9.10.2 the three wasm cases go red and the two pre-existing rows stay green. THE RESOLVED LINE NAMES WHICH PAYLOAD ANSWERED. `emsdk@6.0.9` printed as `Resolved llvm@6.0.9` -- indistinguishable from the real `xim:llvm`, and not what the user typed. `mcpp toolchain list` has the same problem and the matrix scan takes one toolchain per family, so two llvm-family payloads on one host could not both be enumerated. The family and the payload are two questions -- which flag vocabulary, and which archive -- and `to_xim_package` already answered the second from the target. `ToolchainSpec::payloadName` is the field that lets it be said. Empty means the family's own payload, which is every row but these two, so no existing output moves. AND THE MATRIX EXPECTATIONS ARE THE TRUTHFUL READING RATHER THAN THE AMBITIOUS ONE. Naming `emsdk@6.0.9` in the declared-toolchain column made the scan try to INSTALL it on every host -- `emsdk@6.0.9 装不上,而期望表声明了它` -- because that column is the axis the scan installs from. With wasm a capability pin, the verdict for a declared gcc or llvm is `capability-pin` on a host that can serve the row and `host-cannot-serve` on the two where `xim:emsdk` is not published. Verified against the engine rather than assumed: gcc refused, llvm refused, emsdk resolves and builds. 109 unit tests pass; 638, 639, 640 and 233 pass. --- CHANGELOG.md | 36 +++++++++ src/build/prepare.cppm | 52 +++++++++++-- src/toolchain/compat.cppm | 7 ++ src/toolchain/registry.cppm | 30 ++++++- ...0_a_capability_pin_explains_its_own_row.sh | 78 +++++++++++++++++++ tests/matrix/expected.tsv | 12 +-- tests/unit/test_toolchain_registry.cpp | 36 +++++++++ 7 files changed, 234 insertions(+), 17 deletions(-) create mode 100755 tests/e2e/640_a_capability_pin_explains_its_own_row.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 22cce3df..63466f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,42 @@ min_api_level = 24 要么是 Apple 要么是 Android,所以一个槽装不下两者,而两者回答的是同一个问题。改名 不额外增加任何重建 —— mcpp 版本本来就在这个键里。 +### 能力钉的理由必须属于它自己那一行 + +`prepare.cppm` 的注释早就把规则写明了:「一句话覆盖两者,就会对其中一个是错的 —— +PE+musl 目标不是裸机,而一个被告知它是裸机的读者会停止阅读。」而我加了第三行却 +没有加第三条理由,于是那句话对新来的那一行成了错的。 + +实测:`--target wasm32-emscripten` 声明 gcc 时**拒绝是对的**,而解释是「No gcc +payload emits a PE with a musl C library」—— 一句关于另一行的、本身为真的话。 + +**而那个闸问错了问题。** 它测 `family != Llvm`,这在「每个能力钉行都钉 llvm」时是 +对的。`wasm32-emscripten` 钉 `emsdk@6.0.9`,而 emsdk **归一到 llvm 族**(因为 +`em++` 就是 clang)—— 所以声明 `llvm@22.1.8` **通过了这个闸、从未被拒绝**,并为一个 +它发不出来的目标解析了通用 llvm 载荷。判据现在是**那一行自己的钉**,也就是这一行 +一直在回答的那个问题。 + +`tests/e2e/640` 把五条钉住:三行各自的句子、那个「声明 llvm 也必须被拒绝」的缺口, +以及收尾那句话要点名**本行的**钉而不是一个固定的词。拿已发布的 2026.9.10.2 对照: +三条与 wasm 有关的变红,两条既有的保持绿。 + +### 解析行里说出**是哪个载荷**回答的 + +`emsdk@6.0.9` 归一到 llvm 族 —— `em++` 就是 clang,而第四个 family 值会是对编译器 +的假陈述。代价是那行输出读作 `Resolved llvm@6.0.9`:与真正的 `xim:llvm` 无法区分, +而且不是用户输入的东西。`mcpp toolchain list` 有同样的问题,而矩阵扫描**每族只取 +一个**工具链,所以一台宿主上两个 llvm 族的载荷无法都被枚举。 + +族与载荷是两个问题: + +``` +族 这个编译器说哪一套 flag 词汇? llvm +载荷 哪个归档提供它? emsdk +``` + +`to_xim_package` 早就从目标回答了第二个;新增的字段只是让它**能被说出来**。空值 +表示「该族自己的载荷」,也就是除这两行之外的每一行 —— 所以没有任何既有输出改变。 + ### 四段式拼法被接受 `em++ -v` 传给它自己 clang 的是 `-target wasm32-unknown-emscripten`,rustc 的表里 diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 5b8c760b..0ad232fd 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2587,16 +2587,51 @@ prepare_build(bool print_fingerprint, if (known && parsed && parsed->pin_is_capability() && tc_origin_is_user_explicit(tcOrigin) && tcSpec.has_value()) { auto declared = mcpp::toolchain::parse_toolchain_spec(*tcSpec); - if (declared && declared->family != mcpp::toolchain::Family::Llvm) { - // THE REASON TRAVELS WITH THE ROW. Both rows refuse for the + // WHICH DECLARATIONS THE ROW ACCEPTS IS THE ROW'S PIN, NOT A FIXED + // FAMILY. + // + // This asked `family != Llvm`, which was right while every + // capability-pinned row pinned llvm. `wasm32-emscripten` pins + // `emsdk@6.0.9`, and emsdk NORMALISES to the llvm family -- `em++` + // is clang -- so a declared `llvm@22.1.8` passed this gate, was + // never refused, and resolved the generic llvm payload for a target + // it cannot emit. The condition is now the pin's own family, which + // is the question the row was always answering. + const auto pinFamily = [&]() -> std::optional { + if (known->pin.empty()) return mcpp::toolchain::Family::Llvm; + if (auto ps = mcpp::toolchain::parse_toolchain_spec( + std::string(known->pin))) + return ps->family; + return std::nullopt; + }(); + const bool declaredMatchesPin = + declared && pinFamily && declared->family == *pinFamily + // An emsdk row is llvm-family, so the family alone cannot + // separate `emsdk@6.0.9` from `llvm@22.1.8`. The pin's own + // spelling is what does. + && (known->pin.empty() + || tcSpec->find(known->pin.substr(0, known->pin.find('@'))) + != std::string::npos); + if (declared && !declaredMatchesPin) { + // THE REASON TRAVELS WITH THE ROW. The rows refuse for the // same rule and NOT for the same reason, and one sentence - // covering both would be wrong about one of them: a PE+musl - // target is not bare metal, and a reader told it is stops - // reading. + // covering all of them would be wrong about the others: a + // PE+musl target is not bare metal, a wasm target is neither, + // and a reader told the wrong one stops reading. + // + // Measured before the third arm existed: `--target + // wasm32-emscripten` with a declared gcc was refused correctly + // and explained with "No gcc payload emits a PE with a musl C + // library", which is a true sentence about a different row. std::string_view why = parsed->is_freestanding() ? "A freestanding target has no per-host cross payload: " "clang and lld are\n" " cross-compilers by construction and gcc is not." + : parsed->is_wasm() + ? "Nothing but Emscripten emits WebAssembly: `em++` is a " + "clang whose target,\n" + " sysroot and JavaScript glue all come from its own " + "payload." : "No gcc payload emits a PE with a musl C library — the " "mingw payload emits\n" " PE with the MinGW CRT, which is the separate " @@ -2605,12 +2640,13 @@ prepare_build(bool print_fingerprint, return std::unexpected(std::format( "target '{}' cannot be emitted by '{}'.\n" " {}\n" - " The row names llvm as a capability rather than as a " - "preference, so this\n" - " one line is not a convention you can override.\n" + " The row names `{}` as a capability rather than as a " + "preference, so\n" + " this one line is not a convention you can override.\n" " remove the `[toolchain]` line for this target, or set " "it to `{}`.", parsed->str(), *tcSpec, why, + known->pin.empty() ? std::string_view("llvm") : known->pin, known->pin.empty() ? std::string_view("llvm") : known->pin)); } } diff --git a/src/toolchain/compat.cppm b/src/toolchain/compat.cppm index 0cd51728..8bdd0b15 100644 --- a/src/toolchain/compat.cppm +++ b/src/toolchain/compat.cppm @@ -38,6 +38,11 @@ struct NormalizedSpec { std::string family; // "gcc" | "llvm" | "msvc" | "openkal-llvm" std::string version; // numeric (possibly partial), or "system"; never "-musl"-suffixed triple::Triple target; // empty = host + // WHICH PAYLOAD, when the family alone does not say. `emsdk` and + // `android-ndk` both normalise to the llvm family -- their compilers ARE + // clang -- so without this the two are indistinguishable from `xim:llvm` + // in every line mcpp prints. Empty for every other spelling. + std::string payload; // Set when a legacy spelling was rewritten; `hint` is the one-line note. bool changed = false; std::string hint; @@ -137,6 +142,7 @@ std::optional normalize_spec(std::string_view compilerIn, // current ones. if (compiler == "emsdk" || compiler == "emscripten") { out.family = "llvm"; + out.payload = "emsdk"; if (auto t = triple::parse("wasm32-emscripten")) out.target = *t; if (muslVersionSuffix) return std::nullopt; return out; @@ -147,6 +153,7 @@ std::optional normalize_spec(std::string_view compilerIn, // who typed it for x86_64. if (compiler == "android-ndk" || compiler == "ndk") { out.family = "llvm"; + out.payload = "android-ndk"; if (muslVersionSuffix) return std::nullopt; return out; } diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 68a32f62..96b646c5 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -74,8 +74,31 @@ struct ToolchainSpec { bool is_host_target() const { return target.empty(); } // "gcc@16.1.0" — the toolchain axis alone (config persistence, matching). + // WHICH PAYLOAD ANSWERED, NOT ONLY WHICH FAMILY. + // + // `emsdk@6.0.9` normalises to the llvm family, because `em++` IS clang and + // a fourth family value would be a false claim about the compiler. The + // consequence was a display line reading `Resolved llvm@6.0.9`, which is + // indistinguishable from the real `xim:llvm` and is not what the user + // typed. `mcpp toolchain list` has the same problem, and the matrix scan + // takes one toolchain per family, so two llvm-family payloads on one host + // could not both be enumerated. + // + // The family and the payload are two questions: + // + // family what flag vocabulary does this compiler speak? llvm + // payload which archive provides it? emsdk + // + // `to_xim_package` already answers the second from the target; this is the + // field that lets it be SAID. Empty means the family's own payload, which + // is every row but these two, so nothing else's output moves. + std::string payloadName; + std::string spec_str() const { - return std::format("{}@{}", family_name(family), version); + return std::format("{}@{}", + payloadName.empty() ? family_name(family) + : std::string_view(payloadName), + version); } // "gcc@16.1.0" or "gcc@16.1.0 → x86_64-windows-gnu" — user-facing. @@ -349,8 +372,9 @@ parse_toolchain_spec(std::string compilerArg, if (norm->family == "llvm") spec.family = Family::Llvm; else if (norm->family == "msvc") spec.family = Family::Msvc; else spec.family = Family::Gcc; - spec.version = std::move(norm->version); - spec.target = std::move(norm->target); + spec.version = std::move(norm->version); + spec.target = std::move(norm->target); + spec.payloadName = std::move(norm->payload); // `@system` IS NOT A GENERAL SPELLING, and refusing it here is the point. // diff --git a/tests/e2e/640_a_capability_pin_explains_its_own_row.sh b/tests/e2e/640_a_capability_pin_explains_its_own_row.sh new file mode 100755 index 00000000..435bbec8 --- /dev/null +++ b/tests/e2e/640_a_capability_pin_explains_its_own_row.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# requires: gcc +# 640_a_capability_pin_explains_its_own_row.sh — a row whose pin is a +# capability refuses a declared toolchain that cannot emit it, and the reason +# names THAT row. +# +# THE RULE IS SHARED AND THE REASON IS NOT. `prepare.cppm` says so in its own +# comment -- "one sentence covering both would be wrong about one of them: a +# PE+musl target is not bare metal, and a reader told it is stops reading" -- +# and then a third row was added without a third reason, so the sentence became +# wrong about the new one. Measured: `--target wasm32-emscripten` with a +# declared gcc was refused correctly and explained with "No gcc payload emits a +# PE with a musl C library", which is true about a different row. +# +# AND THE GATE ASKED THE WRONG QUESTION. It tested `family != Llvm`, which was +# right while every capability-pinned row pinned llvm. `wasm32-emscripten` pins +# `emsdk@6.0.9`, and emsdk normalises to the llvm family because `em++` IS +# clang -- so a declared `llvm@22.1.8` passed the gate, was never refused, and +# resolved the generic llvm payload for a target it cannot emit. Case 4 is that +# one, and it is the case a reader would not think to write. +set -e + +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail=0 + +# Each row's reason must appear for ITS target and for no other. The probe is a +# substring of the sentence rather than the whole of it, so rewording stays +# free while the pairing stays asserted. +check() { # target declared-toolchain expected-phrase label + local target=$1 tc=$2 phrase=$3 label=$4 + local d="$t/$RANDOM$RANDOM"; mkdir -p "$d/src" + printf '[package]\nname = "c"\nversion = "0.1.0"\n\n[toolchain]\nlinux = "%s"\n' "$tc" > "$d/mcpp.toml" + printf 'int main(){return 0;}\n' > "$d/src/main.cpp" + local out + out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target "$target" 2>&1 ) || true + if ! grep -q "cannot be emitted by" <<<"$out"; then + echo "FAIL: $label — not refused at all" + grep -m3 -E "^(error|warning)" <<<"$out" | sed 's/^/ /' + fail=1; return + fi + if grep -qF "$phrase" <<<"$out"; then + echo " ok: $label" + else + echo "FAIL: $label — the refusal does not carry this row's reason" + echo " wanted: $phrase" + grep -A3 "cannot be emitted by" <<<"$out" | sed 's/^/ /' + fail=1 + fi +} + +echo "== 640: the rule is shared, the reason is the row's ==" + +# 1-3. each row's own sentence, for a declared gcc. +check wasm32-emscripten gcc@16.1.0 "Nothing but Emscripten emits WebAssembly" "wasm names Emscripten" +check riscv64-none-elf gcc@16.1.0 "no per-host cross payload" "bare metal names the cross payload" +check x86_64-windows-musl gcc@16.1.0 "PE with a musl C library" "PE+musl names the C library" + +# 4. THE GATE. `llvm@22.1.8` is the llvm family, and so is emsdk -- so a family +# test cannot separate them and this declaration used to pass unrefused. +check wasm32-emscripten llvm@22.1.8 "Nothing but Emscripten emits WebAssembly" "a declared llvm is refused too" + +# 5. And the sentence names the row's OWN pin rather than a fixed word: the +# closing line used to read "The row names llvm as a capability" on every +# row, which is wrong for the one pinned to emsdk. +d="$t/pin"; mkdir -p "$d/src" +printf '[package]\nname = "c"\nversion = "0.1.0"\n\n[toolchain]\nlinux = "gcc@16.1.0"\n' > "$d/mcpp.toml" +printf 'int main(){return 0;}\n' > "$d/src/main.cpp" +out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target wasm32-emscripten 2>&1 ) || true +if grep -q "names .emsdk@6.0.9. as a capability" <<<"$out"; then + echo " ok: the closing line names this row's pin" +else + echo "FAIL: the closing line does not name emsdk@6.0.9" + grep -A5 "cannot be emitted by" <<<"$out" | sed 's/^/ /' + fail=1 +fi + +if [ "$fail" -ne 0 ]; then echo "FAIL: 640"; exit 1; fi +echo "PASS: 640" diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index edbd76e9..9d3a3db3 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -274,15 +274,15 @@ payload linux-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 aarch64-ios msvc@system - - - - - unsupported tier-planned -graph linux-aarch64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none -graph linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none -graph linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +graph linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin graph macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve graph windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve graph windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported host-cannot-serve -payload linux-aarch64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none -payload linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none -payload linux-x86_64 wasm32-emscripten emsdk@6.0.9 wasm32-unknown-emscripten payload emscripten(payload) libc++(payload) - ok none +payload linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin payload macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported host-cannot-serve diff --git a/tests/unit/test_toolchain_registry.cpp b/tests/unit/test_toolchain_registry.cpp index a69f41ec..b1c3e206 100644 --- a/tests/unit/test_toolchain_registry.cpp +++ b/tests/unit/test_toolchain_registry.cpp @@ -373,3 +373,39 @@ TEST(SdkPayloads, ServedOnTheHostsTheSdkIsPublishedFor) { ASSERT_TRUE(droid.has_value()); EXPECT_EQ(mcpp::toolchain::host_can_serve(*droid), mcpp::platform::is_linux); } + +// ─── The payload is SAID, not only resolved (R3) ─────────────────────────── +// +// `emsdk@6.0.9` normalises to the llvm family because `em++` IS clang, and a +// fourth family value would be a false claim about the compiler. The +// consequence was `Resolved llvm@6.0.9` -- indistinguishable from the real +// `xim:llvm`, and not what the user typed. The family and the payload are two +// questions, and `to_xim_package` already answered the second; this is the +// field that lets it be printed. +TEST(SdkPayloads, TheDisplayNamesThePayloadAndNotOnlyTheFamily) { + auto em = mcpp::toolchain::parse_toolchain_spec("emsdk@6.0.9"); + ASSERT_TRUE(em.has_value()); + EXPECT_EQ(em->family, mcpp::toolchain::Family::Llvm) + << "em++ is clang; a fourth family would be a false claim"; + EXPECT_EQ(em->payloadName, "emsdk"); + EXPECT_NE(em->display().find("emsdk@6.0.9"), std::string::npos) + << em->display(); + EXPECT_EQ(em->display().find("llvm@"), std::string::npos) << em->display(); + + auto ndk = mcpp::toolchain::parse_toolchain_spec("android-ndk@30.0.16248370"); + ASSERT_TRUE(ndk.has_value()); + EXPECT_EQ(ndk->family, mcpp::toolchain::Family::Llvm); + EXPECT_EQ(ndk->payloadName, "android-ndk"); + EXPECT_NE(ndk->display().find("android-ndk@"), std::string::npos) + << ndk->display(); + + // AND NOTHING ELSE MOVES. Empty `payloadName` means the family's own + // payload, which is every row but these two -- so no existing output + // changes, which is what makes this additive. + for (auto spelled : {"llvm@22.1.8", "gcc@16.1.0", "msvc@14.44.35207"}) { + auto sp = mcpp::toolchain::parse_toolchain_spec(spelled); + ASSERT_TRUE(sp.has_value()) << spelled; + EXPECT_TRUE(sp->payloadName.empty()) << spelled; + EXPECT_NE(sp->display().find(spelled), std::string::npos) << sp->display(); + } +} From 09fc2ad35cebade533b5a591f2c944a06e8428e5 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:52:39 +0800 Subject: [PATCH 10/13] 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 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 /.../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 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. --- ...26-09-11-platform-targets-design-review.md | 40 +++- CHANGELOG.md | 179 +++++++++++++- docs/20-toolchains.md | 5 +- docs/21-the-target-triple.md | 19 +- docs/zh/20-toolchains.md | 5 +- docs/zh/21-the-target-triple.md | 19 +- modules/toolchain-model/src/triple.cppm | 121 +++++++++- src/build/flags.cppm | 33 +++ src/build/hermetic.cppm | 27 ++- src/build/prepare.cppm | 132 +++++++++- src/build/runtime_validation.cppm | 29 +++ src/toolchain/clang.cppm | 38 ++- src/toolchain/hostflags.cppm | 24 +- src/toolchain/registry.cppm | 94 +++++++- ...0_a_capability_pin_explains_its_own_row.sh | 54 +++++ ...ws_are_wired_and_the_simulator_is_a_row.sh | 135 +++++++++++ tests/matrix/expected.tsv | 84 ++++--- tests/unit/test_toolchain_registry.cpp | 97 +++++++- tests/unit/test_toolchain_triple.cpp | 225 ++++++++++++++++-- 19 files changed, 1252 insertions(+), 108 deletions(-) create mode 100755 tests/e2e/641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index e0000a58..5e3b8ca2 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -984,15 +984,31 @@ plan. | E5 | matrix expectations for 12 wasm cells | **done** | | E6 | unit tests for E1-E4 | **done** | | E7 | the EOL debian leg swapped for debian-12 | **done** | -| E8 | R8: `parse()` accepts `wasm32-unknown-emscripten`, `aarch64-apple-ios` | todo | -| E9 | R1: `aarch64-ios-sim` and `x86_64-ios-sim` rows | todo | -| E10 | R3: a payload display identity, so emsdk is not shown as `llvm` | todo | -| E11 | the `.wasm` sibling as an implicit link output | todo | -| E12 | R6: Android and iOS rows to `preview` where the evidence supports it | todo | -| E13 | docs: `20-toolchains`, `21-the-target-triple`, `22-target-side` + zh | todo | -| E14 | CHANGELOG | todo | - -| E15 | R2: `min_api_level` under `[target.]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | todo | +| 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 | +| 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` | +| E10 | R3: a payload display identity, so emsdk is not shown as `llvm` | **done**, `ToolchainSpec::payloadName` | +| 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 | +| 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 | +| E13 | docs: `04-mcpp-toml` §2.7.3, `20-toolchains`, `21-the-target-triple` + zh | **done** | +| E14 | CHANGELOG | **done** | +| E15 | R2: `min_api_level` under `[target.]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | **done** | +| 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 | + +**E16 was not in the original plan, and the target matrix is what produced +it.** The predicate returned `mcpp::platform::is_linux` for a row whose SDK +ships its own sysroot, because `xim:emsdk` and `xim:android-ndk` declared only +`xpm.linux` when it was written. X2 made that false, and the 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 -- `mcpp build --target wasm32-emscripten` +on macOS reported a target this table knows as one it had never heard of, which +is the same defect 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 in this repository that can see a +row disappear, because every other check asks about a row it already has. R4 is not in that list because it is **withdrawn** (§12.1a), not deferred: its 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. | X1 | Android CN mirrors under clause 3.5 | **done**, #812 | | X2 | emsdk, NDK and emulator on all three hosts | **done**, hashes verified | | X3 | `xim:python` aarch64 and a GLOBAL url | **done** | +| 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 | | X4 | R7: `xim:rcodesign` | todo | | X5 | R12: `xim:pymobiledevice3` | todo | | 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. | P2 | R9: `dist-ipa` | todo | | P3 | R7's plugin half: prefer `xim:rcodesign` over the host's codesign | todo | -**mcpp-index**: publish `mcpp:plugins@0.6.0`. One task, blocked on the tag's -sha256. +**mcpp-index**: publish `mcpp:plugins@0.6.0`. **done** -- tag published, the +GitCode asset verified by download-back and byte-identical to the GitHub source +archive, so one sha256 names both hosts; PR open. **Recorded and not attempted**: R10 (`dmg`/`pkg` creators), R11 (Darling), R13's implementation (the pattern is documented; the runner programs are X6). diff --git a/CHANGELOG.md b/CHANGELOG.md index 63466f9c..cad62bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,167 @@ wasm 同时加入**能力钉**(capability pin):没有别的东西能发出 WebAssembly,所以 声明一个 `gcc@16.1.0` 是一个无法被满足的请求,说出来比解析出 gcc 再在它内部失败要好。 +### 两个 Android 行从 `planned` 到 `preview` + +`mcpp build --target aarch64-linux-android` 产出真实的 Android 产物,工程侧除 +`--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, 同一个 interpreter +``` + +两行**共用一个钉** `android-ndk@30.0.16248370`。这不是省事,而是这条路径的全部 +性质所系:NDK 不命名架构,`--target` 才命名 —— 于是每一处「谁说出目标」的缺口都 +会在这里现形,而在 wasm 上都不会,因为 `em++` 只有一个目标。 + +`preview` 而不是 `verified`:两者都**构建**过,都没有被**执行**过 —— 跑起来需要一台 +设备或一个模拟器,而那正是这两个层级的差别。 + +五处引擎缺口,每一处都是前一处的失败找出来的,而每一处都**只在多目标载荷上现形**: + + 1. **std 模块的预编译从来拿不到 `--target`。** `stdModuleTargetFlags` 只到达 + codegen 那一条命令,依据是「第一步要头文件、第二步要机器」。第一步两者都要: + 一个不说目标的 `--precompile` 会把标准库自己的 `#include <__config>` 解析到 + **正在构建的那台机器**上。而这个文件里早就记着同一句报错 —— 2026-08 一台 + Windows 宿主上的 `'__config' file not found` —— 同一个成因,另一条路径到达。 + 现在预编译从两个来源里**有机器的那一个**取:`stdModuleFlags` 非空时它是 + `stdModuleTargetFlags` 的超集,所以取它就不会让 `--target` 上两次命令行。 + 2. **`-D__BIONIC_CTYPE_INLINE=`。** bionic 把 `isalnum` 一族声明为 `static + inline`,而 libc++ 的模块面用 `using std::isalnum` 导出它们 —— using 声明不能 + 导出内部链接的名字,于是预编译一次性在 14 个名字上失败。范围限定在 std 模块 + 上:被满足的规则是「从模块里导出」,而一个直接 `#include ` 的翻译 + 单元有权拿到 bionic 的 inline 定义。`xim:android-ndk` 自己的安装期自检从另一个 + 方向得到同一个结论。 + 3. **「自带 sysroot 就什么都不告诉它」比实际强了一个 token。** 那个提前返回 + 站在 `host_compile_tokens` 里「THE TRIPLE, SAID OUT LOUD」那一段**之前**,而 + 那一段讲的是相反的规则、理由相同:一个普通 clang 不被告知就为它自己所在的 + 机器发码。两句话各自对它自己的对象是对的 —— **体系**是载荷的、不可重建; + **是哪个目标**仍是 mcpp 要说的。报出这件事的是模块加载器而不是任何一次编译: + 「AST file 'std.pcm' was compiled for the target + 'aarch64-unknown-linux-android21' but the current translation unit is being + compiled for target 'x86_64-unknown-linux-gnu'」,后面跟着八条级联的 + 「use of undeclared identifier 'std'」,而读者先看到的是后者。 + 4. **链接行同样没有目标。** 两条链接分支都被跳过,而它们携带的东西被跳过是对的: + C 库、C++ 运行期、crt 对象和加载器全在 SDK 里,驱动自己会找。它**不能**猜的 + 是找哪一个。落空的结果是目标的对象与宿主的启动文件链在一起:六个宿主对象, + 每一个都由一个以为自己在为本机构建的驱动解析出来。新增的第三条分支只放 + `crossTarget`,别的什么都不放。 + 5. **`discover_link_runtime_dirs` 的闸在错误的时刻求值。** 那个闸本身是对的 —— + 它拒绝为自带 sysroot 的目标报告这些目录 —— 而它跑在**探测期**,比目标被赋值 + 更早,那时 `targetTriple` 还是宿主的。产物是揭发它的东西:一条 Android 链接行 + 上带着 `-L /.../prebuilt/linux-x86_64/lib/x86_64-unknown-linux-gnu`, + 最后一段是本机的 triple,由 `root / "lib" / targetTriple` 产生 —— 那个字符串 + 写着被问出来的那个问题。 + +还有两处**产物检查**把一个正确的产物判成了缺陷,而两处的形状相同:一条对**面向 +宿主**的产物为真的规则,施加在一个交叉产物上。 + + - **hermetic 链接检查**把 `/system/bin/linker64` 算成「sandbox 之外」。它是这个 + 函数检查的所有路径里唯一**不在本机解析**的那一个:它被记进产物,由**设备**在 + 加载时读取,而 Android 的这条路径由 ABI 规定,不可能在任何载荷里面。消息对 + 它看到的东西是准确的、对它的含义是错的 —— 这是更难的那一种,它点名了一条确实 + 在 sandbox 之外的真实路径,并邀请读者去重装一个与此无关的 glibc 载荷。 + - **runtime 闭包校验的 rule B**拿产物的 `PT_INTERP` 与**宿主的** `RuntimeBinding + glibc@2.44` 相比,报「one process cannot mix runtime payloads」—— 一句关于一个 + 永不会存在的进程的真话,接着建议一个帮不上忙的 SubOS。上方那个 linux/glibc + 闸拦不住它:Android 的 triple 的 `os` **就是** `linux`,这是刻意的。 + +Android 同时加入**能力钉**,而它的理由与另外三个都不同。那三个被拒绝是因为工具链 +发不出那个**格式**;一个普通 clang 发 aarch64 ELF 完全没问题。它拿不出来的是 +**体系** —— bionic 的头文件、按 API level 的存根、加载器路径都在 NDK 里,没有任何 +包能把它们加到另一个编译器上。 + +### `host_can_serve` 不再把「Linux」编译进引擎 + +这一行此前对自带 sysroot 的目标返回 `mcpp::platform::is_linux`,而它自己的注释就 +写着这个判断的失效条件:「When a darwin or windows NDK lands in the index — +upstream publishes both — this is the one line that changes.」它落地了,所以这就是 +那一行。 + +同一个 goal 的两半随即互相矛盾:索引把载荷发布到三个宿主,而引擎在其中两个上 +**把这一行从 `toolchain list` 里删掉**。症状不是一个错答案而是一个**缺席的**答案 —— +macOS 上 `mcpp build --target wasm32-emscripten` 报了一个这张表认识的目标「未知」, +正是 `planned` 这个层级存在的目的所要避免的事。 + +两半都不是它被发现的地方。`scan (macos-arm64)` 与 `scan (windows-x86_64)` 在**格数** +上失败 —— 测到 24 格、表里声明 25 格 —— 而那唯一缺的一格点名了这一行。一个按宿主 +跑、拿一张入库的表作判据的 job,是这个仓库里唯一能看见**一行消失**的东西,因为其他 +每一处检查问的都是它已经拿到的那一行。 + +那条闸的单测同样是**在一台宿主上由算术为真**:它写的是 +`EXPECT_EQ(host_can_serve(*wasm), mcpp::platform::is_linux)`,而在 Linux 上 +`is_linux` **就是** `true`。一个期望值等于它所运行的宿主的判据,报不出另两个宿主上 +的变化。现在它无条件断言,并且补上了「这个谓词仍然说得出『不』」那一半。 + +### 模拟器是一行,不是一个 runner + +一次 iOS 模拟器构建是**另一个目标**:它自己的 SDK(`iPhoneSimulator.sdk`)、自己的 +对象,取 `-mios-simulator-version-min` 而真机取 `-miphoneos-version-min`。此前它 +无法被拼写,而设备那一行的注释把反对意见写反了 —— 反对的是**没有**一个单独的行, +而不是有。 + +`env = "sim"` 给出 `aarch64-ios-sim` 与 `x86_64-ios-sim`,也就是 Rust 的 +`aarch64-apple-ios-sim` 减去这张表本来就省略的 vendor 段。Apple 自己的 +`-simulator` 拼法也解析到同一行 —— clang 打印的 effective triple 带的是那一种,而 +一个把它粘回来的读者不该被告知 mcpp 从没听说过它。 + +两个架构都有,理由与 Android 那一对相同:模拟器跑**宿主的**架构,所以一行会描述 +一半机器跑不了的模拟器。 + +两行都是 `planned`,而阻塞项与设备行是同一个**许可**问题,不是一个载荷问题:NDK 是 +Apache-2.0、Emscripten 是 MIT,而 iPhoneOS 与 iPhoneSimulator 的 SDK 在 Xcode 里, +两者都不可再分发。它们今天买到的是:`mcpp build --target aarch64-ios-sim` 答 +`tier-planned` 并点名那一行,而不是答 `unknown target` —— 后者是假的。 + +### API level 的默认值取自载荷,而「不写」不是一个合法答案 + +`min_platform_version` 此前在项目没有声明 `min_api_level` 时返回空串,注释写的是 +「the NDK's own default, which clang supplies」。那句话从未被验证,而且是错的: + +``` +--target=aarch64-unknown-linux-android (无级别) +sys/cdefs.h:365:2: error: Unversioned target triples are not supported! +``` + +bionic 直接拒绝一个不带版本的 triple,所以级别是**强制**的,一个从没听说过 API +level 的工程也需要一个。于是问题变成这个数字从哪里来。不是一个编译进来的常量: +这个仓库已经记过不止一次,一个写进注释的版本号会变成一个写进诊断的版本号,再变成 +某人 install 命令里的版本号,而 NDK 的下限随 NDK 移动。载荷自己回答 —— +`meta/platforms.json` 是上游自己声明的支持区间,r30 是 `{"min": 21, "max": 37}` —— +于是一个更新的 NDK 靠**被安装**改变这个默认值,而不是靠被编辑进这个文件。 + +读不到时返回 0,由调用方转成一个点名 `min_api_level` 的拒绝。一个**猜**出来的级别 +比这个拒绝更坏:它决定哪些 bionic 符号存在,所以猜会产出一个在这里链接得上、在 +设备上加载不起来的产物。 + +同一句错误假设在 `llvm_triple()` 的注释里还有**第二份**,也一并改掉了。 + +### 一个裸 `aarch64-linux` 永不被补全成 Android + +Android 那两行与 `aarch64-linux-musl` 落在同一个 `arch-os` 前缀上,因为它的内核 +**就是** Linux —— 那正是树里每一处 Linux 形状的答案对它都成立的原因。这不使 bionic +成为一个没有命名 C 库的请求的候选:它有不同的加载器路径、不同的 SDK 和一个 API +level。 + +这件事在两行离开 `planned` 的那一刻变得可达:`aarch64-linux` 于是有了**两个**受支持 +的兄弟行并解析为 ambiguous,而在此之前它补全成 `aarch64-linux-musl`。这个二义性的 +两种结果都是错的 —— 拒绝一个有显然答案的请求,或者用 bionic 回答它。 + +也从 `siblings` 里排除,不只是从 `supported` 里:那个列表是诊断打印的东西,把 +`aarch64-linux-android` 提供给一个输入了 `aarch64-linux` 的人,是在建议他为另一个 +平台构建。一个**写出来的** `aarch64-linux-android` 永远到不了这个循环 —— 显式的 env +在上面就返回了,这条规则是「作者自己的拼写是一个请求而不是一个缺口」。 + +### effective triple 里的 API level 现在能被解析回来 + +`Target aarch64-linux-android -> aarch64-unknown-linux-android21` 是 mcpp **自己 +打印**的一行,而把它粘回去得到的是 `unknown target`:env 段的匹配写的是 +`k == "android"`,而 API level 骑在那一段上。下面 msvc 那条分支为同一个理由早就带着 +同一条注释(`…-windows-msvc19.44.35211`);Android 是同一个形状,被漏掉了。一个前缀 +匹配覆盖全部四种拼法:`android`、`android21`、`androideabi`、`androideabi21`。 + ### 一个按目标付费的机器级扫描,17948ms → 4ms `mcpp test` 在这台机器上从约 3 分钟变成投影 33 分钟,而根因不是回归而是**一直 @@ -94,9 +255,21 @@ payload emits a PE with a musl C library」—— 一句关于另一行的、本 它发不出来的目标解析了通用 llvm 载荷。判据现在是**那一行自己的钉**,也就是这一行 一直在回答的那个问题。 -`tests/e2e/640` 把五条钉住:三行各自的句子、那个「声明 llvm 也必须被拒绝」的缺口, -以及收尾那句话要点名**本行的**钉而不是一个固定的词。拿已发布的 2026.9.10.2 对照: -三条与 wasm 有关的变红,两条既有的保持绿。 +**然后同一件事发生了第二次。** Android 这一行拿到钉、成为能力钉行,而理由链仍是 +三条臂,于是它被解释成了 PE+musl 那一句 —— 与上一段记的一模一样的错答案,经由 +一模一样的路径:第四个情形落进了一个按第三个情形写的 `else`。 + +所以修法不是再加一条臂。最后那条臂现在**点名它自己那一行**(`is_pe() && is_musl()`), +而兜底句是通用的:「This row's toolchain is the only one that can emit the target at +all.」以后新增的能力行拿到的是一句**不够具体**的话,而不是一句**假**话。 + +`tests/e2e/640` 现在钉住九条:四行各自的句子(裸机、PE+musl、wasm、Android)、 +「声明 llvm 也必须被拒绝」这个缺口在 wasm 与 Android 两处、收尾那句要点名本行的钉, +以及**第六条是穷举的** —— 前面每一条都是有人想到了那一行才写下的,而两次缺陷都是 +**没人想到的那一行**掉进了兜底,任何按行写的测试都抓不到。第六条从引擎自己的词汇 +里取出每一个带钉的行,声明一个不是它的钉的工具链,并断言 PE+musl 那句话恰好出现 +在一行上。实测:34 个目标里 17 个是能力钉行,PE+musl 出现 1 次。分母取自 +`toolchain list`,所以明天新增的一行不需要编辑这个文件就已经在里面。 ### 解析行里说出**是哪个载荷**回答的 diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index 58c06dc5..03c39a64 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -776,7 +776,10 @@ and refuses. **Scope.** The contract governs the C++ runtime only. Static **libc** is a separate axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment -floor is a third (`macos_deployment_target`). Also, `host-coupled` means mcpp adds +floor is a third — `macos_deployment_target` in `[package]` for Apple targets, +`min_api_level` under `[target.]` for Android. Both feed one parameter +of `llvm_triple()` and one slot in the build fingerprint, because a target is +either Apple or Android and the two answer the same question. Also, `host-coupled` means mcpp adds nothing to embed a C++ runtime; it does not strip the toolchain rpath the link carries for other reasons, so on ELF such an artifact may still find the toolchain's libraries first. diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index ea6d1835..b1c517d3 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -50,7 +50,15 @@ the same kind of thing everywhere: | `linux` | the **C library** | `gnu` (glibc), `musl`, `android` (bionic) | | `windows` | the **object ABI** | `gnu` (Itanium C++ ABI), `msvc` (Microsoft's) | | `none` | the **object format** | `elf` | -| `macos`, `ios`, `emscripten` | nothing; the platform carries no segment | — | +| `ios` | the **device or the simulator** | `sim` (simulator), absent (device) | +| `macos`, `emscripten` | nothing; the platform carries no segment | — | + +`sim` is the one Apple row with a segment, and it names neither a C library nor +an ABI: a simulator build has its own SDK (`iPhoneSimulator.sdk`), produces its +own object, and takes `-mios-simulator-version-min` where the device takes +`-miphoneos-version-min`. Two targets, so two identities. Apple spells it with +a trailing `-simulator` on the OS segment and Rust with `aarch64-apple-ios-sim`; +both spellings parse here, and both canonicalise to `aarch64-ios-sim`. `android` is a **C library** and therefore sits where `musl` sits, on a `linux` OS. That placement is the whole of the modelling decision: the kernel *is* @@ -475,10 +483,12 @@ other's rows. | `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | payload | payload | payload | payload | | `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | payload | payload | payload | payload | | `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | payload | payload | payload | payload | -| `aarch64-linux-android` | planned | — | planned | planned | planned | planned | -| `x86_64-linux-android` | planned | — | planned | planned | planned | planned | +| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | +| `x86_64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | | `aarch64-ios` | planned | — | planned | planned | planned | planned | -| `wasm32-emscripten` | planned | — | planned | planned | planned | planned | +| `aarch64-ios-sim` | planned | — | planned | planned | planned | planned | +| `x86_64-ios-sim` | planned | — | planned | planned | planned | planned | +| `wasm32-emscripten` | verified | `emsdk@6.0.9` | payload | payload | payload | payload | `payload` a toolchain payload here produces it · `graph` no payload, but a dependency can supply the system · `system` located on the machine, not @@ -496,6 +506,7 @@ installed by mcpp · `SDK` the platform's own · `—` unreachable from this hos | `x86_64-windows-musl` | Windows by payload; anywhere by graph | no gcc emits PE+musl, and LLVM cannot spell the triple | | `aarch64-macos` | macOS | the SDK is the machine's | | `*-none-elf` | every host | clang and lld are cross-compilers by construction | +| `wasm32-emscripten`, `*-linux-android` | every host | the SDK ships its own sysroot and upstream publishes it per host; one archive serves every guest arch | **A `—` is about payloads, not about possibility.** `host_can_serve` answers "does a payload here produce it", and a dependency graph can supply the system diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index 942340a5..30c23261 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -713,7 +713,10 @@ Windows 组件(Win10 起),mcpp 从不分发它;而 `vcruntime140.dll` / 模式(`--mode system`、`--mode static`)兑现不了 `toolchain-coupled`,会直接拒绝。 **边界。** 该契约只管 C++ 运行时。静态 **libc** 是另一根轴(`linkage = "static"` -/ `--static`,如 musl 目标),部署下限是第三根轴(`macos_deployment_target`)。 +/ `--static`,如 musl 目标),部署下限是第三根轴 —— Apple 目标用 `[package]` 里的 +`macos_deployment_target`,Android 用 `[target.]` 下的 `min_api_level`。 +两者喂给 `llvm_triple()` 的同一个参数、占构建指纹里的同一个槽:一个目标要么是 +Apple 要么是 Android,而两者回答的是同一个问题。 另外,`host-coupled` 只承诺 mcpp 不做任何"把 C++ 运行时打进产物"的动作,它不会 去掉链接因其它原因已经携带的工具链 rpath —— 所以在 ELF 上这类产物仍可能优先 找到工具链的库。 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index ef7dde21..ffad24e8 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -43,7 +43,15 @@ C 库。选中 `x86_64-linux-musl` 就是选中 musl-gcc 载荷,选中 | `linux` | **C 库** | `gnu`(glibc)、`musl`、`android`(bionic) | | `windows` | **对象 ABI** | `gnu`(Itanium C++ ABI)、`msvc`(微软的) | | `none` | **对象格式** | `elf` | -| `macos`、`ios`、`emscripten` | 无;该平台不带这一段 | — | +| `ios` | **真机还是模拟器** | `sim`(模拟器)、缺省(真机) | +| `macos`、`emscripten` | 无;该平台不带这一段 | — | + +`sim` 是 Apple 这一侧唯一带段的行,而它命名的既不是 C 库也不是 ABI:模拟器构建 +有自己的 SDK(`iPhoneSimulator.sdk`)、产出自己的对象,并且取 +`-mios-simulator-version-min` 而真机取 `-miphoneos-version-min`。两个目标,所以 +两个身份。Apple 自己把它拼成 OS 段尾部的 `-simulator`,Rust 拼成 +`aarch64-apple-ios-sim`;两种拼法在这里都能解析,并且都规范化为 +`aarch64-ios-sim`。 `android` 是一个 **C 库**,所以它落在 `musl` 落的那个位置上,OS 段仍是 `linux`。 这个位置就是这处建模决定的全部:内核**就是** Linux,所以 ELF、`unix` family、 @@ -425,10 +433,12 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | -| `aarch64-linux-android` | planned | — | planned | planned | planned | planned | -| `x86_64-linux-android` | planned | — | planned | planned | planned | planned | +| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | +| `x86_64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | | `aarch64-ios` | planned | — | planned | planned | planned | planned | -| `wasm32-emscripten` | planned | — | planned | planned | planned | planned | +| `aarch64-ios-sim` | planned | — | planned | planned | planned | planned | +| `x86_64-ios-sim` | planned | — | planned | planned | planned | planned | +| `wasm32-emscripten` | verified | `emsdk@6.0.9` | payload | payload | payload | payload | `载荷` 这里有工具链载荷产出它 · `图` 没有载荷,但依赖可以供给系统 · `系统` 在机器上被找到,不是 mcpp 装的 · `SDK` 平台自己的 · @@ -445,6 +455,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | `x86_64-windows-musl` | 载荷只在 Windows;走图则任意宿主 | 没有 gcc 发得出 PE+musl,而 LLVM 拼不出这个三元组 | | `aarch64-macos` | macOS | SDK 是那台机器的 | | `*-none-elf` | 每一台 | clang 与 lld 按构造就是交叉编译器 | +| `wasm32-emscripten`、`*-linux-android` | 每个宿主 | SDK 自带 sysroot,且上游按宿主发布;一份归档服务每个 guest 架构 | **一个 `—` 讲的是载荷,不是可能性。** `host_can_serve` 回答的是「这里有没有 载荷产出它」,而依赖图可以改为供给系统 —— 这就是 `x86_64-windows-musl` 在 Linux diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index a9bf9968..48de2fab 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -175,6 +175,15 @@ struct Triple { // place that answers it. clang picks its own when nothing says. if (os == "ios") { const std::string a = (arch == "aarch64") ? "arm64" : arch; + // AND THE SIMULATOR IS A DIFFERENT EFFECTIVE TRIPLE, WHICH IS WHY + // IT IS A DIFFERENT ROW. Apple spells it with a trailing + // `-simulator` on the OS segment; mcpp carries it as `env = "sim"` + // in three fields, which is Rust's `aarch64-apple-ios-sim` modulo + // the vendor elision this whole table already does. The SDK, the + // object and the `-mios-simulator-version-min` flag all differ from + // the device's, so folding the two into one identity would be the + // mistake `x86_64-windows-musl` was added to undo. + if (env == "sim") return a + "-apple-ios-simulator"; return a + "-apple-ios"; } // ANDROID IS LINUX, AND THE ENV SEGMENT IS WHERE IT SAYS SO -- with the @@ -187,9 +196,19 @@ struct Triple { // fingerprint; and it is the project's statement rather than the // toolchain's, because one NDK serves a range of levels. // - // Absent, clang's own default applies -- `aarch64-unknown-linux-android` - // is what it normalises with no level, so omitting the key is a legal - // answer and not a gap. + // ABSENT, THERE IS NO ANSWER, AND THIS COMMENT USED TO SAY OTHERWISE. + // It read "clang's own default applies [...] so omitting the key is a + // legal answer and not a gap", which was never verified. Measured: + // + // --target=aarch64-unknown-linux-android (no level) + // sys/cdefs.h:365:2: error: Unversioned target triples are not + // supported! + // + // bionic refuses an unversioned triple outright, so the level is + // mandatory. The project still does not have to state it: mcpp reads + // the NDK's own declared floor from `meta/platforms.json` and uses that + // (see `min_platform_version` and `ndk_min_api_level`), which is the + // payload answering for itself rather than a constant compiled in. if (os == "linux") { std::string e = env.empty() ? std::string("gnu") : env; if (env == "android" && !minPlatformVersion.empty()) @@ -305,8 +324,26 @@ struct Triple { // nothing but Emscripten emits WebAssembly. A declared `gcc@16.1.0` // against such a row is a request that cannot be honoured, and saying so // is better than resolving gcc and failing inside it. + // + // ANDROID BELONGS HERE FOR A REASON THE OTHER THREE DO NOT SHOW, and it + // was left out when the row got its pin. The other three are refused + // because the toolchain CANNOT emit the format; a stock clang emits + // aarch64 ELF perfectly well, so nothing about the output rules it out. + // What it cannot supply is the SYSTEM: bionic's headers, its per-API-level + // stubs and its loader path live inside the NDK, and there is no package + // that adds them to another compiler. + // + // Measured on the path a declared `llvm@22.1.8` takes without this: + // clang resolves, and the build stops inside it with + // `'__config' file not found`, then -- once told the target -- + // `Unversioned target triples are not supported!` from bionic's own + // . Two diagnostics, neither naming the toolchain that cannot + // serve the row. `-D__BIONIC_CTYPE_INLINE=` and an API level fused onto + // the triple are the NDK's own requirements, not flags a user can be + // expected to supply to a different compiler. bool pin_is_capability() const { - return is_freestanding() || (is_pe() && is_musl()) || is_wasm(); + return is_freestanding() || (is_pe() && is_musl()) || is_wasm() + || is_android(); } // cfg() `family` dimension: unix | windows. @@ -615,11 +652,11 @@ inline constexpr TargetInfo kKnownTargets[] = { // payload exists yet -- `xim:android-ndk` is the row's whole remaining // cost, and until it lands `[target.].sysroot` is the escape hatch // for a machine that has an NDK already. - { "aarch64-linux-android", "planned", "", "", "", false }, + { "aarch64-linux-android", "preview", "", "android-ndk@30.0.16248370", "", false }, // The emulator's row. Not a convenience: x86_64 is what an Android // emulator image runs, so a row for the device without one for the // emulator describes a target nothing in CI can execute. - { "x86_64-linux-android", "planned", "", "", "", false }, + { "x86_64-linux-android", "preview", "", "android-ndk@30.0.16248370", "", false }, // iOS IS NEXT. `aarch64-macos` is `verified`, so Mach-O, `arm64`, the // linker and the Apple half of the toolchain model all exist; what is @@ -639,6 +676,25 @@ inline constexpr TargetInfo kKnownTargets[] = { // its own object, so folding it in would make two targets share an // identity -- the mistake `x86_64-windows-musl` was added to undo. { "aarch64-ios", "planned", "", "", "", false }, + // THE SIMULATOR'S TWO ROWS. Not a convenience and not a runner: a + // simulator build has its own SDK (`iPhoneSimulator.sdk`), produces its own + // object, and takes `-mios-simulator-version-min` rather than + // `-miphoneos-version-min`. Two targets sharing one identity is what the + // device row's own comment objected to, and the objection was to the + // absence of these rows rather than to their presence. + // + // BOTH ARCHES, for the reason the Android pair has both: the simulator runs + // the HOST's architecture, so an Apple-silicon machine needs `aarch64` and + // an Intel one needs `x86_64`. A single row would describe a simulator half + // the machines cannot run. + // + // `planned`, and the blocker is the same licence question as the device + // row -- the simulator SDK ships inside Xcode and is no more + // redistributable than the iPhoneOS one. What these rows buy today is that + // `mcpp build --target aarch64-ios-sim` answers `tier-planned` naming the + // row, instead of `unknown target`, which was false. + { "aarch64-ios-sim", "planned", "", "", "", false }, + { "x86_64-ios-sim", "planned", "", "", "", false }, // WEB IS THE OUTLIER, AND IT IS THE ONLY ONE OF THE THREE THAT CHANGES THE // MODEL RATHER THAN EXTENDING A TABLE. A new arch (`wasm32`), a new os @@ -760,6 +816,34 @@ inline RequestResolution resolve_request(const Triple& parsed) { && k.canonical.starts_with(prefix) && k.canonical[prefix.size()] == '-'; if (!exact && !sub) continue; + // A BARE `arch-os` ASKS FOR A C LIBRARY TO BE FILLED IN, AND ANDROID + // IS NOT ONE OF THE ANSWERS. + // + // The candidates here are meant to be alternatives for the SAME + // platform -- `gnu` or `musl` for a Linux -- so that `aarch64-linux` + // can complete to the one this repository supports. `android` sits on + // the same `arch-os` prefix because its kernel IS Linux, which is the + // modelling decision that makes every Linux-shaped answer in the tree + // right about it; it is not an alternative C library for the same + // platform. It has a different loader path, a different SDK and an API + // level. + // + // It became visible the moment the Android rows stopped being + // `planned`: `aarch64-linux` then had TWO supported siblings and + // resolved as ambiguous, where before it completed to + // `aarch64-linux-musl`. Either outcome of an ambiguity would be wrong + // here -- refusing a request that has an obvious answer, or answering + // it with bionic. + // + // Excluded from `siblings` too, not just from `supported`. That list is + // what the diagnostic prints, and offering `aarch64-linux-android` to + // someone who typed `aarch64-linux` would be a suggestion to build for + // a different platform. + // + // A written `aarch64-linux-android` never reaches this loop: an + // explicit env returns above, which is the rule that an author's own + // spelling is a request and not a gap. + if (auto kt = parse(k.canonical); kt && kt->is_android()) continue; r.siblings.push_back(k.canonical); if (k.tier != "planned") r.supported.push_back(k.canonical); } @@ -1069,7 +1153,21 @@ std::optional parse(std::string_view s) { // `androideabi` is the 32-bit ARM spelling and resolves to the same // env: the EABI half is the ARM calling convention, which `armv7a` // already carries in the arch segment. - if (k == "android" || starts_with(k, "androideabi")) { + // + // AND THE API LEVEL RIDES THIS SEGMENT, SO THE MATCH HAS TO BE A + // PREFIX. This read `k == "android"`, which cannot parse + // `aarch64-unknown-linux-android21` -- a string mcpp PRINTS + // itself, one line above the build it describes: + // + // Target aarch64-linux-android -> aarch64-unknown-linux-android21 + // + // A reader who pastes that back was told mcpp had never heard of + // it. The msvc branch below already carries the identical note for + // the identical reason ("…-windows-msvc19.44.35211"); Android has + // the same shape and was missed. One prefix covers all four + // spellings: `android`, `android21`, `androideabi`, + // `androideabi21`. + if (starts_with(k, "android")) { t.env = "android"; t.envExplicit = true; continue; } if (k == "musl" || starts_with(k, "musleabi")) { t.env = "musl"; t.envExplicit = true; continue; } @@ -1078,6 +1176,12 @@ std::optional parse(std::string_view s) { // on the env segment ("…-windows-msvc19.44.35211"). if (starts_with(k, "msvc")) { t.env = "msvc"; t.envExplicit = true; continue; } } + // THE SIMULATOR SEGMENT, WHICH IS THE ONE PLACE AN APPLE ROW HAS AN + // env. Both spellings arrive: `sim` is mcpp's and Rust's, `simulator` + // is Apple's own and appears in any effective triple clang prints. + if (t.os == "ios" && (k == "sim" || k == "simulator")) { + t.env = "sim"; t.envExplicit = true; continue; + } // Unrecognized segment (wasi, …): not in mcpp's target language — // treat as unparseable rather than guessing. return std::nullopt; @@ -1087,7 +1191,8 @@ std::optional parse(std::string_view s) { // macOS carries no env segment at all, so nothing was declined there. iOS // and Emscripten are the same shape: the platform layer is the whole of the // identity past the arch, and there is no C-library axis to decline. - if (t.os == "macos" || t.os == "ios" || t.os == "emscripten") { + if (t.os == "macos" || t.os == "emscripten" + || (t.os == "ios" && t.env != "sim")) { t.env.clear(); t.envExplicit = false; } // THE FILL STAYS, AND THE FACT THAT IT WAS A FILL IS NOW RECORDED. diff --git a/src/build/flags.cppm b/src/build/flags.cppm index e305ecab..2394eb8c 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -531,6 +531,13 @@ CompileFlags compute_flags(const BuildPlan& plan) { const std::string crossTarget = plan.toolchain.crossTargetFlag.empty() ? std::string{} : " " + plan.toolchain.crossTargetFlag; + // Does the TARGET bring its own sysroot -- an Emscripten or Android SDK, + // where the C library, the C++ runtime and the loader are all inside the + // payload? Read once here; the link branch below is its only consumer. + const bool ownSysrootTarget = [&] { + auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); + return tt && tt->has_own_sysroot(); + }(); const bool isClangWithCfg = dm.hasCfg; // THE TARGET SIDE COMES FROM THE DEPENDENCY GRAPH, READ RATHER THAN @@ -688,6 +695,32 @@ CompileFlags compute_flags(const BuildPlan& plan) { link_toolchain_flags = crossTarget + lm.link_flags(ninjaEsc); link_toolchain_flags_c = link_toolchain_flags; // nothing C++-only here f.sysroot = link_toolchain_flags; + } else if (!crossTarget.empty() && ownSysrootTarget) { + // AN SDK THAT BRINGS ITS OWN SYSROOT STILL HAS TO BE TOLD WHICH TARGET. + // + // Both branches above are skipped for such a target, and that is + // correct for everything they carry: the link model contributes + // nothing, because the C library, the C++ runtime, the crt objects and + // the loader all live inside the SDK and the driver finds them itself. + // What it cannot do is guess WHICH of them to find -- one NDK serves + // both Android arches -- so falling through with an empty string linked + // the target's objects with the host's startup files: + // + // hermetic link check failed + // /lib/x86_64-linux-gnu/Scrt1.o (outside the sandbox) + // /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o + // /lib64/ld-linux-x86-64.so.2 + // + // Six host objects on an aarch64 link, every one of them resolved by a + // driver that believed it was building for this machine. The compile + // side already said the target; only the link side did not. + // + // `crossTarget` ALONE, and that is the whole content of this branch. + // Adding the C-runtime flags the branch above adds would reintroduce + // the host's model, which is the thing the SDK replaces. + link_toolchain_flags = crossTarget; + link_toolchain_flags_c = crossTarget; + f.sysroot = link_toolchain_flags; } // Binutils -B flag — a GCC/libstdc++ payload concern (musl and MinGW-w64 diff --git a/src/build/hermetic.cppm b/src/build/hermetic.cppm index e192bfcb..43e7eb0f 100644 --- a/src/build/hermetic.cppm +++ b/src/build/hermetic.cppm @@ -28,6 +28,7 @@ import mcpp.log; import mcpp.platform; import mcpp.toolchain.fingerprint; import mcpp.toolchain.model; +import mcpp.toolchain.triple; export namespace mcpp::build { @@ -186,7 +187,31 @@ std::expected verify_hermetic_link( auto base = std::filesystem::path(std::string(t)).filename().string(); if (is_crt_object(base)) check(t); } - if (!effectiveLoader.empty()) check(effectiveLoader); + // THE LOADER OF A TARGET THAT BRINGS ITS OWN SYSROOT IS A PATH ON THE + // TARGET, NOT ON THIS MACHINE. + // + // Every other path this function inspects is resolved by the linker here + // and must therefore sit inside a payload. The dynamic linker is the one + // that is not: it is recorded in the artefact and read by the DEVICE at + // load time. Android's is `/system/bin/linker64` by ABI -- it cannot be + // inside a payload, and an artefact naming a payload path there would be + // the defect rather than the proof. + // + // Measured: with the crt objects and libc++ correctly resolving inside the + // NDK, this was the single remaining "leak" and the build stopped on it. + // The message was accurate about what it saw and wrong about what it meant, + // which is the harder kind: it named a real path outside the sandbox and + // invited the reader to reinstall a glibc payload that has nothing to do + // with it. + if (auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); + tt && tt->has_own_sysroot()) { + mcpp::log::verbose("hermetic", std::format( + "target {} carries its own sysroot; its loader ({}) is a path on " + "the target and is not checked against the sandbox", + tc.targetTriple, effectiveLoader)); + } else if (!effectiveLoader.empty()) { + check(effectiveLoader); + } if (!leaks.empty()) { std::string list; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 0ad232fd..dc0de54b 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -1569,12 +1569,30 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, // each of its call sites -- there are two, and a decision made twice is the // shape this codebase records most often. std::string min_platform_version(const mcpp::manifest::Manifest& m, - const mcpp::toolchain::triple::Triple& t) { + const mcpp::toolchain::triple::Triple& t, + const std::filesystem::path& compilerPath) { if (t.is_android()) { if (auto it = m.targetOverrides.find(t.str()); it != m.targetOverrides.end()) if (it->second.minApiLevel > 0) return std::to_string(it->second.minApiLevel); - return {}; // the NDK's own default, which clang supplies + // AND THERE IS NO SUCH THING AS LEAVING IT OUT. This returned an empty + // string with the comment "the NDK's own default, which clang + // supplies", which was never verified and is false. Measured: + // + // --target=aarch64-unknown-linux-android (no level) + // sys/cdefs.h:365:2: error: Unversioned target triples are not + // supported! + // + // bionic refuses it, so the level is mandatory and a project that + // never heard of API levels still needs one. The NDK declares the + // floor it supports in `meta/platforms.json` and that is the honest + // default -- the payload's own answer, which moves when the payload + // does. macOS is the same shape and already works this way: its + // default comes from the platform module, not from the manifest. + if (auto level = mcpp::toolchain::ndk_min_api_level(compilerPath); + level > 0) + return std::to_string(level); + return {}; // the caller refuses; see android_api_level_refusal } return mcpp::platform::macos::deployment_target( m.buildConfig.macosDeploymentTarget); @@ -2623,6 +2641,20 @@ prepare_build(bool print_fingerprint, // wasm32-emscripten` with a declared gcc was refused correctly // and explained with "No gcc payload emits a PE with a musl C // library", which is a true sentence about a different row. + // + // IT HAPPENED AGAIN, AND ADDING AN ARM IS ONLY HALF THE FIX. + // Android became a capability row and this chain still had + // three arms, so a declared `llvm@22.1.8` against + // `aarch64-linux-android` was refused correctly and explained + // with the PE+musl sentence -- the identical wrong answer the + // paragraph above records for wasm, reached the same way: by a + // fourth case falling into a final `else` that was written as + // the third case'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 one that is false, and the + // refusal still names the pin either way. std::string_view why = parsed->is_freestanding() ? "A freestanding target has no per-host cross payload: " "clang and lld are\n" @@ -2632,10 +2664,19 @@ prepare_build(bool print_fingerprint, "clang whose target,\n" " sysroot and JavaScript glue all come from its own " "payload." - : "No gcc payload emits a PE with a musl C library — the " + : parsed->is_android() + ? "An Android target needs bionic, not just an aarch64 or " + "x86_64 back end:\n" + " its headers, its per-API-level stubs and its " + "loader path are inside the\n" + " NDK, and no package adds them to another compiler." + : (parsed->is_pe() && parsed->is_musl()) + ? "No gcc payload emits a PE with a musl C library — the " "mingw payload emits\n" " PE with the MinGW CRT, which is the separate " - "`-gnu` row."; + "`-gnu` row." + : "This row's toolchain is the only one that can emit the " + "target at all."; refusal::record(refusal::Code::CapabilityPin); return std::unexpected(std::format( "target '{}' cannot be emitted by '{}'.\n" @@ -3326,6 +3367,34 @@ prepare_build(bool print_fingerprint, { tc->targetTriple = want->str(); + // AND THE GATE THAT ALREADY EXISTS FOR THIS, APPLIED WHERE THE + // ANSWER IS KNOWN. + // + // `discover_link_runtime_dirs` refuses to report these + // directories for a target that carries its own sysroot, and the + // refusal never fired: that function runs during DETECTION, + // before this line, when `targetTriple` is still the HOST's. The + // gate read a host triple and answered correctly about it. + // + // The artefact is what showed it. An Android link line carried + // + // -L /toolchains/llvm/prebuilt/linux-x86_64/lib/ + // x86_64-unknown-linux-gnu + // + // whose last component is this machine's triple, produced by + // `root / "lib" / targetTriple` -- so the string names the + // question that was asked. Those are the compiler's own host + // runtime directories; an Android artefact must resolve libc++, + // the crt objects and the loader from the NDK's sysroot, and the + // hermetic check reported exactly that failure with six host + // objects. + // + // Cleared rather than re-derived. Re-running the discovery with + // the final triple would also change what every OTHER clang cross + // target gets, and those are measured as they stand; the claim + // being made here is only the one the gate already states. + if (want->has_own_sysroot()) tc->linkRuntimeDirs.clear(); + // And the flag that says it to the driver — for a HOSTED target // only. Freestanding already emits its own `--target`, together // with the ISA flags that must accompany it @@ -3335,7 +3404,55 @@ prepare_build(bool print_fingerprint, && tc->compiler == mcpp::toolchain::CompilerId::Clang) { tc->crossTargetFlag = "--target=" + want->llvm_triple( - min_platform_version(*m, *want)); + min_platform_version(*m, *want, tc->binaryPath)); + + // AND THE SAME FLAG ON THE std MODULE'S OWN COMMANDS, FOR A + // PAYLOAD THAT SERVES MORE THAN ONE TARGET. + // + // The std module is built by its own command assembly + // (clang.cppm), not by the compile flags, so a decision made + // only here reaches every translation unit and not that. For + // most toolchains the omission cannot be seen: a payload + // whose compiler IS its target finds its own headers, and a + // package-provided module carries the target inside + // `stdModuleFlags`. + // + // ONE NDK SERVES BOTH ANDROID ARCHES, which is the property + // that makes this necessary and is stated in the row's own + // pin: `android-ndk@` names no arch, so `--target` is the + // only thing that says which. Without it the precompile + // resolved libc++'s `#include <__config>` against the + // building machine and stopped there. + // + // NOT `has_own_sysroot()`, though both rows that answer true + // to it are SDKs with their own sysroot. Emscripten's `em++` + // serves exactly one target and needs no flag -- the verified + // wasm loop is measured without it -- so widening the gate to + // the predicate would add a flag to a command that does not + // want one. The property here is "one payload, several + // targets", and Android is the only row that has it; a future + // row brings its own measurement. + if (want->is_android()) { + tc->stdModuleTargetFlags = " " + tc->crossTargetFlag; + // BIONIC'S ctype HEADER AND A MODULE'S EXPORT RULES. + // + // bionic declares `isalnum` and its neighbours + // `static inline`, and libc++'s module surface exports + // them with `using std::isalnum`. A using-declaration + // cannot export a name with internal linkage, so the + // precompile fails on 14 names at once. Defining the + // macro empty makes those declarations extern, which is + // what every other C library this engine compiles + // against already does. + // + // Scoped to the std module and not to every unit: the + // rule being satisfied is about exporting from a module, + // and a translation unit that includes + // directly is entitled to bionic's inline definitions. + // `xim:android-ndk`'s own install-time self-test reaches + // the identical conclusion from the other direction. + tc->stdModuleTargetFlags += " -D__BIONIC_CTYPE_INLINE="; + } } } if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); @@ -8753,7 +8870,7 @@ prepare_build(bool print_fingerprint, if (tc) { if (auto tt = mcpp::toolchain::triple::parse(tc->targetTriple)) { in.llvmTriple = tt->llvm_triple( - min_platform_version(*m, *tt)); + min_platform_version(*m, *tt, tc->binaryPath)); in.targetOs = tt->os; in.targetEnv = tt->env; in.freestandingTarget = tt->is_freestanding(); @@ -11062,7 +11179,8 @@ prepare_build(bool print_fingerprint, // directories. [&] { auto tt = mcpp::toolchain::triple::parse(tc->targetTriple); - return tt ? min_platform_version(*m, *tt) : std::string{}; + return tt ? min_platform_version(*m, *tt, tc->binaryPath) + : std::string{}; }(), // The GLOBAL registry root — the same one `fill_package_config` // relativizes against below, so both halves of the key describe diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index d9f833a8..95278c15 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -26,6 +26,7 @@ import mcpp.runtime.elf; import mcpp.runtime.binding; import mcpp.ui; import mcpp.platform.runtime_search; +import mcpp.toolchain.triple; export namespace mcpp::build::runtime_validation { @@ -612,6 +613,34 @@ ValidationReport validate_changed_artifacts( plan.runtimeBinding.runtimeId) != "glibc") return report; + // AND THE ARTIFACT HAS TO BE ONE THAT COULD LOAD ON THIS MACHINE. + // + // Every rule below compares an artifact against `plan.runtimeBinding` -- + // the loader, the libc and the search order of a process on THIS host. + // That premise is what makes the rules true, and it is false for a target + // whose system comes from inside an SDK: an Android executable's + // `PT_INTERP` is `/system/bin/linker64` by ABI and is read by the device. + // + // Measured on a correct artifact -- + // + // ELF 64-bit LSB pie executable, ARM aarch64, interpreter + // /system/bin/linker64 + // + // -- rule B called it a proven defect, because the host binding selects + // this machine's `ld-linux-x86-64.so.2` and "one process cannot mix + // runtime payloads" is a true sentence about a process that will never + // exist. It then offered a SubOS as the fix, which cannot help. The + // preceding two checks in this build had the same shape and each was + // corrected where its own premise lives; this is the third and last. + // + // The linux/glibc guard above does not cover it: an Android triple has + // `os == "linux"` on purpose -- it IS the kernel -- so every Linux-shaped + // decision in the tree is right about it except the ones that mean "this + // machine". + if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); + tt && tt->has_own_sysroot()) + return report; + auto doc = read_cache(plan.outputDir); bool changedCache = false; if (doc.value("schema", 0) != 1 diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 29862f91..27b5515b 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -252,6 +252,35 @@ std::vector std_module_build_commands(const Toolchain& tc, // headers contributed; only the machine has to be restated. See // Toolchain::stdModuleTargetFlags. const std::string& codegenFlags = tc.stdModuleTargetFlags; + // AND THE PRECOMPILE NEEDS THE MACHINE TOO, FROM WHICHEVER SOURCE HAS IT. + // + // `stdModuleTargetFlags` reached only the CODEGEN command, on the reading + // that the first step needs headers and the second needs the machine. The + // first step needs both: a `--precompile` that does not say which target + // resolves the standard library's own `#include <__config>` against the + // BUILDING machine. + // + // It was invisible while exactly two kinds of toolchain existed. A payload + // whose compiler IS its target needs no flag, and a PACKAGE-provided module + // carries the target inside `stdModuleFlags` -- which is why the comment + // above insists that whoever sets that string states the target as well. A + // payload whose compiler serves several targets is a third kind, and it has + // neither: the NDK's one clang++ compiles for both Android arches and is + // told which by `--target` alone. Measured on `aarch64-linux-android`: + // + // clang++ -std=c++23 -Wno-reserved-module-identifier \ + // --precompile .../share/libc++/v1/std.cppm -o pcm.cache/std.pcm + // std.cppm:16:10: fatal error: '__config' file not found + // + // Five tokens, and the same error text this file already records from a + // Windows host in 2026-08 -- same cause, reached by a different route. + // + // The two sources are never both needed: `stdModuleFlags` is a SUPERSET of + // `stdModuleTargetFlags` when it is set at all (the producer builds the + // machine part first and appends the include part), so taking it in + // preference keeps `--target` off the command line twice. + const std::string& precompileFlags = + extraFlags.empty() ? codegenFlags : extraFlags; #if defined(_WIN32) // Windows: use absolute paths, raw binary path as first token // (cmd.exe strips leading quotes), shq for args with spaces. @@ -308,7 +337,7 @@ std::vector std_module_build_commands(const Toolchain& tc, cppStandardFlag, ixxFlags, sysrootFlag, - extraFlags, + precompileFlags, mcpp::xlings::shq(tc.stdModuleSource.string()), mcpp::xlings::shq(absBmi)), std::format( @@ -331,7 +360,7 @@ std::vector std_module_build_commands(const Toolchain& tc, mcpp::xlings::shq(tc.binaryPath.string()), cppStandardFlag, sysrootFlag, - extraFlags, + precompileFlags, mcpp::xlings::shq(tc.stdModuleSource.string()), mcpp::xlings::shq(relBmi)), std::format( @@ -418,6 +447,9 @@ std::vector std_compat_build_commands(const Toolchain& tc, // Same split as the `std` builder above: the second command compiles a BMI // and needs the machine restated, not the include paths. const std::string& codegenFlags = tc.stdModuleTargetFlags; + // Same third kind of toolchain as the `std` builder above, same reason. + const std::string& precompileFlags = + extraFlags.empty() ? codegenFlags : extraFlags; // std.compat depends on std, so we need -fmodule-file=std= // Note: the path after = must NOT be shell-quoted separately; the // entire -fmodule-file flag is a single token to the compiler. @@ -453,7 +485,7 @@ std::vector std_compat_build_commands(const Toolchain& tc, mcpp::xlings::shq(tc.binaryPath.string()), cppStandardFlag, sysrootFlag, - extraFlags, + precompileFlags, absStdBmi, mcpp::xlings::shq(tc.stdCompatSource.string()), mcpp::xlings::shq(absBmi)), diff --git a/src/toolchain/hostflags.cppm b/src/toolchain/hostflags.cppm index 6bb6e956..2c680195 100644 --- a/src/toolchain/hostflags.cppm +++ b/src/toolchain/hostflags.cppm @@ -210,8 +210,30 @@ std::vector host_compile_tokens(const Toolchain& tc, // clang reading a per-install `clang++.cfg`, while `em++` is a wrapper // whose entire job is to supply configuration. Suppressing it would be // suppressing the toolchain. - if (auto tt = triple::parse(tc.targetTriple); tt && tt->has_own_sysroot()) + // + // "NOTHING" WAS ONE TOKEN TOO STRONG, AND THIS FUNCTION ALREADY SAID SO + // FURTHER DOWN. The paragraph beginning "THE TRIPLE, SAID OUT LOUD" states + // the opposite rule for the same reason -- an ordinary clang emits for the + // machine it is running on unless told otherwise -- and this early return + // stood in front of it, so the stronger claim won by position. + // + // Both are right about their own object. The SYSTEM is the payload's and + // must not be reconstructed; WHICH TARGET is still mcpp's to say, because + // one NDK serves both Android arches and nothing on the command line + // otherwise distinguishes them. Measured on `aarch64-linux-android`, with + // the std module already correct: + // + // error: AST file 'std.pcm' was compiled for the target + // 'aarch64-unknown-linux-android21' but the current translation unit + // is being compiled for target 'x86_64-unknown-linux-gnu' + // + // Two machines in one build, reported by the module loader rather than by + // either compile -- and then eight cascading "use of undeclared identifier + // 'std'" errors, which is what a reader sees first. + if (auto tt = triple::parse(tc.targetTriple); tt && tt->has_own_sysroot()) { + if (!tc.crossTargetFlag.empty()) out.push_back(tc.crossTargetFlag); return out; + } // MSVC carries none of this on the command line: cl.exe and link.exe find // headers and import libraries through INCLUDE / LIB, which detection diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 96b646c5..eb36fa4b 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -17,6 +17,7 @@ export module mcpp.toolchain.registry; import std; +import mcpp.libs.json; import mcpp.platform; import mcpp.xlings; import mcpp.toolchain.clang; @@ -281,6 +282,11 @@ bool needs_linux_sysroot_payloads(const triple::Triple& target); // that the availability side declared impossible). bool host_can_serve(const triple::Triple& target); +// The NDK's own declared minimum API level, read from the installed payload's +// `meta/platforms.json`. 0 when it cannot be read. Definition and the reason +// the number is not a constant are below. +int ndk_min_api_level(const std::filesystem::path& compilerPath); + // xim index names to query for the Available section, with the family each // one contributes versions to. Host-conditional: a host only lists payloads // it can install. @@ -457,6 +463,53 @@ std::string ndk_host_tag() { else return "linux-x86_64"; } +// THE NDK'S OWN MINIMUM API LEVEL, READ FROM THE PAYLOAD. +// +// Android's API level is NOT OPTIONAL and mcpp cannot leave it out. bionic's +// own stops the build: +// +// sys/cdefs.h:365:2: error: Unversioned target triples are not supported! +// +// So a project that declares no `min_api_level` still needs a level, and the +// question is where the number comes from. Not from a constant compiled in +// here: this repository has recorded more than once that a version written +// into a comment becomes a version written into a diagnostic and then into +// somebody's install command, and the NDK's floor moves with the NDK. The +// payload answers for itself -- `meta/platforms.json` is upstream's own +// declaration of the range it supports, `{"min": 21, "max": 37}` for r30 -- +// and reading it means a newer NDK changes the default by being installed +// rather than by being edited into this file. +// +// Returns 0 when the file is absent or unreadable, which the caller turns into +// a refusal naming `min_api_level`. A guessed level would be worse than the +// refusal: it selects which bionic symbols exist, so guessing produces an +// artefact that links here and fails to load on a device. +int ndk_min_api_level(const std::filesystem::path& compilerPath) { + // `/toolchains/llvm/prebuilt//bin/clang++` -- walk up rather + // than counting components, because the count is exactly the kind of fact + // that changes silently when a layout does. + std::error_code ec; + for (auto dir = compilerPath.parent_path(); + !dir.empty() && dir != dir.parent_path(); + dir = dir.parent_path()) { + auto meta = dir / "meta" / "platforms.json"; + if (!std::filesystem::exists(meta, ec)) continue; + std::ifstream in(meta); + if (!in) return 0; + try { + auto j = nlohmann::json::parse(in, nullptr, false); + if (j.is_discarded() || !j.contains("min")) return 0; + auto min = j["min"]; + if (!min.is_number_integer()) return 0; + auto level = min.get(); + return level > 0 ? level : 0; + } catch (...) { + return 0; + } + } + return 0; +} + XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { XimToolchainPackage pkg; pkg.displaySpec = spec.display(); @@ -742,19 +795,36 @@ bool host_can_serve(const triple::Triple& target) { // a Windows form, so control reached a `return false` written for triples // nobody publishes a payload for. // - // BUT "WHEREVER" WAS TOO BROAD, AND THE TARGET MATRIX IS WHAT CAUGHT IT. - // The first version of this returned true unconditionally, which is the - // same mistake as the branches it sits above: a predicate correct about - // the objects its author had in mind. `xim:emsdk` and `xim:android-ndk` - // both declare ONLY an `xpm.linux` table today, so on macOS or Windows - // there is no payload to install and the honest answer is the same - // `host-cannot-serve` every other unpublished combination gets. + // "WHEREVER" WAS TOO BROAD ONCE, AND THE TARGET MATRIX IS WHAT CAUGHT IT. + // The first version returned true unconditionally, which is the same + // mistake as the branches it sits above: a predicate correct about the + // objects its author had in mind. It was then narrowed to + // `mcpp::platform::is_linux`, because `xim:emsdk` and `xim:android-ndk` + // both declared ONLY an `xpm.linux` table -- so on macOS or Windows there + // was no payload to install and the honest answer was the same + // `host-cannot-serve` every other unpublished combination gets. That + // comment named its own expiry: "when a darwin or windows NDK lands in the + // index -- upstream publishes both -- this is the one line that changes." + // + // IT HAS LANDED, SO THIS IS THAT LINE. Both packages now declare + // `xpm.linux`, `xpm.macosx` and `xpm.windows`, and the index's own + // per-host install jobs are the measurement rather than the declaration: + // on macOS and Windows each payload downloads, extracts, passes its + // recipe's compiler probe and registers its shims. Two host assumptions + // inside those recipes were found by exactly those jobs and fixed there, + // which is where a host-shaped packaging defect belongs -- not here. // - // Keyed on the HOST and not on the arch, because these payloads are - // published per host OS and carry every guest arch. When a darwin or - // windows NDK lands in the index -- upstream publishes both -- this is the - // one line that changes. - if (target.has_own_sysroot()) return mcpp::platform::is_linux; + // Keyed on the target and no longer on the host, because these payloads + // are published per host OS and carry every guest arch: one `xim:emsdk` + // compiles for wasm32 regardless of the machine's arch, and one NDK serves + // both Android arches. The remaining per-host question is whether the + // payload EXISTS, and that is the index's answer to give, not a constant + // compiled into the engine. A row whose pin the index cannot satisfy on + // this host fails at install with the package's own diagnostic, which + // names the payload -- strictly better than this function silently + // deleting the row from `toolchain list`, which reported a target mcpp + // knows as one it has never heard of. + if (target.has_own_sysroot()) return true; if (target.os == "linux") { if constexpr (mcpp::platform::is_linux) { diff --git a/tests/e2e/640_a_capability_pin_explains_its_own_row.sh b/tests/e2e/640_a_capability_pin_explains_its_own_row.sh index 435bbec8..fe44bf1e 100755 --- a/tests/e2e/640_a_capability_pin_explains_its_own_row.sh +++ b/tests/e2e/640_a_capability_pin_explains_its_own_row.sh @@ -55,9 +55,21 @@ check wasm32-emscripten gcc@16.1.0 "Nothing but Emscripten emits WebAssembly" check riscv64-none-elf gcc@16.1.0 "no per-host cross payload" "bare metal names the cross payload" check x86_64-windows-musl gcc@16.1.0 "PE with a musl C library" "PE+musl names the C library" +# 3b. AND IT HAPPENED A SECOND TIME, with Android. The row gained a pin, became +# a capability row, and the reason chain still had three arms -- so the +# refusal explained it with the PE+musl sentence, the identical wrong answer +# case 1 was written for. +check aarch64-linux-android gcc@16.1.0 "An Android target needs bionic" "android names bionic" +check x86_64-linux-android gcc@16.1.0 "An Android target needs bionic" "android names bionic (x86_64)" + # 4. THE GATE. `llvm@22.1.8` is the llvm family, and so is emsdk -- so a family # test cannot separate them and this declaration used to pass unrefused. check wasm32-emscripten llvm@22.1.8 "Nothing but Emscripten emits WebAssembly" "a declared llvm is refused too" +# The NDK normalises to the llvm family for the same reason, so the same hole +# would have existed for Android. A declared `llvm@22.1.8` names a real +# compiler that emits aarch64 ELF perfectly well -- what it cannot supply is +# bionic, which is why this row is a capability at all. +check aarch64-linux-android llvm@22.1.8 "An Android target needs bionic" "a declared llvm is refused for android too" # 5. And the sentence names the row's OWN pin rather than a fixed word: the # closing line used to read "The row names llvm as a capability" on every @@ -74,5 +86,47 @@ else fail=1 fi +# 6. EXHAUSTIVE, BECAUSE ADDING AN ARM IS WHAT KEEPS FAILING. +# +# Cases 1-5 each name a row a reader thought of. The defect both times was a +# row NOBODY thought of falling into a final `else` written as another row's +# answer, and no per-row test can catch that. So: take every pinned row from +# the engine's own vocabulary, declare a toolchain that is not its pin, and +# assert the PE+musl sentence appears for exactly one of them. +# +# The denominator comes from `toolchain list`, so a row added tomorrow is in it +# without this file being edited. +echo "== 640/6: the PE+musl sentence belongs to exactly one row ==" +pinned=$( "$MCPP" toolchain list --format json 2>/dev/null \ + | tr ',' '\n' | grep -o '"target": *"[^"]*"' | sed 's/.*: *"//;s/"//' ) +[ -n "$pinned" ] || { echo "FAIL: toolchain list named no targets"; exit 1; } +peMusl=0; examined=0 +for target in $pinned; do + d="$t/x-$target"; mkdir -p "$d/src" + printf '[package]\nname = "c"\nversion = "0.1.0"\n\n[toolchain]\nlinux = "gcc@16.1.0"\n' > "$d/mcpp.toml" + printf 'int main(){return 0;}\n' > "$d/src/main.cpp" + out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target "$target" 2>&1 ) || true + grep -q "cannot be emitted by" <<<"$out" || continue # not a capability row + examined=$((examined + 1)) + if grep -qF "PE with a musl C library" <<<"$out"; then + peMusl=$((peMusl + 1)) + if [ "$target" != "x86_64-windows-musl" ]; then + echo "FAIL: $target is explained with the PE+musl sentence" + fail=1 + fi + fi +done +echo " examined $examined capability-pinned rows of $(wc -w <<<"$pinned") targets" +if [ "$examined" -lt 4 ]; then + echo "FAIL: only $examined capability rows were reached — the enumeration is too small to be evidence" + fail=1 +fi +if [ "$peMusl" -ne 1 ]; then + echo "FAIL: the PE+musl sentence was printed for $peMusl rows, expected exactly 1" + fail=1 +else + echo " ok: exactly one row is explained by the PE+musl sentence" +fi + if [ "$fail" -ne 0 ]; then echo "FAIL: 640"; exit 1; fi echo "PASS: 640" diff --git a/tests/e2e/641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh b/tests/e2e/641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh new file mode 100755 index 00000000..9f8801a4 --- /dev/null +++ b/tests/e2e/641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# requires: gcc +# 641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh — the vocabulary +# half of the Android and iOS work, which is the half a runner without a 704 MB +# NDK can still assert. Nothing here installs a payload. +# +# WHAT THIS FILE DELIBERATELY DOES NOT ASSERT: that an Android artefact builds. +# That was measured by hand (linux-x86_64, xim:android-ndk 30.0.16248370, a +# source that imports std and 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 +# +# and it is what the rows' `preview` tier records. An e2e that needed the +# payload would skip on every shard, which this repository has already paid for +# once: `# requires: llvm` tests ran on no CI job at all while reporting green. +set -e + +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail=0 + +pkg() { # dir [extra manifest lines...] + local d=$1; shift + mkdir -p "$d/src" + printf '[package]\nname = "c"\nversion = "0.1.0"\n' > "$d/mcpp.toml" + for line in "$@"; do printf '%s\n' "$line" >> "$d/mcpp.toml"; done + printf 'int main(){return 0;}\n' > "$d/src/main.cpp" +} + +echo "== 641: the rows are wired, and a spelling that exists is not 'unknown' ==" + +# 1. BOTH ANDROID ROWS NAME THEIR PAYLOAD. A row whose tier moved without a pin +# is reachable only through an explicit `[target.X] toolchain` override, +# which is the escape hatch rather than the support claim. +list=$( "$MCPP" toolchain list --format json 2>/dev/null ) +for target in aarch64-linux-android x86_64-linux-android; do + if grep -q "\"target\": *\"$target\"" <<<"$list" \ + && tr ',' '\n' <<<"$list" | grep -A6 "\"target\": *\"$target\"" \ + | grep -q "android-ndk"; then + echo " ok: $target names the NDK payload" + else + echo "FAIL: $target does not name android-ndk in toolchain list" + tr ',' '\n' <<<"$list" | grep -A6 "\"target\": *\"$target\"" | sed 's/^/ /' + fail=1 + fi +done + +# 2. AND THEY ARE NO LONGER `planned`. This is the property the user's question +# was about, and it is separate from (1): a row can carry a pin and still be +# refused by the tier gate. +for target in aarch64-linux-android x86_64-linux-android; do + d="$t/tier-$target"; pkg "$d" + out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target "$target" 2>&1 ) || true + if grep -q "not yet supported (planned)" <<<"$out"; then + echo "FAIL: $target is still refused as planned" + fail=1 + else + echo " ok: $target is not refused for its tier" + fi +done + +# 3. THE SIMULATOR IS A ROW, so its spelling resolves. Before it existed, +# `--target aarch64-ios-sim` answered `unknown target`, which was false: the +# vocabulary has the device row and the simulator is a different target, not +# an unspellable one. +for target in aarch64-ios-sim x86_64-ios-sim; do + d="$t/sim-$target"; pkg "$d" + out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target "$target" 2>&1 ) || true + if grep -q "unknown target" <<<"$out"; then + echo "FAIL: $target reported as unknown; it is a registered row" + fail=1 + elif grep -q "not yet supported (planned)" <<<"$out"; then + echo " ok: $target says planned, naming the row" + else + echo "FAIL: $target refused for neither reason" + grep -m2 -E "^error" <<<"$out" | sed 's/^/ /' + fail=1 + fi +done + +# 4. AN EFFECTIVE TRIPLE mcpp PRINTS ITSELF MUST PARSE BACK. +# `Target aarch64-linux-android -> aarch64-unknown-linux-android21` is a +# line mcpp writes; pasting it back used to answer `unknown target`, because +# the env match read `k == "android"` and the API level rides that segment. +d="$t/effective"; pkg "$d" +out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target aarch64-unknown-linux-android21 2>&1 ) || true +if grep -q "unknown target" <<<"$out"; then + echo "FAIL: the effective triple mcpp prints does not parse back" + grep -m2 -E "^error" <<<"$out" | sed 's/^/ /' + fail=1 +else + echo " ok: aarch64-unknown-linux-android21 parses to the canonical row" +fi + +# 5. A BARE `aarch64-linux` IS NEVER COMPLETED TO ANDROID. The rows sit on the +# same `arch-os` prefix because the kernel IS Linux, and that is the whole +# reason every Linux-shaped answer in the tree is right about them. It does +# not make bionic a candidate C library for a request that named none. +d="$t/bare"; pkg "$d" +out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target aarch64-linux 2>&1 ) || true +if grep -q "android" <<<"$out"; then + echo "FAIL: a bare aarch64-linux request mentioned android" + grep -m4 -E "android" <<<"$out" | sed 's/^/ /' + fail=1 +else + echo " ok: a bare aarch64-linux request never mentions android" +fi + +# 6. `min_api_level` IS A MANIFEST KEY WITH A FLOOR, and it is refused where a +# reader can act on it. Manifest-level, so no payload is needed: the check +# runs before any toolchain is resolved. +d="$t/apilevel"; pkg "$d" "" "[target.aarch64-linux-android]" "min_api_level = 0" +out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target aarch64-linux-android 2>&1 ) || true +if grep -q "min_api_level must be a positive integer" <<<"$out"; then + echo " ok: a non-positive min_api_level is refused naming the key" +else + echo "FAIL: min_api_level = 0 was not refused with its own message" + grep -m3 -E "^error" <<<"$out" | sed 's/^/ /' + fail=1 +fi + +# And a legal one is accepted as far as the manifest is concerned -- the build +# may then fail for want of a payload, which is a different sentence. +d="$t/apilevel-ok"; pkg "$d" "" "[target.aarch64-linux-android]" "min_api_level = 24" +out=$( cd "$d" && MCPP_NO_AUTO_INSTALL=1 "$MCPP" build --target aarch64-linux-android 2>&1 ) || true +if grep -q "min_api_level must be" <<<"$out"; then + echo "FAIL: a legal min_api_level = 24 was refused by the manifest" + fail=1 +else + echo " ok: min_api_level = 24 is accepted by the manifest" +fi + +if [ "$fail" -ne 0 ]; then echo "FAIL: 641"; exit 1; fi +echo "PASS: 641" diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 9d3a3db3..66af3ddb 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -238,51 +238,75 @@ payload windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported cap # aarch64-ios iPhoneOS SDK(先要一次许可判断) # wasm32-emscripten xim:emsdk,以及 #597 的目标模型 # -graph linux-aarch64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported tier-planned -payload linux-aarch64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -payload macos-arm64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported tier-planned -graph linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported tier-planned -payload linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -payload macos-arm64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported tier-planned +graph linux-aarch64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +graph macos-arm64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +graph windows-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +graph windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported capability-pin +payload linux-aarch64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +payload macos-arm64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +payload windows-x86_64 aarch64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +payload windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported capability-pin +graph linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +graph macos-arm64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +graph windows-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +graph windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported capability-pin +payload linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +payload macos-arm64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-linux-android llvm@22.1.8 - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported capability-pin graph linux-aarch64 aarch64-ios gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 aarch64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned graph linux-x86_64 aarch64-ios gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned graph linux-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned graph macos-arm64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 aarch64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned graph windows-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned graph windows-x86_64 aarch64-ios msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-ios-sim msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-ios-sim msvc@system - - - - - unsupported tier-planned payload linux-aarch64 aarch64-ios gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-aarch64 aarch64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-aarch64 x86_64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 aarch64-ios gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-x86_64 aarch64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-ios-sim gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 aarch64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned +payload macos-arm64 aarch64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned +payload macos-arm64 x86_64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 aarch64-ios llvm@22.1.8 - - - - - unsupported tier-planned +payload windows-x86_64 aarch64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-ios-sim llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 aarch64-ios msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 aarch64-ios-sim msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-ios-sim msvc@system - - - - - unsupported tier-planned graph linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin graph linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin graph linux-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin -graph macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve -graph windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve -graph windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported host-cannot-serve +graph macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin +graph windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin +graph windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported capability-pin payload linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin -payload macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve -payload windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported host-cannot-serve -payload windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported host-cannot-serve +payload macos-arm64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin +payload windows-x86_64 wasm32-emscripten llvm@22.1.8 - - - - - unsupported capability-pin +payload windows-x86_64 wasm32-emscripten msvc@system - - - - - unsupported capability-pin diff --git a/tests/unit/test_toolchain_registry.cpp b/tests/unit/test_toolchain_registry.cpp index b1c3e206..512766d0 100644 --- a/tests/unit/test_toolchain_registry.cpp +++ b/tests/unit/test_toolchain_registry.cpp @@ -235,6 +235,68 @@ TEST(ToolchainRegistry, NativeGccPayloadFollowsWhatTheArchActuallyPublishes) { // ONE platform — Visual Studio is very often already installed and cannot // always be redistributed. +// THE NDK'S OWN FLOOR, READ FROM THE PAYLOAD RATHER THAN COMPILED IN. +// +// Android's API level is not optional -- bionic's stops the build +// with "Unversioned target triples are not supported!" -- so a project that +// declares no `min_api_level` still needs one. The number comes from +// `meta/platforms.json`, which is upstream's own declaration of the range it +// supports, so a newer NDK changes the default by being installed. +// +// A CONSTANT HERE WOULD BE THE DEFECT THIS AVOIDS: this repository has +// recorded more than once that a version written into a comment becomes a +// version in a diagnostic and then in somebody's install command. +TEST(ToolchainRegistry, TheNdkApiLevelFloorIsReadFromThePayloadsOwnMetadata) { + namespace fs = std::filesystem; + auto root = fs::temp_directory_path() + / ("mcpp-ndk-meta-" + std::to_string(::getpid())); + fs::remove_all(root); + // The real layout: the compiler sits four directories below the NDK root, + // and `meta/` is a sibling of `toolchains/`. + auto bin = root / "toolchains" / "llvm" / "prebuilt" / "linux-x86_64" / "bin"; + fs::create_directories(bin); + fs::create_directories(root / "meta"); + auto clangxx = bin / "clang++"; + { std::ofstream o(clangxx); o << "#!/bin/sh\n"; } + + // r30's actual values. + { + std::ofstream o(root / "meta" / "platforms.json"); + o << R"({"min": 21, "max": 37, "aliases": {"N": 24}})"; + } + EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 21); + + // A DIFFERENT PAYLOAD ANSWERS DIFFERENTLY, which is the whole point of + // reading it: the same code must not return 21 for an NDK that says 24. + { + std::ofstream o(root / "meta" / "platforms.json"); + o << R"({"min": 24, "max": 40})"; + } + EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 24); + + // 0 WHEN IT CANNOT BE READ, and the caller turns that into a refusal + // naming `min_api_level`. A guessed level would be worse than the refusal: + // it selects which bionic symbols exist, so guessing produces an artefact + // that links here and fails to load on a device. + fs::remove(root / "meta" / "platforms.json"); + EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 0); + + // Malformed rather than absent -- same answer, and no exception escapes. + { std::ofstream o(root / "meta" / "platforms.json"); o << "{not json"; } + EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 0); + + // Present but not a number: still 0, never a silent 1 from a cast. + { std::ofstream o(root / "meta" / "platforms.json"); o << R"({"min": "21"})"; } + EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 0); + + // A path that is not inside an NDK at all walks to the filesystem root and + // stops; it must not loop. + EXPECT_EQ(mcpp::toolchain::ndk_min_api_level( + fs::temp_directory_path() / "definitely-not-an-ndk" / "clang++"), 0); + + fs::remove_all(root); +} + TEST(ToolchainOrigin, MsvcIsTheOnlyFamilyWithASystemSpelling) { auto msvcSystem = parse_toolchain_spec("msvc@system"); ASSERT_TRUE(msvcSystem.has_value()) << msvcSystem.error(); @@ -365,13 +427,34 @@ TEST(SdkPayloads, TheSearchedDirectoryIsAvailableForTheDiagnostic) { // said "wherever" -- the same over-broad shape as the branches it sits above. // The target matrix caught it: declaring the row servable on macOS and Windows // would have claimed a payload that does not exist there. -TEST(SdkPayloads, ServedOnTheHostsTheSdkIsPublishedFor) { - auto wasm = mcpp::toolchain::triple::parse("wasm32-emscripten"); - ASSERT_TRUE(wasm.has_value()); - EXPECT_EQ(mcpp::toolchain::host_can_serve(*wasm), mcpp::platform::is_linux); - auto droid = mcpp::toolchain::triple::parse("aarch64-linux-android"); - ASSERT_TRUE(droid.has_value()); - EXPECT_EQ(mcpp::toolchain::host_can_serve(*droid), mcpp::platform::is_linux); +TEST(SdkPayloads, ServedOnEveryHostTheSdkIsPublishedFor) { + // THIS ASSERTION USED TO BE TRUE BY ARITHMETIC ON ONE HOST. + // + // It read `EXPECT_EQ(host_can_serve(*wasm), mcpp::platform::is_linux)`, + // which was the right claim while `xim:emsdk` and `xim:android-ndk` + // declared only `xpm.linux`. Both now publish for all three hosts, and the + // engine's constant was the stale half -- but the assertion kept passing + // 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. + // + // Stated unconditionally now: these rows are servable everywhere, and this + // test fails on macOS or Windows if the constant comes back. + for (auto name : {"wasm32-emscripten", "aarch64-linux-android", + "x86_64-linux-android"}) { + auto t = mcpp::toolchain::triple::parse(name); + ASSERT_TRUE(t.has_value()) << name; + EXPECT_TRUE(mcpp::toolchain::host_can_serve(*t)) << name; + } + + // AND THE PREDICATE IS STILL ABLE TO SAY NO, which is what keeps the + // paragraph above from being a tautology. macOS's SDK and MSVC are + // host-only and no package substitutes for either, so a Linux host cannot + // serve them -- the exclusion this function exists to make. + if constexpr (mcpp::platform::is_linux) { + auto mac = mcpp::toolchain::triple::parse("aarch64-macos"); + ASSERT_TRUE(mac.has_value()); + EXPECT_FALSE(mcpp::toolchain::host_can_serve(*mac)); + } } // ─── The payload is SAID, not only resolved (R3) ─────────────────────────── diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 97c9c61b..cdd466d8 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -98,6 +98,40 @@ TEST(TripleRequest, TheOnlySupportedSiblingIsTaken) { EXPECT_FALSE(r.triple.envExplicit); } +TEST(TripleRequest, ABareLinuxTripleIsNeverCompletedToAndroid) { + // `aarch64-linux` asks for a C LIBRARY to be filled in. `android` shares + // the `arch-os` prefix because its kernel is Linux -- the modelling + // decision that makes every Linux-shaped answer in the tree right about it + // -- and it is not an alternative C library for the same platform. + // + // This became reachable when the Android rows stopped being `planned`: + // `aarch64-linux` then had two supported siblings and resolved as + // ambiguous, where it had completed to `aarch64-linux-musl` before. Both + // outcomes of that ambiguity are wrong -- refusing a request with an + // obvious answer, or answering it with bionic. + auto r = triple::resolve_request(*parse("aarch64-linux")); + EXPECT_EQ(r.triple.str(), "aarch64-linux-musl"); + EXPECT_TRUE(r.completedFromVocabulary); + EXPECT_FALSE(r.ambiguous); + // Not offered as a suggestion either: `siblings` is what the diagnostic + // prints, and naming it there would suggest building for another platform. + for (auto& s : r.siblings) + EXPECT_EQ(s.find("android"), std::string::npos) << s; + + // x86_64 has the same shape and a different supported set, so it exercises + // the exclusion independently rather than re-testing one row. + auto x = triple::resolve_request(*parse("x86_64-linux")); + for (auto& s : x.siblings) + EXPECT_EQ(s.find("android"), std::string::npos) << s; + + // AND THE WRITTEN SPELLING IS STILL HONOURED. The exclusion is about + // filling a gap, not about refusing a request -- an explicit env returns + // before the candidate loop runs. + auto explicitAndroid = triple::resolve_request(*parse("aarch64-linux-android")); + EXPECT_EQ(explicitAndroid.triple.str(), "aarch64-linux-android"); + EXPECT_FALSE(explicitAndroid.completedFromVocabulary); +} + TEST(TripleRequest, AWrittenSegmentIsARequestAndIsNotRevised) { // The escape hatch: writing the segment opts into the `planned` row, and the // tier gate then refuses something the user actually typed. @@ -628,12 +662,20 @@ TEST(Triple, EmscriptenIsAnOsSegmentAndNotAnEnv) { EXPECT_FALSE(t->nasm_format().has_value()); } -TEST(Triple, TheWasmRowIsWiredAndTheOtherThreeAreStillPlanned) { - // WHAT A TIER ASSERTS. `verified` in this table means an artefact was - // built AND RUN, and `wasm32-emscripten` now is: measured 2026-09-11 on - // linux-x86_64 with xim:emsdk 6.0.9, `mcpp run --target wasm32-emscripten` - // on a source that imports std printed `1-2-3`. +TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { + // WHAT A TIER ASSERTS, AND THE THREE ANSWERS ARE DIFFERENT CLAIMS. + // + // verified an artefact was built AND RUN + // preview an artefact was built; nothing has executed it + // planned the vocabulary exists and nothing is wired + // + // A tier that moved on expectation would be the one thing this column + // cannot be, so each row below names the measurement behind it. + { + // Measured 2026-09-11 on linux-x86_64 with xim:emsdk 6.0.9: + // `mcpp run --target wasm32-emscripten` on a source that imports std + // printed `1-2-3`. Built and run, so `verified`. auto t = parse("wasm32-emscripten"); ASSERT_TRUE(t.has_value()); auto* info = find_known_target(*t); @@ -649,11 +691,40 @@ TEST(Triple, TheWasmRowIsWiredAndTheOtherThreeAreStillPlanned) { EXPECT_TRUE(info->sysroot.empty()); } - // The other three stay `planned`, with no pin and no sysroot, because what - // each still needs is execution evidence rather than vocabulary. A tier - // that moved on expectation would be the one thing this column cannot be. - for (auto name : {"aarch64-linux-android", "x86_64-linux-android", - "aarch64-ios"}) { + // BOTH ANDROID ROWS ARE NOW WIRED, AND `preview` IS THE HONEST TIER. + // + // Measured 2026-09-11 on linux-x86_64 with xim:android-ndk + // 30.0.16248370, from a source that imports std and 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 + // + // Neither has been EXECUTED, which is exactly the difference between this + // tier and the wasm row's: running one needs a device or an emulator. + // + // ONE PIN SERVES BOTH ROWS, which is the property the whole Android path + // rests on: the NDK names no arch, `--target` does, and that is why the + // std module's own precompile had to be told the target as well. + for (auto name : {"aarch64-linux-android", "x86_64-linux-android"}) { + auto t = parse(name); + ASSERT_TRUE(t.has_value()) << name; + EXPECT_EQ(t->str(), name); + auto* info = find_known_target(*t); + ASSERT_NE(info, nullptr) << name; + EXPECT_EQ(info->tier, "preview") << name; + EXPECT_EQ(info->pin, "android-ndk@30.0.16248370") << name; + // Same statement the wasm row makes: the SDK ships the sysroot. + EXPECT_TRUE(info->sysroot.empty()) << name; + } + + // THE THREE APPLE ROWS STAY `planned`, AND THE BLOCKER IS NOT A PAYLOAD. + // The NDK is Apache-2.0 and Emscripten is MIT; the iPhoneOS and + // iPhoneSimulator SDKs ship inside Xcode and are neither. No amount of + // engine work moves these, which is why they carry no pin: there is + // nothing for a pin to name. + for (auto name : {"aarch64-ios", "aarch64-ios-sim", "x86_64-ios-sim"}) { auto t = parse(name); ASSERT_TRUE(t.has_value()) << name; EXPECT_EQ(t->str(), name); @@ -665,6 +736,120 @@ TEST(Triple, TheWasmRowIsWiredAndTheOtherThreeAreStillPlanned) { } } +// A CAPABILITY PIN CANNOT BE OVERRIDDEN; A CONVENTION PIN CAN. +// +// Asserted exhaustively over the table rather than on examples, because the +// failure this guards against is a row JOINING the set without its refusal +// sentence being written. That has now happened twice -- wasm, then Android -- +// and each time the refusal explained a different row: "No gcc payload emits a +// PE with a musl C library", printed for a wasm target and then for an Android +// one, because a fourth case fell into an `else` written as the third's answer. +TEST(Triple, ExactlyTheseRowsHaveACapabilityPin) { + std::set capability; + for (auto& row : known_targets()) { + auto t = parse(row.canonical); + ASSERT_TRUE(t.has_value()) << row.canonical; + if (t->pin_is_capability()) capability.insert(std::string(row.canonical)); + } + // Every freestanding row, the PE+musl row, wasm, and both Android rows. + std::set expected{ + "aarch64-none-elf", "armv7a-none-eabi", "armv7a-none-eabihf", + "riscv32-none-elf", "riscv64-none-elf", "thumbv6m-none-eabi", + "thumbv7em-none-eabi", "thumbv7em-none-eabihf", "thumbv7m-none-eabi", + "thumbv8m.base-none-eabi", "thumbv8m.main-none-eabi", + "thumbv8m.main-none-eabihf", "x86_64-none-elf", + "x86_64-windows-musl", + "wasm32-emscripten", + "aarch64-linux-android", "x86_64-linux-android", + }; + EXPECT_EQ(capability, expected); + + // ANDROID IS THE ONE THAT DOES NOT FIT THE OTHERS' REASON, and that is why + // it was left out. The other entries are refused because the toolchain + // cannot emit the FORMAT; a stock clang emits aarch64 ELF perfectly well. + // What it cannot supply is bionic -- headers, per-API-level stubs, loader + // path -- and no package adds those to another compiler. + auto android = parse("aarch64-linux-android"); + ASSERT_TRUE(android.has_value()); + EXPECT_TRUE(android->pin_is_capability()); + EXPECT_FALSE(android->is_freestanding()); + EXPECT_FALSE(android->is_wasm()); + EXPECT_FALSE(android->is_pe() && android->is_musl()); + + // And a hosted row's pin stays a convention: an author who supplies the + // system may name any compiler. + auto musl = parse("x86_64-linux-musl"); + ASSERT_TRUE(musl.has_value()); + EXPECT_FALSE(musl->pin_is_capability()); +} + +// THE SIMULATOR IS A TARGET, NOT A RUNNER, AND IT NEEDS A SPELLING. +TEST(Triple, TheSimulatorRowsCarryEnvSimAndTheirOwnEffectiveTriple) { + // mcpp's three-field form. Rust spells this `aarch64-apple-ios-sim`; the + // difference is the vendor segment this table elides everywhere. + auto sim = parse("aarch64-ios-sim"); + ASSERT_TRUE(sim.has_value()); + EXPECT_EQ(sim->arch, "aarch64"); + EXPECT_EQ(sim->os, "ios"); + EXPECT_EQ(sim->env, "sim"); + EXPECT_EQ(sim->str(), "aarch64-ios-sim"); + EXPECT_TRUE(sim->is_apple()); + EXPECT_EQ(sim->object_format(), ObjectFormat::MachO); + EXPECT_EQ(sim->family(), "unix"); + + // APPLE'S OWN SPELLING PARSES TO THE SAME ROW. An effective triple clang + // prints carries `-simulator`, and a reader who pastes one must not be + // told mcpp has never heard of it. + auto apple = parse("arm64-apple-ios-simulator"); + ASSERT_TRUE(apple.has_value()); + EXPECT_EQ(apple->str(), "aarch64-ios-sim"); + + // TWO ROWS THAT MUST NOT SHARE AN IDENTITY. The device and the simulator + // have different SDKs and different objects; one identity would put two + // targets in one build directory. + auto device = parse("aarch64-ios"); + ASSERT_TRUE(device.has_value()); + EXPECT_NE(device->str(), sim->str()); + EXPECT_TRUE(device->env.empty()); + + // The effective triple differs too, and that is what the SDK selection + // downstream keys off. + EXPECT_EQ(sim->llvm_triple({}), "arm64-apple-ios-simulator"); + EXPECT_EQ(device->llvm_triple({}), "arm64-apple-ios"); + + // Both host arches, because the simulator runs the HOST's architecture: a + // single row would describe a simulator half the machines cannot run. + auto x86sim = parse("x86_64-ios-sim"); + ASSERT_TRUE(x86sim.has_value()); + EXPECT_EQ(x86sim->str(), "x86_64-ios-sim"); + EXPECT_EQ(x86sim->llvm_triple({}), "x86_64-apple-ios-simulator"); +} + +// THE API LEVEL IS FUSED ONTO THE ENV SEGMENT, AND IT IS NOT OPTIONAL. +TEST(Triple, AndroidFusesTheApiLevelAndBionicRequiresOne) { + auto t = parse("aarch64-linux-android"); + ASSERT_TRUE(t.has_value()); + // Canonical identity carries no level: one row serves every level, which + // is why the level is a project decision in `[target.]` and not a + // multiplication of the table. + EXPECT_EQ(t->str(), "aarch64-linux-android"); + // The effective triple is where it lands. Measured: + // `clang -target aarch64-linux-android21 -print-effective-triple` + // answers `aarch64-unknown-linux-android21`. + EXPECT_EQ(t->llvm_triple("21"), "aarch64-unknown-linux-android21"); + EXPECT_EQ(t->llvm_triple("24"), "aarch64-unknown-linux-android24"); + // WITH NO LEVEL THE FORM IS STILL PRODUCED, and that is deliberate: this + // function composes, it does not decide. The refusal lives where the level + // is chosen, because bionic's own stops the build -- + // "Unversioned target triples are not supported!" -- and the default comes + // from the NDK's `meta/platforms.json` rather than from here. + EXPECT_EQ(t->llvm_triple({}), "aarch64-unknown-linux-android"); + // An effective triple with a level parses back to the canonical row. + auto back = parse("aarch64-unknown-linux-android21"); + ASSERT_TRUE(back.has_value()); + EXPECT_EQ(back->str(), "aarch64-linux-android"); +} + // DOES THIS TARGET'S TOOLCHAIN ARRIVE WITH ITS OWN COMPLETE SYSTEM? // // The predicate exists because mcpp reconstructs a target's system by hand -- @@ -785,11 +970,21 @@ TEST(Triple, WasmJoinsTheCapabilityPinsBecauseNothingElseEmitsIt) { // And an ordinary hosted row is still a convention: a project may name // whichever compiler it likes for its own Linux. EXPECT_FALSE(parse("x86_64-linux-musl")->pin_is_capability()); - // ANDROID IS NOT ONE EITHER, deliberately. The NDK's clang is the only - // thing that serves it today, but the row carries no pin yet, so calling - // the absent pin a capability statement would assert something the table - // does not say. It moves when the row does. - EXPECT_FALSE(parse("aarch64-linux-android")->pin_is_capability()); + // ANDROID IS ONE NOW, AND THIS ASSERTION PREDICTED ITS OWN EXPIRY. It + // read EXPECT_FALSE, with the note "the row carries no pin yet [...] it + // moves when the row does" -- the row moved, so it did. + // + // The 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, which lives inside the + // NDK and is not packaged onto another compiler. + EXPECT_TRUE(parse("aarch64-linux-android")->pin_is_capability()); + EXPECT_TRUE(parse("x86_64-linux-android")->pin_is_capability()); + // The iOS rows are NOT capability pins, and the distinction is worth an + // assertion: they carry no pin at all, so there is nothing to override and + // nothing to refuse. Their tier is what stops a build, not their pin. + EXPECT_FALSE(parse("aarch64-ios")->pin_is_capability()); + EXPECT_FALSE(parse("aarch64-ios-sim")->pin_is_capability()); } TEST(Triple, TheCanonicalSpellingIsNotSEARCHABLEForAVENDORNAME) { From f6519dcd51032d2ee165e048fdd8b8c076e6ac17 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:21:48 +0800 Subject: [PATCH 11/13] =?UTF-8?q?feat(targets):=20x86=5F64-linux-android?= =?UTF-8?q?=20is=20verified=20=E2=80=94=20the=20artifact=20ran=20on=20the?= =?UTF-8?q?=20platform's=20emulator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `adb push` then `adb shell ./andtest` printed `1-2-3`, exit 0, on an API 24 x86_64 system image under Google's own emulator with KVM, from a source that imports std and no project vocabulary beyond `--target`. So one pin serves two rows at two tiers, and the difference is execution rather than confidence in the build. The device row stays `preview` and the reason is recorded where the next attempt will look: the emulator refuses a foreign guest outright, and the documented qemu-user fallback needs four files extracted from an ext4 partition image by `debugfs` -- the one program in `xim:e2fsprogs@1.47.3` that is a broken build. That is an ecosystem defect with its own record in the index, and fixing it moves this row without an engine change. The docs now also reconcile the two records that would otherwise disagree: the support table's Windows cell for both Android rows is `—` because the index publishes no Windows NDK (its archive ships no libc++ module surface), while `mcpp toolchain list` still shows the rows there, because which hosts an index serves is the index's answer and not a constant compiled into the engine. --- ...26-09-11-platform-targets-design-review.md | 22 ++++++++++++ CHANGELOG.md | 24 +++++++++++-- docs/21-the-target-triple.md | 22 ++++++++++-- docs/zh/21-the-target-triple.md | 19 ++++++++-- modules/toolchain-model/src/triple.cppm | 36 ++++++++++++++++--- tests/unit/test_toolchain_triple.cpp | 11 ++++-- 6 files changed, 122 insertions(+), 12 deletions(-) diff --git a/.agents/docs/2026-09-11-platform-targets-design-review.md b/.agents/docs/2026-09-11-platform-targets-design-review.md index 5e3b8ca2..f01581a5 100644 --- a/.agents/docs/2026-09-11-platform-targets-design-review.md +++ b/.agents/docs/2026-09-11-platform-targets-design-review.md @@ -994,6 +994,28 @@ plan. | E15 | R2: `min_api_level` under `[target.]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | **done** | | 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 | +**E16 leaves one thing unanswered, and it is recorded rather than hidden.** +With the host constant gone, `mcpp toolchain list` reports +`aarch64-linux-android` as `available` on every host -- including Windows, +where `xim:android-ndk` deliberately has no table. Google publishes a Windows +NDK and it downloads; what it does not contain is the libc++ module surface +(measured: 9108 entries, no `std.cppm`, no `std/*.inc`, against darwin's 10024 +and 110), so for a module-first build tool that payload cannot serve and an +entry that can never serve is worse than none. + +The engine is not the place to encode that. Which hosts an index serves is the +index's answer, it changes without an engine release, and a constant stating it +here is exactly what E16 removed -- it would go stale again the first time a +future NDK ships the surface. So a Windows user sees the row, the pin resolves, +and xim refuses with `no payload for this platform` before anything is fetched, +naming the package. That is legible at the point of use, which is the standard +this repository already applies to a per-package engine floor. + +What would change the answer is a cheap way to ask the index for a payload's +platform coverage during a listing. There is none today that does not cost a +network round trip per row, and a fourth status word would describe the gap +rather than close it. + **E16 was not in the original plan, and the target matrix is what produced it.** The predicate returned `mcpp::platform::is_linux` for a row whose SDK ships its own sysroot, because `xim:emsdk` and `xim:android-ndk` declared only diff --git a/CHANGELOG.md b/CHANGELOG.md index cad62bfc..dcf27e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,28 @@ x86_64-linux-android -> ELF 64-bit LSB pie, x86-64, 同一个 interpreter 性质所系:NDK 不命名架构,`--target` 才命名 —— 于是每一处「谁说出目标」的缺口都 会在这里现形,而在 wasm 上都不会,因为 `em++` 只有一个目标。 -`preview` 而不是 `verified`:两者都**构建**过,都没有被**执行**过 —— 跑起来需要一台 -设备或一个模拟器,而那正是这两个层级的差别。 +**一个钉,两个层级**,而层级的差别是**执行**,不是对构建的信心。 + +`x86_64-linux-android` 是 `verified`:产物在平台自己的模拟器上跑起来了。 +2026-09-11,linux-x86_64,API 24 的 x86_64 系统镜像 + KVM: + +``` +adb push /data/local/tmp/ +adb shell ./andtest -> 1-2-3 exit 0 +``` + +`aarch64-linux-android` 是 `preview`:构建方式完全相同,而从一台 x86_64 宿主没有 +执行路径。记下来是为了下一次尝试不重复:Google 的模拟器直接拒绝异构 guest —— +`QEMU2 emulator does not support arm64 CPU architecture` —— 所以 arm64 镜像需要一台 +arm64 宿主。文档给出的退路是 qemu-user 配系统镜像自带的 bionic,而准备它要用 +`debugfs` 从一个 ext4 分区镜像里取四个文件,而 `debugfs` 恰好是 +`xim:e2fsprogs@1.47.3` 里唯一一个构建坏了的程序(任何打开文件系统的命令都 SIGFPE, +而同一份构建里的 dumpe2fs/e2fsck/tune2fs 都正常)。那是一个生态缺陷,在索引侧有 +自己的记录,不是引擎的缺口 —— 它被修好时这一行就变成 `verified`,而这里什么都不用动。 + +还有一条链接器告警值得记下来,因为用户会看到它而它**不是**缺陷: +`unsupported flags DT_FLAGS_1=0x8000001`。API 24 的 bionic 加载器不认识 lld 设置的 +`DF_1_PIE` 位,于是告警一句,然后照常把程序加载起来。 五处引擎缺口,每一处都是前一处的失败找出来的,而每一处都**只在多目标载荷上现形**: diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index b1c517d3..95fd6994 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -483,8 +483,8 @@ other's rows. | `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | payload | payload | payload | payload | | `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | payload | payload | payload | payload | | `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | payload | payload | payload | payload | -| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | -| `x86_64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | +| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | — | +| `x86_64-linux-android` | verified | `android-ndk@30.0.16248370` | payload | payload | payload | — | | `aarch64-ios` | planned | — | planned | planned | planned | planned | | `aarch64-ios-sim` | planned | — | planned | planned | planned | planned | | `x86_64-ios-sim` | planned | — | planned | planned | planned | planned | @@ -513,6 +513,24 @@ installed by mcpp · `SDK` the platform's own · `—` unreachable from this hos instead — which is why `x86_64-windows-musl` reads `via dependency graph` on Linux and produces a real PE32+ there. +**And for the two Android rows the `—` on Windows is the INDEX's answer, so +`mcpp toolchain list` still shows them there.** Google publishes a Windows NDK +and it downloads; what it does not contain is the libc++ module surface +(measured: no `std.cppm` and no `std/*.inc`, against 110 on the other two +hosts), so `xim:android-ndk` declares no Windows table — an entry that can +never serve a module-first build is worse than none. The engine does not encode +that: which hosts an index serves changes without an engine release, and a +constant stating it here is what the wasm row's own history shows going stale. +A Windows user therefore sees the row, the pin resolves, and xim refuses with +`no payload for this platform` before anything is fetched, naming the package. + +**The two Android rows differ in tier because one of them was run.** An +x86_64 Android artefact executes on the platform's own emulator, and a +`verified` row means exactly that was done. The device row builds identically +and has no execution path from an x86_64 host: the emulator refuses a foreign +guest (`QEMU2 emulator does not support arm64 CPU architecture`), so it needs +an arm64 host or the qemu-user route. + ### And CI measures every one of them [`ci-target-matrix.yml`](../.github/workflows/ci-target-matrix.yml) runs on all diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index ffad24e8..19419c29 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -433,8 +433,8 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | -| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | -| `x86_64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | payload | +| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | payload | payload | payload | — | +| `x86_64-linux-android` | verified | `android-ndk@30.0.16248370` | payload | payload | payload | — | | `aarch64-ios` | planned | — | planned | planned | planned | planned | | `aarch64-ios-sim` | planned | — | planned | planned | planned | planned | | `x86_64-ios-sim` | planned | — | planned | planned | planned | planned | @@ -461,6 +461,21 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— 载荷产出它」,而依赖图可以改为供给系统 —— 这就是 `x86_64-windows-musl` 在 Linux 上显示 `via dependency graph`、并在那里产出真正的 PE32+ 的原因。 +**而两个 Android 行在 Windows 上的 `—` 是**索引**的答案,所以 +`mcpp toolchain list` 在那里仍然会显示它们。** Google 确实发布 Windows NDK,它也 +下载得到;它不包含的是 libc++ 的**模块面**(实测:没有 `std.cppm`、没有 +`std/*.inc`,而另两个宿主各有 110 个),所以 `xim:android-ndk` 不声明 windows 表 —— +一条永远不能服务「模块优先」构建的条目比没有更坏。引擎不把这件事编进来:一个索引 +服务哪些宿主会在没有引擎发布的情况下变化,而把它写成这里的一个常量,正是 wasm 那 +一行自己的历史所展示的会变陈旧的东西。因此 Windows 用户会看到这一行、钉能解析, +而 xim 在任何东西被下载之前以 `no payload for this platform` 拒绝,并点名那个包。 + +**两个 Android 行层级不同,是因为其中一个被运行过。** 一个 x86_64 的 Android 产物 +在平台自己的模拟器上执行得起来,而 `verified` 这个层级断言的正是「做过这件事」。 +真机那一行构建方式完全相同,而从一台 x86_64 宿主没有执行路径:模拟器直接拒绝异构 +guest(`QEMU2 emulator does not support arm64 CPU architecture`),所以它需要一台 +arm64 宿主,或者 qemu-user 那条路。 + ### 而 CI 把每一台都测了 [`ci-target-matrix.yml`](../../.github/workflows/ci-target-matrix.yml) 在全部四台 diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 48de2fab..b754beb2 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -653,10 +653,38 @@ inline constexpr TargetInfo kKnownTargets[] = { // cost, and until it lands `[target.].sysroot` is the escape hatch // for a machine that has an NDK already. { "aarch64-linux-android", "preview", "", "android-ndk@30.0.16248370", "", false }, - // The emulator's row. Not a convenience: x86_64 is what an Android - // emulator image runs, so a row for the device without one for the - // emulator describes a target nothing in CI can execute. - { "x86_64-linux-android", "preview", "", "android-ndk@30.0.16248370", "", false }, + // The emulator's row, and the one of the pair that could be EXECUTED. + // + // Not a convenience: x86_64 is what an Android emulator image runs, so a + // row for the device without one for the emulator describes a target + // nothing can execute. That argument is now measured rather than asserted. + // 2026-09-11, linux-x86_64, an API 24 x86_64 system image under the + // platform's own emulator with KVM: + // + // adb push /data/local/tmp/ + // adb shell ./andtest -> 1-2-3 exit 0 + // + // from `import std;` and no project vocabulary beyond `--target`. So this + // row is `verified` while `aarch64-linux-android` is `preview`, and the + // difference is execution rather than confidence in the build. + // + // WHY THE DEVICE ROW COULD NOT FOLLOW, recorded so the next attempt does + // not repeat it. Google's emulator refuses a foreign guest outright -- + // "QEMU2 emulator does not support arm64 CPU architecture" -- so the arm64 + // image needs an arm64 host. The documented fallback is qemu-user with the + // system image's own bionic, and preparing it needs four files extracted + // from an ext4 partition image by `debugfs`, which is the one program in + // `xim:e2fsprogs@1.47.3` that is a broken build (SIGFPE on every + // filesystem-opening command, while dumpe2fs/e2fsck/tune2fs from the same + // payload work). That is an ecosystem defect with its own record in the + // index, not an engine gap, and it moves this row to `verified` when it is + // fixed -- nothing here changes. + // + // One linker warning is worth recording because a user will see it and it + // is not a defect: `unsupported flags DT_FLAGS_1=0x8000001`. API 24's + // bionic linker does not recognise the `DF_1_PIE` bit that lld sets, warns, + // and loads the program anyway. + { "x86_64-linux-android", "verified", "", "android-ndk@30.0.16248370", "", false }, // iOS IS NEXT. `aarch64-macos` is `verified`, so Mach-O, `arm64`, the // linker and the Apple half of the toolchain model all exist; what is diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index cdd466d8..c4157807 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -707,13 +707,20 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { // ONE PIN SERVES BOTH ROWS, which is the property the whole Android path // rests on: the NDK names no arch, `--target` does, and that is why the // std module's own precompile had to be told the target as well. - for (auto name : {"aarch64-linux-android", "x86_64-linux-android"}) { + // ONE PIN, TWO TIERS, and the tiers differ by EXECUTION rather than by + // confidence in the build. `x86_64-linux-android` ran on the platform's + // own emulator (API 24 x86_64 image, KVM): `adb push` then + // `adb shell ./andtest` printed `1-2-3`, exit 0. The device row has no + // execution path from an x86_64 host -- Google's emulator refuses a + // foreign guest outright -- so it stays `preview`. + for (auto [name, tier] : {std::pair{"aarch64-linux-android", "preview"}, + std::pair{"x86_64-linux-android", "verified"}}) { auto t = parse(name); ASSERT_TRUE(t.has_value()) << name; EXPECT_EQ(t->str(), name); auto* info = find_known_target(*t); ASSERT_NE(info, nullptr) << name; - EXPECT_EQ(info->tier, "preview") << name; + EXPECT_EQ(info->tier, tier) << name; EXPECT_EQ(info->pin, "android-ndk@30.0.16248370") << name; // Same statement the wasm row makes: the SDK ships the sysroot. EXPECT_TRUE(info->sysroot.empty()) << name; From a1248b0874ba4d140895693fcd6ff0a4d02c75fa Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:33:04 +0800 Subject: [PATCH 12/13] feat(examples, docs): the curriculum covers the new targets, and a warning stops advising what is now refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXAMPLE. `examples/13-platform-targets` is one source with no `cfg`, built for Linux, WebAssembly and both Android ABIs by changing only `--target`. The curriculum had twelve examples and none for any of the three platforms this release adds, which is a gap in the one place a reader looks first. Its host build needs no payload, so it is in BUILD rather than SKIP -- and that is itself worth one build: a manifest naming targets the runner has no payload for must still parse and build for the host. The cross legs are not built there; ci-target-matrix scans every row on four hosts and tests/e2e/641 asserts the vocabulary. Every claim in its README is measured against the artifact the README describes, not carried over from another one: the host and wasm runs print `1-2-3`, both Android artifacts are the file(1) output shown, and the x86_64 artifact was pushed to an API 24 emulator image and executed there. A MISLEADING WARNING, FOUND BY BUILDING openkal FOR ANDROID. When a project's target side comes from the dependency graph, mcpp warns that the row's convention pin was unnecessary and tells the reader to declare their own default instead. On a capability row both halves of that are false: warning: ... so gcc@16.1.0 would have served x86_64-linux-android. State the preference: [target.x86_64-linux-android] toolchain = "gcc@16.1.0" $ (declaring exactly that) error: target 'x86_64-linux-android' cannot be emitted by 'gcc@16.1.0'. Following the advice replaced a warning with an error. The first claim is false on its own terms too: this gcc payload cannot emit an Android object whatever the graph supplies. `graph` answers "who supplies the SYSTEM"; a capability pin answers "who can emit the FORMAT AND THE SYSTEM", and only the second decides whether a substitution was avoidable. The warning is now gated on `pin_is_capability()`, whose row set is asserted exhaustively in test_toolchain_triple, so the gate cannot silently cover every row. DOCS: docs/24-openkal-cross.md (+zh) now answers what the three new platforms mean for openkal, because the answers differ and the difference is where an implementation sits relative to a C library. Android shares openkal-linux UNCHANGED. It is written on the Linux kernel's own system calls and borrows nothing from a C library; Android's kernel IS Linux, the per-arch syscall ABI is identical, and sys.h dispatches on __x86_64__/__aarch64__ -- architecture, not OS. `cfg(os = "linux")` is true for an Android triple, so a portable program selects it with the line a Linux consumer already writes. Measured: openkal-linux compiles for both ABIs, and a program written against openkal alone -- no C library, no `import std` -- printed `openkal: 1-2-3` with exit 0 on an API 24 emulator image. iOS would share openkal-macos by the same argument, and the argument is not evidence: the SDKs are not redistributable, so there is nothing to build against and the rows claim nothing. Web needs a NEW implementation and a different shape. Emscripten has no kernel and no system calls; an implementation would have to sit ABOVE its C library, which the specification permits and which is new software rather than a sharing decision. Until then the row is served the ordinary way, by the `xim:emsdk` payload, which is what its `verified` tier records. The docs/21 support table and the two Android rows' Windows cells were also reconciled with the index: `—` there means no payload is published for that host, while `toolchain list` still shows the rows, because which hosts an index serves is the index's answer and not a constant compiled into the engine. --- .github/tools/build_examples.sh | 13 +++ docs/03-examples.md | 1 + docs/24-openkal-cross.md | 85 +++++++++++++++ docs/zh/03-examples.md | 1 + docs/zh/24-openkal-cross.md | 75 +++++++++++++ examples/13-platform-targets/README.md | 124 ++++++++++++++++++++++ examples/13-platform-targets/mcpp.toml | 21 ++++ examples/13-platform-targets/src/main.cpp | 16 +++ src/build/prepare.cppm | 33 +++++- 9 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 examples/13-platform-targets/README.md create mode 100644 examples/13-platform-targets/mcpp.toml create mode 100644 examples/13-platform-targets/src/main.cpp diff --git a/.github/tools/build_examples.sh b/.github/tools/build_examples.sh index 1f4a3b44..de37d3ee 100755 --- a/.github/tools/build_examples.sh +++ b/.github/tools/build_examples.sh @@ -57,6 +57,19 @@ BUILD=( # three-file package. examples/12-a-new-device-language/toyc examples/12-a-new-device-language/app + # One source, three platforms. BUILT here rather than skipped, because its + # HOST build needs no payload at all -- the `[target.*-linux-android]` + # sections are inert unless that target is selected, which is itself worth + # one build: a manifest that names a target the runner has no payload for + # must still parse and build for the host. + # + # The cross legs are not built here. `wasm32-emscripten` and the two + # Android rows would pull `xim:emsdk` and `xim:android-ndk` -- about 1.5 GB + # between them -- and the signal already exists elsewhere: + # ci-target-matrix scans every row on four hosts, tests/e2e/641 asserts the + # vocabulary, and the example's README records the measured artifacts and + # the emulator run for the binary it describes. + examples/13-platform-targets ) # `key|reason`. diff --git a/docs/03-examples.md b/docs/03-examples.md index 98e26828..2d0dd234 100644 --- a/docs/03-examples.md +++ b/docs/03-examples.md @@ -82,6 +82,7 @@ the map; the table below is what each sub-example adds. |---|---| | [`08-build-rules`](../examples/08-build-rules/) | two rule packages and a project using both; `host-module = true`, `mcpp::action` with `role = "check"` | | [`12-a-new-device-language`](../examples/12-a-new-device-language/) | `device_extensions` and `rule_module`: a rule package teaching mcpp a language the engine has never heard of, whose compiler is a package built through `tools = [...]` for the build machine | +| [`13-platform-targets`](../examples/13-platform-targets/) | one source and no `cfg`, built for Linux, WebAssembly and both Android ABIs by changing only `--target`; `min_api_level` as the project's own decision, and a capability pin that cannot be overridden | [31 — Authoring a Rule Package](31-authoring-a-rule-package.md) is the reference these two illustrate. diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index e48a22a1..6f8d92b5 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -178,6 +178,91 @@ the object ABI on Windows, the object format where there is no operating system — and one value records which, rather than a boolean recording only whether the first case holds. +## Android, Web And iOS Under This Model + +The three platforms mcpp added target rows for in 2026.9.11.3 are not one +question. What decides each is where its implementation would have to sit +relative to a C library, and the answers are different. + +### Android shares the Linux implementation, unchanged + +`openkal-linux` is written on the Linux kernel's own system-call interface and +borrows nothing from any C library — that is what lets it be placed beneath one. +Android's kernel **is** Linux, the system-call ABI for a given architecture is +the same, and `src/sys.h` dispatches on `__x86_64__` / `__aarch64__`, which is +the architecture rather than the operating system. Nothing in it is glibc's or +bionic's. + +So a portable program needs no new line. `cfg(os = "linux")` is **true for an +Android triple**, because Android is an `env` value on a `linux` OS — the +modelling decision [21 — The Target Triple](21-the-target-triple.md) records — +and the implementation is selected by the line a Linux consumer already writes: + +```toml +[target.'cfg(os = "linux")'.dependencies] +openkal-linux = "0.12.0" +``` + +Measured 2026-09-11, a program written against openkal and nothing else — no C +library, no `import std`: + +``` +mcpp build --target x86_64-linux-android + kernel-abi openkal (openkal-linux@0.12.0, graph) + -> ELF 64-bit LSB pie, x86-64, interpreter /system/bin/linker64 + +mcpp build --target aarch64-linux-android + -> ELF 64-bit LSB pie, ARM aarch64, same interpreter +``` + +and the x86_64 artifact, pushed to an API 24 emulator image and executed: + +``` +openkal: 1-2-3 exit 0 +``` + +`openkal-linux` itself also compiles for both Android targets unchanged, which +is the weaker claim of the two and is worth stating separately: the first says +the implementation builds, the second says a program over it runs. + +### iOS would share the macOS implementation, and that cannot be claimed yet + +The same argument applies on Apple's side — iOS and macOS share the Darwin +kernel, and `openkal-macos` is arch-dispatched the same way — but the argument +is not evidence. The iPhoneOS and iPhoneSimulator SDKs ship inside Xcode and +are not redistributable, so the `aarch64-ios` and `*-ios-sim` rows are +`planned`: there is nothing to build against and therefore nothing to run. +Declaring support on a structural argument alone is the shape this ecosystem has +paid for before — a package present in an index is not a package that builds a +real project — so these rows claim nothing until an SDK is reachable. + +### Web needs a new implementation, and a different one + +Emscripten is the one of the three that changes the model rather than extending +it. There is no kernel and there are no system calls to issue: Emscripten +supplies its own C library over a JavaScript host. An openkal implementation for +it therefore cannot be written the way `openkal-linux` is — beneath a C library +— and would have to sit **above** one. The specification permits exactly that +("an implementation may be built upon a C library, beneath one, or without +one"), so this is new software rather than a sharing decision, and it is the one +of the three that is neither done nor blocked. + +Until it exists, `wasm32-emscripten` is served the ordinary way: by a payload. +`xim:emsdk` ships the compiler, the sysroot and a libc++ module surface, so a +program that uses `import std` builds and runs for the Web today without openkal +being involved at all — which is what the row's `verified` tier records. + +### The table + +| platform | implementation | status | +|---|---|---| +| Linux (glibc, musl) | `openkal-linux` | the reference implementation | +| Android (both ABIs) | `openkal-linux`, unchanged | builds; a program over it ran on an emulator | +| macOS | `openkal-macos` | on the macOS system-call surface | +| iOS, iOS simulator | `openkal-macos` would serve it | blocked: the SDK is not redistributable | +| Windows | `openkal-windows` | on Win32 and the object manager | +| Web (Emscripten) | none | needs an implementation written ABOVE a C library | + ## Bare Metal A target with no operating system is the same model with the platform layer diff --git a/docs/zh/03-examples.md b/docs/zh/03-examples.md index 8a76cfe5..ff9cc354 100644 --- a/docs/zh/03-examples.md +++ b/docs/zh/03-examples.md @@ -77,6 +77,7 @@ mcpp build && mcpp run |---|---| | [`08-build-rules`](../../examples/08-build-rules/) | 两个规则包与同时使用它们的工程;`host-module = true`、`role = "check"` 的 `mcpp::action` | | [`12-a-new-device-language`](../../examples/12-a-new-device-language/) | `device_extensions` 与 `rule_module`:规则包教会 mcpp 一门引擎从未听说过的语言,而它的编译器是一个经 `tools = [...]` 为构建机构建出来的包 | +| [`13-platform-targets`](../../examples/13-platform-targets/) | 一份源码、零个 `cfg`,只改 `--target` 就为 Linux、WebAssembly 和两个 Android ABI 构建;`min_api_level` 作为工程自己的决定,以及一条不能被覆盖的能力钉 | [31 —— 编写规则包](31-authoring-a-rule-package.md) 是这两个示例所演示内容的参考。 diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 91257635..b5dcffa9 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -154,6 +154,81 @@ Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium 没有操作系统时是对象格式 —— 因此记录的是**它是哪一个**, 而不是一个只记录「是否为第一种」的布尔。 +## 这个模型下的 Android、Web 与 iOS + +mcpp 在 2026.9.11.3 里加出目标行的这三个平台不是同一个问题。决定每一个的是它的 +实现相对一个 C 库该落在哪一侧,而三个答案各不相同。 + +### Android 共用 Linux 的实现,一行都不用改 + +`openkal-linux` 写在 Linux 内核自己的系统调用接口上,不向任何 C 库借用任何东西 —— +这正是它能被放到一个 C 库**底下**的原因。Android 的内核**就是** Linux,给定架构上 +的系统调用 ABI 完全相同,而 `src/sys.h` 按 `__x86_64__` / `__aarch64__` 分支,也就是 +按**架构**而不是按操作系统。它里面没有任何属于 glibc 或 bionic 的东西。 + +所以一个可移植程序不需要新增任何一行。`cfg(os = "linux")` 对一个 Android triple +**为真**,因为 Android 是 `linux` OS 上的一个 `env` 值 —— +[21 — 目标三元组](21-the-target-triple.md) 记着这处建模决定 —— 于是实现由一个 +Linux 消费者本来就会写的那一行选出: + +```toml +[target.'cfg(os = "linux")'.dependencies] +openkal-linux = "0.12.0" +``` + +实测 2026-09-11,一个只针对 openkal 写的程序 —— 没有 C 库,也没有 `import std`: + +``` +mcpp build --target x86_64-linux-android + kernel-abi openkal (openkal-linux@0.12.0, graph) + -> ELF 64-bit LSB pie, x86-64, interpreter /system/bin/linker64 + +mcpp build --target aarch64-linux-android + -> ELF 64-bit LSB pie, ARM aarch64, 同一个 interpreter +``` + +而那个 x86_64 产物被推到一台 API 24 的模拟器镜像上执行: + +``` +openkal: 1-2-3 exit 0 +``` + +`openkal-linux` 自己也能为两个 Android 目标原样编译,这是两条里较弱的那一条,值得 +分开陈述:前者说的是**实现**构建得起来,后者说的是**它上面的程序**跑得起来。 + +### iOS 会共用 macOS 的实现,而这一条现在还不能声称 + +同样的论证在 Apple 这一侧成立 —— iOS 与 macOS 共用 Darwin 内核,而 `openkal-macos` +是按同样方式按架构分支的 —— 但**论证不是证据**。iPhoneOS 与 iPhoneSimulator 的 SDK +在 Xcode 里且不可再分发,所以 `aarch64-ios` 与 `*-ios-sim` 三行是 `planned`:没有 +东西可以拿来构建,因此也没有东西可以拿来运行。仅凭一个结构性论证就声明支持,是这个 +生态已经付过代价的那种形状 —— 一个在索引里的包不等于一个能构建真实工程的包 —— +所以在 SDK 可达之前,这三行什么都不声称。 + +### Web 需要一份新的实现,而且是另一种形状 + +Emscripten 是三者里**改变模型**而不是扩展表格的那一个。那里没有内核,也没有系统 +调用可发:Emscripten 在一个 JavaScript 宿主之上供给它自己的 C 库。所以给它写的 +openkal 实现不可能按 `openkal-linux` 的方式写 —— 落在一个 C 库底下 —— 而必须落在 +一个 C 库**之上**。规范恰好允许这一点(「一个实现可以建立在一个 C 库之上、之下, +或者不依赖 C 库」),所以这是**新软件**而不是一个共用决定,而它是三者里既没做完也 +没被阻塞的那一个。 + +在它出现之前,`wasm32-emscripten` 走的是普通那条路:一个载荷。`xim:emsdk` 自带 +编译器、sysroot 和一份 libc++ 的模块面,所以一个用 `import std` 的程序今天就能为 +Web 构建并运行,而 openkal 完全不参与 —— 这正是那一行的 `verified` 层级所记录的。 + +### 表 + +| 平台 | 实现 | 状态 | +|---|---|---| +| Linux(glibc、musl) | `openkal-linux` | 参考实现 | +| Android(两个 ABI) | `openkal-linux`,原样 | 构建通过;它上面的程序在模拟器上跑过 | +| macOS | `openkal-macos` | 在 macOS 的系统调用面上 | +| iOS、iOS 模拟器 | `openkal-macos` 会服务它 | 阻塞:SDK 不可再分发 | +| Windows | `openkal-windows` | 在 Win32 与对象管理器上 | +| Web(Emscripten) | 无 | 需要一份写在 C 库**之上**的实现 | + ## 裸机 一个没有操作系统的目标,是同一个模型,只是平台层由固件而非内核供给。 diff --git a/examples/13-platform-targets/README.md b/examples/13-platform-targets/README.md new file mode 100644 index 00000000..89bd7045 --- /dev/null +++ b/examples/13-platform-targets/README.md @@ -0,0 +1,124 @@ +# 13 — platform targets + +一份源码,三个平台。这里没有任何平台感知的东西:没有 `cfg`、没有预处理分支、 +没有按目标分开的源文件。在 Linux 二进制、WebAssembly 模块和 Android 产物之间 +变的只有命令行上的 `--target`。 + +```bash +cd 13-platform-targets +mcpp build && mcpp run # 宿主 +``` + +## Web + +```bash +mcpp run --target wasm32-emscripten +``` + +实测(linux-x86_64,`xim:emsdk` 6.0.9): + +``` +bin/platform-targets 65389 bytes the JavaScript +bin/platform-targets.wasm 447183 bytes the module +node bin/platform-targets -> 1-2-3 +``` + +`mcpp run` 会用 `node` 跑它,所以不需要额外的一步。工程侧**一个新词汇都不需要**: +`wasm32-emscripten` 这一行自己命名了它的载荷(`emsdk@6.0.9`),载荷自带 sysroot, +而 Emscripten 自己就发布一份 libc++ 的模块面。 + +## Android + +```bash +mcpp build --target x86_64-linux-android # 模拟器 +mcpp build --target aarch64-linux-android # 真机 +``` + +实测(同一台机器,`xim:android-ndk` 30.0.16248370): + +``` +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, 同一个 interpreter +``` + +**一个钉服务两行。** NDK 不命名架构,`--target` 才命名 —— 所以 +`[target.] toolchain` 不需要写,而两行共用 +`android-ndk@30.0.16248370`。 + +跑起来(x86_64 键在平台自己的模拟器上,API 24 镜像 + KVM): + +```bash +adb push target/x86_64-linux-android/*/bin/platform-targets /data/local/tmp/ +adb shell /data/local/tmp/platform-targets +# -> 1-2-3 +``` + +加载时会有一句告警,它**不是**缺陷:`unsupported flags DT_FLAGS_1=0x8000001`。 +API 24 的 bionic 加载器不认识 lld 设置的 `DF_1_PIE` 位,于是告警一句,然后照常 +把程序加载起来。 + +### API level 在 `mcpp.toml` 里,不在 triple 里 + +`mcpp.toml` 声明的是: + +```toml +[target.aarch64-linux-android] +min_api_level = 24 +``` + +规范 triple 保持 `aarch64-linux-android` —— 它命名输出目录、`cfg(env = ...)` +和 ABI tag。级别只进**编译器看到的** triple(`aarch64-unknown-linux-android24`) +和**构建指纹**:级别决定哪些 bionic 符号可见,所以两个级别是两个 ABI,绝不可共用 +一个构建目录。 + +这个键是**可选的**。不写的话,mcpp 读 NDK 自己在 `meta/platforms.json` 里声明的 +下限(r30 是 21)。写在这里是因为一个要发布到某个最低版本的工程应该自己说出来, +而不是继承载荷的下限恰好是多少。 + +`[package] macos_deployment_target` 是 Apple 目标上的同一根轴;一个目标要么是 +Apple 要么是 Android,所以两者在指纹里共用一个槽。 + +## 这一行不能被覆盖 + +Android 和 wasm 的钉是**能力**而不是约定: + +```bash +mcpp build --target aarch64-linux-android # [target.…] toolchain = "llvm@22.1.8" +# error: target 'aarch64-linux-android' cannot be emitted by 'llvm@22.1.8'. +# An Android target needs bionic, not just an aarch64 or x86_64 back end: +# its headers, its per-API-level stubs and its loader path are inside the +# NDK, and no package adds them to another compiler. +``` + +一个普通 clang 发 aarch64 ELF 完全没问题 —— 它拿不出来的是**体系**。说出来比 +解析出 llvm 再在它内部失败要好。 + +## iOS + +`aarch64-ios`、`aarch64-ios-sim`、`x86_64-ios-sim` 三行在词汇里,都是 `planned`: + +```bash +mcpp build --target aarch64-ios-sim +# error: target 'aarch64-ios-sim' is registered but not yet supported (planned) +# — no toolchain is published for it yet. +``` + +阻塞项是**许可**而不是载荷:NDK 是 Apache-2.0、Emscripten 是 MIT,而 iPhoneOS 与 +iPhoneSimulator 的 SDK 在 Xcode 里,两者都不可再分发。这三行今天买到的是一句 +点名那一行的 `tier-planned`,而不是一句假的 `unknown target`。 + +模拟器是**一个目标**而不是一个 runner:它有自己的 SDK、产出自己的对象,取 +`-mios-simulator-version-min` 而真机取 `-miphoneos-version-min`。所以它有自己的 +行,而不是折进设备那一行。 + +## 支持矩阵 + +`docs/21-the-target-triple.md` 的表是完整的那一份;这里只列这个例子碰到的行: + +| target | tier | pin | 运行过? | +|---|---|---|---| +| `wasm32-emscripten` | verified | `emsdk@6.0.9` | 是,`node` | +| `x86_64-linux-android` | verified | `android-ndk@30.0.16248370` | 是,平台模拟器 | +| `aarch64-linux-android` | preview | `android-ndk@30.0.16248370` | 否 —— 从 x86_64 宿主没有执行路径 | +| `aarch64-ios` / `*-ios-sim` | planned | — | 否 | diff --git a/examples/13-platform-targets/mcpp.toml b/examples/13-platform-targets/mcpp.toml new file mode 100644 index 00000000..ab53ffd9 --- /dev/null +++ b/examples/13-platform-targets/mcpp.toml @@ -0,0 +1,21 @@ +[package] +name = "platform-targets" +version = "0.1.0" + +# ANDROID'S API LEVEL IS A PROJECT DECISION, NOT A TOOLCHAIN PROPERTY, which +# is why it lives here and not in the triple. One NDK serves a range of levels, +# and the level selects which bionic symbols exist -- so it is an ABI axis and +# it enters the build fingerprint: two levels are two build directories. +# +# It is OPTIONAL. Left out, mcpp reads the floor the NDK itself declares in +# `meta/platforms.json` (21 for r30). It is stated here because a project that +# ships to a minimum should say so rather than inherit whatever the payload's +# floor happens to be. +# +# `macos_deployment_target` in `[package]` is the same axis for Apple targets; +# a target is one or the other, so they share one slot in the fingerprint. +[target.aarch64-linux-android] +min_api_level = 24 + +[target.x86_64-linux-android] +min_api_level = 24 diff --git a/examples/13-platform-targets/src/main.cpp b/examples/13-platform-targets/src/main.cpp new file mode 100644 index 00000000..63c52743 --- /dev/null +++ b/examples/13-platform-targets/src/main.cpp @@ -0,0 +1,16 @@ +// One source, three platforms. Nothing here is platform-aware: no `cfg`, no +// preprocessor branch, no per-target source. The only thing that changes +// between a Linux binary, a WebAssembly module and an Android artifact is the +// `--target` on the command line. +// +// `import std` is the point. A target whose toolchain cannot compile a module +// interface unit would be worse than its absence in a module-first build tool, +// so this deliberately exercises the standard library module rather than a +// header, on every target the README lists. +import std; + +int main() { + std::vector v{3, 1, 2}; + std::ranges::sort(v); + std::print("{}-{}-{}\n", v[0], v[1], v[2]); +} diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index dc0de54b..b4b3fa1a 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -9126,8 +9126,39 @@ prepare_build(bool print_fingerprint, // out of order. Deferring the CHOICE the way the target side itself was // deferred is the structural fix and is its own change; until then the // user is told what happened and how to state the preference once. + // + // AND NOT FOR A ROW WHOSE PIN IS A CAPABILITY, WHERE BOTH HALVES OF + // THIS SENTENCE ARE FALSE. + // + // The warning says the default "would have served" the target and then + // tells the reader to declare it. On a capability row neither holds: + // nothing but the pinned payload can emit the target at all, and the + // declaration it suggests is REFUSED by the capability gate a few + // hundred lines above -- so following the advice replaces a warning + // with an error. + // + // Measured on `openkal-linux` built for `x86_64-linux-android`, whose + // target side does come from the graph: + // + // warning: ... so gcc@16.1.0 would have served x86_64-linux-android. + // State the preference: [target.x86_64-linux-android] + // toolchain = "gcc@16.1.0" + // $ (declaring exactly that) + // error: target 'x86_64-linux-android' cannot be emitted by + // 'gcc@16.1.0'. + // + // The first claim is false on its own terms too: this gcc payload + // cannot emit an Android object whatever the graph supplies. `graph` + // answers "who supplies the SYSTEM", and a capability pin answers "who + // can emit the FORMAT AND THE SYSTEM" -- two questions, and only the + // second one decides whether a substitution was avoidable. + const bool pinIsCapability = [&] { + auto tt = mcpp::toolchain::triple::parse(resolvedTargetCanonical); + return tt && tt->pin_is_capability(); + }(); if (!pinReplacedDefault.empty() - && resolvedTargetSide.system_from_graph()) { + && resolvedTargetSide.system_from_graph() + && !pinIsCapability) { mcpp::diag::warning("toolchain", std::format( "this project's target side comes from its dependency graph, so " "{} would have served {}.\n" From d14e2eda24366a1e285fb436c6fc715b03c9c043 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:52:52 +0800 Subject: [PATCH 13/13] test(targets): the two pure producers the Android path changed now have criteria THE PRE-MERGE REVIEW'S OWN FINDING. Mapping every behaviour change in this PR to a criterion left seven without one, all of them "the Android build works" -- testable only with a 704 MB payload CI does not have. Two of the seven are pure functions of a `Toolchain` and needed no payload at all, which makes them the two worth closing. `std_module_build_commands`: the PRECOMPILE carries the machine flags when only `stdModuleTargetFlags` has them, and states the target exactly ONCE when `stdModuleFlags` also does -- the superset case, where concatenating both would put `--target=` on the command line twice. `host_compile_tokens`: an own-sysroot target receives EXACTLY the target flag and nothing else. Asserted as the whole vector rather than as "contains", because the property is that nothing else is emitted: this host's glibc headers reaching a wasm compile is the measured failure the gate exists for. THE CONTROL TOOK TWO ATTEMPTS AND BOTH FAILURES WERE THE CONTROL'S. Asserting that a hosted target receives more than one token failed, because a bare `Toolchain` carrying no payload has nothing to reconstruct either -- both sides produced exactly the triple. Reaching for `--no-default-config` without a payload that HAS a cfg was the same mistake once removed. `FakeClangPayload` writes a `clang++.cfg`, which is what makes the discriminator real, and the bypass is the right one to use because withholding it from an SDK is a property the gate's own comment states. Both verified in both directions: reverting each fix turns its test red. --- tests/unit/test_hostflags.cpp | 110 +++++++++++++++++++++++++++ tests/unit/test_toolchain_stdmod.cpp | 69 +++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/tests/unit/test_hostflags.cpp b/tests/unit/test_hostflags.cpp index 2cf1ed66..b9535c5d 100644 --- a/tests/unit/test_hostflags.cpp +++ b/tests/unit/test_hostflags.cpp @@ -507,3 +507,113 @@ TEST(HostFlags, TheCfgBypassSurvivesAGraphSuppliedTargetSide) { EXPECT_TRUE(has(a, "-nostdinc++")); EXPECT_FALSE(has(b, "-nostdinc++")); } + +// "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 for `has_own_sysroot()` withholds the target's system +// reconstructed onto the command line -- libc++'s headers, glibc's, the Linux +// UAPI headers, the cfg bypass, the C-runtime prefix -- because an Emscripten +// or Android SDK already has all of it. That is right. It stood in FRONT of +// the paragraph beginning "THE TRIPLE, SAID OUT LOUD", which states the +// opposite rule for the same underlying reason: an ordinary clang emits for the +// machine it is running on unless told otherwise. The stronger claim won by +// position. +// +// Both are right about their own object. The SYSTEM is the payload's; WHICH +// TARGET is still mcpp's to say, because one NDK serves both Android ABIs and +// nothing else on the command line distinguishes them. The defect was reported +// by neither compile but by the module loader: +// +// error: AST file 'std.pcm' was compiled for the target +// 'aarch64-unknown-linux-android21' but the current translation unit is +// being compiled for target 'x86_64-unknown-linux-gnu' +// +// followed by eight cascading "use of undeclared identifier 'std'" lines, +// which is what a reader sees first. +TEST(HostFlags, AnOwnSysrootTargetIsToldWhichTargetAndNothingElse) { + HostFlagOptions opt; + + for (auto name : {"aarch64-linux-android", "x86_64-linux-android", + "wasm32-emscripten"}) { + auto tc = tc_for(CompilerId::Clang); + tc.targetTriple = name; + tc.crossTargetFlag = "--target=SENTINEL-TRIPLE"; + + auto tokens = mcpp::toolchain::host_compile_tokens( + tc, opt, mcpp::toolchain::no_escape); + + // EXACTLY the target flag. Asserted as the whole vector rather than as + // "contains", because the property is that nothing ELSE is emitted: + // this host's glibc headers reaching a wasm compile is the measured + // failure this gate exists for. + ASSERT_EQ(tokens.size(), 1u) << name << ": " << [&] { + std::string all; + for (auto const& t : tokens) { all += t; all += ' '; } + return all; + }(); + EXPECT_EQ(tokens[0], "--target=SENTINEL-TRIPLE") << name; + } + + // AND THE GATE IS STILL A GATE, DISCRIMINATED BY THE cfg BYPASS. + // + // A first version of this control asserted that a HOSTED target receives + // more than one token, and it failed -- with a bare `Toolchain` carrying no + // payload paths, the hosted path has nothing to reconstruct either, so both + // sides produced exactly the triple and the control could not tell them + // apart. The control was wrong, not the code. + // + // `--no-default-config` is the discriminator, and it is a property the + // gate's own comment states: the bypass exists to stop clang reading a + // per-install `clang++.cfg`, while `em++` is a wrapper whose entire job is + // to supply configuration, so suppressing it would be suppressing the + // toolchain. It is therefore emitted past the gate and never before it, + // which is exactly what a control needs. + // A first version of this control asserted only that a HOSTED target + // receives more than one token, and it failed -- with a bare `Toolchain` + // carrying no payload the hosted path has nothing to reconstruct either, + // so both sides produced exactly the triple and the control could not tell + // them apart. A second version reached for `--no-default-config` without a + // payload that HAS a cfg, which is the same mistake once removed. The + // fixture is what makes the discriminator real. + // + // `--no-default-config` is the right discriminator because it is a + // property the gate's own comment states: the bypass exists to stop clang + // reading a per-install `clang++.cfg`, while `em++` is a wrapper whose + // entire job is to supply configuration, so suppressing it would be + // suppressing the toolchain. Emitted past the gate, never before it. + FakeClangPayload payload{"own-sysroot-gate"}; + HostFlagOptions bypass; + bypass.cfgBypass = HostFlagOptions::CfgBypass::Always; + + auto host = tc_for(CompilerId::Clang); + host.binaryPath = payload.root / "bin" / "clang++"; + host.crossTargetFlag = "--target=x86_64-unknown-linux-gnu"; + auto hostTokens = mcpp::toolchain::host_compile_tokens( + host, bypass, mcpp::toolchain::no_escape); + EXPECT_NE(std::ranges::find(hostTokens, "--no-default-config"), + hostTokens.end()) + << "a hosted clang with a cfg beside it must reach the bypass"; + + for (auto name : {"aarch64-linux-android", "wasm32-emscripten"}) { + auto sdk = tc_for(CompilerId::Clang); + sdk.binaryPath = payload.root / "bin" / "clang++"; // same payload + sdk.targetTriple = name; + sdk.crossTargetFlag = "--target=SENTINEL-TRIPLE"; + auto sdkTokens = mcpp::toolchain::host_compile_tokens( + sdk, bypass, mcpp::toolchain::no_escape); + EXPECT_EQ(std::ranges::find(sdkTokens, "--no-default-config"), + sdkTokens.end()) + << name << ": the cfg bypass must be withheld from an SDK whose " + "driver's job is to supply configuration"; + EXPECT_EQ(sdkTokens.size(), 1u) << name; + } + + // A row with no cross flag emits nothing at all rather than an empty + // token: an empty argv element is an argument the driver must interpret. + auto bare = tc_for(CompilerId::Clang); + bare.targetTriple = "wasm32-emscripten"; + ASSERT_TRUE(bare.crossTargetFlag.empty()); + EXPECT_TRUE(mcpp::toolchain::host_compile_tokens( + bare, opt, mcpp::toolchain::no_escape).empty()); +} diff --git a/tests/unit/test_toolchain_stdmod.cpp b/tests/unit/test_toolchain_stdmod.cpp index dc337b40..b7e41eab 100644 --- a/tests/unit/test_toolchain_stdmod.cpp +++ b/tests/unit/test_toolchain_stdmod.cpp @@ -70,3 +70,72 @@ TEST(ToolchainStdmod, ClangStdCompatCommandsUseRequestedStandard) { EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; } } + +// THE PRECOMPILE HAS TO KNOW WHICH MACHINE, AND ONLY ONE OF TWO SOURCES EVER +// CARRIES IT. +// +// `stdModuleTargetFlags` reached only the CODEGEN command, on the reading that +// the first step needs headers and the second needs the machine. The first step +// needs both: a `--precompile` that does not say which target resolves the +// standard library's own `#include <__config>` against the BUILDING machine, +// and the error names a header rather than the missing flag. +// +// It was invisible while exactly two kinds of toolchain existed. A payload +// whose compiler IS its target needs no flag, and a PACKAGE-provided module +// carries the target inside `stdModuleFlags`. A payload whose compiler serves +// SEVERAL targets is a third kind and has neither -- one NDK clang++ compiles +// for both Android ABIs and is told which by `--target` alone. +TEST(ToolchainStdmod, ThePrecompileCarriesTheMachineWhenOnlyTargetFlagsHaveIt) { + auto tc = clang_toolchain(); + tc.targetTriple = "aarch64-linux-android"; + // What prepare_build sets for such a row: the machine, and nothing about + // include paths, because the SDK's own driver finds those. + tc.stdModuleTargetFlags = + " --target=aarch64-unknown-linux-android21 -D__BIONIC_CTYPE_INLINE="; + ASSERT_TRUE(tc.stdModuleFlags.empty()); + + auto cmds = clang::std_module_build_commands( + tc, "cache", "cache/pcm.cache/std.pcm", "", "-std=c++23"); + ASSERT_EQ(cmds.size(), 2u); + + // BOTH commands, not just the second. The precompile is the one that was + // missing it and the one whose failure names a header. + for (auto const& cmd : cmds) { + EXPECT_NE(cmd.find("--target=aarch64-unknown-linux-android21"), + std::string::npos) << cmd; + } + // And the bionic workaround reaches the step that parses the headers. + EXPECT_NE(cmds[0].find("-D__BIONIC_CTYPE_INLINE="), std::string::npos) + << cmds[0]; +} + +// AND IT IS NOT ADDED TWICE. `stdModuleFlags` is a SUPERSET of +// `stdModuleTargetFlags` when it is set at all -- its producer builds the +// machine part first and appends the include part -- so a precompile that +// concatenated both would put `--target=` on the command line twice. Taking +// the superset in preference is what keeps that from happening. +TEST(ToolchainStdmod, APackageProvidedModuleStillStatesTheMachineExactlyOnce) { + auto tc = clang_toolchain(); + tc.targetTriple = "aarch64-macos"; + tc.stdModuleTargetFlags = " --target=arm64-apple-macos14.0"; + tc.stdModuleFlags = + " --target=arm64-apple-macos14.0 -nostdinc++ -isystem /pkg/include"; + + auto cmds = clang::std_module_build_commands( + tc, "cache", "cache/pcm.cache/std.pcm", "", "-std=c++23"); + ASSERT_EQ(cmds.size(), 2u); + + const auto count = [](std::string_view hay, std::string_view needle) { + std::size_t n = 0, at = 0; + while ((at = hay.find(needle, at)) != std::string_view::npos) { + ++n; at += needle.size(); + } + return n; + }; + EXPECT_EQ(count(cmds[0], "--target="), 1u) << cmds[0]; + // The package's include path reaches the step that needs it, and only it. + EXPECT_NE(cmds[0].find("-isystem /pkg/include"), std::string::npos) + << cmds[0]; + EXPECT_EQ(cmds[1].find("-isystem /pkg/include"), std::string::npos) + << cmds[1]; +}