Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .agents/docs/2026-11-12-three-plane-platform-architecture-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
subject: architecture
status: active
---

# The three-plane platform architecture: mcpp × openkal × xim

Date: 2026-11-12
Status: reference design (a synthesis; nothing in this document adds engine
behaviour — it names what the merged changes established and what the next
systems consume)

## 1. The statement

A platform enters this ecosystem as **data**, never as engine code. Three
data planes carry everything a target needs, and one normalisation layer
decides whether what was built is right:

| Plane | Carries | Where it lives | Invariant |
|---|---|---|---|
| ① row | serves/hosts/toolchain/sysroot/linkage/abi of a target identity | the toolchain model's table, or a platform package's own rows | target differences are data; the engine holds no per-target branch |
| ② payload | the toolchain bytes, and the base package that is the target's C library | the index (xim), installed by the fetcher through the revision channel | a build host needs mcpp itself and nothing else; where a row names a base package the host's link model yields entirely |
| ③ runner | how an artifact of another system starts (a declared prefix, an emulator, a loader) | row/platform data; resolved by the runner lookup | build never depends on a runner; running consumes the declaration |
| verification | one script, one expected output, normalised (CRLF, colour, codeset, console noise) | the shared harness | the harness knows no target; every executor family answers through it |

Two red lines hold throughout, and the gates hold them mechanically:
default behaviour is unchanged by any of it (the regressions stay green), and
no host name enters the engine — host differences are statements rows make.

## 2. The layering the planes serve

```mermaid
flowchart TB
subgraph consumer["a program of any system"]
A["import the C++ modules / C sources"]
end
subgraph libc["one C library, ported once"]
M["libc above the kernel ABI (spec)</br>static where the row says static"]
end
subgraph kal["the kernel-ABI specification"]
K["openkal: the interface, its conformance suite</br>and one implementation per system family"]
end
R1["row ① + payload ②"] --> A
M --> A
K --> M
KF["firmware implementations</br>(UEFI boot services, RISC-V SBI)"] --> K
KP["kernel implementations</br>(the reference kernel's own interface,</br>a BSD kernel's, a Windows kernel's)"] --> K
KX["a POSIX-library implementation</br>(for systems whose own libc is the interface)"] --> K
R3["runner ③"] -. runs the artifact on.-> consumer
```

The specification's own rules make the layering safe: an implementation may
declare an interface absent (the conformance suite decides which absences are
legal), a C library above the interface never sits above an implementation
that would call back into it (layer-cycle refuses that graph at resolution),
and a program states which ABI it speaks by depending on one.

## 3. What the merged round established

Measured, not aspirational (each item landed with its gate green):

1. **Rows as the only place targets differ.** Eighteen identities build from
one engine with zero per-target code; the identity that was missing
arrived as one table row plus one base package.
2. **Base packages.** Where a row names one, the payload is the target's C
library: the driver gets `--sysroot`, the payload's own `-B/-L`, a
loader folded sysroot-relative, and the compiler's runtime directory —
and the host's link words, runtime directories and closure rules yield
entirely. A cross needs nothing from the machine it stands on.
3. **Foreign-kernel execution.** An artifact of another system, of this
machine's architecture, runs as a child: its death by signal is named,
its status returned unchanged, and the runner declaration follows it.
4. **ABI members as data.** `abi longjmp` states how a jump crosses the
WebAssembly boundary; the probe renders the flag on both sides, and a
row that is not the member's subject is refused rather than ignored.
5. **Corrective revisions.** A resource whose bytes must change without a
version change is re-published under the same key with a raised
revision; the updater channel enforces that a raise is total — every
platform, one sha per url, shapes compared.
6. **One file, several systems.** A pack can hold the programs of several
systems: a PE leads with a POSIX script in its DOS header, and every
POSIX system's program rides behind it.
7. **Layer vocabulary.** `kernel-abi` is a layer the rows, the payloads and
the requirement side all speak (`layers.kernel-abi`, `requires`), an
implementation written above a C library never sits beneath one it would
call back into — the resolution refuses that graph before anything is
compiled — and a payload's kernel-abi is the target's system, so a
requirement stated on it holds across every route to the row.

## 4. What the next systems consume

The same planes, with no engine work:

