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..9c8e0fb22 --- /dev/null +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -0,0 +1,117 @@ +--- +subject: architecture +status: active +--- + +# The three-plane platform architecture: mcpp × openkal × xim + +Date: 2026-11-12 +Status: reference design (a synthesis; nothing in this document adds engine +behaviour — it names what the merged changes established and what the next +systems consume) + +## 1. The statement + +A platform enters this ecosystem as **data**, never as engine code. Three +data planes carry everything a target needs, and one normalisation layer +decides whether what was built is right: + +| Plane | Carries | Where it lives | Invariant | +|---|---|---|---| +| ① row | serves/hosts/toolchain/sysroot/linkage/abi of a target identity | the toolchain model's table, or a platform package's own rows | target differences are data; the engine holds no per-target branch | +| ② payload | the toolchain bytes, and the base package that is the target's C library | the index (xim), installed by the fetcher through the revision channel | a build host needs mcpp itself and nothing else; where a row names a base package the host's link model yields entirely | +| ③ runner | how an artifact of another system starts (a declared prefix, an emulator, a loader) | row/platform data; resolved by the runner lookup | build never depends on a runner; running consumes the declaration | +| verification | one script, one expected output, normalised (CRLF, colour, codeset, console noise) | the shared harness | the harness knows no target; every executor family answers through it | + +Two red lines hold throughout, and the gates hold them mechanically: +default behaviour is unchanged by any of it (the regressions stay green), and +no host name enters the engine — host differences are statements rows make. + +## 2. The layering the planes serve + +```mermaid +flowchart TB + subgraph consumer["a program of any system"] + A["import the C++ modules / C sources"] + end + subgraph libc["one C library, ported once"] + M["libc above the kernel ABI (spec)
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 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 +``` + +The specification's own rules make the layering safe: an implementation may +declare an interface absent (the conformance suite decides which absences are +legal), a C library above the interface never sits above an implementation +that would call back into it (layer-cycle refuses that graph at resolution), +and a program states which ABI it speaks by depending on one. + +## 3. What the merged round established + +Measured, not aspirational (each item landed with its gate green): + +1. **Rows as the only place targets differ.** Eighteen identities build from + one engine with zero per-target code; the identity that was missing + arrived as one table row plus one base package. +2. **Base packages.** Where a row names one, the payload is the target's C + library: the driver gets `--sysroot`, the payload's own `-B/-L`, a + loader folded sysroot-relative, and the compiler's runtime directory — + and the host's link words, runtime directories and closure rules yield + entirely. A cross needs nothing from the machine it stands on. +3. **Foreign-kernel execution.** An artifact of another system, of this + machine's architecture, runs as a child: its death by signal is named, + its status returned unchanged, and the runner declaration follows it. +4. **ABI members as data.** `abi longjmp` states how a jump crosses the + WebAssembly boundary; the probe renders the flag on both sides, and a + row that is not the member's subject is refused rather than ignored. +5. **Corrective revisions.** A resource whose bytes must change without a + version change is re-published under the same key with a raised + revision; the updater channel enforces that a raise is total — every + platform, one sha per url, shapes compared. +6. **One file, several systems.** A pack can hold the programs of several + systems: a PE leads with a POSIX script in its DOS header, and every + POSIX system's program rides behind it. +7. **Layer vocabulary.** `kernel-abi` is a layer the rows, the payloads and + the requirement side all speak (`layers.kernel-abi`, `requires`), an + implementation written above a C library never sits beneath one it would + call back into — the resolution refuses that graph before anything is + compiled — and a payload's kernel-abi is the target's system, so a + requirement stated on it holds across every route to the row. + +## 4. What the next systems consume + +The same planes, with no engine work: + +| Scenario | What it uses | What is left to build | +|---|---|---| +| A POSIX-family kernel with its own libc | the POSIX-library implementation of the ABI (already above every such libc), rows, a cross toolchain payload | the toolchain payload; a disk-image asset; an emulator runner | +| Firmware (UEFI, SBI) | the existing firmware implementations; an ordinary PE row and three link flags state a UEFI application — measured, no new target kind; a runner's exit status is the program's (the contract already carries firmware: the harness states the status) | an OVMF/emulator runner entry | +| A system's official media, booted | the disk-image asset class (`-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. 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ca51646..bd30f93da 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.12.1] - 2026-11-12 + +### Added + +- The cross rows of the platform table are served from what their own data + names, not from a list of hosts written in the engine. `aarch64-linux-gnu` + takes its C library from the sysroot payload its row declares + (`xim:aarch64-linux-gnu-sysroot`, glibc 2.44 with the kernel headers, + libgcc and libc++ inside; the package root is the sysroot), so the row is + built wherever that payload is published — an x86_64 Linux host included — + and `host_can_serve` reads the declaration instead of refusing every + arch-crossing glibc target. The row links its GCC run-time in, as the musl + rows do: that sysroot carries no shared libgcc. When such a base package + serves the target, the host contributes nothing to the link: the payload's + own `-B`/`-L`/loader words and its `compiler_runtime_dir` resource + directory are the whole C-runtime group, on the C link exactly as on the + C++ one (before, the host's runtime directories and `-latomic` shadowed + the target's — an x86_64 `libatomic.a` the linker then called incompatible + with aarch64 — and a C link could not find the payload's builtins at all), + and the runtime-closure rules state the premise they always had: the + artifact's loader is the payload's, a process on this machine is not the + one that would load it. The test that holds the row skips itself until the + package index carries the payload. +- `longjmp` joins `threads` and `exceptions` as a member of + `[target..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/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 1df48eea8..e14d83dfe 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.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. + **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/22-target-side.md b/docs/22-target-side.md index ae6f7e205..1c6f0f413 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.12.1+)* | `"wasm"` or `"js"` | on `os = "emscripten"` only, on the compile line of every C and C++ translation unit and on the link: `"wasm"` is `-sSUPPORT_LONGJMP=wasm`, WebAssembly's own stack switching; `"js"` is `-sSUPPORT_LONGJMP=emscripten`, the JavaScript model and the toolchain's default. Declared on any other row, it is refused. An absent member renders nothing | the same set `threads` reaches | -Both members enter the dependency cache key through the dialect flags, so a +All three members enter the dependency cache key through the flags they render, so a dependency built without one is never reused by a build with it, and one built with one exception model is never reused by a build with the other. An unknown member, and a value outside the table, are refused naming the values. @@ -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/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 0b70acfd9..b68daabf1 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.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 c56ce4699..dc39019df 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.10.2" +version = "2026.11.12.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index f120a9ceb..b235a3389 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.12.1): "wasm" or + // "js", no boolean -- the engine's default is the + // toolchain's own ("js"), and an absent member is that + // default rather than a switch turned off. + if (ak == "longjmp") { + if (!av.is_string() + || (av.as_string() != "wasm" && av.as_string() != "js")) + return std::unexpected(error(origin, std::format( + "[target.{}.abi].longjmp must be \"wasm\" or \"js\"", + triple))); + cc.abiLongjmpModel = av.as_string(); + cc.abiLongjmpDeclared = true; + continue; + } if (!av.is_bool() && !model) return std::unexpected(error(origin, ak == "exceptions" ? std::format("[target.{}.abi].exceptions must be true, false, " diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d07c3bf97..21453c223 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.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`, + // 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..8130c137f 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.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"] 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..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.10.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.12.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..37cc75a60 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.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 // SHAPE HAS BUILT ITS LINE. diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index 51795fd10..8a4cb7e5d 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.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; + } // 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..4479df961 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.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 + // 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..0d00e37c0 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,45 @@ 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). +// +// 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 {}; +} + // 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 +141,21 @@ 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); + // 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 +169,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); @@ -161,6 +214,54 @@ 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) +{ + 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. +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) ── // @@ -200,6 +301,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"; + // 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; @@ -210,32 +319,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); @@ -385,7 +478,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..47bad2cf9 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.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. 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..646314580 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 { @@ -168,4 +168,82 @@ 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 fb2b960be..475cf0ef9 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.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 diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index a3d4bca69..2798662ce 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.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); // 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.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", {} }; } 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.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()) 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/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/src/project.cppm b/src/project.cppm index 4c5e9619b..4a34f1392 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,14 +1095,24 @@ 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. 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"; + // 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); @@ -1196,6 +1206,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 +1222,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 +1233,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..a1b0de2ae 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.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; + } + 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/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..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 \ @@ -86,9 +87,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/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..eb6815594 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; @@ -52,14 +53,26 @@ 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.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. 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" }))); + // `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..07d2a8940 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.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())); 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 144aca228..5e86587a7 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,48 @@ 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 74df15d30..748cb3c85 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.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. 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()); }