Skip to content

Commit 51dff99

Browse files
No distribution format lives in the engine, and the three platforms a package cannot add (2026.9.11.1) (#605)
* feat(pack): `--format <name>` dispatches to a package, and no distribution format lives in the engine (2026.9.11.1) `tar` and `dir` answer the same question `msi` and `appimage` answer -- what shape does the output take -- so they are values of one flag rather than the beginning of a second one. The split that keeps every other format out of the engine is: `mcpp pack` owns the mechanism and the one universal format, and every other format lives in a package that `mcpp pack` dispatches to. The universal format is what it already produces: an archive that extracts and runs, universal in the only sense that matters here -- it needs no knowledge of anyone else's release. Everything past it does. dpkg's control fields, AppImage's runtime, WiX's schema, Apple's notarisation, Android's signing scheme: each one bound into the engine couples an mcpp release to a release mcpp does not control. The project already made this argument for languages, where Slang is supported without being named in the engine, and a distribution format has less claim to a name in the engine than a language does. Three additions, each FORMAT-NEUTRAL, which is the test for whether something belongs in the engine at all: - A staged tree an artifact action can consume. `mcpp pack` already computes one -- the dependency closure after the strip policy, the debug-symbol split and `include`/`exclude` -- and then compressed it and the directory was gone, so a `.deb`, an AppImage, a `.app` and an `.msi` each had to rebuild it. `${mcpp.stage_dir}` exposes it. - The rest of `[package]` in the build program: `MCPP_PKG_VERSION` / `_DESCRIPTION` / `_LICENSE` / `_AUTHORS` / `_REPO` and the matching `mcpp::package_*()`. Every installer states a version; without these a project restates it in the member's own options, where the copy drifts from `[package]` with nothing able to detect it. - `--format` resolving its value through the graph. A package declares with `mcpp::provides_pack_format("<name>")`; `--format <name>` finds the provider among the resolved dependencies. The refusal for an unknown value names what IS available rather than a constant, and arrives before anything is compiled. DECLARE UNCONDITIONALLY, SUBMIT CONDITIONALLY. This is the load-bearing rule of the dispatch and the one a member author is most likely to get wrong, because a member that gets it wrong still works for whoever wrote it -- they always pass their own format. The declaration must not be gated, or the engine can never answer "which formats does this graph provide"; the submission must be, or a plain `mcpp build` grows an edge it must not have. A format nothing submitted for is refused by name rather than reported as a pack that produced no package. `mcpp pack --format <name>` PREPARES TWICE, AND NOTHING IS RE-DERIVED BETWEEN THE PASSES. An artifact action is a ninja edge and the staged tree is produced after the link, so the tree cannot be an input of the pass that built it. The first pass collects declarations and refuses an unknown format; the build and the staging follow; the second pass sets `pack_format` and `pack_stage_dir` and builds the submitted edge. Its triple and staged path come from what the first pass and `make_plan` already answered -- `stagingRoot` is a function of the resolved triple, and a second derivation of it before prepare is the shape where two answers agree on every machine the author has. build.ninja's header line gains a fourth field, `dist=`, and the fast paths require it to read `none`. The format is deliberately NOT in the fingerprint -- putting it there would cost a full recompile to package an already-built tree -- so the two graphs share a directory, and `target/<triple>/<fp>/build.ninja` is shared mutable state two fast paths replay. That is the third instance of the failure `graph=` and `accel=` each already record. The criterion is a unit test rather than an end-to-end assertion: measured on 2026-09-11, a plain build after the pack pass regenerates the graph even with the field ignored, so an end-to-end check would pass whether or not the field works and would keep passing if it were deleted. `${mcpp.stage_dir}` REFUSES rather than expanding to nothing, in two places: a build that is not packaging, and a role other than `artifact`. An empty path is still a token the command accepts, and the tool then reads the build directory root, which exists -- so the mistake produces a plausible artifact instead of a diagnostic. The measured prototype is a valid, empty, 52 KB installer with nothing said about it. An action that names the placeholder automatically gains a dependency on `<staged tree>.stage-manifest` -- a sibling, never a member, so it never travels inside anyone's installer. The engine adds it because the use implies it: without it the edge is dirty only when a link output changes, and a closure that grew a dependency's shared library while the program's own bytes did not would leave the previous distributable in place, reported as up to date. Build-program protocol v9 (`mcpp:pack-format=`). The row carries a non-empty `tag`, so the declaration is replayed from the build program's cache record -- the pass that reads the set is `mcpp pack`, which is never a project's first build, and an unpersisted declaration would be absent exactly when a user names a format. Tests: 6 unit tests for the staged-tree contract, the graph-shape field in both of its two readers, and the directive row's persistence; one e2e holding the four properties of the dispatch, each with the wrong answer it excludes. Docs: `docs/10` for the `--format` axis, `docs/30` for the three-category taxonomy and the new placeholders and accessors, `docs/31` for the six constraints a distribution member owes its consumer. Both languages. * fix(pack): an unsupported `--format` value writes nothing to stdout `mcpp pack --format bogus` must write NOTHING to stdout and exit 2. That is the machine-output contract, and 202_machine_output_contract.sh asserts it for exactly this command, because it is the path a client hits when it probes an mcpp for a capability -- the most common machine-facing failure, and the one that used to print to stdout. Moving the refusal from the CLI parser to after `prepare_build` broke it. It had to move: the set of valid values is a property of the RESOLVED GRAPH, so a refusal written in the parser could only compare against a constant, which is the coupling this whole mechanism exists to remove. But prepare narrates what it resolves, so the refusal now arrived after three lines on stdout. FAIL: unsupported value (pack) wrote to stdout: Resolving toolchain Measured on macos-arm64, and it would have failed on every platform -- the macOS shard is simply the one that reached it first. The fix is to be quiet until the value is validated, and only then. Nothing is lost when the value IS valid: the dispatch pass prepares a second time and prints the same lines, so a successful `pack --format <name>` narrates once rather than twice. `--format tar` and `--format dir` are untouched, because their values were never in question. The musl re-prepare is quieted on the same grounds: it also runs before the format has been validated. The assertion is added to 638_pack_format_dispatch.sh as well as living in 202. That is deliberate duplication: the tension is local to this feature -- the valid set needs the graph, and the graph narrates -- so the test for the feature should fail when the contract does, rather than only the general contract test noticing. * feat(target): Android, iOS and wasm are rows, and the object format is an axis Section 3 of the design record draws the boundary exactly: a package can add a language, a tool, an action, a payload and a generated module, and IT CANNOT ADD A TRIPLE. Identity is three strings and `kKnownTargets` is compiled into the binary, so every layer below the first -- the `.apk` step, the `.app` step, the `.html`+`.wasm` step, the runner, the signing, the non-C++ glue -- waits on a row here and on nothing else in the engine. Registering the rows is what turns each of those from a plugin with nowhere to attach into a plugin that can be written. aarch64-linux-android x86_64-linux-android aarch64-ios wasm32-emscripten ALL FOUR ARE `planned`, WHICH IS A REFUSAL AND NOT A GAP. The tier gate answers `tier-planned` naming the row: error: target 'aarch64-linux-android' is registered but not yet supported (planned) -- no toolchain is published for it yet error: target 'aarch64-linux-androideabi' (which resolves to 'aarch64-linux-android') is registered but not yet supported rather than `unknown target`, which was false, or a build that resolves and produces nothing, which section 3.1 argues would be worse than the row's absence. What each row still needs is a PAYLOAD in every case and never engine work: `xim:android-ndk`, `xim:emsdk`, and for iOS a licence reading before a packaging decision. THE OBJECT FORMAT IS NOW ONE ANSWER, AND THAT IS WHAT #597 ACTUALLY NEEDED. The binary format was never a field. It was re-derived from `os` wherever it was needed -- `is_pe()` asked `os == "windows"`, artifact naming asked again, the packer asked a third time -- which is affordable only while the answer has two values. `wasm32` is the first target in mcpp's vocabulary whose format is neither, and a THIRD value turns those derivations into an addition at every such site. A site that is missed does not fail: it silently answers ELF, because ELF is what every `else` branch in the tree assumes. `ObjectFormat` is that addition made once, with `is_pe()` / `is_mach_o()` / `is_wasm()` reading it. It is deliberately NOT the same question as `is_freestanding()`. A bare-metal RISC-V image is ELF with no OS; a wasm module has an OS-like layer (Emscripten's POSIX emulation) and is not ELF. Merging the two axes is the mistake this replaces. ANDROID'S PLACEMENT IS THE MODELLING DECISION: `env = "android"` on a `linux` OS, not `os = "android"`. The kernel IS Linux, so ELF, the `unix` family and `nasm -f elf64` are already right; an OS value would have made every one of them wrong by default and required a new answer at each site. What differs from `gnu` is bionic, the loader path and the SDK -- which is what an `env` value is for. `androideabi` resolves to the same env: the EABI half is the ARM calling convention, which the arch segment already carries. `is_apple()` exists because a site that means "Apple" and asks "macOS" gets iOS wrong in the direction that still links. iOS shares the object format, the linker, the `arm64` spelling and `codesign` with macOS, and differs in the SDK and the deployment-target flag. No deployment target is baked into `llvm_triple()` for it, unlike the macOS branch: `-miphoneos-version-min` belongs to the layer that owns the SDK and the bundle, and a default here would be a second place that answers it. A DISPLAY DEFECT THE ROWS EXPOSED. `x86_64-linux-android` showed no `cross` tag, because that test compared arch and OS only -- and this target agrees with an x86_64 Linux host on both. An Android artifact needs bionic's loader at `/system/bin/linker64`, which no ordinary Linux host has, so it cannot run there. Spelled as a property rather than by adding `env != env`, which would have taken `x86_64-linux-musl` with it -- that one is static and does run here. 48 new cells in tests/matrix/expected.tsv, one per (mode, host, compiler) the table declares, all `unsupported / tier-planned`; the declared per-host counts move with them, because compare.sh checks the total before it checks a cell. The day a row is wired, its cells go red and say so. Verified locally: 50/50 payload and 24/24 graph on linux-x86_64. Docs: `docs/21`'s segment tables gain the new values, a section states the object-format axis and why it is not the freestanding question, and the host/target matrix gains four rows. Both READMEs' platform tables record what each row waits on. Both languages. * fix(pack): the distributable is what the request introduced, not what names the staged tree `mcpp pack --format <name>` reported as the distributable only those artifact actions that named `${mcpp.stage_dir}`, on the assumption that a distributable consumes the staged closure. NOT EVERY FORMAT DOES, and the one that does not is the one the guidance recommends. An `.msi` built from ONE NAMED PROGRAM takes `${mcpp.target_file:<name>}` and never looks at the tree. That is `docs/31`'s "name the input, do not harvest a directory", written after a bind path that resolved to nothing produced a valid, empty, 52 KB installer with no diagnostic. So a member following that guidance failed the check, and `mcpp pack --format msi` reported no action claimed --format 'msi' after a `wix build` that had succeeded. The workaround available to the member was to declare the placeholder as an extra, otherwise-unused input purely to satisfy the engine -- which also gave it a dependency on a tree its one `File` row never reads. The property actually wanted is presence in the dispatch pass. An artifact action present whether or not a format was asked for -- a codesign stamp, a size budget -- existed before anyone asked, and reporting one as the package would be a wrong answer that looks like a right one. So the first pass's artifact actions are collected, and the dispatch reports the difference. Identity is (package, id): an id is unique within the package that declared it and nothing more. `consumesStageDir` keeps its one real job -- the implicit dependency on the staged tree's manifest, so an action that reads the tree is dirty when the staged SET changes and not only when a link output does. Its comment now says what it is not. `638_pack_format_dispatch.sh` gains the case that pins this from both sides: a fixture submitting an UNGATED artifact stamp and a GATED action that names no staged tree at all. The gated one must be reported and the stamp must not, and the stamp must still have been built -- it was simply not the answer. Verified load-bearing by removing the guard, which reports `size.stamp` as the package and fails the test. Found while porting `mcpp.dist.wix`, which is to say by writing the second member. The first one consumed the staged tree and agreed with the check by accident. * fix(target): every object-format question reads the single answer, and two copies were searching for a vendor name An audit of the 35 sites that derived the binary format from `os`. Twelve of them asked "which object format" and were converted to read `object_format()`; the rest ask a different question -- which payload to install, which loader variable a platform reads, which flag spelling a compiler wants -- and are unchanged. The classification mattered more than the count: a site that means "which OS" and is converted becomes wrong in a new way. WHAT EACH CONVERTED SITE ANSWERED FOR `aarch64-ios` BEFORE: -femulated-tls, -fvisibility-hidden not passed (Mach-O needs both) shared_library_link_flags $ORIGIN, which ld64 rejects shared_soname_flag -Wl,-soname, a BFD-only flag exports_file_contents / exports_flag a GNU version script debug_info_is_in_band true; Mach-O splits to dSYM dist::format_for the BUILD HOST's format Every one of those is the ELF branch reached by falling off the end of a two-valued test, which is the failure `ObjectFormat` was introduced to make impossible. Verified for every row in `kKnownTargets` that the answer changes only for `aarch64-ios` and `wasm32-emscripten`, and only toward correctness. TWO COPIES WERE SEARCHING FOR A VENDOR NAME IN THE WRONG STRING, AND ONE OF THEM AFFECTS AN ALREADY-VERIFIED ROW. `compute_flags`'s `linkIntentFlavor` and `resolution.json`'s `format` both derived the object format by looking for "apple" / "darwin" / "windows" / "mingw" in `plan.toolchain.targetTriple`. That string is mcpp's CANONICAL spelling, and `aarch64-macos` contains none of those words. The words live in the LLVM spelling, which is a different string -- the build report prints both, either side of an arrow: Target aarch64-macos → arm64-apple-macos14.0 ^ the identity ^ what clang is given So an explicit `--target aarch64-macos` linked and recorded as ELF. A NATIVE macOS build was right by a different branch -- an empty triple reaching the `needs_explicit_libcxx` rescue -- which is why nothing caught it: two paths through one function disagreed and only the exercised one was correct. Both now ask the parsed triple, and the substring test survives only for a spelling `parse` REJECTS, which is the `[target.<triple>]` escape hatch where an LLVM-shaped string is what an author actually wrote. `test_toolchain_triple.cpp` states this as a fact about the vocabulary rather than as a comment elsewhere: for `aarch64-macos`, `x86_64-macos` and `aarch64-ios` it asserts the canonical spelling contains neither "apple" nor "darwin", that the format is Mach-O anyway because it is asked of the fields, and that the LLVM spelling is where the vendor name lives. A second test takes its denominator from the table, so a row added without an answer cannot be covered by a test whose name says every row is. TWO GAPS ARE NAMED RATHER THAN GUESSED. `dist::Format` and `LinkIntentFlavor` have no `Wasm` member, so `wasm32-emscripten` still resolves to `Elf` in both. What "self-contained" and "link_lib" mean for an Emscripten link is a distribution-contract decision and the open half of #597, not a rename; the switch names the case so the gap is visible instead of reached by falling through. `mcpp::pack::run` now refuses a file that is neither ELF, PE nor Mach-O by name, where it previously handed anything not-PE-not-Mach-O to `LD_TRACE_LOADED_OBJECTS`. Docs: the design record's section 3.1 is corrected twice, because two of its guesses were measured wrong in the same direction -- a vendor had already done the work and nobody looked. NDK r30 ships the 133-file module surface itself (r27 shipped none), so for Android there is nothing to derive; and Apple's libc++ IS a build of a public revision (210106 -> llvmorg-21.1.6), the SDK ships no surface, and an `import std` Mach-O arm64 binary was linked from a Linux host. Section 8 names the pattern rather than only listing the facts. 109 unit tests pass. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 2d10281 commit 51dff99

45 files changed

Lines changed: 3487 additions & 102 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md

Lines changed: 916 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
274 records.
21+
275 records.
2222

2323
## By subject
2424

@@ -40,10 +40,15 @@ Records that declare one. Everything else is listed by date below.
4040

4141
- [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
4242

43+
### plugins
44+
45+
- [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
46+
4347
## By date
4448

4549
### 2026-09
4650

51+
- [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
4752
- [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
4853
- [A dlopen surface no closure walks, and a process with two unwinders](2026-09-09-dlopen-surface-and-two-unwinders.md) — landed
4954
- [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active

CHANGELOG.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,71 @@
55
66
## [Unreleased]
77

8+
## [2026.9.11.1] - 2026-09-11
9+
10+
### `mcpp pack --format <name>` 分派到包,而引擎里不再需要住进任何一种分发格式
11+
12+
`tar``dir` 回答的问题,和 `msi``appimage` 回答的问题是同一个 —— 输出取什么
13+
形状 —— 所以它们是一个 flag 的取值,而不是第二个 flag 的开端。分界是:`mcpp pack`
14+
拥有机制,以及那一种通用格式(一个解开就能跑的归档,它不需要知道任何别人的发布);
15+
其余每一种格式都住在包里。dpkg 的 control 字段、AppImage 的 runtime、WiX 的 schema、
16+
Apple 的公证,其中任何一个被绑进引擎,都会把一次 mcpp 的发布耦合到一次 mcpp 并不控制
17+
的发布上。这与本项目早已为语言做过的论证是同一个 —— Slang 被支持,而引擎里没有它的
18+
名字。
19+
20+
三样与格式无关的东西被加进引擎,「与格式无关」正是判断某样东西该不该进引擎的判据:
21+
22+
- **一棵 `artifact` action 可以消费的暂存树。** `mcpp pack` 一直在算它 —— 依赖闭包,
23+
过了 strip 策略、调试信息拆分与 `include`/`exclude` —— 然后把它压掉,目录就没了。
24+
`${mcpp.stage_dir}` 把它暴露出来。它是一个 **bundle** 树(`bin/``lib/`、可重定位),
25+
这正是 AppImage、`.app``.msi` 要的形状;要一棵 FHS 树的格式(`.deb``.rpm`)自己
26+
负责重排,因为一个文件该落在哪个目录是那个格式的知识。
27+
- **`[package]` 的其余字段进入构建程序。** `MCPP_PKG_VERSION` / `_DESCRIPTION` /
28+
`_LICENSE` / `_AUTHORS` / `_REPO`,以及对应的 `mcpp::package_*()`。每一种安装包格式
29+
都要写版本号;在这之前,项目只能把版本号在成员自己的 options 里再写一遍,而那份副本
30+
会与 `[package]` 漂移,且没有任何东西能发现。
31+
- **`--format` 经图解析它的取值。** 包用 `mcpp::provides_pack_format("<name>")` 声明,
32+
`--format <name>` 在解析后的依赖里找到提供方并把暂存树交给它。未知取值点名**当下确实
33+
可用**的那些,而不是一份固定清单,并且这次拒绝发生在任何东西被编译之前。
34+
35+
**无条件声明,有条件提交。** 这是整套分派最承重的一条规则,也是最容易被成员作者写错
36+
的一条 —— 因为写错了对作者自己仍然照常工作:他永远传的是自己那个格式。声明必须不加闸,
37+
否则引擎永远回答不出「这张图提供哪些格式」;提交必须加闸,否则普通 `mcpp build` 会多出
38+
一条它不该有的边。对一个谁都没为之提交的格式,mcpp 会拒绝并点名,而不是报告一次「什么
39+
包都没产出」的成功打包。
40+
41+
**`mcpp pack --format <name>` 会 prepare 两次,而两次之间没有任何值被重新推导。** 一条
42+
`artifact` action 是一条 ninja 边,而暂存树是 mcpp 在链接**之后**产出的,所以这棵树不
43+
可能成为构建出它自己那一次 pass 的输入。第一趟收集声明并拒绝未知格式;随后是构建与暂存;
44+
第二趟设上 `pack_format``pack_stage_dir` 并构建提供方提交的那条边。第二趟用的三元组
45+
与暂存路径,都是第一趟和 `make_plan` 已经回答过的 —— 重新推导一遍会得到那种「在作者所有
46+
机器上都一致、只在他没有的那台上不一致」的缺陷。
47+
48+
**build.ninja 的头行多了第四个字段 `dist=`,而快路径要求它读作 `none`** 格式故意
49+
**不进指纹**:进了就要为「把一棵已经构建好的树打成包」付一次全量重编。于是两张图落在同
50+
一个目录里,而 `target/<triple>/<fp>/build.ninja` 是被两条快路径回放的共享可变状态 ——
51+
这正是这一行上另外两个字段(`graph=``accel=`)已经各自记过一次的那种失败。判据放在
52+
单元测试里而不是端到端:实测(2026-09-11)即使忽略这个字段,pack 之后的普通构建也会因为
53+
更早的一个新鲜度条件而重新生成图,所以端到端断言无论字段是否生效都会通过,连字段被删掉
54+
都照样通过。
55+
56+
**`${mcpp.stage_dir}` 在两种位置上是拒绝而不是空展开:** 本次构建不在打包时,以及
57+
role 不是 `artifact` 时。一个空路径仍然是命令接受的 token,而工具随后读到的是构建目录
58+
根 —— 那个目录存在,所以这个错误会产出一个看起来合理的产物而不是一条诊断。实测过的原型
59+
是那个「有效的、空的、52 KB 的安装包,并且没有任何诊断」。
60+
61+
写了 `${mcpp.stage_dir}` 的 action 会自动获得一条对 `<暂存树>.stage-manifest` 的依赖
62+
(一个兄弟文件,永不是成员,所以它不会跑进任何人的安装包里)。依赖由引擎添加,因为「用
63+
了」本身就意味着「依赖」:没有它,这条边只在链接产物变化时才变脏,而一个闭包多出了某个
64+
依赖的共享库、同时程序自己的字节没变的情况,会把上一次的可分发物原地留下并报告为最新。
65+
66+
构建程序协议升到 v9(`mcpp:pack-format=`)。这条指令带非空 `tag`,因此会随构建程序的
67+
缓存记录一起被回放 —— 读它的那一趟是 `mcpp pack`,而那从来不是一个项目的第一次构建。
68+
69+
文档:`docs/10`(`--format` 那一个轴)、`docs/30`(三类成员的分类表、新占位符与新访问器)、
70+
`docs/31`(分发成员的六条约束),中英双份。
71+
72+
873
## [2026.9.10.2] - 2026-09-10
974

1075
### dlopen 面检查:不适用的那一趟也会发布记录,并且不会盖掉已经量出来的答案

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,9 @@ list` reports for this machine):
419419
| `aarch64-none-elf` · `x86_64-none-elf` | llvm 22 — bare metal, no C library by default ² | preview |
420420
| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview |
421421
| `riscv64-linux-musl` · `aarch64-linux-gnu` · `x86_64-macos` || planned |
422+
| `aarch64-linux-android` · `x86_64-linux-android` | needs `xim:android-ndk`; `import std` measured working on the NDK's clang | planned |
423+
| `aarch64-ios` | needs the iPhoneOS SDK, which is a licence question before it is a packaging one | planned |
424+
| `wasm32-emscripten` | needs `xim:emsdk`; `import std` measured working on `em++`, and the target model is [#597](https://github.com/mcpp-community/mcpp/issues/597) | planned |
422425

423426
`verified` an image has been built **and run** for the row, qemu and wine
424427
included · `preview` it builds and links, and no emulator run has been recorded

README.zh-CN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,9 @@ mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family
406406
| `aarch64-none-elf` · `x86_64-none-elf` | llvm 22——裸机,默认不带 C 库 ² | preview |
407407
| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22——Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview |
408408
| `riscv64-linux-musl` · `aarch64-linux-gnu` · `x86_64-macos` || planned |
409+
| `aarch64-linux-android` · `x86_64-linux-android` |`xim:android-ndk`;`import std` 在 NDK 自带的 clang 上已实测可用 | planned |
410+
| `aarch64-ios` | 待 iPhoneOS SDK,而它先是一个许可问题再是一个打包问题 | planned |
411+
| `wasm32-emscripten` |`xim:emsdk`;`import std``em++` 上已实测可用,目标模型见 [#597](https://github.com/mcpp-community/mcpp/issues/597) | planned |
409412

410413
`verified` 该行的镜像已被构建**并运行**过,qemu 与 wine 都算 · `preview` 可构建
411414
可链接,未记录过模拟器运行 · `planned` 已登记在词表中,尚未接线 —— 面向这类目标

docs/10-pack-and-release.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,55 @@ mcpp pack --mode self-contained # alias: --mode bundle-all
140140
mcpp pack --target x86_64-linux-musl # equivalent to --mode static
141141
mcpp pack --target aarch64-linux-musl # ARM64 equivalent
142142
mcpp pack --format dir # output as a directory, no tarball
143+
mcpp pack --format appimage # a format a package in the graph provides
143144
mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz
144145
mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path
145146
mcpp pack --profile dev # build with a different profile (default: release)
146147
mcpp pack --no-strip # ship the artifacts as built
147148
mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/
148149
```
149150

151+
### `--format` owns one axis, and the engine owns two of its values
152+
153+
`tar` and `dir` answer the same question `msi` and `appimage` answer — what
154+
shape does the output take — so they are values of one flag rather than the
155+
beginning of a second one. The split between what the engine holds and what a
156+
package holds is:
157+
158+
> **`mcpp pack` owns the mechanism and the one universal format. Every other
159+
> format lives in a package, and `mcpp pack` dispatches to it.**
160+
161+
The universal format is what it already produces: an archive that extracts and
162+
runs. It is universal in the only sense that matters here — it needs no
163+
knowledge of anyone else's release. Everything past it does. dpkg's control
164+
fields, AppImage's runtime, WiX's schema, Apple's notarisation, Android's
165+
signing scheme: each one bound into the engine would couple an mcpp release to
166+
a release mcpp does not control. The same argument the project already made for
167+
languages, where Slang is supported without being named in the engine.
168+
169+
So the value set is open (mcpp 2026.9.11.1+). `--format <name>` finds the
170+
package in the resolved graph that declares `<name>` and hands it the staged
171+
tree; an unknown value names what *is* available rather than a fixed list:
172+
173+
```
174+
error: unknown --format 'bogus'.
175+
available in this build: tar, dir, appimage
176+
A format past `tar` and `dir` comes from a package in the resolved graph, which declares
177+
it with `mcpp::provides_pack_format("<name>")` in its build program. Add the package
178+
that provides 'bogus' to [build-dependencies] and activate its feature.
179+
```
180+
181+
The refusal arrives before anything is compiled. Writing such a package is
182+
[Producing a distributable](30-build-mcpp.md#producing-a-distributable-pack_format--stage_dir-20269111);
183+
the engine's three additions are a staged tree an `artifact` action can consume,
184+
the rest of `[package]` in the build program, and this dispatch. Each is
185+
format-neutral, which is the test for whether something belongs in the engine
186+
at all.
187+
188+
A dispatched format applies to a **program** target. A library package ships an
189+
interface plus prebuilt binaries per triple and has no single staged tree, so
190+
`mcpp pack <lib> --format <name>` is refused rather than ignored.
191+
150192
When `-o` is given a bare filename, the output is placed under `target/dist/`;
151193
when it includes a directory (relative or absolute), the literal path is used.
152194

@@ -421,8 +463,13 @@ macOS **program** bundling (the Mach-O dependency closure, via `otool -L` /
421463
`LC_LOAD_DYLIB`, and `install_name_tool` for relocation) is still on the
422464
roadmap; until it lands `mcpp pack <program>` refuses on that format rather than
423465
producing something that only looks like a bundle. Windows DLL bundling beyond
424-
the current `.zip`, and distribution formats such as `.deb` / `.rpm` / AppImage,
425-
are also on the roadmap. This document evolves alongside the
426-
`mcpp pack` implementation; for the latest options, refer to
427-
`mcpp pack --help`.
466+
the current `.zip` is also on the roadmap.
467+
468+
Distribution formats such as `.deb`, `.rpm`, AppImage and `.msi` are **not** on
469+
this list, and that is a decision rather than an omission: they live in
470+
packages and reach the user through `--format <name>`, for the reason the
471+
section above gives. Nothing further needs to join `[pack]`'s built-in modes.
472+
473+
This document evolves alongside the `mcpp pack` implementation; for the latest
474+
options, refer to `mcpp pack --help`.
428475

docs/21-the-target-triple.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,26 @@ and the build reports what it resolved.
3838

3939
| Segment | Content | Example |
4040
|---|---|---|
41-
| `arch` | instruction set | `x86_64`, `aarch64`, `riscv64` |
42-
| `os` | operating system, or `none` | `linux`, `windows`, `macos`, `none` |
43-
| `env` | see below — it is a different axis per platform | `gnu`, `musl`, `msvc`, `elf` |
41+
| `arch` | instruction set | `x86_64`, `aarch64`, `riscv64`, `wasm32` |
42+
| `os` | operating system, or `none` | `linux`, `windows`, `macos`, `ios`, `emscripten`, `none` |
43+
| `env` | see below — it is a different axis per platform | `gnu`, `musl`, `msvc`, `android`, `elf` |
4444

4545
The third segment is the one that repays attention, because it does not name
4646
the same kind of thing everywhere:
4747

4848
| Platform | `env` names | Values |
4949
|---|---|---|
50-
| `linux` | the **C library** | `gnu` (glibc), `musl` |
50+
| `linux` | the **C library** | `gnu` (glibc), `musl`, `android` (bionic) |
5151
| `windows` | the **object ABI** | `gnu` (Itanium C++ ABI), `msvc` (Microsoft's) |
5252
| `none` | the **object format** | `elf` |
53-
| `macos` | nothing; the platform carries no segment ||
53+
| `macos`, `ios`, `emscripten` | nothing; the platform carries no segment ||
54+
55+
`android` is a **C library** and therefore sits where `musl` sits, on a `linux`
56+
OS. That placement is the whole of the modelling decision: the kernel *is*
57+
Linux, so ELF, the `unix` family and `nasm -f elf64` are already right, and an
58+
`os = "android"` would have made every one of them wrong by default and needed
59+
a new answer at each site. What differs from `gnu` is bionic, the loader path
60+
and the SDK — which is exactly what an `env` value is for.
5461

5562
On Windows the segment is frequently misread, because the word `gnu` suggests a
5663
C library that is not there. Measured on an artefact built for
@@ -68,6 +75,30 @@ openkal. `gnu` is LLVM's label for the non-MSVC ABI, inherited from MinGW, and
6875
clang requires that spelling to select the right internal toolchain. mcpp
6976
cannot rename it.
7077

78+
### The object format is an axis, not a derivation
79+
80+
A triple's binary format used to be nothing at all: it was re-derived from `os`
81+
wherever it was needed. `is_pe()` asked `os == "windows"`, artifact naming asked
82+
again, the packer asked a third time. That is affordable while the answer has
83+
two values.
84+
85+
`wasm32` is the first target in mcpp's vocabulary whose format is neither, and a
86+
third value turns those derivations into an addition **at every such site** — and
87+
a site that is missed does not fail. It silently answers ELF, because ELF is
88+
what every `else` branch in the tree assumes. So the format is now one answer:
89+
90+
| target | format |
91+
|---|---|
92+
| `x86_64-linux-gnu`, `aarch64-linux-android`, `riscv64-none-elf` | ELF |
93+
| `aarch64-macos`, `aarch64-ios` | Mach-O |
94+
| `x86_64-windows-gnu`, `x86_64-windows-msvc` | PE |
95+
| `wasm32-emscripten` | wasm |
96+
97+
It is **not** the same question as "is there an operating system to link
98+
against". A bare-metal RISC-V image is ELF with no OS; a wasm module has an
99+
OS-like layer (Emscripten's POSIX emulation) and is not ELF. Merging the two
100+
axes is the mistake this replaces.
101+
71102
## Declining The Third Segment
72103

73104
`<arch>-<os>` is a complete target on every platform:
@@ -444,6 +475,10 @@ other's rows.
444475
| `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | payload | payload | payload | payload |
445476
| `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | payload | payload | payload | payload |
446477
| `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | payload | payload | payload | payload |
478+
| `aarch64-linux-android` | planned || planned | planned | planned | planned |
479+
| `x86_64-linux-android` | planned || planned | planned | planned | planned |
480+
| `aarch64-ios` | planned || planned | planned | planned | planned |
481+
| `wasm32-emscripten` | planned || planned | planned | planned | planned |
447482

448483
`payload` a toolchain payload here produces it · `graph` no payload, but a
449484
dependency can supply the system · `system` located on the machine, not

0 commit comments

Comments
 (0)