| Scenario | What it uses | What is left to build |
|---|---|---|
| A POSIX-family kernel with its own libc | the POSIX-library implementation of the ABI (already above every such libc), rows, a cross toolchain payload | the toolchain payload; a disk-image asset; an emulator runner |
| Firmware (UEFI, SBI) | the existing firmware implementations; an ordinary PE row and three link flags state a UEFI application — measured, no new target kind; a runner's exit status is the program's (the contract already carries firmware: the harness states the status) | an OVMF/emulator runner entry |
| A system's official media, booted | the disk-image asset class (`<system>-disk-image`): official media assembled into a bootable image, serial console, a marker protocol that separates the program's output from the machine's noise, exit codes carried through | the generic emulator harness (overlay isolation, expect/send primitives, the marker protocol); the naming and attribution conventions |
| Freestanding and bare-metal targets | the freestanding rows; a C-only, no-module path | the conformance hook-ups |
| Any corrective republish | the revision channel | nothing |

The one boundary this architecture states out loud: the **semantic plane** —
a process model, a terminal — is the program's own. The infrastructure
answers that a target can be built, started and judged; it does not port the
program.

## 5. Naming (index side)

- Generic assets and toolchains carry no prefix; a tool whose contract is a
build-system session carries that system's prefix.
- A bootable-image asset is named `<system>-disk-image` — the thing, not a
mechanism, not a format; the asset's file names carry the format.
- Official media is attributed to its publisher in the recipe; repacked
bytes are provenance-recorded, double-mirrored, sha-identical.

These are conventions of the index, not engine vocabulary; the engine knows
none of the names.
7 changes: 6 additions & 1 deletion .agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```

358 records.
359 records.

## By subject

Records that declare one. Everything else is listed by date below.

### architecture

- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — active

### build

- [大型工作区的构建空间与复用(mcpp#874)](2026-10-31-workspace-space-and-reuse-design.md) — active
Expand Down Expand Up @@ -188,6 +192,7 @@ Records that declare one. Everything else is listed by date below.

### 2026-11

- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — active
- [测试与依赖解析补充方案:mcpp、mcpp test、mtest、mcpp-ide](2026-11-10-test-and-resolution-supplement-design.md) — active
- [生态方案:跨平台通用基础设施(mcpp + openkal + xim),以一个 C89 工程为验证](2026-11-10-cross-platform-ecosystem-design.md) — active
- [目标与提供者:统一的目标模型(结合 mcpp#911)](2026-11-09-target-model-design.md) — active
Expand Down
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,70 @@
> Each `## [<version>]` section is that release's notes. Entries are written in English
> from 2026.9.28.3 on; earlier entries remain as written.

## [2026.11.12.1] - 2026-11-12

### Added

