From 495edd5a6c85efaed509fcce50d288cdc596ddee Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 04:45:36 +0900 Subject: [PATCH 01/30] 2026.11.11.1: five rows the platform's own data serves The cross rows read their payloads, not a host list: aarch64-linux-gnu takes its C library from the sysroot payload its row declares, and when such a base package serves the target the host contributes nothing to the link -- the base's own -B/-L words, its loader named sysroot- relative, its compiler_runtime_dir resource directory on the C link exactly as on the C++ one, and --sysroot naming the package root the payload's own manifest states it is. host_can_serve reads the declaration instead of refusing every arch-crossing glibc target; the runtime-closure rules state the premise they always had (the loader is the payload's; a process on this machine is not the one that would load it). Measured end to end: a plain C program builds as a dynamic aarch64 ELF and runs under qemu-aarch64 -L with its exit code intact (e2e 1076). A row whose system the graph supplies compiles its resources with the Windows headers the rows declare (e2e 1075); a program of another system that dies at once names the runner and returns its status unchanged (1077); pack --systems all answers with the platform's own data, FreeBSD's runner among them (1078, 986); longjmp joins threads and exceptions as the third abi member, the Emscripten long-jump model (1079). Stability-neutral: every default behavior is unchanged; each capability appears only where it previously failed. No host is named in the engine. --- CHANGELOG.md | 64 +++++++++++ README.md | 2 +- README.zh-CN.md | 2 +- docs/01-getting-started.md | 6 +- docs/10-pack-and-release.md | 11 ++ docs/20-toolchains.md | 24 ++-- docs/22-target-side.md | 12 +- mcpp.toml | 8 +- modules/manifest/src/toml.cppm | 18 ++- modules/manifest/src/types.cppm | 14 ++- modules/toolchain-model/data/builtin.toml | 16 ++- modules/versioning/src/version.cppm | 2 +- src/build/execute.cppm | 36 +++++- src/build/flags.cppm | 68 ++++++++--- src/build/prepare/base.cpp | 15 +++ src/build/prepare/config.cpp | 4 + src/build/prepare/toolchain.cpp | 25 ++++ src/build/prepare/windows_resources.cpp | 45 +++++++- src/build/resources.cppm | 24 +++- src/build/runner_lookup.cppm | 83 +++++++++++++- src/build/runtime_validation.cppm | 12 ++ src/pack/pack.cppm | 43 +++++-- src/pack/pipeline.cppm | 4 +- src/project.cppm | 13 ++- src/toolchain/registry.cppm | 22 +++- ..._windows_row_finds_its_resource_headers.sh | 62 ++++++++++ ..._gnu_cross_comes_from_a_sysroot_payload.sh | 64 +++++++++++ ...stem_that_dies_at_once_names_the_runner.sh | 44 +++++++ ...rries_the_runners_the_platform_declares.sh | 57 +++++++++ ...scripten_rows_longjmp_model_round_trips.sh | 108 ++++++++++++++++++ ...single_files_cover_the_legs_and_systems.sh | 22 +++- tests/e2e/run_all.sh | 10 +- tests/fixtures/target-rows.snapshot | 6 +- tests/unit/test_pack_modes.cpp | 11 +- tests/unit/test_runner_lookup.cpp | 45 ++++++++ tests/unit/test_toolchain_triple.cpp | 9 +- 36 files changed, 936 insertions(+), 75 deletions(-) create mode 100755 tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh create mode 100755 tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh create mode 100755 tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh create mode 100755 tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh create mode 100755 tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ca51646..81913d6a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.11.1] - 2026-11-11 + +### 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..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 + `-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.]` 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 diff --git a/README.md b/README.md index 466fd4e08..70e60b194 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ list` reports for this machine): | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23 — Cortex-A 32-bit, the first row with an MMU ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23 — bare metal, no C library by default ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | | `aarch64-windows-gnu` | llvm 23 with `xim:mingw-w64-sysroot` (llvm-mingw's target side) — built on any host; the same base serves `x86_64-windows-gnu` where no gcc payload exists | preview | | `x86_64-freebsd` · `aarch64-freebsd` | the platform that serves them, over `xim:-freebsd-sysroot` ([22](docs/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23 — a static FreeBSD program with a musl C library over openkal-freebsd, built on any host *(what `-freebsd` is with no platform chosen)* | preview | diff --git a/README.zh-CN.md b/README.zh-CN.md index cb61423a5..92083e472 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -402,7 +402,7 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`( | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23;Cortex-A 32 位,第一个带 MMU 的目标 ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23;裸机,默认不带 C 库 ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | | `aarch64-windows-gnu` | llvm 23 加 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧);任意宿主可构建;没有 gcc 载荷之处同一基座也服务 `x86_64-windows-gnu` | preview | | `x86_64-freebsd` · `aarch64-freebsd` | 服务它们的平台,基于 `xim:-freebsd-sysroot`([22](docs/zh/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23;openkal-freebsd 之上带 musl C 库的静态 FreeBSD 程序,任意宿主可构建 *(未选平台时 `-freebsd` 即此身份)* | preview | diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md index ce92416d4..84fbadba4 100644 --- a/docs/01-getting-started.md +++ b/docs/01-getting-started.md @@ -42,10 +42,10 @@ the host: | host | default | |---|---| | Linux x86_64 | `gcc@16.1.0` | -| Linux aarch64 | `llvm@23.1.3` | +| Linux aarch64 | `llvm@23.1.3-r2` | | other Linux architectures | `gcc@15.1.0-musl` | -| macOS | `llvm@23.1.3` | -| Windows with usable MSVC | `llvm@23.1.3` | +| macOS | `llvm@23.1.3-r2` | +| Windows with usable MSVC | `llvm@23.1.3-r2` | | Windows without it | `gcc@16.1.0` for `x86_64-windows-gnu` | The Linux aarch64 default applies from 2026.10.8.1. Existing configured diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 1df48eea8..e0fb56e5a 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -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.11.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-` gets FreeBSD's runners, +built as `-freebsd-musl` over the platform's hosted arrangement — the +same static identity a FreeBSD request takes — into +`libexec//freebsd-/`. A pack without a platform answers with +the machine's own system alone, and `--systems` naming an `` or an +`-` 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 `-` pairs the package runs on with what it carries, and `with.`, the pairs it runs on where the machine diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index f4db148ee..c699ae62e 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -24,19 +24,19 @@ host-aware: - Linux x86_64 uses `gcc@16.1.0` for the native glibc ABI, so X11, OpenGL, and system libraries work out of the box. -- Linux aarch64 uses `llvm@23.1.3` for the native glibc ABI (2026.10.8.1+). +- Linux aarch64 uses `llvm@23.1.3-r2` for the native glibc ABI (2026.10.8.1+). - Other Linux architectures use `gcc@15.1.0-musl`, a self-contained static toolchain. -- macOS uses `llvm@23.1.3`. -- Windows with a usable MSVC installation uses `llvm@23.1.3` for the MSVC ABI. +- macOS uses `llvm@23.1.3-r2`. +- Windows with a usable MSVC installation uses `llvm@23.1.3-r2` for the MSVC ABI. Without usable MSVC, it uses `gcc@16.1.0` with target `x86_64-windows-gnu` (MinGW-w64, static by default), says which half of the MSVC ABI is missing — Visual Studio's C++ tools or the Windows SDK — and where it looked, and records the pair as the machine's default. Once both halves - are present, `mcpp toolchain default llvm@23.1.3` moves the machine to the + are present, `mcpp toolchain default llvm@23.1.3-r2` moves the machine to the MSVC ABI. - Android (Termux) uses the default of GNU/Linux on its architecture: on - aarch64, `llvm@23.1.3` with the managed glibc. Android 10 or later is + aarch64, `llvm@23.1.3-r2` with the managed glibc. Android 10 or later is required (the shell is reached as `/bin/sh`), with xlings 2026.10.10.2 or later, which unpacks payloads in the app sandbox, where hard links cannot be created. @@ -78,7 +78,7 @@ this model with a one-line `note:` hint. ```bash mcpp toolchain install gcc 16.1.0 # host target (GNU libc on Linux) -mcpp toolchain install llvm 23.1.3 # LLVM/Clang, default on macOS and Windows with usable MSVC +mcpp toolchain install llvm 23.1.3-r2 # LLVM/Clang, default on macOS and Windows with usable MSVC mcpp toolchain install gcc 16 --target x86_64-linux-musl # musl target payload mcpp toolchain install --target x86_64-windows-gnu # family omitted → the # target's convention pin (gcc@16.1.0) @@ -133,14 +133,14 @@ configs with combined spellings like `default = "gcc@15.1.0-musl"` keep working unchanged.) On native Linux aarch64 (2026.10.8.1+), an explicit migration selects both -LLVM 23.1.3 and the GNU target after the native payload is published: +LLVM 23.1.3-r2 and the GNU target after the native payload is published: ```bash -mcpp toolchain install llvm 23.1.3 -mcpp toolchain default llvm@23.1.3 --target aarch64-linux-gnu +mcpp toolchain install llvm 23.1.3-r2 +mcpp toolchain default llvm@23.1.3-r2 --target aarch64-linux-gnu ``` -The command records `llvm@23.1.3` and `aarch64-linux-gnu` as the default pair. +The command records `llvm@23.1.3-r2` and `aarch64-linux-gnu` as the default pair. Existing configurations remain effective until explicitly changed; project manifest declarations retain their precedence. @@ -166,7 +166,7 @@ the required family for that build: ``` $ mcpp build Resolving toolchain - Resolved llvm@23.1.3 → …/xim-x-llvm/23.1.3/bin/clang++ + Resolved llvm@23.1.3-r2 → …/xim-x-llvm/23.1.3-r2/bin/clang++ required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`), not your gcc@16.1.0 — this project only ``` @@ -212,7 +212,7 @@ The output has two blocks — one per axis: Toolchains: * gcc 16.1.0 (default) gcc 15.1.0 - llvm 23.1.3 + llvm 23.1.3-r2 Targets: TARGET NOTE TOOLCHAIN STATUS diff --git a/docs/22-target-side.md b/docs/22-target-side.md index ae6f7e205..f79be1572 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -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.11.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. @@ -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..abi]` is reported (`abi/dependency-table`) and changes diff --git a/mcpp.toml b/mcpp.toml index c56ce4699..7ddc633f4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.10.2" +version = "2026.11.11.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] @@ -66,11 +66,11 @@ programs = "directory" # (mcpp#669). Linux x86_64 retains GCC; native ARM64 uses LLVM and glibc. [toolchain] default = "gcc@16.1.0" -macos = "llvm@23.1.3" -windows = "llvm@23.1.3" +macos = "llvm@23.1.3-r2" +windows = "llvm@23.1.3-r2" [target.aarch64-linux-gnu] -toolchain = "llvm@23.1.3" +toolchain = "llvm@23.1.3-r2" # Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the # four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 16.1 + full diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index f120a9ceb..8f913df36 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -4481,14 +4481,28 @@ std::expected 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.11.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, " diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d07c3bf97..d1673b65c 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1173,6 +1173,15 @@ struct BuildConfig : BuildInputs { // "wasm" (`-fwasm-exceptions`), "js" (`-fexceptions`), or empty for // `exceptions = true`, which keeps its meaning since 2026.9.12.3: "js". std::string abiExceptionsModel; + // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary on an + // Emscripten row. A model, not a switch: "wasm" (`-sSUPPORT_LONGJMP=wasm`, + // WebAssembly's own stack switching) or "js" (`-sSUPPORT_LONGJMP=emscripten`, + // the JavaScript model, which is also the toolchain's default). The root's + // statement only, rendered into every translation unit and the link, like + // `threads`; the model reaches every cache key through those flags. + bool abiLongjmpDeclared = false; + std::string abiLongjmpModel; std::string cStandard; // Escape hatch for the hermetic link check: a sandbox toolchain whose // CRT/loader resolve OUTSIDE the sandbox is a hard error by default @@ -1732,6 +1741,9 @@ struct ConditionalConfig { bool abiExceptions = false; bool abiExceptionsDeclared = false; std::string abiExceptionsModel; + // `[target..abi] longjmp` -- see BuildConfig::abiLongjmpModel. + bool abiLongjmpDeclared = false; + std::string abiLongjmpModel; // `[target..build] dialect_cxxflags` -- a GRAPH-WIDE switch under a // target condition (issue #717, design 2026-09-27 §6). Like `abiThreads`/ // `abiExceptions` above, only the ROOT's value is rendered; unlike them it @@ -1846,7 +1858,7 @@ inline bool is_empty(const ConditionalConfig& c) { && c.buildDependencies.empty() && c.featureDeps.empty() && c.targetKinds.empty() && c.xlings.empty() && c.xlings.overrides.empty() - && !c.abiThreadsDeclared && !c.abiExceptionsDeclared + && !c.abiThreadsDeclared && !c.abiExceptionsDeclared && !c.abiLongjmpDeclared && !c.requiresAbiThreads && !c.requiresAbiExceptions && c.requires_.empty() && c.privateRequires.empty() && c.featureRequiresAbiThreads.empty() && c.featureRequiresAbiExceptions.empty() diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 4629e010d..38368dc87 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -418,7 +418,8 @@ standard = true # base package beside llvm where a payload of the system's own exists too, 5. # glibc from the payload, at the host's default toolchain natively. A cross to -# another architecture's glibc pins llvm; the payload exists for the host's arch. +# another architecture's glibc pins llvm and names the sysroot payload its C +# library comes from (the aarch64 row below; x86_64's hosts list admits it). [[platform.provider]] name = "glibc" serves = ["x86_64-linux-gnu"] @@ -429,8 +430,19 @@ hosts = ["linux-{arch}"] name = "glibc" serves = ["aarch64-linux-gnu"] priority = 100 -hosts = ["linux-{arch}"] +# The payload: native on each Linux arch; the cross from x86_64 takes the C +# library from a sysroot payload the row names (2026.11.11.1, G1) -- llvm +# alone carries no glibc of another architecture, and the host's is not the +# target's. The payload's package root IS the sysroot (glibc 2.44 with the +# kernel headers, libgcc and libc++ inside), so the row is served wherever +# that payload is published, with no host named here beyond the data. +hosts = ["linux-{arch}", "linux-x86_64"] toolchain = "llvm@23.1.3" +sysroot = "xim:aarch64-linux-gnu-sysroot@2.44.3" +# glibc's own linkage is dynamic; the compiler run-time is the payload's +# (libgcc's archives and compiler-rt's builtins, both static), so a link +# takes nothing from the host. +linkage = "dynamic" # The musl cross payloads: native on each Linux arch, every arch from x86_64, # and the Canadian cross on a Windows host of the same arch. diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index b05648255..1ca3d45db 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.11.10.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.11.1"; } // namespace mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index ad4cb0065..eed11b290 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -3493,8 +3493,32 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, emit_launch(env); return 0; } + // A PROGRAM OF ANOTHER SYSTEM'S KERNEL INTERFACE, of this machine's own + // architecture, STARTS AND DIES ON ITS FIRST KERNEL CALL (G6): the kernel + // maps a static foreign ELF without asking which system it names, the + // program then states a syscall in that system's vocabulary, and the death + // is by signal — where mcpp, having become the program on POSIX, can say + // nothing at all. Such an artifact is run as a child instead, so its death + // is observed, named with the [target.] runner declaration, and + // its status returned unchanged: 139 stays 139, exactly what a direct + // launch would have reported. Every other launch — a declared runner, + // `run_under`, a native or cross-arch target — keeps the launch it always + // had, and a native signal death is never interpreted: that death is the + // program's own. + const bool foreignKernel = [&] { + if (!choice.tmpl.empty() || !host.prefix.empty()) return false; + auto t = mcpp::toolchain::triple::parse(choice.tripleKey); + if (!t || t->os.empty()) return false; + const std::string_view here = mcpplibs::os::is_windows ? "windows" + : mcpplibs::os::is_macos ? "macos" : "linux"; + return t->os != here && t->arch == mcpp::toolchain::platform::host_arch; + }(); int spawnErr = 0; - const int rc = mcpp::run::run_foreground(argv, childEnv, &spawnErr); + int rc = 0; + if (foreignKernel) + rc = mcpp::run::run_exec(argv, childEnv, &spawnErr); + else + rc = mcpp::run::run_foreground(argv, childEnv, &spawnErr); if (spawnErr != 0) { using namespace mcpp::build::runner_lookup; if (!choice.tmpl.empty() || !host.prefix.empty()) @@ -3502,10 +3526,20 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, else if (classify(spawnErr) == SpawnClass::Unloadable) std::println(libc::stderr(), "error: {}", unrunnable_message(choice.tripleKey, exe, spawnErr)); + else if (foreignKernel) + // The program was found and its interpreter was not (ENOENT, a + // dynamic linker of the other system): the bare errno names + // neither fact, so the same runner declaration follows it. + std::println(libc::stderr(), "error: {}{}", + spawn_failed_message(exe.string(), spawnErr), + runner_advice_text(choice.tripleKey)); else std::println(libc::stderr(), "error: {}", spawn_failed_message(exe.string(), spawnErr)); return launcher_status(spawnErr); } + if (foreignKernel && mcpp::build::runner_lookup::death_by_signal(rc)) + std::println(libc::stderr(), "error: {}", + mcpp::build::runner_lookup::signal_death_message(choice.tripleKey, exe, rc)); return rc; } diff --git a/src/build/flags.cppm b/src/build/flags.cppm index d0363ef51..85df28235 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1017,8 +1017,30 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { link_toolchain_flags += " -fuse-ld=lld"; link_toolchain_flags_c = link_toolchain_flags; } else { - if (lm.mode == mcpp::toolchain::CLibMode::Sysroot) + // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot + // form): the host's model contributes nothing, exactly as when the + // graph supplies it -- the base's own -B/-L/loader words reach the + // line through the payload channel, and this machine's runtime + // directories would shadow the target's (measured: the host payload's + // x86_64 libatomic.a resolved ahead of the sysroot's, and lld called + // it incompatible with aarch64linux). + if (lm.mode == mcpp::toolchain::CLibMode::Sysroot + && plan.toolchain.base.layers.cAbi.empty()) link_toolchain_flags += lm.link_flags(ninjaEsc); + + // THE BASE'S ROOT IS A SYSROOT (the gnu-sysroot form): name it to + // the driver, and the ld scripts inside it (/lib GROUPs), its crt + // objects and its loader all resolve the way a --sysroot link + // resolves them -- the payload's own mcpp.toml states the package + // root IS the sysroot, and without the flag a dynamic link reads + // /lib/libc.so.6 against this machine's root instead. + if (!plan.toolchain.base.layers.cAbi.empty()) { + std::filesystem::path baseDir = plan.toolchain.base.root; + if (baseDir.empty() && plan.toolchain.base.payload) + baseDir = plan.toolchain.base.payload->glibcLib.parent_path(); + if (!baseDir.empty()) + link_toolchain_flags += " --sysroot=" + ninjaEsc(baseDir); + } link_toolchain_flags_c = link_toolchain_flags + std::string(mcpp::toolchain::ClangDriverModel::kLinkDriverFlagsC); // A graph-supplied libstdc++ under the payload's clang: the driver @@ -1033,9 +1055,9 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { } f.sysroot = link_toolchain_flags; } else if (lm.mode != mcpp::toolchain::CLibMode::None) { - // GCC (or Clang without cfg): --sysroot from probe, or the payload - // headers + C runtime (-B for crt discovery, -L for -lc/-lm). - link_toolchain_flags = crossTarget + lm.link_flags(ninjaEsc); + if (lm.mode == mcpp::toolchain::CLibMode::Sysroot + && plan.toolchain.base.layers.cAbi.empty()) + link_toolchain_flags += 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) { @@ -1895,11 +1917,18 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { std::string runtime_dirs; if constexpr (mcpp::toolchain::platform::supports_rpath) { - // Toolchain runtime dirs (glibc/gcc) as before... - for (auto& dir : plan.toolchain.linkRuntimeDirs) { - runtime_dirs += " -L" + escape_path(dir); - runtime_dirs += " -Wl,-rpath," + escape_path(dir); - } + // Toolchain runtime dirs (glibc/gcc) as before -- but not for a + // target whose C library a row's base package serves (the + // gnu-sysroot form): those directories are THIS MACHINE's, and on a + // cross they would both shadow the target's libraries (-L precedes + // the base's own) and bake a host RUNPATH into the target's + // artifact. The base's directories reach the line through its own + // channel. + if (plan.toolchain.base.layers.cAbi.empty()) + for (auto& dir : plan.toolchain.linkRuntimeDirs) { + runtime_dirs += " -L" + escape_path(dir); + runtime_dirs += " -Wl,-rpath," + escape_path(dir); + } // Dependency/runtime provider search comes from LinkIntent and is // rendered separately. In particular runtimeSearchDirs contributes // RUNPATH only; it must never become a link-time -L path. @@ -1982,16 +2011,19 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // A BASE PACKAGE'S RUN-TIMES ON THE LINK (design 2026-11-10 E5): where its // compiler runtime stands (`compiler_runtime_dir`: clang reads it as its // resource directory, gcc through `-B`), and, when it supplies the C++ - // layer, its `ldflags` in place of the driver's own C++ library, which - // names the toolchain's. - auto base_link_words = [&] { + auto base_link_words = [&](bool cxx) { std::string words; auto const& L = plan.toolchain.base.layers; if (!L.compilerRuntimeDir.empty()) words += plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang ? " -resource-dir=" + ninjaEsc(L.compilerRuntimeDir) : " -B" + ninjaEsc(L.compilerRuntimeDir); - if (!L.cxx.empty() && !L.ldflags.empty()) { + // The C++ library words are the C++ link's alone; the run-time + // directory above is as much a C decision -- a C link resolves + // __udivti3 from the same builtins the C++ one does, and a base + // package that names one names it for every link (the same sentence + // `kLinkDriverFlagsC` states for the driver's own selection). + if (cxx && !L.cxx.empty() && !L.ldflags.empty()) { words += " -nostdlib++"; for (auto const& w : L.ldflags) words += " " + ninja_command_word(w); } @@ -2065,7 +2097,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { const std::string baseStatic = !plan.toolchain.pairedSysroot.empty() && !layers.cxx.empty() && f.linkage == "static" ? " -static" : ""; - f.ld += baseStatic + base_link_words(); + f.ld += baseStatic + base_link_words(true); // `-lstdc++exp` is named explicitly, so swapping g++ for gcc would not // drop it — the C line has to leave it out. f.ldC = std::format("{}{}{}{}{}{}", crossTarget, paired, link_intent_ld, @@ -2321,6 +2353,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // `--push-state`/`--pop-state` pair), which is what sent a reader here. std::string atomic_ld = plan.targetSide.cAbi.prebuilt() + && plan.toolchain.base.layers.cAbi.empty() ? atomic_link_flag(plan.toolchain.linkRuntimeDirs, !full_static.empty()) : std::string{}; @@ -2699,7 +2732,12 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { } // A BASE PACKAGE'S RUN-TIMES (design 2026-11-10 E5). - f.ld += base_link_words(); + f.ld += base_link_words(true); + // The base's run-time directory reaches the C link too (2026.11.11.1): + // a hosted cross over a sysroot payload links its compiler run-time from + // the payload's own resource directory, and the C program's link is the + // one that had nowhere to find it. + f.ldC += base_link_words(false); // A LINKER STATED BY ROLE (mcpp#755, `tools = { ld = ... }`), AFTER EVERY // SHAPE HAS BUILT ITS LINE. diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index 51795fd10..c12d1388a 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -538,6 +538,21 @@ std::expected step2_resolve_base(PrepareState& state) { && !fs::exists(dir / "usr" / "include" / "linux" / "limits.h", ec)) tc.base.addedIncludes.push_back(ecosystemLinux); } + + // A LOADER INSIDE A CROSS BASE IS THE SYSROOT'S, NOT THIS MACHINE'S + // (2026.11.11.1): a --sysroot link names it sysroot-relative + // (/lib/ld-linux-aarch64.so.1), and so does the artifact's PT_INTERP -- + // the form qemu -L , a runner declaration and the ld scripts + // beside it (the base's own libc.so GROUPs /lib/...) all resolve. An + // absolute path would bake this machine's install directory into every + // image it links; the native case keeps its absolute path, as the + // Locator form's distro path above already states. + if (!native && !tc.base.loader.empty()) { + std::error_code lec; + if (auto rel = fs::relative(tc.base.loader, dir, lec); !lec && !rel.empty() + && rel.native().find("..") == std::string_view::npos) + tc.base.loader = fs::path("/") / rel; + } // A base package's own `provides` is its declaration, as a statement's is. if (tc.base.libc.empty() && !tc.base.layers.cAbi.empty()) tc.base.libc = tc.base.layers.cAbi; // ITS C++ RUN-TIME STANDS WHERE THE PAYLOAD'S WOULD (E5): the std module is diff --git a/src/build/prepare/config.cpp b/src/build/prepare/config.cpp index d55e58190..1dd4a485f 100644 --- a/src/build/prepare/config.cpp +++ b/src/build/prepare/config.cpp @@ -342,6 +342,10 @@ void merge_conditional_config(mcpp::manifest::Manifest& m, m.buildConfig.abiExceptionsModel = cc.abiExceptionsModel; m.buildConfig.abiExceptionsDeclared = true; } + if (cc.abiLongjmpDeclared) { + m.buildConfig.abiLongjmpModel = cc.abiLongjmpModel; + m.buildConfig.abiLongjmpDeclared = true; + } // `[target..build] dialect_cxxflags` (#717): recorded for every // package, like the abi switches above, but APPENDED rather than // replaced -- there is no "last matching section wins" here, because diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 859aec7bc..f5edafbbc 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1428,6 +1428,31 @@ static std::expected step1_device_axis_and_layer_merge(Prepar state.add_once(state.m->buildConfig.ldflags, flag); state.add_once(state.m->buildConfig.graphLdflags, flag); } + // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary. The flag + // changes the CODE emcc emits for the two calls, so it reaches both + // sides -- every translation unit's flags (C and C++ alike; the C library + // calls are what it is about) and the link, the channels `threads` uses. + // Declared on any row but Emscripten's it is refused, not ignored: the + // member names a WebAssembly model, and a build that wrote it for another + // target has a selector that matches the wrong row. + if (state.m->buildConfig.abiLongjmpDeclared) { + if (!emscripten) + return std::unexpected(std::format( + "[target..abi] longjmp states a WebAssembly long-jump model, " + "and the target '{}' is not an Emscripten row; the member belongs to " + "`[target.'cfg(os = \"emscripten\")'.abi]` alone", + state.overrides.target_triple.empty() + ? mcpp::toolchain::triple::host_triple().str() + : state.overrides.target_triple)); + const std::string flag = "-sSUPPORT_LONGJMP=" + + (state.m->buildConfig.abiLongjmpModel == "wasm" + ? std::string("wasm") : std::string("emscripten")); + state.add_once(state.m->buildConfig.dialectCxxflags, flag); + state.add_once(state.m->buildConfig.cflags, flag); + state.add_once(state.m->buildConfig.ldflags, flag); + state.add_once(state.m->buildConfig.graphLdflags, flag); + } // #851: emcc runs wasm-opt and minifies the JavaScript launcher AT THE // LINK, from the optimisation level the link is given; a link without // one is -O0 whatever the compiles were. The profile's level reaches it. diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 4039134a0..4c789209b 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -30,6 +30,8 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; import mcpp.build.linkage_form; // #519 — which form each dependency takes import mcpp.build.plan; import mcpp.build.schedule.policy; @@ -72,6 +74,40 @@ import mcpp.bmi_cache; namespace mcpp::build { +// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.11.1, G3): a row whose +// system the graph supplies -- `x86_64-windows-musl` over openkal -- has no +// paired payload, and its llvm binary carries no Windows headers, so +// `winres.h` reaches neither of the include sources the toolchain states. The +// MinGW-w64 sysroot payload a platform row declares for this architecture's +// GNU-ABI identity has them, under one `/include` for every +// architecture (E9's layout). Resolved through the payload channel the row's +// own sysroot goes through, and probed relative to what it resolves to -- +// never PATH. Empty when no row declares one or the payload is not here: the +// resource compile then fails with the tool's own missing-header text, which +// is the truthful message. +static std::filesystem::path +mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& trip) { + if (trip.os != "windows") return {}; + const auto gnuTriple = std::format("{}-windows-gnu", trip.arch); + for (auto const* p : mcpp::toolchain::target::providers()) { + if (p->sysroot.empty() || p->sysroot == "toolchain") continue; + if (!mcpp::toolchain::target::serves(*p, gnuTriple)) continue; + auto cfg = state.get_cfg(true); + if (!cfg) return {}; + mcpp::fetcher::Fetcher fetcher(**cfg); + mcpp::fetcher::InstallProgressHandler progress; + if (auto payload = fetcher.resolve_xpkg_path( + std::string(p->sysroot), mcpp::fetcher::PayloadIntent::Use, &progress)) + return payload->root / "include"; + auto xl = mcpp::config::make_xlings_env(**cfg); + if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), + mcpp::provider::parse_ref(p->sysroot))) + return *dir / "include"; + return {}; // one declared payload per row; the first provider wins + } + return {}; +} + // The resource compiler of the plan's resource units, and each unit's flags. static std::expected step13_resource_compiler(PrepareState& state, BuildContext& ctx) { @@ -100,9 +136,11 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; + const auto mingwHeaders = mingw_headers_root(state, trip); + // UTF-8 input, always. `[package]` metadata is user text and // routinely non-ASCII; without this llvm-rc refuses the script - // outright ("Non-ASCII 8-bit codepoint can't be interpreted in + // outright ("Non-ASCII 8-bit codepage can't be interpreted in // the current codepage") rather than mangling it, so a project // with a Chinese description could not build at all. // @@ -116,7 +154,7 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { const std::string ip = msvcStyle ? "/I" : "-I"; std::vector systemIncludes; if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos) - for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId)) + for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId, mingwHeaders)) systemIncludes.push_back(ip + dir.string()); for (auto& ru : ctx.plan.resourceUnits) { if (auto target = rsrc::coff_target_flag(*tool, state.tc->targetTriple); @@ -200,6 +238,7 @@ std::expected step13_windows_resources(PrepareState& state, B // windres), found as the compile step below finds it (E3). const auto rcTool = mcpp::build::resources::find_rc_tool(*state.tc, dialectId); const bool msvcStyle = rcTool ? rcTool->style == "msvc" : dialectId == "msvc"; + const auto mingwHeaders = mingw_headers_root(state, trip); for (auto const& S : subjects) { const auto& M = *S.m; @@ -385,7 +424,7 @@ std::expected step13_windows_resources(PrepareState& state, B // quoted `"winres.h"` is the toolchain's header, not a file // a build action produces (E3). auto scanIncludes = rcIncludes; - for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId)) + for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId, mingwHeaders)) scanIncludes.push_back(d); auto scan = rsrc::scan_rc(rcSrc, scanIncludes); if (scan.versionInfoNamedByString) { diff --git a/src/build/resources.cppm b/src/build/resources.cppm index 143c1fd55..4dbe50470 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -100,8 +100,17 @@ std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple); // MSVC backend states, and on a GNU-ABI row the paired payload's (or a cross // gcc's own) `-w64-mingw32/include` (design 2026-11-10 E3). One list for the // compile's `/I` and for the scan of what the script reads. +// +// `mingw_headers` is the third source (2026.11.11.1, G3): the include +// directory of the MinGW-w64 sysroot payload the platform's rows declare, +// which is where the Windows headers of a row whose system the graph supplies +// -- `x86_64-windows-musl`, no paired payload, an llvm binary that carries no +// headers of its own -- come from. Every PE row shares that one set. It is +// read ONLY when the two sources above found nothing, so a GNU-ABI row keeps +// its paired payload's headers and an MSVC row its SDK's. std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, - std::string_view dialectId); + std::string_view dialectId, + const std::filesystem::path& mingw_headers = {}); // Split a Windows environment list (PATH, INCLUDE, LIB) into its entries. // @@ -205,7 +214,8 @@ bool is_llvm_windres(const std::filesystem::path& p) { } std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, - std::string_view dialectId) { + std::string_view dialectId, + const std::filesystem::path& mingw_headers) { std::vector out; for (auto const& ev : tc.envOverrides) { if (ev.key != "INCLUDE") continue; @@ -233,6 +243,16 @@ std::vector system_include_dirs(const mcpp::toolchain::To // built, not where it is). add_triple_dirs(tc.binaryPath.parent_path().parent_path()); } + // THE THIRD SOURCE, ONLY WHERE THE TWO ABOVE FOUND NOTHING: a PE row whose + // system the graph supplies has neither a paired payload nor a cross gcc, + // and its llvm payload carries no Windows headers -- but the MinGW-w64 + // sysroot payload the rows declare does, with every architecture's headers + // under one `/include` (E9's layout). The caller resolved that + // payload; this stays a probe beside the directories it already probed, + // never a search of PATH. + if (out.empty() && !mingw_headers.empty() + && std::filesystem::is_directory(mingw_headers, ec)) + out.push_back(mingw_headers); return out; } diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 84e66e055..2577629a7 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -20,11 +20,13 @@ // // Design: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md §4.3-4.4. +module; +#include + export module mcpp.build.runner_lookup; import std; import mcpplibs.os.platform; - export namespace mcpp::build::runner_lookup { struct Lookup { @@ -168,4 +170,83 @@ inline std::string unrunnable_message(std::string_view triple, artifact.string(), errno_text(e), e, triple, triple); } +// ─── Started, and dead at once ──────────────────────────────────────────── +// +// A SPAWN THAT SUCCEEDS CAN STILL DIE BEFORE IT DOES ANYTHING. The kernel +// maps a static ELF of its own architecture without reading the system the +// ELF names — FreeBSD's OSABI byte is not a question Linux asks — so the +// program runs, states its first kernel interface call in its own system's +// vocabulary, and the kernel answers with a signal: SIGSEGV or SIGILL for a +// syscall number that maps nowhere, SIGBUS for a stack it never grew, +// SIGABRT where the C library gave up first. `mcpp::run` reports the exit +// status a shell would (128 + the signal), and the status is the only fact +// that distinguishes this death from a crash in a working program — which is +// why the classification is asked about artifacts of another system only, +// never about a native one, where the same status is the program's own bug +// and advice about runners would be noise on top of it. +inline bool death_by_signal(int rc) { + switch (rc) { + case 128 + SIGSEGV: + case 128 + SIGILL: + case 128 + SIGABRT: +#ifdef SIGBUS + case 128 + SIGBUS: +#endif + return true; + default: + return false; + } +} + +// The signal's name, for a message a reader can grep; empty for a status the +// classification did not take. +inline std::string_view signal_of_death(int rc) { + switch (rc) { + case 128 + SIGSEGV: return "SIGSEGV"; + case 128 + SIGILL: return "SIGILL"; + case 128 + SIGABRT: return "SIGABRT"; +#ifdef SIGBUS + case 128 + SIGBUS: return "SIGBUS"; +#endif + default: return {}; + } +} + +// What an artifact of another system's kernel interface says when it dies at +// once: the [target.] runner declaration, the same template +// `unrunnable_message` prints, under a first line that states the death and +// the fact of the build rather than guessing at a diagnosis. +inline std::string signal_death_message(std::string_view triple, + const std::filesystem::path& artifact, int rc) { + return std::format( + "'{}' was started and died at once ({}, status {}): it was built for " + "'{}', another system's kernel interface, and this machine cannot " + "execute it directly.\n" + " Declare how to run it here:\n" + "\n" + " [target.{}]\n" + " runner = [\"qemu-aarch64-static\"]\n" + "\n" + " The artifact path is appended, or substituted for `{{}}` if the " + "template contains it.\n" + " A host that can execute it directly may pass --no-runner.", + artifact.string(), signal_of_death(rc), rc, triple, triple); +} + +// The same declaration, appended to a spawn refusal whose bare text names +// only the errno (an interpreter of another system the kernel could not +// find, ENOENT) — the reader's remedy is the same either way. +inline std::string runner_advice_text(std::string_view triple) { + return std::format( + "\n The artifact was built for '{}'. Declare how to run it here:\n" + "\n" + " [target.{}]\n" + " runner = [\"qemu-aarch64-static\"]\n" + "\n" + " The artifact path is appended, or substituted for `{{}}` if the " + "template contains it.\n" + " A host that can execute it directly may pass --no-runner.", + triple, triple); +} + } // namespace mcpp::build::runner_lookup diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index fb2b960be..fe7b326e6 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -684,6 +684,18 @@ ValidationReport validate_changed_artifacts( record_without_rules(plan, before); return report; } + + // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot form, + // 2026.11.11.1): the artifact's loader and libc are the payload's, and + // the process these rules describe -- one process on this machine with + // this machine's binding -- is not the one that would load it. The row's + // declaration is the statement, exactly as an SDK's is above; the + // closure belongs to the payload that served the link, and a runner + // declaration (or qemu) is how such an artifact runs. + if (!plan.toolchain.base.layers.cAbi.empty()) { + record_without_rules(plan, before); + return report; + } // THE ARTIFACT'S OWN SYSTEM DECIDES (mcpp#911 G5): a row a platform defines // states where its artifacts run, and an artifact for another system is // judged by that system's loader, not this machine's. Its closure is not diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index a3d4bca69..1b769fa27 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -568,7 +568,16 @@ std::string unbuilt_row(std::string_view system, std::string_view arch); // The systems `--systems` names for an architecture (design 2026-10-29 // §4.1): an ``, an `-` of that architecture, or every system // for `all`; this machine's system when it names none. -std::vector systems_for(const std::vector& stated, std::string_view arch); +// +// `all` is the DATA's answer (2026.11.11.1, G5), not a list spelled here: +// this machine's system, every system a platform defines a row of for the +// architecture (mcpp#911 G10), and every system the platform's runner states +// a host of for it (`runner.hosts`) -- which is where FreeBSD enters, a +// runner the platform builds for `freebsd-`. A pack without a platform +// answers with the machine's own system alone; `--systems` naming an `` +// keeps its exact meaning. +std::vector systems_for(const std::vector& stated, std::string_view arch, + const mcpp::platform::Resolved* P = nullptr); // The single files of an image (design 2026-10-29 §4.5): one per leg and // system of the leg's architecture, the legs in the plan's order. `skipped` @@ -2390,6 +2399,16 @@ ProgramRow program_row(std::string_view system, std::string_view arch, bool sing if (t && t->os == system && t->arch == arch) return { t->str(), "static", platform_ref_of(*P) }; } + // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.11.1, + // G5), as FreeBSD's own request takes it (design 2026-11-10 E4): + // `*-freebsd-musl`, openkal-musl over openkal-freebsd, one static + // program with no C library of the system beneath -- built over the + // platform's hosted arrangement wherever the machine that packs it is, + // needing no FreeBSD and no SDK. + if (system == "freebsd") { + const auto triple = std::format("{}-freebsd-musl", arch); + return { triple, "static", hosted_platform_for(P, triple) }; + } return { std::format("{}-{}", arch, system), "static", {} }; } if (system == "linux") { @@ -2756,7 +2775,7 @@ run_loaded(const Plan& plan) } if (plan.opts.mode == Mode::BundleAll) { for (auto const& leg : legs) - for (auto const& system : mcpp::pack::systems_for(plan.opts.systems, leg.arch)) { + for (auto const& system : mcpp::pack::systems_for(plan.opts.systems, leg.arch, &plan.platform)) { // A row this mcpp builds no program for (a planned // target, `x86_64-macos`) carries no runner: said, and // the package's metadata lists the systems it carries. @@ -3509,7 +3528,8 @@ std::expected finalize_staged(const Plan& plan) { return detail::finalize_tree(plan); } -std::vector systems_for(const std::vector& stated, std::string_view arch) { +std::vector systems_for(const std::vector& stated, std::string_view arch, + const mcpp::platform::Resolved* P) { if (stated.empty()) return { std::string(detail::host_system()) }; std::vector out; auto add = [&](std::string os) { @@ -3517,12 +3537,21 @@ std::vector systems_for(const std::vector& stated, std }; for (auto const& e : stated) { if (e == "all") { - for (auto os : { "linux", "macos", "windows" }) add(os); - // And every system the platforms of this process define a row of - // for the architecture (mcpp#911 G10). + // This machine's system, every system the platforms of this + // process define a row of for the architecture (mcpp#911 G10), + // and every system the carrier's runner states a host of for it + // (2026.11.11.1, G5) -- the rows the platform itself says its + // runner exists for, FreeBSD's among them. + add(std::string(detail::host_system())); for (auto const* r : mcpp::toolchain::target::rows()) if (r->defined && r->arch == arch && !r->os.empty() && r->os != "none") add(std::string(r->os)); + if (P && P->active) + for (auto const& h : P->fields.runner.hosts) { + const auto dash = h.find('-'); + if (dash == std::string_view::npos) continue; + if (h.substr(dash + 1) == arch) add(std::string(h.substr(0, dash))); + } continue; } const auto dash = e.find('-'); @@ -3538,7 +3567,7 @@ std::vector single_file_rows(const Plan& plan) { const bool all = std::ranges::find(plan.opts.systems, "all") != plan.opts.systems.end(); std::vector rows; for (auto const& arch : archs) - for (auto const& system : systems_for(plan.opts.systems, arch)) { + for (auto const& system : systems_for(plan.opts.systems, arch, &plan.platform)) { SingleFileRow r{ system, arch, {} }; if (all) if (auto row = unbuilt_row(system, arch); !row.empty()) diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 683732810..0a4631128 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -964,7 +964,7 @@ std::optional check_format(PackRun& run, GroupJob& g) { std::vector unbuilt, built; const bool all = std::ranges::find(run.opts.systems, "all") != run.opts.systems.end(); for (auto const& arch : archs) - for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch)) { + for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch, &ctx.plan.platform)) { auto row = mcpp::pack::unbuilt_row(system, arch); if (row.empty()) built.push_back(std::format("{}-{}", system, arch)); else if (!all) unbuilt.push_back(std::format("{}-{} ({})", system, arch, row)); @@ -1256,7 +1256,7 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { std::error_code xec; const bool windowsLauncher = std::filesystem::is_regular_file(plan->stagingRoot / "bin" / (stem + ".exe"), xec); for (auto const& arch : archs) - for (auto const& system : carries ? mcpp::pack::systems_for(opts.systems, arch) + for (auto const& system : carries ? mcpp::pack::systems_for(opts.systems, arch, &plan->platform) : std::vector{}) { const auto pair = std::format("{}-{}", system, arch); if (covered(pair)) continue; diff --git a/src/project.cppm b/src/project.cppm index 4c5e9619b..a96f8d347 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,14 +1095,17 @@ export std::string root_position_key( auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; std::string abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" + b.abiExceptionsModel : "0") : "-"; + std::string abiLongjmp = b.abiLongjmpDeclared ? b.abiLongjmpModel : "-"; if (applies) for (auto const& cc : m.conditionalConfigs) { - if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared + && !cc.abiExceptionsDeclared && !cc.abiLongjmpDeclared) continue; if (!applies(cc.predicate)) continue; dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" + cc.abiExceptionsModel : "0"; + if (cc.abiLongjmpDeclared) abiLongjmp = cc.abiLongjmpModel; } list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); @@ -1116,6 +1119,7 @@ export std::string root_position_key( field("ios_deployment_target", b.iosDeploymentTarget); field("abi_threads", abiThreads); field("abi_exceptions", abiExceptions); + field("abi_longjmp", abiLongjmp); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); @@ -1196,6 +1200,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, b.abiExceptions = f.abiExceptions; b.abiExceptionsModel = f.abiExceptionsModel; b.abiExceptionsDeclared = f.abiExceptionsDeclared; + b.abiLongjmpModel = f.abiLongjmpModel; + b.abiLongjmpDeclared = f.abiLongjmpDeclared; b.allowHostLibs = f.allowHostLibs; b.macosDeploymentTarget = f.macosDeploymentTarget; b.iosDeploymentTarget = f.iosDeploymentTarget; @@ -1210,7 +1216,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, // member declares (or inherits) reaches the build when the member is // planned under a virtual root (#786). Nothing a package owns travels. for (auto const& cc : first.conditionalConfigs) { - if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared + && !cc.abiExceptionsDeclared && !cc.abiLongjmpDeclared) continue; mcpp::manifest::ConditionalConfig row; row.predicate = cc.predicate; @@ -1220,6 +1227,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, row.abiExceptions = cc.abiExceptions; row.abiExceptionsModel = cc.abiExceptionsModel; row.abiExceptionsDeclared = cc.abiExceptionsDeclared; + row.abiLongjmpModel = cc.abiLongjmpModel; + row.abiLongjmpDeclared = cc.abiLongjmpDeclared; v.conditionalConfigs.push_back(std::move(row)); } return v; diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 243196fbe..ed668ce06 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -29,6 +29,8 @@ import mcpp.toolchain.llvm; import mcpp.toolchain.model; import mcpp.toolchain.msvc; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; export namespace mcpp::toolchain { @@ -1365,7 +1367,25 @@ bool host_can_serve(const triple::Triple& target) { const bool crossArch = target.arch != mcpp::toolchain::platform::host_arch; if (target.is_musl()) return !crossArch || mcpp::toolchain::platform::host_arch == "x86_64"; - return !crossArch; + // A CROSS WHOSE ROW NAMES A SYSROOT PAYLOAD IS SERVED WHEREVER + // THAT PAYLOAD IS PUBLISHED (2026.11.11.1, G1). llvm alone carries + // no glibc of another architecture, which is what `return + // !crossArch` answered: no payload here produces it. The row's + // own declaration is the better answer -- a provider that serves + // the identity with a sysroot payload, available on this host, + // supplies the C library whatever the architectures are. The + // answer stays data: no host is named in this function. + if (crossArch) { + const auto host = std::format("linux-{}", + mcpp::toolchain::platform::host_arch); + for (auto const* p : mcpp::toolchain::target::providers()) + if (mcpp::toolchain::target::serves(*p, target.str()) + && !p->sysroot.empty() && p->sysroot != "toolchain" + && mcpp::toolchain::target::available_on(*p, host, target.arch)) + return true; + return false; + } + return true; } // Non-Linux host: only the self-contained musl payloads can work at // all (nothing else would find a C library). Today exactly one such diff --git a/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh b/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh new file mode 100755 index 000000000..06f578b08 --- /dev/null +++ b/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# requires: llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1075 -- a Windows row whose system the graph supplies compiles its resources +# with the Windows headers the platform's rows declare (2026.11.11.1, G3; +# design 2026-11-10 E3's third source). +# +# `x86_64-windows-musl` takes its system from the graph (openkal-platform's +# hosted part), so its toolchain has no paired payload, and its llvm binary +# carries no Windows headers of its own: neither include source +# `system_include_dirs` read existed, and a script that includes +# `"winres.h"` failed in llvm-rc with `winres.h: No such file or directory`. +# The MinGW-w64 sysroot payload a platform row declares for the +# architecture's GNU-ABI identity holds them, under one `include/` for every +# architecture; every PE row shares that set, and the two existing sources +# keep their precedence. +# +# Criteria: the project builds with `[resources]` on x86_64-windows-musl and +# the program carries a VERSIONINFO resource; a GNU-ABI row (1058) is not +# re-tested here. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'TOML' +[package] +name = "verinfo" +version = "0.1.0" + +[resources] +files = ["app.rc"] + +[targets.verinfo] +kind = "bin" +main = "src/main.c" +TOML +echo 'int main(void) { return 0; }' > "$t/src/main.c" +cat > "$t/app.rc" <<'RC' +#include "winres.h" +1 VERSIONINFO +FILEVERSION 1,2,3,4 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "ProductName", "verinfo" + END + END +END +RC +cd "$t" +out=$("$MCPP" build --target x86_64-windows-musl --toolchain "llvm@${LLVM_VERSION}" 2>&1) \ + || fail "[resources] did not build on x86_64-windows-musl" "$out" +exe=$(find target/x86_64-windows-musl -path '*/bin/verinfo.exe' -type f | head -1) +[ -n "$exe" ] || fail "no program" "$out" +# The llvm payload of the build, not the host's tools. +readobj="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readobj" +[ -x "$readobj" ] || fail "no llvm-readobj in the llvm payload ($readobj)" +"$readobj" --coff-resources "$exe" | grep -q 'VERSIONINFO' \ + || fail "the program carries no VERSIONINFO" "$("$readobj" --coff-resources "$exe" | head -20)" +echo "PASS: 1075 a graph-supplied Windows row finds its resource headers" diff --git a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh new file mode 100755 index 000000000..12f13f23b --- /dev/null +++ b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# requires: llvm aarch64-gnu-sysroot +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1076 -- an aarch64-linux-gnu cross takes its C library from the sysroot +# payload its row names (2026.11.11.1, G1). +# +# `host_can_serve` answered "no payload here produces it" for every arch-cross +# glibc target: llvm alone carries no glibc of another architecture. The +# row's own declaration is the better answer -- the provider that serves the +# identity names a sysroot payload (`xim:aarch64-linux-gnu-sysroot`, the +# package root IS the sysroot: glibc with the kernel headers, libgcc and +# libc++ inside) and states the hosts it is published for, so the gate reads +# the data and no host is named in the engine. +# +# The capability token probes the payload's install directory: the recipe is +# pending in the package index, so a machine without it (CI until the index +# carries it) skips rather than guesses. Where the payload IS installed the +# criteria are: a minimal C program builds for aarch64-linux-gnu, the +# artifact is an aarch64 ELF, and -- with qemu-aarch64 on the machine -- it +# runs and its exit status is passed through. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } + +# The cap token already probed the registry; re-check so the script also runs +# standalone without run_all.sh. +sysroot="" +for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-aarch64-linux-gnu-sysroot/*/; do + [ -d "$d/lib" ] && sysroot="$d" +done +if [ -z "$sysroot" ]; then + echo "SKIP: 1076 -- no aarch64-linux-gnu-sysroot payload installed (xim:aarch64-linux-gnu-sysroot)" + echo "PASS: 1076 (skipped)" + exit 0 +fi + +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'TOML' +[package] +name = "cross" +version = "0.1.0" + +[targets.cross] +kind = "bin" +main = "src/main.c" +TOML +printf '#include \nint main(void) { printf("cross-ok\\n"); return 42; }\n' > "$t/src/main.c" +cd "$t" +out=$("$MCPP" build --target aarch64-linux-gnu --toolchain "llvm@${LLVM_VERSION}" 2>&1) \ + || fail "aarch64-linux-gnu did not build from the row's sysroot payload" "$out" +exe=$(find target/aarch64-linux-gnu -path '*/bin/cross' -type f | head -1) +[ -n "$exe" ] || fail "no program" "$out" +readelf="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readelf" +[ -x "$readelf" ] || fail "no llvm-readelf in the llvm payload ($readelf)" +arch=$("$readelf" -h "$exe" | awk '/Machine:/ {print $2}') +[ "$arch" = "AArch64" ] || fail "the artifact is not an aarch64 ELF (machine: $arch)" "$("$readelf" -h "$exe" | head -20)" +# Where the machine can run it, the program's own exit status is the truth. +if command -v qemu-aarch64 >/dev/null 2>&1; then + rc=0 + qemu-aarch64 -L "${sysroot%/}" "$exe" > run.log 2>&1 || rc=$? + [ "$rc" -eq 42 ] || fail "qemu-aarch64 run: expected exit 42, got $rc" run.log + grep -q '^cross-ok$' run.log || fail "the program's output did not arrive" run.log +fi +echo "PASS: 1076 an aarch64-linux-gnu cross comes from a sysroot payload" diff --git a/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh b/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh new file mode 100755 index 000000000..f4c4ecd6e --- /dev/null +++ b/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# requires: llvm +# 1077 -- a program of another system's kernel interface that dies at once +# names the runner that would run it (2026.11.11.1, G6/G7). +# +# `mcpp run --target x86_64-freebsd-musl` on a Linux host spawns a static +# FreeBSD ELF of the host's own architecture: the kernel maps it without +# asking which system it names, the program states its first kernel call in +# FreeBSD's vocabulary, and the death is by signal -- 139, with nothing to +# read. `mcpp run` now runs such an artifact as a child, so the death is +# observed and named with the [target.] runner declaration; the +# status itself is returned unchanged, exactly what a direct launch would +# have reported. +# +# Criteria: the exit status stays 139; the error text states the death, the +# other system's kernel interface, and the runner declaration for the row. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app +"$MCPP" build --target x86_64-freebsd-musl > b.log 2>&1 || fail "the static FreeBSD program did not build" b.log +exe=$(find target/x86_64-freebsd-musl -path '*/bin/app' -type f | head -1) +[ -n "$exe" ] || fail "no program" b.log + +set +e +"$MCPP" run --target x86_64-freebsd-musl > run.log 2>&1 +rc=$? +set -e +[ "$rc" -eq 139 ] || fail "run exited $rc, expected the program's own death status 139" run.log +grep -q "was started and died at once" run.log \ + || fail "the run does not state the death at once" run.log +grep -q "another system's kernel interface" run.log \ + || fail "the run does not state the kernel interface fact" run.log +grep -q "\[target.x86_64-freebsd-musl\]" run.log \ + || fail "the runner declaration names the wrong target" run.log +grep -q "runner = \[" run.log \ + || fail "no runner declaration is printed" run.log +grep -q -- "--no-runner" run.log \ + || fail "the direct-execution escape hatch is not named" run.log +echo "PASS: 1077 a program of another system that dies at once names the runner" diff --git a/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh new file mode 100755 index 000000000..a68bbc8af --- /dev/null +++ b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell +# 1078 -- `--systems all` carries the runners the platform's own data names +# (2026.11.11.1, G5): this machine's system, the rows a platform defines, and +# the systems its runner states a host of -- FreeBSD's among them, whose +# runner is built as -freebsd-musl over the platform's hosted +# arrangement. +# +# The default is unchanged: a pack without `--systems` carries the runners +# of the legs' own systems -- this machine's system for every architecture +# the package has a leg of -- and no other system's. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -m)" in + x86_64|amd64) ARCH=x86_64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac +case "$(uname -s)" in Linux) OS=linux ;; *) echo "SKIP: a Linux host"; exit 0 ;; esac + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app + +# The default: this machine's system for each leg's architecture, nothing +# else (`mcpp new app` carries an x86_64 and an aarch64 leg). A `dir` pack's +# tree lands under target/dist whatever `-o` says (design 2026-10-29 §4.6), +# so the pack is located, not assumed. +"$MCPP" pack --target openkal --mode self-contained --pkg-format dir > d.log 2>&1 \ + || fail "the default pack did not build" d.log +one=$(find target/dist -mindepth 1 -maxdepth 1 -type d -newer mcpp.toml | head -1) +[ -n "$one" ] || fail "no pack tree under target/dist" d.log +[ -d "$one/libexec/okrun/${OS}-${ARCH}" ] || fail "the default pack carries no runner of its own system" d.log +others=$(ls "$one/libexec/okrun" 2>/dev/null | grep -v "^${OS}-" || true) +[ -z "$others" ] || fail "the default pack carries a system other than this machine's: $others" d.log + +# `--systems all`: the data's answer, FreeBSD included. The same tree name +# replaces the default's, so dist is cleared first and the tree that lands +# is the all-pack's own. +rm -rf target/dist +"$MCPP" pack --target openkal --mode self-contained --systems all --pkg-format dir > a.log 2>&1 \ + || fail "--systems all did not pack" a.log +all_=$(find target/dist -mindepth 1 -maxdepth 1 -type d | head -1) +[ -n "$all_" ] || fail "no --systems all tree under target/dist" a.log +for pair in "linux-x86_64" "linux-aarch64" "freebsd-x86_64" "freebsd-aarch64"; do + [ -d "$all_/libexec/okrun/$pair" ] || fail "no runner carried for $pair" a.log +done +# The Windows row builds wherever this mcpp builds it; where it does not, the +# warning says so instead. +if [ ! -d "$all_/libexec/okrun/windows-x86_64" ]; then + grep -q "no runner for windows-x86_64" a.log \ + || fail "the Windows runner is neither carried nor said" a.log +fi +echo "PASS: 1078 --systems all carries the runners the platform declares" diff --git a/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh new file mode 100755 index 000000000..b0f8c9f71 --- /dev/null +++ b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# requires: elf +# 1079 -- `longjmp` is the third `abi` member: the model a WebAssembly +# program's sigsetjmp/siglongjmp cross the boundary by (2026.11.11.1). +# +# 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 (measured: memory access out of bounds) +# while the build and the link succeed -- a failure nothing names. With +# `-sSUPPORT_LONGJMP=wasm` on both the compiles and the link, the pair +# round-trips. A deep exit wants a bigger stack; that is the project's own +# knob (`-sSTACK_SIZE` in ldflags), not an ABI fact, and the engine does not +# render it. +# +# Same self-probe gate as 653/650: `# requires: elf` only, because whether +# THIS machine has the emsdk payload is not a portable capability. Declared +# on a non-Emscripten row the member is refused naming the row's own os. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +have_emsdk=0 +for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripten \ + "$HOME"/.xlings/data/xpkgs/xim-x-emsdk/*/emscripten; do + [[ -x "$d/em++" ]] && have_emsdk=1 +done +if [ "$have_emsdk" -ne 1 ] || ! command -v node >/dev/null 2>&1; then + echo "SKIP: 1079 -- no emsdk payload or node on this machine" + echo "PASS: 1079 (skipped)" + exit 0 +fi + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "lj" +version = "0.1.0" + +[targets.lj] +kind = "bin" +main = "src/main.c" + +[target.wasm32-emscripten] +runner = ["node"] + +[target.'cfg(os = "emscripten")'.abi] +longjmp = "wasm" + +[target.'cfg(os = "emscripten")'.build] +ldflags = ["-sSTACK_SIZE=1048576"] +TOML +cat > src/main.c <<'C' +/* Emscripten's libc declares the sigsetjmp/siglongjmp aliases behind the + feature macros, like every libc; the fixture states the one it needs. */ +#define _GNU_SOURCE 1 +#include +#include + +static jmp_buf home; + +static void level_two(void) { + volatile int kept = 7; + siglongjmp(home, 1); + (void)kept; +} + +static void level_one(void) { level_two(); } + +int main(void) { + volatile int marker = 0; + if (sigsetjmp(home, 1) == 0) { + marker = 1; + level_one(); + printf("unreachable\n"); + return 1; + } + /* The volatile locals of the frames unwound by the non-local exit. */ + if (marker != 1) { printf("marker lost\n"); return 2; } + printf("round-trip\n"); + return 0; +} +C + +"$MCPP" build --target wasm32-emscripten > b.log 2>&1 || fail "the wasm build with longjmp = \"wasm\" failed" b.log +"$MCPP" run --target wasm32-emscripten > run.log 2>&1 || fail "the run failed -- the non-local exit did not round-trip" run.log +grep -q '^round-trip$' run.log || fail "the program did not reach its end" run.log + +# The model is a member of Emscripten rows alone: a Linux build that declares +# it is refused naming that fact, not silently satisfied. +cat > mcpp.toml <<'TOML' +[package] +name = "lj" +version = "0.1.0" + +[targets.lj] +kind = "bin" +main = "src/main.c" + +[target.'cfg(os = "linux")'.abi] +longjmp = "wasm" +TOML +if "$MCPP" build --target x86_64-linux-musl > refuse.log 2>&1; then + fail "longjmp = \"wasm\" on a Linux row was accepted" +fi +grep -q "not an Emscripten row" refuse.log || fail "the refusal does not name the row's own os" refuse.log +echo "PASS: 1079 an Emscripten row's longjmp model round-trips" diff --git a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh index 0571bfee9..f2576261b 100644 --- a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh +++ b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh @@ -53,12 +53,28 @@ echo "ok: --systems - makes that pair's file" # C if [ "$OS" = linux ]; then + # `--systems all` is the data's answer (2026.11.11.1): this machine's + # system, the systems the platform defines a row of for the + # architecture, and the systems its runner states a host of -- FreeBSD + # included, and never a pair no row exists for, so nothing is "Not + # covered" and nothing needs to say so. "$MCPP" pack --target openkal --pkg-format single-file --systems all -o all/ > c.log 2>&1 || fail "C: --systems all" c.log - for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64; do + for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64 \ + freebsd-x86_64 freebsd-aarch64; do ls all/sf-0.1.0-$pair* > /dev/null 2>&1 || fail "C: no file for $pair" c.log done - grep -q "Not covered macos-x86_64 (no carrier row" c.log || fail "C: the pair without a row is not said" c.log - echo "ok: --systems all makes every file this mcpp can and says the rest" + [ ! -e all/sf-0.1.0-macos-x86_64 ] || fail "C: a pair with no row was packed" c.log + # Naming an keeps its exact meaning: for a single file per pair, + # a system one of whose architectures has no row is refused naming the + # pair and the spelling that would work. + if "$MCPP" pack --target openkal --pkg-format single-file --systems macos -o mac/ > m.log 2>&1; then + fail "C: --systems macos was accepted with a rowless pair" m.log + fi + grep -q "builds no program for macos-x86_64" m.log \ + || fail "C: the pair without a row is not said" m.log + grep -q "use: --systems macos-aarch64" m.log \ + || fail "C: the refusal does not name the working spelling" m.log + echo "ok: --systems all makes every file the data names; a named system with a rowless pair is refused naming it" fi # D diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index f7294893b..24deac883 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -93,6 +93,14 @@ case "$OS" in if ls "${MCPP_HOME}"/registry/data/xpkgs/xim-x-llvm/*/bin/clang++ 2>/dev/null | head -1 | grep -q .; then CAPS+=(llvm) fi + # aarch64-gnu-sysroot: the glibc sysroot payload the aarch64-linux-gnu + # row names (xim:aarch64-linux-gnu-sysroot). A payload directory, not a + # version: the row's e2e skips until the package index carries the + # recipe and the runner prewarms it, which is why the token probes + # rather than assuming. + if ls "${MCPP_HOME}"/registry/data/xpkgs/xim-x-aarch64-linux-gnu-sysroot/*/lib/libc.so.6 2>/dev/null | head -1 | grep -q .; then + CAPS+=(aarch64-gnu-sysroot) + fi # Host MinGW headers can come from a distro prefix or the dedicated # xlings fixture outside MCPP_HOME. Test 738 proves that unrestricted # Clang finds the latter before checking graph isolation. @@ -271,7 +279,7 @@ echo "Detected capabilities: ${CAPS[*]:-}" # absent on Linux and must stay legal to declare. It is checked against the # CAPS+=() calls above by tests/e2e/README or by reading them -- keep it in # sync when adding a capability. -KNOWN_CAPS=(android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos +KNOWN_CAPS=(aarch64-gnu-sysroot android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw mingw-cross mingw-host-headers msvc musl nasm no-msvc pack patchelf python3 qemu-arm qemu-riscv scan-deps symlink unix-shell windows wine xlings-msvc) diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot index 712dc7e6c..a99e7d229 100644 --- a/tests/fixtures/target-rows.snapshot +++ b/tests/fixtures/target-rows.snapshot @@ -37,7 +37,7 @@ row x86_64-linux-musl descriptor-keys linux sysroot row aarch64-linux-gnu - table tier=preview note= pin=llvm@23.1.3 sysroot= static=0 platform= + table tier=preview note= pin=llvm@23.1.3 sysroot=xim:aarch64-linux-gnu-sysroot@2.44.3 static=0 platform= str aarch64-linux-gnu llvm aarch64-unknown-linux-gnu llvm@15.0 aarch64-unknown-linux-gnu @@ -54,7 +54,7 @@ row aarch64-linux-gnu android-abi arm64-v8a platform-name linux descriptor-keys linux - sysroot + sysroot xim:aarch64-linux-gnu-sysroot@2.44.3 row aarch64-linux-musl table tier=verified note= pin=gcc@16.1.0 sysroot= static=1 platform= str aarch64-linux-musl @@ -861,7 +861,7 @@ request x86_64-linux on windows-x86_64 -> x86_64-linux-musl musl-gcc fallback=1 | x86_64-linux-gnu/glibc/100/x x86_64-linux-musl/musl-gcc/100 x86_64-linux-musl/openkal/50 request aarch64-linux parsed aarch64-linux-gnu envExplicit=0 - on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 + on linux-x86_64 -> aarch64-linux-gnu glibc fallback=0 | aarch64-linux-gnu/glibc/100 aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 request riscv64-linux diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index dcc94bab2..104402d5a 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -52,14 +52,17 @@ UTest dispatched_format_stages_without_it{"PackClosureUnavailableOutcome/Dispatc expect(eq(closure_unavailable_outcome(Format::Dispatched), ClosureUnavailableOutcome::StageWithoutClosure)); }}; -// `--systems` (design 2026-10-29 §4.1): `` for every architecture of the -// legs, `-` for one, `all` for the three systems; unstated, this -// machine's. +// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.11.1 answers +// with the data): `` for every architecture of the legs, `-` +// for one; unstated, this machine's. `all` is this machine's system, the +// systems the builtin rows define for the architecture, and -- with a +// resolved platform -- the systems its runner states a host of. Without a +// platform that is the machine's system and this table's linux rows alone. UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystemsForAnArchitecture", [] { using V = std::vector; expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux", "macos", "windows" }))); + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); }}; diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 144aca228..0851ecf2a 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -1,4 +1,5 @@ #include +#include #include import std; @@ -148,6 +149,50 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; +// The classification of a death by signal (2026.11.11.1, G6): the four +// signals an artifact of another system's kernel interface dies by on its +// first kernel call, and nothing else — a normal exit, an exit by a signal +// the classification does not take (SIGTERM, 143) and the launcher's own +// 127/126 band are all someone else's fact. +UTest death_by_signal_takes_four_signals{"RunnerLookup/DeathBySignalTakesFourSignals", [] { + expect(death_by_signal(128 + SIGSEGV)); + expect(death_by_signal(128 + SIGILL)); + expect(death_by_signal(128 + SIGABRT)); +#ifdef SIGBUS + expect(death_by_signal(128 + SIGBUS)); +#endif + expect(!death_by_signal(0)); + expect(!death_by_signal(1)); + expect(!death_by_signal(128 + SIGTERM)); + expect(!death_by_signal(128 + SIGINT)); + expect(!death_by_signal(127)); + expect(!death_by_signal(-11)); + expect(eq(signal_of_death(128 + SIGSEGV), std::string_view("SIGSEGV"))); + expect(eq(signal_of_death(128 + SIGILL), std::string_view("SIGILL"))); + expect(eq(signal_of_death(0), std::string_view{})); +}}; + +UTest signal_death_message_names_the_runner_declaration{"RunnerLookup/SignalDeathMessageNamesTheRunnerDeclaration", [] { + const std::filesystem::path art{"/x/target/x86_64-freebsd-musl/debug/bin/app"}; + auto msg = signal_death_message("x86_64-freebsd-musl", art, 128 + SIGSEGV); + expect(ne(msg.find("was started and died at once"), std::string::npos)) << msg; + expect(ne(msg.find("SIGSEGV"), std::string::npos)) << msg; + expect(ne(msg.find("status 139"), std::string::npos)) << msg; + expect(ne(msg.find("another system's kernel interface"), std::string::npos)) << msg; + expect(ne(msg.find("[target.x86_64-freebsd-musl]"), std::string::npos)) << msg; + expect(ne(msg.find("runner = ["), std::string::npos)) << msg; + expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; + // The status stays the caller's to return; the message only says it. + expect(eq(msg.find("status 139, but"), std::string::npos)) << msg; +}}; + +UTest runner_advice_text_declares_the_target{"RunnerLookup/RunnerAdviceTextDeclaresTheTarget", [] { + auto msg = runner_advice_text("aarch64-linux-gnu"); + expect(ne(msg.find("The artifact was built for 'aarch64-linux-gnu'"), std::string::npos)) << msg; + expect(ne(msg.find("[target.aarch64-linux-gnu]"), std::string::npos)) << msg; + expect(ne(msg.find("runner = ["), std::string::npos)) << msg; +}}; + #else UTest runner_lookup_windows_covered_by_integration{"RunnerLookup/WindowsCoveredByIntegration", [] { /* passes */; }}; diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 74df15d30..e529ba405 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -637,7 +637,12 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie // the LLVM 23.1.3/glibc default, its deployed pack, GNU self-host, // all 147 suites and four real index members, from source. The tier // stays `preview` until the released engine is consumed on a clean - // ARM64 host (SPEC-009 §10.7). + // ARM64 host (SPEC-009 §10.7). From 2026.11.11.1 the row names the + // sysroot payload its C library comes from (G1) -- llvm alone + // carries no glibc of another architecture -- and the linkage is + // glibc's own dynamic one: the compiler run-time is the payload's + // (libgcc's archives and compiler-rt's builtins), so a link takes + // nothing static from the host. auto [name, tier] = std::pair{"aarch64-linux-gnu", "preview"}; auto t = parse(name); expect(fatal(t.has_value())); @@ -645,7 +650,7 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie expect(fatal(ne(info, nullptr))); expect(eq(info->tier, tier)); expect(eq(info->pin, "llvm@23.1.3")); - expect(info->sysroot.empty()); + expect(eq(info->sysroot, "xim:aarch64-linux-gnu-sysroot@2.44.3")); expect(!info->defaultStatic); expect(!t->pin_is_capability()); } From 6c325b181bbb09cf36f9bb113ef8b994b894c1b3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 04:56:19 +0900 Subject: [PATCH 02/30] the llvm pin stays 23.1.3 everywhere; no #ifdef in module code The corrective r2 repack lands in the package index under the same version key (openxlings/xpkgindex-update#9), so no pin or doc names -r2 and the arm/macos/windows legs resolved a package that does not exist. runner_lookup's SIGBUS cases lose their #ifdef guards: POSIX defines the signal everywhere mcpp builds, and the include gate keeps module code conditional-free. --- README.md | 2 +- README.zh-CN.md | 2 +- docs/01-getting-started.md | 6 +++--- docs/20-toolchains.md | 24 ++++++++++++------------ mcpp.toml | 6 +++--- src/build/runner_lookup.cppm | 6 ++---- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 70e60b194..466fd4e08 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ list` reports for this machine): | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23 — Cortex-A 32-bit, the first row with an MMU ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23 — bare metal, no C library by default ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | | `aarch64-windows-gnu` | llvm 23 with `xim:mingw-w64-sysroot` (llvm-mingw's target side) — built on any host; the same base serves `x86_64-windows-gnu` where no gcc payload exists | preview | | `x86_64-freebsd` · `aarch64-freebsd` | the platform that serves them, over `xim:-freebsd-sysroot` ([22](docs/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23 — a static FreeBSD program with a musl C library over openkal-freebsd, built on any host *(what `-freebsd` is with no platform chosen)* | preview | diff --git a/README.zh-CN.md b/README.zh-CN.md index 92083e472..cb61423a5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -402,7 +402,7 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`( | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23;Cortex-A 32 位,第一个带 MMU 的目标 ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23;裸机,默认不带 C 库 ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | | `aarch64-windows-gnu` | llvm 23 加 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧);任意宿主可构建;没有 gcc 载荷之处同一基座也服务 `x86_64-windows-gnu` | preview | | `x86_64-freebsd` · `aarch64-freebsd` | 服务它们的平台,基于 `xim:-freebsd-sysroot`([22](docs/zh/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23;openkal-freebsd 之上带 musl C 库的静态 FreeBSD 程序,任意宿主可构建 *(未选平台时 `-freebsd` 即此身份)* | preview | diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md index 84fbadba4..ce92416d4 100644 --- a/docs/01-getting-started.md +++ b/docs/01-getting-started.md @@ -42,10 +42,10 @@ the host: | host | default | |---|---| | Linux x86_64 | `gcc@16.1.0` | -| Linux aarch64 | `llvm@23.1.3-r2` | +| Linux aarch64 | `llvm@23.1.3` | | other Linux architectures | `gcc@15.1.0-musl` | -| macOS | `llvm@23.1.3-r2` | -| Windows with usable MSVC | `llvm@23.1.3-r2` | +| macOS | `llvm@23.1.3` | +| Windows with usable MSVC | `llvm@23.1.3` | | Windows without it | `gcc@16.1.0` for `x86_64-windows-gnu` | The Linux aarch64 default applies from 2026.10.8.1. Existing configured diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index c699ae62e..f4db148ee 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -24,19 +24,19 @@ host-aware: - Linux x86_64 uses `gcc@16.1.0` for the native glibc ABI, so X11, OpenGL, and system libraries work out of the box. -- Linux aarch64 uses `llvm@23.1.3-r2` for the native glibc ABI (2026.10.8.1+). +- Linux aarch64 uses `llvm@23.1.3` for the native glibc ABI (2026.10.8.1+). - Other Linux architectures use `gcc@15.1.0-musl`, a self-contained static toolchain. -- macOS uses `llvm@23.1.3-r2`. -- Windows with a usable MSVC installation uses `llvm@23.1.3-r2` for the MSVC ABI. +- macOS uses `llvm@23.1.3`. +- Windows with a usable MSVC installation uses `llvm@23.1.3` for the MSVC ABI. Without usable MSVC, it uses `gcc@16.1.0` with target `x86_64-windows-gnu` (MinGW-w64, static by default), says which half of the MSVC ABI is missing — Visual Studio's C++ tools or the Windows SDK — and where it looked, and records the pair as the machine's default. Once both halves - are present, `mcpp toolchain default llvm@23.1.3-r2` moves the machine to the + are present, `mcpp toolchain default llvm@23.1.3` moves the machine to the MSVC ABI. - Android (Termux) uses the default of GNU/Linux on its architecture: on - aarch64, `llvm@23.1.3-r2` with the managed glibc. Android 10 or later is + aarch64, `llvm@23.1.3` with the managed glibc. Android 10 or later is required (the shell is reached as `/bin/sh`), with xlings 2026.10.10.2 or later, which unpacks payloads in the app sandbox, where hard links cannot be created. @@ -78,7 +78,7 @@ this model with a one-line `note:` hint. ```bash mcpp toolchain install gcc 16.1.0 # host target (GNU libc on Linux) -mcpp toolchain install llvm 23.1.3-r2 # LLVM/Clang, default on macOS and Windows with usable MSVC +mcpp toolchain install llvm 23.1.3 # LLVM/Clang, default on macOS and Windows with usable MSVC mcpp toolchain install gcc 16 --target x86_64-linux-musl # musl target payload mcpp toolchain install --target x86_64-windows-gnu # family omitted → the # target's convention pin (gcc@16.1.0) @@ -133,14 +133,14 @@ configs with combined spellings like `default = "gcc@15.1.0-musl"` keep working unchanged.) On native Linux aarch64 (2026.10.8.1+), an explicit migration selects both -LLVM 23.1.3-r2 and the GNU target after the native payload is published: +LLVM 23.1.3 and the GNU target after the native payload is published: ```bash -mcpp toolchain install llvm 23.1.3-r2 -mcpp toolchain default llvm@23.1.3-r2 --target aarch64-linux-gnu +mcpp toolchain install llvm 23.1.3 +mcpp toolchain default llvm@23.1.3 --target aarch64-linux-gnu ``` -The command records `llvm@23.1.3-r2` and `aarch64-linux-gnu` as the default pair. +The command records `llvm@23.1.3` and `aarch64-linux-gnu` as the default pair. Existing configurations remain effective until explicitly changed; project manifest declarations retain their precedence. @@ -166,7 +166,7 @@ the required family for that build: ``` $ mcpp build Resolving toolchain - Resolved llvm@23.1.3-r2 → …/xim-x-llvm/23.1.3-r2/bin/clang++ + Resolved llvm@23.1.3 → …/xim-x-llvm/23.1.3/bin/clang++ required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`), not your gcc@16.1.0 — this project only ``` @@ -212,7 +212,7 @@ The output has two blocks — one per axis: Toolchains: * gcc 16.1.0 (default) gcc 15.1.0 - llvm 23.1.3-r2 + llvm 23.1.3 Targets: TARGET NOTE TOOLCHAIN STATUS diff --git a/mcpp.toml b/mcpp.toml index 7ddc633f4..e31c70818 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -66,11 +66,11 @@ programs = "directory" # (mcpp#669). Linux x86_64 retains GCC; native ARM64 uses LLVM and glibc. [toolchain] default = "gcc@16.1.0" -macos = "llvm@23.1.3-r2" -windows = "llvm@23.1.3-r2" +macos = "llvm@23.1.3" +windows = "llvm@23.1.3" [target.aarch64-linux-gnu] -toolchain = "llvm@23.1.3-r2" +toolchain = "llvm@23.1.3" # Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the # four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 16.1 + full diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 2577629a7..6fda71aeb 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -184,14 +184,14 @@ inline std::string unrunnable_message(std::string_view triple, // why the classification is asked about artifacts of another system only, // never about a native one, where the same status is the program's own bug // and advice about runners would be noise on top of it. +// POSIX defines SIGBUS everywhere mcpp builds; no conditional compilation +// is needed (and the include gate forbids #ifdef here). inline bool death_by_signal(int rc) { switch (rc) { case 128 + SIGSEGV: case 128 + SIGILL: case 128 + SIGABRT: -#ifdef SIGBUS case 128 + SIGBUS: -#endif return true; default: return false; @@ -205,9 +205,7 @@ inline std::string_view signal_of_death(int rc) { case 128 + SIGSEGV: return "SIGSEGV"; case 128 + SIGILL: return "SIGILL"; case 128 + SIGABRT: return "SIGABRT"; -#ifdef SIGBUS case 128 + SIGBUS: return "SIGBUS"; -#endif default: return {}; } } From d7a9bc3d11fe304ae80e6441a5ab4b7a10960787 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 05:10:41 +0900 Subject: [PATCH 03/30] runner_lookup: the signal constants come from the mwrap of libc's macros The engine includes no headers (the include-region rule); the C library's macros arrive as constants through mcpplibs.mwrap.libc, so the death classification takes the three signals the wrap carries. SIGBUS is not among them -- its typical cause here, a stack the kernel never grew, arrives as SIGSEGV on the systems that report it. --- src/build/runner_lookup.cppm | 27 +++++++++++++-------------- tests/unit/test_runner_lookup.cpp | 16 +++++++--------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 6fda71aeb..646314580 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -20,13 +20,11 @@ // // Design: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md §4.3-4.4. -module; -#include - export module mcpp.build.runner_lookup; import std; import mcpplibs.os.platform; +import mcpplibs.mwrap.libc; export namespace mcpp::build::runner_lookup { struct Lookup { @@ -184,14 +182,16 @@ inline std::string unrunnable_message(std::string_view triple, // why the classification is asked about artifacts of another system only, // never about a native one, where the same status is the program's own bug // and advice about runners would be noise on top of it. -// POSIX defines SIGBUS everywhere mcpp builds; no conditional compilation -// is needed (and the include gate forbids #ifdef here). +// The constants come from the mwrap of the C library's macros (the engine +// includes no headers); SIGBUS is not among them, and its typical cause here +// -- a stack the kernel never grew -- arrives as SIGSEGV on the systems that +// report it, so the classification keeps the three the wrap carries. +namespace sigc = mcpplibs::mwrap::libc::macros; inline bool death_by_signal(int rc) { switch (rc) { - case 128 + SIGSEGV: - case 128 + SIGILL: - case 128 + SIGABRT: - case 128 + SIGBUS: + case 128 + sigc::SIGSEGV: + case 128 + sigc::SIGILL: + case 128 + sigc::SIGABRT: return true; default: return false; @@ -202,11 +202,10 @@ inline bool death_by_signal(int rc) { // classification did not take. inline std::string_view signal_of_death(int rc) { switch (rc) { - case 128 + SIGSEGV: return "SIGSEGV"; - case 128 + SIGILL: return "SIGILL"; - case 128 + SIGABRT: return "SIGABRT"; - case 128 + SIGBUS: return "SIGBUS"; - default: return {}; + case 128 + sigc::SIGSEGV: return "SIGSEGV"; + case 128 + sigc::SIGILL: return "SIGILL"; + case 128 + sigc::SIGABRT: return "SIGABRT"; + default: return {}; } } diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 0851ecf2a..4ef7d5271 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -149,18 +149,16 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; -// The classification of a death by signal (2026.11.11.1, G6): the four -// signals an artifact of another system's kernel interface dies by on its -// first kernel call, and nothing else — a normal exit, an exit by a signal -// the classification does not take (SIGTERM, 143) and the launcher's own -// 127/126 band are all someone else's fact. -UTest death_by_signal_takes_four_signals{"RunnerLookup/DeathBySignalTakesFourSignals", [] { +// The classification of a death by signal (2026.11.11.1, G6): the three +// signals the mwrap of the C library's macros carries that an artifact of +// another system's kernel interface dies by on its first kernel call, and +// nothing else — a normal exit, an exit by a signal the classification +// does not take (SIGTERM, 143) and the launcher's own 127/126 band are all +// someone else's fact. +UTest death_by_signal_takes_the_wrapped_three{"RunnerLookup/DeathBySignalTakesTheWrappedThree", [] { expect(death_by_signal(128 + SIGSEGV)); expect(death_by_signal(128 + SIGILL)); expect(death_by_signal(128 + SIGABRT)); -#ifdef SIGBUS - expect(death_by_signal(128 + SIGBUS)); -#endif expect(!death_by_signal(0)); expect(!death_by_signal(1)); expect(!death_by_signal(128 + SIGTERM)); From 33c80e629701f11088bf52d1b5680f32c93a645c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 05:14:14 +0900 Subject: [PATCH 04/30] docs/zh: the longjmp row joins the abi table --- docs/zh/22-target-side.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 0b70acfd9..2d803403d 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -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.11.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `threads` 相同的那一套 | 两个成员都经由方言 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。 From 09b1779b88904e6e1897d95ad5b606c8b6e131c9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 06:05:25 +0900 Subject: [PATCH 05/30] fix: the loader fold reads the generic string, not the native wide pointer --- src/build/prepare/base.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index c12d1388a..c12712e6d 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -550,7 +550,7 @@ std::expected step2_resolve_base(PrepareState& state) { if (!native && !tc.base.loader.empty()) { std::error_code lec; if (auto rel = fs::relative(tc.base.loader, dir, lec); !lec && !rel.empty() - && rel.native().find("..") == std::string_view::npos) + && rel.generic_string().find("..") == std::string::npos) tc.base.loader = fs::path("/") / rel; } // A base package's own `provides` is its declaration, as a statement's is. From c18bd6f37f9027ef645de45f71330d76c351afd6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 07:24:42 +0900 Subject: [PATCH 06/30] the CI legs the change itself turned up - polyglot holds FreeBSD's program beside Linux's (G5 made --systems all name it; the assembler's whitelist was the half that lagged) and refuses a system it has no uname pattern for, not FreeBSD - 299 accepts a row whose payload package this home cannot install: the row resolved, the package is the index's to give, and the refusal's subject names the row -- the claim is completion, as it always was - the MinGW headers are read only where the toolchain's own include sources found nothing, standing payloads before fetched ones: an MSVC or GNU-ABI project no longer fetches a payload its link will not read (measured on the upgrade lane: mingw-w64-sysroot, 21.3 MB, first run) - pack_modes states 'all' host-aware: the machine asking is one of the facts, and the suite runs on four hosts - 1076 is excused in the coverage table: no hosted shard installs the sysroot payload, and the test skips rather than guesses --- src/build/prepare/windows_resources.cpp | 31 +++++++++++++------ src/pack/polyglot.cppm | 10 +++--- ...c_library_resolves_to_a_row_that_exists.sh | 24 ++++++++++++-- tests/e2e/coverage-exceptions.tsv | 1 + tests/unit/test_pack_modes.cpp | 12 ++++++- tests/unit/test_pack_polyglot.cpp | 13 ++++++-- 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 4c789209b..7969771cc 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -80,11 +80,16 @@ namespace mcpp::build { // `winres.h` reaches neither of the include sources the toolchain states. The // MinGW-w64 sysroot payload a platform row declares for this architecture's // GNU-ABI identity has them, under one `/include` for every -// architecture (E9's layout). Resolved through the payload channel the row's -// own sysroot goes through, and probed relative to what it resolves to -- -// never PATH. Empty when no row declares one or the payload is not here: the -// resource compile then fails with the tool's own missing-header text, which -// is the truthful message. +// architecture (E9's layout). +// +// READ WHERE THE TWO SOURCES ABOVE FOUND NOTHING, AND STANDING PAYLOADS +// FIRST: a row that already states its headers -- the SDK's, a paired +// payload's -- never resolves this, so preparing an MSVC or GNU-ABI project +// does not fetch a payload its link will not read. Only a compile that would +// otherwise lack `winres.h` resolves the declaration, and it takes the +// payload where it already stands before it fetches one. Empty when no row +// declares one or the payload is not here: the resource compile then fails +// with the tool's own missing-header text, which is the truthful message. static std::filesystem::path mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& trip) { if (trip.os != "windows") return {}; @@ -94,15 +99,15 @@ mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& t if (!mcpp::toolchain::target::serves(*p, gnuTriple)) continue; auto cfg = state.get_cfg(true); if (!cfg) return {}; + auto xl = mcpp::config::make_xlings_env(**cfg); + if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), + mcpp::provider::parse_ref(p->sysroot))) + return *dir / "include"; mcpp::fetcher::Fetcher fetcher(**cfg); mcpp::fetcher::InstallProgressHandler progress; if (auto payload = fetcher.resolve_xpkg_path( std::string(p->sysroot), mcpp::fetcher::PayloadIntent::Use, &progress)) return payload->root / "include"; - auto xl = mcpp::config::make_xlings_env(**cfg); - if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), - mcpp::provider::parse_ref(p->sysroot))) - return *dir / "include"; return {}; // one declared payload per row; the first provider wins } return {}; @@ -136,7 +141,13 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; - const auto mingwHeaders = mingw_headers_root(state, trip); + // The third source is resolved only where it will be read: below the + // toolchain's own include sources, never beside them (G3) -- a row that + // states its headers does not fetch a payload its compile will not read. + std::filesystem::path mingwHeaders; + if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos + && rsrc::system_include_dirs(*state.tc, dialectId).empty()) + mingwHeaders = mingw_headers_root(state, trip); // UTF-8 input, always. `[package]` metadata is user text and // routinely non-ASCII; without this llvm-rc refuses the script diff --git a/src/pack/polyglot.cppm b/src/pack/polyglot.cppm index a51f50ad2..27967453e 100644 --- a/src/pack/polyglot.cppm +++ b/src/pack/polyglot.cppm @@ -29,8 +29,8 @@ import std; export namespace mcpp::pack::polyglot { -// One system's program: `system` is `linux`, `macos` or `windows`, `arch` -// `x86_64` or `aarch64`. +// One system's program: `system` is `linux`, `freebsd`, `macos` or `windows`, +// `arch` `x86_64` or `aarch64`. struct Program { std::string system; std::string arch; @@ -66,7 +66,9 @@ void put32(std::string& b, std::size_t o, std::uint32_t v) { // `uname -s`-`uname -m` spellings of a system and architecture. std::string uname_pattern(std::string_view system, std::string_view arch) { - const std::string s = system == "macos" ? "Darwin" : "Linux"; + const std::string s = system == "macos" ? "Darwin" + : system == "freebsd" ? "FreeBSD" + : "Linux"; if (arch == "x86_64") return std::format("{0}-x86_64|{0}-amd64", s); return std::format("{0}-aarch64|{0}-arm64", s); } @@ -145,7 +147,7 @@ std::expected assemble(std::string_view name, const st "a polyglot file leads with one Windows program, and this pack has two ({}, {})", windows->arch, p.arch)); windows = &p; - } else if (p.system == "linux" || p.system == "macos") { + } else if (p.system == "linux" || p.system == "freebsd" || p.system == "macos") { posix.push_back(p); } else { return std::unexpected(std::format("a polyglot file holds no program of {}", p.system)); diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index 92ea117f0..14cda1314 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -86,9 +86,27 @@ case "$r" in host-cannot-serve) echo " ok --target aarch64-linux completed (this host serves no such payload)" ;; *) - echo "FAIL: --target aarch64-linux refused for reason '$r'" - message_of aarch64-linux | sed 's/^/ /' - exit 1 ;; + # THE ROW'S PAYLOAD IS A PACKAGE, AND A PACKAGE MAY NOT BE INSTALLABLE + # WHERE THIS RUNS (2026.11.11.1, G1): the GNU row names its sysroot, and + # an index without it -- a CI runner ahead of the index PR, an offline + # machine -- refuses about the PACKAGE, quoting the row it resolved. That + # refusal still answers this half's question: the request reached a row + # of the system, which the refusal's subject and the row checks below + # both name. Anything else it names is not about the row and stays red. + if [ "$row" = aarch64-linux-gnu ] || [ "$row" = aarch64-unknown-linux-gnu ] \ + || [ "$row" = aarch64-linux-musl ] || [ "$row" = aarch64-unknown-linux-musl ]; then + if message_of aarch64-linux | grep -q "could not be installed"; then + echo " ok --target aarch64-linux reached a row whose payload package is not installable here" + else + echo "FAIL: --target aarch64-linux refused for reason '$r'" + message_of aarch64-linux | sed 's/^/ /' + exit 1 + fi + else + echo "FAIL: --target aarch64-linux refused for reason '$r'" + message_of aarch64-linux | sed 's/^/ /' + exit 1 + fi ;; esac # AND THE ROW IS THE IDENTITY, NOT THE SPELLING. The output directory is the diff --git a/tests/e2e/coverage-exceptions.tsv b/tests/e2e/coverage-exceptions.tsv index 13a168b7a..6e346fca3 100644 --- a/tests/e2e/coverage-exceptions.tsv +++ b/tests/e2e/coverage-exceptions.tsv @@ -2,3 +2,4 @@ # One `\t` line each. A test that runs on some shard, or that a # workflow names, does not belong here, and the check fails if it is listed. 658_a_program_runs_on_an_attached_android_device.sh needs an Android device attached over adb; the android job of ci-linux-e2e.yml runs `mcpp test` on an emulator instead +1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh needs the xim:aarch64-linux-gnu-sysroot payload installed; no hosted shard installs it, and the test skips rather than guesses until the index carries the payload diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index 104402d5a..228e898bc 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -1,6 +1,7 @@ import std; import mcpplibs.mtest; +import mcpplibs.os.platform; import mcpp.pack; using namespace mcpplibs::mtest; @@ -62,7 +63,16 @@ UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystem using V = std::vector; expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); + // `all` answers with the data, and one of the facts is the machine asking: + // its system stands first, wherever the same suite runs. A Linux host and + // the table's linux rows coincide; a macOS host names macos beside them. + const char* here = "linux"; + if constexpr (mcpplibs::os::is_windows) here = "windows"; + else if constexpr (mcpplibs::os::is_macos) here = "macos"; + if (std::string_view(here) == "linux") + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); + else + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ here, "linux" }))); expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); }}; diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 88866317e..5ce8bfc09 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -31,8 +31,10 @@ std::uint64_t number_after(std::string_view text, std::string_view key) { } // namespace UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_says{"PackPolyglot/WithoutWindowsTheFileIsAScriptAndEachProgramLiesWhereItsLineSays", [] { - const std::string linux64(1000, 'L'), mac(777, 'M'); - auto file = pg::assemble("app", { program("linux", "x86_64", linux64), program("macos", "aarch64", mac) }); + const std::string linux64(1000, 'L'), mac(777, 'M'), fbsd(333, 'F'); + auto file = pg::assemble("app", { program("linux", "x86_64", linux64), + program("freebsd", "x86_64", fbsd), + program("macos", "aarch64", mac) }); if (auto&& failed = expect(fatal(file.has_value())); !failed) failed << file.error(); expect(file->starts_with("#!/bin/sh\no=$0;")); expect(pg::is_polyglot(*file)); @@ -47,6 +49,7 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ expect(eq(script.find("MINGW*"), std::string_view::npos)) << "no Windows program, no MSYS line"; for (auto [key, bytes] : { std::pair{std::string_view("k=linux-x86_64 a="), std::string_view(linux64)}, + std::pair{std::string_view("k=freebsd-x86_64 a="), std::string_view(fbsd)}, std::pair{std::string_view("k=macos-aarch64 a="), std::string_view(mac)} }) { const auto line = script.find(key); expect(fatal(ne(line, std::string_view::npos))) << key; @@ -57,6 +60,7 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ } // `uname` spells an architecture two ways on some systems. expect(ne(script.find("Linux-x86_64|Linux-amd64)"), std::string_view::npos)); + expect(ne(script.find("FreeBSD-x86_64|FreeBSD-amd64)"), std::string_view::npos)); expect(ne(script.find("Darwin-aarch64|Darwin-arm64)"), std::string_view::npos)); }}; @@ -64,7 +68,10 @@ UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [ expect(!(pg::assemble("app", {}).has_value())); expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - expect(!(pg::assemble("app", { program("freebsd", "x86_64", "x") }).has_value())); + // FreeBSD is a POSIX system of the file since 2026.11.11.1 (G5): its + // program rides behind the script head beside Linux's. What still has + // no place is a system the script names no `uname` pattern for. + expect(!(pg::assemble("app", { program("plan9", "x86_64", "x") }).has_value())); auto notPe = pg::assemble("app", { program("windows", "x86_64", std::string(256, 'x')) }); expect(fatal(!notPe.has_value())); if (auto&& failed = expect(ne(notPe.error().find("no PE"), std::string::npos)); !failed) failed << notPe.error(); From d6f73eda3ce91d9d87b00fa3c8a41e1f973920d4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:32:40 +0900 Subject: [PATCH 07/30] the mingw headers resolve lazily at both sites, and 299 reads the bracketed row The resource scan resolved the MinGW-w64 sysroot payload unconditionally (gdb, a fresh home, why --target x86_64-windows-gnu): the compiler step had been gated since the last push, the scan step had not, so every PE prepare fetched 21.3 MB its link would not read -- measured on the upgrade lane's first run. Both sites now share one sentence: llvm-rc, a row whose Windows headers are not the machine's own (a located MSVC SDK states that they are), the toolchain's own include sources found nothing, standing payloads before fetched ones. 299's row extraction now also reads the bracketed subject of the base refusal ([target.aarch64-linux-gnu].sysroot = ...), the form the payload refusal takes; the single-quoted form it alone read before stays first. --- src/build/prepare/windows_resources.cpp | 19 +++++++++++++++---- ...c_library_resolves_to_a_row_that_exists.sh | 3 ++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 7969771cc..a632dec3a 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -141,11 +141,15 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; - // The third source is resolved only where it will be read: below the - // toolchain's own include sources, never beside them (G3) -- a row that - // states its headers does not fetch a payload its compile will not read. + // The third source is resolved only where it will be read, and only for + // a row whose Windows headers are not the machine's own (G3's subject): + // a located MSVC SDK is the row's stated header source -- an INCLUDE the + // shell did not export is not that SDK's absence, and an MSVC row never + // fetches another row's payload. Below the toolchain's own include + // sources, never beside them. The same sentence gates both call sites. std::filesystem::path mingwHeaders; if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos + && state.tc->base.msvc.sdkRoot.empty() && rsrc::system_include_dirs(*state.tc, dialectId).empty()) mingwHeaders = mingw_headers_root(state, trip); @@ -249,7 +253,14 @@ std::expected step13_windows_resources(PrepareState& state, B // windres), found as the compile step below finds it (E3). const auto rcTool = mcpp::build::resources::find_rc_tool(*state.tc, dialectId); const bool msvcStyle = rcTool ? rcTool->style == "msvc" : dialectId == "msvc"; - const auto mingwHeaders = mingw_headers_root(state, trip); + // Lazy, as at the compiler step above: a row that states its own headers + // resolves nothing, and a scan that finds no script reads none. + namespace rsrc = mcpp::build::resources; + std::filesystem::path mingwHeaders; + if (rcTool && msvcStyle && rcTool->name().find("llvm-rc") != std::string::npos + && state.tc->base.msvc.sdkRoot.empty() + && rsrc::system_include_dirs(*state.tc, dialectId).empty()) + mingwHeaders = mingw_headers_root(state, trip); for (auto const& S : subjects) { const auto& M = *S.m; diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index 14cda1314..7fa686c6f 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -65,7 +65,8 @@ resolved_row() { # request → the row it resolved to t="$(printf '%s' "$doc" | jq -r '.data.triple.llvm // empty')" if [ -n "$t" ]; then printf '%s' "$t"; return; fi printf '%s' "$doc" | jq -r '.diagnostics[0].message // empty' \ - | head -1 | sed -n "s/^[^']*'\\([^']*\\)'.*/\\1/p" + | head -1 | sed -n -e "s/^[^']*'\([^']*\)'.*/\1/p" \ + -e "s/^\[target\.\([^]]*\)\].*/\1/p" } message_of() { "$MCPP" why toolchain --target "$1" --format json 2>/dev/null \ From ead6bad0daf91ea8184714d45305b5734e6a2eae Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:39:33 +0900 Subject: [PATCH 08/30] docs: the three-plane platform architecture, as the reference the next systems consume A synthesis of what the merged round established and what the families after it use: rows, payloads, runners as data; the ABI layering beneath one C library ported once; corrective revisions; the disk-image asset class and its harness; the semantic plane stated as the program's own. No behaviour changes -- this names what is already gated green. --- ...hree-plane-platform-architecture-design.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .agents/docs/2026-11-12-three-plane-platform-architecture-design.md diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md new file mode 100644 index 000000000..ad4851af1 --- /dev/null +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -0,0 +1,106 @@ +# 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)
static where the row says static"] + end + subgraph kal["the kernel-ABI specification"] + K["openkal: the interface, its conformance suite
and one implementation per system family"] + end + R1["row ① + payload ②"] --> A + M --> A + K --> M + KF["firmware implementations
(UEFI boot services, RISC-V SBI)"] --> K + KP["kernel implementations
(the reference one, a BSD one,") --> K + KX["a POSIX-library implementation
(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. + +## 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 | an OVMF/emulator runner entry; a status convention for environments without exit codes | +| A system's official media, booted | the disk-image asset class (`-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 `-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. From d6abf3a7550e59ea56981796995dedcc21c05ffd Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:43:58 +0900 Subject: [PATCH 09/30] 2026.11.12.1: the release the ecosystem round lands in; the three-plane architecture as its reference The version the PR will release under is dated for its merge day, and every comment that cites its introducing release follows it. The architecture record names what the round established and what the families after it consume: rows, payloads and runners as data; one C library above one kernel-ABI specification, ported once; layer vocabulary the rows, payloads and requirement side already share; corrective revisions; the disk-image asset class and the harness that boots it; the semantic plane stated as the program's own. It adds no behaviour -- it names what the gates already hold green. --- ...6-11-12-three-plane-platform-architecture-design.md | 10 ++++++++-- CHANGELOG.md | 2 +- docs/10-pack-and-release.md | 2 +- docs/22-target-side.md | 2 +- docs/zh/22-target-side.md | 2 +- mcpp.toml | 2 +- modules/manifest/src/toml.cppm | 2 +- modules/manifest/src/types.cppm | 2 +- modules/toolchain-model/data/builtin.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/build/flags.cppm | 2 +- src/build/prepare/base.cpp | 2 +- src/build/prepare/toolchain.cpp | 2 +- src/build/prepare/windows_resources.cpp | 2 +- src/build/resources.cppm | 2 +- src/build/runtime_validation.cppm | 2 +- src/pack/pack.cppm | 6 +++--- src/toolchain/registry.cppm | 2 +- tests/unit/test_pack_modes.cpp | 2 +- tests/unit/test_pack_polyglot.cpp | 2 +- tests/unit/test_runner_lookup.cpp | 2 +- tests/unit/test_toolchain_triple.cpp | 2 +- 22 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md index ad4851af1..83ce7aab8 100644 --- a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -39,7 +39,7 @@ flowchart TB M --> A K --> M KF["firmware implementations
(UEFI boot services, RISC-V SBI)"] --> K - KP["kernel implementations
(the reference one, a BSD one,") --> K + KP["kernel implementations
(the reference kernel's own interface,
a BSD kernel's, a Windows kernel's)"] --> K KX["a POSIX-library implementation
(for systems whose own libc is the interface)"] --> K R3["runner ③"] -. runs the artifact on.-> consumer ``` @@ -75,6 +75,12 @@ Measured, not aspirational (each item landed with its gate green): 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 @@ -83,7 +89,7 @@ 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 | an OVMF/emulator runner entry; a status convention for environments without exit codes | +| 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 (`-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 | diff --git a/CHANGELOG.md b/CHANGELOG.md index 81913d6a9..bd30f93da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. -## [2026.11.11.1] - 2026-11-11 +## [2026.11.12.1] - 2026-11-12 ### Added diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index e0fb56e5a..e14d83dfe 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -857,7 +857,7 @@ 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.11.1+).** The machine's +**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-` gets FreeBSD's runners, diff --git a/docs/22-target-side.md b/docs/22-target-side.md index f79be1572..1c6f0f413 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1363,7 +1363,7 @@ 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.11.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 | +| `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 | 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 diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 2d803403d..b68daabf1 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -1147,7 +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.11.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `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 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。 diff --git a/mcpp.toml b/mcpp.toml index e31c70818..dc39019df 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.11.1" +version = "2026.11.12.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 8f913df36..b235a3389 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -4489,7 +4489,7 @@ std::expected parse_document(const t::Document& documen // 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.11.1): "wasm" or + // `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. diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d1673b65c..21453c223 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1173,7 +1173,7 @@ struct BuildConfig : BuildInputs { // "wasm" (`-fwasm-exceptions`), "js" (`-fexceptions`), or empty for // `exceptions = true`, which keeps its meaning since 2026.9.12.3: "js". std::string abiExceptionsModel; - // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // `[target..abi] longjmp` -- the third member (2026.11.12.1): // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary on an // Emscripten row. A model, not a switch: "wasm" (`-sSUPPORT_LONGJMP=wasm`, // WebAssembly's own stack switching) or "js" (`-sSUPPORT_LONGJMP=emscripten`, diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 38368dc87..8130c137f 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -431,7 +431,7 @@ name = "glibc" serves = ["aarch64-linux-gnu"] priority = 100 # The payload: native on each Linux arch; the cross from x86_64 takes the C -# library from a sysroot payload the row names (2026.11.11.1, G1) -- llvm +# library from a sysroot payload the row names (2026.11.12.1, G1) -- llvm # alone carries no glibc of another architecture, and the host's is not the # target's. The payload's package root IS the sysroot (glibc 2.44 with the # kernel headers, libgcc and libc++ inside), so the row is served wherever diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 1ca3d45db..769ff0148 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.11.11.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.12.1"; } // namespace mcpp diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 85df28235..37cc75a60 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -2733,7 +2733,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // A BASE PACKAGE'S RUN-TIMES (design 2026-11-10 E5). f.ld += base_link_words(true); - // The base's run-time directory reaches the C link too (2026.11.11.1): + // The base's run-time directory reaches the C link too (2026.11.12.1): // a hosted cross over a sysroot payload links its compiler run-time from // the payload's own resource directory, and the C program's link is the // one that had nowhere to find it. diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index c12712e6d..8a4cb7e5d 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -540,7 +540,7 @@ std::expected step2_resolve_base(PrepareState& state) { } // A LOADER INSIDE A CROSS BASE IS THE SYSROOT'S, NOT THIS MACHINE'S - // (2026.11.11.1): a --sysroot link names it sysroot-relative + // (2026.11.12.1): a --sysroot link names it sysroot-relative // (/lib/ld-linux-aarch64.so.1), and so does the artifact's PT_INTERP -- // the form qemu -L , a runner declaration and the ld scripts // beside it (the base's own libc.so GROUPs /lib/...) all resolve. An diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index f5edafbbc..4479df961 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1428,7 +1428,7 @@ static std::expected step1_device_axis_and_layer_merge(Prepar state.add_once(state.m->buildConfig.ldflags, flag); state.add_once(state.m->buildConfig.graphLdflags, flag); } - // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // `[target..abi] longjmp` -- the third member (2026.11.12.1): // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary. The flag // changes the CODE emcc emits for the two calls, so it reaches both // sides -- every translation unit's flags (C and C++ alike; the C library diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index a632dec3a..d0bc179ad 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -74,7 +74,7 @@ import mcpp.bmi_cache; namespace mcpp::build { -// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.11.1, G3): a row whose +// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.12.1, G3): a row whose // system the graph supplies -- `x86_64-windows-musl` over openkal -- has no // paired payload, and its llvm binary carries no Windows headers, so // `winres.h` reaches neither of the include sources the toolchain states. The diff --git a/src/build/resources.cppm b/src/build/resources.cppm index 4dbe50470..47bad2cf9 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -101,7 +101,7 @@ std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple); // gcc's own) `-w64-mingw32/include` (design 2026-11-10 E3). One list for the // compile's `/I` and for the scan of what the script reads. // -// `mingw_headers` is the third source (2026.11.11.1, G3): the include +// `mingw_headers` is the third source (2026.11.12.1, G3): the include // directory of the MinGW-w64 sysroot payload the platform's rows declare, // which is where the Windows headers of a row whose system the graph supplies // -- `x86_64-windows-musl`, no paired payload, an llvm binary that carries no diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index fe7b326e6..475cf0ef9 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -686,7 +686,7 @@ ValidationReport validate_changed_artifacts( } // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot form, - // 2026.11.11.1): the artifact's loader and libc are the payload's, and + // 2026.11.12.1): the artifact's loader and libc are the payload's, and // the process these rules describe -- one process on this machine with // this machine's binding -- is not the one that would load it. The row's // declaration is the statement, exactly as an SDK's is above; the diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 1b769fa27..2798662ce 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -569,7 +569,7 @@ std::string unbuilt_row(std::string_view system, std::string_view arch); // §4.1): an ``, an `-` of that architecture, or every system // for `all`; this machine's system when it names none. // -// `all` is the DATA's answer (2026.11.11.1, G5), not a list spelled here: +// `all` is the DATA's answer (2026.11.12.1, G5), not a list spelled here: // this machine's system, every system a platform defines a row of for the // architecture (mcpp#911 G10), and every system the platform's runner states // a host of for it (`runner.hosts`) -- which is where FreeBSD enters, a @@ -2399,7 +2399,7 @@ ProgramRow program_row(std::string_view system, std::string_view arch, bool sing if (t && t->os == system && t->arch == arch) return { t->str(), "static", platform_ref_of(*P) }; } - // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.11.1, + // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.12.1, // G5), as FreeBSD's own request takes it (design 2026-11-10 E4): // `*-freebsd-musl`, openkal-musl over openkal-freebsd, one static // program with no C library of the system beneath -- built over the @@ -3540,7 +3540,7 @@ std::vector systems_for(const std::vector& stated, std // This machine's system, every system the platforms of this // process define a row of for the architecture (mcpp#911 G10), // and every system the carrier's runner states a host of for it - // (2026.11.11.1, G5) -- the rows the platform itself says its + // (2026.11.12.1, G5) -- the rows the platform itself says its // runner exists for, FreeBSD's among them. add(std::string(detail::host_system())); for (auto const* r : mcpp::toolchain::target::rows()) diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index ed668ce06..a1b0de2ae 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -1368,7 +1368,7 @@ bool host_can_serve(const triple::Triple& target) { if (target.is_musl()) return !crossArch || mcpp::toolchain::platform::host_arch == "x86_64"; // A CROSS WHOSE ROW NAMES A SYSROOT PAYLOAD IS SERVED WHEREVER - // THAT PAYLOAD IS PUBLISHED (2026.11.11.1, G1). llvm alone carries + // THAT PAYLOAD IS PUBLISHED (2026.11.12.1, G1). llvm alone carries // no glibc of another architecture, which is what `return // !crossArch` answered: no payload here produces it. The row's // own declaration is the better answer -- a provider that serves diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index 228e898bc..eb6815594 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -53,7 +53,7 @@ UTest dispatched_format_stages_without_it{"PackClosureUnavailableOutcome/Dispatc expect(eq(closure_unavailable_outcome(Format::Dispatched), ClosureUnavailableOutcome::StageWithoutClosure)); }}; -// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.11.1 answers +// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.12.1 answers // with the data): `` for every architecture of the legs, `-` // for one; unstated, this machine's. `all` is this machine's system, the // systems the builtin rows define for the architecture, and -- with a diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 5ce8bfc09..07d2a8940 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -68,7 +68,7 @@ UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [ expect(!(pg::assemble("app", {}).has_value())); expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - // FreeBSD is a POSIX system of the file since 2026.11.11.1 (G5): its + // FreeBSD is a POSIX system of the file since 2026.11.12.1 (G5): its // program rides behind the script head beside Linux's. What still has // no place is a system the script names no `uname` pattern for. expect(!(pg::assemble("app", { program("plan9", "x86_64", "x") }).has_value())); diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 4ef7d5271..5e86587a7 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -149,7 +149,7 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; -// The classification of a death by signal (2026.11.11.1, G6): the three +// The classification of a death by signal (2026.11.12.1, G6): the three // signals the mwrap of the C library's macros carries that an artifact of // another system's kernel interface dies by on its first kernel call, and // nothing else — a normal exit, an exit by a signal the classification diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index e529ba405..748cb3c85 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -637,7 +637,7 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie // the LLVM 23.1.3/glibc default, its deployed pack, GNU self-host, // all 147 suites and four real index members, from source. The tier // stays `preview` until the released engine is consumed on a clean - // ARM64 host (SPEC-009 §10.7). From 2026.11.11.1 the row names the + // ARM64 host (SPEC-009 §10.7). From 2026.11.12.1 the row names the // sysroot payload its C library comes from (G1) -- llvm alone // carries no glibc of another architecture -- and the linkage is // glibc's own dynamic one: the compiler run-time is the payload's From ff990b2e750e983a4c1b934c25726972adb5eacf Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:44:33 +0900 Subject: [PATCH 10/30] docs: the architecture record carries its front matter, as the gate requires --- .../2026-11-12-three-plane-platform-architecture-design.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md index 83ce7aab8..9c8e0fb22 100644 --- a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -1,3 +1,8 @@ +--- +subject: architecture +status: active +--- + # The three-plane platform architecture: mcpp × openkal × xim Date: 2026-11-12 From 891e1f5896be5710031297e1155209fbcdda3c41 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:44:52 +0900 Subject: [PATCH 11/30] docs: the agents index lists the new record --- .agents/docs/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.agents/docs/README.md b/.agents/docs/README.md index f7cd9cdd8..06873dfa9 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -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 @@ -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 From 262357aa935638e453e55731c16988e45ba27e68 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:59:00 +0900 Subject: [PATCH 12/30] 1076 runs again: the index carries the sysroot payload it skips without The exception belonged to the window where no hosted shard could install xim:aarch64-linux-gnu-sysroot; the index PR is merged, every shard that declares the payload's capabilities runs the test for real, and a test that runs does not belong in the table. --- tests/e2e/coverage-exceptions.tsv | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/coverage-exceptions.tsv b/tests/e2e/coverage-exceptions.tsv index 6e346fca3..13a168b7a 100644 --- a/tests/e2e/coverage-exceptions.tsv +++ b/tests/e2e/coverage-exceptions.tsv @@ -2,4 +2,3 @@ # One `\t` line each. A test that runs on some shard, or that a # workflow names, does not belong here, and the check fails if it is listed. 658_a_program_runs_on_an_attached_android_device.sh needs an Android device attached over adb; the android job of ci-linux-e2e.yml runs `mcpp test` on an emulator instead -1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh needs the xim:aarch64-linux-gnu-sysroot payload installed; no hosted shard installs it, and the test skips rather than guesses until the index carries the payload From e603382021afb6d56d80934554d3cad02609346f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 09:25:17 +0900 Subject: [PATCH 13/30] root_position_key keeps its cross-version shape; abi longjmp invalidates through the flags it renders The key is a contract between releases: an engine of one release finds a project's graph current through it, and 1068's inner build -- the subos's own engine -- is that reader by design. A field for the new member made every plan another release wrote look foreign, and the self-host shard measured it twice (1068, 1069). The member needs no field: it reaches the compile line through dialect_cxxflags, which the key already lists, and 1079 round-trips it unchanged. Threads and exceptions keep their fields -- they are switches that render no flag of their own. --- src/project.cppm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/project.cppm b/src/project.cppm index a96f8d347..4a34f1392 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,7 +1095,12 @@ export std::string root_position_key( auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; std::string abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" + b.abiExceptionsModel : "0") : "-"; - std::string abiLongjmp = b.abiLongjmpDeclared ? b.abiLongjmpModel : "-"; + // `abi longjmp` needs no field of its own here: it reaches the compile + // line through `dialect_cxxflags` (prepare adds the flag there), and this + // key is a CROSS-VERSION contract -- a project's plan stays current for an + // engine of another release reading it (1068's inner build, the subos's + // own engine, must find the graph current). Threads and exceptions have + // fields because they are booleans that render no flag of their own. if (applies) for (auto const& cc : m.conditionalConfigs) { if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared @@ -1105,7 +1110,9 @@ export std::string root_position_key( dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" + cc.abiExceptionsModel : "0"; - if (cc.abiLongjmpDeclared) abiLongjmp = cc.abiLongjmpModel; + // abi longjmp: rendered into dialect_cxxflags by prepare; adding + // a field here would orphan every plan a differently-released + // engine wrote (measured: 1068 on a self-host shard, twice). } list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); @@ -1119,7 +1126,6 @@ export std::string root_position_key( field("ios_deployment_target", b.iosDeploymentTarget); field("abi_threads", abiThreads); field("abi_exceptions", abiExceptions); - field("abi_longjmp", abiLongjmp); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); From cf1492ff55be35ac85bbb4a3f3fea71b33ba3de5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 12:04:10 +0900 Subject: [PATCH 14/30] split the per-subject manifest policy out of step13_windows_resources: the clang-tidy 400-line gate under src/build/prepare (the predicates the block already named) --- src/build/prepare/windows_resources.cpp | 75 +++++++++++++++++-------- 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index d0bc179ad..7d70ebb5b 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -214,6 +214,53 @@ static std::vector resource_subjects(PrepareState& state, BuildContext& return subjects; } + +// ─── The per-subject manifest policy: code page and long paths ──────────── +// +// The predicates step13 applies to every subject, as functions so the gate's +// 400-line budget holds (split at the boundary the block already had). + +static bool subject_declares_own_manifest(const mcpp::manifest::Manifest& M, + const Subject& S, bool hostToolBuild) +{ + return hostToolBuild + && std::ranges::any_of(M.resources.files, [&](const auto& f) { + const auto abs = (f.is_absolute() ? f : (S.dir / f)).lexically_normal(); + return mcpp::build::resources::scan_rc(abs).declaresManifest; + }); +} + +static std::string_view target_code_page(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + if (!t.windowsCodePage.empty()) return t.windowsCodePage; + return (hostToolBuild && t.is_program() && !ownManifest) ? "utf-8" : "legacy"; +} + +// `windows_long_paths` (design 2026-10-16 §11), with the same default: a +// host tool is long-path aware unless it says otherwise. +static bool target_long_paths(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + if (const auto& stated = t.windowsLongPaths) return *stated; + return hostToolBuild && t.is_program() && !ownManifest; +} + +// A test program carries the code page `[test] windows_code_page` gave it, +// and nothing by default: the host-tool default is a program's. +static bool target_takes_code_page(const mcpp::manifest::Target& t) +{ + return t.is_program() || t.kind == mcpp::manifest::Target::TestBinary; +} + +// Whether an image carries an application manifest of mcpp's: a UTF-8 code +// page or long paths asked for. +static bool target_manifested(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + return target_code_page(t, hostToolBuild, ownManifest) == "utf-8" + || target_long_paths(t, hostToolBuild, ownManifest); +} std::expected step13_windows_resources(PrepareState& state, BuildContext& ctx) { // ─── Windows resources: [resources] → a tracked link input (mcpp#365) ── // @@ -271,32 +318,16 @@ std::expected step13_windows_resources(PrepareState& state, B && (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary); }); }; - const bool ownManifest = hostToolBuild - && std::ranges::any_of(M.resources.files, [&](const auto& f) { - const auto abs = (f.is_absolute() ? f : (S.dir / f)).lexically_normal(); - return mcpp::build::resources::scan_rc(abs).declaresManifest; - }); - auto codePageOf = [&](const mcpp::manifest::Target& t) -> std::string_view { - if (!t.windowsCodePage.empty()) return t.windowsCodePage; - return (hostToolBuild && t.is_program() && !ownManifest) ? "utf-8" : "legacy"; + const bool ownManifest = subject_declares_own_manifest(M, S, hostToolBuild); + auto codePageOf = [&](const mcpp::manifest::Target& t) { + return target_code_page(t, hostToolBuild, ownManifest); }; - // `windows_long_paths` (design 2026-10-16 §11), with the same - // defaults: a host tool is long-path aware unless it says otherwise. auto longPathsOf = [&](const mcpp::manifest::Target& t) { - const auto& stated = t.windowsLongPaths; - if (stated) return *stated; - return hostToolBuild && t.is_program() && !ownManifest; - }; - // A test program carries the code page `[test] windows_code_page` - // gave it, and nothing by default: the host-tool default is a - // program's. - auto takesCodePage = [](const mcpp::manifest::Target& t) { - return t.is_program() || t.kind == mcpp::manifest::Target::TestBinary; + return target_long_paths(t, hostToolBuild, ownManifest); }; - // Whether an image carries an application manifest of mcpp's: a - // UTF-8 code page or long paths asked for. + auto takesCodePage = target_takes_code_page; auto manifested = [&](const mcpp::manifest::Target& t) { - return codePageOf(t) == "utf-8" || longPathsOf(t); + return target_manifested(t, hostToolBuild, ownManifest); }; const bool anyUtf8Image = std::ranges::any_of(M.targets, [&](const auto& t) { return active_target(t) && takesCodePage(t) && manifested(t); From 0819c80eccd64095f24b0a36bc3f22112b34397b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 12:08:09 +0900 Subject: [PATCH 15/30] keep the long-path lookup's if off the windowsLongPaths line: the ownership gate reads substrings --- src/build/prepare/windows_resources.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 7d70ebb5b..0d00e37c0 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -242,7 +242,8 @@ static std::string_view target_code_page(const mcpp::manifest::Target& t, static bool target_long_paths(const mcpp::manifest::Target& t, bool hostToolBuild, bool ownManifest) { - if (const auto& stated = t.windowsLongPaths) return *stated; + const auto& stated = t.windowsLongPaths; + if (stated) return *stated; return hostToolBuild && t.is_program() && !ownManifest; } From 9faee2799ce4f5839cfbcabbf24fec764ae1643d Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 16:16:19 +0900 Subject: [PATCH 16/30] wip: corrections C1-C3, C6-C10; system table, provider runner, one loads-on-host predicate --- ...system-architecture-optimization-design.md | 808 ++++++++++++++++++ .github/tools/check_system_names.py | 173 ++++ .github/tools/system-names.baseline | 18 + modules/manifest/src/platform.cppm | 25 +- modules/manifest/src/platform_providers.cppm | 5 +- modules/manifest/src/platform_toml.cppm | 52 +- modules/toolchain-model/build.mcpp | 57 +- modules/toolchain-model/data/builtin.toml | 102 ++- modules/toolchain-model/src/base.cppm | 4 + modules/toolchain-model/src/linkmodel.cppm | 70 +- modules/toolchain-model/src/target_rows.cppm | 24 + .../toolchain-model/src/target_schema.cppm | 20 + modules/toolchain-model/src/triple.cppm | 16 + src/build/execute.cppm | 170 ++-- src/build/flags.cppm | 62 +- src/build/hermetic.cppm | 4 +- src/build/prepare.cppm | 6 + src/build/prepare/base.cpp | 97 ++- src/build/prepare/platform.cpp | 52 +- src/build/prepare/state.cppm | 9 + src/build/prepare/toolchain.cpp | 20 +- src/build/prepare/windows_resources.cpp | 100 +-- src/build/resources.cppm | 22 +- src/build/runner_lookup.cppm | 86 +- src/build/runtime_validation.cppm | 44 +- src/pack/pack.cppm | 86 +- src/pack/pipeline.cppm | 8 +- src/pack/polyglot.cppm | 30 +- src/platform/platform.cppm | 4 + src/project.cppm | 17 +- src/toolchain/registry.cppm | 185 +--- ...gram_of_another_system_is_built_not_run.sh | 54 ++ ...stem_that_dies_at_once_names_the_runner.sh | 44 - ...single_files_cover_the_legs_and_systems.sh | 22 +- tests/fixtures/target-rows.snapshot | 10 +- tests/unit/test_pack_modes.cpp | 23 +- tests/unit/test_pack_polyglot.cpp | 26 +- tests/unit/test_runner_lookup.cpp | 48 +- tests/unit/test_toolchain_triple.cpp | 9 +- 39 files changed, 1813 insertions(+), 799 deletions(-) create mode 100644 .agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md create mode 100644 .github/tools/check_system_names.py create mode 100644 .github/tools/system-names.baseline create mode 100755 tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh delete mode 100755 tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh diff --git a/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md new file mode 100644 index 000000000..ec52c5a8a --- /dev/null +++ b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md @@ -0,0 +1,808 @@ +--- +subject: ecosystem +status: active +--- + +# 生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings) + +**日期:** 2026-11-12 +**状态:** 草案第二版,待评审。评审定稿前不实施任何一项。第二版并入:D16–D23 的裁决、CI 触发模型(5.11)、固件与裸机 +(5.12)、归属判据(P16)、层级三与固件的六个本地探针(第 8 节,结果在 `okexp/t3/PROBES.md`),以及多视角自审(第 14 节) +对第一版的七处修改。第 11 节列出仍需裁决的决定。 +**范围:** mcpp(引擎、内置数据、规范、CI);openkal 家族(规范、各实现、openkal-musl、openkal-llvm-runtime、 +openkal-image、openkal-platform);xlings 生态(xim-pkgindex、xpkgindex-update、xlings-res 资产、dirun);mtest(库与引擎)。 +验证工程(一个 C89 命令解释器)只用于检验,不在上游范围内,本文不点名。 +**输入:** +[2026-11-08 顶层设计](2026-11-08-ecosystem-top-level-design.md)、 +[2026-11-09 目标模型](2026-11-09-target-model-design.md)、 +[2026-11-10 跨平台生态方案](2026-11-10-cross-platform-ecosystem-design.md)(P1–P9、Q1–Q6、E/K/X 工作项仍然有效)、 +[2026-11-10 测试与解析补充](2026-11-10-test-and-resolution-supplement-design.md)、 +[2026-11-12 三平面架构](2026-11-12-three-plane-platform-architecture-design.md)(本文取代其第 3 节的"已成立"陈述,见第 13 节); +mcpp#924 的审计(2026-11-12,head `0819c80e`,分支二进制与 2026.11.10.2 的对照实测);验证工程的本地报告与缺口分析(G1–G8)。 + +**证据标记。** 本文每个事实带一个标记:〔测〕本地实测或 CI 日志可复核;〔读〕读代码或数据得出,未运行; +〔析〕分析与推断,实施前需要先做第 8.4 节的探针。 + +## 1. 目的 + +2026-11-10 方案回答了"能力够不够"。它落地后的一轮实施(mcpp#924 与外围九个 PR)暴露的是另一类问题:能力在增加, +但**同一类缺陷重复出现**——数据驱动的陈述与引擎里新增的系统名并存,本地通过而 CI 与新装用户路径不通过,资产修订两次 +违反同一条布局约定。本文不再逐项补能力,而是回答三个问题: + +1. 哪些结构使这些缺陷可以重复发生,改哪里能让它们**机械地不可能**(第 3–5 节)。 +2. 已合入与在途的改动里,哪些不合理,各自怎样纠正(第 6 节)。负责人对三个生态都有决定权,所以已合入项可以用追加 PR + 纠正;纠正仍然先在本文定稿。 +3. 以验证工程为尺,通用基础设施要到什么程度才算"支持一个程序去到所有平台",包括 32 位与 16 位 DOS 的可能性;这些设施 + 对验证工程之外的领域有什么收益(第 7–9 节)。 + +## 2. 现状台账 + +### 2.1 已合入并有门禁的(基线,不重做) + +| 项 | 版本 | 门禁 | +|---|---|---| +| E1–E10、E13、E14、E15:行与提供者由数据读取、共用入口分别编译、GNU ABI 资源、FreeBSD 两种身份、基座包、层依赖不成环、接口集合暴露 | mcpp 2026.11.10.1–.2 | e2e 1054–1064、1073 | +| E9、X1:`mingw-llvm` 提供者与 `mingw-w64-sysroot` 基座,三种宿主产出 Windows PE | 同上 | e2e 1063(wine) | +| K1、K5、K6、K8、K9:按接口绑定的 okrun、硬链接、stop word 计数、openkal-freebsd、FreeBSD 的 B 路线(K11 按系统约定查找程序:是否已发布待确认) | openkal 0.25 族、openkal-platform 0.26.0 | 各仓 CI;FreeBSD 虚拟机作业 | +| 修订通道:同一版本键下的矫正修订 | xpkgindex-update #9、#10 | 仓内 CI | +| G4:okrun 打开向上的相对名字 | openkal-image 0.6.2 | 仓内 CI | +| 测试体系迁到 mtest | 各仓 | 各仓 CI | + +### 2.2 已合入但需要纠正的(第 6 节给出方案) + +| 项 | 位置 | 问题 | 证据 | +|---|---|---|---| +| `aarch64-linux-gnu-sysroot` 2.44.3 修订 2 | xim #1140 | 归档顶层目录与资产名不一致;`lib/libc++.so` 是指向缺席文件的链接脚本;清单没有 `std-module`;`provides` 写 libgcc 而链接用 compiler-rt | 〔测〕 | +| llvm 23.1.3 修订 2 | xim #1140 → #1143 | 修订 2 使全平台安装失败约 1 小时 40 分后由修订 3 修复;索引 PR 的安装检查有 15 项被跳过 | 〔测〕 | +| FreeBSD 镜像的包名 | xim #1140、#1141 | `freebsd-vm` 与 `freebsd-disk` 两个包并存,约定文档写的是 `-disk-image`,三个名字 | 〔读〕 | +| FreeBSD 运行器 | openkal-freebsd #2 | 运行器声明是 README 里手写的绝对路径加环境变量,只有 x86_64;与 dirun 是同一机制的第二份实现 | 〔读〕 | +| dirun | Sunrisepeak/dirun #1 | 自带测试模块而不用 mtest;Windows 的 pty 是桩;mcpplibs 下留有已归档的同名空仓 | 〔读〕 | +| 评审规则 | xim #1140–#1143 | 在 `REVIEW_REQUIRED` 状态下以管理员权限合入 | 〔测〕 | + +### 2.3 在途未合入的(mcpp#924) + +| 项 | 状态 | 证据 | +|---|---|---| +| G3 资源头第三源、G5 `--systems all`、G6 异系统死亡诊断、G8 `abi longjmp` | Linux 腿绿;各有硬编码或语义问题(第 6 节 C6–C10) | 〔测〕〔读〕 | +| G1 `aarch64-linux-gnu` 交叉 | 只对纯 C 成立;C++ 两种写法都失败;产物的 `PT_INTERP` 与 `RPATH` 是构建机的绝对路径;CI 零覆盖 | 〔测〕 | +| 回归:GCC + `PayloadFirst` 无法链接 | `flags.cppm` 的 GCC 分支被改写;gcc 15.1.0 在 2026.11.10.2 可构建运行,分支上失败 | 〔测〕 | +| 回归:`--target aarch64-linux` 在 x86_64 主机上由 musl 静态变为 glibc 动态 | 行表快照与两个引擎对照 | 〔测〕 | +| 回归:e2e 295(aarch64 主机)、299(Windows 主机)每轮失败 | CI 日志 | 〔测〕 | +| 未定性:Windows 单元测试访问违例、macOS xcode-27 的 1068、Windows 的 860 | 原交接的"缓存毒化"解释与同一轮日志不符 | 〔测〕 | + +### 2.4 尚未实现的 + +| 项 | 来源 | +|---|---| +| K7、E11:32 位一类目标(规范的 32 位约定、ELF32 映像、32 位系统调用、`i686-*` 行的载荷) | 2026-11-10 方案 P5 | +| K2 的收尾、X4:第二类系统(OpenBSD、Haiku、GNU/Hurd)的基座包与 okrun;openkal-netbsd 与 K10 | 同上 P4 | +| K12:openkal-macos 经 libSystem | 同上,讨论项 | +| X5:xlings 的"任意宿主"写法(目标侧数据在三个宿主块里重复同一 URL) | 同上 | +| RP:运行器包(wine 没有 xpkg;qemu-user、adb、虚拟机没有统一声明形式) | 同上第 6.4 节 | +| 跨系统的 `mcpp test` 作为常设门禁 | 同上第 8.2 节 | +| UEFI 的平台包与 openkal 实现;固件与运行器所需的 xim 资产(OVMF、DOSBox、wine) | 探针已做(第 8 节),未开始 | +| DOS、SVR4、Serenity、Plan 9 | 勘测或未开始 | + +## 3. 诊断:六个结构性原因 + +审计发现的三十余个问题归为六个原因。每个原因对应第 4 节的一条原则与第 5 节的一处结构。 + +| # | 原因 | 表现 | +|---|---|---| +| R1 | **行是数据,读行的代码不是。** 行与提供者进了表,但"某个系统的载体以哪个身份构建""某个系统在 `uname` 里叫什么""宿主的系统是哪个"这类**系统级事实**没有表,于是每个消费点各写一个分支 | `pack.cppm` 的 `system == "freebsd"`;`polyglot` 的系统白名单;`execute.cppm` 的三元式;`windows_resources.cpp` 拼 `{arch}-windows-gnu`;`registry.cppm` 拼 `linux-{arch}` | +| R2 | **一条提供者条目承载两种载荷安排。** 原生与交叉写在同一条目上,行为随宿主分叉,"写出宿主自己的目标不改变任何东西"这一不变量随之失效 | e2e 295;默认身份漂移 | +| R3 | **链接模型没有单一求值点。** "C 库由谁供给"在五处用 `base.layers.cAbi.empty()` 重新推断,基座的两种布局走两条通道,其中一条不读折叠后的加载器 | 交叉产物烙入构建机路径;GCC 分支被顺手改坏 | +| R4 | **陈述先于门禁。** "已测""默认行为不变"写进了 PR 正文与设计记录,对应的门禁不存在、被跳过或是红的 | 1076 跳过即通过;三平面文档第 3 节;交接清单 | +| R5 | **资产准入只校验字节,不校验可用。** 修订通道核对摘要与平台齐全,不核对归档布局、清单内容与"装上之后能不能用" | llvm 修订 2;sysroot 修订 2 的三处缺陷 | +| R6 | **运行器仍是文档而不是数据。** 行表里没有"这个身份在这台主机上怎样启动"的条目,每个系统各有一份胶水 | FreeBSD 的手写路径;诊断里对 FreeBSD 产物建议 `qemu-aarch64-static`;三份虚拟机驱动 | + +## 4. 原则 + +2026-11-10 方案的 P1–P9 全部保留。新增六条,各自针对一个原因。 + +- **P10 系统级事实也是数据(R1)。** 凡是按系统名分支的地方,分支的条件上移为系统表的一个键。验收是机械的:引擎源码里 + 系统名的出现次数由一个 CI 检查持有,只许减少(第 5.2 节)。 +- **P11 一条提供者条目只陈述一种载荷安排(R2)。** 原生与交叉是两条条目,各有自己的 `hosts`、`sysroot`、`linkage` + 与优先级。推论:一个身份在其原生主机上,写与不写 `--target` 得到同一条链接行。 +- **P12 只写 `-` 的请求,其回答是契约(R2)。** 新增提供者不得改变任何主机上既有请求的回答。新的提供者以 + 低于现有回答的优先级进入;要改变缺省,是一次单独的、写进 CHANGELOG 的决定。这与 2026-11-10 的 Q3(默认 B 路线)一致。 +- **P13 链接模型求值一次,渲染不再推断(R3)。** "谁供给 C 库、sysroot 在哪、加载器写成什么、哪些目录进 RUNPATH" + 在 prepare 里求成一个值;`flags.cppm` 只渲染这个值,不再读行、提供者或基座。 +- **P14 陈述引用门禁(R4)。** 设计记录与 PR 正文里的"已测"必须指向一个在 CI 里运行的检查;本地测量写作"本地测得", + 并写明主机与载荷布局。跳过不是通过:跳过的脚本以跳过报告,覆盖检查把"无处运行"当作失败。 +- **P16 归属判据。** 一个事实放在哪里,依次问四句,第一个回答"否"的地方就是它的归属: + + | 问 | 回答"否"时归属 | + |---|---| + | 换一个程序,它还成立吗 | 程序自己的仓库(数据结构的大小、平台层的选择、进程与终端的语义) | + | 换一块板、一个固件、一个客体镜像,它还成立吗 | 板级包(平台包)或镜像资产附带的描述(链接脚本、设备地址、模拟器参数、退出约定、客体的包装) | + | 换一个系统,它还成立吗 | 行、系统表、提供者——主流系统在内置数据,长尾系统在平台包 | + | 都成立 | 引擎的机制,或 openkal 的规范 | + + 这是 docs/40 已有的一句话("位置是目标事实,选择是板级事实")向两端的延伸。 + +- **P15 新系统的准入以"引擎零改动"为验收(R1、R6)。** 一个新系统由数据(行、系统表条目、提供者)、包(实现、基座、 + 平台、运行器、镜像)与门禁(一致性、金丝雀)组成。准入需要改引擎,说明缺的是一个通用机制,先补机制,再以该系统验收。 + +- **P17 一个事实只有一个实现。** 引擎里同一个问题有两处回答时,收敛为一处,另一处改为读它。这次实施中遇到的重复 + 按 5.13 的清单逐项收敛;收敛不改变行为,由收敛前后的同一组测试证明。 + +## 5. 目标架构 + +### 5.1 四个平面与一张系统表 + +三平面记录把验证写成"归一化层"。本文把它升为第四个平面,因为 R4 与 R5 都出在这里。 + +``` + ┌──────────────────────────── 程序(语义面:进程模型、终端、路径写法,属于程序自己) ────────────────────────────┐ + │ │ + │ ① 身份面 ② 载荷面 ③ 运行面 ④ 验证面 │ + │ 行 + 提供者 工具链 + 基座包 运行器描述 + 运行器包 金丝雀 × 矩阵 × 准入门禁 │ + │ + 系统表 + 平台包 + 实现 + 镜像资产 │ + │ (mcpp 内置数据 (xim-pkgindex、 (内置数据 / 平台包 / (mcpp CI、xim CI、 │ + │ 与平台包) mcpp-index) xim 资产 / dirun) openkal-conformance) │ + └──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + 机制在 mcpp 引擎:选择规则 · 层的合并 · 链接模型 · 运行器协议 · 打包 · 修订通道的客户端 +``` + +| 平面 | 持有的数据 | 所在 | 不变量 | 门禁 | +|---|---|---|---|---| +| ① 身份 | 行、提供者、**系统表** | `builtin.toml`、平台包 | 目标差异全在数据(P10);一条条目一种安排(P11);请求的回答稳定(P12) | 行表快照;e2e 295、299;系统名检查 | +| ② 载荷 | 工具链、基座包、平台包、openkal 实现 | xim-pkgindex、mcpp-index | 构建主机只需要 mcpp;基座供给 C 库时宿主链接模型整体让位(P13) | 基座准入契约;修订通道的安装检查 | +| ③ 运行 | 运行器描述、运行器包、磁盘镜像 | 内置数据、平台包、xim | 构建不依赖运行器;每个身份在每台主机上要么能启动,要么指名缺哪个运行器 | 运行矩阵 | +| ④ 验证 | 金丝雀工程、目标矩阵、期望输出 | mcpp 仓库、各仓 CI | 陈述引用门禁(P14) | 覆盖检查;夜间矩阵报告 | + +### 5.2 系统级事实:先用已有的行键,系统表只补缺的一项 + +**问题。** 行回答身份级的问题。系统级的问题今天由代码回答。 + +**已有而未被消费的键〔读〕。** SPEC-016 §2.1 的行字段已经包括 `loader`、`execution`、`family`、`exe-suffix`、 +`object-format`。内置数据只在 FreeBSD 四行上写了 `family`,其余键没有值,于是代码里各有一张自己的表。所以这里的 +工作首先是**给已有的键填数据并让代码读它**,而不是新增 schema。 + +**新增的只有两处。** + +```toml +[[platform.system]] # 系统表:一个系统一条,内置数据与平台包同一 schema +name = "freebsd" +uname = "FreeBSD" # 多系统单文件的 POSIX 头里识别该系统的词;没有该键的系统不进这种文件 + +[[platform.provider]] +name = "openkal" +serves = ["x86_64-windows-musl"] +system-headers = "xim:mingw-w64-sysroot@20261104-2" # 系统自身接口的头文件由谁给;只有资源编译等需要它的步骤读 +``` + +"某个系统上自带运行器以哪个身份构建"不进系统表:它只对带运行器与载体的平台有意义,按 P16 属于那个平台。平台的 +`runner` 陈述里 `hosts` 的每一项可以带它的身份(`{ host = "freebsd-x86_64", built-as = "x86_64-freebsd-musl" }`), +由 openkal-platform 写出。 + +**每个硬编码点的去向。** + +| 现在的代码 | 改为读 | +|---|---| +| `pack.cppm` 对 linux、macos、windows、freebsd 各一个分支 | 平台 `runner.hosts` 各项的 `built-as` | +| `polyglot.cppm` 的系统白名单与 `uname` 词 | 系统表的 `uname`;没有该键的系统被拒绝并指名 | +| `execute.cppm` 把宿主映射成三个系统名之一(`unreachable_execution` 里原有一处,mcpp#924 又加一处) | 宿主身份所在行的 `execution`(缺省为 os 段)与目标行的比较 | +| `windows_resources.cpp` 拼 `{arch}-windows-gnu` 再找服务它的提供者 | 选中提供者的 `system-headers` | +| `registry.cppm::host_can_serve` 拼 `linux-{arch}`,musl 另有 `x86_64` 特例 | 只调用 `available_on(提供者, 宿主)`;该函数里不再有架构或系统判断 | +| `linkmodel.cppm::distro_loader_path` 按三元组前缀返回 `/lib64` 或 `/lib` | 行的 `loader`(已有键,补数据) | +| 运行器建议文字里的字面量 `qemu-aarch64-static`(三处) | 5.5 的运行器陈述;没有陈述时只写声明的形状,不写示例程序 | + +**机械门禁。** 新增检查脚本,统计 `src/` 与 `modules/`(除 `modules/platform/` 与数据文件)里系统名词表的出现次数, +与仓库里的基线文件比较:多于基线则失败,少于基线则要求更新基线。P10 由此从评审意见变成检查。 + +**第二个使用者。** 任何平台包定义的系统(长尾系统、DOS、UEFI)自动得到多系统单文件与"已构建,未运行"的回答,不需要 +引擎改动。 + +### 5.3 提供者:拆分、优先级与等级 + +**拆分(P11)。** `aarch64-linux-gnu` 的两种安排写成两条: + +```toml +[[platform.provider]] # 原生:宿主的载荷 +name = "glibc" +serves = ["aarch64-linux-gnu"] +priority = 100 +hosts = ["linux-{arch}"] +toolchain = "llvm@23.1.3" + +[[platform.provider]] # 交叉:基座包 +name = "glibc-sysroot" +serves = ["aarch64-linux-gnu"] +priority = 40 # 低于 musl-gcc 的 100 与 openkal 的 50(P12) +hosts = ["linux-x86_64"] # 归档与宿主无关,范围随验证扩大 +toolchain = "llvm@23.1.3" +sysroot = "xim:aarch64-linux-gnu-sysroot@2.44.3" +linkage = "dynamic" +``` + +这是 `mingw`(10)与 `mingw-llvm`(5)已有的写法。结果:aarch64 主机上两种拼写仍取原生条目(e2e 295 恢复);x86_64 主机 +上 `aarch64-linux` 仍回答 musl,`aarch64-linux-gnu` 写全才取交叉条目。 + +**等级随条目(建议)。** `tier` 现在在行上。同一身份的原生安排是 preview、交叉安排尚无门禁时,行的等级说不清是哪一个。 +提供者条目可以带自己的 `tier`,缺省继承行;`mcpp toolchain list` 显示选中条目的等级。 + +### 5.4 链接模型:一次求值 + +**设计。** prepare 的末尾得到一个值,此后只有它被读取: + +``` +LinkModel + c_library : host-payload | base-package | graph | sdk 谁供给 C 库 + sysroot : path? base-package:包根 + search : { crt: [path], lib: [path] } -B 与 -L + loader : { file: path, name: path } 链接时用到的文件;写进产物的名字 + runpath : [path] 只含产物运行时确实存在的目录 + runtime : { rtlib, unwind, cxx, resource_dir } +``` + +**规则。** + +1. `c_library` 的四个值两两互斥,由选中的提供者条目与依赖图决定;`flags.cppm` 里的五处推断删除。 +2. **加载器的名字与文件分开。** 产物在本机运行的安排(原生的宿主载荷):`name = file`,即今天的密封行为,不变。产物不在 + 本机运行的安排(交叉、另一系统):`name` 取行的 `loader`(SPEC-016 已有的键),`runpath` 为空。构建机的安装路径不进入 + 这类产物。 +3. 运行器拿到 sysroot:运行器模板增加 `{sysroot}` 占位,交叉 glibc 产物的缺省运行器是 `qemu- -L {sysroot} {}`。 +4. 基座包只有一种读法。"包根即 sysroot"与"生态自己的 glibc 包布局"今天走两条通道;基座包的清单是它的陈述,带清单的 + 一律按包根 sysroot 处理,布局探测只用于没有清单的目录。 + +**门禁:链接行的金样。** 渲染是纯函数,所以不需要工具链就能测:对 `编译器族 × c_library × 链接形态 × 原生/交叉` 的每个 +组合保存一份期望的链接词表,作为单元测试。这次 GCC 分支的回归(gcc 15.1.0 的 `-B`、`-L`、加载器整组消失)在这种测试下 +是一行差异。CI 另加一条 gcc `PayloadFirst` 的真实构建腿。 + +### 5.5 运行面:运行器是提供者的陈述 + +**已有的机制〔读〕。** 运行器今天可以写在四处:平台的 `runner = { cmd, host, hosts }`、`[platform.runners]` 的具名 +运行器、项目与本机配置的 `[target.] runner`、作为目标工具的运行器包(`[target..tools]`,docs/41 的运行协议)。 +SPEC-016 §6 已经规定:行的 `execution` 不是本机系统、又没有运行器时,报告"已构建,未运行"。缺的只有一处:**内置提供者 +不能陈述运行器**,于是 wine、qemu-user、虚拟机这些缺省安排没有数据可写。 + +**设计。** 不新增表。提供者条目可以带 `runner`,形状与平台的相同,外加它依赖的包: + +```toml +[[platform.provider]] +name = "openkal" +serves = ["*-freebsd-musl"] +runner = { cmd = ["dirun", "--profile", "{image}/dirun.toml", "--", "{}"], hosts = ["linux-*", "macos-*"], + requires = ["xim:dirun", "xim:freebsd-disk-image@14.5", "xim:qemu-system"] } +``` + +**规则。** + +1. 优先级沿用 P8:项目 `[target.] runner` 先于本机配置,先于平台,先于提供者。 +2. `requires` 里的包在第一次 `mcpp run` 或 `mcpp test` 需要时按工具链同样的同意方式安装;构建从不安装它们。 +3. 模板的占位:`{}` 是产物,`{sysroot}` 来自链接模型(5.4),`{image}` 是 `requires` 中镜像包的根。 +4. **不运行另一系统的程序来观察它。** 内置行与平台定义的行由同一个谓词判断:行的执行系统不是本机、又没有运行器,就在启动 + 之前回答"已构建,未运行",指名身份与可写的声明。另一系统的系统调用号在本机内核上指向别的操作,启动它的后果不确定 + (第 14 节第 2 条)。诊断的文字由数据生成,只有一处。 + +**运行器的五种来历**(是分类,不是 schema 里的键): + +| 来历 | 含义 | 实例 | 取状态的方式 | +|---|---|---|---| +| 用户态模拟 | 同内核、异架构 | `qemu- -L {sysroot}` | 进程的退出码 | +| 接口的宿主实现 | 另一系统的接口在本机的实现 | wine;DOSBox(探针 P2:0.33 秒,退出码如实) | 进程的退出码,或客体包装写出的状态 | +| 映像的加载器 | — | okrun、node | 进程的退出码 | +| 板 | 产物本身就是模拟的机器启动的东西 | qemu 加固件(探针 P5:OVMF 下 2 秒) | 板级包约定的出口(5.12) | +| 客体 | 启动目标系统,在里面运行产物 | dirun 加磁盘镜像 | 标记协议 | + +"客体"只有一个实现:dirun。openkal-freebsd 的 `vm-run.py` 在 dirun 等价后退役。 + +**xim 侧。** 缺的包:wine、DOSBox、OVMF(本机的 `/usr/share/OVMF` 是宿主依赖,按 P9 应是 xpkg)。命名统一为约定文档的 +`-disk-image`;`freebsd-vm` 与 `freebsd-disk` 保留为指向它的别名一个周期后移除。 + +### 5.6 载荷面:基座包的准入契约 + +基座包的 schema 不变(2026-11-10 方案 E5)。增加的是**准入**:一个基座包进入索引之前,由索引仓库的 CI 用已发布的 mcpp +对它跑一组固定探针。 + +| 编号 | 探针 | 这次会拦下的缺陷 | +|---|---|---| +| BP1 | 归档顶层目录等于资产文件名主干(含 `-r`) | llvm 修订 2、sysroot 修订 2 | +| BP2 | 清单能被引擎解析、无未知键;`provides` 的每一层在包内有对应的文件 | `compiler-runtime=libgcc` 而实际给的是 compiler-rt | +| BP3 | C 程序:静态与动态各链接一次 | — | +| BP4 | C++ 程序(头文件):静态与动态各链接一次 | `libc++.so` 指向缺席的 `libc++.so.1` | +| BP5 | 声明 c++-abi 的包:`import std` 的程序构建 | 没有 `std-module` | +| BP6 | 有运行器的身份:BP3–BP5 的产物运行,退出码与输出相符 | — | +| BP7 | 产物里没有构建机的路径(`PT_INTERP`、`RUNPATH`、调试信息之外的字符串) | 烙入的加载器 | + +BP1 与 BP2 的布局部分写进 xpkgindex-update 的 guard,对所有资产生效;BP3–BP7 是基座包专有的作业。**安装检查不可跳过:** +一个改了某个包的索引 PR,必须在该包声明的每个宿主上真实安装它一次;被跳过的安装检查视为失败。 + +镜像一致性:CN 镜像滞后时客户端装到旧字节。修订通道的客户端在摘要不符时回落到另一个镜像并报告,而不是由每个 CI 作业 +手工钉 GLOBAL。 + +### 5.7 验证面:金丝雀与矩阵 + +**金丝雀。** mcpp 仓库持有两个中性的小工程,它们把验证工程对生态的要求(第 7 节)写成断言: + +| 工程 | 形状 | 断言 | +|---|---|---| +| `canary-c` | C89、只用 POSIX、两个程序共享源码、带资源脚本、用 `sigsetjmp`、启动子进程、读写文件 | V1–V6、V8–V10 | +| `canary-cxx` | C++23 模块、`import std`、异常、线程 | 每个声明 c++-abi 的安排 | + +**矩阵是数据。** 一份表列出 `身份 × 主机 × {构建, 静态链接, 动态链接, import std, 运行, 测试, 打包, 映像}` 的期望 +(通过、拒绝并给出原因词、不适用)。夜间作业对全表运行两个金丝雀,产出覆盖报告;报告是 CI 的产物。"十八个目标全部 +通过"这类句子从此引用报告的运行号。 + +**顺序。** 不变量测试(295、299 一类)在功能测试之前运行,失败则后者不运行。能力词由 CI 作业预装满足;一个脚本要求的 +能力没有任何作业提供,覆盖检查失败——这条已有,补上的是"删除豁免需要引用一次真实运行"。 + +### 5.8 openkal 侧 + +| 项 | 内容 | +|---|---| +| 环境的三类 | 2026-11-10 方案分了两类(内核 ABI 稳定、libc 才是稳定 ABI)。增加第三类:**单任务环境**(UEFI、DOS、引导链)——没有调度器,启动的程序运行到结束后启动者才继续。规范文本没有禁止这样的实现,并发相关的能力已是可不声明的属性(T9);需要补的是一句明示与一致性套件的覆盖。它有三个使用者,先于其中任何一个的实现 | +| K7 提前 | 32 位一类目标从"设计讨论"提为正式阶段。i686 Linux、SVR4、32 位 DOS、armv7 都卡在它上面;先做 `i686-linux-musl`,因为它有 qemu-user 可以立刻验证 | +| 运行器统一 | 各实现的开发用虚拟机脚本收敛到 dirun;实现仓库只留控制台驱动词 | +| 接口声明的三方核对 | 清单的 `provides-interfaces`、`kal_interfaces()` 的回答、实际导出的名字,一致性套件核对(K1 的配套项,确认是否已全部落地) | +| 探针的跳过条件 | openkal-emscripten 的 longjmp 探针靠匹配引擎的报错文字来判断跳过;改为读引擎版本 | + +### 5.9 测试体系的收敛:e2e 与 bench 归到 mtest 的 C++ 用例 + +**现状〔测〕。** e2e 是 762 个 bash 脚本、约 9 万行,作为 mtest 的 0 级程序运行(一个脚本一个结果)。一次 PR 运行是 +72 个作业、518 作业分钟、墙钟约 45 分钟(运行 43146150459)。 + +| 事实 | 数值 | 含义 | +|---|---|---| +| 同一批 189 个测试,Windows 与 macOS 的耗时 | 71.0 分钟对 21.0 分钟,3.4 倍 | Windows 上 bash 的进程创建是主要开销之一 | +| Windows 的耗时分布 | 219 个测试共 79.7 分钟;耗时最长的 18 个占一半 | 先治长尾,而不是平均用力 | +| 要求 `unix-shell` 的脚本 | 154 个 | 两成的套件在 Windows 上不运行,原因是写法而不是被测行为 | +| 要求 `python3`、`jq` 的脚本 | 99、11 个 | 测试依赖宿主工具,与 P9 不符 | +| 不调用构建、运行、测试、打包的脚本 | 81 个 | 这些是解析与清单层面的断言,不需要是端到端测试 | +| Linux 的 8 个分片在这次运行里的测试时间 | 0.8、1.0、7.3、0.0 分钟…… | 分片数固定,作业的准备时间超过了它运行的测试 | +| 单元测试(168 个文件)在 Windows 上 | 构建 270 秒,运行 11 秒 | C++ 测试的成本在编译,不在运行 | + +**方向。** 收敛,但不是逐个翻译。脚本按三类处理: + +| 类 | 判据 | 去向 | 估计占比〔析〕 | +|---|---|---|---| +| 下沉 | 断言的是解析、清单、选择规则的回答,不需要真实工具链 | 所属模块的单元测试(`modules//tests/`),毫秒级,每个宿主都运行 | 一成到两成 | +| 合并 | 构建一个小工程再断言输出 | 按子系统合成 mtest 的 `E2E` 对象;同一对象的用例共用预热的 home 与夹具工程;原脚本名的句子成为用例名,原编号留作标签 | 七成 | +| 保留 | 断言的是机器状态:全新安装、升级、容器内无宿主工具链、模拟器 | 仍是 0 级脚本,数量由基线文件持有,只许减少 | 一成 | + +**迁移的规则。** 沿用 2026-11-10 方案第 16 节:保留每一条断言,不能迁移的在 PR 中点名;每个子系统先以原脚本验证,再以新 +用例验证,两次运行都引用。迁移按子系统分批,一批一个 PR。编号标签与映射表使"旧脚本的断言去了哪里"可查。 + +**测试支持层。** `tests/support/` 下一个小模块,建立在 `mcpplibs.mtest.e2e` 之上:声明式的夹具工程(文件表到目录树)、 +`mcpp(...)` 调用(选择共用的预热 home 或全新 home)、对 `build.ninja` 的行与 JSON 文档的读取、对 ELF/PE/Mach-O 头的 +读取(引擎的打包模块已有这些读取,不再调用 `readelf`、`file`、`llvm-readobj`)。失败时打印可以直接粘贴到 shell 的命令行, +保住脚本"读起来就是一次会话"的优点。 + +**对 mtest 的升级要求。** mtest 已有的〔读〕:集合(`smoke`、`regression`、`pr`、`full`、`release` 与自定义)、按改动选择 +(`--since`、覆盖图、`file:` 产物、程序的 `sources`)、按历史分片(`--shard`)、预算(`--budget`)、资源与运行级夹具、 +`skip_if`、bench 的判定策略、`gate`、`ci report` 与 `ci timeline`。需要补的: + +| # | 能力 | 取代什么 | +|---|---|---| +| M1 | 能力词的探测写在 `mtest.toml`(探测命令到能力词),引擎逐机器求值;"某能力词无任何作业提供"由引擎报告 | `run_all.sh` 里的探测与白名单;`check_e2e_coverage.py` | +| M2 | 0 级程序的跳过约定(一个专用的退出码),使保留类脚本的跳过也是独立结果 | 脚本里的 `echo PASS (skipped)` | +| M3 | e2e 模块:环境、工作目录、标准输入、超时;输出匹配器;失败时的会话记录与保留的目录 | 脚本里的 `grep`、`sed`、`jq`、`tail` | +| M4 | 交互会话:对伪终端或管道的 expect/send(D23:伪终端在 `mcpplibs.os`) | dirun 的控制台模块;终端类脚本 | +| M5 | 由数据展开的参数化用例;长时间 bench 的续跑 | `bench/matrix.json` 加 `run-standard.sh` 加 `report.py` | +| M6 | `mtest ci plan`:由选择与历史给出每个宿主的作业矩阵(是否有工作、分片数、每片的预计时间) | 固定的分片数;固定的作业集合 | + +M1、M2、M3 是迁移的前置;M4、M5、M6 随用到它们的批次。每项实施前先核对 mtest 的现状,已有的不重做。 + +**bench。** `bench/` 有两部分。引擎对比的 harness 是一个 C++ 程序,自带协议、日志、注册表与运行器,外加 `run-standard.sh`、 +`harness.sh`、`report.py`;`bench/tests/engine.cpp` 已经是 mtest 的 Bench 用例。收敛为:各引擎的适配器留作夹具代码, +矩阵由 M5 展开为参数化的 Bench 用例,日志与续跑交给 mtest 的存储,"不可发布"的判定交给 `mtest gate`,报告由 +`mtest report` 生成。`proto-bmi-release/` 是已结束的实验,移入结果目录。 + +**CI 的触发与编排**见 5.11。 + +**不做的。** 不为了统一而翻译"保留"类脚本;不在迁移中顺手改被测行为;不在第 10 节的 S1(纠正)完成之前开始迁移。 + +### 5.10 dirun 的形状 + +dirun 是 5.5 中 `machine` 类运行器的唯一实现,所以它的形状属于本方案。 + +**现状〔读〕。** 产品代码约 1,300 行,分在四个工作区成员(protocol 95 行、console 246 加 pty 212 行、runner 418 行、 +testing 106 行)与根包(cli 131 行、main 57 行);另有约 1,100 行的 Python 参考实现与它的测试;README 约 150 行。 + +| 问题 | 说明 | +|---|---| +| 拆得过细 | 四个包各自的清单、版本与依赖声明,服务于一千余行代码;包的边界应当对应"有第二个使用者的库",现在没有 | +| 重新实现了生态已有的库 | `modules/testing` 对应 `mcpplibs.mtest`;`src/cli.cppm` 手写参数解析,对应 `mcpplibs.cmdline` | +| 两份实现 | Python 参考被称作"冻结的规范",但它是第二份要维护的实现;合同应当是条款加测试 | +| "与客体系统无关"只成立一半 | 标记协议的包装(`stty -echo`、`rc=$?`、单引号转义)是 POSIX shell 的;`--machine` 到架构的映射写在代码里。客体是 DOS 或 Windows 时两者都不成立 | +| Windows 宿主 | pty 是桩。串口接到标准流时未必需要伪终端:用管道加带超时的读取,三个宿主是同一份代码〔析〕 | +| README | 定位、用法、内部机制(标记协议细节、FAT 工具的回落)、未来计划混在一处 | + +**建议的形状。** + +``` +dirun/ + README.md 一句定位 · 三行上手 · 退出码表 · 指向 docs/ 的链接(五十行以内) + mcpp.toml 一个包;依赖 mcpplibs.os、mcpplibs.cmdline、JSON;开发依赖 mcpplibs.mtest + src/ main.cpp · cli.cppm(cmdline 的声明)· runner.cppm · protocol.cppm · console.cppm + profiles/ 内置的缺省客体描述(POSIX 登录式) + tests/ mtest:单元用例 · 合同条款(cts)· 一个真实引导的 E2E(要求 qemu 能力词) + docs/ contract.md · profiles.md +``` + +- **客体描述是数据。** 引导脚本、命令的包装模板、取状态的写法、转义规则、挂载文件盘的命令,合成一份描述文件,随 + `-disk-image` 资产一起发布。dirun 的核心只有 expect/send、标记切分、覆盖盘与退出码。探针 T4 是一个实例: + DOS 的命令解释器取状态的写法与 POSIX shell 不同。 +- **两种方式,一份核心。** "客体"方式启动一个系统并在其中运行产物;"载荷"方式里产物本身就是被启动的东西(固件应用、 + 裸机程序),状态的读法由板级包的描述给出(5.12)。两者共用串口、超时与清理。 +- **控制台模块的归属。** expect/send 是通用能力,第二个使用者是 mtest 的交互会话(M4)。伪终端属于 `mcpplibs.os`, + expect/send 属于 mtest 的 e2e 模块;dirun 依赖它们。迁出之前留在 `src/console.cppm`。 +- **生态库不够用时升级生态库。** `mcpplibs.cmdline` 若缺少可重复选项或 `--` 之后原样传递,补在 cmdline 里。这条对 + 生态内的每个项目成立:项目内重新实现是对库的缺陷报告没有被提交。 +- **定位一句话。** dirun 在一个引导起来的磁盘镜像里运行一条命令,返回它的输出与退出码;它之于一台虚拟机,如同 + qemu-user 之于一个异架构程序。它不构建镜像、不管理虚拟机、不做配置。 + +### 5.11 CI 的触发模型 + +收敛到 mtest 之后,"这次运行跑什么"由三个轴决定,每个轴都是数据:**事件**(什么时候)、**集合**(跑哪些用例,mtest 的 +集合与按改动选择)、**宿主**(在哪里跑,由 `mtest ci plan` 给出)。 + +**事件对集合。** + +| 事件 | 集合 | bench | 宿主与作业数 | +|---|---|---|---| +| PR 的每次推送 | `pr`:改动影响到的用例,加 `smoke`、`regression`、不变量 | 不跑 | 只有选择非空的宿主;分片数随工作量 | +| PR 带标签 `ci:full`,或手动触发并选 `full` | `full` | 不跑 | 全部宿主 | +| PR 带标签 `ci:bench` | `pr` | 引擎自身开销的 Bench 用例,与 main 的基线比较 | 一台 Linux | +| 合入 main | `full`;更新存储(历史、覆盖图、bench 基线) | 引擎自身开销 | 全部宿主 | +| 夜间 | `full`、金丝雀矩阵、保留类脚本、基座准入的抽查 | 引擎自身开销的趋势 | 全部宿主 | +| 发布 | `release`:`full` 加 bench 判定(`mtest gate --set release`) | 判定 | 全部宿主 | +| 手动触发 | 输入:集合、宿主、用例的模式、是否带 bench | 按输入 | 按输入 | + +- **bench 分两种。** 引擎自身开销的 Bench 用例(`bench/tests/engine.cpp`)便宜,现在随每次 CI 的 Linux 单元测试作业运行; + 改为只在 main、夜间、发布与 `ci:bench` 运行。引擎对比的标准数据集要数小时,只由手动触发,不挂在任何事件上。 +- **全量可以手动。** `workflow_dispatch` 的输入就是 `mtest run` 的选择参数;本地 `mtest run --set full` 与 CI 用同一份集合 + 定义,所以"CI 跑了什么"在本地可以原样重复。 +- **按改动选择的安全边界。** 覆盖图不认识的用例视为受影响;覆盖图过期(`max_age`)时全部视为受影响;内置数据文件 + (`builtin.toml`)的改动影响全部目标类用例;不变量与 `smoke` 永远在 `pr` 里。选择只会多跑,不会漏跑。 + +**依赖与并行。** + +``` + changes ──► plan(mtest ci plan:每个宿主的选择、分片数、预计时间;无构建,约一分钟) + │ + ├─► build(linux) ──► gate(linux) ──┬─► unit · e2e 分片 × N(linux) + ├─► build(windows) ─► gate(windows) ┼─► unit · e2e 分片 × N(windows) + ├─► build(macos) ──► gate(macos) ──┼─► unit · e2e 分片 × N(macos) + └─► build(linux-arm) ─► gate ──────┴─► 需要多个构建的作业:交叉、目标矩阵、升级、openkal +``` + +- **构建立即并行开始。** 构建是每个宿主的长杆(Linux 约 16 分钟),不排在任何闸的后面。 +- **每个宿主一道闸。** 构建完成后先跑该宿主的 `smoke` 与不变量(预算 2–3 分钟),通过才扇出该宿主的分片;任一宿主的闸 + 失败,取消整次运行。绿的路径不增加等待,红的路径在第一个闸处停下。 +- **测试程序随构建产出。** 构建作业同时编译被选中的测试程序,与 mcpp 一起作为产物下发;分片不编译。 +- **作业内并行靠资源声明。** 用例声明它独占的东西(一个 home、整台机器),其余并行;全局的 `E2E_JOBS=2` 取消。分片只在 + 一个宿主的预计时间超过预算时才出现:`分片数 = ⌈预计时间 ÷ 每片预算⌉`。 +- **夹具只建一次。** 预热的 home 与模块缓存是运行级夹具,一个作业建一次,用例声明使用它。 + +**时间成本的账。** 现状(PR 运行 43146150459):72 个作业、518 作业分钟、墙钟约 45 分钟;其中 Windows e2e 的测试时间约 +80 分钟,最长的 18 个占一半。目标〔析,落地时以测量校正〕:PR 墙钟 30 分钟以内,作业分钟减少三成以上,Windows e2e 的测试 +时间 35 分钟以内,失败的推送在闸处停下(数分钟的测试成本,不含已开始的构建)。`mtest ci timeline` 每夜产出一份成本 +报告:作业分钟、关键路径、最慢的用例;集合的预算被突破时由 `gate` 报告。 + +### 5.12 固件与裸机 + +**已有的〔读〕〔测〕。** docs/40 的分层是:引擎持有 ISA 档与 freestanding 的链接行,目标持有编译器与 C 库,**板级支持包** +(一个平台包)持有启动对象、链接脚本与模拟器调用。十余个裸机行是内置数据;RISC-V 与 Cortex-M 的构建与 qemu 运行在 CI 里; +openkal-opensbi 是写在 SBI 上的实现,提供核心三个接口加 `time` 与 `env`,如实缺席 `fs`、`process`、`task`。 + +**这次测到的。** UEFI 不需要引擎认识它(探针 P5):一个三十行的平台包用 `[[platform.define]]` 定义 `x86_64-uefi` +(`clang-triple = "x86_64-unknown-uefi"`、`object-format = "pe"`、`execution = "uefi"`、`exe-suffix = ".efi"`), +已发布的 2026.11.10.2 构建出 EFI 应用,在 qemu 加 OVMF 下打印并返回状态,墙钟 2 秒。 + +**归属(P16 的应用)。** + +| 事实 | 归属 | 不归属 | +|---|---|---| +| ISA、对象格式、产物后缀、执行系统 | 行(数据) | 引擎的分支 | +| 入口符号、子系统、链接脚本、内存布局 | 固件或板的平台包 | mcpp、openkal | +| 设备地址、模拟器的机器型号与参数、固件文件 | 板级包;固件文件是 xim 资产 | mcpp、openkal | +| 状态怎样回到宿主(调试端口、semihosting、测试设备、输出里的标记) | 板级包:它既给程序一侧的出口,也给运行器一侧的读法 | 引擎、dirun 的核心 | +| 固件提供的服务到 `kal_*` 的翻译 | 每种固件接口一个 openkal 实现(openkal-opensbi 已有;UEFI 的启动服务是下一个) | openkal 规范(规范不认识任何固件) | +| 程序用到哪些接口、没有时怎么办 | 程序 | 生态 | + +**三点设计。** + +1. **固件环境是平台包,不是内置行。** UEFI 先以平台包定义自己的行(今天就能做);出现第二个要用同一身份的平台包时, + 再把行升为内置数据。板与固件的差异永远在包里。 +2. **状态的出口由板级包两头负责。** 探针里 x86 的调试端口把状态变成 `状态×2+1`,RISC-V 的测试设备与 ARM 的 semihosting + 各有约定。这些都不进引擎:板级包给程序链接一个出口实现,并在运行器陈述里写出读法。dirun 增加"载荷"方式(产物即 + 被启动的东西),读法来自描述文件,与"客体"方式共用串口、超时与清理。 +3. **固件属于单任务环境。** UEFI 的 `StartImage` 与 DOS 的 EXEC 一样,被启动者结束后启动者才继续。5.8 的规范问题因此有 + 三个使用者(DOS、UEFI、未来的引导链),值得在规范里回答一次。规范文本没有禁止这种实现,并发相关的能力(通道、终止) + 已经是可不声明的属性〔读〕;缺的是一句明示与一致性套件的覆盖。 + +**收益。** openkal 的 UEFI 实现之上是同一个 openkal-musl 与同一个 C++ 运行时:UEFI 应用可以用 C++23 模块与 `import std` +编写,在 CI 里两秒跑完。这对引导加载器、固件工具与教学是直接可用的。 + +## 6. 纠正清单 + +每项:现状、纠正、兼容性。C1–C11 在 mcpp#924 合入之前完成;C12–C16 是对已合入项的追加 PR。 + +| # | 现状 | 纠正 | 仓库 | 兼容性 | +|---|---|---|---|---| +| C1 | `flags.cppm` GCC 分支只在 `Sysroot` 模式出链接词,丢了 `crossTarget` 与 `PayloadFirst` 的整组 | 恢复原分支;随 5.4 的金样测试与 gcc `PayloadFirst` 腿一起进 | mcpp | 恢复 2026.11.10.2 的行为 | +| C2 | 原生与交叉共用一条 `glibc` 条目 | 按 5.3 拆分,交叉条目优先级 40 | mcpp(数据) | `aarch64-linux` 的回答与 2026.11.10.2 相同 | +| C3 | 交叉产物的 `PT_INTERP`、`RPATH` 为构建机路径 | 按 5.4:行的 `loader`、空 `runpath`、运行器的 `{sysroot}` | mcpp | 新能力,无既有使用者 | +| C4 | sysroot 修订 2 的四处缺陷 | 发修订 3:顶层目录对齐、补 `libc++.so.1` 一族、清单加 `std-module`、`provides` 与内容一致;先过 BP1–BP7 | xim、xlings-res | 同一版本键的矫正修订 | +| C5 | 1076 在 CI 无处运行,跳过时打印 PASS | 一个 Linux 分片预装基座并提供能力词;脚本增加 `PT_INTERP`、C++、`import std` 三条断言;跳过如实报告 | mcpp | — | +| C6 | 五处系统名硬编码 | 按 5.2:先填已有的行键,新增只有 `uname` 与 `system-headers`;载体的身份由 openkal-platform 陈述 | mcpp、openkal-platform | 行为不变,来源改为数据 | +| C7 | `--systems all` 不再写出未覆盖的对;SPEC 的打包一章仍写"其余写为未覆盖" | 恢复报告:`all` 取数据给出的系统,其中本引擎构建不了的对逐个写出。规范与实现一致 | mcpp | 恢复规范行为 | +| C8 | `abi.longjmp` 不进成员分组键;与 `exceptions` 的组合不校验 | 见决定 D6 | mcpp | 键尚未发布,可自由调整 | +| C9 | G6 启动另一系统的程序、按信号归类它的死亡,再建议 `qemu-aarch64-static` | 撤回这种做法。把 `unreachable_execution` 已有的谓词(只对平台定义的行生效)扩到内置行:启动之前回答"已构建,未运行";文字由运行器数据生成(5.5 第 4 条) | mcpp | 另一系统的产物不再被启动;`--no-runner` 仍是"此处可直接执行"的声明 | +| C10 | 资源头第三源:门控条件写两遍;取载荷失败时静默 | 一个谓词;失败报告为载荷无法安装;来源改为 `system-headers` | mcpp | — | +| C11 | 规范一处未改;中文文档不对等;三平面记录的"已成立"与实测不符 | 每项行为变更同 PR 改 SPEC 与两种语言;三平面记录第 3 节改写为引用门禁,或标为由本文取代 | mcpp | — | +| C12 | 三个镜像包名 | `freebsd-disk-image` 为正名,另两个为别名 | xim | 别名保留一个周期 | +| C13 | FreeBSD 运行器是手写路径 | 5.5 的运行器描述;`vm-run.py` 在 dirun 等价后退役;补 aarch64 | openkal-freebsd、openkal-platform | README 的旧写法继续可用 | +| C14 | 索引 PR 的安装检查可跳过 | 5.6:安装检查不可跳过;guard 增加布局检查 | xim、xpkgindex-update | — | +| C15 | dirun 自带测试模块;mcpplibs 下的归档空仓 | 测试改用 mtest;空仓的去留由负责人决定 | dirun | — | +| C16 | 在评审规则未满足、检查未全绿时以管理员权限合入 | 见决定 D12 | 三个生态 | — | + +**未定性的三个失败**(Windows 单元测试访问违例、xcode-27 的 1068、Windows 的 860)不在纠正清单里:先查明是否由本分支 +引入。访问违例发生在 `test_pack_distribution`,本分支改动了打包;在查明之前不以"环境问题"结案。 + +## 7. 验证工程对生态的要求 + +验证工程是一个 C89、只依赖 POSIX libc 的命令解释器,从同一组源码产出两个程序。把它对生态的要求抽成与它无关的十条, +即 `canary-c` 的断言: + +| # | 要求 | 对应的通用能力 | 状态 | +|---|---|---|---| +| V1 | 上游源码不改即可构建;需要适配时,每个身份的适配行数被统计 | 行、提供者、基座 | 上游原样 18 个身份过 10 个;4 行源码守卫加 5 行清单后过 16 个,去掉资源脚本的变体补到 17 个〔测,本地,2026.11.10.2〕 | +| V2 | 两个程序共享源码,各有入口与宏 | E2 | ✅ | +| V3 | 资源脚本写一次,每个 Windows 身份都能编,其余身份忽略 | E3、G3 | G3 在途 | +| V4 | 同一脚本在每个身份上的输出,归一化后与参考一致 | 运行面、验证面 | 本地做过,无门禁 | +| V5 | 同一份映像的字节在每个 okrun 主机上运行;两次打包字节相同 | oke、okrun | ✅〔测,本地〕,无门禁 | +| V6 | 自带运行器的包覆盖平台声明的全部系统 | G5、平台 `runner.hosts` 的 `built-as` | 在途 | +| V7 | 平台层的选择按 C 库与内核接口,不按系统宏 | `cfg(c-abi = …)` | ✅;属于程序自己 | +| V8 | 每个身份的 `mcpp run` 有回答:运行,或指名缺的运行器 | 运行面 | G6 在途;G7 的运行器已有包 | +| V9 | C 的非局部跳转在每个身份可用 | G8 | 在途 | +| V10 | 产物的名字与后缀由程序决定 | 构建程序 | ✅ | + +**平台覆盖。** 验证工程自己列出的平台,加上本文新增考察的两行: + +| 平台 | 路线 | 取决于 | 状态 | +|---|---|---|---| +| Linux、Windows、macOS、FreeBSD | B 默认,A 可选;oke | — | ✅(FreeBSD 的运行靠本地虚拟机) | +| Android、Web | A(NDK、emsdk) | G7、G8 | 构建 ✅;运行在途 | +| NetBSD | B | openkal-netbsd、K10 | 未开始 | +| OpenBSD、Haiku、GNU/Hurd | A + okrun 在 openkal-posix 上 | X4、K2、各自的磁盘镜像 | 未开始 | +| System V(i386) | 32 位 | K7 | 未开始 | +| **DOS,32 位** | A(DJGPP)或 B(openkal-dos) | 第 8 节 | 〔析〕 | +| **DOS,16 位** | A,仅 C | 第 8 节 | 〔析〕 | + +### 7.1 通用设施就绪后,验证工程能到什么程度 + +"就绪"指第 10 节的 S1–S4 完成;S5、S6 的内容单独标出。层级的划分是推断〔析〕,依据是第 2 节的台账、本地报告与第 8 节的探针。 + +| 层级 | 内容 | 身份数 | 取决于 | +|---|---|---|---| +| 一:每次提交构建并运行 | Linux(x86_64、aarch64 的 musl 与 gnu)、Windows(gnu、musl,wine 与真机)、macOS(两个架构的 musl,真机运行)、FreeBSD(四个身份,dirun)、Android(两个,模拟器)、Web(node)、ok 映像(两个架构,在四种宿主的 okrun 上) | 18 个身份,约 14 个运行环境 | S1–S4 | +| 二:每次提交构建,夜间运行 | NetBSD;OpenBSD、Haiku、GNU/Hurd(A 路线,okrun 在 openkal-posix 上) | 再加 5–7 个 | S6;各系统官方镜像可再分发 | +| 二甲:每次提交构建并运行 | DOS 32 位(DJGPP 加 DOSBox;它的 74 个源文件今天有 70 个不改即过,T5) | 1 个 | S5;程序补作业控制、多路等待、优先级三处 | +| 三:不进门禁 | System V(i386):K7 之后可做一次手工验证;DOS 16 位:程序先要拆掉超过 64 KB 的结构体(T7);PureDarwin:无法验证 | — | 第 8.3、8.4 节 | + +**它得到的。** + +- **一份清单覆盖全部身份。** 资源脚本无条件书写(G3 之后),共用入口的两个程序不需要包装文件(E2 已落地)。源码侧 + 剩下的是取自身路径的 4 行守卫——这是程序自己的平台探测,改为按 C 库选择后可以提交给它的上游。 +- **一台 Linux 机器上的全部验证。** `mcpp test --target <身份>` 经运行器描述到达每个层级一的环境;它自己的 CI 不需要 + Windows、macOS、FreeBSD 的机器就能断言行为同构,真机运行留给发布前。 +- **一条命令的发布矩阵。** 各身份的原生程序、一份多架构的 ok 映像、覆盖 Linux、macOS、Windows、FreeBSD 的多系统 + 单文件;经 xim 成为 xlings 支持的每个宿主上可安装的包。 +- **新代码可以用 C++23 模块。** C89 内核不动,新功能在支持 `import std` 的身份上用模块写,其余身份回落到 C 入口。 + +**通用设施给不了的(语义面,属于程序)。** + +| 方面 | 说明 | +|---|---| +| 进程模型 | DOS 没有并发的子进程与管道,Web 没有进程;管道在这些环境里要由程序用临时文件实现,或者如实不支持 | +| 终端 | 行编辑依赖终端的原始模式;浏览器里的终端界面是程序自己的工作,设施只保证 node 下的行为 | +| 系统的原生语义 | Windows 上的路径写法与命令行原样传递使它按 P4 默认走 A 路线;B 路线的 Windows 程序是可选项 | +| 16 位的体量 | 一个超过 64 KB 的上下文结构体(T7)与 54 KB 的消息表(T6),只能由程序改数据布局、裁语言集合 | +| 不能再分发的系统 | 没有可再分发镜像的系统(System V)进不了 CI,只能有一次性的手工验证 | + +**对它上游的含义。** 它现在手工维护多套构建描述(Makefile、CMake、Meson、xmake、Gradle、Visual Studio 与两种 IDE +工程)。设施就绪后,一份 `mcpp.toml` 在覆盖面上超过它们的并集。是否采用是它上游的决定,不在本方案范围内。 + + +## 8. 层级三与固件:探针的结果 + +本节的每个"测"都是 2026-11-12 在一台 x86_64 Linux 主机上做的本地测量(无 KVM),命令与摘要在 `okexp/t3/PROBES.md`; +它们不是门禁。未测的部分仍标〔析〕。 + +### 8.1 探针 + +| # | 探针 | 结果 | 说明了什么 | +|---|---|---|---| +| T1 | 已发布的 2026.11.10.2,`--target i686-none-elf`,不带 C 库,经 `int 0x80` 写一行后退出 | ELF32;`qemu-i386` 下输出一行,退出码 42〔测〕 | 32 位一类的编译、链接、运行路径在引擎里已经通;K7 缺的是 C 库与运行时,不是引擎 | +| T2 | DJGPP(GCC 12.2)编 C 程序,DOSBox Staging 无显示运行 | 输出与退出码 42 如实;墙钟 0.33 秒〔测〕 | DOS 的运行器是"接口的宿主实现",与 wine 同类,不需要磁盘镜像,可以进每次提交的门禁 | +| T3 | 同上,C++23 | `import std` 没有,`` 没有〔测〕 | 这条路线是"C 与旧标准 C++"的行 | +| T4 | DOSBox 的命令解释器不展开 `%ERRORLEVEL%` | 要用 `if errorlevel N`〔测〕 | 客体的包装必须是数据(D22) | +| T5 | 验证工程的 74 个源文件,DJGPP,不改源码 | 70 个通过;4 个失败在 `SIGTSTP`、`sys/select.h`、`clock_gettime`、`setpriority`〔测〕 | 缺口是作业控制、多路等待与优先级——语义面,属于程序 | +| T6 | 上述 70 个目标文件的大小 | 代码加只读数据 113.8 KB,bss 13.4 KB;四套语言的消息表占 54.0 KB〔测〕 | 16 位下,单语言构建才可能放进一个数据段 | +| T7 | 同一批源文件,Open Watcom 16 位 large 模型 | 13 个通过,61 个失败;主因是它的上下文结构体超过 64 KB(`E1098`)〔测〕 | 16 位的拦路石是程序自己的数据结构,不是工具链或设施 | +| T8 | 三十行的平台包定义 `x86_64-uefi`,已发布的 2026.11.10.2 构建,qemu 加 OVMF 运行 | PE32+ EFI 应用;串口输出一行;状态经调试端口回到宿主;墙钟 2 秒〔测〕 | P15 对固件成立:引擎零改动 | +| T9 | 对照 openkal 规范检查 `openkal.process` | 文本未禁止"启动即等待";通道、终止等并发相关能力已是可不声明的属性〔读〕 | 单任务环境可以提供该接口,缺一句明示与一致性覆盖 | + +没有做的:DOS 的 B 路线(需要 K7 的 i386 C 库);gcc-ia16(没有预编译的发布物);System V 与 PureDarwin(没有可用的 +镜像)。 + +### 8.2 DOS,32 位 + +| 身份(名字待定) | 路线 | C 库 | C++ | 引擎需要什么 | 状态 | +|---|---|---|---|---|---| +| `i386-dos-djgpp` | A | DJGPP 的 libc | libstdc++ 12,无模块 | 零:GCC 族的一个新载荷加一条提供者 | 工具链与运行器已测(T2、T5);经 mcpp 构建未测〔析〕 | +| `i386-dos-musl` | B | openkal-musl(i386) | openkal-llvm-runtime,可以有 `import std` | 零,若 P15 成立 | 〔析〕;前置是 K7 | + +**B 路线**由四个包组成:编译与链接沿用 `i686-none-elf` 行的做法(T1);内核层实现 openkal-dos 经 DPMI 的实模式中断模拟 +调用 `int 21h`,如实提供核心三个接口、`env`、`time`、`fs`、`terminal`、`region`,缺席 `task`、`net`、`datagram`、 +`space`、`map`、`random`;容器用平台包的 `[platform.link] seal`(链接后对产物执行的命令序列)把 ELF 包进带 DPMI 入口桩 +的 MZ 文件,与 ok 映像的 `okld` 封印同一机制;运行器是 DOSBox。 + +**A 路线**是先做的那条:它不检验 openkal,但工具链、运行器、验证工程的源码三者今天就对得上(T2、T5)。 + +### 8.3 DOS,16 位 + +| 方面 | 事实 | 后果 | +|---|---|---| +| 编译器 | LLVM 没有 8086 后端〔析〕;Open Watcom 可用(T7),是自己的命令行;gcc-ia16 是 GCC 命令行,没有预编译发布物 | 不可能有 C++23 与模块;这一行是"仅 C"的行 | +| C 库 | musl 假定平坦的 32 位以上地址空间〔析〕 | 没有 B 路线,没有 openkal,没有映像 | +| 程序 | 一个超过 64 KB 的结构体(T7);消息表 54 KB(T6) | 先要程序改自己的数据布局与语言集合;这之前工具链帮不上 | +| 引擎 | Watcom 需要一种新的驱动方言;gcc-ia16 需要"按能力探测旗标"——旧编译器不接受引擎缺省给的某些旗标〔析〕 | 后者是通用机制,第二个使用者是 avr、msp430、m68k 一类老的 GCC 交叉链 | + +结论:16 位 DOS 在设施一侧只值得做"按能力探测旗标"这一个通用机制,而且要等出现第二个使用者;它本身的可行性由 +程序决定。不立项。 + +### 8.4 32 位一类、System V 与 PureDarwin + +- **32 位一类(K7)。** T1 说明引擎一侧没有缺口。工作全在 openkal 家族:规范的 32 位约定、openkal-linux 的 i386 系统调用层、 + musl 与 C++ 运行时的 32 位构建、32 位映像。先做 `i686-linux-musl`,它有 qemu-user,可以进门禁。 +- **System V(i386)。** K7 之后是一个 openkal 实现加一个平台包。没有可再分发的镜像,进不了 CI;只能有一次性的手工 + 验证,结果写进记录而不是门禁。 +- **PureDarwin。** macOS 的 B 路线产物只依赖 libSystem〔读〕,推断可以直接运行;没有 Mach-O 的宿主运行器,也没有可用 + 镜像,无法验证。不投入。 + +## 9. 辐射范围与收益 + +通用设施的使用者不限于验证工程。每一行的"收益"是在该设施落地后不需要额外引擎工作即可得到的。 + +| 领域 | 用到的平面 | 收益 | +|---|---|---| +| **生态自举** | ①②③ | mcpp 与 xlings 自身经 B 路线产出 FreeBSD 等系统的静态程序,包管理器与构建工具的宿主范围随 openkal 实现的增加而扩大;新宿主上的第一份 mcpp 不需要在该宿主上编译 | +| **库作者的跨系统测试** | ③④ | mcpplibs 的任何库在一台 Linux CI 机器上对 Windows(wine)、FreeBSD(虚拟机)、Android(模拟器)、aarch64(qemu)运行自己的 `mcpp test`;今天这需要四种 CI 机器或手写胶水 | +| **命令行工具的发布** | ①②,oke | 一份映像或一个多系统单文件覆盖各系统;构建可复现(两次打包字节相同已在本地测得),可以作为供应链证明的基础 | +| **嵌入式与固件** | ①②③ | 裸机行已有十余个;金样测试与运行器陈述同样服务它们。UEFI 应用由一个平台包给出(T8,已测),其上接 openkal 的实现后可以用 C++23 模块与 `import std` 写引导加载器与固件工具;"按能力探测旗标"(8.3)覆盖 avr、msp430 一类老的交叉链 | +| **图形与游戏** | ②(基座) | #913:GUI 工程在三种宿主上产出 Windows PE;基座准入契约保证这类载荷的 C++ 与 `import std` 可用 | +| **教学** | ①③④ | 操作系统与编译课程:同一份作业在多个系统的模拟器里运行并自动比对输出;环境由 xlings 一条命令给出 | +| **非 mcpp 用户** | ②,xim | 基座包与磁盘镜像是普通的 xim 资产,CMake、xmake 用户同样可以把 `aarch64-linux-gnu-sysroot` 当作 `--sysroot`;dirun 不含生态词汇,任何 CI 可用 | +| **复古与长尾系统** | 全部 | DOS、SVR4、Haiku 的准入成本由 P15 界定:数据加包。每接入一个,就多一次对"引擎零改动"的验收 | +| **IDE 与语言服务** | ① | 行、系统表与运行器描述是数据,mcpp-ide 可以直接列出"此工程可去的身份、每个身份怎样运行",不需要自己的表 | +| **自动化代理** | ②④ | 密封的工具链与机械门禁使代理的改动可以被检查拦下;本次审计发现的问题大多属于"门禁本可以拦下" | + +## 10. 阶段 + +| 阶段 | 内容 | 出口 | +|---|---|---| +| S0 冻结 | mcpp#924 不合入;本文评审定稿 | 第 11 节的决定全部有结论 | +| S1 纠正 | C1–C11;mcpp#924 拆分(D5);C4 的修订 3 | 分支上 2026.11.10.2 的全部行为恢复;295、299 绿;三个未定性失败有结论 | +| S2 门禁 | 链接行金样、gcc `PayloadFirst` 腿、基座准入契约、安装检查不可跳过、系统名检查的基线 | 本次审计的每个阻断问题都有一个会失败的检查 | +| S3 数据化 | 已有行键的数据(`loader`、`execution`)、系统表的 `uname`、提供者拆分规则入规范、提供者的 `runner` 与 `system-headers`;C12–C15 | 系统名基线下降到只剩 `modules/platform/` | +| S4 验证面 | 两个金丝雀、矩阵数据、夜间报告;跨系统 `mcpp test`;mtest 的 M1–M6 与 e2e、bench 的收敛(5.9,按子系统分批,可与 S3 并行);dirun 的整形(5.10);CI 的触发模型(5.11) | 覆盖报告作为 CI 产物;0 级脚本只剩"保留"类;PR 墙钟与作业分钟达到 5.11 的目标 | +| S5 32 位与固件 | K7(先 `i686-linux-musl`);DOS 的 A 路线(DJGPP 载荷、DOSBox 运行器);UEFI 的平台包与 openkal 实现;单任务环境的规范明示 | `i686-linux-musl` 为 verified;DOS 与 UEFI 的金丝雀进门禁,引擎零改动 | +| S6 新系统 | DOS 的 B 路线(以 P15 验收);第二类系统(K2、X4、各磁盘镜像);openkal-netbsd | 每个系统:引擎零改动 | +| 不排期 | 16 位 DOS(8.3);System V 的一次手工验证;Serenity;Plan 9(仅 C);K12;PureDarwin | 出现第二个使用者或程序一侧就绪时再议 | + +S1 与 S2 之间不插入新能力。S3 之后的每个新系统是对架构的一次验收,而不是一次扩建。 + +## 11. 决定 + +### 11.1 已定(2026-11-12 评审) + +| # | 决定 | +|---|---| +| D16 | e2e 收敛到 mtest 的 C++ 用例,按下沉、合并、保留三类,按子系统分批,在 S1 之后开始 | +| D17 | 保留类允许长期存在,数量由基线持有、只许减少 | +| D18 | bench 的 harness 并入 mtest;bench 不随每次 CI 运行,触发见 5.11 | +| D19 | mtest 的升级项纳入本方案(5.9 的 M1–M6) | +| D20 | CI 按 5.11 编排:事件对集合、按改动选择、每个宿主一道闸、分片数随工作量、可手动触发全量 | +| D21 | dirun 收为一个包,测试用 mtest,命令行用 cmdline;Python 参考在合同条款测试等价后删除 | +| D22 | 客体与板的描述是随资产发布的数据 | +| D23 | 伪终端归 `mcpplibs.os`,expect/send 归 mtest 的 e2e 模块,dirun 依赖二者 | + +### 11.2 待裁决 + +| # | 问题 | 推荐 | 备选 | +|---|---|---|---| +| D1 | `aarch64-linux` 在 x86_64 主机上回答哪个身份 | musl(与 2026.11.10.2 相同);交叉 glibc 条目优先级 40,写全三元组才取到。依据 Q3 与 P12 | 接受变为 glibc,写进 CHANGELOG 作为一次缺省变更 | +| D2 | 是否把"一条条目一种安排"(P11)与"请求的回答是契约"(P12)写进 SPEC-016 | 是 | 只作为评审惯例 | +| D3 | 不在本机运行的动态产物,加载器写成什么 | 行的 `loader`(已有键),`RUNPATH` 为空,运行器给 `{sysroot}` | 维持构建机路径,由 `pack` 改写 | +| D4 | 系统级事实的 schema | 第二版的小方案:填已有的行键;新增只有系统表的 `uname` 与提供者的 `system-headers`;载体身份由平台的 `runner.hosts` 陈述 | 第一版的大方案:系统表带 `family`、`kernel-abi`、`carrier` | +| D5 | mcpp#924 怎样处理 | 拆开:G3、G5、G8 各一个 PR,各带规范与两种语言的文档;G6 按 C9 重做为数据加已有谓词的扩展;G1 推到 S3 之后;版本号在合入时取 | 整体修到绿后合入 | +| D6 | `abi.longjmp` 的形状 | 保留键名;未写时随 `exceptions` 的模型;两者都写且不相容时在解析期拒绝;成员分组键里"声明了才出字段" | 改为一个覆盖异常与跳转的 `unwind` 键 | +| D7 | abi 成员的渲染是否改为提供者上的数据表 | **推迟**:目前只有 Emscripten 一个使用者(P2);出现第二个时再做 | 现在就做 | +| D8 | 运行器包何时安装 | 第一次 `mcpp run`/`mcpp test` 需要时,与工具链同一种同意方式 | 只提示,由用户自己装 | +| D9 | 客体方式的运行器是否只留 dirun | 是;`vm-run.py` 退役;镜像正名 `-disk-image` | 两者并存 | +| D10 | 基座准入契约放在哪里 | xim-pkgindex 的 CI 作业,调用已发布的 mcpp;不新增 mcpp 子命令 | `mcpp` 增加一个检查基座的子命令 | +| D11 | K7 是否提为正式阶段 | 是,先 `i686-linux-musl`;探针 T1 说明引擎一侧没有缺口 | 维持为讨论项 | +| D12 | 合入纪律 | 必需检查未全绿不合入;管理员权限只用于评审规则本身无人可满足的情形,且在 PR 里写明;一个 PR 一件事;交接记录放 `.agents/docs/todos` | — | +| D13 | 提供者条目是否带自己的 `tier` | 是,但只在 C2 的拆分落地时一起做 | 等级只在行上 | +| D14 | 16 位 DOS | 不立项(8.3):拦路石在程序一侧;"按能力探测旗标"等第二个使用者 | 立项做 gcc-ia16 的"仅 C"行 | +| D15 | 单任务环境上的 `openkal.process` | 在规范里明示"启动者可能在被启动者结束后才继续",一致性套件覆盖;在 UEFI 或 DOS 的实现之前完成 | 让这类环境缺席该接口 | +| D24 | 运行器的缺省安排写在哪里 | 提供者条目的 `runner`(与平台的 `runner` 同一形状),不新增表 | 第一版的 `[[platform.runner]]` | +| D25 | G6 的做法 | 不启动另一系统的程序;内置行与平台行同一谓词,启动前回答"已构建,未运行" | 维持 mcpp#924 的子进程加信号归类 | +| D26 | UEFI 的身份放在哪里 | 先由平台包定义(今天可做,T8);出现第二个使用同一身份的平台包时升为内置行 | 现在就加内置行 | +| D27 | DOS 的先后 | A 路线(DJGPP 加 DOSBox)进 S5;B 路线在 K7 之后作为 P15 的验收 | 只做 A;或不做 | +| D28 | 固件所需的宿主文件(OVMF)与运行器(DOSBox、wine)是否做成 xim 包 | 是(P9) | 维持宿主依赖,测试以能力词跳过 | + +## 12. 风险 + +- **schema 变更。** 第二版把新增压到两个键(`uname`、`system-headers`)加平台 `runner.hosts` 的一个字段;平台包的最低 + mcpp 版本仍随之上升。旧平台包没有这些数据时,引擎对内置系统的回答与今天相同(数据的缺省,不是代码分支)。 +- **加载器规则改变交叉产物的运行方式。** 采纳 D3 后,交叉 glibc 产物在构建机上不能直接启动,必须经运行器。这是正确 + 的语义,但与"构建完马上能跑"的直觉不同,诊断要说清楚。 +- **K7 跨六个仓库。** 它是 S5、S6 多数内容的前置;若推迟,DOS 的 B 路线、SVR4 与 i686 一起推迟,A 路线与 UEFI 不受影响。 +- **准入契约拉长索引 PR 的周期。** BP3–BP7 需要已发布的引擎认识被测的载荷;引擎与载荷同时变化时有先后问题——载荷先 + 以候选修订进入,引擎发布后再转正。 +- **e2e 迁移改写九万行。** 风险是迁移中丢断言或改了被测行为。对策已在 5.9:先原脚本后新用例、映射表、一个子系统一个 PR。 + 在 M1–M3 就绪之前不开始;第一批选耗时最长而断言最少的子系统,用它校正 5.11 的目标数字。 +- **按改动选择漏跑。** 对策是 5.11 的安全边界与 main 上每次合入的 `full`:PR 漏掉的,合入后的那次运行会抓到。 +- **范围。** 本方案有七个阶段与一次测试体系迁移。S1 与 S2 是不可分的最小切片;S3 之后的每一项都应当在前一项的门禁 + 绿了之后才开始,同一时间进行中的阶段不超过两个。 +- **探针不是门禁。** 第 8 节的测量是一台主机上的一次运行。它们回答"路是否通",不回答"是否稳定"。 + +## 13. 与既有记录的关系 + +| 记录 | 关系 | +|---|---| +| [2026-11-08 顶层设计](2026-11-08-ecosystem-top-level-design.md) | 保留 | +| [2026-11-09 目标模型](2026-11-09-target-model-design.md) | 保留;本文第 5.2、5.3 节是对它的增补,定稿后进入 SPEC-016 | +| [2026-11-10 跨平台生态方案](2026-11-10-cross-platform-ecosystem-design.md) | 保留;P1–P9 与 Q1–Q6 是本文的前提;其未完成的工作项列在第 2.4 节 | +| [2026-11-10 测试与解析补充](2026-11-10-test-and-resolution-supplement-design.md) | 保留 | +| [2026-11-12 三平面架构](2026-11-12-three-plane-platform-architecture-design.md) | 其第 1、2、4、5 节由本文第 5 节扩展为四个平面;第 3 节"已成立"的七条中,第 1、2 条与实测不符(第 2.3 节),由本文第 2 节的台账取代。该记录随 mcpp#924 的处理一并修订或标为 superseded | + +## 14. 多视角自审 + +### 14.1 自审中发现并已修改的(第一版 → 第二版) + +| # | 视角 | 发现 | 修改 | +|---|---|---|---| +| 1 | 规范一致性 | 第一版把行的 `loader` 当作新键,并让系统表带 `family`、`kernel-abi`。SPEC-016 §2.1 已经有 `loader`、`execution`、`family`、`exe-suffix`、`object-format`,只是内置数据没有填 | 5.2 改为"先填已有的键";新增压到 `uname` 与 `system-headers` | +| 2 | 语义与安全 | 第一版沿用了 mcpp#924 中 G6 的做法(把另一系统的程序作为子进程启动,观察它的死亡)。另一系统的系统调用号在本机内核上是别的操作,参数未定,后果不确定;而 SPEC-016 §6 已经规定这种情形在启动前回答"已构建,未运行",代码里的谓词只是没有覆盖内置行 | C9 与 D25:撤回信号归类,扩展已有谓词 | +| 3 | 简洁 | 第一版新增 `[[platform.runner]]` 表与 `kind` 键。运行器已有四处可写,缺的只是"内置提供者也能陈述" | 5.5 改为提供者条目的 `runner`,不新增表;"种类"降为文档里的分类 | +| 4 | 归属(P16) | 第一版把载体的身份(`carrier`)放进内置的系统表。它只对带运行器的平台有意义 | 移到平台的 `runner.hosts`,由 openkal-platform 陈述 | +| 5 | 第二个使用者(P2) | abi 成员的数据化渲染(D7)只有 Emscripten 一个使用者 | 推迟 | +| 6 | 时间成本 | 第一版的"两级流水"让全部扇出等 Linux 的闸,绿的路径会比现在更慢(Linux 构建约 16 分钟是长杆) | 5.11 改为构建立即并行、每个宿主一道闸、红了取消整次运行 | +| 7 | 证据 | 第一版的 DOS 与固件全是推断;"机器"类运行器把 DOS 也算了进去 | 做了九个探针(第 8 节):DOS 的运行器是 DOSBox,不需要镜像;UEFI 今天就能以平台包构建并运行;16 位的拦路石在程序一侧 | + +### 14.2 各视角的结论 + +| 视角 | 结论 | +|---|---| +| mcpp 规范 | 新增的 schema 是两个键与一个字段;P11、P12 进 SPEC-016;C7 让实现回到 SPEC 的打包章;G6 回到 SPEC-016 §6。没有一项与现行规范冲突 | +| openkal 规范 | 只有一处明示(单任务环境的 `process`)与 K7 的 32 位约定;固件与 DOS 都是新的实现,不改接口清单 | +| xlings 生态 | 新的资产类只有运行器所需的三个包;准入契约与布局检查是对修订通道的补全,不改它的模型 | +| 架构 | 四个平面各有数据、归属与门禁;引擎新增的机制只有链接模型的单值化与运行器模板的两个占位 | +| 不硬编码 | 系统名的出现次数由检查持有;这次列出的七处各有去向;新系统以引擎零改动验收,UEFI 已经通过一次(T8) | +| 归属 | P16 的四问把板、固件、客体、程序的事实各自留在自己的包里;16 位 DOS 的结论(T7)是这条判据的一个实例 | +| 简洁 | 第二版比第一版少一张表、一个键、一个决定;每个保留的新增都指得出它取代的代码 | +| 兼容性 | S1 恢复 2026.11.10.2 的全部行为;新能力以低优先级进入(P12);`abi.longjmp` 尚未发布,可以自由调整 | +| 跨平台 | Windows 的两成套件因写法不运行,这由迁移解决;dirun 的 Windows 宿主取决于"管道代替伪终端"是否成立,未测 | +| 性能与成本 | CI 的目标数字是估计,由第一批迁移校正;bench 从每次 CI 移出 | +| 测试与验证 | 本次审计的每个阻断问题在 S2 之后都有一个会失败的检查;金丝雀把"十八个目标通过"变成 CI 产物 | +| 供应链 | 资产的可用性进入准入;镜像不一致时客户端回落并报告 | +| 可实施性 | 最小切片是 S1 加 S2;其余每项可以独立排期,彼此的依赖写在第 10 节 | + +### 14.3 仍然没有把握的 + +- **第 5.9、5.11 的占比与目标数字**是估计。迁移的三类各占多少要靠一次逐脚本的清点;PR 墙钟与作业分钟的目标要靠第一批 + 迁移后的测量。 +- **mtest 的现状**只读了文档。M1–M6 里可能有已经存在的,实施前逐项核对。 +- **`mcpplibs.cmdline` 的能力**没有核对;dirun 改用它之前先确认可重复选项与 `--` 之后原样传递。 +- **dirun 以管道代替伪终端**没有测过。 +- **DJGPP 经 mcpp 构建**没有测过:探针直接调用了它的编译器。GCC 族的新载荷在引擎里是否真的只是数据,要做一次才知道。 +- **按改动选择对数据文件的处理**:内置数据编进引擎后,覆盖图能否把它与读它的用例对应起来,没有核对;5.11 因此把它 + 写成"影响全部目标类用例"的保守规则。 +- **三个未定性的 CI 失败**(Windows 访问违例、xcode-27 的 1068、Windows 的 860)仍然没有根因。 diff --git a/.github/tools/check_system_names.py b/.github/tools/check_system_names.py new file mode 100644 index 000000000..e59c48848 --- /dev/null +++ b/.github/tools/check_system_names.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""check_system_names.py -- the engine's sources name no more systems than the baseline. + +WHY THIS EXISTS + +AGENTS.md section 2 states that the engine names no operating system: target +rows, providers and systems are data (modules/toolchain-model/data/builtin.toml, +SPEC-016), and code that is specific to one host lives in modules/platform/. +A review cannot hold that sentence: a branch on `"freebsd"` reads as an +ordinary line, and several entered the tree beside the tables that were meant +to replace them (design 2026-11-12, cause R1). This check counts instead. + +WHAT IS COUNTED + +Each word of the vocabulary below, in the engine's sources -- `src/` and +`modules/*/src/`, without `modules/platform/` and without tests and data -- +in the two forms a branch on a system takes: the word as a string literal +(`"freebsd"`), and the host predicates of mcpplibs.os (`is_linux`, +`is_windows`, `is_macos`). Comments are not counted. + +THE RATCHET + +`system-names.baseline` records one number per word. More occurrences than the +baseline fail: the new line states a system fact, which belongs in a row, in +the system table or in a provider. Fewer occurrences fail as well, with the +line to write: the baseline follows the code down, so the count never returns. + + check_system_names.py compare with the baseline + check_system_names.py --update rewrite the baseline to the counts found + check_system_names.py --list W print every counted line of word W +""" + +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[2] +BASELINE = pathlib.Path(__file__).with_name("system-names.baseline") + +LITERALS = ["linux", "macos", "windows", "freebsd", "android", "emscripten", "ios", + "darwin", "netbsd", "openbsd", "haiku", "dos", "uefi"] +PREDICATES = ["is_linux", "is_windows", "is_macos"] + +SOURCE_SUFFIXES = {".cppm", ".cpp", ".mcpp"} + + +def sources(): + for base in (ROOT / "src", ROOT / "modules"): + for path in sorted(base.rglob("*")): + if path.suffix not in SOURCE_SUFFIXES or not path.is_file(): + continue + rel = path.relative_to(ROOT).as_posix() + if rel.startswith("modules/platform/"): + continue + parts = rel.split("/") + if "tests" in parts or "target" in parts or "data" in parts: + continue + yield rel, path + + +def code_of(line, state): + """The line without its comments. `state["block"]` carries a /* */ across lines.""" + out = [] + i = 0 + in_string = False + while i < len(line): + if state["block"]: + end = line.find("*/", i) + if end < 0: + return "".join(out) + state["block"] = False + i = end + 2 + continue + ch = line[i] + if in_string: + out.append(ch) + if ch == "\\" and i + 1 < len(line): + out.append(line[i + 1]) + i += 2 + continue + if ch == '"': + in_string = False + i += 1 + continue + if ch == '"': + in_string = True + out.append(ch) + i += 1 + continue + if line.startswith("//", i): + break + if line.startswith("/*", i): + state["block"] = True + i += 2 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def count(): + patterns = {w: re.compile(r'"%s"' % re.escape(w)) for w in LITERALS} + patterns.update({w: re.compile(r"\b%s\b" % w) for w in PREDICATES}) + counts = {w: 0 for w in patterns} + lines = {w: [] for w in patterns} + for rel, path in sources(): + state = {"block": False} + for n, raw in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1): + code = code_of(raw, state) + if not code.strip(): + continue + for word, pat in patterns.items(): + hits = len(pat.findall(code)) + if hits: + counts[word] += hits + lines[word].append(f"{rel}:{n}: {raw.strip()}") + return counts, lines + + +def read_baseline(): + base = {} + if not BASELINE.is_file(): + return base + for line in BASELINE.read_text().splitlines(): + line = line.split("#", 1)[0].strip() + if not line: + continue + word, number = line.split() + base[word] = int(number) + return base + + +def write_baseline(counts): + text = ("# One number per word: its occurrences in the engine's sources\n" + "# (.github/tools/check_system_names.py). The numbers only fall.\n") + for word in LITERALS + PREDICATES: + text += f"{word} {counts[word]}\n" + BASELINE.write_text(text) + + +def main(argv): + counts, lines = count() + if len(argv) == 2 and argv[0] == "--list": + print("\n".join(lines.get(argv[1], []))) + return 0 + if argv == ["--update"]: + write_baseline(counts) + print(f"wrote {BASELINE.relative_to(ROOT)}") + return 0 + base = read_baseline() + more, fewer = [], [] + for word in LITERALS + PREDICATES: + was, now = base.get(word, 0), counts[word] + if now > was: + more.append(f" {word}: {now} occurrences, the baseline is {was}") + elif now < was: + fewer.append(f" {word}: {now} occurrences, the baseline is {was}") + if more: + print("the engine's sources name a system more often than the baseline:") + print("\n".join(more)) + print("A fact of a system is data: a key of its row, of the system table or of a provider\n" + "(modules/toolchain-model/data/builtin.toml, SPEC-016), read where it was branched on.\n" + "Code specific to one host lives in modules/platform/.\n" + "`check_system_names.py --list ` prints the counted lines.") + if fewer: + print("the engine's sources name a system less often than the baseline; record it:") + print("\n".join(fewer)) + print(" python3 .github/tools/check_system_names.py --update") + return 1 if more or fewer else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.github/tools/system-names.baseline b/.github/tools/system-names.baseline new file mode 100644 index 000000000..106788fcd --- /dev/null +++ b/.github/tools/system-names.baseline @@ -0,0 +1,18 @@ +# One number per word: its occurrences in the engine's sources +# (.github/tools/check_system_names.py). The numbers only fall. +linux 66 +macos 54 +windows 81 +freebsd 2 +android 19 +emscripten 21 +ios 25 +darwin 10 +netbsd 1 +openbsd 0 +haiku 0 +dos 0 +uefi 0 +is_linux 30 +is_windows 130 +is_macos 32 diff --git a/modules/manifest/src/platform.cppm b/modules/manifest/src/platform.cppm index 9ac684653..fee29752e 100644 --- a/modules/manifest/src/platform.cppm +++ b/modules/manifest/src/platform.cppm @@ -120,6 +120,12 @@ struct Runner { bool hostsSet = false; bool exclusive = false; bool exclusiveSet = false; + // The identity the runner program of a host pair is built as, where the + // platform builds it for a system (`built-as = { "freebsd-x86_64" = + // "x86_64-freebsd-musl" }`): a package that carries its runner reads it. + std::map builtAs; + // The packages the runner is installed from on the first run. + std::vector requires_; Origin origin; bool set = false; }; @@ -267,6 +273,16 @@ struct ProviderSpec { std::map layers; Link link; std::string execution; + // The tier of this arrangement (empty: the row's), the package of the + // system's own headers, and how its artifacts are started (SPEC-016 §3, §6). + std::string tier, systemHeaders; + Runner runner; + Origin origin; +}; + +// A SYSTEM (`[[platform.system]]`, SPEC-016 §2.3). +struct SystemSpec { + std::string name, uname; Origin origin; }; @@ -308,6 +324,7 @@ struct Statement { std::optional cHosted; std::vector defines; std::vector providers; + std::vector systems; int priority = 100; bool prioritySet = false; @@ -317,7 +334,8 @@ struct Statement { && tools.empty() && build.empty() && buildCfg.empty() && !link.driver.set && !link.flags.set && !link.start.set && !link.script.set && !link.inputsSet && !link.exe.set && !link.shared.set && !link.sealSet - && versions.empty() && !hosted.set && defines.empty() && providers.empty(); + && versions.empty() && !hosted.set && defines.empty() && providers.empty() + && systems.empty(); } }; @@ -352,7 +370,7 @@ constexpr bool is_layer(std::string_view k) { inline constexpr std::string_view kPlatformKeys[] = { "targets", "toolchain", "sysroot", "runner", "runners", "carrier", "plugin", "universal", "descriptor_keys", "layers", "tools", "build", "link", "versions", "hosted", - "define", "provider", "priority", + "define", "provider", "system", "priority", }; inline constexpr std::string_view kDefineKeys[] = { "triple", "tier", "note", "standard", "exact", "clang-triple", "object-format", "family", @@ -361,8 +379,9 @@ inline constexpr std::string_view kDefineKeys[] = { }; inline constexpr std::string_view kProviderKeys[] = { "name", "serves", "priority", "hosts", "pairs", "toolchain", "sysroot", "linkage", "base", - "hosted", "layers", "link", "execution", + "hosted", "layers", "link", "execution", "tier", "system-headers", "runner", }; +inline constexpr std::string_view kSystemKeys[] = { "name", "uname" }; inline constexpr std::string_view kBuildKeys[] = { "cflags", "cxxflags", "asmflags", "ldflags" }; inline constexpr std::string_view kLinkKeys[] = { "driver", "flags", "start", "script", "inputs", "exe", "shared", "seal", diff --git a/modules/manifest/src/platform_providers.cppm b/modules/manifest/src/platform_providers.cppm index 691bc147a..e267ff0cc 100644 --- a/modules/manifest/src/platform_providers.cppm +++ b/modules/manifest/src/platform_providers.cppm @@ -23,6 +23,8 @@ struct StatedProvider { int priority = 100; std::string toolchain, sysroot, linkage; bool shorthand = false; // `[platform]` or `[platform.hosted]` + std::string tier, systemHeaders; + std::vector runner, runnerHosts, runnerRequires; }; // `name` is the platform's name (`platform_name`), the name of every provider @@ -78,7 +80,8 @@ std::vector stated_providers(const Statement& s, std::string_vie std::vector out; for (auto const& p : s.providers) out.push_back({ p.name.empty() ? std::string(name) : p.name, p.serves, p.hosts, p.pairs, p.priority, - p.toolchain.empty() ? s.toolchain.value : p.toolchain, p.sysroot, p.linkage, false }); + p.toolchain.empty() ? s.toolchain.value : p.toolchain, p.sysroot, p.linkage, false, + p.tier, p.systemHeaders, p.runner.cmd, p.runner.hosts, p.runner.requires_ }); if (s.targets.set) out.push_back({ std::string(name), s.targets.value, {"*"}, {}, s.prioritySet ? s.priority : 100, s.toolchain.value, {}, {}, true }); diff --git a/modules/manifest/src/platform_toml.cppm b/modules/manifest/src/platform_toml.cppm index 660f4b1ba..714de9d84 100644 --- a/modules/manifest/src/platform_toml.cppm +++ b/modules/manifest/src/platform_toml.cppm @@ -268,10 +268,26 @@ std::expected Reader::runner(const toml::Value& v, std::str if (!h) return std::unexpected(h.error()); r.hosts = std::move(*h); r.hostsSet = true; + } else if (k == "requires") { + auto w = strings(val, key); + if (!w) return std::unexpected(w.error()); + r.requires_ = std::move(*w); + } else if (k == "built-as") { + if (!val.is_table()) + return std::unexpected(std::format( + "{} must be a table of \"-\" = \"\"", key)); + for (auto const& [pair, triple] : val.as_table()) { + if (!mcpp::manifest::platform::is_host_pair(pair) || !triple.is_string() + || triple.as_string().empty()) + return std::unexpected(std::format( + "{}.{} must name the identity the runner of that host pair is built as, " + "for example \"freebsd-x86_64\" = \"x86_64-freebsd-musl\"", key, pair)); + r.builtAs[pair] = triple.as_string(); + } } else { warn(std::format( - "{}: '{}' is not a key of a runner (ignored); the keys are cmd, host, hosts, exclusive", - where, k)); + "{}: '{}' is not a key of a runner (ignored); the keys are cmd, host, hosts, " + "exclusive, requires, built-as", where, k)); } } } else { @@ -616,6 +632,13 @@ std::expected Reader::provider(const toml::Table& t, } if (path.empty()) return std::unexpected(std::format("{} is a table without `path`", key)); p.sysroot = "path:" + absolute(path).generic_string(); + } else if (k == "runner") { + auto r = runner(v, key); + if (!r) return std::unexpected(r.error()); + p.runner = std::move(*r); + } else if (k == "tier" || k == "system-headers") { + if (!v.is_string()) return std::unexpected(std::format("{} must be a string", key)); + (k == "tier" ? p.tier : p.systemHeaders) = v.as_string(); } else if (k == "name" || k == "toolchain" || k == "sysroot" || k == "linkage" || k == "execution" || k == "base") { if (!v.is_string()) @@ -650,6 +673,8 @@ std::expected Reader::provider(const toml::Table& t, return std::unexpected(std::format("{}.linkage is static or dynamic", where)); if (p.base != "platform" && p.base != "none") return std::unexpected(std::format("{}.base is \"platform\" or \"none\"", where)); + if (!p.tier.empty() && p.tier != "verified" && p.tier != "preview" && p.tier != "planned") + return std::unexpected(std::format("{}.tier is verified, preview or planned", where)); return p; } @@ -757,6 +782,29 @@ std::expected Reader::statement(const toml::Table& t) co return std::unexpected(std::format("{} must be an integer", key)); s.priority = static_cast(v.as_int()); s.prioritySet = true; + } else if (k == "system") { + if (!v.is_array()) + return std::unexpected(std::format("{} is an array of tables: write [[platform.system]]", key)); + std::size_t i = 0; + for (auto const& e : v.as_array()) { + const auto where = std::format("[[platform.system]] #{}", ++i); + if (!e.is_table()) return std::unexpected(std::format("{} must be a table", where)); + SystemSpec sys; + sys.origin = Origin{ tier, file.generic_string(), 0, where }; + for (auto const& [sk, sv] : e.as_table()) { + if (!contains(kSystemKeys, sk)) { + warn(std::format("{}: '{}' is not a key of a system (ignored); the keys are {}", + where, sk, list_of(kSystemKeys))); + continue; + } + if (!sv.is_string()) + return std::unexpected(std::format("{}.{} must be a string", where, sk)); + (sk == "name" ? sys.name : sys.uname) = sv.as_string(); + } + if (sys.name.empty()) + return std::unexpected(std::format("{} names no system: write name = \"\"", where)); + s.systems.push_back(std::move(sys)); + } } else if (k == "define" || k == "provider") { if (!v.is_array()) return std::unexpected(std::format( diff --git a/modules/toolchain-model/build.mcpp b/modules/toolchain-model/build.mcpp index 80cd9d78a..6b79f4586 100644 --- a/modules/toolchain-model/build.mcpp +++ b/modules/toolchain-model/build.mcpp @@ -16,12 +16,15 @@ namespace { constexpr std::string_view kRowKeys[] = { "triple", "tier", "note", "standard", "exact", "march", "mabi", "code-model", "multilib", "flags", - "link-emulation", "clang-triple", "family", + "link-emulation", "clang-triple", "family", "object-format", "data-model", "exe-suffix", + "shared-suffix", "loader", "execution", }; constexpr std::string_view kProviderKeys[] = { "name", "serves", "priority", "hosts", "probe", "toolchain", "pairs", "sysroot", "linkage", - "platform", + "platform", "tier", "system-headers", "runner", }; +constexpr std::string_view kRunnerKeys[] = { "cmd", "hosts", "requires" }; +constexpr std::string_view kSystemKeys[] = { "name", "uname" }; constexpr std::string_view kTiers[] = { "verified", "preview", "planned" }; struct Failure { std::string text; }; @@ -113,13 +116,18 @@ std::string row_entry(const toml::Table& t, Arrays& arrays, std::set& rows) { @@ -146,13 +154,40 @@ std::string provider_entry(const toml::Table& t, Arrays& arrays, const std::set< const auto linkage = text(t, "linkage", where); if (!linkage.empty() && linkage != "static" && linkage != "dynamic") throw Failure{std::format("{}: linkage `{}` is not static or dynamic", where, linkage)}; + const auto tier = text(t, "tier", where); + if (!tier.empty() && std::ranges::find(kTiers, tier) == std::end(kTiers)) + throw Failure{std::format("{}: tier `{}` is not verified, preview or planned", where, tier)}; + // `runner = { cmd = [...], hosts = [...], requires = [...] }`: how an + // artifact of the arrangement is started, where, and from which packages. + std::vector runner, runnerHosts, runnerRequires; + if (auto it = t.find("runner"); it != t.end()) { + if (!it->second.is_table()) throw Failure{std::format("{}: `runner` is not a table", where)}; + auto const& r = it->second.as_table(); + const auto rwhere = where + " runner"; + known_keys(r, kRunnerKeys, rwhere); + runner = words(r, "cmd", rwhere); + runnerHosts = words(r, "hosts", rwhere); + runnerRequires = words(r, "requires", rwhere); + if (runner.empty()) throw Failure{std::format("{}: `cmd` is empty", rwhere)}; + } return std::format( " {{ .name = {}, .serves = {}, .priority = {}, .hosts = {}, .probe = {}, .toolchain = {}, " - ".pairs = {}, .sysroot = {}, .linkage = {}, .platform = {} }},\n", + ".pairs = {}, .sysroot = {}, .linkage = {}, .platform = {}, .tier = {}, .systemHeaders = {}, " + ".runner = {}, .runnerHosts = {}, .runnerRequires = {} }},\n", lit(name), arrays.add(serves), priority, arrays.add(words(t, "hosts", where)), lit(text(t, "probe", where)), lit(text(t, "toolchain", where)), arrays.add(words(t, "pairs", where)), lit(text(t, "sysroot", where)), lit(linkage), - lit(text(t, "platform", where))); + lit(text(t, "platform", where)), lit(tier), lit(text(t, "system-headers", where)), + arrays.add(runner), arrays.add(runnerHosts), arrays.add(runnerRequires)); +} + +std::string system_entry(const toml::Table& t, std::set& seen) { + known_keys(t, kSystemKeys, "[[platform.system]]"); + const auto name = text(t, "name", "[[platform.system]]"); + if (name.empty()) throw Failure{"[[platform.system]]: an entry without `name`"}; + if (!seen.insert(name).second) throw Failure{std::format("[[platform.system]] {}: stated twice", name)}; + return std::format(" {{ .name = {}, .uname = {} }},\n", lit(name), + lit(text(t, "uname", "[[platform.system]] " + name))); } const toml::Array& entries(const toml::Document& doc, std::string_view path) { @@ -166,7 +201,8 @@ const toml::Array& entries(const toml::Document& doc, std::string_view path) { std::string generate(const toml::Document& doc) { Arrays arrays; std::set rowNames; - std::string rows, providers; + std::set systemNames; + std::string rows, providers, systems; for (auto const& e : entries(doc, "platform.define")) { if (!e.is_table()) throw Failure{"[[platform.define]]: an entry that is not a table"}; rows += row_entry(e.as_table(), arrays, rowNames); @@ -175,6 +211,10 @@ std::string generate(const toml::Document& doc) { if (!e.is_table()) throw Failure{"[[platform.provider]]: an entry that is not a table"}; providers += provider_entry(e.as_table(), arrays, rowNames); } + for (auto const& e : entries(doc, "platform.system")) { + if (!e.is_table()) throw Failure{"[[platform.system]]: an entry that is not a table"}; + systems += system_entry(e.as_table(), systemNames); + } return std::format( "// Generated by modules/toolchain-model/build.mcpp from data/builtin.toml.\n" "export module mcpp.toolchain.builtin_targets;\n\n" @@ -184,8 +224,9 @@ std::string generate(const toml::Document& doc) { "export namespace mcpp::toolchain::builtin {{\n\n" "inline constexpr mcpp::toolchain::target::Row kRows[] = {{\n{}}};\n\n" "inline constexpr mcpp::toolchain::target::Provider kProviders[] = {{\n{}}};\n\n" + "inline constexpr mcpp::toolchain::target::System kSystems[] = {{\n{}}};\n\n" "}} // namespace mcpp::toolchain::builtin\n", - arrays.decls, rows, providers); + arrays.decls, rows, providers, systems); } } // namespace diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 8130c137f..b8f0da8df 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -30,6 +30,14 @@ # is not derived from the identity (`x86_64-unknown-freebsd14.0`) # family the system's family, `unix` or `windows` (`cfg(unix)`); a # system the engine has always known answers it by convention +# loader the program interpreter a dynamic program of the row names: the +# path on the machine that runs it. A link whose artifact this +# machine does not load writes it; a native link writes the +# payload's own loader +# execution the system the row's artifacts run on, when it is not the os +# segment (`android`: the kernel is Linux, the system is not) +# object-format, data-model, exe-suffix, shared-suffix +# facts of a system the engine's conventions do not answer # march, mabi, code-model, multilib, flags, link-emulation # the ISA profile of a bare-metal row: `-march`, `-mabi`, # `-mcmodel` (empty: the driver's default), the multilib @@ -51,16 +59,37 @@ # toolchain ships the whole sysroot itself # linkage the default linkage, `static` or `dynamic` # platform a platform package that implements it (SPEC-013) +# tier the tier of this arrangement, when it differs from the row's +# system-headers the package whose headers state the system's own interfaces, +# read by the steps that compile against them (resources) +# runner how an artifact of the arrangement is started when nothing +# nearer says: `{ cmd = [argv], hosts = [...], requires = [...] }`. +# `{}` is the artifact, `{sysroot}` the arrangement's base; +# `requires` are installed on the first run, never by a build +# +# ONE ENTRY STATES ONE ARRANGEMENT OF PAYLOADS. A native arrangement and a cross +# one are two entries, each with its `hosts`; an entry whose facts depend on the +# host it is read on is two entries written as one. A NEW ENTRY DOES NOT CHANGE +# THE ANSWER OF A REQUEST THAT WAS ANSWERED BEFORE IT: it enters below the +# priority of the entries that answer `arch-os` today (SPEC-016 §4.7). +# +# System keys (`[[platform.system]]`) +# name the os segment the system's rows carry +# uname the word `uname -s` answers there; a file that carries one +# program per system selects by it, and a system without the +# key joins no such file # ── Linux ──────────────────────────────────────────────────────────────────── [[platform.define]] triple = "x86_64-linux-gnu" +loader = "/lib64/ld-linux-x86-64.so.2" tier = "verified" standard = true [[platform.define]] triple = "x86_64-linux-musl" +loader = "/lib/ld-musl-x86_64.so.1" tier = "verified" standard = true @@ -70,11 +99,13 @@ standard = true # (SPEC-009 §10.7). [[platform.define]] triple = "aarch64-linux-gnu" +loader = "/lib/ld-linux-aarch64.so.1" tier = "preview" standard = true [[platform.define]] triple = "aarch64-linux-musl" +loader = "/lib/ld-musl-aarch64.so.1" tier = "verified" standard = true @@ -126,6 +157,7 @@ standard = false [[platform.define]] triple = "riscv64-linux-musl" +loader = "/lib/ld-musl-riscv64.so.1" tier = "planned" standard = true @@ -135,11 +167,13 @@ standard = true # an explicit `[target.] toolchain` opts in early. [[platform.define]] triple = "i686-linux-musl" +loader = "/lib/ld-musl-i386.so.1" tier = "planned" standard = true [[platform.define]] triple = "i686-linux-gnu" +loader = "/lib/ld-linux.so.2" tier = "planned" standard = true @@ -171,6 +205,7 @@ standard = false # COMPAT_FREEBSD14, on 15. [[platform.define]] triple = "x86_64-freebsd" +loader = "/libexec/ld-elf.so.1" family = "unix" tier = "preview" standard = true @@ -178,6 +213,7 @@ clang-triple = "x86_64-unknown-freebsd14.0" [[platform.define]] triple = "aarch64-freebsd" +loader = "/libexec/ld-elf.so.1" family = "unix" tier = "preview" standard = true @@ -363,12 +399,14 @@ link-emulation = "elf_i386" [[platform.define]] triple = "aarch64-linux-android" +execution = "android" tier = "verified" standard = true exact = true [[platform.define]] triple = "x86_64-linux-android" +execution = "android" tier = "verified" standard = true exact = true @@ -417,9 +455,7 @@ standard = true # which serves the musl identities of every system, 50; other conventions 10; a # base package beside llvm where a payload of the system's own exists too, 5. -# glibc from the payload, at the host's default toolchain natively. A cross to -# another architecture's glibc pins llvm and names the sysroot payload its C -# library comes from (the aarch64 row below; x86_64's hosts list admits it). +# glibc from the payload, at the host's default toolchain natively. [[platform.provider]] name = "glibc" serves = ["x86_64-linux-gnu"] @@ -430,19 +466,25 @@ hosts = ["linux-{arch}"] name = "glibc" serves = ["aarch64-linux-gnu"] priority = 100 -# The payload: native on each Linux arch; the cross from x86_64 takes the C -# library from a sysroot payload the row names (2026.11.12.1, G1) -- llvm -# alone carries no glibc of another architecture, and the host's is not the -# target's. The payload's package root IS the sysroot (glibc 2.44 with the -# kernel headers, libgcc and libc++ inside), so the row is served wherever -# that payload is published, with no host named here beyond the data. -hosts = ["linux-{arch}", "linux-x86_64"] +hosts = ["linux-{arch}"] +toolchain = "llvm@23.1.3" + +# THE CROSS ARRANGEMENT OF THE SAME IDENTITY: the C library, libc++ and the +# compiler run-time come from a base package whose root is the sysroot, since +# the toolchain's payload carries the libraries of its own host only. Below the +# musl entries (100, 50), so `aarch64-linux` is answered as before it existed +# and the whole identity reaches it. The loader the artifact names is the row's; +# it runs through the runner, which is given the base as its root. +[[platform.provider]] +name = "glibc-sysroot" +serves = ["aarch64-linux-gnu"] +priority = 40 +hosts = ["linux-x86_64"] toolchain = "llvm@23.1.3" sysroot = "xim:aarch64-linux-gnu-sysroot@2.44.3" -# glibc's own linkage is dynamic; the compiler run-time is the payload's -# (libgcc's archives and compiler-rt's builtins, both static), so a link -# takes nothing from the host. linkage = "dynamic" +tier = "preview" +runner = { cmd = ["qemu-aarch64-static", "-L", "{sysroot}", "{}"], hosts = ["linux-x86_64"], requires = ["xim:qemu-user-aarch64@7.2.0"] } # The musl cross payloads: native on each Linux arch, every arch from x86_64, # and the Canadian cross on a Windows host of the same arch. @@ -562,10 +604,42 @@ platform = "mcpplibs.openkal-platform@0.26.0" [[platform.provider]] name = "openkal" -serves = ["x86_64-windows-musl", "*-linux-musl", "*-macos-musl", "*-freebsd-musl"] +serves = ["*-linux-musl", "*-macos-musl", "*-freebsd-musl"] priority = 50 hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" platform = "mcpplibs.openkal-platform@0.26.0" + +# The same arrangement on Windows, whose own interfaces a resource script +# compiles against: their headers are the MinGW-w64 base's, which states them +# for every architecture under one `include`. +[[platform.provider]] +name = "openkal" +serves = ["x86_64-windows-musl"] +priority = 50 +hosts = ["*"] +toolchain = "llvm@23.1.3" +pairs = ["llvm"] +linkage = "static" +platform = "mcpplibs.openkal-platform@0.26.0" +system-headers = "xim:mingw-w64-sysroot@20261104-2" + +# ── Systems ────────────────────────────────────────────────────────────────── + +[[platform.system]] +name = "linux" +uname = "Linux" + +[[platform.system]] +name = "macos" +uname = "Darwin" + +[[platform.system]] +name = "freebsd" +uname = "FreeBSD" + +# A Windows program is not selected by `uname`: it leads the file. +[[platform.system]] +name = "windows" diff --git a/modules/toolchain-model/src/base.cppm b/modules/toolchain-model/src/base.cppm index 160768ced..3d3334421 100644 --- a/modules/toolchain-model/src/base.cppm +++ b/modules/toolchain-model/src/base.cppm @@ -451,6 +451,10 @@ struct Resolution { // What a binding artifact loads: the base's dynamic loader and the // directories its RUNPATH names. std::filesystem::path loader; + // What a floor's dynamic artifact names as its program interpreter: the + // row's `loader`, a path on the machine that runs it (SPEC-016 §6). Empty + // for a binding (which names `loader`) and for a static link. + std::string interpreter; std::vector runtimeDirs; // Kernel headers the base lacks, supplied by the engine (§2.4, additive). std::vector addedIncludes; diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index dbe03def1..3c0648f36 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -22,6 +22,8 @@ import mcpplibs.os.platform; import mcpp.toolchain.apple_sdk; import mcpp.toolchain.model; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; export namespace mcpp::toolchain { @@ -302,50 +304,27 @@ struct ClangDriverModel { } }; -// ── loader resolution: data over hardcodes ─────────────────────────────── +// ── loader resolution: the row's data ──────────────────────────────────── // -// Priority: -// 1. Triple map (x86_64/aarch64/riscv64/loongarch64 glibc + musl) — -// loader file names are platform-ABI-level stable conventions. -// 2. Glob: first `ld-*.so*` regular file in the lib dir (covers arches -// the map doesn't know yet). -// -// A third, declared-metadata source (a persisted `.xpkg-exports.json` -// written by the installer) was evaluated and removed: its only consumer -// would have been this resolver, while the two sources above already cover -// every real payload — see docs/91-toolchain-internals.md for the record. -// -// Returns the loader's absolute path, or empty when none was found (callers -// then omit --dynamic-linker and the hermeticity check reports the gap). - -// Loader *file name* for a target triple; empty when the arch is unknown. -std::string loader_filename(std::string_view targetTriple) { - const bool musl = targetTriple.find("musl") != std::string_view::npos; - struct ArchLoader { std::string_view arch, gnuName, muslArch; }; - static constexpr std::array kMap{{ - {"x86_64", "ld-linux-x86-64.so.2", "x86_64"}, - {"aarch64", "ld-linux-aarch64.so.1", "aarch64"}, - {"riscv64", "ld-linux-riscv64-lp64d.so.1", "riscv64"}, - {"loongarch64", "ld-linux-loongarch-lp64d.so.1", "loongarch64"}, - {"i686", "ld-linux.so.2", "i386"}, - }}; - for (auto& m : kMap) { - if (targetTriple.starts_with(m.arch)) { - if (musl) return std::format("ld-musl-{}.so.1", m.muslArch); - return std::string(m.gnuName); - } - } - return {}; +// A row states the program interpreter its dynamic programs name (`loader`, +// SPEC-016 §2.1): the path on the machine that runs them. The file name of +// that path is what a payload's library directory holds, and the path itself +// is what a shipped binary's PT_INTERP reads. A triple no row states one for +// has neither; `resolve_loader` then takes the first `ld-*.so*` of the +// directory. + +// The distro-side loader path of a target (`mcpp pack`, the host's own root). +std::string distro_loader_path(std::string_view targetTriple) { + auto t = triple::parse(targetTriple); + auto const* r = target::find_row(t ? t->str() : std::string(targetTriple)); + return r ? std::string(r->loader) : std::string{}; } -// The distro-side loader path a *shipped* binary's PT_INTERP should point at -// (LSB layout), by target triple. Used by `mcpp pack`. -std::string distro_loader_path(std::string_view targetTriple) { - auto name = loader_filename(targetTriple); - if (name.empty()) return {}; - if (targetTriple.starts_with("x86_64")) - return "/lib64/" + name; // LSB-mandated symlink on glibc distros - return "/lib/" + name; +// Its file name; empty when the row states none. +std::string loader_filename(std::string_view targetTriple) { + const auto p = distro_loader_path(targetTriple); + const auto slash = p.rfind('/'); + return slash == std::string::npos ? p : p.substr(slash + 1); } std::filesystem::path resolve_loader(const std::filesystem::path& libDir, @@ -353,7 +332,7 @@ std::filesystem::path resolve_loader(const std::filesystem::path& libDir, if (libDir.empty()) return {}; std::error_code ec; - // 1. Triple map. + // 1. The row's loader. if (auto name = loader_filename(targetTriple); !name.empty()) { auto p = libDir / name; if (std::filesystem::exists(p, ec)) return p; @@ -577,6 +556,13 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { lm.loader = tc.base.loader; for (auto const& d : tc.base.runtimeDirs) lm.libDirs.push_back(d); } + // A FLOOR'S DYNAMIC ARTIFACT NAMES THE ROW'S LOADER (SPEC-016 §6): + // the path on the machine that runs it, which the base resolution + // took from the row. No directory joins it: the artifact's + // libraries are that machine's. + else if (tc.base.runtime == base::Runtime::Floor && !tc.base.interpreter.empty()) { + lm.loader = tc.base.interpreter; + } if (tc.base.multiarch && !tc.base.multiarchTuple.empty()) { lm.multiarchIncludes.push_back(root / "usr" / "include" / tc.base.multiarchTuple); lm.multiarchLibDirs.push_back(root / "usr" / "lib" / tc.base.multiarchTuple); diff --git a/modules/toolchain-model/src/target_rows.cppm b/modules/toolchain-model/src/target_rows.cppm index 2719e549d..4d91dc566 100644 --- a/modules/toolchain-model/src/target_rows.cppm +++ b/modules/toolchain-model/src/target_rows.cppm @@ -27,6 +27,7 @@ struct DefinitionSet { std::deque> lists; std::vector rows; std::vector providers; + std::vector systems; std::string_view keep(std::string s) { return strings.emplace_back(std::move(s)); } std::span keep(const std::vector& v) { @@ -45,6 +46,11 @@ std::vector rows(); std::vector providers(); const Row* find_row(std::string_view triple); +// The system table: the built-in systems, then those the platforms state. A +// system stated twice is the first statement's. +std::vector systems(); +const System* find_system(std::string_view name); + // Whether a defined row names this os, or this env under this os: the words the // parser accepts beyond its own. bool defines_os(std::string_view os); @@ -134,6 +140,24 @@ const Row* find_row(std::string_view triple) { return nullptr; } +std::vector systems() { + std::vector out; + for (auto const& b : mcpp::toolchain::builtin::kSystems) out.push_back(&b); + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& d : s.systems) + if (std::ranges::none_of(out, [&](const System* k) { return k->name == d.name; })) + out.push_back(&d); + return out; +} + +const System* find_system(std::string_view name) { + for (auto const* s : systems()) + if (s->name == name) return s; + return nullptr; +} + bool defines_os(std::string_view os) { // The built-in rows' systems are words of the language too (design // 2026-11-10 E4): a system the data names -- FreeBSD -- needs no word in diff --git a/modules/toolchain-model/src/target_schema.cppm b/modules/toolchain-model/src/target_schema.cppm index a36e0a4a1..928cbad1f 100644 --- a/modules/toolchain-model/src/target_schema.cppm +++ b/modules/toolchain-model/src/target_schema.cppm @@ -59,6 +59,26 @@ struct Provider { std::string_view sysroot; // a payload, or `toolchain` std::string_view linkage; // static | dynamic std::string_view platform; // a platform package reference + std::string_view tier; // of this arrangement; empty: the row's + // The package whose headers state the system's own interfaces, for the + // steps that compile against them and not against the C library (a + // resource compiler's `windows.h`). + std::string_view systemHeaders; + // HOW AN ARTIFACT OF THE ARRANGEMENT IS STARTED (SPEC-016 §6): the argv + // template, the hosts on which that program exists (empty: every host), + // and the packages it is installed from on the first run. + std::span runner; + std::span runnerHosts; + std::span runnerRequires; +}; + +// A SYSTEM (SPEC-016 §2.3): the facts of an operating system that no single +// row states. `uname` is the word `uname -s` answers there, which a file that +// carries one program per system selects by; a system without it joins no +// such file. +struct System { + std::string_view name; + std::string_view uname; }; // THE PATTERN OF `serves`: segment by segment, `*` standing for any one segment diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 60df8b5bd..9a5f7f1b8 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -311,6 +311,17 @@ struct Triple { // -- belongs to such a target; what does is the platform's sysroot or base. bool c_library_by_platform() const; + // WHERE THE ROW'S ARTIFACT RUNS, COMPARED WITH THIS MACHINE (SPEC-016 §6), + // asked in one place. `system_is_host`: its execution system is this + // machine's -- the question a launch asks, since a kernel refuses or + // emulates a program of its own system and another architecture, and gives + // a program of another system's call numbers its own meaning for them. + // `loads_on_host`: the system and the architecture both -- the question a + // check of the artifact's loader and run-time closure asks, since those + // describe a process of this machine. + bool system_is_host() const; + bool loads_on_host() const; + // 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 @@ -693,6 +704,11 @@ inline Triple host_triple() { return t; } +inline bool Triple::system_is_host() const { return execution_system() == host_triple().os; } +inline bool Triple::loads_on_host() const { + return system_is_host() && arch == mcpp::toolchain::platform::host_arch; +} + // ── Version pins (single site; §4.6 of the design doc) ─────────────────────── // Every default/convention toolchain version literal lives here. Help and // error strings format these — never inline a pinned version elsewhere. diff --git a/src/build/execute.cppm b/src/build/execute.cppm index eed11b290..4d2b93e83 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -1356,35 +1356,44 @@ ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string return h; } -// THE PHYSICAL BOUNDARY (§3.8): an artifact no path from this machine can -// execute is refused before anything is spawned, naming the key that adds one. -// Today that is an Apple artifact on a host that is not macOS: the simulator -// and the OS run only there. `--no-runner` states that the triple is native -// here, and is believed. +// THE PHYSICAL BOUNDARY (§3.8, SPEC-016 §6): an artifact this machine does +// not execute is refused before anything is spawned, naming the key that adds +// a way. One predicate for every row, built-in or defined: the row's execution +// system against this machine's. `--no-runner` states that the triple is +// native here, and is believed. std::optional unreachable_execution(const mcpp::build::BuildContext& ctx, std::string_view tripleKey, bool noRunner, bool runnerDeclared = false) { if (noRunner || !ctx.runUnder.empty()) return std::nullopt; auto t = mcpp::toolchain::triple::parse(tripleKey); - // AN ARTIFACT OF A SYSTEM A PLATFORM DEFINES runs where its row's - // `execution` names (mcpp#911 G11): on this machine when that is this - // machine's system, else through a runner -- a runner package's program, - // which reaches a VM, a remote host or a device by the run protocol - // (docs/41) -- or an execution host. With neither it is built, not run. - if (auto const* d = t ? t->defined() : nullptr; d && !runnerDeclared) { - const std::string_view system = d->execution.empty() ? std::string_view(t->os) : d->execution; - const std::string_view here = mcpplibs::os::is_windows ? "windows" - : mcpplibs::os::is_macos ? "macos" : "linux"; - if (system != here) - return std::format( - "{} artifacts run on {}, not on this {} host: built, not run.\n" - " State how they run: [target.{}] runner = [\"\", \"{{}}\"], the program a " - "runner package builds\n" - " ([target.{}.tools] = {{ ... }}; the run protocol is docs/41), or an " - "execution host:\n" - " [target.{}] run_under = [\"remote-run\", \"\", \"--\"].", - tripleKey, system, here, tripleKey, tripleKey, tripleKey); - } + // An Apple artifact off macOS: no runner reaches it, only an execution host. + if (!mcpplibs::os::is_macos && t && t->is_apple()) + return std::format( + "cannot run {} artifacts on this host: {} runs only on macOS.\n" + " Name an execution host: [target.{}] run_under = [\"remote-run\", \"\", \"--\"]\n" + " (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine).", + tripleKey, t->is_ios_simulator() ? "the iOS Simulator" : t->is_ios() ? "iOS" : "macOS", + tripleKey); + // AN ARTIFACT OF ANOTHER SYSTEM IS NOT STARTED TO SEE WHAT HAPPENS (SPEC-016 + // §6). A row's artifacts run on its execution system -- the `execution` its + // row states, built-in or defined, else its own -- and that is this + // machine's system or it is not. Where it is not, the artifact runs through + // a runner (a provider's, a platform's, a runner package's program by the + // run protocol, docs/41) or an execution host; with neither it is built, + // not run. The kernel of this machine gives another system's call numbers + // its own meaning, so a launch is not an observation. An artifact of this + // system and another architecture is launched as before: the kernel + // refuses it or an emulator it knows runs it. + if (t && !t->is_freestanding() && !runnerDeclared && !t->system_is_host()) + return std::format( + "{} artifacts run on {}, not on this {} host: built, not run.\n" + " State how they run: [target.{}] runner = [\"\", \"{{}}\"], the program a " + "runner package builds\n" + " ([target.{}.tools] = {{ ... }}; the run protocol is docs/41), or an " + "execution host:\n" + " [target.{}] run_under = [\"remote-run\", \"\", \"--\"].", + tripleKey, t->execution_system(), mcpp::toolchain::triple::host_triple().os, + tripleKey, tripleKey, tripleKey); // A RUNNER THAT RUNS THE ARTIFACT ON A MACHINE OF ITS ARCHITECTURE // (`runner = { ..., host = "same-arch" }`, SPEC-013): an ok image's loader // is a program of this machine, and an image of another architecture is @@ -1402,14 +1411,7 @@ std::optional unreachable_execution(const mcpp::build::BuildContext tripleKey, mcpp::toolchain::platform::host_arch, r.cmd.empty() ? std::string() : r.cmd.front(), mcpp::platform::origin_text(r.origin), tripleKey); - if (mcpplibs::os::is_macos) return std::nullopt; - if (!t || !t->is_apple()) return std::nullopt; - return std::format( - "cannot run {} artifacts on this host: {} runs only on macOS.\n" - " Name an execution host: [target.{}] run_under = [\"remote-run\", \"\", \"--\"]\n" - " (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine).", - tripleKey, t->is_ios_simulator() ? "the iOS Simulator" : t->is_ios() ? "iOS" : "macOS", - tripleKey); + return std::nullopt; } // Compile a prepared BuildContext. Shared between `mcpp build` and `mcpp run` @@ -1448,6 +1450,10 @@ struct RunnerChoice { bool ignored = false; // --no-runner dropped a declared template bool longLived = false; // declared by the package; no natural end bool fromPayload = false; // the toolchain payload's runner, nothing declared + // The packages the runner is installed from that do not stand on this + // machine (SPEC-016 §6): a run or a test installs them and asks again; a + // build installs nothing. + std::vector missing; // The spelling that names this target in the manifest: the canonical form, // which is also the output directory's name and the key every // `[target.]` reader resolves. Every diagnostic below prints this @@ -1459,6 +1465,50 @@ struct RunnerChoice { std::string tripleKey; }; +// THE PLACES A RUNNER'S TEMPLATE NAMES (SPEC-016 §6). `{sysroot}` is the base +// the artifact was linked against (a user-mode emulator's root); `{pkg:}` +// the root of the package of that name among `requires` (an image, a firmware +// file); the template's program is taken from a required package before PATH. +// Packages are located where they stand, and the ones that do not stand are +// named for the caller: what installs them is a run, never this lookup. +void settle_runner_places(const BuildContext& ctx, RunnerChoice& c, + std::span requires_) { + std::map roots; + for (auto const& ref : requires_) { + auto name = ref.substr(ref.find(':') == std::string::npos ? 0 : ref.find(':') + 1); + name = name.substr(0, name.find('@')); + if (auto dir = mcpp::build::package_root(ref, false)) roots[name] = *dir; + else c.missing.push_back(ref); + } + if (c.tmpl.empty()) return; + for (auto const& [name, root] : roots) { + auto found = mcpp::build::runner_lookup::locate( + c.tmpl.front(), mcpp::build::runner_lookup::payload_search_dirs(root), {}); + if (!found.program) continue; + c.tmpl.front() = found.program->string(); + break; + } + auto place = [](std::string& word, std::string_view key, const std::string& value) { + for (auto at = word.find(key); at != std::string::npos; at = word.find(key, at + value.size())) + word.replace(at, key.size(), value); + }; + for (auto& word : c.tmpl) { + if (!ctx.tc.base.root.empty()) place(word, "{sysroot}", ctx.tc.base.root.generic_string()); + for (auto const& [name, root] : roots) + place(word, std::format("{{pkg:{}}}", name), root.generic_string()); + } +} + +// What a run or a test does about `missing`: installs each through the channel +// a toolchain pin uses, with the same consent. Empty: all stand. +std::string install_runner_packages(const RunnerChoice& c) { + for (auto const& ref : c.missing) + if (auto dir = mcpp::build::package_root(ref, true); !dir) + return std::format("the runner of '{}' is installed from {}, and {}", c.tripleKey, ref, + dir.error()); + return {}; +} + // ONE READER FOR FOUR SLOTS, PARAMETERISED BY THE SLOT. // // `run`, `flash`, `monitor` and `debug` resolve identically: a dependency @@ -1493,6 +1543,7 @@ RunnerChoice choose_device_action(const BuildContext& ctx, c.tmpl = pf.runner.cmd; c.fromManifest = overrides_program(pf.runner.origin, !ctx.manifest.buildConfig.runner.empty()); + settle_runner_places(ctx, c, pf.runner.requires_); } } else if (auto it = pf.runners.find(std::string(which)); it != pf.runners.end()) { c.tmpl = it->second.value; @@ -3294,7 +3345,14 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, // while debugging, or naming `qemu-aarch64-static` for a cross target. const bool isRunSlot = runner_name.empty(); const std::string slotName{runner_name}; - const auto choice = choose_device_action(ctx, runner_name, no_runner); + auto choice = choose_device_action(ctx, runner_name, no_runner); + if (!choice.missing.empty()) { + if (auto why = install_runner_packages(choice); !why.empty()) { + std::println(libc::stderr(), "error: {}", why); + return 2; + } + choice = choose_device_action(ctx, runner_name, no_runner); + } if (choice.ignored) mcpp::ui::info("note", std::format( "--no-runner: ignoring the runner declared for {}", choice.tripleKey)); @@ -3493,32 +3551,8 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, emit_launch(env); return 0; } - // A PROGRAM OF ANOTHER SYSTEM'S KERNEL INTERFACE, of this machine's own - // architecture, STARTS AND DIES ON ITS FIRST KERNEL CALL (G6): the kernel - // maps a static foreign ELF without asking which system it names, the - // program then states a syscall in that system's vocabulary, and the death - // is by signal — where mcpp, having become the program on POSIX, can say - // nothing at all. Such an artifact is run as a child instead, so its death - // is observed, named with the [target.] runner declaration, and - // its status returned unchanged: 139 stays 139, exactly what a direct - // launch would have reported. Every other launch — a declared runner, - // `run_under`, a native or cross-arch target — keeps the launch it always - // had, and a native signal death is never interpreted: that death is the - // program's own. - const bool foreignKernel = [&] { - if (!choice.tmpl.empty() || !host.prefix.empty()) return false; - auto t = mcpp::toolchain::triple::parse(choice.tripleKey); - if (!t || t->os.empty()) return false; - const std::string_view here = mcpplibs::os::is_windows ? "windows" - : mcpplibs::os::is_macos ? "macos" : "linux"; - return t->os != here && t->arch == mcpp::toolchain::platform::host_arch; - }(); int spawnErr = 0; - int rc = 0; - if (foreignKernel) - rc = mcpp::run::run_exec(argv, childEnv, &spawnErr); - else - rc = mcpp::run::run_foreground(argv, childEnv, &spawnErr); + const int rc = mcpp::run::run_foreground(argv, childEnv, &spawnErr); if (spawnErr != 0) { using namespace mcpp::build::runner_lookup; if (!choice.tmpl.empty() || !host.prefix.empty()) @@ -3526,20 +3560,10 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, else if (classify(spawnErr) == SpawnClass::Unloadable) std::println(libc::stderr(), "error: {}", unrunnable_message(choice.tripleKey, exe, spawnErr)); - else if (foreignKernel) - // The program was found and its interpreter was not (ENOENT, a - // dynamic linker of the other system): the bare errno names - // neither fact, so the same runner declaration follows it. - std::println(libc::stderr(), "error: {}{}", - spawn_failed_message(exe.string(), spawnErr), - runner_advice_text(choice.tripleKey)); else std::println(libc::stderr(), "error: {}", spawn_failed_message(exe.string(), spawnErr)); return launcher_status(spawnErr); } - if (foreignKernel && mcpp::build::runner_lookup::death_by_signal(rc)) - std::println(libc::stderr(), "error: {}", - mcpp::build::runner_lookup::signal_death_message(choice.tripleKey, exe, rc)); return rc; } @@ -4674,6 +4698,16 @@ static void test_bulk(TestBuild& tb, const TestOptions& testOpts, Keep&& keep) { static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool json) { auto* ctx = &*tb.ctx; tb.runnerChoice = choose_device_action(*ctx, testOpts.runnerName, testOpts.noRunner); + if (!tb.runnerChoice.missing.empty()) { + // A package that cannot be installed leaves the tests not run, said + // once for the build as a missing program is. + if (auto why = install_runner_packages(tb.runnerChoice); !why.empty()) { + tb.invocationNotRunReason = why; + std::println(libc::stderr(), "error: {}", why); + return; + } + tb.runnerChoice = choose_device_action(*ctx, testOpts.runnerName, testOpts.noRunner); + } auto& runnerChoice = tb.runnerChoice; if (!testOpts.runnerName.empty() && runnerChoice.tmpl.empty()) { tb.invocationNotRunReason = std::format("no runner named '{}' for '{}'", diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 37cc75a60..ad027b2fe 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1017,30 +1017,8 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { link_toolchain_flags += " -fuse-ld=lld"; link_toolchain_flags_c = link_toolchain_flags; } else { - // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot - // form): the host's model contributes nothing, exactly as when the - // graph supplies it -- the base's own -B/-L/loader words reach the - // line through the payload channel, and this machine's runtime - // directories would shadow the target's (measured: the host payload's - // x86_64 libatomic.a resolved ahead of the sysroot's, and lld called - // it incompatible with aarch64linux). - if (lm.mode == mcpp::toolchain::CLibMode::Sysroot - && plan.toolchain.base.layers.cAbi.empty()) + if (lm.mode == mcpp::toolchain::CLibMode::Sysroot) link_toolchain_flags += lm.link_flags(ninjaEsc); - - // THE BASE'S ROOT IS A SYSROOT (the gnu-sysroot form): name it to - // the driver, and the ld scripts inside it (/lib GROUPs), its crt - // objects and its loader all resolve the way a --sysroot link - // resolves them -- the payload's own mcpp.toml states the package - // root IS the sysroot, and without the flag a dynamic link reads - // /lib/libc.so.6 against this machine's root instead. - if (!plan.toolchain.base.layers.cAbi.empty()) { - std::filesystem::path baseDir = plan.toolchain.base.root; - if (baseDir.empty() && plan.toolchain.base.payload) - baseDir = plan.toolchain.base.payload->glibcLib.parent_path(); - if (!baseDir.empty()) - link_toolchain_flags += " --sysroot=" + ninjaEsc(baseDir); - } link_toolchain_flags_c = link_toolchain_flags + std::string(mcpp::toolchain::ClangDriverModel::kLinkDriverFlagsC); // A graph-supplied libstdc++ under the payload's clang: the driver @@ -1055,9 +1033,9 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { } f.sysroot = link_toolchain_flags; } else if (lm.mode != mcpp::toolchain::CLibMode::None) { - if (lm.mode == mcpp::toolchain::CLibMode::Sysroot - && plan.toolchain.base.layers.cAbi.empty()) - link_toolchain_flags += lm.link_flags(ninjaEsc); + // GCC (or Clang without cfg): --sysroot from probe, or the payload + // headers + C runtime (-B for crt discovery, -L for -lc/-lm). + 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) { @@ -1917,18 +1895,11 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { std::string runtime_dirs; if constexpr (mcpp::toolchain::platform::supports_rpath) { - // Toolchain runtime dirs (glibc/gcc) as before -- but not for a - // target whose C library a row's base package serves (the - // gnu-sysroot form): those directories are THIS MACHINE's, and on a - // cross they would both shadow the target's libraries (-L precedes - // the base's own) and bake a host RUNPATH into the target's - // artifact. The base's directories reach the line through its own - // channel. - if (plan.toolchain.base.layers.cAbi.empty()) - for (auto& dir : plan.toolchain.linkRuntimeDirs) { - runtime_dirs += " -L" + escape_path(dir); - runtime_dirs += " -Wl,-rpath," + escape_path(dir); - } + // Toolchain runtime dirs (glibc/gcc) as before... + for (auto& dir : plan.toolchain.linkRuntimeDirs) { + runtime_dirs += " -L" + escape_path(dir); + runtime_dirs += " -Wl,-rpath," + escape_path(dir); + } // Dependency/runtime provider search comes from LinkIntent and is // rendered separately. In particular runtimeSearchDirs contributes // RUNPATH only; it must never become a link-time -L path. @@ -2011,6 +1982,10 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // A BASE PACKAGE'S RUN-TIMES ON THE LINK (design 2026-11-10 E5): where its // compiler runtime stands (`compiler_runtime_dir`: clang reads it as its // resource directory, gcc through `-B`), and, when it supplies the C++ + // layer, its `ldflags` in place of the driver's own C++ library, which + // names the toolchain's. The run-time directory is as much a C link's as a + // C++ one's (`kLinkDriverFlagsC` states the same of the driver's own + // selection); the C++ library's words belong to the C++ link alone. auto base_link_words = [&](bool cxx) { std::string words; auto const& L = plan.toolchain.base.layers; @@ -2018,11 +1993,6 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { words += plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang ? " -resource-dir=" + ninjaEsc(L.compilerRuntimeDir) : " -B" + ninjaEsc(L.compilerRuntimeDir); - // The C++ library words are the C++ link's alone; the run-time - // directory above is as much a C decision -- a C link resolves - // __udivti3 from the same builtins the C++ one does, and a base - // package that names one names it for every link (the same sentence - // `kLinkDriverFlagsC` states for the driver's own selection). if (cxx && !L.cxx.empty() && !L.ldflags.empty()) { words += " -nostdlib++"; for (auto const& w : L.ldflags) words += " " + ninja_command_word(w); @@ -2102,6 +2072,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // drop it — the C line has to leave it out. f.ldC = std::format("{}{}{}{}{}{}", crossTarget, paired, link_intent_ld, user_ldflags, link_extra, baseStatic); + f.ldC += base_link_words(false); return f; } @@ -2353,7 +2324,6 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // `--push-state`/`--pop-state` pair), which is what sent a reader here. std::string atomic_ld = plan.targetSide.cAbi.prebuilt() - && plan.toolchain.base.layers.cAbi.empty() ? atomic_link_flag(plan.toolchain.linkRuntimeDirs, !full_static.empty()) : std::string{}; @@ -2733,10 +2703,6 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // A BASE PACKAGE'S RUN-TIMES (design 2026-11-10 E5). f.ld += base_link_words(true); - // The base's run-time directory reaches the C link too (2026.11.12.1): - // a hosted cross over a sysroot payload links its compiler run-time from - // the payload's own resource directory, and the C program's link is the - // one that had nowhere to find it. f.ldC += base_link_words(false); // A LINKER STATED BY ROLE (mcpp#755, `tools = { ld = ... }`), AFTER EVERY diff --git a/src/build/hermetic.cppm b/src/build/hermetic.cppm index e63bed78f..85037f3ea 100644 --- a/src/build/hermetic.cppm +++ b/src/build/hermetic.cppm @@ -245,9 +245,9 @@ std::expected verify_hermetic_link( // 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() || tt->c_library_by_platform())) { + tt && (!tt->loads_on_host() || tt->c_library_by_platform())) { mcpp::log::verbose("hermetic", std::format( - "target {} carries its own sysroot; its loader ({}) is a path on " + "target {} is not loaded by this machine; its loader ({}) is a path on " "the target and is not checked against the sandbox", tc.targetTriple, effectiveLoader)); } else if (!effectiveLoader.empty()) { diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 31f35bbb1..43b7e4917 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -803,6 +803,12 @@ export std::string profile_override_from_flags(std::string_view profileOption, // Command-level overrides (--target / --static). // Empty defaults preserve pre-existing behaviour exactly. +// THE ROOT OF A PACKAGE A STATEMENT NAMES (`xim:@`), for the +// readers outside prepare (a runner's packages, SPEC-016 §6): where the payload +// stands, installed first when `install` and it does not stand. +export std::expected +package_root(const std::string& ref, bool install); + export struct BuildOverrides { // Where the package being built LIVES (its mcpp.toml). Empty = walk up from // the process cwd, which is what every user-facing invocation does. Set by diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index 8a4cb7e5d..1ce36bc39 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -27,6 +27,7 @@ import mcpp.toolchain.base; import mcpp.toolchain.base_probe; import mcpp.toolchain.triple; import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_select; import mcpp.toolchain.target_schema; import mcpp.ui; import mcpp.invocation; @@ -285,18 +286,23 @@ bool provides_c_abi(const tb::Statement& s, std::string* impl) { return false; } -// The directory a package statement names, installed through the channel a -// row's toolchain pin uses (and `MCPP_NO_AUTO_INSTALL` / offline are the -// fetcher's to decide, as they are there). +// The directory a package statement names: where its payload stands, and, +// when `install`, installed through the channel a row's toolchain pin uses +// (`MCPP_NO_AUTO_INSTALL` and offline are the fetcher's to decide, as they +// are there). One resolution for every statement that names a package -- a +// base, the system's headers, a runner's packages. std::expected -package_dir(PrepareState& state, const std::string& ref) { - auto cfg = state.get_cfg(true); - if (!cfg) return std::unexpected(cfg.error()); - mcpp::fetcher::Fetcher f(**cfg); +package_dir_in(const mcpp::config::GlobalConfig& cfg, const std::string& ref, bool install) { + auto xl = mcpp::config::make_xlings_env(cfg); + if (!install) { + if (auto d = mcpp::provider::payload(*mcpp::provider::open(xl), mcpp::provider::parse_ref(ref))) + return *d; + return std::unexpected(std::format("the package {} is not installed", ref)); + } + mcpp::fetcher::Fetcher f(cfg); mcpp::fetcher::InstallProgressHandler progress; if (auto p = f.resolve_xpkg_path(ref, mcpp::fetcher::PayloadIntent::Use, &progress)) return p->root; - auto xl = mcpp::config::make_xlings_env(**cfg); if (auto d = mcpp::provider::payload(*mcpp::provider::open(xl), mcpp::provider::parse_ref(ref))) return *d; return std::unexpected(std::format("the package {} could not be installed", ref)); @@ -304,6 +310,36 @@ package_dir(PrepareState& state, const std::string& ref) { } // namespace +std::expected +package_dir(PrepareState& state, const std::string& ref, bool install) { + auto cfg = state.get_cfg(true); + if (!cfg) return std::unexpected(cfg.error()); + return package_dir_in(**cfg, ref, install); +} + +std::expected package_root(const std::string& ref, bool install) { + auto cfg = mcpp::config::load_or_init(/*quiet=*/true); + if (!cfg) return std::unexpected(cfg.error().message); + return package_dir_in(*cfg, ref, install); +} + +const mcpp::toolchain::target::Provider* chosen_provider(const PrepareState& state) { + if (state.targetChoice.provider.empty()) return nullptr; + const auto host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + const mcpp::toolchain::target::Provider* named = nullptr; + for (auto const* p : mcpp::toolchain::target::providers()) { + if (p->name != state.targetChoice.provider + || !mcpp::toolchain::target::serves(*p, state.targetChoice.identity)) + continue; + const auto dash = state.targetChoice.identity.find('-'); + if (mcpp::toolchain::target::available_on(*p, host, state.targetChoice.identity.substr(0, dash))) + return p; + if (!named) named = p; + } + return named; +} + // THE ONE RESOLUTION (§4.2 steps 5-7) for the rows whose base the engine // renders itself: a gnu-sysroot (Linux), and the identity and report of every // other stated base. The Apple, MSVC and multilib rows locate theirs where @@ -410,7 +446,7 @@ std::expected step2_resolve_base(PrepareState& state) { && (stmt->form == tb::Form::Package || stmt->form == tb::Form::Path)) { fs::path dir = stmt->form == tb::Form::Path ? fs::path(stmt->path) : fs::path{}; if (stmt->form == tb::Form::Package) { - auto p = package_dir(state, stmt->text); + auto p = package_dir(state, stmt->text, true); if (!p) return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, p.error())); dir = *p; @@ -460,15 +496,17 @@ std::expected step2_resolve_base(PrepareState& state) { return std::unexpected(missing_path_message(*tt, *stmt)); } } else { - auto p = package_dir(state, stmt->text); + auto p = package_dir(state, stmt->text, true); if (!p) return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, p.error())); dir = *p; } tc.base.layers = {}; - if (auto b = read_base_package(dir, tc.base.layers); !b) { + const auto basePackage = read_base_package(dir, tc.base.layers); + if (!basePackage) { refusal::record(refusal::Code::TargetBase); - return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, b.error())); + return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, + basePackage.error())); } const auto ecosystemLinux = tc.base.payload ? tc.base.payload->linuxInclude : fs::path{}; const bool native = tt->os == "linux" && tt->arch == mcpp::toolchain::platform::host_arch @@ -481,7 +519,11 @@ std::expected step2_resolve_base(PrepareState& state) { std::vector evidence; std::vector hostLibcDirs; std::string impl; - if (tb::is_payload_layout(dir)) { + // A BASE PACKAGE HAS ONE READING (SPEC-002 §2.5): its manifest states that + // the package root is the sysroot, so it is rendered as one whatever its + // directories are called. The layout is consulted only for a directory + // that states nothing about itself. + if (!*basePackage && tb::is_payload_layout(dir)) { // The ecosystem's own layout: a glibc package (`xim:glibc@2.39`). It // is linked as the default base is, from its own directories. tc.base.payload = tb::payload_layout_paths(dir, ecosystemLinux); @@ -539,20 +581,21 @@ std::expected step2_resolve_base(PrepareState& state) { tc.base.addedIncludes.push_back(ecosystemLinux); } - // A LOADER INSIDE A CROSS BASE IS THE SYSROOT'S, NOT THIS MACHINE'S - // (2026.11.12.1): a --sysroot link names it sysroot-relative - // (/lib/ld-linux-aarch64.so.1), and so does the artifact's PT_INTERP -- - // the form qemu -L , a runner declaration and the ld scripts - // beside it (the base's own libc.so GROUPs /lib/...) all resolve. An - // absolute path would bake this machine's install directory into every - // image it links; the native case keeps its absolute path, as the - // Locator form's distro path above already states. - if (!native && !tc.base.loader.empty()) { - std::error_code lec; - if (auto rel = fs::relative(tc.base.loader, dir, lec); !lec && !rel.empty() - && rel.generic_string().find("..") == std::string::npos) - tc.base.loader = fs::path("/") / rel; - } + // THE LOADER AN ARTIFACT NAMES IS A PATH ON THE MACHINE THAT RUNS IT + // (SPEC-016 §6). A native dynamic link names the base's own loader, where + // it stands; an artifact this machine does not load names the row's + // `loader`, and no directory of this machine enters it. + tc.base.interpreter.clear(); + if (!native && !staticLink) + if (auto const* r = mcpp::toolchain::target::find_row(tt->str())) + tc.base.interpreter = std::string(r->loader); + // THE PAYLOAD'S RUN-TIME DIRECTORIES ARE ITS HOST'S. A retargetable driver + // discovers them for the machine it runs on (`lib/`), and a + // base of another machine takes none of them: on the link they precede the + // base's own (measured: the x86_64 libatomic.a ahead of the sysroot's on an + // aarch64 link), and in the artifact they are a RUNPATH of this machine. + // Decided here, where the base is resolved, so no renderer asks again. + if (!native && mcpp::toolchain::is_clang(tc)) tc.linkRuntimeDirs.clear(); // A base package's own `provides` is its declaration, as a statement's is. if (tc.base.libc.empty() && !tc.base.layers.cAbi.empty()) tc.base.libc = tc.base.layers.cAbi; // ITS C++ RUN-TIME STANDS WHERE THE PAYLOAD'S WOULD (E5): the std module is diff --git a/src/build/prepare/platform.cpp b/src/build/prepare/platform.cpp index 2fa18e722..9c2da5519 100644 --- a/src/build/prepare/platform.cpp +++ b/src/build/prepare/platform.cpp @@ -215,22 +215,26 @@ void define_platform(const mp::Statement& s, std::string_view shortName, std::st r.defined = true; set.rows.push_back(r); } - auto provider = [&](std::string pname, const std::vector& serves, int priority, - std::string_view toolchain, const std::vector& hosts, - const std::vector& pairs, std::string_view sysroot, - std::string_view linkage) { + auto provider = [&](const mp::StatedProvider& sp) { target::Provider p; - p.name = set.keep(std::move(pname)); - p.serves = set.keep(serves); - p.priority = priority; - p.hosts = set.keep(hosts.empty() ? std::vector{"*"} : hosts); - p.pairs = set.keep(pairs); - p.toolchain = set.keep(std::string(toolchain)); - p.sysroot = set.keep(std::string(sysroot)); - p.linkage = set.keep(std::string(linkage)); + p.name = set.keep(sp.name); + p.serves = set.keep(sp.serves); + p.priority = sp.priority; + p.hosts = set.keep(sp.hosts.empty() ? std::vector{"*"} : sp.hosts); + p.pairs = set.keep(sp.pairs); + p.toolchain = set.keep(sp.toolchain); + p.sysroot = set.keep(sp.sysroot); + p.linkage = set.keep(sp.linkage); p.platform = set.keep(std::string(ref)); + p.tier = set.keep(sp.tier); + p.systemHeaders = set.keep(sp.systemHeaders); + p.runner = set.keep(sp.runner); + p.runnerHosts = set.keep(sp.runnerHosts); + p.runnerRequires = set.keep(sp.runnerRequires); set.providers.push_back(p); }; + for (auto const& sys : s.systems) + set.systems.push_back({ set.keep(sys.name), set.keep(sys.uname) }); // A platform the engine's own providers name (openkal-platform) is served by // those; its shorthands are not registered twice. const bool builtin = std::ranges::any_of(mcpp::toolchain::builtin::kProviders, [&](auto const& b) { @@ -238,8 +242,7 @@ void define_platform(const mp::Statement& s, std::string_view shortName, std::st return r && *r && (*r)->spec.shortName == shortName; }); for (auto const& p : mp::stated_providers(s, name)) - if (!p.shorthand || !builtin) - provider(p.name, p.serves, p.priority, p.toolchain, p.hosts, p.pairs, p.sysroot, p.linkage); + if (!p.shorthand || !builtin) provider(p); target::define(std::move(set)); } @@ -996,7 +999,26 @@ mp::Statement platform_statement(const PrepareState& state, std::vectorrunner.empty()) { + const auto host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + const bool here = p->runnerHosts.empty() + || std::ranges::any_of(p->runnerHosts, [&](std::string_view h) { + return mcpp::toolchain::target::host_matches(h, host, t.arch); + }); + if (here) { + s.runner.cmd.assign(p->runner.begin(), p->runner.end()); + s.runner.requires_.assign(p->runnerRequires.begin(), p->runnerRequires.end()); + s.runner.origin = mp::Origin{ mp::Tier::BuiltIn, {}, 0, + std::format("the provider '{}' of {}", p->name, state.targetChoice.identity) }; + s.runner.set = true; + } + } + if (P.active) mcpp::platform::merge(s, state.platformData); mp::Statement program = state.programPlatform; // The root's own program states them as well (applied into its manifest); // it is the last program to run, and it speaks for the project. diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index b69abbd96..66559eccd 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -42,6 +42,7 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; import mcpp.toolchain.target_select; import mcpp.toolchain.base; import mcpp.build.linkage_form; // #519 — which form each dependency takes @@ -1156,6 +1157,14 @@ base_statement(PrepareState& state, const mcpp::toolchain::triple::Triple& t); const std::string* base_statement_text(PrepareState& state, const mcpp::toolchain::triple::Triple& t); std::expected step2_resolve_base(PrepareState& state); +// The directory a package statement names (`xim:@`): where its +// payload stands, installed first when `install` and it does not stand. +std::expected +package_dir(PrepareState& state, const std::string& ref, bool install); +// The provider entry the selection took: the one of that name that serves the +// identity, the entry available on this host before one that is not. Null when +// no provider serves it. +const mcpp::toolchain::target::Provider* chosen_provider(const PrepareState& state); // The execution host's wrapper (`run_under`), decided once (§3.7). std::expected step2_resolve_run_under(PrepareState& state); diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 4479df961..ce240a045 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1445,9 +1445,25 @@ static std::expected step1_device_axis_and_layer_merge(Prepar state.overrides.target_triple.empty() ? mcpp::toolchain::triple::host_triple().str() : state.overrides.target_triple)); + // THE TWO MEMBERS NAME ONE MECHANISM (design 2026-11-12 D6): WebAssembly's + // exception handling carries both the throw and the long jump, and the + // JavaScript one carries both. A build that states one model for + // exceptions and the other for long jumps is refused here, with both + // statements, rather than by the SDK's driver after the compiles. + // Unstated, the long-jump model is the SDK's own default, which + // follows the exception model. + const bool wasmLongjmp = state.m->buildConfig.abiLongjmpModel == "wasm"; + if (abiExceptionsRendered + && wasmLongjmp != (state.m->buildConfig.abiExceptionsModel == "wasm")) + return std::unexpected(std::format( + "[target..abi] longjmp = \"{}\" and exceptions = \"{}\" name two " + "unwinding mechanisms for one program: state the same model for both, or leave " + "`longjmp` unstated and it follows `exceptions`", + state.m->buildConfig.abiLongjmpModel, + state.m->buildConfig.abiExceptionsModel.empty() + ? std::string("js") : state.m->buildConfig.abiExceptionsModel)); const std::string flag = "-sSUPPORT_LONGJMP=" - + (state.m->buildConfig.abiLongjmpModel == "wasm" - ? std::string("wasm") : std::string("emscripten")); + + (wasmLongjmp ? std::string("wasm") : std::string("emscripten")); state.add_once(state.m->buildConfig.dialectCxxflags, flag); state.add_once(state.m->buildConfig.cflags, flag); state.add_once(state.m->buildConfig.ldflags, flag); diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 0d00e37c0..48c37ee63 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -74,43 +74,36 @@ import mcpp.bmi_cache; namespace mcpp::build { -// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.12.1, G3): a row whose -// system the graph supplies -- `x86_64-windows-musl` over openkal -- has no -// paired payload, and its llvm binary carries no Windows headers, so -// `winres.h` reaches neither of the include sources the toolchain states. The -// MinGW-w64 sysroot payload a platform row declares for this architecture's -// GNU-ABI identity has them, under one `/include` for every -// architecture (E9's layout). +// THE SYSTEM'S OWN HEADERS, FOR A ROW WHOSE TOOLCHAIN STATES NONE (SPEC-016 +// §3, `system-headers`). A row whose system the graph supplies has no paired +// payload, and its compiler carries no headers of the system, so a resource +// script's `winres.h` reaches neither of the include sources the toolchain +// states. The provider the selection took names the package that has them. // -// READ WHERE THE TWO SOURCES ABOVE FOUND NOTHING, AND STANDING PAYLOADS -// FIRST: a row that already states its headers -- the SDK's, a paired -// payload's -- never resolves this, so preparing an MSVC or GNU-ABI project -// does not fetch a payload its link will not read. Only a compile that would -// otherwise lack `winres.h` resolves the declaration, and it takes the -// payload where it already stands before it fetches one. Empty when no row -// declares one or the payload is not here: the resource compile then fails -// with the tool's own missing-header text, which is the truthful message. -static std::filesystem::path -mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& trip) { - if (trip.os != "windows") return {}; - const auto gnuTriple = std::format("{}-windows-gnu", trip.arch); - for (auto const* p : mcpp::toolchain::target::providers()) { - if (p->sysroot.empty() || p->sysroot == "toolchain") continue; - if (!mcpp::toolchain::target::serves(*p, gnuTriple)) continue; - auto cfg = state.get_cfg(true); - if (!cfg) return {}; - auto xl = mcpp::config::make_xlings_env(**cfg); - if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), - mcpp::provider::parse_ref(p->sysroot))) - return *dir / "include"; - mcpp::fetcher::Fetcher fetcher(**cfg); - mcpp::fetcher::InstallProgressHandler progress; - if (auto payload = fetcher.resolve_xpkg_path( - std::string(p->sysroot), mcpp::fetcher::PayloadIntent::Use, &progress)) - return payload->root / "include"; - return {}; // one declared payload per row; the first provider wins - } - return {}; +// ONE QUESTION, ASKED BY BOTH STEPS THAT READ THE ANSWER: is the script +// compiled by llvm-rc (which reads no INCLUDE of its own) for a row that +// states no header source. A row that states its headers -- the SDK's, a +// paired payload's -- resolves nothing, so preparing it installs no package +// its build does not read. Where the provider names a package and it cannot +// be installed, the build is refused with the package's name: a resource +// compile left to fail on a missing header would name neither. +static bool resource_headers_wanted(const mcpp::toolchain::Toolchain& tc, + const mcpp::build::resources::RcTool* tool, + std::string_view dialectId, bool msvcStyle) { + return tool && msvcStyle && tool->name().find("llvm-rc") != std::string::npos + && tc.base.msvc.sdkRoot.empty() + && mcpp::build::resources::system_include_dirs(tc, dialectId).empty(); +} + +static std::expected system_headers_root(PrepareState& state) { + auto const* p = chosen_provider(state); + if (!p || p->systemHeaders.empty()) return std::filesystem::path{}; + auto dir = package_dir(state, std::string(p->systemHeaders), true); + if (!dir) + return std::unexpected(std::format( + "the provider '{}' of {} takes the system's headers from {}, and {}", + p->name, state.targetChoice.identity, p->systemHeaders, dir.error())); + return *dir / "include"; } // The resource compiler of the plan's resource units, and each unit's flags. @@ -141,17 +134,13 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; - // The third source is resolved only where it will be read, and only for - // a row whose Windows headers are not the machine's own (G3's subject): - // a located MSVC SDK is the row's stated header source -- an INCLUDE the - // shell did not export is not that SDK's absence, and an MSVC row never - // fetches another row's payload. Below the toolchain's own include - // sources, never beside them. The same sentence gates both call sites. - std::filesystem::path mingwHeaders; - if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos - && state.tc->base.msvc.sdkRoot.empty() - && rsrc::system_include_dirs(*state.tc, dialectId).empty()) - mingwHeaders = mingw_headers_root(state, trip); + // Below the toolchain's own include sources, never beside them. + std::filesystem::path systemHeaders; + if (resource_headers_wanted(*state.tc, &*tool, dialectId, msvcStyle)) { + auto headers = system_headers_root(state); + if (!headers) return std::unexpected(headers.error()); + systemHeaders = *headers; + } // UTF-8 input, always. `[package]` metadata is user text and // routinely non-ASCII; without this llvm-rc refuses the script @@ -169,7 +158,7 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { const std::string ip = msvcStyle ? "/I" : "-I"; std::vector systemIncludes; if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos) - for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId, mingwHeaders)) + for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId, systemHeaders)) systemIncludes.push_back(ip + dir.string()); for (auto& ru : ctx.plan.resourceUnits) { if (auto target = rsrc::coff_target_flag(*tool, state.tc->targetTriple); @@ -301,14 +290,13 @@ std::expected step13_windows_resources(PrepareState& state, B // windres), found as the compile step below finds it (E3). const auto rcTool = mcpp::build::resources::find_rc_tool(*state.tc, dialectId); const bool msvcStyle = rcTool ? rcTool->style == "msvc" : dialectId == "msvc"; - // Lazy, as at the compiler step above: a row that states its own headers - // resolves nothing, and a scan that finds no script reads none. namespace rsrc = mcpp::build::resources; - std::filesystem::path mingwHeaders; - if (rcTool && msvcStyle && rcTool->name().find("llvm-rc") != std::string::npos - && state.tc->base.msvc.sdkRoot.empty() - && rsrc::system_include_dirs(*state.tc, dialectId).empty()) - mingwHeaders = mingw_headers_root(state, trip); + std::filesystem::path systemHeaders; + if (resource_headers_wanted(*state.tc, rcTool ? &*rcTool : nullptr, dialectId, msvcStyle)) { + auto headers = system_headers_root(state); + if (!headers) return std::unexpected(headers.error()); + systemHeaders = *headers; + } for (auto const& S : subjects) { const auto& M = *S.m; @@ -478,7 +466,7 @@ std::expected step13_windows_resources(PrepareState& state, B // quoted `"winres.h"` is the toolchain's header, not a file // a build action produces (E3). auto scanIncludes = rcIncludes; - for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId, mingwHeaders)) + for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId, systemHeaders)) scanIncludes.push_back(d); auto scan = rsrc::scan_rc(rcSrc, scanIncludes); if (scan.versionInfoNamedByString) { diff --git a/src/build/resources.cppm b/src/build/resources.cppm index 47bad2cf9..8aa61cc84 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -101,16 +101,14 @@ std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple); // gcc's own) `-w64-mingw32/include` (design 2026-11-10 E3). One list for the // compile's `/I` and for the scan of what the script reads. // -// `mingw_headers` is the third source (2026.11.12.1, G3): the include -// directory of the MinGW-w64 sysroot payload the platform's rows declare, -// which is where the Windows headers of a row whose system the graph supplies -// -- `x86_64-windows-musl`, no paired payload, an llvm binary that carries no -// headers of its own -- come from. Every PE row shares that one set. It is -// read ONLY when the two sources above found nothing, so a GNU-ABI row keeps -// its paired payload's headers and an MSVC row its SDK's. +// `system_headers` is the third source: the include directory of the package +// the selected provider names for the system's own headers (`system-headers`, +// SPEC-016 §3), for a row whose toolchain states none. It is read ONLY when +// the two sources above found nothing, so a GNU-ABI row keeps its paired +// payload's headers and an MSVC row its SDK's. std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, std::string_view dialectId, - const std::filesystem::path& mingw_headers = {}); + const std::filesystem::path& system_headers = {}); // Split a Windows environment list (PATH, INCLUDE, LIB) into its entries. // @@ -215,7 +213,7 @@ bool is_llvm_windres(const std::filesystem::path& p) { std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, std::string_view dialectId, - const std::filesystem::path& mingw_headers) { + const std::filesystem::path& system_headers) { std::vector out; for (auto const& ev : tc.envOverrides) { if (ev.key != "INCLUDE") continue; @@ -250,9 +248,9 @@ std::vector system_include_dirs(const mcpp::toolchain::To // under one `/include` (E9's layout). The caller resolved that // payload; this stays a probe beside the directories it already probed, // never a search of PATH. - if (out.empty() && !mingw_headers.empty() - && std::filesystem::is_directory(mingw_headers, ec)) - out.push_back(mingw_headers); + if (out.empty() && !system_headers.empty() + && std::filesystem::is_directory(system_headers, ec)) + out.push_back(system_headers); return out; } diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 646314580..82ee0722c 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -24,7 +24,7 @@ export module mcpp.build.runner_lookup; import std; import mcpplibs.os.platform; -import mcpplibs.mwrap.libc; + export namespace mcpp::build::runner_lookup { struct Lookup { @@ -152,7 +152,9 @@ inline std::string spawn_failed_message(std::string_view program, int e, // The hosted sibling of mcpp::freestanding::no_runner_message. It reports what // the kernel answered rather than asserting why: on a hosted triple mcpp does // not know whether the refusal is a foreign ISA or a file that is not an -// executable at all, and the example it prints is a user-mode emulator. +// executable at all. The declaration it prints is the key's shape; which +// program runs the artifact is the statement of a provider, a platform or the +// project (SPEC-016 §6), and no program is named here. inline std::string unrunnable_message(std::string_view triple, const std::filesystem::path& artifact, int e) { return std::format( @@ -160,7 +162,7 @@ inline std::string unrunnable_message(std::string_view triple, " The artifact was built for '{}'. Declare how to run it here:\n" "\n" " [target.{}]\n" - " runner = [\"qemu-aarch64-static\"]\n" + " runner = [\"\", \"{{}}\"]\n" "\n" " The artifact path is appended, or substituted for `{{}}` if the " "template contains it.\n" @@ -168,82 +170,4 @@ inline std::string unrunnable_message(std::string_view triple, artifact.string(), errno_text(e), e, triple, triple); } -// ─── Started, and dead at once ──────────────────────────────────────────── -// -// A SPAWN THAT SUCCEEDS CAN STILL DIE BEFORE IT DOES ANYTHING. The kernel -// maps a static ELF of its own architecture without reading the system the -// ELF names — FreeBSD's OSABI byte is not a question Linux asks — so the -// program runs, states its first kernel interface call in its own system's -// vocabulary, and the kernel answers with a signal: SIGSEGV or SIGILL for a -// syscall number that maps nowhere, SIGBUS for a stack it never grew, -// SIGABRT where the C library gave up first. `mcpp::run` reports the exit -// status a shell would (128 + the signal), and the status is the only fact -// that distinguishes this death from a crash in a working program — which is -// why the classification is asked about artifacts of another system only, -// never about a native one, where the same status is the program's own bug -// and advice about runners would be noise on top of it. -// The constants come from the mwrap of the C library's macros (the engine -// includes no headers); SIGBUS is not among them, and its typical cause here -// -- a stack the kernel never grew -- arrives as SIGSEGV on the systems that -// report it, so the classification keeps the three the wrap carries. -namespace sigc = mcpplibs::mwrap::libc::macros; -inline bool death_by_signal(int rc) { - switch (rc) { - case 128 + sigc::SIGSEGV: - case 128 + sigc::SIGILL: - case 128 + sigc::SIGABRT: - return true; - default: - return false; - } -} - -// The signal's name, for a message a reader can grep; empty for a status the -// classification did not take. -inline std::string_view signal_of_death(int rc) { - switch (rc) { - case 128 + sigc::SIGSEGV: return "SIGSEGV"; - case 128 + sigc::SIGILL: return "SIGILL"; - case 128 + sigc::SIGABRT: return "SIGABRT"; - default: return {}; - } -} - -// What an artifact of another system's kernel interface says when it dies at -// once: the [target.] runner declaration, the same template -// `unrunnable_message` prints, under a first line that states the death and -// the fact of the build rather than guessing at a diagnosis. -inline std::string signal_death_message(std::string_view triple, - const std::filesystem::path& artifact, int rc) { - return std::format( - "'{}' was started and died at once ({}, status {}): it was built for " - "'{}', another system's kernel interface, and this machine cannot " - "execute it directly.\n" - " Declare how to run it here:\n" - "\n" - " [target.{}]\n" - " runner = [\"qemu-aarch64-static\"]\n" - "\n" - " The artifact path is appended, or substituted for `{{}}` if the " - "template contains it.\n" - " A host that can execute it directly may pass --no-runner.", - artifact.string(), signal_of_death(rc), rc, triple, triple); -} - -// The same declaration, appended to a spawn refusal whose bare text names -// only the errno (an interpreter of another system the kernel could not -// find, ENOENT) — the reader's remedy is the same either way. -inline std::string runner_advice_text(std::string_view triple) { - return std::format( - "\n The artifact was built for '{}'. Declare how to run it here:\n" - "\n" - " [target.{}]\n" - " runner = [\"qemu-aarch64-static\"]\n" - "\n" - " The artifact path is appended, or substituted for `{{}}` if the " - "template contains it.\n" - " A host that can execute it directly may pass --no-runner.", - triple, triple); -} - } // namespace mcpp::build::runner_lookup diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index 475cf0ef9..142d94c82 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -679,42 +679,21 @@ ValidationReport validate_changed_artifacts( // `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". + // ONE QUESTION FOR EVERY SUCH ROW (SPEC-016 §6): does this machine load the + // artifact. An SDK's row, a row of another system and a row of another + // architecture all answer no, and the row's data answers it -- Android's + // `execution`, a defined row's, the architecture segment. An artifact for + // another system says so, since its closure is the one thing not checked. if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - tt && tt->has_own_sysroot()) { + tt && !tt->loads_on_host()) { + if (!before.empty() && tt->c_library_by_platform() && !tt->system_is_host()) + mcpp::ui::info("note", std::format( + "the artifacts are for {}, not this machine: their runtime closure is not " + "checked here", tt->execution_system())); record_without_rules(plan, before); return report; } - // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot form, - // 2026.11.12.1): the artifact's loader and libc are the payload's, and - // the process these rules describe -- one process on this machine with - // this machine's binding -- is not the one that would load it. The row's - // declaration is the statement, exactly as an SDK's is above; the - // closure belongs to the payload that served the link, and a runner - // declaration (or qemu) is how such an artifact runs. - if (!plan.toolchain.base.layers.cAbi.empty()) { - record_without_rules(plan, before); - return report; - } - // THE ARTIFACT'S OWN SYSTEM DECIDES (mcpp#911 G5): a row a platform defines - // states where its artifacts run, and an artifact for another system is - // judged by that system's loader, not this machine's. Its closure is not - // checked here, and the build says so rather than calling it a defect. - // A row whose C library a platform describes, compared with this machine's - // system rather than with a name (E1; the comparison read "linux", so a - // row run on its own macOS host was called foreign there). Built-in rows - // keep the checks their own providers answer for. - if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); tt) - if (auto const sys = tt->execution_system(); - tt->c_library_by_platform() && sys != mcpp::toolchain::triple::host_triple().os) { - if (!before.empty()) - mcpp::ui::info("note", std::format( - "the artifacts are for {}, not this machine: their runtime closure is not " - "checked here", sys)); - record_without_rules(plan, before); - return report; - } - auto doc = read_cache(plan.outputDir); bool changedCache = false; if (doc.value("schema", 0) != 1 @@ -990,8 +969,7 @@ check_symbol_provision(const mcpp::build::BuildPlan& plan, // machine's search directories do not hold (E1, mcpp#911 N4: a FreeBSD // program was compared with the host's glibc). if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - tt && tt->c_library_by_platform() - && tt->execution_system() != mcpp::toolchain::triple::host_triple().os) + tt && !tt->loads_on_host()) return findings; // The flags this build hands the linker, as ONE vector, because the diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 2798662ce..35299015c 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -563,21 +563,13 @@ std::expected, Error> single_file(const Plan& // on -- an image's single file, a self-contained tree's runner -- when this mcpp // builds no program for it: a row its table states as planned (`x86_64-macos`) // or does not state (`aarch64-windows-gnu`). Empty when it builds one. -std::string unbuilt_row(std::string_view system, std::string_view arch); +std::string unbuilt_row(std::string_view system, std::string_view arch, + const mcpp::platform::Resolved* P = nullptr); // The systems `--systems` names for an architecture (design 2026-10-29 // §4.1): an ``, an `-` of that architecture, or every system // for `all`; this machine's system when it names none. -// -// `all` is the DATA's answer (2026.11.12.1, G5), not a list spelled here: -// this machine's system, every system a platform defines a row of for the -// architecture (mcpp#911 G10), and every system the platform's runner states -// a host of for it (`runner.hosts`) -- which is where FreeBSD enters, a -// runner the platform builds for `freebsd-`. A pack without a platform -// answers with the machine's own system alone; `--systems` naming an `` -// keeps its exact meaning. -std::vector systems_for(const std::vector& stated, std::string_view arch, - const mcpp::platform::Resolved* P = nullptr); +std::vector systems_for(const std::vector& stated, std::string_view arch); // The single files of an image (design 2026-10-29 §4.5): one per leg and // system of the leg's architecture, the legs in the plan's order. `skipped` @@ -2347,11 +2339,7 @@ std::string dependency_entry(const mcpp::manifest::platform::PackageRef& r) { } // The system names a package speaks of, and this machine's. -constexpr std::string_view host_system() { - if constexpr (mcpplibs::os::is_windows) return "windows"; - else if constexpr (mcpplibs::os::is_macos) return "macos"; - else return "linux"; -} +constexpr std::string_view host_system() { return mcpp::toolchain::platform::name; } // The triple a program of `system` on `arch` is built for, and the mode it // is packed in: a Linux program is static (musl), the one form that runs on @@ -2359,7 +2347,9 @@ constexpr std::string_view host_system() { // macOS or Windows takes this machine's own row; another system's is its // conventional row. `single`: the program is the whole package (a single // file), so on Windows it takes its C++ run-time from the system. -struct ProgramRow { std::string triple; std::string mode; std::string platform; }; +// `served`: false for a system whose row nothing states -- neither the engine's +// conventions nor the platform -- which no program is built for. +struct ProgramRow { std::string triple; std::string mode; std::string platform; bool served = true; }; // The platform a generated program of `triple` is built over, as `--platform` // names it: the target's platform where its hosted arrangement serves the @@ -2389,9 +2379,19 @@ std::string platform_ref_of(const mcpp::platform::Resolved& P) { ProgramRow program_row(std::string_view system, std::string_view arch, bool single, const mcpp::platform::Resolved* P = nullptr) { + // THE IDENTITY THE PLATFORM BUILDS ITS RUNNER AS (`runner.built-as`, + // SPEC-013 §11): the platform that serves a system says which of its rows a + // program for that system is, and the engine holds no such table. One + // static program over the platform's hosted arrangement, the same whichever + // machine packs it. + if (P && P->active) + if (auto it = P->fields.runner.builtAs.find(std::format("{}-{}", system, arch)); + it != P->fields.runner.builtAs.end()) + return { it->second, "static", hosted_platform_for(P, it->second) }; // A SYSTEM A PLATFORM DEFINES (mcpp#911 G10): its row of that architecture, // built over the platform that defines it, as the platform's carrier and - // packs of every other row are. + // packs of every other row are. A system nothing states a row for has no + // program. if (system != "linux" && system != "macos" && system != "windows") { if (P && P->active) for (auto const& d : P->fields.defines) { @@ -2399,17 +2399,7 @@ ProgramRow program_row(std::string_view system, std::string_view arch, bool sing if (t && t->os == system && t->arch == arch) return { t->str(), "static", platform_ref_of(*P) }; } - // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.12.1, - // G5), as FreeBSD's own request takes it (design 2026-11-10 E4): - // `*-freebsd-musl`, openkal-musl over openkal-freebsd, one static - // program with no C library of the system beneath -- built over the - // platform's hosted arrangement wherever the machine that packs it is, - // needing no FreeBSD and no SDK. - if (system == "freebsd") { - const auto triple = std::format("{}-freebsd-musl", arch); - return { triple, "static", hosted_platform_for(P, triple) }; - } - return { std::format("{}-{}", arch, system), "static", {} }; + return { std::format("{}-{}", arch, system), "static", {}, false }; } if (system == "linux") { const auto triple = std::format("{}-linux-musl", arch); @@ -2541,6 +2531,7 @@ std::string_view launcher_platform() { // state (`aarch64-windows-gnu`), or states as planned (`x86_64-macos`, its // toolchain not published), is not built. bool row_is_built(const ProgramRow& row) { + if (!row.served) return false; if (row.triple.empty()) return true; // this machine's own row auto t = mcpp::toolchain::triple::parse(row.triple); if (!t) return false; @@ -2775,7 +2766,7 @@ run_loaded(const Plan& plan) } if (plan.opts.mode == Mode::BundleAll) { for (auto const& leg : legs) - for (auto const& system : mcpp::pack::systems_for(plan.opts.systems, leg.arch, &plan.platform)) { + for (auto const& system : mcpp::pack::systems_for(plan.opts.systems, leg.arch)) { // A row this mcpp builds no program for (a planned // target, `x86_64-macos`) carries no runner: said, and // the package's metadata lists the systems it carries. @@ -3221,8 +3212,7 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) // with it; every other name is the target system's. const bool foreignSystem = [&] { auto t = mcpp::toolchain::triple::parse(plan.triple); - return t && t->c_library_by_platform() - && t->execution_system() != mcpp::toolchain::triple::host_triple().os; + return t && !t->loads_on_host(); }(); std::vector targetSystemNames; auto foreign_closure = [&]() -> std::expected { @@ -3528,8 +3518,7 @@ std::expected finalize_staged(const Plan& plan) { return detail::finalize_tree(plan); } -std::vector systems_for(const std::vector& stated, std::string_view arch, - const mcpp::platform::Resolved* P) { +std::vector systems_for(const std::vector& stated, std::string_view arch) { if (stated.empty()) return { std::string(detail::host_system()) }; std::vector out; auto add = [&](std::string os) { @@ -3537,21 +3526,15 @@ std::vector systems_for(const std::vector& stated, std }; for (auto const& e : stated) { if (e == "all") { - // This machine's system, every system the platforms of this - // process define a row of for the architecture (mcpp#911 G10), - // and every system the carrier's runner states a host of for it - // (2026.11.12.1, G5) -- the rows the platform itself says its - // runner exists for, FreeBSD's among them. - add(std::string(detail::host_system())); + // Every system the table states (SPEC-016 §2.3), built-in or a + // platform's, and every system the platforms of this process + // define a row of for the architecture (mcpp#911 G10). A pair no + // program is built for is written as uncovered (SPEC-014 §4.5), + // not left out. + for (auto const* s : mcpp::toolchain::target::systems()) add(std::string(s->name)); for (auto const* r : mcpp::toolchain::target::rows()) if (r->defined && r->arch == arch && !r->os.empty() && r->os != "none") add(std::string(r->os)); - if (P && P->active) - for (auto const& h : P->fields.runner.hosts) { - const auto dash = h.find('-'); - if (dash == std::string_view::npos) continue; - if (h.substr(dash + 1) == arch) add(std::string(h.substr(0, dash))); - } continue; } const auto dash = e.find('-'); @@ -3567,10 +3550,10 @@ std::vector single_file_rows(const Plan& plan) { const bool all = std::ranges::find(plan.opts.systems, "all") != plan.opts.systems.end(); std::vector rows; for (auto const& arch : archs) - for (auto const& system : systems_for(plan.opts.systems, arch, &plan.platform)) { + for (auto const& system : systems_for(plan.opts.systems, arch)) { SingleFileRow r{ system, arch, {} }; if (all) - if (auto row = unbuilt_row(system, arch); !row.empty()) + if (auto row = unbuilt_row(system, arch, &plan.platform); !row.empty()) r.skipped = std::format("no carrier row: this mcpp builds no program for {}", row); if (all && r.skipped.empty() && plan.platform.active) if (auto why = detail::unbuilt_carrier(plan.platform, system, arch, true)) @@ -3588,8 +3571,9 @@ bool output_names_directory(const std::filesystem::path& output) { return std::filesystem::is_directory(output, ec); } -std::string unbuilt_row(std::string_view system, std::string_view arch) { - const auto row = detail::program_row(system, arch, true); +std::string unbuilt_row(std::string_view system, std::string_view arch, + const mcpp::platform::Resolved* P) { + const auto row = detail::program_row(system, arch, true, P); return detail::row_is_built(row) ? std::string() : row.triple; } @@ -3801,7 +3785,7 @@ std::expected, Error> single_file(const Plan& const std::string system = !t ? std::string(detail::host_system()) : t->os == "windows" ? std::string("windows") : t->os == "macos" ? std::string("macos") : std::string("linux"); - const auto row = detail::program_row(system, arch, true); + const auto row = detail::program_row(system, arch, true, &plan.platform); if (!write(dir / "payload.zst", *payload) || !write(dir / "mcpp.toml", d::launcher_manifest(stem, plan.packageVersion) + detail::static_row_pin(row)) || !write(dir / "src" / "main.cpp", d::launcher_main()) diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 0a4631128..48f27a308 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -964,8 +964,8 @@ std::optional check_format(PackRun& run, GroupJob& g) { std::vector unbuilt, built; const bool all = std::ranges::find(run.opts.systems, "all") != run.opts.systems.end(); for (auto const& arch : archs) - for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch, &ctx.plan.platform)) { - auto row = mcpp::pack::unbuilt_row(system, arch); + for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch)) { + auto row = mcpp::pack::unbuilt_row(system, arch, &ctx.plan.platform); if (row.empty()) built.push_back(std::format("{}-{}", system, arch)); else if (!all) unbuilt.push_back(std::format("{}-{} ({})", system, arch, row)); if (row.empty() || !all) ++files; @@ -1256,11 +1256,11 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { std::error_code xec; const bool windowsLauncher = std::filesystem::is_regular_file(plan->stagingRoot / "bin" / (stem + ".exe"), xec); for (auto const& arch : archs) - for (auto const& system : carries ? mcpp::pack::systems_for(opts.systems, arch, &plan->platform) + for (auto const& system : carries ? mcpp::pack::systems_for(opts.systems, arch) : std::vector{}) { const auto pair = std::format("{}-{}", system, arch); if (covered(pair)) continue; - const auto row = mcpp::pack::unbuilt_row(system, arch); + const auto row = mcpp::pack::unbuilt_row(system, arch, &plan->platform); out.notCovered.push_back(std::format("{} ({})", pair, !row.empty() ? std::format("no runner row: this mcpp builds no program for {}", row) : system == "windows" && !windowsLauncher ? std::string("no Windows launcher") diff --git a/src/pack/polyglot.cppm b/src/pack/polyglot.cppm index 27967453e..e10f802ec 100644 --- a/src/pack/polyglot.cppm +++ b/src/pack/polyglot.cppm @@ -26,11 +26,14 @@ export module mcpp.pack.polyglot; import std; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; export namespace mcpp::pack::polyglot { -// One system's program: `system` is `linux`, `freebsd`, `macos` or `windows`, -// `arch` `x86_64` or `aarch64`. +// One system's program: `system` is a system of the table (SPEC-016 §2.3), +// `arch` `x86_64` or `aarch64`. A system that states a `uname` is selected by +// the script; the one that states none leads the file as its PE. struct Program { std::string system; std::string arch; @@ -64,11 +67,16 @@ void put32(std::string& b, std::size_t o, std::uint32_t v) { for (int i = 0; i < 4; ++i) b[o + static_cast(i)] = static_cast(v >> (8 * i) & 0xFF); } +// The word `uname -s` answers on a system: the system table's (SPEC-016 +// §2.3). Empty for a system that states none, or that no table knows. +std::string uname_of(std::string_view system) { + auto const* s = mcpp::toolchain::target::find_system(system); + return s ? std::string(s->uname) : std::string{}; +} + // `uname -s`-`uname -m` spellings of a system and architecture. std::string uname_pattern(std::string_view system, std::string_view arch) { - const std::string s = system == "macos" ? "Darwin" - : system == "freebsd" ? "FreeBSD" - : "Linux"; + const std::string s = uname_of(system); if (arch == "x86_64") return std::format("{0}-x86_64|{0}-amd64", s); return std::format("{0}-aarch64|{0}-arm64", s); } @@ -141,16 +149,20 @@ std::expected assemble(std::string_view name, const st const Program* windows = nullptr; std::vector posix; for (auto const& p : programs) { - if (p.system == "windows") { + if (!uname_of(p.system).empty()) { + posix.push_back(p); + } else if (mcpp::toolchain::target::find_system(p.system)) { + // A system that states no `uname` is not selected by the script: + // its program is the PE the file leads with. if (windows) return std::unexpected(std::format( "a polyglot file leads with one Windows program, and this pack has two ({}, {})", windows->arch, p.arch)); windows = &p; - } else if (p.system == "linux" || p.system == "freebsd" || p.system == "macos") { - posix.push_back(p); } else { - return std::unexpected(std::format("a polyglot file holds no program of {}", p.system)); + return std::unexpected(std::format( + "a polyglot file holds no program of {}: no system of that name is stated " + "([[platform.system]] name, uname)", p.system)); } } if (programs.empty()) return std::unexpected(std::string("a polyglot file holds at least one program")); diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm index 024641ff9..d40f9bb72 100644 --- a/src/platform/platform.cppm +++ b/src/platform/platform.cppm @@ -351,11 +351,15 @@ void merge(mp::Statement& b, const mp::Statement& o) { if (o.sysroot.set) { b.sysroot = o.sysroot; b.sysrootProvides = o.sysrootProvides; } // Field by field: an argv-only statement keeps the platform's `host` and // `exclusive`; a table, or a program's `run-host`, states what it states. + // `requires` are the packages of THAT command, so a statement that + // replaces the command replaces them. if (o.runner.set) { b.runner.cmd = o.runner.cmd; + b.runner.requires_ = o.runner.requires_; b.runner.origin = o.runner.origin; b.runner.set = true; } + if (!o.runner.builtAs.empty()) b.runner.builtAs = o.runner.builtAs; if (!o.runner.host.empty()) b.runner.host = o.runner.host; if (o.runner.exclusiveSet) { b.runner.exclusive = o.runner.exclusive; diff --git a/src/project.cppm b/src/project.cppm index 4a34f1392..e3d6c364a 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,12 +1095,12 @@ export std::string root_position_key( auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; std::string abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" + b.abiExceptionsModel : "0") : "-"; - // `abi longjmp` needs no field of its own here: it reaches the compile - // line through `dialect_cxxflags` (prepare adds the flag there), and this - // key is a CROSS-VERSION contract -- a project's plan stays current for an - // engine of another release reading it (1068's inner build, the subos's - // own engine, must find the graph current). Threads and exceptions have - // fields because they are booleans that render no flag of their own. + // `abi longjmp` joins the key WHERE IT IS DECLARED, and only there: this + // key is a cross-version contract -- a project's plan stays current for an + // engine of another release reading it -- so a project that does not state + // the member keeps the key it had, and one that states it has a key an + // engine that cannot read the member never wrote. + std::string abiLongjmp = b.abiLongjmpDeclared ? b.abiLongjmpModel : std::string{}; if (applies) for (auto const& cc : m.conditionalConfigs) { if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared @@ -1110,9 +1110,7 @@ export std::string root_position_key( dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" + cc.abiExceptionsModel : "0"; - // abi longjmp: rendered into dialect_cxxflags by prepare; adding - // a field here would orphan every plan a differently-released - // engine wrote (measured: 1068 on a self-host shard, twice). + if (cc.abiLongjmpDeclared) abiLongjmp = cc.abiLongjmpModel; } list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); @@ -1126,6 +1124,7 @@ export std::string root_position_key( field("ios_deployment_target", b.iosDeploymentTarget); field("abi_threads", abiThreads); field("abi_exceptions", abiExceptions); + if (!abiLongjmp.empty()) field("abi_longjmp", abiLongjmp); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index a1b0de2ae..df76d9b50 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -31,6 +31,7 @@ import mcpp.toolchain.msvc; import mcpp.toolchain.triple; import mcpp.toolchain.target_schema; import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_select; export namespace mcpp::toolchain { @@ -1298,173 +1299,25 @@ bool is_system_toolchain(const ToolchainSpec& spec) { 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. - // - // "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 - // `mcpplibs::os::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 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 (mcpplibs::os::is_linux) { - // "SELF-CONTAINED" IS ABOUT THE PAYLOAD'S CONTENTS, NOT ABOUT - // WHICH HOSTS IT IS PUBLISHED FOR — and this line read it as both. - // - // A musl payload really does carry its own sysroot, so no host-side - // libc is needed. It still has to EXIST for the host running it, - // and the cross packages are published per host arch: - // - // x86_64-linux-musl-gcc archs = { "x86_64" } - // aarch64-linux-musl-gcc archs = { "x86_64", "aarch64" } - // - // Measured on ubuntu-24.04-arm: `--target x86_64-linux-musl` was - // admitted, resolved a package with no aarch64 asset, and failed at - // install — `mismatch / build-failed` in the target matrix, twice. - // - // The native row stays reachable on every arch: `musl-gcc` - // publishes both, which is why `aarch64-linux-musl` is `ok` there. - const bool crossArch = target.arch != mcpp::toolchain::platform::host_arch; - if (target.is_musl()) - return !crossArch || mcpp::toolchain::platform::host_arch == "x86_64"; - // A CROSS WHOSE ROW NAMES A SYSROOT PAYLOAD IS SERVED WHEREVER - // THAT PAYLOAD IS PUBLISHED (2026.11.12.1, G1). llvm alone carries - // no glibc of another architecture, which is what `return - // !crossArch` answered: no payload here produces it. The row's - // own declaration is the better answer -- a provider that serves - // the identity with a sysroot payload, available on this host, - // supplies the C library whatever the architectures are. The - // answer stays data: no host is named in this function. - if (crossArch) { - const auto host = std::format("linux-{}", - mcpp::toolchain::platform::host_arch); - for (auto const* p : mcpp::toolchain::target::providers()) - if (mcpp::toolchain::target::serves(*p, target.str()) - && !p->sysroot.empty() && p->sysroot != "toolchain" - && mcpp::toolchain::target::available_on(*p, host, target.arch)) - return true; - return false; - } + // THE DATA ANSWERS (SPEC-016 §3): a provider that brings its own payloads + // serves the identity, and its `hosts` name this machine. The question is + // about payloads, so a provider a platform package implements is not + // counted -- whether the graph supplies the system is asked where the + // graph is known. Which hosts a payload exists for is a fact of the index, + // written once beside the provider; this function names no system and no + // architecture. + const auto host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + const auto identity = target.str(); + for (auto const* p : mcpp::toolchain::target::providers()) + if (p->platform.empty() && mcpp::toolchain::target::serves(*p, identity) + && mcpp::toolchain::target::available_on(*p, host, target.arch)) return true; - } - // Non-Linux host: only the self-contained musl payloads can work at - // all (nothing else would find a C library). Today exactly one such - // payload exists — the windows-hosted canadian cross, built per host - // arch — so an arch-crossing combination stays unserviceable until one - // is published. macOS has no Linux-targeting payload at all. - return mcpplibs::os::is_windows - && target.is_musl() - && target.arch == mcpp::toolchain::platform::host_arch; - } - // THE mingw CROSS IS PUBLISHED FOR ONE HOST ARCH. `mingw-cross-gcc` - // declares `archs = { "x86_64" }`, so a Linux host that is not x86_64 - // cannot obtain it — measured on ubuntu-24.04-arm, where the row was - // listed and its refusal carried no reason at all (`unsupported / other`). - if (target.is_windows_gnu()) { - if constexpr (mcpplibs::os::is_windows) return true; - return mcpplibs::os::is_linux - && mcpp::toolchain::platform::host_arch == "x86_64"; - } - // PE + musl HAS NO PAYLOAD ON ANY HOST, INCLUDING WINDOWS. - // - // `triple::pin_is_capability()` already says so — no gcc emits a PE with a - // musl C library, and LLVM cannot spell the triple — and chapter 16 states - // it in those words: "A payload for it does not exist on any host; its - // system can only come from a dependency graph." - // - // This line disagreed, on exactly one host. Measured on windows-2022, - // payload system: - // - // c-abi musl (payload) - // c++-abi msvc-stl (payload) - // lld-link: error: undefined symbol: __main - // lld-link: error: undefined symbol: __mingw_vfprintf - // - // — musl's C library, MSVC's STL and MinGW's CRT symbols in one link. On - // Linux the same cell already answered `host-cannot-serve`, which is the - // right answer everywhere. - // - // The graph path is untouched: this refusal is held and released only - // when nothing supplies the target's system, and `graph × windows-musl` is - // `ok` on both hosts. - if (target.is_pe() && target.is_musl()) return false; - if (target.os == "windows") return bool(mcpplibs::os::is_windows); - // APPLE, AND ONE ARM FOR BOTH OF ITS PLATFORMS. - // - // This read `os == "macos"`, which was the same question while macOS was - // the only Apple target mcpp had. An iOS target then fell through to the - // `return false` at the end of this function -- on EVERY host, including - // the one that serves it. - // - // The consequence was not a refused build: the SDK gate in prepare answers - // before this one. It was `toolchain list`, which drops a row this says no - // host can serve -- so the three iOS rows were absent from the list on - // macOS, and present on linux-x86_64 for the wrong reason (see - // `graphCouldServe` in mcpp.toolchain.lifecycle). Measured as a - // target-matrix scan that reached three fewer cells on linux-aarch64 than - // the expected table declares. - // - // `is_apple()` is the predicate the table already carries, and until now - // it had no reader at all. - if (target.is_apple()) return bool(mcpplibs::os::is_macos); - - // Bare metal: every host can serve it, and that is a property of the - // toolchain rather than a claim about payload coverage. clang and lld are - // cross-compilers by construction — one binary emits every target it was - // built with — so a freestanding target needs NO per-host cross payload, - // unlike every hosted case above, which needs a C library that only exists - // for some (host, target) pairs. - // - // Serviceable is not the same as complete: a target with no C library - // still links only `-nostdlib` programs. That gap belongs to the ecosystem - // (a libc wrapper package), and saying `false` here would hide it behind - // "this host cannot build it", which is the wrong diagnosis. - if (target.is_freestanding()) return true; - // An ok image the same way: clang and lld emit it on every host, and what - // the image holds above `kal_*` is the graph's. - if (target.loader_supplies_os()) return true; - - return false; + // A triple no row defines is a freestanding one the driver emits by + // itself (clang and lld are cross-compilers by construction), or an ok + // image, whose every part above `kal_*` is the graph's. + if (!mcpp::toolchain::target::find_row(identity)) return target.is_freestanding(); + return target.loader_supplies_os(); } std::vector available_toolchain_indexes() { diff --git a/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh b/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh new file mode 100755 index 000000000..9e6bc0755 --- /dev/null +++ b/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# requires: llvm +# 1077 -- a program of another system is built, not run: `mcpp run` starts +# nothing it cannot observe (SPEC-016 §6; design 2026-11-12 C9, D25). +# +# A row's artifacts run on its execution system. Where that is not this +# machine's system and nothing states a runner, the artifact is not started: +# this machine's kernel maps a static program of its own architecture without +# reading the system it names, and gives that system's call numbers its own +# meaning. The answer is given before anything is spawned, by one predicate for +# built-in rows and for rows a platform defines. +# +# Criteria: on a host whose system is not FreeBSD, `mcpp run --target +# -freebsd-musl` builds the program, exits 2 without running it, states +# "built, not run", names both systems and prints the `[target.]` +# runner declaration for the row; `mcpp test` of the same target reports the +# tests as built, not run. On a FreeBSD host the row is native and the check +# does not apply. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -s)" in FreeBSD) echo "SKIP: FreeBSD is this machine's system"; exit 77 ;; esac +case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; *) ARCH=aarch64 ;; esac +T="$ARCH-freebsd-musl" + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app +"$MCPP" build --target "$T" > b.log 2>&1 || fail "the static FreeBSD program did not build" b.log +exe=$(find "target/$T" -path '*/bin/app*' -type f | head -1) +[ -n "$exe" ] || fail "no program" b.log + +# A marker the program would write if it were started: the program of `mcpp +# new` prints, so its output in the log is the evidence of a launch. +set +e +"$MCPP" run --target "$T" > run.log 2>&1 +rc=$? +set -e +[ "$rc" -eq 2 ] || fail "run exited $rc, expected the refusal's 2" run.log +grep -q "built, not run" run.log || fail "the run does not say built, not run" run.log +grep -q "artifacts run on freebsd" run.log || fail "the artifact's system is not named" run.log +grep -q "\[target.$T\] runner = " run.log || fail "the runner declaration does not name the row" run.log +grep -q "Running" run.log && fail "the artifact was started" run.log + +# `--no-runner` states that the triple is native here, and is believed: the +# launch is then the caller's, and the refusal above does not apply. +set +e +"$MCPP" run --target "$T" --no-runner > direct.log 2>&1 +set -e +grep -q "built, not run" direct.log && fail "--no-runner was not believed" direct.log + +echo "PASS: 1077 a program of another system is built, not run" diff --git a/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh b/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh deleted file mode 100755 index f4c4ecd6e..000000000 --- a/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# requires: llvm -# 1077 -- a program of another system's kernel interface that dies at once -# names the runner that would run it (2026.11.11.1, G6/G7). -# -# `mcpp run --target x86_64-freebsd-musl` on a Linux host spawns a static -# FreeBSD ELF of the host's own architecture: the kernel maps it without -# asking which system it names, the program states its first kernel call in -# FreeBSD's vocabulary, and the death is by signal -- 139, with nothing to -# read. `mcpp run` now runs such an artifact as a child, so the death is -# observed and named with the [target.] runner declaration; the -# status itself is returned unchanged, exactly what a direct launch would -# have reported. -# -# Criteria: the exit status stays 139; the error text states the death, the -# other system's kernel interface, and the runner declaration for the row. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -"$MCPP" new app > /dev/null 2>&1 -cd app -"$MCPP" build --target x86_64-freebsd-musl > b.log 2>&1 || fail "the static FreeBSD program did not build" b.log -exe=$(find target/x86_64-freebsd-musl -path '*/bin/app' -type f | head -1) -[ -n "$exe" ] || fail "no program" b.log - -set +e -"$MCPP" run --target x86_64-freebsd-musl > run.log 2>&1 -rc=$? -set -e -[ "$rc" -eq 139 ] || fail "run exited $rc, expected the program's own death status 139" run.log -grep -q "was started and died at once" run.log \ - || fail "the run does not state the death at once" run.log -grep -q "another system's kernel interface" run.log \ - || fail "the run does not state the kernel interface fact" run.log -grep -q "\[target.x86_64-freebsd-musl\]" run.log \ - || fail "the runner declaration names the wrong target" run.log -grep -q "runner = \[" run.log \ - || fail "no runner declaration is printed" run.log -grep -q -- "--no-runner" run.log \ - || fail "the direct-execution escape hatch is not named" run.log -echo "PASS: 1077 a program of another system that dies at once names the runner" diff --git a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh index f2576261b..0571bfee9 100644 --- a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh +++ b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh @@ -53,28 +53,12 @@ echo "ok: --systems - makes that pair's file" # C if [ "$OS" = linux ]; then - # `--systems all` is the data's answer (2026.11.11.1): this machine's - # system, the systems the platform defines a row of for the - # architecture, and the systems its runner states a host of -- FreeBSD - # included, and never a pair no row exists for, so nothing is "Not - # covered" and nothing needs to say so. "$MCPP" pack --target openkal --pkg-format single-file --systems all -o all/ > c.log 2>&1 || fail "C: --systems all" c.log - for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64 \ - freebsd-x86_64 freebsd-aarch64; do + for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64; do ls all/sf-0.1.0-$pair* > /dev/null 2>&1 || fail "C: no file for $pair" c.log done - [ ! -e all/sf-0.1.0-macos-x86_64 ] || fail "C: a pair with no row was packed" c.log - # Naming an keeps its exact meaning: for a single file per pair, - # a system one of whose architectures has no row is refused naming the - # pair and the spelling that would work. - if "$MCPP" pack --target openkal --pkg-format single-file --systems macos -o mac/ > m.log 2>&1; then - fail "C: --systems macos was accepted with a rowless pair" m.log - fi - grep -q "builds no program for macos-x86_64" m.log \ - || fail "C: the pair without a row is not said" m.log - grep -q "use: --systems macos-aarch64" m.log \ - || fail "C: the refusal does not name the working spelling" m.log - echo "ok: --systems all makes every file the data names; a named system with a rowless pair is refused naming it" + grep -q "Not covered macos-x86_64 (no carrier row" c.log || fail "C: the pair without a row is not said" c.log + echo "ok: --systems all makes every file this mcpp can and says the rest" fi # D diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot index a99e7d229..a575352da 100644 --- a/tests/fixtures/target-rows.snapshot +++ b/tests/fixtures/target-rows.snapshot @@ -37,7 +37,7 @@ row x86_64-linux-musl descriptor-keys linux sysroot row aarch64-linux-gnu - table tier=preview note= pin=llvm@23.1.3 sysroot=xim:aarch64-linux-gnu-sysroot@2.44.3 static=0 platform= + table tier=preview note= pin=llvm@23.1.3 sysroot= static=0 platform= str aarch64-linux-gnu llvm aarch64-unknown-linux-gnu llvm@15.0 aarch64-unknown-linux-gnu @@ -54,7 +54,7 @@ row aarch64-linux-gnu android-abi arm64-v8a platform-name linux descriptor-keys linux - sysroot xim:aarch64-linux-gnu-sysroot@2.44.3 + sysroot row aarch64-linux-musl table tier=verified note= pin=gcc@16.1.0 sysroot= static=1 platform= str aarch64-linux-musl @@ -861,9 +861,9 @@ request x86_64-linux on windows-x86_64 -> x86_64-linux-musl musl-gcc fallback=1 | x86_64-linux-gnu/glibc/100/x x86_64-linux-musl/musl-gcc/100 x86_64-linux-musl/openkal/50 request aarch64-linux parsed aarch64-linux-gnu envExplicit=0 - on linux-x86_64 -> aarch64-linux-gnu glibc fallback=0 | aarch64-linux-gnu/glibc/100 aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 - on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 - on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 + on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 aarch64-linux-gnu/glibc-sysroot/40 + on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 aarch64-linux-gnu/glibc-sysroot/40/x + on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 aarch64-linux-gnu/glibc-sysroot/40/x request riscv64-linux parsed riscv64-linux-gnu envExplicit=0 on linux-x86_64 -> riscv64-linux-musl musl-gcc fallback=0 | riscv64-linux-musl/musl-gcc/100/x riscv64-linux-musl/openkal/50/x diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index eb6815594..7d2e79cd7 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -1,7 +1,6 @@ import std; import mcpplibs.mtest; -import mcpplibs.os.platform; import mcpp.pack; using namespace mcpplibs::mtest; @@ -53,26 +52,16 @@ UTest dispatched_format_stages_without_it{"PackClosureUnavailableOutcome/Dispatc expect(eq(closure_unavailable_outcome(Format::Dispatched), ClosureUnavailableOutcome::StageWithoutClosure)); }}; -// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.12.1 answers -// with the data): `` for every architecture of the legs, `-` -// for one; unstated, this machine's. `all` is this machine's system, the -// systems the builtin rows define for the architecture, and -- with a -// resolved platform -- the systems its runner states a host of. Without a -// platform that is the machine's system and this table's linux rows alone. +// `--systems` (design 2026-10-29 §4.1): `` for every architecture of the +// legs, `-` for one, `all` for the three systems; unstated, this +// machine's. UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystemsForAnArchitecture", [] { using V = std::vector; expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); - // `all` answers with the data, and one of the facts is the machine asking: - // its system stands first, wherever the same suite runs. A Linux host and - // the table's linux rows coincide; a macOS host names macos beside them. - const char* here = "linux"; - if constexpr (mcpplibs::os::is_windows) here = "windows"; - else if constexpr (mcpplibs::os::is_macos) here = "macos"; - if (std::string_view(here) == "linux") - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); - else - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ here, "linux" }))); + // `all` is the system table's answer, in the table's order (SPEC-016 §2.3). + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), + (V{ "linux", "macos", "freebsd", "windows" }))); expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); }}; diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 07d2a8940..c03f48ca4 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -31,10 +31,8 @@ std::uint64_t number_after(std::string_view text, std::string_view key) { } // namespace UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_says{"PackPolyglot/WithoutWindowsTheFileIsAScriptAndEachProgramLiesWhereItsLineSays", [] { - const std::string linux64(1000, 'L'), mac(777, 'M'), fbsd(333, 'F'); - auto file = pg::assemble("app", { program("linux", "x86_64", linux64), - program("freebsd", "x86_64", fbsd), - program("macos", "aarch64", mac) }); + const std::string linux64(1000, 'L'), mac(777, 'M'); + auto file = pg::assemble("app", { program("linux", "x86_64", linux64), program("macos", "aarch64", mac) }); if (auto&& failed = expect(fatal(file.has_value())); !failed) failed << file.error(); expect(file->starts_with("#!/bin/sh\no=$0;")); expect(pg::is_polyglot(*file)); @@ -49,7 +47,6 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ expect(eq(script.find("MINGW*"), std::string_view::npos)) << "no Windows program, no MSYS line"; for (auto [key, bytes] : { std::pair{std::string_view("k=linux-x86_64 a="), std::string_view(linux64)}, - std::pair{std::string_view("k=freebsd-x86_64 a="), std::string_view(fbsd)}, std::pair{std::string_view("k=macos-aarch64 a="), std::string_view(mac)} }) { const auto line = script.find(key); expect(fatal(ne(line, std::string_view::npos))) << key; @@ -60,18 +57,27 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ } // `uname` spells an architecture two ways on some systems. expect(ne(script.find("Linux-x86_64|Linux-amd64)"), std::string_view::npos)); - expect(ne(script.find("FreeBSD-x86_64|FreeBSD-amd64)"), std::string_view::npos)); expect(ne(script.find("Darwin-aarch64|Darwin-arm64)"), std::string_view::npos)); }}; +// The word a system is selected by is the system table's `uname` (SPEC-016 +// §2.3): a system the table states joins the file with no word of the code. +UTest a_system_of_the_table_is_selected_by_its_uname{"PackPolyglot/ASystemOfTheTableIsSelectedByItsUname", [] { + auto file = pg::assemble("app", { program("linux", "x86_64", "LINUX"), + program("freebsd", "x86_64", "FREEBSD") }); + expect(fatal(file.has_value())); + expect(ne(file->find("FreeBSD-x86_64|FreeBSD-amd64)"), std::string::npos)); + expect(ne(file->find("Linux-x86_64|Linux-amd64)"), std::string::npos)); +}}; + UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [] { expect(!(pg::assemble("app", {}).has_value())); expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - // FreeBSD is a POSIX system of the file since 2026.11.12.1 (G5): its - // program rides behind the script head beside Linux's. What still has - // no place is a system the script names no `uname` pattern for. - expect(!(pg::assemble("app", { program("plan9", "x86_64", "x") }).has_value())); + // A system no table states has no `uname` word to be selected by. + auto unstated = pg::assemble("app", { program("plan9", "x86_64", "x") }); + expect(fatal(!unstated.has_value())); + expect(ne(unstated.error().find("[[platform.system]]"), std::string::npos)) << unstated.error(); auto notPe = pg::assemble("app", { program("windows", "x86_64", std::string(256, 'x')) }); expect(fatal(!notPe.has_value())); if (auto&& failed = expect(ne(notPe.error().find("no PE"), std::string::npos)); !failed) failed << notPe.error(); diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 5e86587a7..badcbc3e5 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -1,5 +1,4 @@ #include -#include #include import std; @@ -112,7 +111,10 @@ UTest unrunnable_message_names_kernel_answer_triple_and_key{"RunnerLookup/Unrunn expect(ne(msg.find("/x/bin/app"), std::string::npos)) << msg; expect(ne(msg.find("built for 'aarch64-linux-musl'"), std::string::npos)) << msg; expect(ne(msg.find("[target.aarch64-linux-musl]"), std::string::npos)) << msg; - expect(ne(msg.find("runner = [\"qemu-aarch64-static\"]"), std::string::npos)) << msg; + // The key's shape, and no program: which one runs the artifact is a + // statement of the data (SPEC-016 §6). + expect(ne(msg.find("runner = [\"\", \"{}\"]"), std::string::npos)) << msg; + expect(eq(msg.find("qemu"), std::string::npos)) << msg; expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; }}; @@ -149,48 +151,6 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; -// The classification of a death by signal (2026.11.12.1, G6): the three -// signals the mwrap of the C library's macros carries that an artifact of -// another system's kernel interface dies by on its first kernel call, and -// nothing else — a normal exit, an exit by a signal the classification -// does not take (SIGTERM, 143) and the launcher's own 127/126 band are all -// someone else's fact. -UTest death_by_signal_takes_the_wrapped_three{"RunnerLookup/DeathBySignalTakesTheWrappedThree", [] { - expect(death_by_signal(128 + SIGSEGV)); - expect(death_by_signal(128 + SIGILL)); - expect(death_by_signal(128 + SIGABRT)); - expect(!death_by_signal(0)); - expect(!death_by_signal(1)); - expect(!death_by_signal(128 + SIGTERM)); - expect(!death_by_signal(128 + SIGINT)); - expect(!death_by_signal(127)); - expect(!death_by_signal(-11)); - expect(eq(signal_of_death(128 + SIGSEGV), std::string_view("SIGSEGV"))); - expect(eq(signal_of_death(128 + SIGILL), std::string_view("SIGILL"))); - expect(eq(signal_of_death(0), std::string_view{})); -}}; - -UTest signal_death_message_names_the_runner_declaration{"RunnerLookup/SignalDeathMessageNamesTheRunnerDeclaration", [] { - const std::filesystem::path art{"/x/target/x86_64-freebsd-musl/debug/bin/app"}; - auto msg = signal_death_message("x86_64-freebsd-musl", art, 128 + SIGSEGV); - expect(ne(msg.find("was started and died at once"), std::string::npos)) << msg; - expect(ne(msg.find("SIGSEGV"), std::string::npos)) << msg; - expect(ne(msg.find("status 139"), std::string::npos)) << msg; - expect(ne(msg.find("another system's kernel interface"), std::string::npos)) << msg; - expect(ne(msg.find("[target.x86_64-freebsd-musl]"), std::string::npos)) << msg; - expect(ne(msg.find("runner = ["), std::string::npos)) << msg; - expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; - // The status stays the caller's to return; the message only says it. - expect(eq(msg.find("status 139, but"), std::string::npos)) << msg; -}}; - -UTest runner_advice_text_declares_the_target{"RunnerLookup/RunnerAdviceTextDeclaresTheTarget", [] { - auto msg = runner_advice_text("aarch64-linux-gnu"); - expect(ne(msg.find("The artifact was built for 'aarch64-linux-gnu'"), std::string::npos)) << msg; - expect(ne(msg.find("[target.aarch64-linux-gnu]"), std::string::npos)) << msg; - expect(ne(msg.find("runner = ["), std::string::npos)) << msg; -}}; - #else UTest runner_lookup_windows_covered_by_integration{"RunnerLookup/WindowsCoveredByIntegration", [] { /* passes */; }}; diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 748cb3c85..74df15d30 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -637,12 +637,7 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie // the LLVM 23.1.3/glibc default, its deployed pack, GNU self-host, // all 147 suites and four real index members, from source. The tier // stays `preview` until the released engine is consumed on a clean - // ARM64 host (SPEC-009 §10.7). From 2026.11.12.1 the row names the - // sysroot payload its C library comes from (G1) -- llvm alone - // carries no glibc of another architecture -- and the linkage is - // glibc's own dynamic one: the compiler run-time is the payload's - // (libgcc's archives and compiler-rt's builtins), so a link takes - // nothing static from the host. + // ARM64 host (SPEC-009 §10.7). auto [name, tier] = std::pair{"aarch64-linux-gnu", "preview"}; auto t = parse(name); expect(fatal(t.has_value())); @@ -650,7 +645,7 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie expect(fatal(ne(info, nullptr))); expect(eq(info->tier, tier)); expect(eq(info->pin, "llvm@23.1.3")); - expect(eq(info->sysroot, "xim:aarch64-linux-gnu-sysroot@2.44.3")); + expect(info->sysroot.empty()); expect(!info->defaultStatic); expect(!t->pin_is_capability()); } From a459dc50b97901797ce8962489e90f5b29170653 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 16:34:54 +0900 Subject: [PATCH 17/30] wip: e2e 1076/1077/330, SPEC-016 v1.2, system-name ratchet, golden link line --- docs/specs/targets-and-providers.md | 62 +++++- ..._gnu_cross_comes_from_a_sysroot_payload.sh | 125 ++++++----- tests/e2e/330_runner_hosted_targets.sh | 4 +- tests/e2e/run_all.sh | 10 +- tests/unit/test_link_line_golden.cpp | 199 ++++++++++++++++++ 5 files changed, 334 insertions(+), 66 deletions(-) create mode 100644 tests/unit/test_link_line_golden.cpp diff --git a/docs/specs/targets-and-providers.md b/docs/specs/targets-and-providers.md index 015d7612a..e1953b4c6 100644 --- a/docs/specs/targets-and-providers.md +++ b/docs/specs/targets-and-providers.md @@ -4,11 +4,11 @@ |---|---| | 规范编号 | SPEC-016 | | 标题 | 目标与提供者:行、提供者、选择、平台定义的行、报告与锁 | -| 状态 | 评审中 v1.1 | -| 版本 | 1.1 | -| 最后修改 | 2026-11-10 | -| 对应实现 | mcpp >= 2026.11.9.1;v1.1 条款 mcpp >= 2026.11.10.1 | -| 相关设计文档 | `.agents/docs/2026-11-09-target-model-design.md`、`.agents/docs/2026-11-10-cross-platform-ecosystem-design.md` | +| 状态 | 评审中 v1.2 | +| 版本 | 1.2 | +| 最后修改 | 2026-11-12 | +| 对应实现 | mcpp >= 2026.11.9.1;v1.1 条款 mcpp >= 2026.11.10.1;v1.2 条款 mcpp >= 2026.11.12.1 | +| 相关设计文档 | `.agents/docs/2026-11-09-target-model-design.md`、`.agents/docs/2026-11-10-cross-platform-ecosystem-design.md`、`.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md` | | 相关 issue | mcpp#911、mcpp#913 | | 相关规范 | [SPEC-001](package-identity.md)(引用文法)、[SPEC-002](target-side.md)(层与能力)、[SPEC-013](platforms.md)(平台的语句)、[SPEC-014](packaging.md)(打包)、[SPEC-015](target-sets.md)(目标集合) | | 使用文档 | [21 —— 目标三元组](../zh/21-the-target-triple.md)、[35 —— 平台](../zh/35-platforms.md) | @@ -27,6 +27,7 @@ | 请求 | `--target`、`[build] target`、config 的缺省目标中写出的字符串 | | 身份 | 一个行的三元组;决定输出目录、缓存键、`cfg` 求值与锁中的记录 | | 行 | 身份的数据:段、档位、机器描述、对象格式、执行系统等(§2) | +| 系统 | 一个 os 段所指的操作系统;不属于任何单独一行的事实记在系统表(§2.3) | | 提供者 | 一个身份的一份完整供给:工具链、各层、链接、C 环境(§3) | | 选择 | 由请求得到身份与提供者的规则(§4) | @@ -45,6 +46,9 @@ - `march`、`mabi`、`code-model`、`flags`、`link-emulation` 是行的机器描述,引擎原样使用,**不可**被替换;机器不同的目标以 另一行表达(§5)。 - `clang-triple` 是交给 clang 的目标拼写,未写时由段推出;它只改变编译器所见的三元组,不改变身份(v1.1)。 +- `loader` 是该行的动态程序所写的程序解释器:运行它的机器上的路径(`/lib/ld-linux-aarch64.so.1`)。内置行与平台定义的行 + 同一读法;引擎不另持有按架构的加载器表(v1.2)。 +- `execution` 是产物运行的系统,未写时为 os 段。内核相同而系统不同的行**必须**写出它(Android 的行为 `android`)(v1.2)。 ### 2.2 内置行 @@ -58,6 +62,21 @@ - 32 位 hosted 行(`i686-linux-musl`、`i686-linux-gnu`、`i686-openkal`)为 planned(v1.1):请求按 planned 拒绝,而非按 未知目标;显式的 `[target.<身份>] toolchain` 可提前启用。 +### 2.3 系统表(v1.2) + +`[[platform.system]]` 陈述一个系统不属于任何单独一行的事实,内置数据与平台包同一 schema。 + +| 键 | 语义 | +|---|---| +| `name` | 该系统各行的 os 段 | +| `uname` | `uname -s` 在该系统上的回答;每个系统一个程序的单文件按它选择程序 | + +- 陈述了 `uname` 的系统可以进入多系统单文件;表中未陈述 `uname` 的系统,其程序是文件开头的 PE;表中没有的系统被拒绝, + 并指名 `[[platform.system]]`。 +- `mcpp pack --systems all` 取表中的全部系统,加上平台定义了行的系统;其中没有程序可构建的对逐个写为未覆盖(SPEC-014 §4.5)。 +- 同一系统陈述两次时取先陈述者;内置数据先于平台。 +- 引擎源码中系统名的出现次数由检查持有(`.github/tools/check_system_names.py` 与其基线),只许减少。 + ## 3. 提供者 | 键 | 语义 | @@ -70,6 +89,9 @@ | `toolchain`、`sysroot`、`linkage`、`layers`、`link` | 同 SPEC-013 §1 | | `hosted` | C 环境是否 hosted | | `execution` | 产物运行的系统 | +| `tier` | 这一安排的档位;未写时为行的档位(v1.2) | +| `system-headers` | 陈述系统自身接口的头文件所在的包;只由对它们编译的步骤读取(资源编译)(v1.2) | +| `runner` | 该安排的产物怎样启动:`{ cmd = [argv], hosts = [...], requires = [...] }`(§6)(v1.2) | - 引擎不含提供者的类别,也不含实现名;所有提供者同一结构,只有数据不同。 - 内置提供者:glibc 载荷、musl-gcc、MinGW-w64(与 gcc、llvm 配对)、`mingw-llvm`(v1.1)、MSVC、Apple SDK、Android NDK、 @@ -80,6 +102,13 @@ driver-internal),在项目与本机都未陈述时,先取所选提供者的 `sysroot`(包,或相对平台清单的 `{ path = "<目录>" }`), 再取平台的 `sysroot`(SPEC-006 §3.9.2 的引擎默认档),按项目陈述的同一规则解析;是基座包时按 SPEC-002 §2.5 读取。 平台由此为每个身份指名自己的基座。 +- **一条条目陈述一种载荷安排(v1.2)。** 原生安排与交叉安排是两条条目,各有自己的 `hosts`、`sysroot`、`linkage` 与 + 优先级;其事实随所在宿主而变的条目**必须**拆开。推论:一个身份在其原生宿主上,写与不写 `--target` 得到同一条链接行。 +- **`glibc-sysroot`(v1.2)。** 以基座包 `xim:aarch64-linux-gnu-sysroot` 与 llvm 服务 `aarch64-linux-gnu` 的交叉安排,在 + `linux-x86_64` 可得,优先级 40,动态链接,档位 preview;缺省运行器为 `qemu-aarch64-static -L {sysroot} {}`。原生安排仍是 + `glibc`(优先级 100,`linux-{arch}`)。 +- **`system-headers`(v1.2)。** 资源脚本由 llvm-rc 编译、而工具链未陈述任何头文件来源的行,取所选提供者的 `system-headers` + 所指的包的 `include`;已陈述来源的行(MSVC 的 SDK、配对载荷)不解析它,也不安装该包。该包无法安装时构建被拒绝并指名它。 - **`mingw-llvm`(v1.1)。** 以基座包 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、libunwind、 compiler-rt)与 llvm 服务 `x86_64-windows-gnu` 与 `aarch64-windows-gnu`,在任一宿主可得,优先级 5,低于有 gcc 载荷之处的 MinGW-w64 提供者。基座是 clang 的 sysroot;其 libc++ 的模块源码是 std 模块;编译器运行时取自基座所指的目录;静态链接取基座的 @@ -97,6 +126,10 @@ 5. **身份随 C 库。** 依赖图提供 C 库(`mcpp:c-abi`)而请求未写 env 时,身份取实际 C 库的行,重新准备一次。写出的 env 是项目的 拼写,不被替换;与图不一致时报告(目标报告的 `asks for the … C ABI`),不拒绝。 +6. **请求的回答是契约(v1.2)。** 新增的提供者条目**禁止**改变任一宿主上既有请求的回答:它以低于现有回答的优先级进入, + 只由写全身份的请求或显式选择取得。改变一个请求的缺省回答是一次单独的决定,记入 CHANGELOG 与 §8。行表快照 + (`tests/fixtures/target-rows.snapshot`)记录每个请求在每种宿主上的回答。 + ## 5. 平台定义的行与提供者 平台包以 `[[platform.define]]` 定义行(§2.1 的字段),以 `[[platform.provider]]` 声明提供者(§3 的键);`[platform]` 与 @@ -119,6 +152,22 @@ - **执行。** 行的 `execution` 不是本机系统时:构建不以本机加载器检查产物的运行时闭包;`mcpp run` 与 `mcpp test` 在无运行器、 无执行主机时报告「已构建,未运行」。运行器包是目标的工具(`[target..tools]`),以 docs/41 的运行协议启动。打包从产物 文件读取其闭包,不运行本机加载器(SPEC-014,v1.1)。 +- **一个谓词(v1.2)。** 「产物是否由本机运行」只有两个问法,各在一处回答,内置行与平台定义的行相同:执行系统是否为本机 + 系统(启动之前问);执行系统与架构是否都是本机的(检查加载器与运行时闭包之前问)。 +- **不启动另一系统的程序(v1.2)。** 执行系统不是本机系统、又没有运行器与执行主机时,`mcpp run` 与 `mcpp test` 在启动任何 + 进程之前回答「已构建,未运行」,写出两个系统与可写的声明;退出码为 2。本机内核不读取程序所属的系统,另一系统的系统调用 + 号在本机另有含义,所以启动不是观察。`--no-runner` 陈述该身份在本机可直接执行,照此执行。同一系统、另一架构的产物照旧 + 启动:内核拒绝它,或由已登记的模拟器运行它。 +- **程序解释器(v1.2)。** 由本机运行的动态产物写基座自己的加载器所在的路径;不由本机运行的动态产物写行的 `loader`,且 + 不带任何本机目录的 RUNPATH。构建机的安装路径**禁止**进入后一类产物。 +- **基座包只有一种读法(v1.2)。** 根目录带清单的基座是包根即 sysroot(SPEC-002 §2.5),不论其目录怎样命名;目录布局只对 + 不带清单的目录探测。另一台机器的基座不取工具链载荷为其自身宿主构建的运行时目录。 +- **提供者的运行器(v1.2)。** 提供者条目的 `runner` 是运行器陈述的最低一级:其上依次是平台、构建程序、本机配置、项目与 + 环境变量(SPEC-013 §4.5)。`hosts` 列出该运行器所在的宿主,空为任一宿主。模板中 `{}` 是产物,`{sysroot}` 是该安排的 + 基座,`{pkg:}` 是 `requires` 中该名字的包的根;模板的程序先在 `requires` 的包中查找,再查 PATH。`requires` 的包 + 在第一次 `mcpp run` 或 `mcpp test` 时按工具链的同一方式安装;构建不安装它们。 +- **运行器的身份(v1.2)。** 平台的 `runner` 可陈述 `built-as = { "-" = "<身份>" }`:为该宿主对构建的运行器程序 + 是哪个身份。自带运行器的包按它为各系统构建运行器;引擎只对它一向认识的三个系统保留约定,其余系统无此陈述即无程序。 - **`link.flags` 与链接驱动(v1.1)。** `[platform.link] flags` 只交给 ld.lld;`driver = "compiler"` 的平台陈述它时,构建 给出警告并指名 `[platform.build] ldflags`。 @@ -138,6 +187,8 @@ ## 8. 兼容性 - 已有身份字符串、输出目录与 `cfg` 结果不变;`[target.X]`、`descriptor_keys`、`[platform.hosted]` 照旧可读。 +- 行为变化(v1.2):执行系统不是本机系统的产物,在没有运行器时不再被启动(此前 `mcpp run` 启动它并报告内核的回答); + 经 binfmt 登记了另一系统解释器的宿主以 `--no-runner` 或一条 `runner` 声明照旧运行。 - 行为变化:只写 `-` 的请求在本宿主不可得其标准身份时取下一个可得的身份(无 MSVC 的宿主上 `x86_64-windows` 为 `x86_64-windows-musl`;非 macOS 宿主上 `aarch64-macos` 为 `aarch64-macos-musl`;x86_64 宿主上 `aarch64-linux` 为 `aarch64-linux-musl`);依赖图提供 musl 而请求未写 env 时身份为 `-musl`。 @@ -148,3 +199,4 @@ |---|---|---| | 1.0 | 2026-11-09 | 初版:行、提供者、选择、平台定义的行、C 环境、执行、报告与锁 | | 1.1 | 2026-11-10 | `clang-triple`;内置行的系统词;FreeBSD 两种身份与 32 位 planned 行;提供者 sysroot 为基座陈述的最后一级;`mingw-llvm` 与 `aarch64-windows-gnu`;C 库由平台描述的行与内置行同一读法;打包读文件闭包;`link.flags` 的警告;构建程序的 `MCPP_KERNEL_ABI_INTERFACES` | +| 1.2 | 2026-11-12 | 行的 `loader` 与 `execution` 为内置行所读;系统表(`uname`);提供者的 `tier`、`system-headers`、`runner`;一条条目一种安排;请求的回答是契约;`glibc-sysroot`;一个执行谓词,另一系统的程序不被启动;程序解释器与基座包的单一读法;运行器的占位、`requires` 与 `built-as` | diff --git a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh index 12f13f23b..68d27d593 100755 --- a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh +++ b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh @@ -1,64 +1,87 @@ #!/usr/bin/env bash -# requires: llvm aarch64-gnu-sysroot +# requires: llvm elf source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1076 -- an aarch64-linux-gnu cross takes its C library from the sysroot -# payload its row names (2026.11.11.1, G1). +# 1076 -- the cross arrangement of aarch64-linux-gnu: its C library, C++ +# library and compiler run-time come from a base package, its artifacts name +# the row's loader, and they run through the provider's runner (SPEC-016 §3, +# §6; design 2026-11-12 C2, C3, C5). # -# `host_can_serve` answered "no payload here produces it" for every arch-cross -# glibc target: llvm alone carries no glibc of another architecture. The -# row's own declaration is the better answer -- the provider that serves the -# identity names a sysroot payload (`xim:aarch64-linux-gnu-sysroot`, the -# package root IS the sysroot: glibc with the kernel headers, libgcc and -# libc++ inside) and states the hosts it is published for, so the gate reads -# the data and no host is named in the engine. +# The row has two provider entries: `glibc`, the host's payload on an aarch64 +# host, and `glibc-sysroot`, a base package on linux-x86_64 at a priority +# below the musl entries. The second is what this script holds: # -# The capability token probes the payload's install directory: the recipe is -# pending in the package index, so a machine without it (CI until the index -# carries it) skips rather than guesses. Where the payload IS installed the -# criteria are: a minimal C program builds for aarch64-linux-gnu, the -# artifact is an aarch64 ELF, and -- with qemu-aarch64 on the machine -- it -# runs and its exit status is passed through. +# 1. `aarch64-linux` is answered as before the entry existed (musl); the +# whole identity reaches the cross entry. +# 2. A C program, a C++ program over the headers and a program that imports +# std each build, dynamically; the last one statically as well. +# 3. Each dynamic artifact is an aarch64 ELF whose interpreter is the row's +# `loader`, with no RUNPATH and no path of this machine in it. +# 4. `mcpp run` starts each through the provider's runner, which this run +# installs, and the program's exit status and output arrive. +# +# The entry's hosts are linux-x86_64: elsewhere the script has nothing to hold +# and says so with the skip status. set -e t=$(mktemp -d); trap 'rm -rf "$t"' EXIT fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } -# The cap token already probed the registry; re-check so the script also runs -# standalone without run_all.sh. -sysroot="" -for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-aarch64-linux-gnu-sysroot/*/; do - [ -d "$d/lib" ] && sysroot="$d" -done -if [ -z "$sysroot" ]; then - echo "SKIP: 1076 -- no aarch64-linux-gnu-sysroot payload installed (xim:aarch64-linux-gnu-sysroot)" - echo "PASS: 1076 (skipped)" - exit 0 +if [ "$(uname -s)" != Linux ] || [ "$(uname -m)" != x86_64 ]; then + echo "SKIP: the cross entry's hosts are linux-x86_64" + exit 77 fi +readelf="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readelf" -mkdir -p "$t/src" -cat > "$t/mcpp.toml" <<'TOML' -[package] -name = "cross" -version = "0.1.0" +T=aarch64-linux-gnu +LOADER=/lib/ld-linux-aarch64.so.1 -[targets.cross] -kind = "bin" -main = "src/main.c" -TOML -printf '#include \nint main(void) { printf("cross-ok\\n"); return 42; }\n' > "$t/src/main.c" -cd "$t" -out=$("$MCPP" build --target aarch64-linux-gnu --toolchain "llvm@${LLVM_VERSION}" 2>&1) \ - || fail "aarch64-linux-gnu did not build from the row's sysroot payload" "$out" -exe=$(find target/aarch64-linux-gnu -path '*/bin/cross' -type f | head -1) -[ -n "$exe" ] || fail "no program" "$out" -readelf="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readelf" -[ -x "$readelf" ] || fail "no llvm-readelf in the llvm payload ($readelf)" -arch=$("$readelf" -h "$exe" | awk '/Machine:/ {print $2}') -[ "$arch" = "AArch64" ] || fail "the artifact is not an aarch64 ELF (machine: $arch)" "$("$readelf" -h "$exe" | head -20)" -# Where the machine can run it, the program's own exit status is the truth. -if command -v qemu-aarch64 >/dev/null 2>&1; then +project() { #
+ mkdir -p "$t/$1/src" + printf '[package]\nname = "cross"\nversion = "0.1.0"\n\n[targets.cross]\nkind = "bin"\nmain = "src/%s"\n' "$2" \ + > "$t/$1/mcpp.toml" + printf '%b' "$3" > "$t/$1/src/$2" +} +project c main.c '#include \nint main(void) { printf("cross-c\\n"); return 42; }\n' +project cxx main.cpp '#include \nint main() { std::println("cross-cxx"); return 43; }\n' +project std main.cpp 'import std;\nint main() { std::println("cross-std"); return 44; }\n' + +# 1. The request `aarch64-linux` keeps its answer. +cd "$t/c" +out=$("$MCPP" why target --target aarch64-linux 2>&1) || fail "why target refused" "$out" +grep -q "aarch64-linux-musl" <<<"$out" || fail "aarch64-linux is no longer answered by the musl identity" "$out" + +check() { # + cd "$t/$1" + out=$("$MCPP" build --target "$T" 2>&1) || fail "$1: $T did not build over the base package" "$out" + grep -q "glibc-sysroot\|aarch64-linux-gnu-sysroot" <<<"$out" || fail "$1: the base package is not what served it" "$out" + exe=$(find "target/$T" -path '*/bin/cross' -type f | head -1) + [ -n "$exe" ] || fail "$1: no program" "$out" + [ -x "$readelf" ] || fail "no llvm-readelf in the llvm payload ($readelf)" + hdr=$("$readelf" -h "$exe") + grep -q "Machine:.*AArch64" <<<"$hdr" || fail "$1: the artifact is not an aarch64 ELF" "$hdr" + interp=$("$readelf" -l "$exe" | sed -n 's/.*Requesting program interpreter: \(.*\)\]/\1/p') + [ "$interp" = "$LOADER" ] || fail "$1: the interpreter is '$interp', the row's loader is $LOADER" + dyn=$("$readelf" -d "$exe") + if grep -qiE "RPATH|RUNPATH" <<<"$dyn"; then fail "$1: the artifact carries a run-time path of this machine" "$dyn"; fi + if grep -aq "${MCPP_HOME:-$HOME/.mcpp}" "$exe"; then fail "$1: a path of this machine's store is in the artifact"; fi rc=0 - qemu-aarch64 -L "${sysroot%/}" "$exe" > run.log 2>&1 || rc=$? - [ "$rc" -eq 42 ] || fail "qemu-aarch64 run: expected exit 42, got $rc" run.log - grep -q '^cross-ok$' run.log || fail "the program's output did not arrive" run.log -fi + run=$("$MCPP" run --target "$T" 2>&1) || rc=$? + [ "$rc" -eq "$3" ] || fail "$1: mcpp run exited $rc, the program returns $3" "$run" + grep -q "^$2\$" <<<"$run" || fail "$1: the program's output did not arrive" "$run" + grep -q "Running .*qemu-aarch64" <<<"$run" || fail "$1: the run did not go through the provider's runner" "$run" +} +check c cross-c 42 +check cxx cross-cxx 43 +check std cross-std 44 + +# 2. Statically: no interpreter, and the same exit status through the runner. +cd "$t/std" +printf '\n[target.%s]\nlinkage = "static"\n' "$T" >> mcpp.toml +out=$("$MCPP" build --target "$T" 2>&1) || fail "the static import-std program did not build" "$out" +exe=$(find "target/$T" -path '*/bin/cross' -type f -newer mcpp.toml | head -1) +[ -n "$exe" ] || fail "no static program" "$out" +if "$readelf" -l "$exe" | grep -q "program interpreter"; then fail "a static program names an interpreter"; fi +rc=0 +run=$("$MCPP" run --target "$T" 2>&1) || rc=$? +[ "$rc" -eq 44 ] || fail "static: mcpp run exited $rc, the program returns 44" "$run" + echo "PASS: 1076 an aarch64-linux-gnu cross comes from a sysroot payload" diff --git a/tests/e2e/330_runner_hosted_targets.sh b/tests/e2e/330_runner_hosted_targets.sh index 3392503c8..5e5d36347 100644 --- a/tests/e2e/330_runner_hosted_targets.sh +++ b/tests/e2e/330_runner_hosted_targets.sh @@ -121,7 +121,9 @@ if [[ "$(uname -s)" == Linux ]]; then grep -q "this host cannot execute" <<<"$out" || fail "unrunnable message missing: $out" grep -q "Exec format error" <<<"$out" || fail "the kernel's answer is missing: $out" grep -q "\[target.$HOST\]" <<<"$out" || fail "paste-able key missing: $out" - grep -q 'runner = \["qemu-aarch64-static"\]' <<<"$out" || fail "runner example missing: $out" + grep -q 'runner = \["", "{}"\]' <<<"$out" || fail "the runner declaration's shape is missing: $out" + # No program is named: which one runs the artifact is a statement of the data (SPEC-016 §6). + if grep -q "qemu" <<<"$out"; then fail "the message names an emulator: $out"; fi grep -q -- "--no-runner" <<<"$out" || fail "escape hatch not mentioned: $out" # The criterion measured the patched artifact, not a rebuilt one. [[ "$(od -An -tx1 -j18 -N2 "$bin" | tr -d ' ')" == "ffff" ]] \ diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 24deac883..f7294893b 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -93,14 +93,6 @@ case "$OS" in if ls "${MCPP_HOME}"/registry/data/xpkgs/xim-x-llvm/*/bin/clang++ 2>/dev/null | head -1 | grep -q .; then CAPS+=(llvm) fi - # aarch64-gnu-sysroot: the glibc sysroot payload the aarch64-linux-gnu - # row names (xim:aarch64-linux-gnu-sysroot). A payload directory, not a - # version: the row's e2e skips until the package index carries the - # recipe and the runner prewarms it, which is why the token probes - # rather than assuming. - if ls "${MCPP_HOME}"/registry/data/xpkgs/xim-x-aarch64-linux-gnu-sysroot/*/lib/libc.so.6 2>/dev/null | head -1 | grep -q .; then - CAPS+=(aarch64-gnu-sysroot) - fi # Host MinGW headers can come from a distro prefix or the dedicated # xlings fixture outside MCPP_HOME. Test 738 proves that unrestricted # Clang finds the latter before checking graph isolation. @@ -279,7 +271,7 @@ echo "Detected capabilities: ${CAPS[*]:-}" # absent on Linux and must stay legal to declare. It is checked against the # CAPS+=() calls above by tests/e2e/README or by reading them -- keep it in # sync when adding a capability. -KNOWN_CAPS=(aarch64-gnu-sysroot android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos +KNOWN_CAPS=(android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw mingw-cross mingw-host-headers msvc musl nasm no-msvc pack patchelf python3 qemu-arm qemu-riscv scan-deps symlink unix-shell windows wine xlings-msvc) diff --git a/tests/unit/test_link_line_golden.cpp b/tests/unit/test_link_line_golden.cpp new file mode 100644 index 000000000..98edfa93d --- /dev/null +++ b/tests/unit/test_link_line_golden.cpp @@ -0,0 +1,199 @@ +// THE LINK LINE'S C-LIBRARY GROUP, AS A TABLE (design 2026-11-12 §5.4). +// +// `compute_flags` renders, for every link, the words that name the C library: +// the crt search prefix, the library directories, the loader and the sysroot. +// Which words appear is decided by four facts -- the compiler family, who +// supplies the C library, whether this machine loads the artifact, and the +// linkage -- and each combination has one right answer. This file writes the +// answers down. A change to the renderer that loses a group shows here as one +// differing line, without a toolchain and without a link. +// +// The measurement behind it: a rewrite of the GCC branch dropped `-B`, `-L` +// and the loader of every gcc build over the host's payload (the crt objects +// then resolved from /lib), and no check of the tree asked for those words. +// +// The fixtures are directories of empty files: the renderer reads paths and +// the presence of a loader and of a driver's `.cfg`, never file contents. + +import std; +import mcpplibs.mtest; +import mcpp.build.flags; +import mcpp.build.plan; +import mcpp.manifest; +import mcpp.targetside; +import mcpp.toolchain.base; +import mcpp.toolchain.model; +import mcpplibs.os.platform; + +using namespace mcpplibs::mtest; +using namespace mcpp::build; +namespace tc = mcpp::toolchain; +namespace ts = mcpp::targetside; +namespace fs = std::filesystem; + +namespace { + +struct Tree { + fs::path root; + Tree() { + static std::atomic n{0}; + root = fs::temp_directory_path() + / std::format("mcpp-link-golden-{}-{}", std::chrono::steady_clock::now().time_since_epoch().count(), n++); + fs::create_directories(root); + } + ~Tree() { std::error_code ec; fs::remove_all(root, ec); } + fs::path file(const fs::path& rel) const { + fs::create_directories((root / rel).parent_path()); + std::ofstream(root / rel) << ""; + return root / rel; + } + fs::path dir(const fs::path& rel) const { + fs::create_directories(root / rel); + return root / rel; + } +}; + +BuildPlan plan_for(tc::CompilerId compiler, const fs::path& driver, std::string triple) { + BuildPlan plan; + plan.projectRoot = fs::temp_directory_path() / "mcpp-link-golden"; + plan.outputDir = plan.projectRoot / "target" / "test"; + plan.manifest.package.name = "golden"; + plan.toolchain.compiler = compiler; + plan.toolchain.version = "test"; + plan.toolchain.binaryPath = driver; + plan.toolchain.targetTriple = std::move(triple); + const bool clang = compiler == tc::CompilerId::Clang; + plan.targetSide.compiler = { ts::Origin::Payload, clang ? "llvm" : "gcc", "", false }; + plan.targetSide.kernelAbi = { ts::Origin::Payload, "linux", "", false }; + plan.targetSide.cAbi = { ts::Origin::Payload, "glibc", "", false }; + plan.targetSide.cxx = { ts::Origin::Payload, clang ? "libc++" : "libstdc++", "", false }; + return plan; +} + +// The words of `line` that name the C library, in order, with `tree` written +// as ``: what the table below compares. +std::vector c_library_words(std::string_view line, const fs::path& tree) { + std::vector out; + const auto t = tree.generic_string(); + for (auto part : std::views::split(line, ' ')) { + std::string w(part.begin(), part.end()); + const bool named = w.starts_with("-B") || w.starts_with("-L") || w.starts_with("--sysroot") + || w.starts_with("-Wl,--sysroot") || w.starts_with("-Wl,-rpath") + || w.starts_with("-Wl,--dynamic-linker") || w.starts_with("--target="); + if (!named) continue; + for (auto at = w.find(t); at != std::string::npos; at = w.find(t, at + 3)) w.replace(at, t.size(), ""); + out.push_back(std::move(w)); + } + return out; +} + +using Words = std::vector; + +void expect_words(std::string_view what, const Words& got, const Words& want) { + if (auto&& failed = expect(eq(got, want)); !failed) failed << what; +} + +} // namespace + +// gcc over the host's payload: the crt prefix, the library directory with its +// run-time path, the compiler's own run-time directory, and the payload's +// loader. The regression this file exists for emptied this list. +UTest gcc_over_the_hosts_payload{"LinkLineGolden/GccOverTheHostsPayload", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto glibc = t.dir("glibc/lib64"); + t.file("glibc/lib64/ld-linux-x86-64.so.2"); + t.dir("glibc/include"); + t.dir("gcc/lib64"); + auto plan = plan_for(tc::CompilerId::GCC, t.file("gcc/bin/g++"), "x86_64-linux-gnu"); + plan.toolchain.base.payload = tc::PayloadPaths{ t.root / "glibc/include", glibc, {} }; + const auto f = compute_flags(plan); + const Words want{ "-B/glibc/lib64", "-L/glibc/lib64", "-Wl,-rpath,/glibc/lib64", + "-L/gcc/lib64", "-Wl,-rpath,/gcc/lib64", + "-Wl,--dynamic-linker=/glibc/lib64/ld-linux-x86-64.so.2" }; + expect_words("the C++ link", c_library_words(f.sysroot, t.root), want); +}}; + +// clang with its configuration over the host's payload: the same group, the +// compiler's run-time being compiler-rt's and not a directory of the list. +UTest clang_over_the_hosts_payload{"LinkLineGolden/ClangOverTheHostsPayload", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto glibc = t.dir("glibc/lib64"); + t.file("glibc/lib64/ld-linux-x86-64.so.2"); + t.dir("glibc/include"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "x86_64-linux-gnu"); + plan.toolchain.base.payload = tc::PayloadPaths{ t.root / "glibc/include", glibc, {} }; + const auto f = compute_flags(plan); + const Words want{ "-B/glibc/lib64", "-L/glibc/lib64", "-Wl,-rpath,/glibc/lib64", + "-Wl,--dynamic-linker=/glibc/lib64/ld-linux-x86-64.so.2" }; + expect_words("the C++ link", c_library_words(f.ld, t.root), want); + expect_words("the C link", c_library_words(f.ldC, t.root), want); +}}; + +// A base package of another machine (SPEC-016 §6): the sysroot and the row's +// loader, and nothing of this machine -- no directory of the payload, no +// run-time path. The artifact's interpreter is a path on the machine that +// runs it. +UTest clang_over_a_base_of_another_machine{"LinkLineGolden/ClangOverABaseOfAnotherMachine", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto base = t.dir("base"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "aarch64-linux-gnu"); + plan.toolchain.crossTargetFlag = "--target=aarch64-unknown-linux-gnu"; + plan.toolchain.base.root = base; + plan.toolchain.base.form = tc::base::Form::Package; + plan.toolchain.base.text = "xim:aarch64-linux-gnu-sysroot@2.44.3"; + plan.toolchain.base.runtime = tc::base::Runtime::Floor; + plan.toolchain.base.interpreter = "/lib/ld-linux-aarch64.so.1"; + plan.toolchain.base.layers.cAbi = "glibc"; + plan.targetSide.cAbi = { ts::Origin::Xpkg, "glibc", "xim:aarch64-linux-gnu-sysroot@2.44.3", false }; + const auto f = compute_flags(plan); + const Words want{ "--target=aarch64-unknown-linux-gnu", "--sysroot=/base", + "-Wl,--dynamic-linker=/lib/ld-linux-aarch64.so.1" }; + expect_words("the C++ link", c_library_words(f.sysroot, t.root), want); + // No word of the line names a directory of this machine for the run time. + expect(eq(f.ld.find("-rpath"), std::string::npos)) << f.ld; +}}; + +// The same base, linked statically: no interpreter is named, since a static +// program has none. +UTest a_static_link_over_a_base_names_no_loader{"LinkLineGolden/AStaticLinkOverABaseNamesNoLoader", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto base = t.dir("base"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "aarch64-linux-gnu"); + plan.toolchain.crossTargetFlag = "--target=aarch64-unknown-linux-gnu"; + plan.toolchain.base.root = base; + plan.toolchain.base.form = tc::base::Form::Package; + plan.toolchain.base.text = "xim:aarch64-linux-gnu-sysroot@2.44.3"; + plan.toolchain.base.runtime = tc::base::Runtime::Floor; + plan.targetSide.cAbi = { ts::Origin::Xpkg, "glibc", "xim:aarch64-linux-gnu-sysroot@2.44.3", false }; + const auto f = compute_flags(plan); + const Words want{ "--target=aarch64-unknown-linux-gnu", "--sysroot=/base" }; + expect_words("the C++ link", c_library_words(f.sysroot, t.root), want); +}}; + +// The C library from the dependency graph: the host's model contributes no +// word. What the link names is the graph's, stated by its packages. +UTest the_graph_supplies_the_c_library{"LinkLineGolden/TheGraphSuppliesTheCLibrary", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto glibc = t.dir("glibc/lib64"); + t.file("glibc/lib64/ld-linux-x86-64.so.2"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "x86_64-linux-musl"); + plan.toolchain.base.payload = tc::PayloadPaths{ t.root / "glibc/include", glibc, {} }; + plan.targetSide.kernelAbi = { ts::Origin::Graph, "openkal", "openkal-linux@0", false }; + plan.targetSide.cAbi = { ts::Origin::Graph, "musl", "openkal-musl@0", false }; + plan.targetSide.cxx = { ts::Origin::Graph, "libc++", "openkal-llvm-runtime@0", false }; + const auto f = compute_flags(plan); + expect_words("the link's own group", c_library_words(f.sysroot, t.root), Words{}); +}}; From adc3eb380421612f20e6c5f2fd4886dae5f60b89 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 16:47:24 +0900 Subject: [PATCH 18/30] wip: docs en/zh, changelog, tier in why target, e2e 1080, ratchet in CI, design record v3 --- ...system-architecture-optimization-design.md | 29 +++- .agents/docs/README.md | 4 +- .github/workflows/ci.yml | 3 + CHANGELOG.md | 127 ++++++++++-------- docs/10-pack-and-release.md | 18 ++- docs/21-the-target-triple.md | 9 +- docs/22-target-side.md | 24 +++- docs/35-platforms.md | 47 +++++++ docs/41-devices.md | 20 ++- docs/zh/10-pack-and-release.md | 4 + docs/zh/21-the-target-triple.md | 6 +- docs/zh/22-target-side.md | 21 ++- docs/zh/35-platforms.md | 36 +++++ docs/zh/41-devices.md | 10 +- .../toolchain-model/src/target_select.cppm | 10 ++ src/build/prepare.cppm | 1 + src/build/prepare/plan.cpp | 2 +- src/doctor.cppm | 7 +- ..._gnu_cross_comes_from_a_sysroot_payload.sh | 10 +- ...gram_of_another_system_is_built_not_run.sh | 4 +- ...he_hosts_payload_links_from_the_payload.sh | 38 ++++++ tests/e2e/run_all.sh | 7 +- 22 files changed, 342 insertions(+), 95 deletions(-) create mode 100644 tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh diff --git a/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md index ec52c5a8a..997928ab5 100644 --- a/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md +++ b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md @@ -6,9 +6,10 @@ status: active # 生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings) **日期:** 2026-11-12 -**状态:** 草案第二版,待评审。评审定稿前不实施任何一项。第二版并入:D16–D23 的裁决、CI 触发模型(5.11)、固件与裸机 -(5.12)、归属判据(P16)、层级三与固件的六个本地探针(第 8 节,结果在 `okexp/t3/PROBES.md`),以及多视角自审(第 14 节) -对第一版的七处修改。第 11 节列出仍需裁决的决定。 +**状态:** 第三版,实施中。第二版并入了 D16–D23 的裁决、CI 触发模型(5.11)、固件与裸机(5.12)、归属判据(P16)、 +层级三与固件的本地探针(第 8 节)与多视角自审(第 14 节)。负责人于 2026-11-12 授权按本文实施,第 11.2 节的决定按其推荐 +执行,其中 D5 由"每个仓库一个 PR"取代(mcpp#924 续用,不拆分)。第三版增加 P17 与 5.13(重复实现的收敛),并在第 15 节 +记录实施中对本文的修正与各项的门禁。 **范围:** mcpp(引擎、内置数据、规范、CI);openkal 家族(规范、各实现、openkal-musl、openkal-llvm-runtime、 openkal-image、openkal-platform);xlings 生态(xim-pkgindex、xpkgindex-update、xlings-res 资产、dirun);mtest(库与引擎)。 验证工程(一个 C89 命令解释器)只用于检验,不在上游范围内,本文不点名。 @@ -264,7 +265,7 @@ SPEC-016 §6 已经规定:行的 `execution` 不是本机系统、又没有运 [[platform.provider]] name = "openkal" serves = ["*-freebsd-musl"] -runner = { cmd = ["dirun", "--profile", "{image}/dirun.toml", "--", "{}"], hosts = ["linux-*", "macos-*"], +runner = { cmd = ["dirun", "--profile", "{pkg:freebsd-disk-image}/dirun.toml", "--", "{}"], hosts = ["linux-*", "macos-*"], requires = ["xim:dirun", "xim:freebsd-disk-image@14.5", "xim:qemu-system"] } ``` @@ -272,7 +273,8 @@ runner = { cmd = ["dirun", "--profile", "{image}/dirun.toml", "--", "{}"], hos 1. 优先级沿用 P8:项目 `[target.] runner` 先于本机配置,先于平台,先于提供者。 2. `requires` 里的包在第一次 `mcpp run` 或 `mcpp test` 需要时按工具链同样的同意方式安装;构建从不安装它们。 -3. 模板的占位:`{}` 是产物,`{sysroot}` 来自链接模型(5.4),`{image}` 是 `requires` 中镜像包的根。 +3. 模板的占位:`{}` 是产物,`{sysroot}` 是该安排的基座,`{pkg:}` 是 `requires` 中该名字的包的根(一个模板可以 + 用到两个包——镜像与固件——所以占位按名字,而不是一个 `{image}`)。模板的程序先在 `requires` 的包里找,再找 PATH。 4. **不运行另一系统的程序来观察它。** 内置行与平台定义的行由同一个谓词判断:行的执行系统不是本机、又没有运行器,就在启动 之前回答"已构建,未运行",指名身份与可写的声明。另一系统的系统调用号在本机内核上指向别的操作,启动它的后果不确定 (第 14 节第 2 条)。诊断的文字由数据生成,只有一处。 @@ -517,6 +519,23 @@ openkal-opensbi 是写在 SBI 上的实现,提供核心三个接口加 `time` **收益。** openkal 的 UEFI 实现之上是同一个 openkal-musl 与同一个 C++ 运行时:UEFI 应用可以用 C++23 模块与 `import std` 编写,在 CI 里两秒跑完。这对引导加载器、固件工具与教学是直接可用的。 +### 5.13 重复实现的收敛(P17) + +实施中遇到的"同一个问题有两处以上回答"逐项收敛。每项收敛前后由同一组测试证明行为不变,有意的行为变化单列。 + +| 问题 | 收敛前 | 收敛后 | +|---|---|---| +| 产物是否由本机运行 | 六处各自近似:`hermetic.cppm`(自带 sysroot 或由平台描述)、`runtime_validation.cppm` 三处、`pack.cppm`、`execute.cppm` 两处三元式 | `Triple::system_is_host()` 与 `Triple::loads_on_host()`,读行的 `execution` 与架构;Android 的行以数据陈述 `execution = "android"` | +| 基座供给 C 库时宿主让位 | `flags.cppm` 五处读 `base.layers.cAbi.empty()`,与 `targetSide.cAbi.origin` 并存 | 基座解析处决定一次(带清单即包根 sysroot;另一台机器的基座清空载荷的宿主运行时目录),`flags.cppm` 回到只渲染链接模型 | +| 加载器的名字 | `linkmodel.cppm` 的按架构表(`loader_filename`、`distro_loader_path`),与行的 `loader` 键并存而后者无人读 | 两个函数读行的 `loader`;表删除 | +| 本主机能否服务一个目标 | `registry.cppm::host_can_serve` 约 150 行按系统名的分支,与选择读的 `hosts` 数据并存 | 读提供者的 `hosts`;函数里不再有系统或架构 | +| 一个包引用所指的目录 | `base.cpp` 的 `package_dir` 与 `windows_resources.cpp` 的另一份(顺序相反) | 一个解析,三个使用者(基座、系统头文件、运行器的包) | +| 多系统单文件按什么选择程序 | `polyglot.cppm` 的三元式与系统白名单 | 系统表的 `uname` | +| 运行器建议里的示例程序 | 三处字面量 `qemu-aarch64-static` | 只写声明的形状;可用的运行器由提供者陈述 | + +仍在基线里的(由 `check_system_names.py` 持有,只许减少):打包对 linux、macos、windows 三个系统的约定、 +对象格式与后缀的若干分支、`modules/platform/` 之外的宿主谓词。 + ## 6. 纠正清单 每项:现状、纠正、兼容性。C1–C11 在 mcpp#924 合入之前完成;C12–C16 是对已合入项的追加 PR。 diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 06873dfa9..25ce1c30d 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 --- ``` -359 records. +360 records. ## By subject @@ -83,6 +83,7 @@ Records that declare one. Everything else is listed by date below. ### ecosystem +- [生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings)](2026-11-12-ecosystem-architecture-optimization-design.md) — active - [测试与依赖解析补充方案:mcpp、mcpp test、mtest、mcpp-ide](2026-11-10-test-and-resolution-supplement-design.md) — active - [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active - [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active @@ -193,6 +194,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 × openkal × xlings)](2026-11-12-ecosystem-architecture-optimization-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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bdd56a2e..34a4f320b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,9 @@ jobs: - name: Check modules/ wiring run: bash .github/tools/check_modules_wiring.sh + - name: Check that the engine's sources name no more systems than the baseline + run: python3 .github/tools/check_system_names.py + - name: Check that headers and conditional directives appear only in the system lock files run: bash .github/tools/check_include_regions.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index bd30f93da..8b413f8d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,65 +8,86 @@ ### 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. +- `aarch64-linux-gnu` has a cross arrangement: the provider `glibc-sysroot` + serves it on linux-x86_64 from the base package + `xim:aarch64-linux-gnu-sysroot` (glibc, the kernel headers, libc++ with its + std module, compiler-rt), with llvm. A C program, a C++ program and one that + imports std build for it, dynamically and statically. The entry stands at + priority 40, below the musl entries, so the request `aarch64-linux` is + answered as before on every host and the whole identity reaches the new + entry; the native arrangement on an aarch64 host is unchanged. +- A provider states how its artifacts are started: `runner = { cmd, hosts, + requires }` on a provider entry is the lowest tier of the runner statement, + under the platform's, a build program's, the machine's and the project's. + `{}` is the artifact, `{sysroot}` the arrangement's base, `{pkg:}` the + root of the required package of that name; the packages of `requires` are + installed on the first `mcpp run` or `mcpp test`, never by a build. + `mcpp run --target aarch64-linux-gnu` on linux-x86_64 runs the artifact + under `qemu-aarch64-static -L {sysroot}` with nothing declared. +- The system table: `[[platform.system]] name, uname` states the facts of a + system that no single row carries, in the built-in data and in a platform + package alike. A file that carries one program per system selects by + `uname`; FreeBSD joins it through the table. +- Provider keys `tier` (the tier of one arrangement, where it differs from its + row's) and `system-headers` (the package whose headers state the system's + own interfaces). Row keys `loader` and `execution` are read on built-in rows + as on rows a platform defines. +- A platform's runner states `built-as = { "-" = "" }`: + the identity its runner program for that host pair is built as. A package + that carries its runner builds it so for a system the engine holds no + convention for; `mcpp pack --systems all` takes every system of the table + and writes the pairs no program is built for as not covered. - `longjmp` joins `threads` and `exceptions` as a member of - `[target..abi]`: how `sigsetjmp`/`siglongjmp` cross the + `[target..abi]`: how `sigsetjmp` and `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 - `-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. + link; `"js"` states the JavaScript model the same way. Unstated, the model + follows `exceptions`, as the SDK's default does. A build that states one + model for `exceptions` and the other for `longjmp` is refused with both + statements, and the member is refused on a row that is not Emscripten's. +- A Windows row whose system the graph supplies compiles its resources: its + provider names the package of the system's headers (`system-headers`), read + where the toolchain states no header source. A row that states its own (the + MSVC SDK, a paired payload) resolves nothing and installs nothing. +- `.github/tools/check_system_names.py` holds the number of times the engine's + sources name a system. The count is compared with a baseline that only + falls. +- `tests/unit/test_link_line_golden.cpp` states the C-library group of the + link line for each combination of compiler family, C library supplier and + machine. ### 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.]` 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. +- An artifact of another system is not started when nothing states how it + runs. `mcpp run` and `mcpp test` answer "built, not run" before any process + is spawned, for built-in rows as for rows a platform defines, and name the + `[target.]` runner declaration; the exit status is 2. The kernel of + this machine gives another system's call numbers its own meaning, so a + launch was not an observation. `--no-runner` states that the triple is + native here and is believed. An artifact of this system and another + architecture is launched as before. A host that runs another system's + programs through a registered interpreter passes `--no-runner` or declares + the runner. +- A dynamic artifact that this machine does not load names the row's `loader` + as its program interpreter and carries no RUNPATH of this machine. A native + dynamic artifact names the base's own loader, as before. +- A base package has one reading: a sysroot whose root carries a manifest is + rendered as a sysroot whatever its directories are called. The layout is + probed only for a directory without a manifest. +- Whether this host serves a target from a payload is answered by the + providers' `hosts`, the data the selection reads; the per-system conditions + of `host_can_serve` are removed. +- The messages that print a runner declaration print its shape and no + program. +- Android's rows state `execution = "android"`: the kernel is Linux and the + system is not. + +### Fixed + +- A base package's compiler run-time directory reaches the C link as well as + the C++ one. +- The loader name of a row is read from the row; the per-architecture table + in the link model is removed. ## [2026.11.10.2] - 2026-11-10 diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index e14d83dfe..4212d1238 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -857,16 +857,14 @@ 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-` gets FreeBSD's runners, -built as `-freebsd-musl` over the platform's hosted arrangement — the -same static identity a FreeBSD request takes — into -`libexec//freebsd-/`. A pack without a platform answers with -the machine's own system alone, and `--systems` naming an `` or an -`-` keeps its exact meaning; only an explicit `--systems all` (or -`[pack] systems`) ever packs another system. +**What `all` names is the system table (mcpp 2026.11.12.1+).** Every system +the table states (`[[platform.system]]`, built-in or a platform's) and every +system a platform defines a row of for the architecture. A pair no program is +built for is written as not covered, with the reason, and the pack succeeds for +the rest. The program of a system the engine holds no convention for is built +as the identity the platform's runner states for the host pair +(`runner.built-as`, [35](35-platforms.md)); a pack without `--systems` carries +the runner of the packing machine's system alone. **Where it runs.** The pack writes `runs_on` into `mcpp-package.json` and says it after `Packed`: `self`, the `-` pairs the package runs on with diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 0020b6710..ccb9a00cf 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -514,12 +514,19 @@ not from `linux-aarch64`, while `aarch64-linux-gnu` is served natively by `linux-aarch64`. Collapsing them to `linux` would let one overwrite the other's rows. +`aarch64-linux-gnu` has a second, cross arrangement (2026.11.12.1+): on +`linux-x86_64` the provider `glibc-sysroot` serves it from a base package +([22](22-target-side.md)). It stands below the musl entries, so `--target +aarch64-linux` is still answered by `aarch64-linux-musl` there and the whole +identity selects the glibc cross; `mcpp run` starts its artifacts through the +provider's runner ([41](41-devices.md)). + ### Build hosts and the targets they serve | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| | `x86_64-linux-gnu` | verified | — | payload | — | — | — | -| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | — | payload | — | — | +| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | base package | payload | — | — | | `x86_64-linux-musl` | verified | `gcc@16.1.0` | payload | payload | — | payload | | `aarch64-linux-musl` | verified | `gcc@16.1.0` | payload | payload | — | — | | `riscv64-linux-musl` | planned | — | planned | planned | planned | planned | diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 1c6f0f413..c19ad0e48 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1320,11 +1320,25 @@ A base package is the sysroot of one identity. A platform gives each identity it through each provider's `sysroot` ([35](35-platforms.md)); a system whose driver tells its architectures apart inside one root (MinGW-w64) is served by one package. +**A base package has one reading (2026.11.12.1+).** Its manifest states that the package root +is the sysroot, and it is rendered as one (`--sysroot=`) whatever its directories are +called; the directory layout is probed only for a directory that carries no manifest. Where +this machine does not load the artifact — the base is another architecture's or another +system's — the link takes nothing of the toolchain payload's own run-time directories, a dynamic +artifact names the row's `loader` as its program interpreter (`/lib/ld-linux-aarch64.so.1`), and +no directory of this machine enters its RUNPATH. Such an artifact runs through a runner, which +is given the base as `{sysroot}` ([41](41-devices.md)). A native dynamic artifact names the +base's own loader, where it stands. + The index carries `xim:x86_64-freebsd-sysroot` and `xim:aarch64-freebsd-sysroot` (FreeBSD's base system, with libc++'s module sources) and `xim:mingw-w64-sysroot` (llvm-mingw's target side: MinGW-w64 on UCRT, libc++, libunwind and compiler-rt, for x86_64 and aarch64). On a `*-windows-gnu` row the latter is clang's sysroot; the `mingw-llvm` provider names it, so every -host builds the GNU-ABI Windows rows ([21](21-the-target-triple.md)). +host builds the GNU-ABI Windows rows ([21](21-the-target-triple.md)). It also carries +`xim:aarch64-linux-gnu-sysroot` (glibc for arm64 with the kernel headers, libc++ with its module +sources, and compiler-rt), which the `glibc-sysroot` provider names: on linux-x86_64, +`--target aarch64-linux-gnu` builds C, C++ and `import std` programs over it, dynamically or +statically (2026.11.12.1+). A build program can ask which C library **payload** supplies the sysroot: `mcpp::target_libc()` returns that package's name and @@ -1397,6 +1411,14 @@ 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. +**`longjmp` and `exceptions` name one mechanism.** WebAssembly's exception +handling carries both the throw and the long jump, and the JavaScript model +carries both. An unstated `longjmp` follows `exceptions`, as the SDK's own +default does. A build that states one model for `exceptions` and the other for +`longjmp` is refused with both statements, before any compile. A declared +`longjmp` is part of the configuration's key; a project that does not state it +keeps the key it had. + **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..abi]` is reported (`abi/dependency-table`) and changes diff --git a/docs/35-platforms.md b/docs/35-platforms.md index 131b4ca17..3742c3ad0 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -355,6 +355,53 @@ driver = "compiler" `driver` is `"compiler"` gives them to no program, and the build says so, naming `[platform.build] ldflags`, whose words reach the compiler's link. +**A provider states how its artifacts start, and what a system is called +(2026.11.12.1+).** Three keys of a provider entry and one table: + +```toml +[[platform.provider]] +serves = ["x86_64-haiku"] +tier = "preview" # of this arrangement; unset: the row's +system-headers = "xim:haiku-headers@1.0" # the system's own interfaces, for a step that compiles against them +runner = { cmd = ["dirun", "--profile", "{pkg:haiku-disk-image}/dirun.toml", "--", "{}"], + hosts = ["linux-x86_64", "macos-aarch64"], + requires = ["xim:dirun@0.2.0", "xim:haiku-disk-image@1.0"] } + +[[platform.system]] +name = "haiku" +uname = "Haiku" # what `uname -s` answers there +``` + +A provider's `runner` is the lowest tier of the runner statement: the +platform's `runner`, a build program's, this machine's `config.toml`, the +project's `[target.] runner` and `MCPP_TARGET__RUNNER` each +replace it. `hosts` are the machines the runner exists on (unset: any). In the +template `{}` is the artifact, `{sysroot}` the base the artifact was linked +against, and `{pkg:}` the root of the package of that name among +`requires`; the template's program is looked for in the required packages +before `PATH`. The packages of `requires` are installed by the first `mcpp +run` or `mcpp test` that needs them, never by a build. + +One provider entry states one arrangement of payloads: a native arrangement and +a cross one are two entries, each with its `hosts`, base and priority. A new +entry does not change the answer of a request that was answered before it, so +it enters below the entries that answer `-` today +([SPEC-016 §4](specs/targets-and-providers.md)). + +A system that states a `uname` joins a file that carries one program per +system (`mcpp pack --pkg-format single-file`); the system that states none +leads the file as its PE. A platform whose runner is built for several systems +states the identity of each: + +```toml +[platform] +runner = { cmd = ["okrun", "{}"], hosts = ["linux-x86_64", "freebsd-x86_64"], + built-as = { "freebsd-x86_64" = "x86_64-freebsd-musl" } } +``` + +`mcpp pack --systems all` takes every system of the table, and writes a pair no +program is built for as not covered. + `mcpp new --template platform` writes a platform package defining `-`. Reading a platform's manifest checks its statement against the rows: a definition of a built-in row, or a row defined twice, is an error diff --git a/docs/41-devices.md b/docs/41-devices.md index ab52f6be0..a8f3829dc 100644 --- a/docs/41-devices.md +++ b/docs/41-devices.md @@ -159,10 +159,22 @@ freebsd-vm = { git = "https://example.org/freebsd-vm.git", tag = "v1" } The tool is built for this machine and started with the argv and the environment above: it transfers the artifact and the files `MCPP_RUNTIME_FILES` lists, runs the argv, streams the program's output, and exits with the -program's status. An artifact of a row a platform defines whose `execution` is -another system than this machine's, with no runner and no execution host -(`run_under`), is **built, not run**: `mcpp run` refuses it naming the keys that -state a runner, and `mcpp test` reports its tests as not run. +program's status. An artifact whose row's `execution` is another system than +this machine's, with no runner and no execution host (`run_under`), is +**built, not run**: `mcpp run` refuses it before anything is started, naming +the keys that state a runner, and `mcpp test` reports its tests as not run. +The rule is one for every row, built-in or defined by a platform +(2026.11.12.1+; before, a built-in row's artifact was started and the kernel's +answer reported). This machine's kernel does not read the system a program +names, and gives that system's call numbers its own meaning, so starting it +observes nothing. `--no-runner` states that the triple is native here, for a +host that runs such programs through a registered interpreter. An artifact of +this system and another architecture is launched as before. + +A provider may state the runner of its arrangement, so a target runs with +nothing declared (docs/35): `mcpp run --target aarch64-linux-gnu` on +linux-x86_64 installs the user-mode emulator on its first run and starts the +artifact under it, with the arrangement's base as the emulator's root. ## Running a distributable diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 645333d9c..db6b7f81f 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -736,6 +736,10 @@ Cygwin 的 shell 交给 `bin/.exe`,以该目录中的映像与自身的 openkal-platform 的不含 `windows-aarch64`,因此本 mcpp 能构建的 `aarch64-windows-gnu` 行不从它得到运行器)。在 Windows 主机上构建的映像与其他主机上的字节不同 (#850):树在 Linux 或 macOS 上打包。 +**`all` 所指的是系统表(mcpp 2026.11.12.1+)。** 表中陈述的每个系统(`[[platform.system]]`,内置的或平台的),加上平台为该 +架构定义了行的系统。没有程序可构建的对写为未覆盖并附原因,其余照常打包。引擎不持有约定的系统,其程序按平台运行器为该宿主 +对陈述的身份构建(`runner.built-as`,[35](35-platforms.md));不带 `--systems` 的打包只携带打包机自身系统的运行器。 + **在哪里运行。** 打包把 `runs_on` 写进 `mcpp-package.json`,并在 `Packed` 之后写出:`self` 为凭包内携带的东西即可 运行的 `-`,`with.` 为机器装有该运行器时可运行的组合(腿的架构 × 平台的 `runner.hosts`)。 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 424c4d437..6ec4be629 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -473,12 +473,16 @@ docs/22。 着;`aarch64-linux-gnu` 由 `linux-aarch64` 原生宿主服务。把它们 合并成 `linux`,会让一台的行覆盖掉另一台的行。 +`aarch64-linux-gnu` 还有第二种、交叉的安排(2026.11.12.1+):在 `linux-x86_64` 上,提供者 `glibc-sysroot` 以一个基座包 +服务它([22](22-target-side.md))。它排在 musl 的条目之下,所以 `--target aarch64-linux` 在那里仍由 `aarch64-linux-musl` +回答,写全身份才取 glibc 的交叉安排;`mcpp run` 经该提供者的运行器启动其产物([41](41-devices.md))。 + ### 构建机与它们服务的目标 | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| | `x86_64-linux-gnu` | verified | — | 载荷 | — | — | — | -| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | — | 载荷 | — | — | +| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | 基座包 | 载荷 | — | — | | `x86_64-linux-musl` | verified | `gcc@16.1.0` | 载荷 | 载荷 | — | 载荷 | | `aarch64-linux-musl` | verified | `gcc@16.1.0` | 载荷 | 载荷 | — | — | | `riscv64-linux-musl` | planned | — | planned | planned | planned | planned | diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index b68daabf1..cc651c25b 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -1110,10 +1110,18 @@ std-module-flags = ["-D__fenv_static="] 一个基座包是一个身份的 sysroot。平台以各提供者的 `sysroot` 为每个身份指名自己的基座([35](35-platforms.md));驱动能在 一个根内区分架构的系统(MinGW-w64)由一个包服务。 +**基座包只有一种读法(2026.11.12.1+)。** 它的清单陈述包根即 sysroot,所以不论其目录怎样命名,都按 sysroot 渲染 +(`--sysroot=<根>`);目录布局只对不带清单的目录探测。产物不由本机加载时——基座属于另一架构或另一系统——链接不取工具链 +载荷自身的运行时目录,动态产物以行的 `loader` 为程序解释器(`/lib/ld-linux-aarch64.so.1`),其 RUNPATH 不含本机的任何 +目录。这类产物经运行器运行,运行器以 `{sysroot}` 得到该基座([41](41-devices.md))。原生的动态产物仍写基座自己的加载器 +所在的路径。 + 索引中有 `xim:x86_64-freebsd-sysroot` 与 `xim:aarch64-freebsd-sysroot`(FreeBSD 的基本系统,附 libc++ 的模块源码), 以及 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、libunwind 与 compiler-rt,x86_64 与 aarch64)。在 `*-windows-gnu` 行上后者是 clang 的 sysroot;`mingw-llvm` 提供者指名它,因此每一台宿主都能构建 GNU ABI 的 -Windows 行([21](21-the-target-triple.md))。 +Windows 行([21](21-the-target-triple.md))。索引中还有 `xim:aarch64-linux-gnu-sysroot`(arm64 的 glibc,附内核头文件、 +带模块源码的 libc++ 与 compiler-rt),由 `glibc-sysroot` 提供者指名:在 linux-x86_64 上,`--target aarch64-linux-gnu` 以它 +构建 C、C++ 与 `import std` 的程序,动态或静态皆可(2026.11.12.1+)。 构建程序可以询问供给 sysroot 的是哪个 C 库**载荷**:`mcpp::target_libc()` 返回该包的名字,`mcpp::target_libc_profile()` 返回目标 ISA 档位对应的 @@ -1149,10 +1157,19 @@ WebAssembly 上,每个目标文件、预编译的标准库模块与链接必 | `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 进入依赖缓存键,因此没有启用某个成员的构建,不会 +三个成员都经由它们渲染的 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。 未知成员,以及表外的取值,都会被拒绝,并列出可取的值。 +**`longjmp = "wasm"` 使 C 的 `sigsetjmp`/`siglongjmp` 在 Emscripten 下可用。** 工具链默认的 JavaScript 模型把这一对编译成 +栈拷贝,越过两层调用的非局部退出会读到模块的栈之外,在运行时终止。编译与链接都带上 `-sSUPPORT_LONGJMP=wasm` 后,这一对 +可以往返。非局部退出很深的程序还需要更大的栈;那是项目自己的旋钮(`ldflags = ["-sSTACK_SIZE=..."]`),不是 ABI 事实, +所以引擎不渲染它。 + +**`longjmp` 与 `exceptions` 所指的是同一机制。** WebAssembly 的异常处理同时承载抛出与长跳转,JavaScript 模型也同时承载 +二者。未写 `longjmp` 时它随 `exceptions`,与 SDK 自身的缺省一致。为 `exceptions` 写一种模型、为 `longjmp` 写另一种的构建, +在任何编译之前被拒绝,并写出两处陈述。声明了的 `longjmp` 是配置键的一部分;未声明它的项目保持原有的键。 + **新程序应选 `"wasm"`。** 它的代码比 JavaScript 模型更小、更快,要求宿主支持 WebAssembly 异常处理:Safari 15.2、 Firefox 100、Chrome 95、Node 17 及以上。`true` 保持自 2026.9.12.3 起的含义,即 JavaScript 模型。依赖的 `requires_abi = { exceptions = true }` 要求的是"有异常"而不是某种模型,由根的选择满足。 diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index 19e435388..232182537 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -293,6 +293,42 @@ driver = "compiler" **`[platform.link] flags` 是给 ld.lld 的词(2026.11.10.1+)。** `driver` 为 `"compiler"` 的平台不把它们交给任何程序,构建 会说明这一点,并指名 `[platform.build] ldflags`——后者的词进入编译器的链接。 +**提供者陈述其产物怎样启动,以及一个系统叫什么(2026.11.12.1+)。** 提供者条目的三个键与一张表: + +```toml +[[platform.provider]] +serves = ["x86_64-haiku"] +tier = "preview" # 这一安排的档位;未写时为行的档位 +system-headers = "xim:haiku-headers@1.0" # 系统自身接口的头文件,供对它们编译的步骤读取 +runner = { cmd = ["dirun", "--profile", "{pkg:haiku-disk-image}/dirun.toml", "--", "{}"], + hosts = ["linux-x86_64", "macos-aarch64"], + requires = ["xim:dirun@0.2.0", "xim:haiku-disk-image@1.0"] } + +[[platform.system]] +name = "haiku" +uname = "Haiku" # `uname -s` 在该系统上的回答 +``` + +提供者的 `runner` 是运行器陈述的最低一级:平台的 `runner`、构建程序的、本机 `config.toml` 的、项目的 +`[target.] runner` 与 `MCPP_TARGET__RUNNER` 依次替换它。`hosts` 是该运行器所在的宿主(未写为任一宿主)。 +模板中 `{}` 是产物,`{sysroot}` 是产物链接时所用的基座,`{pkg:}` 是 `requires` 中该名字的包的根;模板的程序先在 +`requires` 的包中查找,再查 `PATH`。`requires` 的包由第一次需要它们的 `mcpp run` 或 `mcpp test` 安装,构建不安装。 + +一条提供者条目陈述一种载荷安排:原生安排与交叉安排是两条条目,各有自己的 `hosts`、基座与优先级。新增的条目不改变 +此前已有回答的请求,所以它以低于现有 `-` 回答的优先级进入 +([SPEC-016 §4](../specs/targets-and-providers.md))。 + +陈述了 `uname` 的系统可进入每个系统一个程序的单文件(`mcpp pack --pkg-format single-file`);未陈述的系统,其程序是 +文件开头的 PE。运行器为多个系统构建的平台,陈述各自的身份: + +```toml +[platform] +runner = { cmd = ["okrun", "{}"], hosts = ["linux-x86_64", "freebsd-x86_64"], + built-as = { "freebsd-x86_64" = "x86_64-freebsd-musl" } } +``` + +`mcpp pack --systems all` 取表中的全部系统,没有程序可构建的对写为未覆盖。 + `mcpp new <名> --template platform` 生成一个定义 `-<名>` 的平台包。读取平台清单时按行检查其语句:定义内置行、 同一行定义两次是错误,无论平台在何处被读取;不服务任何行的提供者、以同一优先级服务同一身份的两个提供者是平台自身构建的 警告。`mcpp why platform` 列出平台定义的行与所服务的身份,各附提供者与优先级:在平台目录中是其自身,在项目中是其目标所选 diff --git a/docs/zh/41-devices.md b/docs/zh/41-devices.md index d152a22b9..d41ac2cb2 100644 --- a/docs/zh/41-devices.md +++ b/docs/zh/41-devices.md @@ -137,8 +137,14 @@ freebsd-vm = { git = "https://example.org/freebsd-vm.git", tag = "v1" } ``` 该工具为本机构建,以上述 argv 与环境启动:它传送产物及 `MCPP_RUNTIME_FILES` 所列文件,执行 argv,转发程序的输出, -并以程序的退出码退出。平台定义的行中,`execution` 为本机以外系统的产物,若无 runner 也无执行主机(`run_under`), -即**已构建、未运行**:`mcpp run` 拒绝并给出声明 runner 的键,`mcpp test` 把其测试报告为未运行。 +并以程序的退出码退出。行的 `execution` 为本机以外系统的产物,若无 runner 也无执行主机(`run_under`), +即**已构建、未运行**:`mcpp run` 在启动任何进程之前拒绝,并给出声明 runner 的键,`mcpp test` 把其测试报告为未运行。 +这条规则对每一行相同,无论内置还是平台定义(2026.11.12.1+;此前内置行的产物会被启动,并报告内核的回答)。本机内核不读取 +程序所属的系统,另一系统的系统调用号在本机另有含义,所以启动它什么也观察不到。`--no-runner` 陈述该身份在本机可直接执行, +供经已登记的解释器运行此类程序的宿主使用。同一系统、另一架构的产物照旧启动。 + +提供者可以陈述其安排的运行器,于是目标无需任何声明即可运行(docs/35):在 linux-x86_64 上, +`mcpp run --target aarch64-linux-gnu` 在第一次运行时安装用户态模拟器,并以该安排的基座为模拟器的根启动产物。 ## 运行一个可分发物 diff --git a/modules/toolchain-model/src/target_select.cppm b/modules/toolchain-model/src/target_select.cppm index 506676fd2..395e8db6d 100644 --- a/modules/toolchain-model/src/target_select.cppm +++ b/modules/toolchain-model/src/target_select.cppm @@ -31,6 +31,11 @@ struct Candidate { int priority = 0; // after the overrides bool available = false; std::string why; // why it is not available here + // The tier of the arrangement: the provider entry's, else the row's. + std::string_view tier() const { + if (provider && !provider->tier.empty()) return provider->tier; + return row ? row->tier : std::string_view{}; + } }; struct Context { @@ -128,6 +133,11 @@ Selection select(std::string_view request, std::string_view arch, std::string_vi } else if (row.tier == "planned") { c.available = false; c.why = std::format("{} is planned", row.triple); + } else if (p->tier == "planned") { + // The arrangement's own tier (SPEC-016 §3): registered intent, + // not a payload that exists. + c.available = false; + c.why = std::format("{} of {} is planned", p->name, row.triple); } s.candidates.push_back(std::move(c)); } diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 43b7e4917..b21a703de 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -467,6 +467,7 @@ export struct TargetCandidate { std::string identity, provider, why; int priority = 0; bool available = false; + std::string tier; // of the arrangement: the provider entry's, else the row's }; export struct TargetSelection { diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 4a76a7553..0621cc8dd 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -2214,7 +2214,7 @@ std::expected phase13_finish(PrepareState& state) { for (auto const& k : c.candidates) s.candidates.push_back({ k.row ? std::string(k.row->triple) : std::string{}, k.provider ? std::string(k.provider->name) : std::string{}, - k.why, k.priority, k.available }); + k.why, k.priority, k.available, std::string(k.tier()) }); } if (!state.runUnder.empty()) { auto const& o = state.runUnderOrigin; diff --git a/src/doctor.cppm b/src/doctor.cppm index 27b6ed4eb..3d92d000b 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -1518,7 +1518,8 @@ nlohmann::json target_selection_json(const mcpp::build::BuildContext& ctx) { auto candidates = nlohmann::json::array(); for (auto const& c : s.candidates) candidates.push_back({ {"identity", c.identity}, {"provider", c.provider}, - {"priority", c.priority}, {"available", c.available}, {"why", c.why} }); + {"priority", c.priority}, {"available", c.available}, {"why", c.why}, + {"tier", c.tier} }); return { {"request", s.request}, {"identity", s.identity}, {"provider", s.provider}, {"priority", s.priority}, {"fallback", s.fallback}, {"why", s.why}, {"candidates", candidates} }; @@ -1661,8 +1662,8 @@ export int why_report(const std::string& topic, const std::string& features, if (!s.candidates.empty()) std::println(" candidates, in order:"); for (auto const& c : s.candidates) { const bool taken = c.identity == s.identity && c.provider == s.provider; - std::println(" {:<26} {:<14} {:>4} {}", c.identity, - c.provider.empty() ? std::string("(platform)") : c.provider, c.priority, + std::println(" {:<26} {:<14} {:>4} {:<9} {}", c.identity, + c.provider.empty() ? std::string("(platform)") : c.provider, c.priority, c.tier, taken ? std::string("taken") : c.available ? std::string("available") : "not available: " + c.why); } diff --git a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh index 68d27d593..5d5db3abb 100755 --- a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh +++ b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# requires: llvm elf +# requires: llvm elf x86_64-host source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" # 1076 -- the cross arrangement of aarch64-linux-gnu: its C library, C++ # library and compiler run-time come from a base package, its artifacts name @@ -19,16 +19,12 @@ source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" # 4. `mcpp run` starts each through the provider's runner, which this run # installs, and the program's exit status and output arrive. # -# The entry's hosts are linux-x86_64: elsewhere the script has nothing to hold -# and says so with the skip status. +# The entry's hosts are linux-x86_64, which the capability line states: on +# another host the script is excluded, and the run reports it as such. set -e t=$(mktemp -d); trap 'rm -rf "$t"' EXIT fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } -if [ "$(uname -s)" != Linux ] || [ "$(uname -m)" != x86_64 ]; then - echo "SKIP: the cross entry's hosts are linux-x86_64" - exit 77 -fi readelf="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readelf" T=aarch64-linux-gnu diff --git a/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh b/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh index 9e6bc0755..7d7756844 100755 --- a/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh +++ b/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh @@ -14,14 +14,12 @@ # -freebsd-musl` builds the program, exits 2 without running it, states # "built, not run", names both systems and prints the `[target.]` # runner declaration for the row; `mcpp test` of the same target reports the -# tests as built, not run. On a FreeBSD host the row is native and the check -# does not apply. +# tests as built, not run. set -e TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } -case "$(uname -s)" in FreeBSD) echo "SKIP: FreeBSD is this machine's system"; exit 77 ;; esac case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; *) ARCH=aarch64 ;; esac T="$ARCH-freebsd-musl" diff --git a/tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh b/tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh new file mode 100644 index 000000000..df856f589 --- /dev/null +++ b/tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# requires: gcc elf +# 1080 -- gcc over the host's C library payload names the payload on its link: +# the crt search prefix, the library directory and the loader (design +# 2026-11-12 C1; SPEC-006). +# +# A gcc whose recorded sysroot does not exist on this machine is linked +# "payload first": the link names the glibc payload's directory for the crt +# objects (`-B`), for the libraries (`-L`, with its run-time path) and for the +# loader. A rewrite of that branch of the flag renderer once dropped the whole +# group, and the crt objects then resolved from the host's /lib -- on a machine +# that had them; the hermetic link check refused the build. No test of the tree +# built with such a gcc, so the first report was a failed build elsewhere. +# +# gcc 15.1.0 records the sysroot of the machine that built it, which exists on +# no other; the engine's own gcc line (16.x) records the subos root and is +# linked through `--sysroot`. Criteria: a project built with gcc@15.1.0 links, +# its link line names the payload three times as above, and the program runs. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } + +cd "$t" +"$MCPP" new app > /dev/null 2>&1 +cd app +out=$("$MCPP" build --toolchain gcc@15.1.0 2>&1) || fail "the project did not build with gcc@15.1.0" "$out" +ninja=$(find target -name build.ninja | head -1) +[ -n "$ninja" ] || fail "no build.ninja" "$out" +line=$(grep -m1 -E '^ *ldflags *=' "$ninja") || fail "no link flags in build.ninja" "$(head -40 "$ninja")" +grep -q -- '-B[^ ]*xim-x-glibc' <<<"$line" || fail "the link names no crt search prefix in the payload" "$line" +grep -q -- '-L[^ ]*xim-x-glibc' <<<"$line" || fail "the link names no library directory in the payload" "$line" +grep -q -- '--dynamic-linker=[^ ]*xim-x-glibc' <<<"$line" || fail "the link names no loader in the payload" "$line" +if grep -q -- '--sysroot=' <<<"$line"; then + fail "this gcc was linked through a sysroot: the script holds the payload-first form" "$line" +fi +run=$("$MCPP" run --toolchain gcc@15.1.0 2>&1) || fail "the program did not run" "$run" +grep -q "Hello" <<<"$run" || fail "the program's output did not arrive" "$run" +echo "PASS: 1080 gcc over the host's payload links from the payload" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index f7294893b..eb2febeee 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -59,6 +59,11 @@ echo "MCPP_HOME: $MCPP_HOME" CAPS=() OS="$(uname -s)" +# x86_64-host: the machine's architecture, for a script that holds a provider +# whose `hosts` name one (the cross entry of aarch64-linux-gnu is published for +# linux-x86_64). A script that cannot hold anything elsewhere is excluded by +# this token, which the run reports; it does not pass by printing a line. +case "$(uname -m)" in x86_64|amd64) CAPS+=(x86_64-host) ;; esac case "$OS" in Linux) @@ -274,7 +279,7 @@ echo "Detected capabilities: ${CAPS[*]:-}" KNOWN_CAPS=(android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw mingw-cross mingw-host-headers msvc musl nasm no-msvc pack patchelf python3 qemu-arm qemu-riscv scan-deps symlink unix-shell windows wine - xlings-msvc) + x86_64-host xlings-msvc) bad_tokens=0 for tf in "$HERE"/[0-9]*.sh; do From 330b4000d31a0849c69081c54ecf646feb6be9cd Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 16:53:18 +0900 Subject: [PATCH 19/30] wip: bench leaves the per-commit run (main, dispatch, ci:bench) --- .github/workflows/ci-linux.yml | 8 ++++++++ .github/workflows/ci.yml | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 926c41840..cff74a76f 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -24,6 +24,13 @@ name: ci-linux on: workflow_call: + inputs: + # The engine's own costs are measured where the measurement is kept: a + # push to main, a run asked for them, a pull request labelled `ci:bench` + # (design 2026-11-12 §5.11). Every other run leaves them out. + bench: + type: boolean + default: false env: # MCPP_HOME pinned so the cache keys below restore into the same path @@ -100,6 +107,7 @@ jobs: # Bench cases; design 2026-11-10 §16): this commit's mcpp timed on a small # fixture, kept in bench/target/mtest beside the unit runs. - name: The engine's costs (bench, mtest Bench) + if: inputs.bench run: | cd bench && MCPP="$MCPP_FRESH" "$MCPP_FRESH" test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34a4f320b..fb0315174 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ on: pull_request: branches: [ main ] workflow_dispatch: + inputs: + bench: + description: "measure the engine's own costs (bench/tests)" + type: boolean + default: false # A superseded pull-request run is cancelled. Every push to main runs to the # end, in a group of its own commit: its build jobs are the only writers of the @@ -213,6 +218,10 @@ jobs: linux: needs: build-linux uses: ./.github/workflows/ci-linux.yml + with: + # A pull request's label is read when a push starts its run; a run + # started by hand states it as an input. + bench: ${{ github.event_name == 'push' || inputs.bench == true || contains(github.event.pull_request.labels.*.name, 'ci:bench') }} linux-e2e: needs: build-linux From eed19cbc1da53a7bf0b4d0b0cefcb0ddc2396002 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 16:57:06 +0900 Subject: [PATCH 20/30] wip: SPEC-013 v1.8, SPEC-014 v2.3, SPEC-002 v1.8 --- docs/specs/packaging.md | 15 ++++++++++----- docs/specs/platforms.md | 15 +++++++++++---- docs/specs/target-side.md | 7 ++++++- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md index 524d8c17c..436effc2e 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -4,8 +4,8 @@ |---|---| | 规范编号 | SPEC-014 | | 标题 | 打包与分发:链接、闭包、容器三个维度与构建腿,腿的合并,暂存树与包元数据,运行覆盖、单文件、容器列表、签名、压缩、合并与分发描述 | -| 状态 | 评审中 v2.2 | -| 版本 | 2.1 | +| 状态 | 评审中 v2.3 | +| 版本 | 2.3 | | 最后修改 | 2026-11-10 | | 对应实现 | mcpp >= 2026.10.31.1;v2.2 条款 mcpp >= 2026.11.10.1 | | 相关设计文档 | `.agents/docs/2026-10-30-ecosystem-part2-ide-and-language-service-design.md`(§19.6)、`.agents/docs/2026-10-29-ecosystem-target-sets-packaging-and-editors-design.md`(§4、§13)、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`(§2–§7、§14)、`.agents/docs/2026-10-28-platforms-design.md`(§4.7)、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md`(§2.2、§2.6) | @@ -225,14 +225,18 @@ openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` ### 4.4 `--systems` -`--systems` 的每个词为 ``(`linux`、`macos`、`windows`,取腿的全部架构)、`-`(只取该架构的腿)或 -`all`(三个系统);可重复,也可逗号分隔;缺省为本机系统;未给出时取 `[pack] systems`。对 `self-contained` 的树,为 +`--systems` 的每个词为 ``(系统表中的系统,取腿的全部架构)、`-`(只取该架构的腿)或 +`all`;可重复,也可逗号分隔;缺省为本机系统;未给出时取 `[pack] systems`。`all` 为系统表陈述的全部系统 +(SPEC-016 §2.3,内置的与平台的)加上平台为该架构定义了行的系统;其中没有程序可构建的对逐个写为未覆盖(§4.7), +不被略去也不使打包失败(**mcpp >= 2026.11.12.1**;此前为 `linux`、`macos`、`windows` 三个系统)。对 `self-contained` 的树,为 每个系统 × 腿的每个架构构建平台的载体(不携带任何内容,即运行器形式,SPEC-013 §11)并放入 `libexec//-/`;对映像的单文件,为每条腿与每个系统写一个文件(§5.2)。每个系统的程序在其行 (`-linux-musl`、`-macos`、`-windows-gnu`)上构建;平台的托管安排(SPEC-013 §5.4)服务该行时以它 构建,于是同一程序在任何打包的机器上相同,不需要该系统的 SDK 与 C 运行时;Linux 的程序为静态。托管安排不服务该行时 按惯常行构建,与本机系统和架构相同时取本机的行。本 mcpp 不构建程序的行(目标表标为 planned 或未列出)不放入包,并 -写出该系统与行。 +写出该系统与行。引擎只对 `linux`、`macos`、`windows` 持有"该系统的程序是哪一行"的约定;其余系统的程序是平台运行器 +为该宿主对陈述的身份(`runner.built-as`,SPEC-013 §10),无此陈述、平台也未为它定义行的系统没有程序 +(**mcpp >= 2026.11.12.1**)。 ### 4.5 `mcpp-package.json` @@ -486,3 +490,4 @@ openkal 目标的单文件:载体携带一条腿的映像本身(映像不引用 | 2.0 | 2026-10-30 | mcpp 2026.10.30.1(设计 2026-10-29 §4、§13):§1.4 腿只是命令命名的目标(目标集合按 SPEC-015 展开),腿按目标的种类与平台合并,原生程序每腿一个包,`-o <目录>/`;删除通用包与 `[platform] universal`、`[pack] universal`、`--no-universal` 的作用(迁移见 §2、docs/51);§1.2、§4.2 声明运行器的平台的映像只有一种布局(树),M3 只属于单文件,`--mode static` 为静态树;§4.3 Windows 启动器为引擎的程序,不再生成 `.cmd`,启动器为派生文件;§4.4 `--systems` 接受 ``、`-`、`all` 与 `[pack] systems`;§4.6 合并重新生成启动器、合并 `runs_on`、树以平台名命名;§4.7 运行覆盖 `runs_on` 与 `Runs on`、未覆盖的摘要,空覆盖被拒绝;§5.2 单文件覆盖腿 × 系统;§5.4 容器列表;§7 签名含 Windows 启动器;§8 条目按 `runs_on` 描述;§9 `pkgFormats`、`runsOn`;判据 3、4、7、9 改写,判据 10–13(e2e 983–987)。 | | 2.1 | 2026-10-31 | mcpp 2026.10.31.1(设计 2026-10-30 part 2 §19.6):§5.3 载体模型 `row` 与 `polyglot`,`--pkg-format polyglot` 的范围、布局、启动、缓存、签名与其他选项;§1.3、§1.5、§5.4、§7、§8 相应条目;`mcpp run --pkg-format` 直接运行 `single-file` 与 `polyglot`、拒绝 `tar`、`zip`、`dir`;§4.3 macOS 上带 quarantine 标记的运行器;判据 14(e2e 992)。 | | 2.2 | 2026-11-10 | mcpp 2026.11.10.1(设计 2026-11-10 E7,mcpp#911 N8):§1.2 另一系统的产物从文件读取闭包,不运行本机加载器;`--mode static` 链接全静态程序并在打包前核对。 | +| 2.3 | 2026-11-12 | mcpp 2026.11.12.1(设计 2026-11-12 C6、C7):§4.4 `--systems all` 取系统表,无程序可构建的对写为未覆盖;三个系统之外的程序按 `runner.built-as` 构建;单文件按系统表的 `uname` 选择程序(SPEC-016 §2.3)。 | diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md index 58db714ad..967d628da 100644 --- a/docs/specs/platforms.md +++ b/docs/specs/platforms.md @@ -4,9 +4,9 @@ |---|---| | 规范编号 | SPEC-013 | | 标题 | 平台:一类目标的可复用配置,其声明、选择、合并、对依赖图的补全、链接与封装、运行器与载体 | -| 状态 | 评审中 v1.7 | -| 版本 | 1.5 | -| 最后修改 | 2026-11-10 | +| 状态 | 评审中 v1.8 | +| 版本 | 1.8 | +| 最后修改 | 2026-11-12 | | 对应实现 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;`runner.hosts`、载体列表、`descriptor_keys` 与 `universal` 的弃用 mcpp >= 2026.10.30.1;§7.5 版本组参与解析、§14 平台系列与 §15.1 系列的锁 mcpp >= 2026.11.7.1;§14 内置平台为确切版本与提示 mcpp >= 2026.11.7.2;v1.7 条款 mcpp >= 2026.11.10.1;逐条标注 | | 相关设计文档 | `.agents/docs/2026-10-28-platforms-design.md`、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md` | | 相关 issue | mcpp#846、mcpp#847、mcpp#848 | @@ -312,7 +312,13 @@ plugin = "build.mcpp" | `cmd` | argv,非空;`{}` 为产物 | | `host` | `"same-arch"`:产物只在其架构的机器上运行;`"any"`:运行器可以从任何机器到达产物(模拟器、探针) | | `exclusive` | 为真时该目标的运行不重叠,`mcpp test` 串行运行 | -| `hosts` | 运行器程序存在的 `-`(`os` 为 `linux`、`macos`、`windows`):需要该运行器的包能在哪里运行(SPEC-014 §4.4 的 `runs_on.with`)。`host = "any"` 时与产物的架构无关。**mcpp >= 2026.10.30.1** | +| `hosts` | 运行器程序存在的 `-`(`os` 为系统表中的系统,SPEC-016 §2.3):需要该运行器的包能在哪里运行(SPEC-014 §4.4 的 `runs_on.with`)。`host = "any"` 时与产物的架构无关。**mcpp >= 2026.10.30.1** | +| `requires` | 运行器从哪些包安装(`xim:<名>@<版本>`)。第一次 `mcpp run` 或 `mcpp test` 时按工具链的同一方式安装;构建不安装。模板的程序先在这些包中查找,再查 PATH。替换 `cmd` 的陈述同时替换它。**mcpp >= 2026.11.12.1** | +| `built-as` | `{ "-" = "<身份>" }`:为该宿主对构建的运行器程序是哪个身份。自带运行器的包(SPEC-014 §4.4)对引擎不持有约定的系统按它构建。**mcpp >= 2026.11.12.1** | + +模板中 `{}` 为产物,`{sysroot}` 为该目标的基座(SPEC-006),`{pkg:}` 为 `requires` 中该名字的包的根 +(**mcpp >= 2026.11.12.1**)。运行器陈述的最低一级是所选提供者条目的 `runner`(SPEC-016 §6),其上依次为平台、构建程序、 +本机配置、项目与环境变量。 `host = "same-arch"` 而产物的架构不是本机架构时,`mcpp run` 与 `mcpp test` 拒绝运行,消息给出 `[target.] run_under` 的写法;`run_under` 存在或给出 `--no-runner` 时不拒绝。运行器的合并顺序与 §6 @@ -548,3 +554,4 @@ openkal-platform 的 `targets` 因此不含 WebAssembly。Web 经 `wasm32-emscri | 1.4 | 2026-11-07 | mcpp 2026.11.7.1(设计 2026-11-07 §4.1.4、§4.2.2 A2/A3):§7.5 版本组参与解析,解析后的检查保留为断言;§3.2 平台引用接受 SemVer 约束;§14 目标表的平台列为系列 `^0.24`,系列取引擎可读的最新版本;§15.1 系列从锁中读回,`mcpp update` 移动它;判据 5 改写、14 新增(e2e 1020、1021)。 | | 1.6 | 2026-11-09 | mcpp 2026.11.9.1(SPEC-016,mcpp#911):§1 `[[platform.define]]` 与 `[[platform.provider]]` 为一般形式,`[platform]`、`[platform.hosted]` 为其简写;§9.2 `driver = "compiler"` 在每一行生效,平台的产物名模式不被构建程序改变;§11 平台定义的系统上的载体与 `--systems`;主机对为任意系统词。 | | 1.7 | 2026-11-10 | mcpp 2026.11.10.1(设计 2026-11-10 E8,mcpp#911 N6):§4 `link.flags` 在 `driver = "compiler"` 时给出警告。 | +| 1.8 | 2026-11-12 | mcpp 2026.11.12.1(设计 2026-11-12 §5.2、§5.5):§10 运行器的 `requires` 与 `built-as`,模板占位 `{sysroot}`、`{pkg:}`,提供者的 `runner` 为陈述的最低一级;`hosts` 的系统取自系统表。 | diff --git a/docs/specs/target-side.md b/docs/specs/target-side.md index 94a5b6ef8..a6db8ce28 100644 --- a/docs/specs/target-side.md +++ b/docs/specs/target-side.md @@ -4,7 +4,7 @@ |---|---| | 规范编号 | SPEC-002 | | 标题 | 目标侧模型与能力声明(`mcpp:` 保留命名空间) | -| 状态 | 评审中 v1.7 | +| 状态 | 评审中 v1.8 | | 最后修改 | 2026-11-10 | | 对应实现 | mcpp >= 2026.8.24.2;§2.2 的对象形式与 §7 mcpp >= 2026.10.26.1 | | 相关设计文档 | `.agents/docs/2026-08-24-target-side-design.md`、`.agents/docs/2026-10-23-target-base-and-execution-host-design.md`、`.agents/docs/2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md` §6 | @@ -115,6 +115,10 @@ mcpp:<层名>[=<实现名>] 在 sysroot 的位置被指名(工程、本机配置、平台或提供者的 `sysroot`)、且根目录带 `mcpp.toml` 的预制目录是**基座包**。 它按普通包读取,不带角色标记: +- **一种读法(mcpp >= 2026.11.12.1)。** 清单陈述包根即 sysroot,所以基座包**必须**按 sysroot 渲染,不论其目录怎样 + 命名;目录布局(`include/features.h` 与 `lib64`,即生态自身的 glibc 包布局)只对不带清单的目录探测。产物不由本机 + 加载时的链接规则见 SPEC-016 §6。 + - `[package] provides` 回答它供给的层(`mcpp:c-abi=…`、`mcpp:c++-abi=…`、`mcpp:compiler-runtime=…`)与 `hosted-standard-library`;这些层的来源为 `prebuilt`,报告附基座包的 `<名>@<版本>`。 - `[build]` 的键与依赖图中供给者的含义相同,路径相对包根,**包根即 sysroot**: @@ -391,3 +395,4 @@ hosted 与否是 C 环境的一个声明:提供 `hosted-standard-library` 的 sy | v1.6 | 2026-11-10 | 随 mcpp 2026.11.10.1:新增 §2.5 基座包(按普通包读取:`provides` 回答层,`[build]` 陈述层,包根即 sysroot;新键 `compiler_runtime_dir`)与 §3.8 层供给者的需求不成环(`layer-cycle`)。 | | v1.7 | 2026-11-10 | 随 mcpp 2026.11.10.2:§3.4 写明段与 C 库名的对应,MSVC 载荷的 `c-abi` 回答 `ucrt`。 | | v1.2 | 2026-10-23 | 随 mcpp 2026.10.24.1:§1.3 `prebuilt` 放宽为被点名、按内容识别的预制目录;§2.4 改为由测量回答、`provides` 声明或核对,标为已实现;规则一增加基座是一个供给者与 c-abi 独占律;规则二增加基座上的编译器运行库与可测量的下限;规则三增加可叠加层;新增 §6 目标侧执行(组合、运行器契约、物理边界)。 | +| v1.8 | 2026-11-12 | 随 mcpp 2026.11.12.1:§2.5 基座包只有一种读法(带清单即包根 sysroot,布局只对无清单的目录探测)。 | From 6582b8e9e9ce1b4397296d7b8050b8dd4a21f6e9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 16:58:26 +0900 Subject: [PATCH 21/30] wip: design record section 15; the three-plane record is superseded --- ...system-architecture-optimization-design.md | 54 +++++++++++++++++-- ...hree-plane-platform-architecture-design.md | 16 ++++-- .agents/docs/README.md | 4 +- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md index 997928ab5..32f1ad0c1 100644 --- a/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md +++ b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md @@ -309,8 +309,9 @@ runner = { cmd = ["dirun", "--profile", "{pkg:freebsd-disk-image}/dirun.toml", | BP6 | 有运行器的身份:BP3–BP5 的产物运行,退出码与输出相符 | — | | BP7 | 产物里没有构建机的路径(`PT_INTERP`、`RUNPATH`、调试信息之外的字符串) | 烙入的加载器 | -BP1 与 BP2 的布局部分写进 xpkgindex-update 的 guard,对所有资产生效;BP3–BP7 是基座包专有的作业。**安装检查不可跳过:** -一个改了某个包的索引 PR,必须在该包声明的每个宿主上真实安装它一次;被跳过的安装检查视为失败。 +BP1、BP2 与 BP7 中仅凭安装后的目录树即可回答的部分,是索引仓库里每个基座包测试模块调用的布局契约 +(`tests/lib/base_package.py`);BP3–BP7 中需要引擎的部分是基座包专有的作业,随引擎发布之后进入。**配方自己的验证在 CI +中运行:** 改动直接编辑的配方,在安装作业的每个宿主上运行其模块的 L3 与 L4 测试(15.2)。 镜像一致性:CN 镜像滞后时客户端装到旧字节。修订通道的客户端在摘要不符时回落到另一个镜像并报告,而不是由每个 CI 作业 手工钉 GLOBAL。 @@ -555,7 +556,7 @@ openkal-opensbi 是写在 SBI 上的实现,提供核心三个接口加 `time` | C11 | 规范一处未改;中文文档不对等;三平面记录的"已成立"与实测不符 | 每项行为变更同 PR 改 SPEC 与两种语言;三平面记录第 3 节改写为引用门禁,或标为由本文取代 | mcpp | — | | C12 | 三个镜像包名 | `freebsd-disk-image` 为正名,另两个为别名 | xim | 别名保留一个周期 | | C13 | FreeBSD 运行器是手写路径 | 5.5 的运行器描述;`vm-run.py` 在 dirun 等价后退役;补 aarch64 | openkal-freebsd、openkal-platform | README 的旧写法继续可用 | -| C14 | 索引 PR 的安装检查可跳过 | 5.6:安装检查不可跳过;guard 增加布局检查 | xim、xpkgindex-update | — | +| C14 | 配方自己的安装后验证(L3、L4)在索引 CI 里只对一个包运行;安装作业只做安装与移除 | 安装作业对改动直接编辑的每个配方运行其模块的 L3、L4;基座包的布局契约(`tests/lib/base_package.py`)进入四个 sysroot 包的模块。第一版写的"安装检查被跳过"不成立,见 15.2 | xim | 编辑配方的 PR 多一步验证 | | C15 | dirun 自带测试模块;mcpplibs 下的归档空仓 | 测试改用 mtest;空仓的去留由负责人决定 | dirun | — | | C16 | 在评审规则未满足、检查未全绿时以管理员权限合入 | 见决定 D12 | 三个生态 | — | @@ -825,3 +826,50 @@ S1 与 S2 之间不插入新能力。S3 之后的每个新系统是对架构的 - **按改动选择对数据文件的处理**:内置数据编进引擎后,覆盖图能否把它与读它的用例对应起来,没有核对;5.11 因此把它 写成"影响全部目标类用例"的保守规则。 - **三个未定性的 CI 失败**(Windows 访问违例、xcode-27 的 1068、Windows 的 860)仍然没有根因。 + +## 15. 实施记录(第三版) + +本节随实施更新。每一项写明它在哪里、由哪个检查持有;本地测量标明主机。 + +### 15.1 已实施 + +| 项 | 位置 | 持有它的检查 | +|---|---|---| +| C1:GCC 分支恢复;基座的运行时目录同样到达 C 链接 | mcpp `flags.cppm` | `tests/unit/test_link_line_golden.cpp`;e2e 1080(在 mcpp#924 的旧头上失败) | +| C2、D1、P11、P12:`glibc` 与 `glibc-sysroot`(优先级 40)两条条目 | mcpp `builtin.toml` | 行表快照(`aarch64-linux` 在三种宿主上的回答不变,新候选排在末尾);e2e 295、1076 | +| C3、D3:不由本机加载的动态产物写行的 `loader`,无本机 RUNPATH;基座包单一读法 | mcpp `base.cpp`、`linkmodel.cppm`、`hermetic.cppm` | 金样测试;e2e 1076(解释器、RUNPATH、产物内无本机路径) | +| C4:sysroot 修订 3 | xlings-res、xim #1146 | 配方的安装钩子与 L4 测试;布局契约 | +| C5:1076 重写(C、C++、`import std`,动态与静态,经提供者的运行器运行);不再有"跳过即通过" | mcpp e2e | 能力词 `x86_64-host`:不适用的宿主被排除并由运行报告 | +| C6、D4:系统表(`uname`)、提供者的 `system-headers`、`tier`、`runner`;平台 `runner.built-as` | mcpp 模式、生成器与平台清单解析;openkal-platform 0.27.0 | 单元测试(polyglot、pack、清单);`check_system_names.py` | +| C7:`--systems all` 取系统表,无程序的对写为未覆盖 | mcpp `pack.cppm` | 单元测试;e2e 986、1078 | +| C8、D6:`abi.longjmp` 与 `exceptions` 不一致时拒绝;声明了才进键 | mcpp `toolchain.cpp`、`project.cppm` | e2e 1079 | +| C9、D25:另一系统的程序在启动前回答"已构建,未运行";mcpp#924 的子进程与信号归类已撤回 | mcpp `execute.cppm` | e2e 1077、1040 | +| C10:系统头文件一个谓词,取所选提供者的 `system-headers`,装不上即拒绝 | mcpp `windows_resources.cpp` | e2e 1075 | +| D8、D24:提供者的 `runner`,`requires` 在第一次运行时安装,占位 `{sysroot}`、`{pkg:}` | mcpp `platform.cpp`、`execute.cppm` | e2e 1076 | +| D13:安排的档位进入选择与 `mcpp why target` | mcpp `target_select.cppm` | 单元测试 | +| D18:bench 只在 main、手动与 `ci:bench` 运行 | mcpp `ci.yml`、`ci-linux.yml` | — | +| C14:配方自己的验证在索引 CI 中运行 | xim #1146 | `tests/test_ci_architecture.py` | +| P10:系统名只减不增 | mcpp `.github/tools/check_system_names.py` 与基线 | CI 的文档与仓库检查作业 | +| P17、5.13:七项重复实现的收敛 | mcpp | 收敛前后同一组单元测试(169 个程序、2627 个用例) | + +本地测量(x86_64 Linux,无 KVM,2026-11-12):交叉 `aarch64-linux-gnu` 的 C、C++ 头文件与 `import std` 程序各自构建并 +经 `mcpp run` 运行,退出码原样返回;`import std` 的静态链接同样。以修订 3 的 sysroot 测得。 + +### 15.2 实施中对本文的修正 + +| 本文原先的说法 | 实测 | 修正 | +|---|---|---| +| "链接模型需要一个新的单值结构"(5.4) | 单值已经存在:`TargetSide::cAbi.origin` 与 `ToolchainLinkModel`。缺陷是 mcpp#924 在渲染处加了第二处推断,以及带清单的基座走了布局探测的通道 | 不新增结构;删去第二处推断,基座解析处决定一次 | +| sysroot 修订 2"归档顶层目录与资产名不一致"是缺陷(2.2) | 该配方没有安装钩子,客户端的缺省解包认 `<名>-<版本>`,修订 2 因此装得正确;改为资产名之后,缺省解包反而把目录多套一层 | 修订 3 的顶层目录取资产名,并由配方的 `install()` 安装,与其余基座包一致。这个问题由 C14 新增的步骤在合入前拦下 | +| 修订 2 有四处缺陷 | 第五处:`libm.a` 是保留了安装期占位符的 ld 脚本,`libm.so` 又被改写为指向它,所以任何 C++ 链接与静态链接都解析不了 libm | 修订 3 以一条规则处理 `lib/` 下的全部 ld 脚本 | +| "索引 PR 的安装检查有 15 项被跳过"(2.2) | 被跳过的是按路径选择、与该改动无关的套件;安装作业在三个宿主上运行了。缺的是安装之后的验证:各配方模块的 L3、L4 在 CI 中只对一个包运行 | C14 改写;修订通道的 guard 不需要改动 | +| D5:mcpp#924 拆成多个 PR | 负责人要求每个仓库一个 PR | mcpp#924 续用,其内容按本文重做 | +| xim-pkgindex 一个 PR | 引擎的 CI 需要索引里已有修订 3;基座准入作业与运行器包又需要已发布的引擎 | 两个 PR:发布前(#1146),发布后 | +| 运行器模板的 `{image}` 占位(5.5) | 一个模板可以用到两个包 | `{pkg:}` | + +### 15.3 尚未实施 + +按第 10 节的阶段。S4 的 e2e 与 bench 向 mtest 的迁移、mtest 的 M1–M6、dirun 的整形、两个金丝雀与夜间矩阵、CI 的按改动选择与每 +宿主一道闸;S5 的 K7、DOS 的 A 路线、UEFI 平台包与单任务环境的规范明示;S6 的新系统;xim 的第二个 PR(基座准入作业 +BP3–BP7、wine、DOSBox、OVMF 的包、`-disk-image` 的正名);openkal-freebsd 的运行器改用 dirun;mcpp#924 三个未定性 +的 CI 失败的根因。 diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md index 9c8e0fb22..c5744579e 100644 --- a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -1,14 +1,22 @@ --- subject: architecture -status: active +status: superseded +superseded_by: 2026-11-12-ecosystem-architecture-optimization-design.md --- # 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) +Status: superseded by +[the ecosystem architecture optimization design](2026-11-12-ecosystem-architecture-optimization-design.md), +which extends sections 1, 2, 4 and 5 into four planes. Section 3 was written +before the round it describes was checked: its items 2 and 3 did not land in +the form stated there. A base package is read as a sysroot and the artifact +names the row's loader (not a loader "folded sysroot-relative" beside the +payload's own `-B/-L`), and an artifact of another system is not started at all +(not run as a child whose death is classified). The record of what was +implemented, with the check that holds each item, is section 15 of the +superseding design. ## 1. The statement diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 25ce1c30d..4f08c0b63 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -26,7 +26,7 @@ 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 +- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — superseded by [2026-11-12-ecosystem-architecture-optimization-design.md](2026-11-12-ecosystem-architecture-optimization-design.md) ### build @@ -193,7 +193,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 +- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — superseded by [2026-11-12-ecosystem-architecture-optimization-design.md](2026-11-12-ecosystem-architecture-optimization-design.md) - [生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings)](2026-11-12-ecosystem-architecture-optimization-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 From 35b854086d6d08f6dc92dcfa116888d3ba18eef5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 17:03:13 +0900 Subject: [PATCH 22/30] wip: the matrix declares the cross arrangement of aarch64-linux-gnu on linux-x86_64 --- tests/matrix/expected.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 1365e5843..83a73639f 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -295,7 +295,7 @@ payload linux-x86_64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14. payload linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported convention-unreplaced -payload linux-x86_64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve +payload linux-x86_64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu payload glibc(prebuilt) libc++(prebuilt) - ok none payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin From 3df4277cc096afa744a89608956a8e0d4c006d8c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 17:40:54 +0900 Subject: [PATCH 23/30] wip: the openkal targets' platform is openkal-platform 0.27.0 --- CHANGELOG.md | 2 ++ docs/21-the-target-triple.md | 2 +- docs/35-platforms.md | 2 +- docs/50-machine-output.md | 2 +- docs/specs/platforms.md | 2 +- docs/specs/targets-and-providers.md | 2 +- docs/zh/21-the-target-triple.md | 2 +- docs/zh/35-platforms.md | 2 +- docs/zh/50-machine-output.md | 2 +- modules/toolchain-model/data/builtin.toml | 6 +++--- ...025_the_default_platform_is_one_release.sh | 12 +++++------ ...ebsd_musl_identity_is_served_by_openkal.sh | 2 +- tests/fixtures/target-rows.snapshot | 20 +++++++++---------- tests/matrix/expected.tsv | 4 ++-- 14 files changed, 32 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b413f8d6..51a5ae7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,8 @@ program. - Android's rows state `execution = "android"`: the kernel is Linux and the system is not. +- The openkal targets' platform is openkal-platform 0.27.0 (from 0.26.0): the + same version group, with the runner's `built-as` statement for FreeBSD. ### Fixed diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index ccb9a00cf..e6710b23e 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -581,7 +581,7 @@ vocabulary, nothing wired yet. | `x86_64-windows-gnu` | Linux and Windows by the gcc payload; every host by the MinGW-w64 base package | one identity, host-split at the distribution layer; the `mingw-llvm` provider serves it with clang and `xim:mingw-w64-sysroot` (llvm-mingw's target side: MinGW-w64 on UCRT, libc++, compiler-rt) where no gcc payload exists | | `aarch64-windows-gnu` | every host | the MinGW-w64 base package alone; no gcc payload targets it | | `*-freebsd` | every host, through a platform | the rows are built-in; a platform names each identity's FreeBSD base package (`xim:x86_64-freebsd-sysroot`, `xim:aarch64-freebsd-sysroot`) and is selected with `[target.] platform`, or by any selector the row meets (`[target.freebsd]`, `cfg(os = "freebsd")`); another platform package's provider does not compete with the chosen platform | -| `*-freebsd-musl` | every host, through the graph | openkal-platform's hosted part (0.26.0): openkal-musl over openkal-freebsd standalone, one static program; a request of `-freebsd` with no platform selected for it takes this identity (2026.11.10.2+), as `aarch64-macos` takes the musl identity on a host that is not a Mac | +| `*-freebsd-musl` | every host, through the graph | openkal-platform's hosted part (0.27.0): openkal-musl over openkal-freebsd standalone, one static program; a request of `-freebsd` with no platform selected for it takes this identity (2026.11.10.2+), as `aarch64-macos` takes the musl identity on a host that is not a Mac | | `x86_64-windows-msvc` | Windows | MSVC is located on the machine | | `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 | diff --git a/docs/35-platforms.md b/docs/35-platforms.md index 3742c3ad0..adba6f9ba 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -203,7 +203,7 @@ manifest, ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.26.0" # a release, or a series: "openkal-platform@^0.26" +platform = "openkal-platform@0.27.0" # a release, or a series: "openkal-platform@^0.27" ``` and a build that fails while the default is in use and the synced index holds a diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 95f514930..1c4344c87 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -552,7 +552,7 @@ installation, which is located on the machine rather than installed by mcpp. A target row is `{target, note, toolchain, pin, status, default}`, and `status` is one of `installed` / `available` / `via dependency graph` / `planned`. A target row also carries `platform` (2026.10.27.1+): the platform package the -target table selects for it (`mcpplibs.openkal-platform@0.26.0` from 2026.11.10.2, `@0.25.0` in 2026.11.10.1, `@0.24.4` from 2026.11.8.1, `@0.24.3` in 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. +target table selects for it (`mcpplibs.openkal-platform@0.27.0` from 2026.11.12.1, `@0.26.0` in 2026.11.10.2, `@0.25.0` in 2026.11.10.1, `@0.24.4` from 2026.11.8.1, `@0.24.3` in 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. **`toolchain` and `pin` are not the same field twice.** `toolchain` is what the row is associated with — the installed payload on an installed row, the diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md index 967d628da..e5470ddbb 100644 --- a/docs/specs/platforms.md +++ b/docs/specs/platforms.md @@ -411,7 +411,7 @@ run_under = ["qemu-aarch64"] ## 14. 内置平台与目标表 宿主目标(Linux、macOS、Windows、Android、iOS、Emscripten)的约定留在引擎中,无平台包。目标表有平台列: -`x86_64-openkal`、`aarch64-openkal` 的值为一个确切的平台版本,即当前引擎验证过的版本(mcpp 2026.11.10.2 为 `mcpplibs.openkal-platform@0.26.0`), +`x86_64-openkal`、`aarch64-openkal` 的值为一个确切的平台版本,即当前引擎验证过的版本(mcpp 2026.11.12.1 为 `mcpplibs.openkal-platform@0.27.0`), 也是这两个目标的内置支持;选择来源报告为 `built-in`。项目可以以 `[target.] platform` 选择另一个版本或系列。 锁中记录的版本不满足内置版本时不被采用,报告写出采用的版本与锁中记录的版本。使用内置版本的构建失败、且已同步的索引中 有更新版本时,构建以一条 `tip` 写出两个版本与选择它的写法(mcpp >= 2026.11.7.2;2026.11.7.1 的值为系列 `^0.24`)。**已实现** diff --git a/docs/specs/targets-and-providers.md b/docs/specs/targets-and-providers.md index e1953b4c6..8c1fa9d91 100644 --- a/docs/specs/targets-and-providers.md +++ b/docs/specs/targets-and-providers.md @@ -58,7 +58,7 @@ - 内置行的 os 与 env 是解析器接受的词,与平台定义的行相同(v1.1)。 - 主流系统的两种身份都是内置数据(v1.1):系统自身 ABI 的行(`x86_64-freebsd`、`aarch64-freebsd`)没有内置提供者,由平台 提供;未选平台时,请求被拒绝并指名 `[target.<身份>] platform`。openkal 的 hosted 行(`x86_64-freebsd-musl`、 - `aarch64-freebsd-musl`)由 openkal 提供者经 openkal-platform 0.26.0 服务(openkal-freebsd 0.1.0 发布后,mcpp >= 2026.11.10.2)。 + `aarch64-freebsd-musl`)由 openkal 提供者经 openkal-platform 服务(0.26.0 起,openkal-freebsd 0.1.0 发布后,mcpp >= 2026.11.10.2;mcpp 2026.11.12.1 钉 0.27.0)。 - 32 位 hosted 行(`i686-linux-musl`、`i686-linux-gnu`、`i686-openkal`)为 planned(v1.1):请求按 planned 拒绝,而非按 未知目标;显式的 `[target.<身份>] toolchain` 可提前启用。 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 6ec4be629..56aa136f0 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -535,7 +535,7 @@ system · `系统` 在机器上被找到,不是 mcpp 安装的 · `SDK` 平台 | `x86_64-windows-gnu` | Linux 与 Windows 经 gcc 载荷;每一台宿主经 MinGW-w64 基座包 | 一个身份,只在分发层按宿主拆分;没有 gcc 载荷之处,`mingw-llvm` 提供者以 clang 与 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、compiler-rt)服务它 | | `aarch64-windows-gnu` | 每一台宿主 | 只由 MinGW-w64 基座包服务;没有 gcc 载荷以它为目标 | | `*-freebsd` | 每一台宿主,经平台 | 行是内置的;平台为每个身份指名 FreeBSD 基座包(`xim:x86_64-freebsd-sysroot`、`xim:aarch64-freebsd-sysroot`),以 `[target.<行>] platform` 或该行满足的任一选择器(`[target.freebsd]`、`cfg(os = "freebsd")`)选择;其他平台包的提供者不与所选平台竞争 | -| `*-freebsd-musl` | 每一台宿主,经依赖图 | openkal-platform 的 hosted 部分(0.26.0):openkal-freebsd 的 standalone 形态之上的 openkal-musl,一个静态程序;未为 `-freebsd` 选平台时,该请求取这一身份(2026.11.10.2+),正如非 Mac 宿主上的 `aarch64-macos` 取 musl 身份 | +| `*-freebsd-musl` | 每一台宿主,经依赖图 | openkal-platform 的 hosted 部分(0.27.0):openkal-freebsd 的 standalone 形态之上的 openkal-musl,一个静态程序;未为 `-freebsd` 选平台时,该请求取这一身份(2026.11.10.2+),正如非 Mac 宿主上的 `aarch64-macos` 取 musl 身份 | | `x86_64-windows-msvc` | Windows | MSVC 是在机器上被找到的 | | `x86_64-windows-musl` | payload 只在 Windows 上;图则任意宿主 | 没有 gcc 能发出 PE + musl,且 LLVM 拼不出这个三元组 | | `aarch64-macos` | macOS | SDK 是那台机器自己的 | diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index 232182537..59982cc71 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -168,7 +168,7 @@ error: openkal-musl is requested as '^0.27.0' by app, and the platform openkal-p ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.26.0" # 一个版本,或一个系列:"openkal-platform@^0.26" +platform = "openkal-platform@0.27.0" # 一个版本,或一个系列:"openkal-platform@^0.27" ``` 使用默认平台的构建失败、且已同步的索引中有更新版本时,最后输出一行 `tip:`,写出两个版本与上面这一行。 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index d92b7a778..37a71c175 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -488,7 +488,7 @@ mcpp toolchain list --format json 的形状是 `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned` 之一。 目标行另有 `platform`(2026.10.27.1+):目标表为该目标选择的平台包 -(2026.11.10.2 起为 `mcpplibs.openkal-platform@0.26.0`,2026.11.10.1 为 `@0.25.0`,2026.11.8.1 起为 `@0.24.4`,2026.11.7.2 为 `@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 +(2026.11.12.1 起为 `mcpplibs.openkal-platform@0.27.0`,2026.11.10.2 为 `@0.26.0`,2026.11.10.1 为 `@0.25.0`,2026.11.8.1 起为 `@0.24.4`,2026.11.7.2 为 `@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 **`toolchain` 与 `pin` 不是把同一个字段写了两遍。** `toolchain` 表示这一行 关联到什么 —— 已装的行是装好的载荷,词表行是它自己的约定。`pin` 只承载目标 diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index b8f0da8df..77a92a3ec 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -600,7 +600,7 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.26.0" +platform = "mcpplibs.openkal-platform@0.27.0" [[platform.provider]] name = "openkal" @@ -610,7 +610,7 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.26.0" +platform = "mcpplibs.openkal-platform@0.27.0" # The same arrangement on Windows, whose own interfaces a resource script # compiles against: their headers are the MinGW-w64 base's, which states them @@ -623,7 +623,7 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.26.0" +platform = "mcpplibs.openkal-platform@0.27.0" system-headers = "xim:mingw-w64-sysroot@20261104-2" # ── Systems ────────────────────────────────────────────────────────────────── diff --git a/tests/e2e/1025_the_default_platform_is_one_release.sh b/tests/e2e/1025_the_default_platform_is_one_release.sh index ee23800ac..4021aa86d 100755 --- a/tests/e2e/1025_the_default_platform_is_one_release.sh +++ b/tests/e2e/1025_the_default_platform_is_one_release.sh @@ -7,9 +7,9 @@ # # Criteria: # A. A project that states no platform builds `x86_64-openkal` over -# openkal-platform 0.26.0, from the target table, and mcpp.lock records it. +# openkal-platform 0.27.0, from the target table, and mcpp.lock records it. # B. A lock that records another release of it does not decide: the build -# says which it takes and which the lock recorded, and records 0.26.0. +# says which it takes and which the lock recorded, and records 0.27.0. # C. A platform the project states is taken as it is (a series here). set -e TMP=$(mktemp -d) @@ -30,20 +30,20 @@ PY # A "$MCPP" build --target x86_64-openkal > a.log 2>&1 || fail "A: the openkal build failed" a.log -grep -q 'Platform *openkal-platform 0.26.0' a.log || fail "A: the build does not use openkal-platform 0.26.0" a.log -[[ "$(locked)" == "0.26.0" ]] || fail "A: mcpp.lock does not record 0.26.0" mcpp.lock +grep -q 'Platform *openkal-platform 0.27.0' a.log || fail "A: the build does not use openkal-platform 0.27.0" a.log +[[ "$(locked)" == "0.27.0" ]] || fail "A: mcpp.lock does not record 0.27.0" mcpp.lock # B $PY - <<'PY' import re text = open("mcpp.lock").read() -text = re.sub(r'(\[package\."openkal-platform"\][^\[]*?version\s*=\s*")0\.26\.0(")', r'\g<1>0.24.3\2', text, count=1) +text = re.sub(r'(\[package\."openkal-platform"\][^\[]*?version\s*=\s*")0\.27\.0(")', r'\g<1>0.24.3\2', text, count=1) open("mcpp.lock", "w").write(text) PY [[ "$(locked)" == "0.24.3" ]] || fail "B: the fixture could not edit mcpp.lock" mcpp.lock "$MCPP" build --target x86_64-openkal > b.log 2>&1 || fail "B: the openkal build failed" b.log grep -q "default; mcpp.lock recorded v0.24.3" b.log || fail "B: the build does not say which release it takes over the lock's" b.log -[[ "$(locked)" == "0.26.0" ]] || fail "B: mcpp.lock was not rewritten to 0.26.0" mcpp.lock +[[ "$(locked)" == "0.27.0" ]] || fail "B: mcpp.lock was not rewritten to 0.27.0" mcpp.lock # C cat >> mcpp.toml <<'EOT' diff --git a/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh b/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh index 375d44460..132adc7b7 100755 --- a/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh +++ b/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh @@ -3,7 +3,7 @@ # 1073 -- FreeBSD's musl identity is served by openkal (design 2026-11-10 E4, K8, # K9; docs/21 'Build hosts and the targets they serve'). # -# `x86_64-freebsd-musl` is openkal-platform's hosted part (0.26.0): openkal-musl +# `x86_64-freebsd-musl` is openkal-platform's hosted part (0.26.0 on): openkal-musl # over openkal-freebsd in its standalone arrangement, linked as one static # program with no C library of the system beneath. A request of `x86_64-freebsd` # with no platform selected for it takes that identity, as `aarch64-macos` diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot index a575352da..1ae622d7a 100644 --- a/tests/fixtures/target-rows.snapshot +++ b/tests/fixtures/target-rows.snapshot @@ -113,7 +113,7 @@ row aarch64-windows-gnu descriptor-keys windows sysroot xim:mingw-w64-sysroot@20261104-2 row x86_64-windows-musl - table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-windows-musl llvm x86_64-w64-windows-gnu llvm@15.0 x86_64-w64-windows-gnu @@ -170,7 +170,7 @@ row aarch64-macos descriptor-keys macosx sysroot row aarch64-macos-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str aarch64-macos-musl llvm arm64-apple-macos14.0 llvm@15.0 arm64-apple-macos15.0 @@ -208,7 +208,7 @@ row riscv64-linux-musl descriptor-keys linux sysroot row i686-linux-musl - table tier=planned note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=planned note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str i686-linux-musl llvm i686-unknown-linux-musl llvm@15.0 i686-unknown-linux-musl @@ -246,7 +246,7 @@ row i686-linux-gnu descriptor-keys linux sysroot row i686-openkal - table tier=planned note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=planned note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str i686-openkal llvm i686-unknown-none-elf llvm@15.0 i686-unknown-none-elf @@ -284,7 +284,7 @@ row x86_64-macos descriptor-keys macosx sysroot row x86_64-macos-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-macos-musl llvm x86_64-apple-macos14.0 llvm@15.0 x86_64-apple-macos15.0 @@ -341,7 +341,7 @@ row aarch64-freebsd descriptor-keys freebsd sysroot row x86_64-freebsd-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-freebsd-musl llvm x86_64-unknown-freebsd14.0 llvm@15.0 x86_64-unknown-freebsd14.0 @@ -360,7 +360,7 @@ row x86_64-freebsd-musl descriptor-keys freebsd sysroot row aarch64-freebsd-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str aarch64-freebsd-musl llvm aarch64-unknown-freebsd14.0 llvm@15.0 aarch64-unknown-freebsd14.0 @@ -817,7 +817,7 @@ row wasm32-emscripten descriptor-keys emscripten linux sysroot row x86_64-openkal - table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-openkal llvm x86_64-unknown-none-elf llvm@15.0 x86_64-unknown-none-elf @@ -836,7 +836,7 @@ row x86_64-openkal descriptor-keys openkal linux sysroot row aarch64-openkal - table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str aarch64-openkal llvm aarch64-unknown-none-elf llvm@15.0 aarch64-unknown-none-elf @@ -971,7 +971,7 @@ set linux-musl -> x86_64-linux-musl aarch64-linux-musl | set windows -> x86_64-windows-gnu aarch64-windows-gnu | set windows-msvc -> x86_64-windows-msvc | set macos -> aarch64-macos | -set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.26.0 +set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.27.0 set none-elf -> riscv64-none-elf riscv32-none-elf aarch64-none-elf x86_64-none-elf i686-none-elf | set none-eabihf -> thumbv7em-none-eabihf thumbv8m.main-none-eabihf armv7a-none-eabihf | set android -> aarch64-linux-android x86_64-linux-android | diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 83a73639f..e9bb32510 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -85,10 +85,10 @@ # sysroot 的缺陷,修正后建出。 # # 2026.11.10.2 改变的格(设计 2026-11-10 K9): -# - `x86_64-freebsd-musl`、`aarch64-freebsd-musl` 由 openkal-platform 0.26.0 的 hosted 部分服务 +# - `x86_64-freebsd-musl`、`aarch64-freebsd-musl` 由 openkal-platform 0.27.0 的 hosted 部分服务 # (openkal-freebsd standalone 之上的 openkal-musl 0.29.2):payload 体系以 llvm 建出;gcc 与 msvc # 是 `capability-pin`;graph 体系里图中钉的版本在平台版本组之外,`platform-version-group`。 -# - openkal 各行的平台为 openkal-platform 0.26.0,openkal 列为 openkal-llvm-runtime@0.20.1。 +# - openkal 各行的平台为 openkal-platform 0.27.0,openkal 列为 openkal-llvm-runtime@0.20.1。 # # 中间五列是报告器输出,不参与支持判定。现有报告器把 aarch64-ios 的 # c-abi 输出为 glibc(payload),这不代表 iOS 产物实际链接 glibc。Apple From b7e66a2322e1d225fbdd703a47b6224e73ebdc41 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 17:45:23 +0900 Subject: [PATCH 24/30] wip: member test and e2e 254 follow the providers' data --- .../tests/test_target_select.cpp | 35 ++++++++++++++++++- .../e2e/254_pack_library_platform_coverage.sh | 22 +++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/modules/toolchain-model/tests/test_target_select.cpp b/modules/toolchain-model/tests/test_target_select.cpp index 17ced6af1..e2e6adf90 100644 --- a/modules/toolchain-model/tests/test_target_select.cpp +++ b/modules/toolchain-model/tests/test_target_select.cpp @@ -134,6 +134,39 @@ UTest an_arm_pattern_takes_the_standard_c_library{"TargetSelect/AnArmPatternTake expect(!s.fallback); const Ask written{"aarch64-linux-gnu", "aarch64", "linux", "gnu", true}; auto w = on("linux-aarch64", written); - expect(fatal(eq(w.candidates.size(), std::size_t(1)))); expect(eq(w.identity(), "aarch64-linux-gnu")); + expect(eq(provider_of(w), "glibc")); + expect(!w.fallback); +}}; + +// ONE ENTRY, ONE ARRANGEMENT; A NEW ENTRY CHANGES NO ANSWER (SPEC-016 §3, §4.6). +// The identity has a native arrangement and a cross one, as two entries. The +// cross entry stands below the entries that answer `aarch64-linux`, so that +// request keeps the answer it had before the entry existed, on every host, and +// only the whole identity reaches the cross arrangement. +UTest a_cross_arrangement_is_its_own_entry_and_changes_no_answer{"TargetSelect/ACrossArrangementIsItsOwnEntryAndChangesNoAnswer", [] { + const Ask written{"aarch64-linux-gnu", "aarch64", "linux", "gnu", true}; + // On its own architecture: the native entry, and the cross one is a + // candidate that host does not have. + auto native = on("linux-aarch64", written); + expect(fatal(eq(native.candidates.size(), std::size_t(2)))); + expect(eq(provider_of(native), "glibc")); + expect(bool(native.candidates[0].available)); + expect(eq(native.candidates[1].provider->name, "glibc-sysroot")); + expect(!native.candidates[1].available); + // From x86_64: the whole identity takes the cross entry, with its base. + auto cross = on("linux-x86_64", written); + expect(eq(cross.identity(), "aarch64-linux-gnu")); + expect(eq(provider_of(cross), "glibc-sysroot")); + expect(fatal(cross.provider != nullptr)); + expect(eq(cross.provider->sysroot, "xim:aarch64-linux-gnu-sysroot@2.44.3")); + expect(eq(cross.provider->linkage, "dynamic")); + expect(!cross.provider->runner.empty()); + // The request without an env is answered as before the entry existed. + auto plain = on("linux-x86_64", {"aarch64-linux", "aarch64", "linux", "gnu"}); + expect(eq(plain.identity(), "aarch64-linux-musl")); + expect(eq(provider_of(plain), "musl-gcc")); + // Elsewhere neither arrangement exists, and the answer is the graph's identity. + auto mac = on("macos-aarch64", {"aarch64-linux", "aarch64", "linux", "gnu"}); + expect(eq(mac.identity(), "aarch64-linux-musl")); }}; diff --git a/tests/e2e/254_pack_library_platform_coverage.sh b/tests/e2e/254_pack_library_platform_coverage.sh index 69e2f6b95..45b0dc1db 100755 --- a/tests/e2e/254_pack_library_platform_coverage.sh +++ b/tests/e2e/254_pack_library_platform_coverage.sh @@ -22,11 +22,15 @@ # # So the silence is as much the feature as the warning, and both are asserted. # -# THE PER-HOST PICKS BELOW MIRROR host_can_serve (registry.cppm:542-566). -# That is deliberate and it is a tripwire: if mcpp ever gains, say, a -# macOS-hosted Windows toolchain, this test starts failing — which is the -# correct outcome, because the table it encodes will have changed and the -# expectations here have to be re-derived rather than assumed. +# THE PER-HOST PICKS BELOW MIRROR THE PROVIDERS' `hosts` (data/builtin.toml, +# which `host_can_serve` reads since 2026.11.12.1). That is deliberate and it +# is a tripwire: when a provider's hosts change, this test starts failing — +# which is the correct outcome, because the table it encodes will have changed +# and the expectations here have to be re-derived rather than assumed. It +# tripped once, as predicted: the `mingw-llvm` provider (2026.11.10.1) serves +# the GNU-ABI Windows rows on every host, a macOS host included, and the +# function this test mirrored then still said a macOS host builds for macOS +# alone. set -e TMP=$(mktemp -d) @@ -79,10 +83,10 @@ echo "host platform: $HOST_OS (from target/$host_triple)" case "$HOST_OS" in linux) UNSERVABLE=macos SERVABLE_UNPACKED=windows ;; windows) UNSERVABLE=macos SERVABLE_UNPACKED=linux ;; - # macOS serves exactly one target — "macOS has no Linux-targeting payload at - # all", and PE needs a Windows or Linux host — so there is no - # servable-but-unpacked platform to name here. Structural, not a gap. - macos) UNSERVABLE=windows SERVABLE_UNPACKED= ;; + # macOS has no Linux-targeting payload (a Linux identity is the graph's + # there, which is not this question), and it builds PE through the + # MinGW-w64 base package as every host does. + macos) UNSERVABLE=linux SERVABLE_UNPACKED=windows ;; esac # ── 1. packed, not declared ───────────────────────────────────────────── From 65ec20f2fea59eb73a3bad2cc0cc1bc689d3a3ed Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 17:58:35 +0900 Subject: [PATCH 25/30] wip: e2e 986 holds FreeBSD's single files --- tests/e2e/986_single_files_cover_the_legs_and_systems.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh index 0571bfee9..6caf40108 100644 --- a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh +++ b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh @@ -54,9 +54,13 @@ echo "ok: --systems - makes that pair's file" # C if [ "$OS" = linux ]; then "$MCPP" pack --target openkal --pkg-format single-file --systems all -o all/ > c.log 2>&1 || fail "C: --systems all" c.log - for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64; do + # `all` is the system table (SPEC-016 §2.3): FreeBSD is a system of it, and + # the platform's runner states the identity its FreeBSD program is built as + # (`runner.built-as`, openkal-platform 0.27.0), so its files are made too. + for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64 freebsd-x86_64 freebsd-aarch64; do ls all/sf-0.1.0-$pair* > /dev/null 2>&1 || fail "C: no file for $pair" c.log done + # A pair no program is built for is written, not left out. grep -q "Not covered macos-x86_64 (no carrier row" c.log || fail "C: the pair without a row is not said" c.log echo "ok: --systems all makes every file this mcpp can and says the rest" fi From 16999761a787ab27ca3280fad4705e13e553705f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 18:08:49 +0900 Subject: [PATCH 26/30] wip: the musl identities of macOS stay listed on a macOS host, as graph-served --- src/toolchain/lifecycle.cppm | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index e7c0932ae..30bde9149 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -16,6 +16,8 @@ import mcpp.toolchain.detect; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; import mcpp.toolchain.post_install; import mcpp.ui; import mcpp.wire; @@ -754,9 +756,22 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // Apple SDK is not redistributable, so none can, which makes every // Apple row like `aarch64-macos` and unlike `x86_64-windows-musl`. // Stating it removes the reliance on an empty field. + // + // + // AND AN APPLE ROW A PLATFORM SERVES IS NOT AN SDK'S (2026.11.12.1): the + // musl identities of macOS take their system from the platform's + // packages and need no SDK. They are listed where their system is this + // machine's, as they were while `host_can_serve` answered every Apple + // row by the host's system -- then as if a payload produced them, now + // as what the data says, the graph's. (They build on every host; the + // list and the matrix name them on a macOS host only, as before.) + const bool platformServesHere = t->system_is_host() && std::ranges::any_of( + mcpp::toolchain::target::providers(), [&](auto const* p) { + return !p->platform.empty() && mcpp::toolchain::target::serves(*p, info.canonical); + }); bool graphCouldServe = false; if (!planned && !installable_here(*t) && !info.pin.empty() - && !t->is_apple()) { + && (platformServesHere || !t->is_apple())) { auto at = info.pin.find('@'); auto fam = info.pin.substr(0, at == std::string_view::npos ? info.pin.size() : at); From 6fc353ffde06101d6efa351f7c0c0b196d9ef10d Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 17:20:13 +0900 Subject: [PATCH 27/30] wip: a script that skips itself is a skip (exit 77, mtest skip_exit); coverage debt list --- .github/tools/check_e2e_coverage.py | 21 +++++++++++++--- ...an_external_toolchain_finds_mcpps_ninja.sh | 6 ++--- tests/e2e/100_cppfly_reflection.sh | 2 +- ...he_std_module_takes_the_platforms_flags.sh | 2 +- tests/e2e/101_cppfly_llvm_soft.sh | 2 +- ...projects_state_directory_ignores_itself.sh | 4 ++-- ..._warning_knows_what_the_last_build_took.sh | 2 +- ..._image_runs_on_the_system_that_built_it.sh | 2 +- tests/e2e/104_asm_sources_gas.sh | 2 +- ...chain_section_is_not_the_build_programs.sh | 2 +- ...c_library_is_up_before_its_initialisers.sh | 2 +- tests/e2e/105_asm_sources_nasm.sh | 2 +- ...rries_the_runners_the_platform_declares.sh | 4 ++-- ...scripten_rows_longjmp_model_round_trips.sh | 3 +-- tests/e2e/108_cfg_conditional_sources.sh | 2 +- .../130_freestanding_riscv_build_and_run.sh | 2 +- ...31_freestanding_bsp_supplies_everything.sh | 4 ++-- .../132_freestanding_test_and_artifacts.sh | 4 ++-- tests/e2e/133_freestanding_std_subset.sh | 8 +++---- tests/e2e/13_toolchain_pin.sh | 2 +- .../e2e/150_clang_module_operator_template.sh | 4 ++-- tests/e2e/156_nested_ld_library_path.sh | 2 +- tests/e2e/163_identity_first_resolution.sh | 2 +- ..._bare_name_cross_namespace_wire_address.sh | 2 +- tests/e2e/166_run_env_no_private_glibc.sh | 2 +- tests/e2e/168_build_mcpp_musl_host_static.sh | 4 ++-- tests/e2e/201_gcc_no_specs_pollution.sh | 2 +- .../208_private_libc_stays_in_the_binary.sh | 2 +- .../209_runtime_verdict_never_reruns_green.sh | 2 +- tests/e2e/220_farm_only_library_runs.sh | 2 +- tests/e2e/230_bench_harness.sh | 2 +- tests/e2e/231_jobs_option.sh | 2 +- tests/e2e/234_bmi_schedule_on.sh | 4 ++-- tests/e2e/235_std_object_only_when_needed.sh | 2 +- ...236_module_extensions_default_toolchain.sh | 2 +- tests/e2e/237_default_subos_without_info.sh | 6 ++--- .../238_c_only_unit_links_with_c_driver.sh | 2 +- tests/e2e/239_msvc_managed_toolset.sh | 2 +- ...283_run_target_flag_owns_its_cache_slot.sh | 4 ++-- ..._from_graph_keeps_the_payload_c_library.sh | 2 +- .../e2e/286_the_openkal_stack_still_builds.sh | 2 +- ...87_the_openkal_stack_crosses_to_aarch64.sh | 4 ++-- ...e_openkal_stack_on_a_machine_with_no_os.sh | 4 ++-- ...9_one_host_reaches_every_openkal_target.sh | 2 +- tests/e2e/28_target_static.sh | 2 +- ...eclaration_puts_an_environment_in_front.sh | 4 ++-- ...ayer_does_not_lose_the_targets_compiler.sh | 2 +- ...target_and_the_resolved_one_name_one_os.sh | 2 +- ...hat_can_be_built_not_what_has_a_payload.sh | 6 ++--- ..._naming_the_host_target_changes_nothing.sh | 2 +- ...report_names_is_what_the_link_line_uses.sh | 2 +- ...97_a_capability_pin_is_not_a_preference.sh | 6 ++--- ...ding_a_convention_requires_replacing_it.sh | 6 ++--- ...ompiler_is_taken_and_nothing_is_written.sh | 2 +- tests/e2e/31_transitive_deps.sh | 2 +- tests/e2e/332_cortex_m_builds_and_boots.sh | 6 ++--- .../e2e/333_named_runners_locked_and_sbom.sh | 2 +- .../334_dependency_declared_tool_is_found.sh | 2 +- tests/e2e/336_armv7a_builds_and_boots.sh | 6 ++--- tests/e2e/338_cortex_m_picolibc_sysroot.sh | 6 ++--- tests/e2e/36_llvm_toolchain.sh | 2 +- tests/e2e/37_llvm_import_std.sh | 6 ++--- tests/e2e/38_llvm_modules.sh | 6 ++--- tests/e2e/39_llvm_incremental.sh | 4 ++-- tests/e2e/40_llvm_bmi_cache.sh | 6 ++--- tests/e2e/41_llvm_std_compat.sh | 6 ++--- tests/e2e/47_cdb_prebuilt_module_path_abs.sh | 2 +- tests/e2e/47_llvm_atomic_link.sh | 8 +++---- tests/e2e/650_wasm_row_names_its_launcher.sh | 2 +- ...653_exceptions_is_the_second_abi_member.sh | 3 +-- ...nder_the_web_row_uses_the_host_compiler.sh | 2 +- .../e2e/65_toolchain_runtime_dirs_for_run.sh | 4 ++-- .../684_a_runner_supplied_twice_is_refused.sh | 2 +- ..._dependency_selects_a_repository_member.sh | 2 +- ...compat_dependency_is_not_a_refresh_miss.sh | 2 +- ...lied_target_closes_the_hosts_own_search.sh | 2 +- ...t_target_is_target_keyed_not_host_keyed.sh | 2 +- tests/e2e/760_msvc_toolset_is_chosen_once.sh | 2 +- ...a_graph_link_searches_no_host_directory.sh | 2 +- tests/e2e/86_llvm_hermetic_link.sh | 6 ++--- tests/e2e/875_toolchain_by_path.sh | 2 +- tests/e2e/876_toolchain_phase.sh | 2 +- ...an_unservable_target_is_still_installed.sh | 2 +- ...1_a_host_header_is_written_not_searched.sh | 2 +- ...an_older_packaging_is_used_not_replaced.sh | 4 ++-- .../925_a_stated_sysroot_is_the_c_library.sh | 4 ++-- tests/e2e/926_the_hosts_own_root_is_a_base.sh | 2 +- ...928_an_apple_sdk_is_located_on_any_host.sh | 2 +- .../930_a_toolchain_phase_states_the_base.sh | 6 ++--- tests/e2e/933_one_cxx_runtime_per_process.sh | 6 ++--- ...34_llvm_with_a_graph_supplied_libstdcxx.sh | 6 ++--- ...8_a_relink_never_exposes_a_partial_file.sh | 2 +- ...ok_image_is_built_once_and_run_by_okrun.sh | 2 +- .../951_an_ok_image_needs_no_declaration.sh | 2 +- ...ost_graph_runs_build_programs_as_images.sh | 2 +- ...l_set_is_one_tree_of_every_architecture.sh | 4 ++-- ..._packages_merge_and_describe_themselves.sh | 2 +- ...tecture_runs_through_its_execution_host.sh | 2 +- ..._image_in_a_program_of_its_architecture.sh | 2 +- ...ery_member_and_runs_where_it_is_reached.sh | 2 +- ...64_tests_built_once_run_from_an_archive.sh | 2 +- ...iles_level_and_the_wasm_exception_model.sh | 3 +-- .../e2e/983_a_pack_holds_the_legs_it_names.sh | 4 ++-- ..._a_set_of_programs_is_a_package_per_leg.sh | 4 ++-- ...single_files_cover_the_legs_and_systems.sh | 4 ++-- ...87_the_universal_spellings_are_migrated.sh | 2 +- ...r_finds_its_package_from_a_windows_path.sh | 2 +- tests/e2e/98_reflection_import_std.sh | 2 +- ...glot_file_runs_on_every_system_it_holds.sh | 4 ++-- .../994_the_contract_names_the_target_side.sh | 2 +- ...uncher_builds_over_any_platform_release.sh | 2 +- ..._an_ok_image_waits_for_a_prepare_action.sh | 2 +- .../999_debug_information_is_stored_once.sh | 2 +- tests/e2e/coverage-debt.tsv | 5 ++++ tests/e2e/mtest.toml | 3 +++ tests/e2e/run_all.sh | 20 +++++++++++++--- tests/scripts/test_check_e2e_coverage.py | 24 ++++++++++++++++++- 117 files changed, 236 insertions(+), 180 deletions(-) create mode 100644 tests/e2e/coverage-debt.tsv diff --git a/.github/tools/check_e2e_coverage.py b/.github/tools/check_e2e_coverage.py index 077d34521..1f0253179 100644 --- a/.github/tools/check_e2e_coverage.py +++ b/.github/tools/check_e2e_coverage.py @@ -29,10 +29,16 @@ and neither does a longer name that contains it; excused tests/e2e/coverage-exceptions.tsv lists it with the reason no hosted runner can run it; + owed tests/e2e/coverage-debt.tsv lists it: it skips itself on every + host that starts it (a script leaves with 77 after a `SKIP:` line + when it can hold nothing on a machine, and the run reports it as a + skip). The list is a debt, and it only shrinks: each entry names + what a host lacks, and is removed when a host gains it; uncovered none of these. The check fails. -An exception for a test that ran, or for a test that does not exist, also fails -the check, so that the list cannot outlive the reason it records. +An exception or a debt entry for a test that ran, or for a test that does not +exist, also fails the check, so that neither list can outlive the reason it +records. Some claims need a test to pass on particular hosts, not on some host: docs/21 calls a target row `verified` when CI builds and runs its artefact, and the @@ -133,6 +139,7 @@ def main() -> int: workflows = workflow_tokens([p.read_text(encoding="utf-8") for p in sorted((root / ".github" / "workflows").glob("*.yml"))]) exceptions = read_exceptions(root / "tests" / "e2e" / "coverage-exceptions.tsv") + debt = read_exceptions(root / "tests" / "e2e" / "coverage-debt.tsv") required = read_host_requirements(root / "tests" / "e2e" / "host-requirements.tsv") ran: dict[str, set[str]] = defaultdict(set) @@ -165,17 +172,25 @@ def main() -> int: counts["ran"] += 1 if test in exceptions: stale.append(f"{test} is excused but ran on {', '.join(sorted(ran[test]))}") + if test in debt: + stale.append(f"{test} is listed as owed but ran on {', '.join(sorted(ran[test]))}: " + f"remove it from coverage-debt.tsv") elif test in unaffected: counts["unaffected"] += 1 elif named_by_a_workflow(test, workflows): counts["job"] += 1 elif test in exceptions: counts["excused"] += 1 + elif test in debt: + counts["owed"] += 1 else: uncovered.append(f"{test}: {'; '.join(sorted(skipped[test])) or 'in no report'}") for test in exceptions: if test not in tests: stale.append(f"{test} is excused but does not exist") + for test in debt: + if test not in tests: + stale.append(f"{test} is listed as owed but does not exist") unmet = [] for test, hosts in sorted(required.items()): if test not in tests: @@ -188,7 +203,7 @@ def main() -> int: print(f"{len(tests)} tests: {counts['ran']} ran on a shard, {counts['unaffected']} unaffected by " f"the change, {counts['job']} run by a dedicated job, {counts['excused']} excused, " - f"{len(uncovered)} uncovered") + f"{counts['owed']} owed, {len(uncovered)} uncovered") for (host, shard), ms in sorted(shard_ms.items()): print(f" {host} shard {shard}: {ms / 60000:.1f} min of tests") for line in uncovered: diff --git a/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh b/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh index c3e3ef0c5..3194e67bc 100755 --- a/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh +++ b/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh @@ -19,14 +19,14 @@ source "$(dirname "$0")/_inherit_toolchain.sh" fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } gxx=$(ls -d "$MCPP_HOME"/registry/data/xpkgs/xim-x-gcc/*/bin/g++ 2>/dev/null | head -1) -[ -n "$gxx" ] || { echo "SKIP: no gcc payload to reach from outside the store"; exit 0; } +[ -n "$gxx" ] || { echo "SKIP: no gcc payload to reach from outside the store"; exit 77; } ninja=$(ls -d "$MCPP_HOME"/registry/data/xpkgs/xim-x-ninja/*/ninja 2>/dev/null | head -1) -[ -n "$ninja" ] || { echo "SKIP: mcpp's home holds no ninja"; exit 0; } +[ -n "$ninja" ] || { echo "SKIP: mcpp's home holds no ninja"; exit 77; } ln -s "$(dirname "$(dirname "$gxx")")" "$TMP/external-gcc" # The xlings mcpp drives, named outright: the PATH below holds no xlings. export MCPP_VENDORED_XLINGS="${MCPP_VENDORED_XLINGS:-$(command -v xlings || true)}" -[ -n "$MCPP_VENDORED_XLINGS" ] || [ -x "$MCPP_HOME/registry/bin/xlings" ] || { echo "SKIP: no xlings to name"; exit 0; } +[ -n "$MCPP_VENDORED_XLINGS" ] || [ -x "$MCPP_HOME/registry/bin/xlings" ] || { echo "SKIP: no xlings to name"; exit 77; } # A PATH with the usual tools and no ninja. An external toolchain is complete # where it is installed (MSYS2's GCC carries its assembler in its own `bin'); diff --git a/tests/e2e/100_cppfly_reflection.sh b/tests/e2e/100_cppfly_reflection.sh index 38cb52f41..90453e142 100755 --- a/tests/e2e/100_cppfly_reflection.sh +++ b/tests/e2e/100_cppfly_reflection.sh @@ -19,7 +19,7 @@ USER_MCPP="${MCPP_HOME:-$HOME/.mcpp}" GXX=$(find "$USER_MCPP/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1) if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then echo "SKIP-INLINE: no -freflection-capable gcc payload available" - exit 0 + exit 77 fi GCC_VER=$(basename "$(dirname "$(dirname "$GXX")")") diff --git a/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh b/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh index a6f15ad05..b80cf29bb 100755 --- a/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh +++ b/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh @@ -15,7 +15,7 @@ # that imports std builds and runs with it, and the module's command names it. # The flag is the x86_64 one, so the test asks it of an x86_64 machine only. set -e -case "$(uname -m)" in x86_64|amd64) ;; *) echo "SKIP: -fcf-protection is an x86_64 flag"; exit 0 ;; esac +case "$(uname -m)" in x86_64|amd64) ;; *) echo "SKIP: -fcf-protection is an x86_64 flag"; exit 77 ;; esac source "$(dirname "$0")/_toolchain_env.sh" TMP=$(mktemp -d) diff --git a/tests/e2e/101_cppfly_llvm_soft.sh b/tests/e2e/101_cppfly_llvm_soft.sh index d846caea5..b2e17936e 100755 --- a/tests/e2e/101_cppfly_llvm_soft.sh +++ b/tests/e2e/101_cppfly_llvm_soft.sh @@ -11,7 +11,7 @@ trap "rm -rf $TMP" EXIT source "$(dirname "$0")/_llvm_env.sh" if [[ ! -d "$LLVM_ROOT" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION:-none} is not installed" - exit 0 + exit 77 fi export MCPP_HOME="$TMP/mcpp-home" diff --git a/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh b/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh index 7550ab073..f9a67cbf1 100755 --- a/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh +++ b/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh @@ -10,7 +10,7 @@ # Criterion: after a build in a repository whose .gitignore names only # target/, `git status` lists nothing under .mcpp/, and `.mcpp/.gitignore` is `*`. set -e -command -v git > /dev/null || { echo "SKIP: no git on this machine"; exit 0; } +command -v git > /dev/null || { echo "SKIP: no git on this machine"; exit 77; } TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } @@ -22,7 +22,7 @@ printf 'int main() { return 0; }\n' > src/main.cpp printf 'target/\n' > .gitignore git init -q . "$MCPP" build > build.log 2>&1 || fail "the build failed" build.log -[ -d .mcpp ] || { echo "SKIP: this build wrote no state directory"; exit 0; } +[ -d .mcpp ] || { echo "SKIP: this build wrote no state directory"; exit 77; } grep -qx '\*' .mcpp/.gitignore || fail ".mcpp/.gitignore does not ignore everything" .mcpp/.gitignore git status --porcelain --untracked-files=all > status.txt grep -q '^?? \.mcpp/' status.txt && fail "version control sees the state directory" status.txt diff --git a/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh b/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh index 4424e2511..2dcaece3b 100755 --- a/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh +++ b/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh @@ -22,7 +22,7 @@ cd "$TMP" cd app "$MCPP" build > a.log 2>&1 || fail "the build failed" a.log rec=$(find target -name .mcpp-written | head -1) -[ -n "$rec" ] || { echo "SKIP: the file system reported no change in free space"; exit 0; } +[ -n "$rec" ] || { echo "SKIP: the file system reported no change in free space"; exit 77; } grep -Eq '^[0-9]+$' "$rec" || fail "A: .mcpp-written is not a number" "$rec" echo "ok: the build recorded what it took ($(cat "$rec") bytes)" echo 1152921504606846976 > "$rec" # 1 EiB: more than any file system here has free diff --git a/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh b/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh index 7b79e2135..3867476b4 100755 --- a/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh +++ b/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh @@ -25,7 +25,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER_ARCH=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER_ARCH=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac mkdir -p "$TMP/app/src" "$TMP/app/tests" diff --git a/tests/e2e/104_asm_sources_gas.sh b/tests/e2e/104_asm_sources_gas.sh index 203cecb8a..4eeadc948 100755 --- a/tests/e2e/104_asm_sources_gas.sh +++ b/tests/e2e/104_asm_sources_gas.sh @@ -8,7 +8,7 @@ set -e # x86_64 assembly — host-aware skip elsewhere (aarch64 self-host e2e). -[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86_64-only asm"; exit 0; } +[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86_64-only asm"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh index c36a6408c..1e7b747be 100755 --- a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh +++ b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh @@ -15,7 +15,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" set -e t=$(mktemp -d); trap 'rm -rf "$t"' EXIT fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -[ "$(uname -m)" = x86_64 ] || { echo "SKIP: an x86_64 host"; exit 0; } +[ "$(uname -m)" = x86_64 ] || { echo "SKIP: an x86_64 host"; exit 77; } mkdir -p "$t/src" && cd "$t" cat > mcpp.toml < build.log 2>&1 || fail "the hosted macOS build" build.log grep -q "openkal-platform" build.log || { echo "SKIP: aarch64-macos-musl is not served by openkal-platform's hosted arrangement here" - exit 0 + exit 77 } bin=$(find target -path '*/bin/early' -type f | head -1) [ -n "$bin" ] || fail "no image was produced" build.log diff --git a/tests/e2e/105_asm_sources_nasm.sh b/tests/e2e/105_asm_sources_nasm.sh index ec8b0e433..ed731098d 100755 --- a/tests/e2e/105_asm_sources_nasm.sh +++ b/tests/e2e/105_asm_sources_nasm.sh @@ -9,7 +9,7 @@ set -e # 2026.10.26.1 ([build] compile_commands); this test reads it. export MCPP_COMPILE_COMMANDS=compile_commands.json -[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86-only NASM"; exit 0; } +[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86-only NASM"; exit 77; } SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" diff --git a/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh index a68bbc8af..23de90eec 100755 --- a/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh +++ b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh @@ -17,9 +17,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 " case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; aarch64|arm64) ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; *) echo "SKIP: a Linux host"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; *) echo "SKIP: a Linux host"; exit 77 ;; esac cd "$TMP" "$MCPP" new app > /dev/null 2>&1 diff --git a/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh index b0f8c9f71..6521b9cb8 100755 --- a/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh +++ b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh @@ -27,8 +27,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ] || ! command -v node >/dev/null 2>&1; then echo "SKIP: 1079 -- no emsdk payload or node on this machine" - echo "PASS: 1079 (skipped)" - exit 0 + exit 77 fi mkdir -p "$TMP/app/src" diff --git a/tests/e2e/108_cfg_conditional_sources.sh b/tests/e2e/108_cfg_conditional_sources.sh index ed256719c..0d8fe517a 100755 --- a/tests/e2e/108_cfg_conditional_sources.sh +++ b/tests/e2e/108_cfg_conditional_sources.sh @@ -12,7 +12,7 @@ HOST_ARCH="$(uname -m)" case "$HOST_ARCH" in x86_64) THIS=x86_64; OTHER=aarch64 ;; aarch64|arm64) THIS=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: unmapped arch $HOST_ARCH"; exit 0 ;; + *) echo "SKIP: unmapped arch $HOST_ARCH"; exit 77 ;; esac cd "$TMP" diff --git a/tests/e2e/130_freestanding_riscv_build_and_run.sh b/tests/e2e/130_freestanding_riscv_build_and_run.sh index c23106df3..32734dff2 100755 --- a/tests/e2e/130_freestanding_riscv_build_and_run.sh +++ b/tests/e2e/130_freestanding_riscv_build_and_run.sh @@ -54,7 +54,7 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/131_freestanding_bsp_supplies_everything.sh b/tests/e2e/131_freestanding_bsp_supplies_everything.sh index 3a4c0bc2f..216ee053a 100755 --- a/tests/e2e/131_freestanding_bsp_supplies_everything.sh +++ b/tests/e2e/131_freestanding_bsp_supplies_everything.sh @@ -33,7 +33,7 @@ set -e # about the seam, and a missing payload is an environment fact. MH="${MCPP_HOME:-$HOME/.mcpp}" SYSROOT_ROOT="$MH/registry/data/xpkgs/xim-x-picolibc-riscv" -[[ -d "$SYSROOT_ROOT" ]] || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 0; } +[[ -d "$SYSROOT_ROOT" ]] || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 77; } # Resolve the emulator to a PATH-independent absolute path. # @@ -48,7 +48,7 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/132_freestanding_test_and_artifacts.sh b/tests/e2e/132_freestanding_test_and_artifacts.sh index b4e4c1648..0817b0a63 100755 --- a/tests/e2e/132_freestanding_test_and_artifacts.sh +++ b/tests/e2e/132_freestanding_test_and_artifacts.sh @@ -27,11 +27,11 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } MH="${MCPP_HOME:-$HOME/.mcpp}" [[ -d "$MH/registry/data/xpkgs/xim-x-picolibc-riscv" ]] \ - || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 0; } + || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/133_freestanding_std_subset.sh b/tests/e2e/133_freestanding_std_subset.sh index 1de03eb99..e9ada8f8d 100755 --- a/tests/e2e/133_freestanding_std_subset.sh +++ b/tests/e2e/133_freestanding_std_subset.sh @@ -26,11 +26,11 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } MH="${MCPP_HOME:-$HOME/.mcpp}" [[ -d "$MH/registry/data/xpkgs/xim-x-picolibc-riscv" ]] \ - || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 0; } + || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 77; } # libc++'s headers come from the llvm payload; pick by CONTENT, not by name # order — an empty directory for an older version sorts first. @@ -38,10 +38,10 @@ LLVM="" for d in "$MH"/registry/data/xpkgs/xim-x-llvm/*/; do [[ -x "$d/bin/clang++" ]] && LLVM="$d" done -[[ -n "$LLVM" ]] || { echo "SKIP: no llvm payload with clang++"; exit 0; } +[[ -n "$LLVM" ]] || { echo "SKIP: no llvm payload with clang++"; exit 77; } LLVM="${LLVM%/}" [[ -d "$LLVM/share/libc++/v1/std" ]] \ - || { echo "SKIP: llvm payload has no libc++ std/*.inc"; exit 0; } + || { echo "SKIP: llvm payload has no libc++ std/*.inc"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/13_toolchain_pin.sh b/tests/e2e/13_toolchain_pin.sh index 0efd404d2..84c39c29c 100755 --- a/tests/e2e/13_toolchain_pin.sh +++ b/tests/e2e/13_toolchain_pin.sh @@ -27,7 +27,7 @@ if [[ -d "$HOME/.xlings/data/xpkgs/xim-x-gcc/16.1.0" ]]; then else echo "SKIP: system xlings has no gcc 16.1.0" echo "OK" - exit 0 + exit 77 fi export MCPP_HOME="$SANDBOX_HOME" diff --git a/tests/e2e/150_clang_module_operator_template.sh b/tests/e2e/150_clang_module_operator_template.sh index 2b6cd53b3..b77dbd28e 100755 --- a/tests/e2e/150_clang_module_operator_template.sh +++ b/tests/e2e/150_clang_module_operator_template.sh @@ -21,11 +21,11 @@ source "$SCRIPT_DIR/_llvm_env.sh" if [[ ! -d "$LLVM_ROOT" ]]; then echo "SKIP: no llvm payload installed" - exit 0 + exit 77 fi CLANGXX="$LLVM_ROOT/bin/clang++" -[[ -x "$CLANGXX" ]] || { echo "SKIP: no clang++ in $LLVM_ROOT"; exit 0; } +[[ -x "$CLANGXX" ]] || { echo "SKIP: no clang++ in $LLVM_ROOT"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/156_nested_ld_library_path.sh b/tests/e2e/156_nested_ld_library_path.sh index dde37385b..100961120 100755 --- a/tests/e2e/156_nested_ld_library_path.sh +++ b/tests/e2e/156_nested_ld_library_path.sh @@ -14,7 +14,7 @@ MCPP_STATE_HOME=${MCPP_HOME:-"$HOME/.mcpp"} GLIBC_LIB=$(ls -d "$MCPP_STATE_HOME"/registry/data/xpkgs/xim-x-glibc/*/lib64 2>/dev/null | head -1) if [[ -z "$GLIBC_LIB" ]]; then echo "SKIP: no private glibc payload installed" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/163_identity_first_resolution.sh b/tests/e2e/163_identity_first_resolution.sh index c34dd848f..e1be88561 100755 --- a/tests/e2e/163_identity_first_resolution.sh +++ b/tests/e2e/163_identity_first_resolution.sh @@ -35,7 +35,7 @@ for c in "$MCPP_HOME/indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua"; do done if [ -z "$SRC" ]; then echo "SKIP 163_identity_first_resolution (no asio descriptor in registry)" - exit 0 + exit 77 fi mkidx() { # mkidx diff --git a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh index 6bb12e0ce..19e7cf16f 100755 --- a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh +++ b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh @@ -32,7 +32,7 @@ cd "$TMP" SRC="$MCPP_HOME/indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua" if [ ! -f "$SRC" ]; then echo "SKIP 165_bare_name_cross_namespace_wire_address (no asio descriptor in registry)" - exit 0 + exit 77 fi # A `compat`-namespaced package named by its SPEC-001 short name. The diff --git a/tests/e2e/166_run_env_no_private_glibc.sh b/tests/e2e/166_run_env_no_private_glibc.sh index ccf56a611..c855ad328 100755 --- a/tests/e2e/166_run_env_no_private_glibc.sh +++ b/tests/e2e/166_run_env_no_private_glibc.sh @@ -85,7 +85,7 @@ GLIBC_STORE=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-glibc/*/ 2>/dev/null if [ -z "$GLIBC_STORE" ]; then echo "SKIP (second half): no private glibc payload installed" echo OK - exit 0 + exit 77 fi cd "$TMP" diff --git a/tests/e2e/168_build_mcpp_musl_host_static.sh b/tests/e2e/168_build_mcpp_musl_host_static.sh index f6b0e6965..58fc4692e 100755 --- a/tests/e2e/168_build_mcpp_musl_host_static.sh +++ b/tests/e2e/168_build_mcpp_musl_host_static.sh @@ -13,7 +13,7 @@ set -euo pipefail if [[ "$(uname -s)" != "Linux" ]]; then echo "SKIP: Linux ELF regression" echo "OK" - exit 0 + exit 77 fi : "${MCPP:?MCPP must point to the mcpp binary under test}" @@ -58,7 +58,7 @@ MUSL_VERSION=$(find_musl_version || true) if [[ -z "$MUSL_VERSION" ]]; then echo "SKIP: no musl-gcc payload installed — not testing the musl host helper" echo "OK" - exit 0 + exit 77 fi echo "musl toolchain: gcc@${MUSL_VERSION}-musl" diff --git a/tests/e2e/201_gcc_no_specs_pollution.sh b/tests/e2e/201_gcc_no_specs_pollution.sh index 20499ad4e..3ce136101 100755 --- a/tests/e2e/201_gcc_no_specs_pollution.sh +++ b/tests/e2e/201_gcc_no_specs_pollution.sh @@ -49,7 +49,7 @@ bin=$(find target -type f -name prog -path '*/bin/*' | head -1) # `readelf` may be an xvm shim needing a per-tool pin; use the payload's own. readelf=$(ls "$MCPP_HOME"/registry/data/xpkgs/xim-x-binutils/*/bin/readelf 2>/dev/null | head -1) -[[ -x "$readelf" ]] || { echo "SKIP: no payload readelf to inspect with"; exit 0; } +[[ -x "$readelf" ]] || { echo "SKIP: no payload readelf to inspect with"; exit 77; } runpath=$("$readelf" -d "$bin" 2>/dev/null | grep -iE 'RUNPATH|RPATH' | sed 's/.*\[\(.*\)\]/\1/' || true) diff --git a/tests/e2e/208_private_libc_stays_in_the_binary.sh b/tests/e2e/208_private_libc_stays_in_the_binary.sh index 189617cc0..8300a308c 100755 --- a/tests/e2e/208_private_libc_stays_in_the_binary.sh +++ b/tests/e2e/208_private_libc_stays_in_the_binary.sh @@ -101,7 +101,7 @@ NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) GLIBC_LIB=$(grep -oE '/[^ ",]*/xim-x-glibc/[0-9.]+/lib(64)?' "$NINJA" | head -1 || true) if [[ -z "$GLIBC_LIB" ]]; then echo "SKIP: this toolchain has no private glibc payload" - exit 0 + exit 77 fi echo "private libc payload: $GLIBC_LIB" diff --git a/tests/e2e/209_runtime_verdict_never_reruns_green.sh b/tests/e2e/209_runtime_verdict_never_reruns_green.sh index d9b947f96..1aa2fe1bf 100755 --- a/tests/e2e/209_runtime_verdict_never_reruns_green.sh +++ b/tests/e2e/209_runtime_verdict_never_reruns_green.sh @@ -41,7 +41,7 @@ cd app CACHE=$(find target -name '.mcpp-runtime-verdicts.json' | head -1) if [[ -z "$CACHE" ]]; then echo "SKIP: no glibc runtime binding on this host (no stored verdicts)" - exit 0 + exit 77 fi # Every artifact must have been validated and recorded as passing, or the rest diff --git a/tests/e2e/220_farm_only_library_runs.sh b/tests/e2e/220_farm_only_library_runs.sh index 605597a26..074e32065 100755 --- a/tests/e2e/220_farm_only_library_runs.sh +++ b/tests/e2e/220_farm_only_library_runs.sh @@ -161,7 +161,7 @@ if [[ -z "$FARM_LIB" ]]; then echo "SKIP: this SubOS farm provides no library the payloads do not already" echo " provide, so there is nothing whose loading proves the farm is" echo " reachable. Install any library into the SubOS to exercise this." - exit 0 + exit 77 fi echo "farm-only library: -l$FARM_LIB (from $FARM_DIR)" diff --git a/tests/e2e/230_bench_harness.sh b/tests/e2e/230_bench_harness.sh index 4d9b7ce2e..897f7719e 100755 --- a/tests/e2e/230_bench_harness.sh +++ b/tests/e2e/230_bench_harness.sh @@ -19,7 +19,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # delegator lives in mcpp's own tree and can simply point at what was built. if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$REPO/.github/tools/newest_artifact.sh" "$REPO" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 77; } export MCPP fi exec bash "$REPO/bench/tests/harness.sh" diff --git a/tests/e2e/231_jobs_option.sh b/tests/e2e/231_jobs_option.sh index 0042f0556..8ffa6b991 100755 --- a/tests/e2e/231_jobs_option.sh +++ b/tests/e2e/231_jobs_option.sh @@ -20,7 +20,7 @@ set -e if [ -z "${MCPP:-}" ]; then _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 77; } case "$MCPP" in /*|?:[/\\]*) ;; *) MCPP="$_root/$MCPP" ;; esac export MCPP fi diff --git a/tests/e2e/234_bmi_schedule_on.sh b/tests/e2e/234_bmi_schedule_on.sh index c05246336..c4dd79bb7 100755 --- a/tests/e2e/234_bmi_schedule_on.sh +++ b/tests/e2e/234_bmi_schedule_on.sh @@ -31,7 +31,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 77; } fi # ABSOLUTISE WHATEVER WE GOT, not just what we derived. This test `cd`s into a # temp directory, so a RELATIVE MCPP — which is what `ls -t target/*/*/bin/mcpp` @@ -90,7 +90,7 @@ EOF # detach-codegen is the GCC strategy. On a machine whose default toolchain # resolves to clang the decision is TwoPhase and there is no semaphore at all, # so the second contract has nothing to test — say so rather than pass hollowly. -say_skip_reason() { echo "SKIP: $1"; exit 0; } +say_skip_reason() { echo "SKIP: $1"; exit 77; } echo "== 1. builds with bmi_schedule = \"on\" ==" "$MCPP" build --release --verbose > build.log 2>&1 || { diff --git a/tests/e2e/235_std_object_only_when_needed.sh b/tests/e2e/235_std_object_only_when_needed.sh index 393cda5d7..21972c022 100755 --- a/tests/e2e/235_std_object_only_when_needed.sh +++ b/tests/e2e/235_std_object_only_when_needed.sh @@ -25,7 +25,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } diff --git a/tests/e2e/236_module_extensions_default_toolchain.sh b/tests/e2e/236_module_extensions_default_toolchain.sh index 3ab7ca0a2..ad7f24312 100755 --- a/tests/e2e/236_module_extensions_default_toolchain.sh +++ b/tests/e2e/236_module_extensions_default_toolchain.sh @@ -26,7 +26,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*|?:[/\\]*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } diff --git a/tests/e2e/237_default_subos_without_info.sh b/tests/e2e/237_default_subos_without_info.sh index bb2c0bc7c..a3a052a6b 100755 --- a/tests/e2e/237_default_subos_without_info.sh +++ b/tests/e2e/237_default_subos_without_info.sh @@ -44,7 +44,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } @@ -62,7 +62,7 @@ mkdir -p "$MCPP_HOME/registry/data" "$MCPP_HOME/registry/subos/default" if [ -d "$REAL_HOME/registry/data/xpkgs" ]; then ln -s "$REAL_HOME/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" else - echo "SKIP: no xpkgs payloads to inherit from $REAL_HOME"; exit 0 + echo "SKIP: no xpkgs payloads to inherit from $REAL_HOME"; exit 77 fi for d in "$REAL_HOME/registry"/*; do b="$(basename "$d")" @@ -146,7 +146,7 @@ PY )" [ -n "$GLIBC_VER" ] && [ -d "$GLIBC_DIR/$GLIBC_VER" ] \ || GLIBC_VER="$(ls "$GLIBC_DIR" 2>/dev/null | head -1)" -[ -n "$GLIBC_VER" ] || { echo "SKIP: no glibc payload to bind to"; exit 0; } +[ -n "$GLIBC_VER" ] || { echo "SKIP: no glibc payload to bind to"; exit 77; } python3 - "$MCPP_HOME/registry/subos/default/.xlings.json" "$GLIBC_VER" <<'PY' import json, sys path, ver = sys.argv[1], sys.argv[2] diff --git a/tests/e2e/238_c_only_unit_links_with_c_driver.sh b/tests/e2e/238_c_only_unit_links_with_c_driver.sh index 6f4717896..313f08441 100755 --- a/tests/e2e/238_c_only_unit_links_with_c_driver.sh +++ b/tests/e2e/238_c_only_unit_links_with_c_driver.sh @@ -31,7 +31,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } diff --git a/tests/e2e/239_msvc_managed_toolset.sh b/tests/e2e/239_msvc_managed_toolset.sh index 942bbba55..b00bb7bee 100755 --- a/tests/e2e/239_msvc_managed_toolset.sh +++ b/tests/e2e/239_msvc_managed_toolset.sh @@ -63,7 +63,7 @@ cd "$TMP" # so that is the question to ask. if ! "$MCPP" toolchain list 2>&1 | grep -q "$TOOLSET"; then echo "SKIP: this index does not offer msvc $TOOLSET (publish window? offline?)" - exit 0 + exit 77 fi # 1) install it. Any failure from here on is a FAILURE. diff --git a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh index 678f45d8b..c4f93e737 100755 --- a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh +++ b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh @@ -58,12 +58,12 @@ cd app cross=x86_64-linux-musl if ! "$MCPP" build --target "$cross" >/dev/null 2>&1; then - echo "SKIP: this machine cannot build $cross"; exit 0 + echo "SKIP: this machine cannot build $cross"; exit 77 fi rm -rf target if ! "$MCPP" run --target "$cross" >/dev/null 2>&1; then - echo "SKIP: $cross builds but does not run here"; exit 0 + echo "SKIP: $cross builds but does not run here"; exit 77 fi # ── The command under test ────────────────────────────────────────────────── diff --git a/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh b/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh index fe1f22e84..336f63da4 100755 --- a/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh +++ b/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh @@ -106,7 +106,7 @@ esac case "$out" in *"c-abi"*"graph"*) echo "SKIP: the C library came from the graph too — not the shape under test" - exit 0 ;; + exit 77 ;; esac if [ "$rc" != 0 ]; then diff --git a/tests/e2e/286_the_openkal_stack_still_builds.sh b/tests/e2e/286_the_openkal_stack_still_builds.sh index b751a8ecc..da7d86a16 100755 --- a/tests/e2e/286_the_openkal_stack_still_builds.sh +++ b/tests/e2e/286_the_openkal_stack_still_builds.sh @@ -92,7 +92,7 @@ if ! out="$("$MCPP" build 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) echo "SKIP: the openkal packages are not reachable from here" - exit 0 ;; + exit 77 ;; esac echo "FAIL: the openkal stack did not build" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh b/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh index 535b6942e..711bccaed 100755 --- a/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh +++ b/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh @@ -62,9 +62,9 @@ CPP if ! out="$("$MCPP" build --target "$TARGET" 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) - echo "SKIP: the openkal packages are not reachable from here"; exit 0 ;; + echo "SKIP: the openkal packages are not reachable from here"; exit 77 ;; *"cannot be built on this host"*) - echo "SKIP: $TARGET cannot be built here"; exit 0 ;; + echo "SKIP: $TARGET cannot be built here"; exit 77 ;; esac echo "FAIL: the cross build failed" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh b/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh index 4c85b9450..a1294f539 100755 --- a/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh +++ b/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh @@ -85,9 +85,9 @@ CPP if ! out="$("$MCPP" build --target "$TARGET" 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) - echo "SKIP: openkal-opensbi is not reachable from here"; exit 0 ;; + echo "SKIP: openkal-opensbi is not reachable from here"; exit 77 ;; *"cannot be built on this host"*) - echo "SKIP: $TARGET cannot be built here"; exit 0 ;; + echo "SKIP: $TARGET cannot be built here"; exit 77 ;; esac echo "FAIL: the freestanding openkal build failed" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/289_one_host_reaches_every_openkal_target.sh b/tests/e2e/289_one_host_reaches_every_openkal_target.sh index 2384063d4..fed6c6cb3 100755 --- a/tests/e2e/289_one_host_reaches_every_openkal_target.sh +++ b/tests/e2e/289_one_host_reaches_every_openkal_target.sh @@ -144,7 +144,7 @@ printf '\n built %d, skipped %d, failed %d\n' "$built" "$skipped" "$failed" # reporting as a skip of the whole file rather than as success. if [ "$built" = 0 ]; then echo "SKIP: no target could be built here" - exit 0 + exit 77 fi echo "OK: one host reached $built of the ecosystem's targets" diff --git a/tests/e2e/28_target_static.sh b/tests/e2e/28_target_static.sh index 07b86a2cf..cbb8fea19 100755 --- a/tests/e2e/28_target_static.sh +++ b/tests/e2e/28_target_static.sh @@ -18,7 +18,7 @@ if ! command -v x86_64-linux-musl-g++ >/dev/null 2>&1 \ then echo "SKIP: no musl-gcc 15.1.0 available — not testing --target=*-linux-musl" echo "OK" - exit 0 + exit 77 fi # Detect installed GNU gcc version for the default-build regression check. diff --git a/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh b/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh index 2389458cf..4fd1a3e7c 100755 --- a/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh +++ b/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh @@ -70,7 +70,7 @@ make_project "$work/plain" "" plain="$(run_probe "$work/plain")" if [ -z "$plain" ]; then echo "SKIP: the build program did not report — it may not have run here" - exit 0 + exit 77 fi if [ "$plain" = "$inherited" ]; then @@ -93,7 +93,7 @@ subos = "default"' declared="$(run_probe "$work/declared")" if [ -z "$declared" ]; then echo "SKIP: the declaring project's build program did not report" - exit 0 + exit 77 fi if [ "$declared" = "$inherited" ]; then diff --git a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh index 0307579d1..efb6a5639 100755 --- a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh +++ b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh @@ -53,7 +53,7 @@ base="$(resolved "$work/plain" riscv64-none-elf)" case "$base" in llvm@*) echo " ok baseline: the bare-metal row resolves $base" ;; *) echo "SKIP: the bare-metal row did not resolve to llvm here (got '${base:-nothing}')" - exit 0 ;; + exit 77 ;; esac make_project "$work/declares" 'provides = ["mcpp:kernel-abi=openkal"]' diff --git a/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh b/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh index 3aa312e0d..46fc9bfed 100755 --- a/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh +++ b/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh @@ -134,7 +134,7 @@ done if [ "$built" = 0 ]; then echo "SKIP: no target resolved here — the sweep proved nothing ($skipped skipped)" - exit 0 + exit 77 fi echo " ok $built correct cross targets went through untouched" diff --git a/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh b/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh index 333d33c0d..209340872 100755 --- a/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh +++ b/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh @@ -29,7 +29,7 @@ targets="$(printf '%s\n' "$out" | awk '/^Targets:/,0')" if [ -z "$targets" ]; then echo "SKIP: this build printed no Targets section" - exit 0 + exit 77 fi row() { printf '%s\n' "$targets" | grep -E "^\s+\*?\s*$1(\s|$)" | head -1; } @@ -41,7 +41,7 @@ row() { printf '%s\n' "$targets" | grep -E "^\s+\*?\s*$1(\s|$)" | head -1; } # means something else. case "$(uname -s)" in Linux) ;; - *) echo "SKIP: the graph-served row under test is a Linux-host arrangement"; exit 0 ;; + *) echo "SKIP: the graph-served row under test is a Linux-host arrangement"; exit 77 ;; esac wm="$(row x86_64-windows-musl)" @@ -72,7 +72,7 @@ esac n="$(printf '%s\n' "$targets" | grep -cE '^\s+\*?\s*[a-z0-9_]+-' || true)" if [ "${n:-0}" -lt 3 ]; then echo "SKIP: only ${n:-0} target rows printed — too few to assert an absence" - exit 0 + exit 77 fi echo " ok $n target rows printed, so an absence below means something" diff --git a/tests/e2e/295_naming_the_host_target_changes_nothing.sh b/tests/e2e/295_naming_the_host_target_changes_nothing.sh index ea3667efc..3a2a07b89 100755 --- a/tests/e2e/295_naming_the_host_target_changes_nothing.sh +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -187,7 +187,7 @@ done if [ "$checked" = 0 ]; then echo "SKIP: no toolchain here produced a link line to compare" - exit 0 + exit 77 fi [ "$fail" = 0 ] || exit 1 echo "OK: naming the host's own target changes nothing ($checked toolchains)" diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index bc007ade7..23e1ec037 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -215,7 +215,7 @@ esac if [ "$checked" = 0 ]; then echo "SKIP: nothing here produced a link line to relate to a report" - exit 0 + exit 77 fi [ "$fail" = 0 ] || exit 1 echo "OK: what the report names is what the link line uses ($checked relations)" diff --git a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh index a7b64a319..59a35721d 100755 --- a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -45,7 +45,7 @@ gccver="$("$MCPP" toolchain list --format json 2>/dev/null \ | jq -r '[.data.toolchains[] | select(.family=="gcc") | .version][0] // empty' | tr -d '\r')" if [ -z "$gccver" ]; then echo "SKIP: gcc is not installed here, and this test is about declaring it" - exit 0 + exit 77 fi # LISTED IS NOT USABLE. A registry entry extracted from another host's @@ -69,7 +69,7 @@ serveReason="$(cd probe && "$MCPP" why toolchain --toolchain "gcc@$gccver" \ | jq -r '.data.reason // "none"' | tr -d '\r')" if [ "$serveReason" != "none" ]; then echo "SKIP: gcc is not installed here, and this test is about declaring it" - exit 0 + exit 77 fi # ── Half one: a bare-metal target refuses, and says why ─────────────────── @@ -146,7 +146,7 @@ hostedTarget="$(cd "$work" && "$MCPP" why toolchain --toolchain "gcc@$gccver" \ --format json 2>/dev/null | jq -r '.data.triple.toolchain // empty' | tr -d '\r')" if [ -z "$hostedTarget" ]; then echo "SKIP: the query did not name a target for gcc@$gccver on this host" - exit 0 + exit 77 fi rm -rf target hosted="$(cd "$work" && "$MCPP" build --target "$hostedTarget" 2>&1 || true)" diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index d3d4ae54e..fd48a419c 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -46,7 +46,7 @@ llvmver="$("$MCPP" toolchain list --format json 2>/dev/null \ | jq -r '[.data.toolchains[] | select(.family=="llvm") | .version][0] // empty' | tr -d '\r')" if [ -z "$llvmver" ]; then echo "SKIP: llvm is not installed here, and this test is about declaring it" - exit 0 + exit 77 fi fail=0 @@ -118,7 +118,7 @@ pinned="$(pick "$hostArch")" [ -n "$pinned" ] || pinned="$(pick "")" if [ -z "$pinned" ]; then echo "SKIP: no target row here is served by a non-llvm convention" - exit 0 + exit 77 fi printf '[package]\nname = "convprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "llvm@%s"\n' \ @@ -152,7 +152,7 @@ case "$reason" in fail=1 ;; *) echo "SKIP: $pinned refused under '$reason', which is not the rule this test is about" - exit 0 ;; + exit 77 ;; esac # AND IT POINTS AT THE WAY OUT. A refusal that names no replacement leaves diff --git a/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh b/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh index 9005c910d..e9db8f66e 100755 --- a/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh +++ b/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh @@ -74,7 +74,7 @@ done if [ -z "$want" ]; then echo "SKIP: this project already resolves '$current' and no other family is" echo " installed here, so no requirement could differ from it" - exit 0 + exit 77 fi printf '[package]\nname = "needs-%s"\nversion = "0.1.0"\nrequires = ["mcpp:compiler=%s"]\n' \ diff --git a/tests/e2e/31_transitive_deps.sh b/tests/e2e/31_transitive_deps.sh index 2ff5edcb6..50218cee0 100755 --- a/tests/e2e/31_transitive_deps.sh +++ b/tests/e2e/31_transitive_deps.sh @@ -18,7 +18,7 @@ MUSL_PAYLOAD="$MCPP_HOME/registry/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-li if [[ ! -x "$MUSL_PAYLOAD" ]]; then echo "SKIP: no reusable musl-gcc xpkg payload" echo "OK" - exit 0 + exit 77 fi # ── 1. Grandchild: a header-providing C lib whose `[build].include_dirs` diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh index 1f81d41c3..67de8dfa0 100755 --- a/tests/e2e/332_cortex_m_builds_and_boots.sh +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -38,7 +38,7 @@ qemu_arm() { command -v qemu-system-arm 2>/dev/null && return 0 return 1 } -QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 77; } llvm_tool() { local c @@ -46,8 +46,8 @@ llvm_tool() { [ -n "$c" ] && { echo "$c"; return 0; } command -v "$1" 2>/dev/null } -NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 0; } -OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 0; } +NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 77; } +OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 77; } mkdir -p "$work/mcu/src" cd "$work/mcu" diff --git a/tests/e2e/333_named_runners_locked_and_sbom.sh b/tests/e2e/333_named_runners_locked_and_sbom.sh index 57c1b70b8..8d55025f1 100755 --- a/tests/e2e/333_named_runners_locked_and_sbom.sh +++ b/tests/e2e/333_named_runners_locked_and_sbom.sh @@ -92,7 +92,7 @@ cat > src/main.cpp <<'CPP' int main() { return 0; } CPP -"$MCPP" build >/dev/null 2>&1 || { echo "SKIP: cmdline@0.0.1 unavailable"; exit 0; } +"$MCPP" build >/dev/null 2>&1 || { echo "SKIP: cmdline@0.0.1 unavailable"; exit 77; } test -f mcpp.lock || { echo "FAIL: no mcpp.lock after a build with a dependency"; exit 1; } "$MCPP" build --locked >/dev/null 2>&1 \ diff --git a/tests/e2e/334_dependency_declared_tool_is_found.sh b/tests/e2e/334_dependency_declared_tool_is_found.sh index 37ff097e6..2ae3a734d 100755 --- a/tests/e2e/334_dependency_declared_tool_is_found.sh +++ b/tests/e2e/334_dependency_declared_tool_is_found.sh @@ -76,7 +76,7 @@ PATH_WITHOUT_TOOL="$PATH" case ":$PATH_WITHOUT_TOOL:" in *":$work/fakepkg/bin:"*) echo "FAIL: fixture leaked onto PATH"; exit 1 ;; esac -command -v demo-tool >/dev/null 2>&1 && { echo "SKIP: a demo-tool already on PATH"; exit 0; } +command -v demo-tool >/dev/null 2>&1 && { echo "SKIP: a demo-tool already on PATH"; exit 77; } # `[xlings] deps` resolution needs a real xim package, which this fixture is # not. What is asserted instead is the ordering the fix establishes: the lookup diff --git a/tests/e2e/336_armv7a_builds_and_boots.sh b/tests/e2e/336_armv7a_builds_and_boots.sh index 9542e2dac..5e903d673 100755 --- a/tests/e2e/336_armv7a_builds_and_boots.sh +++ b/tests/e2e/336_armv7a_builds_and_boots.sh @@ -29,7 +29,7 @@ qemu_arm() { command -v qemu-system-arm 2>/dev/null && return 0 return 1 } -QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 77; } llvm_tool() { local c @@ -37,8 +37,8 @@ llvm_tool() { [ -n "$c" ] && { echo "$c"; return 0; } command -v "$1" 2>/dev/null } -NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 0; } -OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 0; } +NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 77; } +OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 77; } mkdir -p "$work/soc/src" cd "$work/soc" diff --git a/tests/e2e/338_cortex_m_picolibc_sysroot.sh b/tests/e2e/338_cortex_m_picolibc_sysroot.sh index 13247bed8..7112ffe62 100755 --- a/tests/e2e/338_cortex_m_picolibc_sysroot.sh +++ b/tests/e2e/338_cortex_m_picolibc_sysroot.sh @@ -34,7 +34,7 @@ qemu_arm() { command -v qemu-system-arm 2>/dev/null && return 0 return 1 } -QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 77; } llvm_tool() { local c @@ -42,7 +42,7 @@ llvm_tool() { [ -n "$c" ] && { echo "$c"; return 0; } command -v "$1" 2>/dev/null } -READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 0; } +READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 77; } # The payload has to be present. Installing it here rather than skipping # would make the test about the install; skipping when it is absent keeps the @@ -61,7 +61,7 @@ sysroot_dir() { done return 1 } -SYSROOT="$(sysroot_dir)" || { echo "SKIP: picolibc-arm is not installed"; exit 0; } +SYSROOT="$(sysroot_dir)" || { echo "SKIP: picolibc-arm is not installed"; exit 77; } mkdir -p "$work/fw/src" cd "$work/fw" diff --git a/tests/e2e/36_llvm_toolchain.sh b/tests/e2e/36_llvm_toolchain.sh index 13eb23085..6fea4f9db 100755 --- a/tests/e2e/36_llvm_toolchain.sh +++ b/tests/e2e/36_llvm_toolchain.sh @@ -14,7 +14,7 @@ else fi if [[ ! -x "$CLANGPP_BIN" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/37_llvm_import_std.sh b/tests/e2e/37_llvm_import_std.sh index efb043124..55e28d3b4 100755 --- a/tests/e2e/37_llvm_import_std.sh +++ b/tests/e2e/37_llvm_import_std.sh @@ -9,18 +9,18 @@ OS="$(uname -s)" # check in run_all.sh already gates this, but guard here too for direct runs. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/38_llvm_modules.sh b/tests/e2e/38_llvm_modules.sh index f03ee7e12..d1460d5fb 100755 --- a/tests/e2e/38_llvm_modules.sh +++ b/tests/e2e/38_llvm_modules.sh @@ -13,18 +13,18 @@ OS="$(uname -s)" # check in run_all.sh already gates this, but guard here too for direct runs. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/39_llvm_incremental.sh b/tests/e2e/39_llvm_incremental.sh index 09cc26e8b..fcf918408 100755 --- a/tests/e2e/39_llvm_incremental.sh +++ b/tests/e2e/39_llvm_incremental.sh @@ -7,11 +7,11 @@ source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/40_llvm_bmi_cache.sh b/tests/e2e/40_llvm_bmi_cache.sh index 3f7b2b566..d324ef305 100755 --- a/tests/e2e/40_llvm_bmi_cache.sh +++ b/tests/e2e/40_llvm_bmi_cache.sh @@ -9,18 +9,18 @@ OS="$(uname -s)" # check in run_all.sh already gates this, but guard here too for direct runs. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/41_llvm_std_compat.sh b/tests/e2e/41_llvm_std_compat.sh index bcb03bd4a..33acfbc1e 100755 --- a/tests/e2e/41_llvm_std_compat.sh +++ b/tests/e2e/41_llvm_std_compat.sh @@ -9,18 +9,18 @@ OS="$(uname -s)" # capability check in run_all.sh already gates this, but guard here too. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.compat.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.compat.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.compat.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/47_cdb_prebuilt_module_path_abs.sh b/tests/e2e/47_cdb_prebuilt_module_path_abs.sh index d74491a0b..f83c35865 100755 --- a/tests/e2e/47_cdb_prebuilt_module_path_abs.sh +++ b/tests/e2e/47_cdb_prebuilt_module_path_abs.sh @@ -40,7 +40,7 @@ fi command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not on PATH (preinstalled on GitHub-hosted runners)" - exit 0 + exit 77 } # jq returns each value JSON-unescaped (\\ → \, etc.). Stage to a temp diff --git a/tests/e2e/47_llvm_atomic_link.sh b/tests/e2e/47_llvm_atomic_link.sh index 42d794fe1..72194d06c 100755 --- a/tests/e2e/47_llvm_atomic_link.sh +++ b/tests/e2e/47_llvm_atomic_link.sh @@ -10,18 +10,18 @@ set -e OS="$(uname -s)" if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libatomic/-latomic path is Linux-only" - exit 0 + exit 77 fi if [[ "$OS" == Darwin* ]]; then echo "SKIP: libatomic is a GNU/Linux runtime; not applicable on macOS" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi # The fix only emits -latomic when a link-resolvable libatomic is present in # the toolchain (self-guarding). If a slimmed package does not bundle it, the @@ -29,7 +29,7 @@ fi if [[ ! -e "$LLVM_ROOT/lib/x86_64-unknown-linux-gnu/libatomic.so" \ && ! -e "$LLVM_ROOT/lib/x86_64-unknown-linux-gnu/libatomic.a" ]]; then echo "SKIP: llvm@${LLVM_VERSION} package ships no link-resolvable libatomic" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/650_wasm_row_names_its_launcher.sh b/tests/e2e/650_wasm_row_names_its_launcher.sh index e8e0eb279..722b5771f 100755 --- a/tests/e2e/650_wasm_row_names_its_launcher.sh +++ b/tests/e2e/650_wasm_row_names_its_launcher.sh @@ -78,7 +78,7 @@ fi if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 650/2 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)" echo "PASS: 650 (part 1 only)" - exit 0 + exit 77 fi echo "== 650/2: build, run and pack a 1-2-3 program for wasm32-emscripten ==" diff --git a/tests/e2e/653_exceptions_is_the_second_abi_member.sh b/tests/e2e/653_exceptions_is_the_second_abi_member.sh index 1f0601bb2..523ef25ab 100755 --- a/tests/e2e/653_exceptions_is_the_second_abi_member.sh +++ b/tests/e2e/653_exceptions_is_the_second_abi_member.sh @@ -39,8 +39,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 653 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)" - echo "PASS: 653 (skipped)" - exit 0 + exit 77 fi echo "== 653/1: a Linux build of a manifest carrying the table is byte-identical to one without ==" diff --git a/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh b/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh index dc8b1b312..b811da2d9 100755 --- a/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh +++ b/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh @@ -76,7 +76,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 657 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/65_toolchain_runtime_dirs_for_run.sh b/tests/e2e/65_toolchain_runtime_dirs_for_run.sh index dad558146..684c0513f 100755 --- a/tests/e2e/65_toolchain_runtime_dirs_for_run.sh +++ b/tests/e2e/65_toolchain_runtime_dirs_for_run.sh @@ -19,14 +19,14 @@ set -e OS="$(uname -s)" if [[ "$OS" != "Linux" ]]; then echo "SKIP: LD_LIBRARY_PATH runtime-dir check is Linux-specific" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/684_a_runner_supplied_twice_is_refused.sh b/tests/e2e/684_a_runner_supplied_twice_is_refused.sh index 2bd651ab6..46585cdac 100755 --- a/tests/e2e/684_a_runner_supplied_twice_is_refused.sh +++ b/tests/e2e/684_a_runner_supplied_twice_is_refused.sh @@ -80,7 +80,7 @@ case "$arch" in arm64) arch=aarch64 ;; esac case "$(uname -s)" in Linux) row="$arch-linux-gnu" ;; Darwin) row="$arch-macos" ;; - *) echo "SKIP: no canonical host row for $(uname -s)"; exit 0 ;; + *) echo "SKIP: no canonical host row for $(uname -s)"; exit 77 ;; esac printf '\n[target.%s.runners]\napp = ["%s/run-C.sh"]\n' "$row" "$TMP" >> mcpp.toml "$MCPP" build > manifest.log 2>&1 || fail "2: the manifest's runner did not settle it" manifest.log diff --git a/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh b/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh index 940266b1c..9b4ebc440 100755 --- a/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh +++ b/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh @@ -33,7 +33,7 @@ fi fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -command -v git >/dev/null || { echo "SKIP: git is not installed"; exit 0; } +command -v git >/dev/null || { echo "SKIP: git is not installed"; exit 77; } mkdir -p repo/src repo/tool/src cat > repo/mcpp.toml <<'EOF' diff --git a/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh b/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh index 61cb92382..26781f35d 100755 --- a/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh +++ b/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh @@ -25,7 +25,7 @@ for cand in cjson argparse gtest ftxui; do done if [ -z "$DESC" ]; then echo "SKIP: no compat descriptor in the local mcpplibs index" - exit 0 + exit 77 fi VER=$(grep -o '\["[0-9][0-9.]*"\]' "$INDEX_ROOT/compat.$DESC.lua" | head -1 | tr -d '[]"') [ -n "$VER" ] || fail "could not read a version of compat.$DESC" diff --git a/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh b/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh index a536ca388..03c6dbdbb 100755 --- a/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh +++ b/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh @@ -111,7 +111,7 @@ if ! out="$("$MCPP" build --target x86_64-windows-gnu --cache off 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) echo "SKIP: the openkal packages are not reachable from here" - exit 0 ;; + exit 77 ;; esac echo "FAIL: the openkal windows-gnu stack did not build" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh index ff226d854..5960ea5b5 100755 --- a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh +++ b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh @@ -51,7 +51,7 @@ find_llvm() { LLVM_VERSION="$(find_llvm || true)" if [ -z "$LLVM_VERSION" ]; then echo "SKIP: no xim-x-llvm payload on this host to cross-resolve aarch64-macos with" - exit 0 + exit 77 fi echo "using llvm@$LLVM_VERSION" diff --git a/tests/e2e/760_msvc_toolset_is_chosen_once.sh b/tests/e2e/760_msvc_toolset_is_chosen_once.sh index 695d1332d..93c765c0e 100755 --- a/tests/e2e/760_msvc_toolset_is_chosen_once.sh +++ b/tests/e2e/760_msvc_toolset_is_chosen_once.sh @@ -25,7 +25,7 @@ trap "rm -rf $TMP" EXIT cd "$TMP" VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -[[ -x "$VSWHERE" ]] || { echo "SKIP: no vswhere on this runner"; exit 0; } +[[ -x "$VSWHERE" ]] || { echo "SKIP: no vswhere on this runner"; exit 77; } VSROOT="$("$VSWHERE" -latest -prerelease -products '*' \ -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 \ -property installationPath | tr -d '\r')" diff --git a/tests/e2e/778_a_graph_link_searches_no_host_directory.sh b/tests/e2e/778_a_graph_link_searches_no_host_directory.sh index ccf788026..15e31cd95 100644 --- a/tests/e2e/778_a_graph_link_searches_no_host_directory.sh +++ b/tests/e2e/778_a_graph_link_searches_no_host_directory.sh @@ -73,7 +73,7 @@ C skip_if_unreachable() { if grep -qE 'not found in the synced index|install_packages failed' "$1"; then echo "SKIP: the openkal packages are not reachable from here" - exit 0 + exit 77 fi } diff --git a/tests/e2e/86_llvm_hermetic_link.sh b/tests/e2e/86_llvm_hermetic_link.sh index 6db541797..73837ad15 100755 --- a/tests/e2e/86_llvm_hermetic_link.sh +++ b/tests/e2e/86_llvm_hermetic_link.sh @@ -16,17 +16,17 @@ set -e OS="$(uname -s)" if [[ "$OS" != Linux ]]; then echo "SKIP: hermetic CRT/loader model is Linux-only" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/875_toolchain_by_path.sh b/tests/e2e/875_toolchain_by_path.sh index 36a68fd6e..ef0e6774c 100755 --- a/tests/e2e/875_toolchain_by_path.sh +++ b/tests/e2e/875_toolchain_by_path.sh @@ -13,7 +13,7 @@ set -e MCPP="${MCPP:-mcpp}" source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then - echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0 + echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 77 fi work="$(mktemp -d)" # Paths written INTO a manifest go through host_path (see _host_path.sh). diff --git a/tests/e2e/876_toolchain_phase.sh b/tests/e2e/876_toolchain_phase.sh index 30ddf4b5a..8256e7412 100755 --- a/tests/e2e/876_toolchain_phase.sh +++ b/tests/e2e/876_toolchain_phase.sh @@ -9,7 +9,7 @@ set -e MCPP="${MCPP:-mcpp}" source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then - echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0 + echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 77 fi work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT diff --git a/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh b/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh index 4d2720cff..0a21a5369 100755 --- a/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh +++ b/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh @@ -41,7 +41,7 @@ export MCPP_HOME="$TMP/mcpp-home" # its CI home is the macOS leg of the target matrix. case "$(uname -s)" in Darwin|MINGW*|MSYS*|CYGWIN*) ;; - *) echo "SKIP: hosted-guest refusal semantics only bite on a non-linux host"; exit 0 ;; + *) echo "SKIP: hosted-guest refusal semantics only bite on a non-linux host"; exit 77 ;; esac mkdir -p app/src diff --git a/tests/e2e/891_a_host_header_is_written_not_searched.sh b/tests/e2e/891_a_host_header_is_written_not_searched.sh index b5c7edb86..af93d44f4 100755 --- a/tests/e2e/891_a_host_header_is_written_not_searched.sh +++ b/tests/e2e/891_a_host_header_is_written_not_searched.sh @@ -28,7 +28,7 @@ for h in zlib.h X11/Xlib.h curl/curl.h openssl/ssl.h; do done if [ -z "$header" ]; then echo "SKIP: 891 needs a header that only the host's /usr/include has" - exit 0 + exit 77 fi mkdir -p "$TMP/app/src" diff --git a/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh b/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh index 03334a573..d4eb0db01 100755 --- a/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh +++ b/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh @@ -32,11 +32,11 @@ cd probe # The payload the build resolved: `Resolved → /bin/`. COMPILER=$(sed -n 's/.*Resolved [^ ]* → \(.*\)$/\1/p' first.log | tail -1) -[ -n "$COMPILER" ] || { echo "SKIP: the build resolved no managed toolchain payload"; exit 0; } +[ -n "$COMPILER" ] || { echo "SKIP: the build resolved no managed toolchain payload"; exit 77; } case "$COMPILER" in @mcpp/*) COMPILER="$MCPP_HOME/${COMPILER#@mcpp/}" ;; esac PAYLOAD=$(cd "$(dirname "$COMPILER")/.." && pwd) STAMP="$PAYLOAD/.xpkg-install.json" -[ -f "$STAMP" ] || { echo "SKIP: $PAYLOAD carries no install record"; exit 0; } +[ -f "$STAMP" ] || { echo "SKIP: $PAYLOAD carries no install record"; exit 77; } # The payload in this home becomes a directory of its own whose children link # to the inherited one, with a record of its own: the inherited payload (the diff --git a/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh b/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh index 7c1d4fedd..c916b9725 100755 --- a/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh +++ b/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh @@ -40,14 +40,14 @@ $2 $1 TOML } -[[ "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64"; exit 0; } +[[ "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64"; exit 77; } # The default build first: it installs the payloads the fixture is made of, # and its output directory is the one a stated base must not reuse. manifest "" "" out="$("$MCPP" build 2>&1)" || fail "the default build failed" default_dir=$(ls -d target/x86_64-linux-gnu/*/ | head -1) -make_glibc_sysroot "$work/app/third_party/sysroot" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 0; } +make_glibc_sysroot "$work/app/third_party/sysroot" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 77; } SR="$work/app/third_party/sysroot" artifact() { ls target/x86_64-linux-gnu/*/bin/app | xargs ls -t | head -1; } diff --git a/tests/e2e/926_the_hosts_own_root_is_a_base.sh b/tests/e2e/926_the_hosts_own_root_is_a_base.sh index bf130f0d9..789cc79a7 100755 --- a/tests/e2e/926_the_hosts_own_root_is_a_base.sh +++ b/tests/e2e/926_the_hosts_own_root_is_a_base.sh @@ -20,7 +20,7 @@ fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } tuple="$(uname -m)-linux-gnu" if [[ ! -e "/usr/lib/$tuple/crt1.o" ]]; then - echo "SKIP: this host is not a Debian multiarch layout (/usr/lib/$tuple/crt1.o)"; exit 0 + echo "SKIP: this host is not a Debian multiarch layout (/usr/lib/$tuple/crt1.o)"; exit 77 fi host_glibc=$(getconf GNU_LIBC_VERSION | awk '{print $2}') diff --git a/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh b/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh index 5c2656b6e..f8dec40b3 100755 --- a/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh +++ b/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh @@ -11,7 +11,7 @@ set -e MCPP="${MCPP:-mcpp}" -[[ "$(uname -s)" == Linux ]] || { echo "SKIP: the non-Apple arm"; exit 0; } +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: the non-Apple arm"; exit 77; } work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT export NO_COLOR=1 MCPP_NO_AUTO_INSTALL=1 diff --git a/tests/e2e/930_a_toolchain_phase_states_the_base.sh b/tests/e2e/930_a_toolchain_phase_states_the_base.sh index 8ecbb806d..6d42f7509 100755 --- a/tests/e2e/930_a_toolchain_phase_states_the_base.sh +++ b/tests/e2e/930_a_toolchain_phase_states_the_base.sh @@ -10,14 +10,14 @@ set -e MCPP="${MCPP:-mcpp}" source "$(dirname "$0")/_llvm_env.sh" source "$(dirname "$0")/_sysroot_fixture.sh" -[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0; } -[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64 Linux"; exit 0; } +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 77; } +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64 Linux"; exit 77; } work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT export NO_COLOR=1 fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } -make_glibc_sysroot "$work/sr" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 0; } +make_glibc_sysroot "$work/sr" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 77; } mkdir -p "$work/app/src" cd "$work/app" cat > mcpp.toml </dev/null; done; exit 1; } source "$(dirname "$0")/_toolchain_env.sh" -[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 933 is about ELF"; exit 0; } -[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 0; } +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 933 is about ELF"; exit 77; } +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 77; } GLIB="$MCPP_E2E_GCC_ROOT/lib64" -[[ -e "$GLIB/libstdc++.so.6" && -x "$MCPP_E2E_GCC_ROOT/bin/g++" ]] || { echo "SKIP: no gcc payload"; exit 0; } +[[ -e "$GLIB/libstdc++.so.6" && -x "$MCPP_E2E_GCC_ROOT/bin/g++" ]] || { echo "SKIP: no gcc payload"; exit 77; } project() { # $1 dir, $2 target kind, $3 extra manifest text mkdir -p "$1/src" "$1/tests" diff --git a/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh b/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh index e6d17e277..e61267a96 100644 --- a/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh +++ b/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh @@ -16,11 +16,11 @@ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } source "$(dirname "$0")/_toolchain_env.sh" -[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 934 uses the Linux gcc payload"; exit 0; } -[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 0; } +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 934 uses the Linux gcc payload"; exit 77; } +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 77; } G="$MCPP_E2E_GCC_ROOT" [[ -e "$G/include/c++/$GCC_VERSION/bits/std.cc" && -e "$G/lib64/libstdc++.so.6" ]] \ - || { echo "SKIP: no gcc payload with bits/std.cc"; exit 0; } + || { echo "SKIP: no gcc payload with bits/std.cc"; exit 77; } # The provider, as `gcc.libstdcxx`'s install() lays it out. P="$TMP/libstdcxx" diff --git a/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh b/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh index 5492ac68d..2c3a93ec9 100644 --- a/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh +++ b/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh @@ -10,7 +10,7 @@ # A the link rule writes a temporary name and renames it (POSIX); # B starting the program in a loop while it is relinked never fails to exec. set -e -case "$(uname -s)" in Linux|Darwin) ;; *) echo "SKIP: POSIX publication"; exit 0 ;; esac +case "$(uname -s)" in Linux|Darwin) ;; *) echo "SKIP: POSIX publication"; exit 77 ;; esac TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } diff --git a/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh b/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh index e82890ab6..b22a87697 100755 --- a/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh +++ b/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh @@ -40,7 +40,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac # No C library, no C++ run-time, no compiler run-time: the image is its own. diff --git a/tests/e2e/951_an_ok_image_needs_no_declaration.sh b/tests/e2e/951_an_ok_image_needs_no_declaration.sh index 084678f94..a20f79895 100755 --- a/tests/e2e/951_an_ok_image_needs_no_declaration.sh +++ b/tests/e2e/951_an_ok_image_needs_no_declaration.sh @@ -40,7 +40,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac T="$HOST_ARCH-openkal" diff --git a/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh b/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh index ee59d3934..09dfe9511 100755 --- a/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh +++ b/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh @@ -23,7 +23,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64|aarch64|arm64) ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac mkdir -p "$TMP/app/src" "$TMP/app/helper/src" diff --git a/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh b/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh index 91fd5cd2a..6d0f5c3b7 100755 --- a/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh +++ b/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh @@ -29,9 +29,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 77 ;; esac T="$HOST_ARCH-openkal" mkdir -p "$TMP/app/src" diff --git a/tests/e2e/957_packages_merge_and_describe_themselves.sh b/tests/e2e/957_packages_merge_and_describe_themselves.sh index bb9a78d33..ee361f12e 100755 --- a/tests/e2e/957_packages_merge_and_describe_themselves.sh +++ b/tests/e2e/957_packages_merge_and_describe_themselves.sh @@ -31,7 +31,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) T=x86_64-openkal ;; aarch64|arm64) T=aarch64-openkal ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac mkdir -p "$TMP/app/src" diff --git a/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh b/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh index 664b0f781..e17c49fc9 100755 --- a/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh +++ b/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh @@ -39,7 +39,7 @@ TMP_HOST="$(host_path "$TMP")" case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac HT="$HOST_ARCH-openkal" OT="$OTHER-openkal" diff --git a/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh b/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh index 261905414..f7678f69a 100755 --- a/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh +++ b/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh @@ -35,7 +35,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) OTHER=aarch64 ;; aarch64|arm64) OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac case "$(uname -s)" in Darwin) SYSTEM=macos; XSYSTEM=macosx ;; diff --git a/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh b/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh index f0ab22719..83d9a3c0e 100755 --- a/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh +++ b/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh @@ -33,7 +33,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac HT="$HOST_ARCH-openkal" OT="$OTHER-openkal" diff --git a/tests/e2e/964_tests_built_once_run_from_an_archive.sh b/tests/e2e/964_tests_built_once_run_from_an_archive.sh index f61b31679..2adea746b 100755 --- a/tests/e2e/964_tests_built_once_run_from_an_archive.sh +++ b/tests/e2e/964_tests_built_once_run_from_an_archive.sh @@ -33,7 +33,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac OT="$OTHER-openkal" diff --git a/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh b/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh index 3ab66267d..72b96e31b 100755 --- a/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh +++ b/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh @@ -18,8 +18,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 981 -- no emsdk payload installed" - echo "PASS: 981 (skipped)" - exit 0 + exit 77 fi mkdir -p "$TMP/app/src" diff --git a/tests/e2e/983_a_pack_holds_the_legs_it_names.sh b/tests/e2e/983_a_pack_holds_the_legs_it_names.sh index 9e16636b7..30c85014b 100644 --- a/tests/e2e/983_a_pack_holds_the_legs_it_names.sh +++ b/tests/e2e/983_a_pack_holds_the_legs_it_names.sh @@ -34,9 +34,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 77 ;; esac HT="$HOST_ARCH-openkal" mkdir -p "$TMP/app/src" diff --git a/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh b/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh index 6cad66582..95d6216c7 100644 --- a/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh +++ b/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh @@ -21,7 +21,7 @@ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -[ "$(uname -s)" = Linux ] || { echo "SKIP: the linux-musl rows are built on Linux"; exit 0; } +[ "$(uname -s)" = Linux ] || { echo "SKIP: the linux-musl rows are built on Linux"; exit 77; } mkdir -p "$TMP/app/src" cd "$TMP/app" @@ -31,7 +31,7 @@ printf 'import std;\nint main() { std::println("per leg"); }\n' > src/main.cpp # A if ! "$MCPP" pack --target linux-musl -o out/ > a.log 2>&1; then if grep -qE "cannot be built on this host|install_packages failed|not found in the synced index" a.log; then - echo "SKIP: a linux-musl row cannot be built here"; exit 0 + echo "SKIP: a linux-musl row cannot be built here"; exit 77 fi fail "A: pack --target linux-musl" a.log fi diff --git a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh index 6caf40108..9423beb80 100644 --- a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh +++ b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh @@ -27,9 +27,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 77 ;; esac mkdir -p "$TMP/app/src" cd "$TMP/app" diff --git a/tests/e2e/987_the_universal_spellings_are_migrated.sh b/tests/e2e/987_the_universal_spellings_are_migrated.sh index 355f5f54c..178c68299 100644 --- a/tests/e2e/987_the_universal_spellings_are_migrated.sh +++ b/tests/e2e/987_the_universal_spellings_are_migrated.sh @@ -27,7 +27,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac HT="$HOST_ARCH-openkal" diff --git a/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh b/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh index e6512cccc..4ac23096d 100755 --- a/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh +++ b/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh @@ -23,7 +23,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -s)-$(uname -m)" in Linux-x86_64|Linux-amd64) ;; - *) echo "SKIP: the Windows runner is x86_64 and wine runs it on Linux x86_64"; exit 0 ;; + *) echo "SKIP: the Windows runner is x86_64 and wine runs it on Linux x86_64"; exit 77 ;; esac mkdir -p "$TMP/app/src" diff --git a/tests/e2e/98_reflection_import_std.sh b/tests/e2e/98_reflection_import_std.sh index 4d9430b8a..82431d54d 100755 --- a/tests/e2e/98_reflection_import_std.sh +++ b/tests/e2e/98_reflection_import_std.sh @@ -16,7 +16,7 @@ trap "rm -rf $TMP" EXIT GXX=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1) if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then echo "SKIP-INLINE: no -freflection-capable gcc payload available" - exit 0 + exit 77 fi mkdir -p "$TMP/proj/src" "$TMP/proj/meta_dep/src" diff --git a/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh b/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh index 6c108c3eb..8a5fec583 100755 --- a/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh +++ b/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh @@ -34,9 +34,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 77 ;; esac mkdir -p "$TMP/app/src" cd "$TMP/app" diff --git a/tests/e2e/994_the_contract_names_the_target_side.sh b/tests/e2e/994_the_contract_names_the_target_side.sh index d5d0313ad..ead3423c9 100755 --- a/tests/e2e/994_the_contract_names_the_target_side.sh +++ b/tests/e2e/994_the_contract_names_the_target_side.sh @@ -22,7 +22,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; aarch64|arm64) ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac cd "$TMP" diff --git a/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh b/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh index d9525df95..897db1bba 100755 --- a/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh +++ b/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh @@ -20,7 +20,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -s)-$(uname -m)" in Linux-x86_64|Linux-amd64) ;; - *) echo "SKIP: the Windows launcher is built here on Linux x86_64"; exit 0 ;; + *) echo "SKIP: the Windows launcher is built here on Linux x86_64"; exit 77 ;; esac mkdir -p "$TMP/app/src" diff --git a/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh b/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh index 190741ca9..a32efce89 100755 --- a/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh +++ b/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh @@ -22,7 +22,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac T="$HOST_ARCH-openkal" diff --git a/tests/e2e/999_debug_information_is_stored_once.sh b/tests/e2e/999_debug_information_is_stored_once.sh index 46645df3f..e548bbb3b 100755 --- a/tests/e2e/999_debug_information_is_stored_once.sh +++ b/tests/e2e/999_debug_information_is_stored_once.sh @@ -19,7 +19,7 @@ TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT cd "$TMP" fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -[ "$(uname -s)" = Linux ] || { echo "SKIP: the split form is checked on Linux (ELF)"; exit 0; } +[ "$(uname -s)" = Linux ] || { echo "SKIP: the split form is checked on Linux (ELF)"; exit 77; } mkdir -p app/src cd app diff --git a/tests/e2e/coverage-debt.tsv b/tests/e2e/coverage-debt.tsv new file mode 100644 index 000000000..0a0a4289c --- /dev/null +++ b/tests/e2e/coverage-debt.tsv @@ -0,0 +1,5 @@ +# Tests that skip themselves on every host of CI that starts them, read by +# .github/tools/check_e2e_coverage.py. One `\t` line each. +# A debt, not an excuse: the list only shrinks. An entry whose test ran fails the +# check until the entry is removed; a test that begins to skip itself everywhere is +# uncovered until a host is given what it needs, or it is listed here with the reason. diff --git a/tests/e2e/mtest.toml b/tests/e2e/mtest.toml index 1177d9e99..baed72940 100644 --- a/tests/e2e/mtest.toml +++ b/tests/e2e/mtest.toml @@ -14,6 +14,9 @@ kind = "e2e" # A script is level 0, whatever its text says: several name the protocol's marker in their comments, # and a script told the protocol's variables would pass them to what it runs. level = 0 +# A script that cannot hold anything on this machine leaves with 77 after a `SKIP: ` line: +# its case is skipped, with that reason, and is not counted among the passes (mtest 0.6.1). +skip_exit = 77 timeout = "10m" tags_from = { "# requires:" = "requires:" } resources_from = ["# resources:"] diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index eb2febeee..dc6e380e1 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -542,14 +542,22 @@ try: except (OSError, ValueError): sys.exit(0) for c in run.get("cases", []): - status = {"pass": "pass", "flaky": "pass", "timeout": "timeout"}.get(c.get("status"), "fail") - print(status, int(c.get("ms", 0)), c.get("program", c.get("id", ""))) + status = {"pass": "pass", "flaky": "pass", "timeout": "timeout", "skip": "skip"}.get(c.get("status"), "fail") + # The fourth word on: why a skipped script did not apply here (its `SKIP:` line). + print(status, int(c.get("ms", 0)), c.get("program", c.get("id", "")), c.get("reason", "")) PY )" rm -f "$record" - while read -r status ms name; do + while read -r status ms name why; do name="${name%$'\r'}" # Windows' Python ends a printed line with CR LF + why="${why%$'\r'}" [[ -n "$name" ]] || continue + # A script that skipped itself ran nothing: it is reported as a skip with + # its reason, which the coverage check reads as "did not run here". + if [[ "$status" == skip ]]; then + ((SKIP++)); report skip "$name" "$ms" "skipped itself: ${why:-no reason given}" + continue + fi TIMINGS+=("$ms $name") case "$status" in pass) ((PASS++)); report pass "$name" "$ms" "" ;; @@ -628,6 +636,12 @@ for test in "$HERE"/[0-9]*.sh; do echo "PASS: $name (${_dur})" ((PASS++)) report pass "$name" "$_dur_ms" "" + elif [[ $rc -eq 77 ]]; then + # The script's own word for "nothing to hold on this machine" (the + # `skip_exit` of tests/e2e/mtest.toml): a skip, not a pass. + echo "SKIP: $name (skipped itself)" + ((SKIP++)) + report skip "$name" "$_dur_ms" "skipped itself" elif [[ $rc -eq 124 ]]; then report timeout "$name" "$_dur_ms" "exceeded ${E2E_TEST_TIMEOUT}s" # GNU timeout: 124 = killed after deadline (TERM); 137 = SIGKILL after grace. diff --git a/tests/scripts/test_check_e2e_coverage.py b/tests/scripts/test_check_e2e_coverage.py index 54233693a..e1a9c77ba 100644 --- a/tests/scripts/test_check_e2e_coverage.py +++ b/tests/scripts/test_check_e2e_coverage.py @@ -46,7 +46,8 @@ def run_check(self, *extra: str) -> subprocess.CompletedProcess: def test_ran_named_and_excused_tests_are_covered(self) -> None: r = self.run_check() self.assertEqual(r.returncode, 0, r.stdout) - self.assertIn("1 ran on a shard, 0 unaffected by the change, 2 run by a dedicated job, 1 excused, 0 uncovered", r.stdout) + self.assertIn("1 ran on a shard, 0 unaffected by the change, 2 run by a dedicated job, 1 excused, " + "0 owed, 0 uncovered", r.stdout) def test_a_name_in_a_comment_or_inside_a_longer_name_does_not_count(self) -> None: e2e = self.root / "tests" / "e2e" @@ -88,6 +89,27 @@ def test_a_test_a_set_left_out_is_unaffected_and_meets_its_host(self) -> None: self.assertEqual(r.returncode, 1) self.assertIn("REQUIRED ON HOST: 50_untouched.sh did not pass on linux", r.stdout) + def test_a_test_that_skips_itself_everywhere_is_uncovered_until_it_is_owed(self) -> None: + """A script that leaves with its skip status ran nothing: it is not a pass.""" + (self.root / "tests" / "e2e" / "55_skips_itself.sh").write_text("# x\n# requires: llvm\n") + for host in ("linux", "macos"): + (self.reports / f"e2e-report-{host}-3.tsv").write_text( + "skip\t55_skips_itself.sh\t40\tskipped itself: no emulator on this machine\n") + r = self.run_check() + self.assertEqual(r.returncode, 1) + self.assertIn("UNCOVERED: 55_skips_itself.sh", r.stdout) + self.assertIn("skipped itself: no emulator on this machine", r.stdout) + debt = self.root / "tests" / "e2e" / "coverage-debt.tsv" + debt.write_text("# test\twhat the hosts lack\n55_skips_itself.sh\tno shard installs the emulator\n") + r = self.run_check() + self.assertEqual(r.returncode, 0, r.stdout) + self.assertIn("1 owed, 0 uncovered", r.stdout) + # The debt only shrinks: an entry whose test ran is stale. + (self.reports / "e2e-report-linux-4.tsv").write_text("pass\t55_skips_itself.sh\t900\t\n") + r = self.run_check() + self.assertEqual(r.returncode, 1) + self.assertIn("55_skips_itself.sh is listed as owed but ran on linux", r.stdout) + def test_an_exception_for_a_test_that_ran_fails(self) -> None: (self.reports / "e2e-report-macos-1.tsv").write_text("pass\t30_excused.sh\t10\t\n") r = self.run_check() From f7b3c85f0ced4e31b48edae911fcb7376bdf5866 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 17:21:03 +0900 Subject: [PATCH 28/30] wip: the skip status in the test spec and AGENTS.md --- AGENTS.md | 4 +++- docs/specs/test.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f8743c12b..c12f83d13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,9 @@ MCPP= bash tests/e2e/_.sh - An e2e script is named by the behaviour it asserts, states on line 2 the capabilities it requires (`# requires: llvm wine`), opens with the design or issue it verifies and its criteria, and uses - an isolated home. Every e2e script runs on some host or is listed in + an isolated home. A script that can hold nothing on a machine prints `SKIP: ` and exits + 77: the run reports a skip, never a pass. A script that skips itself on every host is listed in + `tests/e2e/coverage-debt.tsv`, which only shrinks. Every e2e script runs on some host or is listed in `tests/e2e/coverage-exceptions.tsv`. The scripts run as mtest programs ([`tests/e2e/mtest.toml`](tests/e2e/mtest.toml)). - A subsystem's unit tests live in `modules//tests/` and build against that package alone; diff --git a/docs/specs/test.md b/docs/specs/test.md index 2d1522969..bdc46d7ac 100644 --- a/docs/specs/test.md +++ b/docs/specs/test.md @@ -66,6 +66,8 @@ | L3 | L2 与 P7(O5) | 每个用例的资源与工件:调度与按影响选择 | - **P1.1** 不实现协议的程序是 L0 程序。运行器**必须**原样运行它,并**必须**只以退出状态判定:0 为通过,其余为失败。 + 运行器的配置**可以**为这类程序陈述一个表示"在本机不适用"的退出状态(mtest 的 `skip_exit`,mtest >= 0.6.1):程序以它 + 退出时,其用例为跳过,原因取其输出中最后一行以 `SKIP` 开头的文字。跳过**不得**计入通过。 - **P1.2** 运行器**禁止**把协议变量(P2)交给不知其实现协议的程序,除非它接受一个 L0 程序完整运行一次(P2.4)。 #### P2. 握手 From eda6d3e96cf7c51f2ea6eef4687c98462031be3e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 18:20:35 +0900 Subject: [PATCH 29/30] wip: a runner template's place the build does not have is refused --- src/build/execute.cppm | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 4d2b93e83..05eb4894f 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -1454,6 +1454,10 @@ struct RunnerChoice { // machine (SPEC-016 §6): a run or a test installs them and asks again; a // build installs nothing. std::vector missing; + // A place the template names that this build does not have (`{sysroot}` + // with no base, `{pkg:}` outside `requires`): the launch is refused + // naming it, since the word would reach the runner as it is written. + std::string unplaced; // The spelling that names this target in the manifest: the canonical form, // which is also the output directory's name and the key every // `[target.]` reader resolves. Every diagnostic below prints this @@ -1496,9 +1500,23 @@ void settle_runner_places(const BuildContext& ctx, RunnerChoice& c, if (!ctx.tc.base.root.empty()) place(word, "{sysroot}", ctx.tc.base.root.generic_string()); for (auto const& [name, root] : roots) place(word, std::format("{{pkg:{}}}", name), root.generic_string()); + // What is left after the packages stand is a place nothing gives. + if (!c.unplaced.empty() || !c.missing.empty()) continue; + if (word.find("{sysroot}") != std::string::npos) c.unplaced = "{sysroot}"; + else if (auto at = word.find("{pkg:"); at != std::string::npos) + c.unplaced = word.substr(at, word.find('}', at) == std::string::npos + ? std::string::npos : word.find('}', at) - at + 1); } } +// The refusal of a template whose place this build does not have. +std::string unplaced_message(const RunnerChoice& c) { + return std::format( + "the runner of '{}' names {}, and this build has no such place: `{{sysroot}}` is the base " + "the target is linked against, `{{pkg:}}` a package of the runner's `requires`", + c.tripleKey, c.unplaced); +} + // What a run or a test does about `missing`: installs each through the channel // a toolchain pin uses, with the same consent. Empty: all stand. std::string install_runner_packages(const RunnerChoice& c) { @@ -3353,6 +3371,10 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, } choice = choose_device_action(ctx, runner_name, no_runner); } + if (!choice.unplaced.empty()) { + std::println(libc::stderr(), "error: {}", unplaced_message(choice)); + return 2; + } if (choice.ignored) mcpp::ui::info("note", std::format( "--no-runner: ignoring the runner declared for {}", choice.tripleKey)); @@ -4704,10 +4726,17 @@ static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool if (auto why = install_runner_packages(tb.runnerChoice); !why.empty()) { tb.invocationNotRunReason = why; std::println(libc::stderr(), "error: {}", why); + tb.runnerFatal = 2; return; } tb.runnerChoice = choose_device_action(*ctx, testOpts.runnerName, testOpts.noRunner); } + if (!tb.runnerChoice.unplaced.empty()) { + tb.invocationNotRunReason = unplaced_message(tb.runnerChoice); + std::println(libc::stderr(), "error: {}", tb.invocationNotRunReason); + tb.runnerFatal = 2; + return; + } auto& runnerChoice = tb.runnerChoice; if (!testOpts.runnerName.empty() && runnerChoice.tmpl.empty()) { tb.invocationNotRunReason = std::format("no runner named '{}' for '{}'", From 64bbe56d054d4103a8b75fc79ea008bae62dcb89 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 18:24:31 +0900 Subject: [PATCH 30/30] wip: CI runs with mtest 0.6.1 --- .github/versions.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index f670c4079..4b2a246c1 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,5 +1,5 @@ # The engines CI pins, read by the composite actions (.github/actions/mtest). # mtest: an xlings package (xim:mtest) whose sha256 the index states; the release # bundle's sha256 checks the fallback download. -MTEST_VERSION=0.5.2 -MTEST_SHA256=be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 +MTEST_VERSION=0.6.1 +MTEST_SHA256=d6caae62095dfc1a3977ce384fb039bb6b97706eaf18e8203dbacb10aa5df9a4