- The cross rows of the platform table are served from what their own data
names, not from a list of hosts written in the engine. `aarch64-linux-gnu`
takes its C library from the sysroot payload its row declares
(`xim:aarch64-linux-gnu-sysroot`, glibc 2.44 with the kernel headers,
libgcc and libc++ inside; the package root is the sysroot), so the row is
built wherever that payload is published — an x86_64 Linux host included —
and `host_can_serve` reads the declaration instead of refusing every
arch-crossing glibc target. The row links its GCC run-time in, as the musl
rows do: that sysroot carries no shared libgcc. When such a base package
serves the target, the host contributes nothing to the link: the payload's
own `-B`/`-L`/loader words and its `compiler_runtime_dir` resource
directory are the whole C-runtime group, on the C link exactly as on the
C++ one (before, the host's runtime directories and `-latomic` shadowed
the target's — an x86_64 `libatomic.a` the linker then called incompatible
with aarch64 — and a C link could not find the payload's builtins at all),
and the runtime-closure rules state the premise they always had: the
artifact's loader is the payload's, a process on this machine is not the
one that would load it. The test that holds the row skips itself until the
package index carries the payload.
- `longjmp` joins `threads` and `exceptions` as a member of
`[target.<selector>.abi]`: how `sigsetjmp`/`siglongjmp` cross the
WebAssembly boundary on an Emscripten row. `"wasm"` renders
`-sSUPPORT_LONGJMP=wasm` on every C and C++ translation unit and on the
link; `"js"` states the toolchain's default model the same way. Declared
on any other row the member is refused naming that fact. A program whose
non-local exits run deep still wants `-sSTACK_SIZE` — a project knob of
its own, not an ABI fact, so the engine does not render it.
- `mcpp pack --systems all` answers with data rather than a fixed list: this
machine's system, every system a platform defines a row of for the
architecture, and every system the platform's runner states a host of
(`runner.hosts`) — FreeBSD's among them. A FreeBSD runner is built as
`<arch>-freebsd-musl` over the platform's hosted arrangement, the same
static identity a FreeBSD request takes. The default pack is unchanged:
without `--systems` it carries the runner of the packing machine's system
alone.
- A Windows row whose system the graph supplies compiles its resources with
the Windows headers the platform's rows declare. `x86_64-windows-musl` has
no paired payload and its llvm binary carries no headers of its own, so a
script including `"winres.h"` failed in llvm-rc; the MinGW-w64 sysroot
payload a row declares for the architecture's GNU-ABI identity supplies
them, and every PE row shares that set. The two existing include sources
keep their precedence.
- The llvm line moves to `llvm@23.1.3-r2`: the same payload with the MinGW
cross builtins beside it, repackaged under its own version key because the
index keeps an existing version's entry immutable. A plain C link for
`aarch64-windows-gnu` now needs no second payload.

### Changed

- A program of another system's kernel interface that starts and dies at
once is told apart from a crash in a working program. `mcpp run` of such
an artifact — a static FreeBSD ELF of the host's own architecture, spawned
by a kernel that does not ask which system it names — now runs it as a
child, so the death by signal is observed and named with the
`[target.<triple>]` runner declaration, and the status itself is returned
unchanged: 139 stays 139. A spawn refusal whose errno names only a missing
interpreter (ENOENT, a dynamic linker of the other system) carries the
same declaration. Native targets keep the launch they always had, and a
native signal death is never interpreted.

## [2026.11.10.2] - 2026-11-10

### Added
Expand Down
11 changes: 11 additions & 0 deletions docs/10-pack-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,17 @@ names no `windows-aarch64`, so `aarch64-windows-gnu`, a row this mcpp builds,
gets no runner from it). An image built on a Windows host is not the bytes of one built
elsewhere (#850): pack trees on Linux or macOS.

**What `all` names is data, not a list (mcpp 2026.11.12.1+).** The machine's
own system, every system a platform defines a row of for the architecture, and
every system the platform's runner states a host of for it (`runner.hosts`):
a platform whose runner exists for `freebsd-<arch>` gets FreeBSD's runners,
built as `<arch>-freebsd-musl` over the platform's hosted arrangement — the
same static identity a FreeBSD request takes — into
`libexec/<runner>/freebsd-<arch>/`. A pack without a platform answers with
the machine's own system alone, and `--systems` naming an `<os>` or an
`<os>-<arch>` keeps its exact meaning; only an explicit `--systems all` (or
`[pack] systems`) ever packs another system.

**Where it runs.** The pack writes `runs_on` into `mcpp-package.json` and says
it after `Packed`: `self`, the `<os>-<arch>` pairs the package runs on with
what it carries, and `with.<runner>`, the pairs it runs on where the machine
Expand Down
12 changes: 11 additions & 1 deletion docs/22-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -1363,8 +1363,9 @@ package needs.
|---|---|---|---|
| `threads` | boolean | `-pthread` on a target that is neither PE nor freestanding; nothing on PE and on freestanding targets | the standard library module prebuild, the dependency scan, every C and C++ translation unit of every package, and the link |
| `exceptions` *(mcpp 2026.9.12.3+)* | boolean, or `"wasm"` / `"js"` *(2026.10.30.1+)* | on `os = "emscripten"` only, on the compile line through the dialect flags and on the link: `"wasm"` is `-fwasm-exceptions`, WebAssembly's own exception handling; `"js"` and `true` are `-fexceptions`, the JavaScript-based model. Nothing on every other target, where exceptions are already the default | the same set `threads` reaches |
| `longjmp` *(mcpp 2026.11.12.1+)* | `"wasm"` or `"js"` | on `os = "emscripten"` only, on the compile line of every C and C++ translation unit and on the link: `"wasm"` is `-sSUPPORT_LONGJMP=wasm`, WebAssembly's own stack switching; `"js"` is `-sSUPPORT_LONGJMP=emscripten`, the JavaScript model and the toolchain's default. Declared on any other row, it is refused. An absent member renders nothing | the same set `threads` reaches |

Both members enter the dependency cache key through the dialect flags, so a
All three members enter the dependency cache key through the flags they render, so a
dependency built without one is never reused by a build with it, and one built
with one exception model is never reused by a build with the other. An unknown
member, and a value outside the table, are refused naming the values.
Expand All @@ -1387,6 +1388,15 @@ enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or
-sEXCEPTION_CATCHING_ALLOWED=[..] to catch.)
```

**`longjmp = "wasm"` is what makes C's `sigsetjmp`/`siglongjmp` work under
Emscripten.** With the toolchain's default JavaScript model the pair compiles
into a stack copy, and a non-local exit over two call levels reads past the
module's stack and dies at run time. With `-sSUPPORT_LONGJMP=wasm` on both the
compiles and the link, the pair round-trips. A program whose non-local exits
run deep also wants a bigger stack; that is a project knob of its own
(`ldflags = ["-sSTACK_SIZE=..."]`), not an ABI fact, so the engine does not
render it.

**Only the root manifest decides.** The switch belongs to the artefact, and the
root is the only package that builds one. A dependency that writes
`[target.<selector>.abi]` is reported (`abi/dependency-table`) and changes
Expand Down
1 change: 1 addition & 0 deletions docs/zh/22-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@ WebAssembly 上,每个目标文件、预编译的标准库模块与链接必
|---|---|---|---|
| `threads` | 布尔 | 在既非 PE 也非 freestanding 的目标上为 `-pthread`;在 PE 与 freestanding 目标上不产生任何 flag | 标准库模块的预构建、依赖扫描、所有包的每个 C 与 C++ 翻译单元,以及链接 |
| `exceptions` *(mcpp 2026.9.12.3+)* | 布尔,或 `"wasm"` / `"js"` *(2026.10.30.1+)* | 只在 `os = "emscripten"` 上,经方言 flag 进入编译行,也进入链接行:`"wasm"` 为 `-fwasm-exceptions`,即 WebAssembly 自身的异常处理;`"js"` 与 `true` 为 `-fexceptions`,即基于 JavaScript 的模型。在其余每个目标上什么都不产生,因为那里异常本来就是默认开启的 | 与 `threads` 相同的那一套 |
| `longjmp` *(mcpp 2026.11.12.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `threads` 相同的那一套 |

两个成员都经由方言 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会
被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "2026.11.10.2"
version = "2026.11.12.1"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
18 changes: 16 additions & 2 deletions modules/manifest/src/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -4481,14 +4481,28 @@ std::expected<Manifest, ManifestError> parse_document(const t::Document& documen
"[target.{}].abi must be a table, e.g. `[target.{}.abi]` "
"with `threads = true`", triple, triple)));
for (auto& [ak, av] : ait->second.as_table()) {
if (ak != "threads" && ak != "exceptions")
if (ak != "threads" && ak != "exceptions" && ak != "longjmp")
return std::unexpected(error(origin, std::format(
"[target.{}.abi] has no member '{}'; the members are: "
"threads, exceptions", triple, ak)));
"threads, exceptions, longjmp", triple, ak)));
// `exceptions` also takes the model on Emscripten (design
// 2026-10-29 §6): "wasm" or "js"; `true` keeps "js".
const bool model = ak == "exceptions" && av.is_string()
&& (av.as_string() == "wasm" || av.as_string() == "js");
// `longjmp` is a model only (2026.11.12.1): "wasm" or
// "js", no boolean -- the engine's default is the
// toolchain's own ("js"), and an absent member is that
// default rather than a switch turned off.
if (ak == "longjmp") {
if (!av.is_string()
|| (av.as_string() != "wasm" && av.as_string() != "js"))
return std::unexpected(error(origin, std::format(
"[target.{}.abi].longjmp must be \"wasm\" or \"js\"",
triple)));
cc.abiLongjmpModel = av.as_string();
cc.abiLongjmpDeclared = true;
continue;
}
if (!av.is_bool() && !model)
return std::unexpected(error(origin, ak == "exceptions"
? std::format("[target.{}.abi].exceptions must be true, false, "
Expand Down
Loading
Loading