From 495edd5a6c85efaed509fcce50d288cdc596ddee Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 04:45:36 +0900 Subject: [PATCH 01/15] 2026.11.11.1: five rows the platform's own data serves The cross rows read their payloads, not a host list: aarch64-linux-gnu takes its C library from the sysroot payload its row declares, and when such a base package serves the target the host contributes nothing to the link -- the base's own -B/-L words, its loader named sysroot- relative, its compiler_runtime_dir resource directory on the C link exactly as on the C++ one, and --sysroot naming the package root the payload's own manifest states it is. host_can_serve reads the declaration instead of refusing every arch-crossing glibc target; the runtime-closure rules state the premise they always had (the loader is the payload's; a process on this machine is not the one that would load it). Measured end to end: a plain C program builds as a dynamic aarch64 ELF and runs under qemu-aarch64 -L with its exit code intact (e2e 1076). A row whose system the graph supplies compiles its resources with the Windows headers the rows declare (e2e 1075); a program of another system that dies at once names the runner and returns its status unchanged (1077); pack --systems all answers with the platform's own data, FreeBSD's runner among them (1078, 986); longjmp joins threads and exceptions as the third abi member, the Emscripten long-jump model (1079). Stability-neutral: every default behavior is unchanged; each capability appears only where it previously failed. No host is named in the engine. --- CHANGELOG.md | 64 +++++++++++ README.md | 2 +- README.zh-CN.md | 2 +- docs/01-getting-started.md | 6 +- docs/10-pack-and-release.md | 11 ++ docs/20-toolchains.md | 24 ++-- docs/22-target-side.md | 12 +- mcpp.toml | 8 +- modules/manifest/src/toml.cppm | 18 ++- modules/manifest/src/types.cppm | 14 ++- modules/toolchain-model/data/builtin.toml | 16 ++- modules/versioning/src/version.cppm | 2 +- src/build/execute.cppm | 36 +++++- src/build/flags.cppm | 68 ++++++++--- src/build/prepare/base.cpp | 15 +++ src/build/prepare/config.cpp | 4 + src/build/prepare/toolchain.cpp | 25 ++++ src/build/prepare/windows_resources.cpp | 45 +++++++- src/build/resources.cppm | 24 +++- src/build/runner_lookup.cppm | 83 +++++++++++++- src/build/runtime_validation.cppm | 12 ++ src/pack/pack.cppm | 43 +++++-- src/pack/pipeline.cppm | 4 +- src/project.cppm | 13 ++- src/toolchain/registry.cppm | 22 +++- ..._windows_row_finds_its_resource_headers.sh | 62 ++++++++++ ..._gnu_cross_comes_from_a_sysroot_payload.sh | 64 +++++++++++ ...stem_that_dies_at_once_names_the_runner.sh | 44 +++++++ ...rries_the_runners_the_platform_declares.sh | 57 +++++++++ ...scripten_rows_longjmp_model_round_trips.sh | 108 ++++++++++++++++++ ...single_files_cover_the_legs_and_systems.sh | 22 +++- tests/e2e/run_all.sh | 10 +- tests/fixtures/target-rows.snapshot | 6 +- tests/unit/test_pack_modes.cpp | 11 +- tests/unit/test_runner_lookup.cpp | 45 ++++++++ tests/unit/test_toolchain_triple.cpp | 9 +- 36 files changed, 936 insertions(+), 75 deletions(-) create mode 100755 tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh create mode 100755 tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh create mode 100755 tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh create mode 100755 tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh create mode 100755 tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ca51646..81913d6a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.11.1] - 2026-11-11 + +### Added + +- The cross rows of the platform table are served from what their own data + names, not from a list of hosts written in the engine. `aarch64-linux-gnu` + takes its C library from the sysroot payload its row declares + (`xim:aarch64-linux-gnu-sysroot`, glibc 2.44 with the kernel headers, + libgcc and libc++ inside; the package root is the sysroot), so the row is + built wherever that payload is published — an x86_64 Linux host included — + and `host_can_serve` reads the declaration instead of refusing every + arch-crossing glibc target. The row links its GCC run-time in, as the musl + rows do: that sysroot carries no shared libgcc. When such a base package + serves the target, the host contributes nothing to the link: the payload's + own `-B`/`-L`/loader words and its `compiler_runtime_dir` resource + directory are the whole C-runtime group, on the C link exactly as on the + C++ one (before, the host's runtime directories and `-latomic` shadowed + the target's — an x86_64 `libatomic.a` the linker then called incompatible + with aarch64 — and a C link could not find the payload's builtins at all), + and the runtime-closure rules state the premise they always had: the + artifact's loader is the payload's, a process on this machine is not the + one that would load it. The test that holds the row skips itself until the + package index carries the payload. +- `longjmp` joins `threads` and `exceptions` as a member of + `[target..abi]`: how `sigsetjmp`/`siglongjmp` cross the + WebAssembly boundary on an Emscripten row. `"wasm"` renders + `-sSUPPORT_LONGJMP=wasm` on every C and C++ translation unit and on the + link; `"js"` states the toolchain's default model the same way. Declared + on any other row the member is refused naming that fact. A program whose + non-local exits run deep still wants `-sSTACK_SIZE` — a project knob of + its own, not an ABI fact, so the engine does not render it. +- `mcpp pack --systems all` answers with data rather than a fixed list: this + machine's system, every system a platform defines a row of for the + architecture, and every system the platform's runner states a host of + (`runner.hosts`) — FreeBSD's among them. A FreeBSD runner is built as + `-freebsd-musl` over the platform's hosted arrangement, the same + static identity a FreeBSD request takes. The default pack is unchanged: + without `--systems` it carries the runner of the packing machine's system + alone. +- A Windows row whose system the graph supplies compiles its resources with + the Windows headers the platform's rows declare. `x86_64-windows-musl` has + no paired payload and its llvm binary carries no headers of its own, so a + script including `"winres.h"` failed in llvm-rc; the MinGW-w64 sysroot + payload a row declares for the architecture's GNU-ABI identity supplies + them, and every PE row shares that set. The two existing include sources + keep their precedence. +- The llvm line moves to `llvm@23.1.3-r2`: the same payload with the MinGW + cross builtins beside it, repackaged under its own version key because the + index keeps an existing version's entry immutable. A plain C link for + `aarch64-windows-gnu` now needs no second payload. + +### Changed + +- A program of another system's kernel interface that starts and dies at + once is told apart from a crash in a working program. `mcpp run` of such + an artifact — a static FreeBSD ELF of the host's own architecture, spawned + by a kernel that does not ask which system it names — now runs it as a + child, so the death by signal is observed and named with the + `[target.]` runner declaration, and the status itself is returned + unchanged: 139 stays 139. A spawn refusal whose errno names only a missing + interpreter (ENOENT, a dynamic linker of the other system) carries the + same declaration. Native targets keep the launch they always had, and a + native signal death is never interpreted. + ## [2026.11.10.2] - 2026-11-10 ### Added diff --git a/README.md b/README.md index 466fd4e08..70e60b194 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ list` reports for this machine): | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23 — Cortex-A 32-bit, the first row with an MMU ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23 — bare metal, no C library by default ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | | `aarch64-windows-gnu` | llvm 23 with `xim:mingw-w64-sysroot` (llvm-mingw's target side) — built on any host; the same base serves `x86_64-windows-gnu` where no gcc payload exists | preview | | `x86_64-freebsd` · `aarch64-freebsd` | the platform that serves them, over `xim:-freebsd-sysroot` ([22](docs/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23 — a static FreeBSD program with a musl C library over openkal-freebsd, built on any host *(what `-freebsd` is with no platform chosen)* | preview | diff --git a/README.zh-CN.md b/README.zh-CN.md index cb61423a5..92083e472 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -402,7 +402,7 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`( | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23;Cortex-A 32 位,第一个带 MMU 的目标 ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23;裸机,默认不带 C 库 ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | | `aarch64-windows-gnu` | llvm 23 加 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧);任意宿主可构建;没有 gcc 载荷之处同一基座也服务 `x86_64-windows-gnu` | preview | | `x86_64-freebsd` · `aarch64-freebsd` | 服务它们的平台,基于 `xim:-freebsd-sysroot`([22](docs/zh/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23;openkal-freebsd 之上带 musl C 库的静态 FreeBSD 程序,任意宿主可构建 *(未选平台时 `-freebsd` 即此身份)* | preview | diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md index ce92416d4..84fbadba4 100644 --- a/docs/01-getting-started.md +++ b/docs/01-getting-started.md @@ -42,10 +42,10 @@ the host: | host | default | |---|---| | Linux x86_64 | `gcc@16.1.0` | -| Linux aarch64 | `llvm@23.1.3` | +| Linux aarch64 | `llvm@23.1.3-r2` | | other Linux architectures | `gcc@15.1.0-musl` | -| macOS | `llvm@23.1.3` | -| Windows with usable MSVC | `llvm@23.1.3` | +| macOS | `llvm@23.1.3-r2` | +| Windows with usable MSVC | `llvm@23.1.3-r2` | | Windows without it | `gcc@16.1.0` for `x86_64-windows-gnu` | The Linux aarch64 default applies from 2026.10.8.1. Existing configured diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 1df48eea8..e0fb56e5a 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -857,6 +857,17 @@ names no `windows-aarch64`, so `aarch64-windows-gnu`, a row this mcpp builds, gets no runner from it). An image built on a Windows host is not the bytes of one built elsewhere (#850): pack trees on Linux or macOS. +**What `all` names is data, not a list (mcpp 2026.11.11.1+).** The machine's +own system, every system a platform defines a row of for the architecture, and +every system the platform's runner states a host of for it (`runner.hosts`): +a platform whose runner exists for `freebsd-` gets FreeBSD's runners, +built as `-freebsd-musl` over the platform's hosted arrangement — the +same static identity a FreeBSD request takes — into +`libexec//freebsd-/`. A pack without a platform answers with +the machine's own system alone, and `--systems` naming an `` or an +`-` keeps its exact meaning; only an explicit `--systems all` (or +`[pack] systems`) ever packs another system. + **Where it runs.** The pack writes `runs_on` into `mcpp-package.json` and says it after `Packed`: `self`, the `-` pairs the package runs on with what it carries, and `with.`, the pairs it runs on where the machine diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index f4db148ee..c699ae62e 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -24,19 +24,19 @@ host-aware: - Linux x86_64 uses `gcc@16.1.0` for the native glibc ABI, so X11, OpenGL, and system libraries work out of the box. -- Linux aarch64 uses `llvm@23.1.3` for the native glibc ABI (2026.10.8.1+). +- Linux aarch64 uses `llvm@23.1.3-r2` for the native glibc ABI (2026.10.8.1+). - Other Linux architectures use `gcc@15.1.0-musl`, a self-contained static toolchain. -- macOS uses `llvm@23.1.3`. -- Windows with a usable MSVC installation uses `llvm@23.1.3` for the MSVC ABI. +- macOS uses `llvm@23.1.3-r2`. +- Windows with a usable MSVC installation uses `llvm@23.1.3-r2` for the MSVC ABI. Without usable MSVC, it uses `gcc@16.1.0` with target `x86_64-windows-gnu` (MinGW-w64, static by default), says which half of the MSVC ABI is missing — Visual Studio's C++ tools or the Windows SDK — and where it looked, and records the pair as the machine's default. Once both halves - are present, `mcpp toolchain default llvm@23.1.3` moves the machine to the + are present, `mcpp toolchain default llvm@23.1.3-r2` moves the machine to the MSVC ABI. - Android (Termux) uses the default of GNU/Linux on its architecture: on - aarch64, `llvm@23.1.3` with the managed glibc. Android 10 or later is + aarch64, `llvm@23.1.3-r2` with the managed glibc. Android 10 or later is required (the shell is reached as `/bin/sh`), with xlings 2026.10.10.2 or later, which unpacks payloads in the app sandbox, where hard links cannot be created. @@ -78,7 +78,7 @@ this model with a one-line `note:` hint. ```bash mcpp toolchain install gcc 16.1.0 # host target (GNU libc on Linux) -mcpp toolchain install llvm 23.1.3 # LLVM/Clang, default on macOS and Windows with usable MSVC +mcpp toolchain install llvm 23.1.3-r2 # LLVM/Clang, default on macOS and Windows with usable MSVC mcpp toolchain install gcc 16 --target x86_64-linux-musl # musl target payload mcpp toolchain install --target x86_64-windows-gnu # family omitted → the # target's convention pin (gcc@16.1.0) @@ -133,14 +133,14 @@ configs with combined spellings like `default = "gcc@15.1.0-musl"` keep working unchanged.) On native Linux aarch64 (2026.10.8.1+), an explicit migration selects both -LLVM 23.1.3 and the GNU target after the native payload is published: +LLVM 23.1.3-r2 and the GNU target after the native payload is published: ```bash -mcpp toolchain install llvm 23.1.3 -mcpp toolchain default llvm@23.1.3 --target aarch64-linux-gnu +mcpp toolchain install llvm 23.1.3-r2 +mcpp toolchain default llvm@23.1.3-r2 --target aarch64-linux-gnu ``` -The command records `llvm@23.1.3` and `aarch64-linux-gnu` as the default pair. +The command records `llvm@23.1.3-r2` and `aarch64-linux-gnu` as the default pair. Existing configurations remain effective until explicitly changed; project manifest declarations retain their precedence. @@ -166,7 +166,7 @@ the required family for that build: ``` $ mcpp build Resolving toolchain - Resolved llvm@23.1.3 → …/xim-x-llvm/23.1.3/bin/clang++ + Resolved llvm@23.1.3-r2 → …/xim-x-llvm/23.1.3-r2/bin/clang++ required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`), not your gcc@16.1.0 — this project only ``` @@ -212,7 +212,7 @@ The output has two blocks — one per axis: Toolchains: * gcc 16.1.0 (default) gcc 15.1.0 - llvm 23.1.3 + llvm 23.1.3-r2 Targets: TARGET NOTE TOOLCHAIN STATUS diff --git a/docs/22-target-side.md b/docs/22-target-side.md index ae6f7e205..f79be1572 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1363,8 +1363,9 @@ package needs. |---|---|---|---| | `threads` | boolean | `-pthread` on a target that is neither PE nor freestanding; nothing on PE and on freestanding targets | the standard library module prebuild, the dependency scan, every C and C++ translation unit of every package, and the link | | `exceptions` *(mcpp 2026.9.12.3+)* | boolean, or `"wasm"` / `"js"` *(2026.10.30.1+)* | on `os = "emscripten"` only, on the compile line through the dialect flags and on the link: `"wasm"` is `-fwasm-exceptions`, WebAssembly's own exception handling; `"js"` and `true` are `-fexceptions`, the JavaScript-based model. Nothing on every other target, where exceptions are already the default | the same set `threads` reaches | +| `longjmp` *(mcpp 2026.11.11.1+)* | `"wasm"` or `"js"` | on `os = "emscripten"` only, on the compile line of every C and C++ translation unit and on the link: `"wasm"` is `-sSUPPORT_LONGJMP=wasm`, WebAssembly's own stack switching; `"js"` is `-sSUPPORT_LONGJMP=emscripten`, the JavaScript model and the toolchain's default. Declared on any other row, it is refused. An absent member renders nothing | the same set `threads` reaches | -Both members enter the dependency cache key through the dialect flags, so a +All three members enter the dependency cache key through the flags they render, so a dependency built without one is never reused by a build with it, and one built with one exception model is never reused by a build with the other. An unknown member, and a value outside the table, are refused naming the values. @@ -1387,6 +1388,15 @@ enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.) ``` +**`longjmp = "wasm"` is what makes C's `sigsetjmp`/`siglongjmp` work under +Emscripten.** With the toolchain's default JavaScript model the pair compiles +into a stack copy, and a non-local exit over two call levels reads past the +module's stack and dies at run time. With `-sSUPPORT_LONGJMP=wasm` on both the +compiles and the link, the pair round-trips. A program whose non-local exits +run deep also wants a bigger stack; that is a project knob of its own +(`ldflags = ["-sSTACK_SIZE=..."]`), not an ABI fact, so the engine does not +render it. + **Only the root manifest decides.** The switch belongs to the artefact, and the root is the only package that builds one. A dependency that writes `[target..abi]` is reported (`abi/dependency-table`) and changes diff --git a/mcpp.toml b/mcpp.toml index c56ce4699..7ddc633f4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.10.2" +version = "2026.11.11.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] @@ -66,11 +66,11 @@ programs = "directory" # (mcpp#669). Linux x86_64 retains GCC; native ARM64 uses LLVM and glibc. [toolchain] default = "gcc@16.1.0" -macos = "llvm@23.1.3" -windows = "llvm@23.1.3" +macos = "llvm@23.1.3-r2" +windows = "llvm@23.1.3-r2" [target.aarch64-linux-gnu] -toolchain = "llvm@23.1.3" +toolchain = "llvm@23.1.3-r2" # Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the # four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 16.1 + full diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index f120a9ceb..8f913df36 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -4481,14 +4481,28 @@ std::expected parse_document(const t::Document& documen "[target.{}].abi must be a table, e.g. `[target.{}.abi]` " "with `threads = true`", triple, triple))); for (auto& [ak, av] : ait->second.as_table()) { - if (ak != "threads" && ak != "exceptions") + if (ak != "threads" && ak != "exceptions" && ak != "longjmp") return std::unexpected(error(origin, std::format( "[target.{}.abi] has no member '{}'; the members are: " - "threads, exceptions", triple, ak))); + "threads, exceptions, longjmp", triple, ak))); // `exceptions` also takes the model on Emscripten (design // 2026-10-29 §6): "wasm" or "js"; `true` keeps "js". const bool model = ak == "exceptions" && av.is_string() && (av.as_string() == "wasm" || av.as_string() == "js"); + // `longjmp` is a model only (2026.11.11.1): "wasm" or + // "js", no boolean -- the engine's default is the + // toolchain's own ("js"), and an absent member is that + // default rather than a switch turned off. + if (ak == "longjmp") { + if (!av.is_string() + || (av.as_string() != "wasm" && av.as_string() != "js")) + return std::unexpected(error(origin, std::format( + "[target.{}.abi].longjmp must be \"wasm\" or \"js\"", + triple))); + cc.abiLongjmpModel = av.as_string(); + cc.abiLongjmpDeclared = true; + continue; + } if (!av.is_bool() && !model) return std::unexpected(error(origin, ak == "exceptions" ? std::format("[target.{}.abi].exceptions must be true, false, " diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d07c3bf97..d1673b65c 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1173,6 +1173,15 @@ struct BuildConfig : BuildInputs { // "wasm" (`-fwasm-exceptions`), "js" (`-fexceptions`), or empty for // `exceptions = true`, which keeps its meaning since 2026.9.12.3: "js". std::string abiExceptionsModel; + // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary on an + // Emscripten row. A model, not a switch: "wasm" (`-sSUPPORT_LONGJMP=wasm`, + // WebAssembly's own stack switching) or "js" (`-sSUPPORT_LONGJMP=emscripten`, + // the JavaScript model, which is also the toolchain's default). The root's + // statement only, rendered into every translation unit and the link, like + // `threads`; the model reaches every cache key through those flags. + bool abiLongjmpDeclared = false; + std::string abiLongjmpModel; std::string cStandard; // Escape hatch for the hermetic link check: a sandbox toolchain whose // CRT/loader resolve OUTSIDE the sandbox is a hard error by default @@ -1732,6 +1741,9 @@ struct ConditionalConfig { bool abiExceptions = false; bool abiExceptionsDeclared = false; std::string abiExceptionsModel; + // `[target..abi] longjmp` -- see BuildConfig::abiLongjmpModel. + bool abiLongjmpDeclared = false; + std::string abiLongjmpModel; // `[target..build] dialect_cxxflags` -- a GRAPH-WIDE switch under a // target condition (issue #717, design 2026-09-27 §6). Like `abiThreads`/ // `abiExceptions` above, only the ROOT's value is rendered; unlike them it @@ -1846,7 +1858,7 @@ inline bool is_empty(const ConditionalConfig& c) { && c.buildDependencies.empty() && c.featureDeps.empty() && c.targetKinds.empty() && c.xlings.empty() && c.xlings.overrides.empty() - && !c.abiThreadsDeclared && !c.abiExceptionsDeclared + && !c.abiThreadsDeclared && !c.abiExceptionsDeclared && !c.abiLongjmpDeclared && !c.requiresAbiThreads && !c.requiresAbiExceptions && c.requires_.empty() && c.privateRequires.empty() && c.featureRequiresAbiThreads.empty() && c.featureRequiresAbiExceptions.empty() diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 4629e010d..38368dc87 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -418,7 +418,8 @@ standard = true # base package beside llvm where a payload of the system's own exists too, 5. # glibc from the payload, at the host's default toolchain natively. A cross to -# another architecture's glibc pins llvm; the payload exists for the host's arch. +# another architecture's glibc pins llvm and names the sysroot payload its C +# library comes from (the aarch64 row below; x86_64's hosts list admits it). [[platform.provider]] name = "glibc" serves = ["x86_64-linux-gnu"] @@ -429,8 +430,19 @@ hosts = ["linux-{arch}"] name = "glibc" serves = ["aarch64-linux-gnu"] priority = 100 -hosts = ["linux-{arch}"] +# The payload: native on each Linux arch; the cross from x86_64 takes the C +# library from a sysroot payload the row names (2026.11.11.1, G1) -- llvm +# alone carries no glibc of another architecture, and the host's is not the +# target's. The payload's package root IS the sysroot (glibc 2.44 with the +# kernel headers, libgcc and libc++ inside), so the row is served wherever +# that payload is published, with no host named here beyond the data. +hosts = ["linux-{arch}", "linux-x86_64"] toolchain = "llvm@23.1.3" +sysroot = "xim:aarch64-linux-gnu-sysroot@2.44.3" +# glibc's own linkage is dynamic; the compiler run-time is the payload's +# (libgcc's archives and compiler-rt's builtins, both static), so a link +# takes nothing from the host. +linkage = "dynamic" # The musl cross payloads: native on each Linux arch, every arch from x86_64, # and the Canadian cross on a Windows host of the same arch. diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index b05648255..1ca3d45db 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.10.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.11.1"; } // namespace mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index ad4cb0065..eed11b290 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -3493,8 +3493,32 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, emit_launch(env); return 0; } + // A PROGRAM OF ANOTHER SYSTEM'S KERNEL INTERFACE, of this machine's own + // architecture, STARTS AND DIES ON ITS FIRST KERNEL CALL (G6): the kernel + // maps a static foreign ELF without asking which system it names, the + // program then states a syscall in that system's vocabulary, and the death + // is by signal — where mcpp, having become the program on POSIX, can say + // nothing at all. Such an artifact is run as a child instead, so its death + // is observed, named with the [target.] runner declaration, and + // its status returned unchanged: 139 stays 139, exactly what a direct + // launch would have reported. Every other launch — a declared runner, + // `run_under`, a native or cross-arch target — keeps the launch it always + // had, and a native signal death is never interpreted: that death is the + // program's own. + const bool foreignKernel = [&] { + if (!choice.tmpl.empty() || !host.prefix.empty()) return false; + auto t = mcpp::toolchain::triple::parse(choice.tripleKey); + if (!t || t->os.empty()) return false; + const std::string_view here = mcpplibs::os::is_windows ? "windows" + : mcpplibs::os::is_macos ? "macos" : "linux"; + return t->os != here && t->arch == mcpp::toolchain::platform::host_arch; + }(); int spawnErr = 0; - const int rc = mcpp::run::run_foreground(argv, childEnv, &spawnErr); + int rc = 0; + if (foreignKernel) + rc = mcpp::run::run_exec(argv, childEnv, &spawnErr); + else + rc = mcpp::run::run_foreground(argv, childEnv, &spawnErr); if (spawnErr != 0) { using namespace mcpp::build::runner_lookup; if (!choice.tmpl.empty() || !host.prefix.empty()) @@ -3502,10 +3526,20 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, else if (classify(spawnErr) == SpawnClass::Unloadable) std::println(libc::stderr(), "error: {}", unrunnable_message(choice.tripleKey, exe, spawnErr)); + else if (foreignKernel) + // The program was found and its interpreter was not (ENOENT, a + // dynamic linker of the other system): the bare errno names + // neither fact, so the same runner declaration follows it. + std::println(libc::stderr(), "error: {}{}", + spawn_failed_message(exe.string(), spawnErr), + runner_advice_text(choice.tripleKey)); else std::println(libc::stderr(), "error: {}", spawn_failed_message(exe.string(), spawnErr)); return launcher_status(spawnErr); } + if (foreignKernel && mcpp::build::runner_lookup::death_by_signal(rc)) + std::println(libc::stderr(), "error: {}", + mcpp::build::runner_lookup::signal_death_message(choice.tripleKey, exe, rc)); return rc; } diff --git a/src/build/flags.cppm b/src/build/flags.cppm index d0363ef51..85df28235 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1017,8 +1017,30 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { link_toolchain_flags += " -fuse-ld=lld"; link_toolchain_flags_c = link_toolchain_flags; } else { - if (lm.mode == mcpp::toolchain::CLibMode::Sysroot) + // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot + // form): the host's model contributes nothing, exactly as when the + // graph supplies it -- the base's own -B/-L/loader words reach the + // line through the payload channel, and this machine's runtime + // directories would shadow the target's (measured: the host payload's + // x86_64 libatomic.a resolved ahead of the sysroot's, and lld called + // it incompatible with aarch64linux). + if (lm.mode == mcpp::toolchain::CLibMode::Sysroot + && plan.toolchain.base.layers.cAbi.empty()) link_toolchain_flags += lm.link_flags(ninjaEsc); + + // THE BASE'S ROOT IS A SYSROOT (the gnu-sysroot form): name it to + // the driver, and the ld scripts inside it (/lib GROUPs), its crt + // objects and its loader all resolve the way a --sysroot link + // resolves them -- the payload's own mcpp.toml states the package + // root IS the sysroot, and without the flag a dynamic link reads + // /lib/libc.so.6 against this machine's root instead. + if (!plan.toolchain.base.layers.cAbi.empty()) { + std::filesystem::path baseDir = plan.toolchain.base.root; + if (baseDir.empty() && plan.toolchain.base.payload) + baseDir = plan.toolchain.base.payload->glibcLib.parent_path(); + if (!baseDir.empty()) + link_toolchain_flags += " --sysroot=" + ninjaEsc(baseDir); + } link_toolchain_flags_c = link_toolchain_flags + std::string(mcpp::toolchain::ClangDriverModel::kLinkDriverFlagsC); // A graph-supplied libstdc++ under the payload's clang: the driver @@ -1033,9 +1055,9 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { } f.sysroot = link_toolchain_flags; } else if (lm.mode != mcpp::toolchain::CLibMode::None) { - // GCC (or Clang without cfg): --sysroot from probe, or the payload - // headers + C runtime (-B for crt discovery, -L for -lc/-lm). - link_toolchain_flags = crossTarget + lm.link_flags(ninjaEsc); + if (lm.mode == mcpp::toolchain::CLibMode::Sysroot + && plan.toolchain.base.layers.cAbi.empty()) + link_toolchain_flags += lm.link_flags(ninjaEsc); link_toolchain_flags_c = link_toolchain_flags; // nothing C++-only here f.sysroot = link_toolchain_flags; } else if (!crossTarget.empty() && ownSysrootTarget) { @@ -1895,11 +1917,18 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { std::string runtime_dirs; if constexpr (mcpp::toolchain::platform::supports_rpath) { - // Toolchain runtime dirs (glibc/gcc) as before... - for (auto& dir : plan.toolchain.linkRuntimeDirs) { - runtime_dirs += " -L" + escape_path(dir); - runtime_dirs += " -Wl,-rpath," + escape_path(dir); - } + // Toolchain runtime dirs (glibc/gcc) as before -- but not for a + // target whose C library a row's base package serves (the + // gnu-sysroot form): those directories are THIS MACHINE's, and on a + // cross they would both shadow the target's libraries (-L precedes + // the base's own) and bake a host RUNPATH into the target's + // artifact. The base's directories reach the line through its own + // channel. + if (plan.toolchain.base.layers.cAbi.empty()) + for (auto& dir : plan.toolchain.linkRuntimeDirs) { + runtime_dirs += " -L" + escape_path(dir); + runtime_dirs += " -Wl,-rpath," + escape_path(dir); + } // Dependency/runtime provider search comes from LinkIntent and is // rendered separately. In particular runtimeSearchDirs contributes // RUNPATH only; it must never become a link-time -L path. @@ -1982,16 +2011,19 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // A BASE PACKAGE'S RUN-TIMES ON THE LINK (design 2026-11-10 E5): where its // compiler runtime stands (`compiler_runtime_dir`: clang reads it as its // resource directory, gcc through `-B`), and, when it supplies the C++ - // layer, its `ldflags` in place of the driver's own C++ library, which - // names the toolchain's. - auto base_link_words = [&] { + auto base_link_words = [&](bool cxx) { std::string words; auto const& L = plan.toolchain.base.layers; if (!L.compilerRuntimeDir.empty()) words += plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang ? " -resource-dir=" + ninjaEsc(L.compilerRuntimeDir) : " -B" + ninjaEsc(L.compilerRuntimeDir); - if (!L.cxx.empty() && !L.ldflags.empty()) { + // The C++ library words are the C++ link's alone; the run-time + // directory above is as much a C decision -- a C link resolves + // __udivti3 from the same builtins the C++ one does, and a base + // package that names one names it for every link (the same sentence + // `kLinkDriverFlagsC` states for the driver's own selection). + if (cxx && !L.cxx.empty() && !L.ldflags.empty()) { words += " -nostdlib++"; for (auto const& w : L.ldflags) words += " " + ninja_command_word(w); } @@ -2065,7 +2097,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { const std::string baseStatic = !plan.toolchain.pairedSysroot.empty() && !layers.cxx.empty() && f.linkage == "static" ? " -static" : ""; - f.ld += baseStatic + base_link_words(); + f.ld += baseStatic + base_link_words(true); // `-lstdc++exp` is named explicitly, so swapping g++ for gcc would not // drop it — the C line has to leave it out. f.ldC = std::format("{}{}{}{}{}{}", crossTarget, paired, link_intent_ld, @@ -2321,6 +2353,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // `--push-state`/`--pop-state` pair), which is what sent a reader here. std::string atomic_ld = plan.targetSide.cAbi.prebuilt() + && plan.toolchain.base.layers.cAbi.empty() ? atomic_link_flag(plan.toolchain.linkRuntimeDirs, !full_static.empty()) : std::string{}; @@ -2699,7 +2732,12 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { } // A BASE PACKAGE'S RUN-TIMES (design 2026-11-10 E5). - f.ld += base_link_words(); + f.ld += base_link_words(true); + // The base's run-time directory reaches the C link too (2026.11.11.1): + // a hosted cross over a sysroot payload links its compiler run-time from + // the payload's own resource directory, and the C program's link is the + // one that had nowhere to find it. + f.ldC += base_link_words(false); // A LINKER STATED BY ROLE (mcpp#755, `tools = { ld = ... }`), AFTER EVERY // SHAPE HAS BUILT ITS LINE. diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index 51795fd10..c12d1388a 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -538,6 +538,21 @@ std::expected step2_resolve_base(PrepareState& state) { && !fs::exists(dir / "usr" / "include" / "linux" / "limits.h", ec)) tc.base.addedIncludes.push_back(ecosystemLinux); } + + // A LOADER INSIDE A CROSS BASE IS THE SYSROOT'S, NOT THIS MACHINE'S + // (2026.11.11.1): a --sysroot link names it sysroot-relative + // (/lib/ld-linux-aarch64.so.1), and so does the artifact's PT_INTERP -- + // the form qemu -L , a runner declaration and the ld scripts + // beside it (the base's own libc.so GROUPs /lib/...) all resolve. An + // absolute path would bake this machine's install directory into every + // image it links; the native case keeps its absolute path, as the + // Locator form's distro path above already states. + if (!native && !tc.base.loader.empty()) { + std::error_code lec; + if (auto rel = fs::relative(tc.base.loader, dir, lec); !lec && !rel.empty() + && rel.native().find("..") == std::string_view::npos) + tc.base.loader = fs::path("/") / rel; + } // A base package's own `provides` is its declaration, as a statement's is. if (tc.base.libc.empty() && !tc.base.layers.cAbi.empty()) tc.base.libc = tc.base.layers.cAbi; // ITS C++ RUN-TIME STANDS WHERE THE PAYLOAD'S WOULD (E5): the std module is diff --git a/src/build/prepare/config.cpp b/src/build/prepare/config.cpp index d55e58190..1dd4a485f 100644 --- a/src/build/prepare/config.cpp +++ b/src/build/prepare/config.cpp @@ -342,6 +342,10 @@ void merge_conditional_config(mcpp::manifest::Manifest& m, m.buildConfig.abiExceptionsModel = cc.abiExceptionsModel; m.buildConfig.abiExceptionsDeclared = true; } + if (cc.abiLongjmpDeclared) { + m.buildConfig.abiLongjmpModel = cc.abiLongjmpModel; + m.buildConfig.abiLongjmpDeclared = true; + } // `[target..build] dialect_cxxflags` (#717): recorded for every // package, like the abi switches above, but APPENDED rather than // replaced -- there is no "last matching section wins" here, because diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 859aec7bc..f5edafbbc 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1428,6 +1428,31 @@ static std::expected step1_device_axis_and_layer_merge(Prepar state.add_once(state.m->buildConfig.ldflags, flag); state.add_once(state.m->buildConfig.graphLdflags, flag); } + // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary. The flag + // changes the CODE emcc emits for the two calls, so it reaches both + // sides -- every translation unit's flags (C and C++ alike; the C library + // calls are what it is about) and the link, the channels `threads` uses. + // Declared on any row but Emscripten's it is refused, not ignored: the + // member names a WebAssembly model, and a build that wrote it for another + // target has a selector that matches the wrong row. + if (state.m->buildConfig.abiLongjmpDeclared) { + if (!emscripten) + return std::unexpected(std::format( + "[target..abi] longjmp states a WebAssembly long-jump model, " + "and the target '{}' is not an Emscripten row; the member belongs to " + "`[target.'cfg(os = \"emscripten\")'.abi]` alone", + state.overrides.target_triple.empty() + ? mcpp::toolchain::triple::host_triple().str() + : state.overrides.target_triple)); + const std::string flag = "-sSUPPORT_LONGJMP=" + + (state.m->buildConfig.abiLongjmpModel == "wasm" + ? std::string("wasm") : std::string("emscripten")); + state.add_once(state.m->buildConfig.dialectCxxflags, flag); + state.add_once(state.m->buildConfig.cflags, flag); + state.add_once(state.m->buildConfig.ldflags, flag); + state.add_once(state.m->buildConfig.graphLdflags, flag); + } // #851: emcc runs wasm-opt and minifies the JavaScript launcher AT THE // LINK, from the optimisation level the link is given; a link without // one is -O0 whatever the compiles were. The profile's level reaches it. diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 4039134a0..4c789209b 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -30,6 +30,8 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; import mcpp.build.linkage_form; // #519 — which form each dependency takes import mcpp.build.plan; import mcpp.build.schedule.policy; @@ -72,6 +74,40 @@ import mcpp.bmi_cache; namespace mcpp::build { +// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.11.1, G3): a row whose +// system the graph supplies -- `x86_64-windows-musl` over openkal -- has no +// paired payload, and its llvm binary carries no Windows headers, so +// `winres.h` reaches neither of the include sources the toolchain states. The +// MinGW-w64 sysroot payload a platform row declares for this architecture's +// GNU-ABI identity has them, under one `/include` for every +// architecture (E9's layout). Resolved through the payload channel the row's +// own sysroot goes through, and probed relative to what it resolves to -- +// never PATH. Empty when no row declares one or the payload is not here: the +// resource compile then fails with the tool's own missing-header text, which +// is the truthful message. +static std::filesystem::path +mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& trip) { + if (trip.os != "windows") return {}; + const auto gnuTriple = std::format("{}-windows-gnu", trip.arch); + for (auto const* p : mcpp::toolchain::target::providers()) { + if (p->sysroot.empty() || p->sysroot == "toolchain") continue; + if (!mcpp::toolchain::target::serves(*p, gnuTriple)) continue; + auto cfg = state.get_cfg(true); + if (!cfg) return {}; + mcpp::fetcher::Fetcher fetcher(**cfg); + mcpp::fetcher::InstallProgressHandler progress; + if (auto payload = fetcher.resolve_xpkg_path( + std::string(p->sysroot), mcpp::fetcher::PayloadIntent::Use, &progress)) + return payload->root / "include"; + auto xl = mcpp::config::make_xlings_env(**cfg); + if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), + mcpp::provider::parse_ref(p->sysroot))) + return *dir / "include"; + return {}; // one declared payload per row; the first provider wins + } + return {}; +} + // The resource compiler of the plan's resource units, and each unit's flags. static std::expected step13_resource_compiler(PrepareState& state, BuildContext& ctx) { @@ -100,9 +136,11 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; + const auto mingwHeaders = mingw_headers_root(state, trip); + // UTF-8 input, always. `[package]` metadata is user text and // routinely non-ASCII; without this llvm-rc refuses the script - // outright ("Non-ASCII 8-bit codepoint can't be interpreted in + // outright ("Non-ASCII 8-bit codepage can't be interpreted in // the current codepage") rather than mangling it, so a project // with a Chinese description could not build at all. // @@ -116,7 +154,7 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { const std::string ip = msvcStyle ? "/I" : "-I"; std::vector systemIncludes; if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos) - for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId)) + for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId, mingwHeaders)) systemIncludes.push_back(ip + dir.string()); for (auto& ru : ctx.plan.resourceUnits) { if (auto target = rsrc::coff_target_flag(*tool, state.tc->targetTriple); @@ -200,6 +238,7 @@ std::expected step13_windows_resources(PrepareState& state, B // windres), found as the compile step below finds it (E3). const auto rcTool = mcpp::build::resources::find_rc_tool(*state.tc, dialectId); const bool msvcStyle = rcTool ? rcTool->style == "msvc" : dialectId == "msvc"; + const auto mingwHeaders = mingw_headers_root(state, trip); for (auto const& S : subjects) { const auto& M = *S.m; @@ -385,7 +424,7 @@ std::expected step13_windows_resources(PrepareState& state, B // quoted `"winres.h"` is the toolchain's header, not a file // a build action produces (E3). auto scanIncludes = rcIncludes; - for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId)) + for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId, mingwHeaders)) scanIncludes.push_back(d); auto scan = rsrc::scan_rc(rcSrc, scanIncludes); if (scan.versionInfoNamedByString) { diff --git a/src/build/resources.cppm b/src/build/resources.cppm index 143c1fd55..4dbe50470 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -100,8 +100,17 @@ std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple); // MSVC backend states, and on a GNU-ABI row the paired payload's (or a cross // gcc's own) `-w64-mingw32/include` (design 2026-11-10 E3). One list for the // compile's `/I` and for the scan of what the script reads. +// +// `mingw_headers` is the third source (2026.11.11.1, G3): the include +// directory of the MinGW-w64 sysroot payload the platform's rows declare, +// which is where the Windows headers of a row whose system the graph supplies +// -- `x86_64-windows-musl`, no paired payload, an llvm binary that carries no +// headers of its own -- come from. Every PE row shares that one set. It is +// read ONLY when the two sources above found nothing, so a GNU-ABI row keeps +// its paired payload's headers and an MSVC row its SDK's. std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, - std::string_view dialectId); + std::string_view dialectId, + const std::filesystem::path& mingw_headers = {}); // Split a Windows environment list (PATH, INCLUDE, LIB) into its entries. // @@ -205,7 +214,8 @@ bool is_llvm_windres(const std::filesystem::path& p) { } std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, - std::string_view dialectId) { + std::string_view dialectId, + const std::filesystem::path& mingw_headers) { std::vector out; for (auto const& ev : tc.envOverrides) { if (ev.key != "INCLUDE") continue; @@ -233,6 +243,16 @@ std::vector system_include_dirs(const mcpp::toolchain::To // built, not where it is). add_triple_dirs(tc.binaryPath.parent_path().parent_path()); } + // THE THIRD SOURCE, ONLY WHERE THE TWO ABOVE FOUND NOTHING: a PE row whose + // system the graph supplies has neither a paired payload nor a cross gcc, + // and its llvm payload carries no Windows headers -- but the MinGW-w64 + // sysroot payload the rows declare does, with every architecture's headers + // under one `/include` (E9's layout). The caller resolved that + // payload; this stays a probe beside the directories it already probed, + // never a search of PATH. + if (out.empty() && !mingw_headers.empty() + && std::filesystem::is_directory(mingw_headers, ec)) + out.push_back(mingw_headers); return out; } diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 84e66e055..2577629a7 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -20,11 +20,13 @@ // // Design: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md §4.3-4.4. +module; +#include + export module mcpp.build.runner_lookup; import std; import mcpplibs.os.platform; - export namespace mcpp::build::runner_lookup { struct Lookup { @@ -168,4 +170,83 @@ inline std::string unrunnable_message(std::string_view triple, artifact.string(), errno_text(e), e, triple, triple); } +// ─── Started, and dead at once ──────────────────────────────────────────── +// +// A SPAWN THAT SUCCEEDS CAN STILL DIE BEFORE IT DOES ANYTHING. The kernel +// maps a static ELF of its own architecture without reading the system the +// ELF names — FreeBSD's OSABI byte is not a question Linux asks — so the +// program runs, states its first kernel interface call in its own system's +// vocabulary, and the kernel answers with a signal: SIGSEGV or SIGILL for a +// syscall number that maps nowhere, SIGBUS for a stack it never grew, +// SIGABRT where the C library gave up first. `mcpp::run` reports the exit +// status a shell would (128 + the signal), and the status is the only fact +// that distinguishes this death from a crash in a working program — which is +// why the classification is asked about artifacts of another system only, +// never about a native one, where the same status is the program's own bug +// and advice about runners would be noise on top of it. +inline bool death_by_signal(int rc) { + switch (rc) { + case 128 + SIGSEGV: + case 128 + SIGILL: + case 128 + SIGABRT: +#ifdef SIGBUS + case 128 + SIGBUS: +#endif + return true; + default: + return false; + } +} + +// The signal's name, for a message a reader can grep; empty for a status the +// classification did not take. +inline std::string_view signal_of_death(int rc) { + switch (rc) { + case 128 + SIGSEGV: return "SIGSEGV"; + case 128 + SIGILL: return "SIGILL"; + case 128 + SIGABRT: return "SIGABRT"; +#ifdef SIGBUS + case 128 + SIGBUS: return "SIGBUS"; +#endif + default: return {}; + } +} + +// What an artifact of another system's kernel interface says when it dies at +// once: the [target.] runner declaration, the same template +// `unrunnable_message` prints, under a first line that states the death and +// the fact of the build rather than guessing at a diagnosis. +inline std::string signal_death_message(std::string_view triple, + const std::filesystem::path& artifact, int rc) { + return std::format( + "'{}' was started and died at once ({}, status {}): it was built for " + "'{}', another system's kernel interface, and this machine cannot " + "execute it directly.\n" + " Declare how to run it here:\n" + "\n" + " [target.{}]\n" + " runner = [\"qemu-aarch64-static\"]\n" + "\n" + " The artifact path is appended, or substituted for `{{}}` if the " + "template contains it.\n" + " A host that can execute it directly may pass --no-runner.", + artifact.string(), signal_of_death(rc), rc, triple, triple); +} + +// The same declaration, appended to a spawn refusal whose bare text names +// only the errno (an interpreter of another system the kernel could not +// find, ENOENT) — the reader's remedy is the same either way. +inline std::string runner_advice_text(std::string_view triple) { + return std::format( + "\n The artifact was built for '{}'. Declare how to run it here:\n" + "\n" + " [target.{}]\n" + " runner = [\"qemu-aarch64-static\"]\n" + "\n" + " The artifact path is appended, or substituted for `{{}}` if the " + "template contains it.\n" + " A host that can execute it directly may pass --no-runner.", + triple, triple); +} + } // namespace mcpp::build::runner_lookup diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index fb2b960be..fe7b326e6 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -684,6 +684,18 @@ ValidationReport validate_changed_artifacts( record_without_rules(plan, before); return report; } + + // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot form, + // 2026.11.11.1): the artifact's loader and libc are the payload's, and + // the process these rules describe -- one process on this machine with + // this machine's binding -- is not the one that would load it. The row's + // declaration is the statement, exactly as an SDK's is above; the + // closure belongs to the payload that served the link, and a runner + // declaration (or qemu) is how such an artifact runs. + if (!plan.toolchain.base.layers.cAbi.empty()) { + record_without_rules(plan, before); + return report; + } // THE ARTIFACT'S OWN SYSTEM DECIDES (mcpp#911 G5): a row a platform defines // states where its artifacts run, and an artifact for another system is // judged by that system's loader, not this machine's. Its closure is not diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index a3d4bca69..1b769fa27 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -568,7 +568,16 @@ std::string unbuilt_row(std::string_view system, std::string_view arch); // The systems `--systems` names for an architecture (design 2026-10-29 // §4.1): an ``, an `-` of that architecture, or every system // for `all`; this machine's system when it names none. -std::vector systems_for(const std::vector& stated, std::string_view arch); +// +// `all` is the DATA's answer (2026.11.11.1, G5), not a list spelled here: +// this machine's system, every system a platform defines a row of for the +// architecture (mcpp#911 G10), and every system the platform's runner states +// a host of for it (`runner.hosts`) -- which is where FreeBSD enters, a +// runner the platform builds for `freebsd-`. A pack without a platform +// answers with the machine's own system alone; `--systems` naming an `` +// keeps its exact meaning. +std::vector systems_for(const std::vector& stated, std::string_view arch, + const mcpp::platform::Resolved* P = nullptr); // The single files of an image (design 2026-10-29 §4.5): one per leg and // system of the leg's architecture, the legs in the plan's order. `skipped` @@ -2390,6 +2399,16 @@ ProgramRow program_row(std::string_view system, std::string_view arch, bool sing if (t && t->os == system && t->arch == arch) return { t->str(), "static", platform_ref_of(*P) }; } + // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.11.1, + // G5), as FreeBSD's own request takes it (design 2026-11-10 E4): + // `*-freebsd-musl`, openkal-musl over openkal-freebsd, one static + // program with no C library of the system beneath -- built over the + // platform's hosted arrangement wherever the machine that packs it is, + // needing no FreeBSD and no SDK. + if (system == "freebsd") { + const auto triple = std::format("{}-freebsd-musl", arch); + return { triple, "static", hosted_platform_for(P, triple) }; + } return { std::format("{}-{}", arch, system), "static", {} }; } if (system == "linux") { @@ -2756,7 +2775,7 @@ run_loaded(const Plan& plan) } if (plan.opts.mode == Mode::BundleAll) { for (auto const& leg : legs) - for (auto const& system : mcpp::pack::systems_for(plan.opts.systems, leg.arch)) { + for (auto const& system : mcpp::pack::systems_for(plan.opts.systems, leg.arch, &plan.platform)) { // A row this mcpp builds no program for (a planned // target, `x86_64-macos`) carries no runner: said, and // the package's metadata lists the systems it carries. @@ -3509,7 +3528,8 @@ std::expected finalize_staged(const Plan& plan) { return detail::finalize_tree(plan); } -std::vector systems_for(const std::vector& stated, std::string_view arch) { +std::vector systems_for(const std::vector& stated, std::string_view arch, + const mcpp::platform::Resolved* P) { if (stated.empty()) return { std::string(detail::host_system()) }; std::vector out; auto add = [&](std::string os) { @@ -3517,12 +3537,21 @@ std::vector systems_for(const std::vector& stated, std }; for (auto const& e : stated) { if (e == "all") { - for (auto os : { "linux", "macos", "windows" }) add(os); - // And every system the platforms of this process define a row of - // for the architecture (mcpp#911 G10). + // This machine's system, every system the platforms of this + // process define a row of for the architecture (mcpp#911 G10), + // and every system the carrier's runner states a host of for it + // (2026.11.11.1, G5) -- the rows the platform itself says its + // runner exists for, FreeBSD's among them. + add(std::string(detail::host_system())); for (auto const* r : mcpp::toolchain::target::rows()) if (r->defined && r->arch == arch && !r->os.empty() && r->os != "none") add(std::string(r->os)); + if (P && P->active) + for (auto const& h : P->fields.runner.hosts) { + const auto dash = h.find('-'); + if (dash == std::string_view::npos) continue; + if (h.substr(dash + 1) == arch) add(std::string(h.substr(0, dash))); + } continue; } const auto dash = e.find('-'); @@ -3538,7 +3567,7 @@ std::vector single_file_rows(const Plan& plan) { const bool all = std::ranges::find(plan.opts.systems, "all") != plan.opts.systems.end(); std::vector rows; for (auto const& arch : archs) - for (auto const& system : systems_for(plan.opts.systems, arch)) { + for (auto const& system : systems_for(plan.opts.systems, arch, &plan.platform)) { SingleFileRow r{ system, arch, {} }; if (all) if (auto row = unbuilt_row(system, arch); !row.empty()) diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 683732810..0a4631128 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -964,7 +964,7 @@ std::optional check_format(PackRun& run, GroupJob& g) { std::vector unbuilt, built; const bool all = std::ranges::find(run.opts.systems, "all") != run.opts.systems.end(); for (auto const& arch : archs) - for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch)) { + for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch, &ctx.plan.platform)) { auto row = mcpp::pack::unbuilt_row(system, arch); if (row.empty()) built.push_back(std::format("{}-{}", system, arch)); else if (!all) unbuilt.push_back(std::format("{}-{} ({})", system, arch, row)); @@ -1256,7 +1256,7 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { std::error_code xec; const bool windowsLauncher = std::filesystem::is_regular_file(plan->stagingRoot / "bin" / (stem + ".exe"), xec); for (auto const& arch : archs) - for (auto const& system : carries ? mcpp::pack::systems_for(opts.systems, arch) + for (auto const& system : carries ? mcpp::pack::systems_for(opts.systems, arch, &plan->platform) : std::vector{}) { const auto pair = std::format("{}-{}", system, arch); if (covered(pair)) continue; diff --git a/src/project.cppm b/src/project.cppm index 4c5e9619b..a96f8d347 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,14 +1095,17 @@ export std::string root_position_key( auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; std::string abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" + b.abiExceptionsModel : "0") : "-"; + std::string abiLongjmp = b.abiLongjmpDeclared ? b.abiLongjmpModel : "-"; if (applies) for (auto const& cc : m.conditionalConfigs) { - if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared + && !cc.abiExceptionsDeclared && !cc.abiLongjmpDeclared) continue; if (!applies(cc.predicate)) continue; dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" + cc.abiExceptionsModel : "0"; + if (cc.abiLongjmpDeclared) abiLongjmp = cc.abiLongjmpModel; } list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); @@ -1116,6 +1119,7 @@ export std::string root_position_key( field("ios_deployment_target", b.iosDeploymentTarget); field("abi_threads", abiThreads); field("abi_exceptions", abiExceptions); + field("abi_longjmp", abiLongjmp); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); @@ -1196,6 +1200,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, b.abiExceptions = f.abiExceptions; b.abiExceptionsModel = f.abiExceptionsModel; b.abiExceptionsDeclared = f.abiExceptionsDeclared; + b.abiLongjmpModel = f.abiLongjmpModel; + b.abiLongjmpDeclared = f.abiLongjmpDeclared; b.allowHostLibs = f.allowHostLibs; b.macosDeploymentTarget = f.macosDeploymentTarget; b.iosDeploymentTarget = f.iosDeploymentTarget; @@ -1210,7 +1216,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, // member declares (or inherits) reaches the build when the member is // planned under a virtual root (#786). Nothing a package owns travels. for (auto const& cc : first.conditionalConfigs) { - if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared + && !cc.abiExceptionsDeclared && !cc.abiLongjmpDeclared) continue; mcpp::manifest::ConditionalConfig row; row.predicate = cc.predicate; @@ -1220,6 +1227,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, row.abiExceptions = cc.abiExceptions; row.abiExceptionsModel = cc.abiExceptionsModel; row.abiExceptionsDeclared = cc.abiExceptionsDeclared; + row.abiLongjmpModel = cc.abiLongjmpModel; + row.abiLongjmpDeclared = cc.abiLongjmpDeclared; v.conditionalConfigs.push_back(std::move(row)); } return v; diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 243196fbe..ed668ce06 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -29,6 +29,8 @@ import mcpp.toolchain.llvm; import mcpp.toolchain.model; import mcpp.toolchain.msvc; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; export namespace mcpp::toolchain { @@ -1365,7 +1367,25 @@ bool host_can_serve(const triple::Triple& target) { const bool crossArch = target.arch != mcpp::toolchain::platform::host_arch; if (target.is_musl()) return !crossArch || mcpp::toolchain::platform::host_arch == "x86_64"; - return !crossArch; + // A CROSS WHOSE ROW NAMES A SYSROOT PAYLOAD IS SERVED WHEREVER + // THAT PAYLOAD IS PUBLISHED (2026.11.11.1, G1). llvm alone carries + // no glibc of another architecture, which is what `return + // !crossArch` answered: no payload here produces it. The row's + // own declaration is the better answer -- a provider that serves + // the identity with a sysroot payload, available on this host, + // supplies the C library whatever the architectures are. The + // answer stays data: no host is named in this function. + if (crossArch) { + const auto host = std::format("linux-{}", + mcpp::toolchain::platform::host_arch); + for (auto const* p : mcpp::toolchain::target::providers()) + if (mcpp::toolchain::target::serves(*p, target.str()) + && !p->sysroot.empty() && p->sysroot != "toolchain" + && mcpp::toolchain::target::available_on(*p, host, target.arch)) + return true; + return false; + } + return true; } // Non-Linux host: only the self-contained musl payloads can work at // all (nothing else would find a C library). Today exactly one such diff --git a/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh b/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh new file mode 100755 index 000000000..06f578b08 --- /dev/null +++ b/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# requires: llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1075 -- a Windows row whose system the graph supplies compiles its resources +# with the Windows headers the platform's rows declare (2026.11.11.1, G3; +# design 2026-11-10 E3's third source). +# +# `x86_64-windows-musl` takes its system from the graph (openkal-platform's +# hosted part), so its toolchain has no paired payload, and its llvm binary +# carries no Windows headers of its own: neither include source +# `system_include_dirs` read existed, and a script that includes +# `"winres.h"` failed in llvm-rc with `winres.h: No such file or directory`. +# The MinGW-w64 sysroot payload a platform row declares for the +# architecture's GNU-ABI identity holds them, under one `include/` for every +# architecture; every PE row shares that set, and the two existing sources +# keep their precedence. +# +# Criteria: the project builds with `[resources]` on x86_64-windows-musl and +# the program carries a VERSIONINFO resource; a GNU-ABI row (1058) is not +# re-tested here. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'TOML' +[package] +name = "verinfo" +version = "0.1.0" + +[resources] +files = ["app.rc"] + +[targets.verinfo] +kind = "bin" +main = "src/main.c" +TOML +echo 'int main(void) { return 0; }' > "$t/src/main.c" +cat > "$t/app.rc" <<'RC' +#include "winres.h" +1 VERSIONINFO +FILEVERSION 1,2,3,4 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "ProductName", "verinfo" + END + END +END +RC +cd "$t" +out=$("$MCPP" build --target x86_64-windows-musl --toolchain "llvm@${LLVM_VERSION}" 2>&1) \ + || fail "[resources] did not build on x86_64-windows-musl" "$out" +exe=$(find target/x86_64-windows-musl -path '*/bin/verinfo.exe' -type f | head -1) +[ -n "$exe" ] || fail "no program" "$out" +# The llvm payload of the build, not the host's tools. +readobj="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readobj" +[ -x "$readobj" ] || fail "no llvm-readobj in the llvm payload ($readobj)" +"$readobj" --coff-resources "$exe" | grep -q 'VERSIONINFO' \ + || fail "the program carries no VERSIONINFO" "$("$readobj" --coff-resources "$exe" | head -20)" +echo "PASS: 1075 a graph-supplied Windows row finds its resource headers" diff --git a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh new file mode 100755 index 000000000..12f13f23b --- /dev/null +++ b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# requires: llvm aarch64-gnu-sysroot +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1076 -- an aarch64-linux-gnu cross takes its C library from the sysroot +# payload its row names (2026.11.11.1, G1). +# +# `host_can_serve` answered "no payload here produces it" for every arch-cross +# glibc target: llvm alone carries no glibc of another architecture. The +# row's own declaration is the better answer -- the provider that serves the +# identity names a sysroot payload (`xim:aarch64-linux-gnu-sysroot`, the +# package root IS the sysroot: glibc with the kernel headers, libgcc and +# libc++ inside) and states the hosts it is published for, so the gate reads +# the data and no host is named in the engine. +# +# The capability token probes the payload's install directory: the recipe is +# pending in the package index, so a machine without it (CI until the index +# carries it) skips rather than guesses. Where the payload IS installed the +# criteria are: a minimal C program builds for aarch64-linux-gnu, the +# artifact is an aarch64 ELF, and -- with qemu-aarch64 on the machine -- it +# runs and its exit status is passed through. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } + +# The cap token already probed the registry; re-check so the script also runs +# standalone without run_all.sh. +sysroot="" +for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-aarch64-linux-gnu-sysroot/*/; do + [ -d "$d/lib" ] && sysroot="$d" +done +if [ -z "$sysroot" ]; then + echo "SKIP: 1076 -- no aarch64-linux-gnu-sysroot payload installed (xim:aarch64-linux-gnu-sysroot)" + echo "PASS: 1076 (skipped)" + exit 0 +fi + +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'TOML' +[package] +name = "cross" +version = "0.1.0" + +[targets.cross] +kind = "bin" +main = "src/main.c" +TOML +printf '#include \nint main(void) { printf("cross-ok\\n"); return 42; }\n' > "$t/src/main.c" +cd "$t" +out=$("$MCPP" build --target aarch64-linux-gnu --toolchain "llvm@${LLVM_VERSION}" 2>&1) \ + || fail "aarch64-linux-gnu did not build from the row's sysroot payload" "$out" +exe=$(find target/aarch64-linux-gnu -path '*/bin/cross' -type f | head -1) +[ -n "$exe" ] || fail "no program" "$out" +readelf="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readelf" +[ -x "$readelf" ] || fail "no llvm-readelf in the llvm payload ($readelf)" +arch=$("$readelf" -h "$exe" | awk '/Machine:/ {print $2}') +[ "$arch" = "AArch64" ] || fail "the artifact is not an aarch64 ELF (machine: $arch)" "$("$readelf" -h "$exe" | head -20)" +# Where the machine can run it, the program's own exit status is the truth. +if command -v qemu-aarch64 >/dev/null 2>&1; then + rc=0 + qemu-aarch64 -L "${sysroot%/}" "$exe" > run.log 2>&1 || rc=$? + [ "$rc" -eq 42 ] || fail "qemu-aarch64 run: expected exit 42, got $rc" run.log + grep -q '^cross-ok$' run.log || fail "the program's output did not arrive" run.log +fi +echo "PASS: 1076 an aarch64-linux-gnu cross comes from a sysroot payload" diff --git a/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh b/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh new file mode 100755 index 000000000..f4c4ecd6e --- /dev/null +++ b/tests/e2e/1077_a_program_of_another_system_that_dies_at_once_names_the_runner.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# requires: llvm +# 1077 -- a program of another system's kernel interface that dies at once +# names the runner that would run it (2026.11.11.1, G6/G7). +# +# `mcpp run --target x86_64-freebsd-musl` on a Linux host spawns a static +# FreeBSD ELF of the host's own architecture: the kernel maps it without +# asking which system it names, the program states its first kernel call in +# FreeBSD's vocabulary, and the death is by signal -- 139, with nothing to +# read. `mcpp run` now runs such an artifact as a child, so the death is +# observed and named with the [target.] runner declaration; the +# status itself is returned unchanged, exactly what a direct launch would +# have reported. +# +# Criteria: the exit status stays 139; the error text states the death, the +# other system's kernel interface, and the runner declaration for the row. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app +"$MCPP" build --target x86_64-freebsd-musl > b.log 2>&1 || fail "the static FreeBSD program did not build" b.log +exe=$(find target/x86_64-freebsd-musl -path '*/bin/app' -type f | head -1) +[ -n "$exe" ] || fail "no program" b.log + +set +e +"$MCPP" run --target x86_64-freebsd-musl > run.log 2>&1 +rc=$? +set -e +[ "$rc" -eq 139 ] || fail "run exited $rc, expected the program's own death status 139" run.log +grep -q "was started and died at once" run.log \ + || fail "the run does not state the death at once" run.log +grep -q "another system's kernel interface" run.log \ + || fail "the run does not state the kernel interface fact" run.log +grep -q "\[target.x86_64-freebsd-musl\]" run.log \ + || fail "the runner declaration names the wrong target" run.log +grep -q "runner = \[" run.log \ + || fail "no runner declaration is printed" run.log +grep -q -- "--no-runner" run.log \ + || fail "the direct-execution escape hatch is not named" run.log +echo "PASS: 1077 a program of another system that dies at once names the runner" diff --git a/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh new file mode 100755 index 000000000..a68bbc8af --- /dev/null +++ b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell +# 1078 -- `--systems all` carries the runners the platform's own data names +# (2026.11.11.1, G5): this machine's system, the rows a platform defines, and +# the systems its runner states a host of -- FreeBSD's among them, whose +# runner is built as -freebsd-musl over the platform's hosted +# arrangement. +# +# The default is unchanged: a pack without `--systems` carries the runners +# of the legs' own systems -- this machine's system for every architecture +# the package has a leg of -- and no other system's. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -m)" in + x86_64|amd64) ARCH=x86_64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac +case "$(uname -s)" in Linux) OS=linux ;; *) echo "SKIP: a Linux host"; exit 0 ;; esac + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app + +# The default: this machine's system for each leg's architecture, nothing +# else (`mcpp new app` carries an x86_64 and an aarch64 leg). A `dir` pack's +# tree lands under target/dist whatever `-o` says (design 2026-10-29 §4.6), +# so the pack is located, not assumed. +"$MCPP" pack --target openkal --mode self-contained --pkg-format dir > d.log 2>&1 \ + || fail "the default pack did not build" d.log +one=$(find target/dist -mindepth 1 -maxdepth 1 -type d -newer mcpp.toml | head -1) +[ -n "$one" ] || fail "no pack tree under target/dist" d.log +[ -d "$one/libexec/okrun/${OS}-${ARCH}" ] || fail "the default pack carries no runner of its own system" d.log +others=$(ls "$one/libexec/okrun" 2>/dev/null | grep -v "^${OS}-" || true) +[ -z "$others" ] || fail "the default pack carries a system other than this machine's: $others" d.log + +# `--systems all`: the data's answer, FreeBSD included. The same tree name +# replaces the default's, so dist is cleared first and the tree that lands +# is the all-pack's own. +rm -rf target/dist +"$MCPP" pack --target openkal --mode self-contained --systems all --pkg-format dir > a.log 2>&1 \ + || fail "--systems all did not pack" a.log +all_=$(find target/dist -mindepth 1 -maxdepth 1 -type d | head -1) +[ -n "$all_" ] || fail "no --systems all tree under target/dist" a.log +for pair in "linux-x86_64" "linux-aarch64" "freebsd-x86_64" "freebsd-aarch64"; do + [ -d "$all_/libexec/okrun/$pair" ] || fail "no runner carried for $pair" a.log +done +# The Windows row builds wherever this mcpp builds it; where it does not, the +# warning says so instead. +if [ ! -d "$all_/libexec/okrun/windows-x86_64" ]; then + grep -q "no runner for windows-x86_64" a.log \ + || fail "the Windows runner is neither carried nor said" a.log +fi +echo "PASS: 1078 --systems all carries the runners the platform declares" diff --git a/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh new file mode 100755 index 000000000..b0f8c9f71 --- /dev/null +++ b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# requires: elf +# 1079 -- `longjmp` is the third `abi` member: the model a WebAssembly +# program's sigsetjmp/siglongjmp cross the boundary by (2026.11.11.1). +# +# With the toolchain's default JavaScript model the pair compiles into a +# stack copy, and a non-local exit over two call levels reads past the +# module's stack and dies at run time (measured: memory access out of bounds) +# while the build and the link succeed -- a failure nothing names. With +# `-sSUPPORT_LONGJMP=wasm` on both the compiles and the link, the pair +# round-trips. A deep exit wants a bigger stack; that is the project's own +# knob (`-sSTACK_SIZE` in ldflags), not an ABI fact, and the engine does not +# render it. +# +# Same self-probe gate as 653/650: `# requires: elf` only, because whether +# THIS machine has the emsdk payload is not a portable capability. Declared +# on a non-Emscripten row the member is refused naming the row's own os. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +have_emsdk=0 +for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripten \ + "$HOME"/.xlings/data/xpkgs/xim-x-emsdk/*/emscripten; do + [[ -x "$d/em++" ]] && have_emsdk=1 +done +if [ "$have_emsdk" -ne 1 ] || ! command -v node >/dev/null 2>&1; then + echo "SKIP: 1079 -- no emsdk payload or node on this machine" + echo "PASS: 1079 (skipped)" + exit 0 +fi + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "lj" +version = "0.1.0" + +[targets.lj] +kind = "bin" +main = "src/main.c" + +[target.wasm32-emscripten] +runner = ["node"] + +[target.'cfg(os = "emscripten")'.abi] +longjmp = "wasm" + +[target.'cfg(os = "emscripten")'.build] +ldflags = ["-sSTACK_SIZE=1048576"] +TOML +cat > src/main.c <<'C' +/* Emscripten's libc declares the sigsetjmp/siglongjmp aliases behind the + feature macros, like every libc; the fixture states the one it needs. */ +#define _GNU_SOURCE 1 +#include +#include + +static jmp_buf home; + +static void level_two(void) { + volatile int kept = 7; + siglongjmp(home, 1); + (void)kept; +} + +static void level_one(void) { level_two(); } + +int main(void) { + volatile int marker = 0; + if (sigsetjmp(home, 1) == 0) { + marker = 1; + level_one(); + printf("unreachable\n"); + return 1; + } + /* The volatile locals of the frames unwound by the non-local exit. */ + if (marker != 1) { printf("marker lost\n"); return 2; } + printf("round-trip\n"); + return 0; +} +C + +"$MCPP" build --target wasm32-emscripten > b.log 2>&1 || fail "the wasm build with longjmp = \"wasm\" failed" b.log +"$MCPP" run --target wasm32-emscripten > run.log 2>&1 || fail "the run failed -- the non-local exit did not round-trip" run.log +grep -q '^round-trip$' run.log || fail "the program did not reach its end" run.log + +# The model is a member of Emscripten rows alone: a Linux build that declares +# it is refused naming that fact, not silently satisfied. +cat > mcpp.toml <<'TOML' +[package] +name = "lj" +version = "0.1.0" + +[targets.lj] +kind = "bin" +main = "src/main.c" + +[target.'cfg(os = "linux")'.abi] +longjmp = "wasm" +TOML +if "$MCPP" build --target x86_64-linux-musl > refuse.log 2>&1; then + fail "longjmp = \"wasm\" on a Linux row was accepted" +fi +grep -q "not an Emscripten row" refuse.log || fail "the refusal does not name the row's own os" refuse.log +echo "PASS: 1079 an Emscripten row's longjmp model round-trips" diff --git a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh index 0571bfee9..f2576261b 100644 --- a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh +++ b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh @@ -53,12 +53,28 @@ echo "ok: --systems - makes that pair's file" # C if [ "$OS" = linux ]; then + # `--systems all` is the data's answer (2026.11.11.1): this machine's + # system, the systems the platform defines a row of for the + # architecture, and the systems its runner states a host of -- FreeBSD + # included, and never a pair no row exists for, so nothing is "Not + # covered" and nothing needs to say so. "$MCPP" pack --target openkal --pkg-format single-file --systems all -o all/ > c.log 2>&1 || fail "C: --systems all" c.log - for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64; do + for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64 \ + freebsd-x86_64 freebsd-aarch64; do ls all/sf-0.1.0-$pair* > /dev/null 2>&1 || fail "C: no file for $pair" c.log done - grep -q "Not covered macos-x86_64 (no carrier row" c.log || fail "C: the pair without a row is not said" c.log - echo "ok: --systems all makes every file this mcpp can and says the rest" + [ ! -e all/sf-0.1.0-macos-x86_64 ] || fail "C: a pair with no row was packed" c.log + # Naming an keeps its exact meaning: for a single file per pair, + # a system one of whose architectures has no row is refused naming the + # pair and the spelling that would work. + if "$MCPP" pack --target openkal --pkg-format single-file --systems macos -o mac/ > m.log 2>&1; then + fail "C: --systems macos was accepted with a rowless pair" m.log + fi + grep -q "builds no program for macos-x86_64" m.log \ + || fail "C: the pair without a row is not said" m.log + grep -q "use: --systems macos-aarch64" m.log \ + || fail "C: the refusal does not name the working spelling" m.log + echo "ok: --systems all makes every file the data names; a named system with a rowless pair is refused naming it" fi # D diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index f7294893b..24deac883 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -93,6 +93,14 @@ case "$OS" in if ls "${MCPP_HOME}"/registry/data/xpkgs/xim-x-llvm/*/bin/clang++ 2>/dev/null | head -1 | grep -q .; then CAPS+=(llvm) fi + # aarch64-gnu-sysroot: the glibc sysroot payload the aarch64-linux-gnu + # row names (xim:aarch64-linux-gnu-sysroot). A payload directory, not a + # version: the row's e2e skips until the package index carries the + # recipe and the runner prewarms it, which is why the token probes + # rather than assuming. + if ls "${MCPP_HOME}"/registry/data/xpkgs/xim-x-aarch64-linux-gnu-sysroot/*/lib/libc.so.6 2>/dev/null | head -1 | grep -q .; then + CAPS+=(aarch64-gnu-sysroot) + fi # Host MinGW headers can come from a distro prefix or the dedicated # xlings fixture outside MCPP_HOME. Test 738 proves that unrestricted # Clang finds the latter before checking graph isolation. @@ -271,7 +279,7 @@ echo "Detected capabilities: ${CAPS[*]:-}" # absent on Linux and must stay legal to declare. It is checked against the # CAPS+=() calls above by tests/e2e/README or by reading them -- keep it in # sync when adding a capability. -KNOWN_CAPS=(android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos +KNOWN_CAPS=(aarch64-gnu-sysroot android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw mingw-cross mingw-host-headers msvc musl nasm no-msvc pack patchelf python3 qemu-arm qemu-riscv scan-deps symlink unix-shell windows wine xlings-msvc) diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot index 712dc7e6c..a99e7d229 100644 --- a/tests/fixtures/target-rows.snapshot +++ b/tests/fixtures/target-rows.snapshot @@ -37,7 +37,7 @@ row x86_64-linux-musl descriptor-keys linux sysroot row aarch64-linux-gnu - table tier=preview note= pin=llvm@23.1.3 sysroot= static=0 platform= + table tier=preview note= pin=llvm@23.1.3 sysroot=xim:aarch64-linux-gnu-sysroot@2.44.3 static=0 platform= str aarch64-linux-gnu llvm aarch64-unknown-linux-gnu llvm@15.0 aarch64-unknown-linux-gnu @@ -54,7 +54,7 @@ row aarch64-linux-gnu android-abi arm64-v8a platform-name linux descriptor-keys linux - sysroot + sysroot xim:aarch64-linux-gnu-sysroot@2.44.3 row aarch64-linux-musl table tier=verified note= pin=gcc@16.1.0 sysroot= static=1 platform= str aarch64-linux-musl @@ -861,7 +861,7 @@ request x86_64-linux on windows-x86_64 -> x86_64-linux-musl musl-gcc fallback=1 | x86_64-linux-gnu/glibc/100/x x86_64-linux-musl/musl-gcc/100 x86_64-linux-musl/openkal/50 request aarch64-linux parsed aarch64-linux-gnu envExplicit=0 - on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 + on linux-x86_64 -> aarch64-linux-gnu glibc fallback=0 | aarch64-linux-gnu/glibc/100 aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 request riscv64-linux diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index dcc94bab2..104402d5a 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -52,14 +52,17 @@ UTest dispatched_format_stages_without_it{"PackClosureUnavailableOutcome/Dispatc expect(eq(closure_unavailable_outcome(Format::Dispatched), ClosureUnavailableOutcome::StageWithoutClosure)); }}; -// `--systems` (design 2026-10-29 §4.1): `` for every architecture of the -// legs, `-` for one, `all` for the three systems; unstated, this -// machine's. +// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.11.1 answers +// with the data): `` for every architecture of the legs, `-` +// for one; unstated, this machine's. `all` is this machine's system, the +// systems the builtin rows define for the architecture, and -- with a +// resolved platform -- the systems its runner states a host of. Without a +// platform that is the machine's system and this table's linux rows alone. UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystemsForAnArchitecture", [] { using V = std::vector; expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux", "macos", "windows" }))); + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); }}; diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 144aca228..0851ecf2a 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -1,4 +1,5 @@ #include +#include #include import std; @@ -148,6 +149,50 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; +// The classification of a death by signal (2026.11.11.1, G6): the four +// signals an artifact of another system's kernel interface dies by on its +// first kernel call, and nothing else — a normal exit, an exit by a signal +// the classification does not take (SIGTERM, 143) and the launcher's own +// 127/126 band are all someone else's fact. +UTest death_by_signal_takes_four_signals{"RunnerLookup/DeathBySignalTakesFourSignals", [] { + expect(death_by_signal(128 + SIGSEGV)); + expect(death_by_signal(128 + SIGILL)); + expect(death_by_signal(128 + SIGABRT)); +#ifdef SIGBUS + expect(death_by_signal(128 + SIGBUS)); +#endif + expect(!death_by_signal(0)); + expect(!death_by_signal(1)); + expect(!death_by_signal(128 + SIGTERM)); + expect(!death_by_signal(128 + SIGINT)); + expect(!death_by_signal(127)); + expect(!death_by_signal(-11)); + expect(eq(signal_of_death(128 + SIGSEGV), std::string_view("SIGSEGV"))); + expect(eq(signal_of_death(128 + SIGILL), std::string_view("SIGILL"))); + expect(eq(signal_of_death(0), std::string_view{})); +}}; + +UTest signal_death_message_names_the_runner_declaration{"RunnerLookup/SignalDeathMessageNamesTheRunnerDeclaration", [] { + const std::filesystem::path art{"/x/target/x86_64-freebsd-musl/debug/bin/app"}; + auto msg = signal_death_message("x86_64-freebsd-musl", art, 128 + SIGSEGV); + expect(ne(msg.find("was started and died at once"), std::string::npos)) << msg; + expect(ne(msg.find("SIGSEGV"), std::string::npos)) << msg; + expect(ne(msg.find("status 139"), std::string::npos)) << msg; + expect(ne(msg.find("another system's kernel interface"), std::string::npos)) << msg; + expect(ne(msg.find("[target.x86_64-freebsd-musl]"), std::string::npos)) << msg; + expect(ne(msg.find("runner = ["), std::string::npos)) << msg; + expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; + // The status stays the caller's to return; the message only says it. + expect(eq(msg.find("status 139, but"), std::string::npos)) << msg; +}}; + +UTest runner_advice_text_declares_the_target{"RunnerLookup/RunnerAdviceTextDeclaresTheTarget", [] { + auto msg = runner_advice_text("aarch64-linux-gnu"); + expect(ne(msg.find("The artifact was built for 'aarch64-linux-gnu'"), std::string::npos)) << msg; + expect(ne(msg.find("[target.aarch64-linux-gnu]"), std::string::npos)) << msg; + expect(ne(msg.find("runner = ["), std::string::npos)) << msg; +}}; + #else UTest runner_lookup_windows_covered_by_integration{"RunnerLookup/WindowsCoveredByIntegration", [] { /* passes */; }}; diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 74df15d30..e529ba405 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -637,7 +637,12 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie // the LLVM 23.1.3/glibc default, its deployed pack, GNU self-host, // all 147 suites and four real index members, from source. The tier // stays `preview` until the released engine is consumed on a clean - // ARM64 host (SPEC-009 §10.7). + // ARM64 host (SPEC-009 §10.7). From 2026.11.11.1 the row names the + // sysroot payload its C library comes from (G1) -- llvm alone + // carries no glibc of another architecture -- and the linkage is + // glibc's own dynamic one: the compiler run-time is the payload's + // (libgcc's archives and compiler-rt's builtins), so a link takes + // nothing static from the host. auto [name, tier] = std::pair{"aarch64-linux-gnu", "preview"}; auto t = parse(name); expect(fatal(t.has_value())); @@ -645,7 +650,7 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie expect(fatal(ne(info, nullptr))); expect(eq(info->tier, tier)); expect(eq(info->pin, "llvm@23.1.3")); - expect(info->sysroot.empty()); + expect(eq(info->sysroot, "xim:aarch64-linux-gnu-sysroot@2.44.3")); expect(!info->defaultStatic); expect(!t->pin_is_capability()); } From 6c325b181bbb09cf36f9bb113ef8b994b894c1b3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 04:56:19 +0900 Subject: [PATCH 02/15] the llvm pin stays 23.1.3 everywhere; no #ifdef in module code The corrective r2 repack lands in the package index under the same version key (openxlings/xpkgindex-update#9), so no pin or doc names -r2 and the arm/macos/windows legs resolved a package that does not exist. runner_lookup's SIGBUS cases lose their #ifdef guards: POSIX defines the signal everywhere mcpp builds, and the include gate keeps module code conditional-free. --- README.md | 2 +- README.zh-CN.md | 2 +- docs/01-getting-started.md | 6 +++--- docs/20-toolchains.md | 24 ++++++++++++------------ mcpp.toml | 6 +++--- src/build/runner_lookup.cppm | 6 ++---- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 70e60b194..466fd4e08 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ list` reports for this machine): | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23 — Cortex-A 32-bit, the first row with an MMU ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23 — bare metal, no C library by default ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | | `aarch64-windows-gnu` | llvm 23 with `xim:mingw-w64-sysroot` (llvm-mingw's target side) — built on any host; the same base serves `x86_64-windows-gnu` where no gcc payload exists | preview | | `x86_64-freebsd` · `aarch64-freebsd` | the platform that serves them, over `xim:-freebsd-sysroot` ([22](docs/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23 — a static FreeBSD program with a musl C library over openkal-freebsd, built on any host *(what `-freebsd` is with no platform chosen)* | preview | diff --git a/README.zh-CN.md b/README.zh-CN.md index 92083e472..cb61423a5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -402,7 +402,7 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`( | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23;Cortex-A 32 位,第一个带 MMU 的目标 ² | verified | | `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23;裸机,默认不带 C 库 ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview | -| `aarch64-linux-gnu` | `llvm@23.1.3-r2` | preview | +| `aarch64-linux-gnu` | `llvm@23.1.3` | preview | | `aarch64-windows-gnu` | llvm 23 加 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧);任意宿主可构建;没有 gcc 载荷之处同一基座也服务 `x86_64-windows-gnu` | preview | | `x86_64-freebsd` · `aarch64-freebsd` | 服务它们的平台,基于 `xim:-freebsd-sysroot`([22](docs/zh/22-target-side.md)) | preview | | `x86_64-freebsd-musl` · `aarch64-freebsd-musl` | llvm 23;openkal-freebsd 之上带 musl C 库的静态 FreeBSD 程序,任意宿主可构建 *(未选平台时 `-freebsd` 即此身份)* | preview | diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md index 84fbadba4..ce92416d4 100644 --- a/docs/01-getting-started.md +++ b/docs/01-getting-started.md @@ -42,10 +42,10 @@ the host: | host | default | |---|---| | Linux x86_64 | `gcc@16.1.0` | -| Linux aarch64 | `llvm@23.1.3-r2` | +| Linux aarch64 | `llvm@23.1.3` | | other Linux architectures | `gcc@15.1.0-musl` | -| macOS | `llvm@23.1.3-r2` | -| Windows with usable MSVC | `llvm@23.1.3-r2` | +| macOS | `llvm@23.1.3` | +| Windows with usable MSVC | `llvm@23.1.3` | | Windows without it | `gcc@16.1.0` for `x86_64-windows-gnu` | The Linux aarch64 default applies from 2026.10.8.1. Existing configured diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index c699ae62e..f4db148ee 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -24,19 +24,19 @@ host-aware: - Linux x86_64 uses `gcc@16.1.0` for the native glibc ABI, so X11, OpenGL, and system libraries work out of the box. -- Linux aarch64 uses `llvm@23.1.3-r2` for the native glibc ABI (2026.10.8.1+). +- Linux aarch64 uses `llvm@23.1.3` for the native glibc ABI (2026.10.8.1+). - Other Linux architectures use `gcc@15.1.0-musl`, a self-contained static toolchain. -- macOS uses `llvm@23.1.3-r2`. -- Windows with a usable MSVC installation uses `llvm@23.1.3-r2` for the MSVC ABI. +- macOS uses `llvm@23.1.3`. +- Windows with a usable MSVC installation uses `llvm@23.1.3` for the MSVC ABI. Without usable MSVC, it uses `gcc@16.1.0` with target `x86_64-windows-gnu` (MinGW-w64, static by default), says which half of the MSVC ABI is missing — Visual Studio's C++ tools or the Windows SDK — and where it looked, and records the pair as the machine's default. Once both halves - are present, `mcpp toolchain default llvm@23.1.3-r2` moves the machine to the + are present, `mcpp toolchain default llvm@23.1.3` moves the machine to the MSVC ABI. - Android (Termux) uses the default of GNU/Linux on its architecture: on - aarch64, `llvm@23.1.3-r2` with the managed glibc. Android 10 or later is + aarch64, `llvm@23.1.3` with the managed glibc. Android 10 or later is required (the shell is reached as `/bin/sh`), with xlings 2026.10.10.2 or later, which unpacks payloads in the app sandbox, where hard links cannot be created. @@ -78,7 +78,7 @@ this model with a one-line `note:` hint. ```bash mcpp toolchain install gcc 16.1.0 # host target (GNU libc on Linux) -mcpp toolchain install llvm 23.1.3-r2 # LLVM/Clang, default on macOS and Windows with usable MSVC +mcpp toolchain install llvm 23.1.3 # LLVM/Clang, default on macOS and Windows with usable MSVC mcpp toolchain install gcc 16 --target x86_64-linux-musl # musl target payload mcpp toolchain install --target x86_64-windows-gnu # family omitted → the # target's convention pin (gcc@16.1.0) @@ -133,14 +133,14 @@ configs with combined spellings like `default = "gcc@15.1.0-musl"` keep working unchanged.) On native Linux aarch64 (2026.10.8.1+), an explicit migration selects both -LLVM 23.1.3-r2 and the GNU target after the native payload is published: +LLVM 23.1.3 and the GNU target after the native payload is published: ```bash -mcpp toolchain install llvm 23.1.3-r2 -mcpp toolchain default llvm@23.1.3-r2 --target aarch64-linux-gnu +mcpp toolchain install llvm 23.1.3 +mcpp toolchain default llvm@23.1.3 --target aarch64-linux-gnu ``` -The command records `llvm@23.1.3-r2` and `aarch64-linux-gnu` as the default pair. +The command records `llvm@23.1.3` and `aarch64-linux-gnu` as the default pair. Existing configurations remain effective until explicitly changed; project manifest declarations retain their precedence. @@ -166,7 +166,7 @@ the required family for that build: ``` $ mcpp build Resolving toolchain - Resolved llvm@23.1.3-r2 → …/xim-x-llvm/23.1.3-r2/bin/clang++ + Resolved llvm@23.1.3 → …/xim-x-llvm/23.1.3/bin/clang++ required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`), not your gcc@16.1.0 — this project only ``` @@ -212,7 +212,7 @@ The output has two blocks — one per axis: Toolchains: * gcc 16.1.0 (default) gcc 15.1.0 - llvm 23.1.3-r2 + llvm 23.1.3 Targets: TARGET NOTE TOOLCHAIN STATUS diff --git a/mcpp.toml b/mcpp.toml index 7ddc633f4..e31c70818 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -66,11 +66,11 @@ programs = "directory" # (mcpp#669). Linux x86_64 retains GCC; native ARM64 uses LLVM and glibc. [toolchain] default = "gcc@16.1.0" -macos = "llvm@23.1.3-r2" -windows = "llvm@23.1.3-r2" +macos = "llvm@23.1.3" +windows = "llvm@23.1.3" [target.aarch64-linux-gnu] -toolchain = "llvm@23.1.3-r2" +toolchain = "llvm@23.1.3" # Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the # four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 16.1 + full diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 2577629a7..6fda71aeb 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -184,14 +184,14 @@ inline std::string unrunnable_message(std::string_view triple, // why the classification is asked about artifacts of another system only, // never about a native one, where the same status is the program's own bug // and advice about runners would be noise on top of it. +// POSIX defines SIGBUS everywhere mcpp builds; no conditional compilation +// is needed (and the include gate forbids #ifdef here). inline bool death_by_signal(int rc) { switch (rc) { case 128 + SIGSEGV: case 128 + SIGILL: case 128 + SIGABRT: -#ifdef SIGBUS case 128 + SIGBUS: -#endif return true; default: return false; @@ -205,9 +205,7 @@ inline std::string_view signal_of_death(int rc) { case 128 + SIGSEGV: return "SIGSEGV"; case 128 + SIGILL: return "SIGILL"; case 128 + SIGABRT: return "SIGABRT"; -#ifdef SIGBUS case 128 + SIGBUS: return "SIGBUS"; -#endif default: return {}; } } From d7a9bc3d11fe304ae80e6441a5ab4b7a10960787 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 05:10:41 +0900 Subject: [PATCH 03/15] runner_lookup: the signal constants come from the mwrap of libc's macros The engine includes no headers (the include-region rule); the C library's macros arrive as constants through mcpplibs.mwrap.libc, so the death classification takes the three signals the wrap carries. SIGBUS is not among them -- its typical cause here, a stack the kernel never grew, arrives as SIGSEGV on the systems that report it. --- src/build/runner_lookup.cppm | 27 +++++++++++++-------------- tests/unit/test_runner_lookup.cpp | 16 +++++++--------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 6fda71aeb..646314580 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -20,13 +20,11 @@ // // Design: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md §4.3-4.4. -module; -#include - export module mcpp.build.runner_lookup; import std; import mcpplibs.os.platform; +import mcpplibs.mwrap.libc; export namespace mcpp::build::runner_lookup { struct Lookup { @@ -184,14 +182,16 @@ inline std::string unrunnable_message(std::string_view triple, // why the classification is asked about artifacts of another system only, // never about a native one, where the same status is the program's own bug // and advice about runners would be noise on top of it. -// POSIX defines SIGBUS everywhere mcpp builds; no conditional compilation -// is needed (and the include gate forbids #ifdef here). +// The constants come from the mwrap of the C library's macros (the engine +// includes no headers); SIGBUS is not among them, and its typical cause here +// -- a stack the kernel never grew -- arrives as SIGSEGV on the systems that +// report it, so the classification keeps the three the wrap carries. +namespace sigc = mcpplibs::mwrap::libc::macros; inline bool death_by_signal(int rc) { switch (rc) { - case 128 + SIGSEGV: - case 128 + SIGILL: - case 128 + SIGABRT: - case 128 + SIGBUS: + case 128 + sigc::SIGSEGV: + case 128 + sigc::SIGILL: + case 128 + sigc::SIGABRT: return true; default: return false; @@ -202,11 +202,10 @@ inline bool death_by_signal(int rc) { // classification did not take. inline std::string_view signal_of_death(int rc) { switch (rc) { - case 128 + SIGSEGV: return "SIGSEGV"; - case 128 + SIGILL: return "SIGILL"; - case 128 + SIGABRT: return "SIGABRT"; - case 128 + SIGBUS: return "SIGBUS"; - default: return {}; + case 128 + sigc::SIGSEGV: return "SIGSEGV"; + case 128 + sigc::SIGILL: return "SIGILL"; + case 128 + sigc::SIGABRT: return "SIGABRT"; + default: return {}; } } diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 0851ecf2a..4ef7d5271 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -149,18 +149,16 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; -// The classification of a death by signal (2026.11.11.1, G6): the four -// signals an artifact of another system's kernel interface dies by on its -// first kernel call, and nothing else — a normal exit, an exit by a signal -// the classification does not take (SIGTERM, 143) and the launcher's own -// 127/126 band are all someone else's fact. -UTest death_by_signal_takes_four_signals{"RunnerLookup/DeathBySignalTakesFourSignals", [] { +// The classification of a death by signal (2026.11.11.1, G6): the three +// signals the mwrap of the C library's macros carries that an artifact of +// another system's kernel interface dies by on its first kernel call, and +// nothing else — a normal exit, an exit by a signal the classification +// does not take (SIGTERM, 143) and the launcher's own 127/126 band are all +// someone else's fact. +UTest death_by_signal_takes_the_wrapped_three{"RunnerLookup/DeathBySignalTakesTheWrappedThree", [] { expect(death_by_signal(128 + SIGSEGV)); expect(death_by_signal(128 + SIGILL)); expect(death_by_signal(128 + SIGABRT)); -#ifdef SIGBUS - expect(death_by_signal(128 + SIGBUS)); -#endif expect(!death_by_signal(0)); expect(!death_by_signal(1)); expect(!death_by_signal(128 + SIGTERM)); From 33c80e629701f11088bf52d1b5680f32c93a645c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 05:14:14 +0900 Subject: [PATCH 04/15] docs/zh: the longjmp row joins the abi table --- docs/zh/22-target-side.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 0b70acfd9..2d803403d 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -1147,6 +1147,7 @@ WebAssembly 上,每个目标文件、预编译的标准库模块与链接必 |---|---|---|---| | `threads` | 布尔 | 在既非 PE 也非 freestanding 的目标上为 `-pthread`;在 PE 与 freestanding 目标上不产生任何 flag | 标准库模块的预构建、依赖扫描、所有包的每个 C 与 C++ 翻译单元,以及链接 | | `exceptions` *(mcpp 2026.9.12.3+)* | 布尔,或 `"wasm"` / `"js"` *(2026.10.30.1+)* | 只在 `os = "emscripten"` 上,经方言 flag 进入编译行,也进入链接行:`"wasm"` 为 `-fwasm-exceptions`,即 WebAssembly 自身的异常处理;`"js"` 与 `true` 为 `-fexceptions`,即基于 JavaScript 的模型。在其余每个目标上什么都不产生,因为那里异常本来就是默认开启的 | 与 `threads` 相同的那一套 | +| `longjmp` *(mcpp 2026.11.11.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `threads` 相同的那一套 | 两个成员都经由方言 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。 From 09b1779b88904e6e1897d95ad5b606c8b6e131c9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 06:05:25 +0900 Subject: [PATCH 05/15] fix: the loader fold reads the generic string, not the native wide pointer --- src/build/prepare/base.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index c12d1388a..c12712e6d 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -550,7 +550,7 @@ std::expected step2_resolve_base(PrepareState& state) { if (!native && !tc.base.loader.empty()) { std::error_code lec; if (auto rel = fs::relative(tc.base.loader, dir, lec); !lec && !rel.empty() - && rel.native().find("..") == std::string_view::npos) + && rel.generic_string().find("..") == std::string::npos) tc.base.loader = fs::path("/") / rel; } // A base package's own `provides` is its declaration, as a statement's is. From c18bd6f37f9027ef645de45f71330d76c351afd6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 07:24:42 +0900 Subject: [PATCH 06/15] the CI legs the change itself turned up - polyglot holds FreeBSD's program beside Linux's (G5 made --systems all name it; the assembler's whitelist was the half that lagged) and refuses a system it has no uname pattern for, not FreeBSD - 299 accepts a row whose payload package this home cannot install: the row resolved, the package is the index's to give, and the refusal's subject names the row -- the claim is completion, as it always was - the MinGW headers are read only where the toolchain's own include sources found nothing, standing payloads before fetched ones: an MSVC or GNU-ABI project no longer fetches a payload its link will not read (measured on the upgrade lane: mingw-w64-sysroot, 21.3 MB, first run) - pack_modes states 'all' host-aware: the machine asking is one of the facts, and the suite runs on four hosts - 1076 is excused in the coverage table: no hosted shard installs the sysroot payload, and the test skips rather than guesses --- src/build/prepare/windows_resources.cpp | 31 +++++++++++++------ src/pack/polyglot.cppm | 10 +++--- ...c_library_resolves_to_a_row_that_exists.sh | 24 ++++++++++++-- tests/e2e/coverage-exceptions.tsv | 1 + tests/unit/test_pack_modes.cpp | 12 ++++++- tests/unit/test_pack_polyglot.cpp | 13 ++++++-- 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 4c789209b..7969771cc 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -80,11 +80,16 @@ namespace mcpp::build { // `winres.h` reaches neither of the include sources the toolchain states. The // MinGW-w64 sysroot payload a platform row declares for this architecture's // GNU-ABI identity has them, under one `/include` for every -// architecture (E9's layout). Resolved through the payload channel the row's -// own sysroot goes through, and probed relative to what it resolves to -- -// never PATH. Empty when no row declares one or the payload is not here: the -// resource compile then fails with the tool's own missing-header text, which -// is the truthful message. +// architecture (E9's layout). +// +// READ WHERE THE TWO SOURCES ABOVE FOUND NOTHING, AND STANDING PAYLOADS +// FIRST: a row that already states its headers -- the SDK's, a paired +// payload's -- never resolves this, so preparing an MSVC or GNU-ABI project +// does not fetch a payload its link will not read. Only a compile that would +// otherwise lack `winres.h` resolves the declaration, and it takes the +// payload where it already stands before it fetches one. Empty when no row +// declares one or the payload is not here: the resource compile then fails +// with the tool's own missing-header text, which is the truthful message. static std::filesystem::path mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& trip) { if (trip.os != "windows") return {}; @@ -94,15 +99,15 @@ mingw_headers_root(PrepareState& state, const mcpp::toolchain::triple::Triple& t if (!mcpp::toolchain::target::serves(*p, gnuTriple)) continue; auto cfg = state.get_cfg(true); if (!cfg) return {}; + auto xl = mcpp::config::make_xlings_env(**cfg); + if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), + mcpp::provider::parse_ref(p->sysroot))) + return *dir / "include"; mcpp::fetcher::Fetcher fetcher(**cfg); mcpp::fetcher::InstallProgressHandler progress; if (auto payload = fetcher.resolve_xpkg_path( std::string(p->sysroot), mcpp::fetcher::PayloadIntent::Use, &progress)) return payload->root / "include"; - auto xl = mcpp::config::make_xlings_env(**cfg); - if (auto dir = mcpp::provider::payload(*mcpp::provider::open(xl), - mcpp::provider::parse_ref(p->sysroot))) - return *dir / "include"; return {}; // one declared payload per row; the first provider wins } return {}; @@ -136,7 +141,13 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; - const auto mingwHeaders = mingw_headers_root(state, trip); + // The third source is resolved only where it will be read: below the + // toolchain's own include sources, never beside them (G3) -- a row that + // states its headers does not fetch a payload its compile will not read. + std::filesystem::path mingwHeaders; + if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos + && rsrc::system_include_dirs(*state.tc, dialectId).empty()) + mingwHeaders = mingw_headers_root(state, trip); // UTF-8 input, always. `[package]` metadata is user text and // routinely non-ASCII; without this llvm-rc refuses the script diff --git a/src/pack/polyglot.cppm b/src/pack/polyglot.cppm index a51f50ad2..27967453e 100644 --- a/src/pack/polyglot.cppm +++ b/src/pack/polyglot.cppm @@ -29,8 +29,8 @@ import std; export namespace mcpp::pack::polyglot { -// One system's program: `system` is `linux`, `macos` or `windows`, `arch` -// `x86_64` or `aarch64`. +// One system's program: `system` is `linux`, `freebsd`, `macos` or `windows`, +// `arch` `x86_64` or `aarch64`. struct Program { std::string system; std::string arch; @@ -66,7 +66,9 @@ void put32(std::string& b, std::size_t o, std::uint32_t v) { // `uname -s`-`uname -m` spellings of a system and architecture. std::string uname_pattern(std::string_view system, std::string_view arch) { - const std::string s = system == "macos" ? "Darwin" : "Linux"; + const std::string s = system == "macos" ? "Darwin" + : system == "freebsd" ? "FreeBSD" + : "Linux"; if (arch == "x86_64") return std::format("{0}-x86_64|{0}-amd64", s); return std::format("{0}-aarch64|{0}-arm64", s); } @@ -145,7 +147,7 @@ std::expected assemble(std::string_view name, const st "a polyglot file leads with one Windows program, and this pack has two ({}, {})", windows->arch, p.arch)); windows = &p; - } else if (p.system == "linux" || p.system == "macos") { + } else if (p.system == "linux" || p.system == "freebsd" || p.system == "macos") { posix.push_back(p); } else { return std::unexpected(std::format("a polyglot file holds no program of {}", p.system)); diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index 92ea117f0..14cda1314 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -86,9 +86,27 @@ case "$r" in host-cannot-serve) echo " ok --target aarch64-linux completed (this host serves no such payload)" ;; *) - echo "FAIL: --target aarch64-linux refused for reason '$r'" - message_of aarch64-linux | sed 's/^/ /' - exit 1 ;; + # THE ROW'S PAYLOAD IS A PACKAGE, AND A PACKAGE MAY NOT BE INSTALLABLE + # WHERE THIS RUNS (2026.11.11.1, G1): the GNU row names its sysroot, and + # an index without it -- a CI runner ahead of the index PR, an offline + # machine -- refuses about the PACKAGE, quoting the row it resolved. That + # refusal still answers this half's question: the request reached a row + # of the system, which the refusal's subject and the row checks below + # both name. Anything else it names is not about the row and stays red. + if [ "$row" = aarch64-linux-gnu ] || [ "$row" = aarch64-unknown-linux-gnu ] \ + || [ "$row" = aarch64-linux-musl ] || [ "$row" = aarch64-unknown-linux-musl ]; then + if message_of aarch64-linux | grep -q "could not be installed"; then + echo " ok --target aarch64-linux reached a row whose payload package is not installable here" + else + echo "FAIL: --target aarch64-linux refused for reason '$r'" + message_of aarch64-linux | sed 's/^/ /' + exit 1 + fi + else + echo "FAIL: --target aarch64-linux refused for reason '$r'" + message_of aarch64-linux | sed 's/^/ /' + exit 1 + fi ;; esac # AND THE ROW IS THE IDENTITY, NOT THE SPELLING. The output directory is the diff --git a/tests/e2e/coverage-exceptions.tsv b/tests/e2e/coverage-exceptions.tsv index 13a168b7a..6e346fca3 100644 --- a/tests/e2e/coverage-exceptions.tsv +++ b/tests/e2e/coverage-exceptions.tsv @@ -2,3 +2,4 @@ # One `\t` line each. A test that runs on some shard, or that a # workflow names, does not belong here, and the check fails if it is listed. 658_a_program_runs_on_an_attached_android_device.sh needs an Android device attached over adb; the android job of ci-linux-e2e.yml runs `mcpp test` on an emulator instead +1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh needs the xim:aarch64-linux-gnu-sysroot payload installed; no hosted shard installs it, and the test skips rather than guesses until the index carries the payload diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index 104402d5a..228e898bc 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -1,6 +1,7 @@ import std; import mcpplibs.mtest; +import mcpplibs.os.platform; import mcpp.pack; using namespace mcpplibs::mtest; @@ -62,7 +63,16 @@ UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystem using V = std::vector; expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); + // `all` answers with the data, and one of the facts is the machine asking: + // its system stands first, wherever the same suite runs. A Linux host and + // the table's linux rows coincide; a macOS host names macos beside them. + const char* here = "linux"; + if constexpr (mcpplibs::os::is_windows) here = "windows"; + else if constexpr (mcpplibs::os::is_macos) here = "macos"; + if (std::string_view(here) == "linux") + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux" }))); + else + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ here, "linux" }))); expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); }}; diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 88866317e..5ce8bfc09 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -31,8 +31,10 @@ std::uint64_t number_after(std::string_view text, std::string_view key) { } // namespace UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_says{"PackPolyglot/WithoutWindowsTheFileIsAScriptAndEachProgramLiesWhereItsLineSays", [] { - const std::string linux64(1000, 'L'), mac(777, 'M'); - auto file = pg::assemble("app", { program("linux", "x86_64", linux64), program("macos", "aarch64", mac) }); + const std::string linux64(1000, 'L'), mac(777, 'M'), fbsd(333, 'F'); + auto file = pg::assemble("app", { program("linux", "x86_64", linux64), + program("freebsd", "x86_64", fbsd), + program("macos", "aarch64", mac) }); if (auto&& failed = expect(fatal(file.has_value())); !failed) failed << file.error(); expect(file->starts_with("#!/bin/sh\no=$0;")); expect(pg::is_polyglot(*file)); @@ -47,6 +49,7 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ expect(eq(script.find("MINGW*"), std::string_view::npos)) << "no Windows program, no MSYS line"; for (auto [key, bytes] : { std::pair{std::string_view("k=linux-x86_64 a="), std::string_view(linux64)}, + std::pair{std::string_view("k=freebsd-x86_64 a="), std::string_view(fbsd)}, std::pair{std::string_view("k=macos-aarch64 a="), std::string_view(mac)} }) { const auto line = script.find(key); expect(fatal(ne(line, std::string_view::npos))) << key; @@ -57,6 +60,7 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ } // `uname` spells an architecture two ways on some systems. expect(ne(script.find("Linux-x86_64|Linux-amd64)"), std::string_view::npos)); + expect(ne(script.find("FreeBSD-x86_64|FreeBSD-amd64)"), std::string_view::npos)); expect(ne(script.find("Darwin-aarch64|Darwin-arm64)"), std::string_view::npos)); }}; @@ -64,7 +68,10 @@ UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [ expect(!(pg::assemble("app", {}).has_value())); expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - expect(!(pg::assemble("app", { program("freebsd", "x86_64", "x") }).has_value())); + // FreeBSD is a POSIX system of the file since 2026.11.11.1 (G5): its + // program rides behind the script head beside Linux's. What still has + // no place is a system the script names no `uname` pattern for. + expect(!(pg::assemble("app", { program("plan9", "x86_64", "x") }).has_value())); auto notPe = pg::assemble("app", { program("windows", "x86_64", std::string(256, 'x')) }); expect(fatal(!notPe.has_value())); if (auto&& failed = expect(ne(notPe.error().find("no PE"), std::string::npos)); !failed) failed << notPe.error(); From d6f73eda3ce91d9d87b00fa3c8a41e1f973920d4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:32:40 +0900 Subject: [PATCH 07/15] the mingw headers resolve lazily at both sites, and 299 reads the bracketed row The resource scan resolved the MinGW-w64 sysroot payload unconditionally (gdb, a fresh home, why --target x86_64-windows-gnu): the compiler step had been gated since the last push, the scan step had not, so every PE prepare fetched 21.3 MB its link would not read -- measured on the upgrade lane's first run. Both sites now share one sentence: llvm-rc, a row whose Windows headers are not the machine's own (a located MSVC SDK states that they are), the toolchain's own include sources found nothing, standing payloads before fetched ones. 299's row extraction now also reads the bracketed subject of the base refusal ([target.aarch64-linux-gnu].sysroot = ...), the form the payload refusal takes; the single-quoted form it alone read before stays first. --- src/build/prepare/windows_resources.cpp | 19 +++++++++++++++---- ...c_library_resolves_to_a_row_that_exists.sh | 3 ++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 7969771cc..a632dec3a 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -141,11 +141,15 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; - // The third source is resolved only where it will be read: below the - // toolchain's own include sources, never beside them (G3) -- a row that - // states its headers does not fetch a payload its compile will not read. + // The third source is resolved only where it will be read, and only for + // a row whose Windows headers are not the machine's own (G3's subject): + // a located MSVC SDK is the row's stated header source -- an INCLUDE the + // shell did not export is not that SDK's absence, and an MSVC row never + // fetches another row's payload. Below the toolchain's own include + // sources, never beside them. The same sentence gates both call sites. std::filesystem::path mingwHeaders; if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos + && state.tc->base.msvc.sdkRoot.empty() && rsrc::system_include_dirs(*state.tc, dialectId).empty()) mingwHeaders = mingw_headers_root(state, trip); @@ -249,7 +253,14 @@ std::expected step13_windows_resources(PrepareState& state, B // windres), found as the compile step below finds it (E3). const auto rcTool = mcpp::build::resources::find_rc_tool(*state.tc, dialectId); const bool msvcStyle = rcTool ? rcTool->style == "msvc" : dialectId == "msvc"; - const auto mingwHeaders = mingw_headers_root(state, trip); + // Lazy, as at the compiler step above: a row that states its own headers + // resolves nothing, and a scan that finds no script reads none. + namespace rsrc = mcpp::build::resources; + std::filesystem::path mingwHeaders; + if (rcTool && msvcStyle && rcTool->name().find("llvm-rc") != std::string::npos + && state.tc->base.msvc.sdkRoot.empty() + && rsrc::system_include_dirs(*state.tc, dialectId).empty()) + mingwHeaders = mingw_headers_root(state, trip); for (auto const& S : subjects) { const auto& M = *S.m; diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index 14cda1314..7fa686c6f 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -65,7 +65,8 @@ resolved_row() { # request → the row it resolved to t="$(printf '%s' "$doc" | jq -r '.data.triple.llvm // empty')" if [ -n "$t" ]; then printf '%s' "$t"; return; fi printf '%s' "$doc" | jq -r '.diagnostics[0].message // empty' \ - | head -1 | sed -n "s/^[^']*'\\([^']*\\)'.*/\\1/p" + | head -1 | sed -n -e "s/^[^']*'\([^']*\)'.*/\1/p" \ + -e "s/^\[target\.\([^]]*\)\].*/\1/p" } message_of() { "$MCPP" why toolchain --target "$1" --format json 2>/dev/null \ From ead6bad0daf91ea8184714d45305b5734e6a2eae Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:39:33 +0900 Subject: [PATCH 08/15] docs: the three-plane platform architecture, as the reference the next systems consume A synthesis of what the merged round established and what the families after it use: rows, payloads, runners as data; the ABI layering beneath one C library ported once; corrective revisions; the disk-image asset class and its harness; the semantic plane stated as the program's own. No behaviour changes -- this names what is already gated green. --- ...hree-plane-platform-architecture-design.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .agents/docs/2026-11-12-three-plane-platform-architecture-design.md diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md new file mode 100644 index 000000000..ad4851af1 --- /dev/null +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -0,0 +1,106 @@ +# The three-plane platform architecture: mcpp × openkal × xim + +Date: 2026-11-12 +Status: reference design (a synthesis; nothing in this document adds engine +behaviour — it names what the merged changes established and what the next +systems consume) + +## 1. The statement + +A platform enters this ecosystem as **data**, never as engine code. Three +data planes carry everything a target needs, and one normalisation layer +decides whether what was built is right: + +| Plane | Carries | Where it lives | Invariant | +|---|---|---|---| +| ① row | serves/hosts/toolchain/sysroot/linkage/abi of a target identity | the toolchain model's table, or a platform package's own rows | target differences are data; the engine holds no per-target branch | +| ② payload | the toolchain bytes, and the base package that is the target's C library | the index (xim), installed by the fetcher through the revision channel | a build host needs mcpp itself and nothing else; where a row names a base package the host's link model yields entirely | +| ③ runner | how an artifact of another system starts (a declared prefix, an emulator, a loader) | row/platform data; resolved by the runner lookup | build never depends on a runner; running consumes the declaration | +| verification | one script, one expected output, normalised (CRLF, colour, codeset, console noise) | the shared harness | the harness knows no target; every executor family answers through it | + +Two red lines hold throughout, and the gates hold them mechanically: +default behaviour is unchanged by any of it (the regressions stay green), and +no host name enters the engine — host differences are statements rows make. + +## 2. The layering the planes serve + +```mermaid +flowchart TB + subgraph consumer["a program of any system"] + A["import the C++ modules / C sources"] + end + subgraph libc["one C library, ported once"] + M["libc above the kernel ABI (spec)
static where the row says static"] + end + subgraph kal["the kernel-ABI specification"] + K["openkal: the interface, its conformance suite
and one implementation per system family"] + end + R1["row ① + payload ②"] --> A + M --> A + K --> M + KF["firmware implementations
(UEFI boot services, RISC-V SBI)"] --> K + KP["kernel implementations
(the reference one, a BSD one,") --> K + KX["a POSIX-library implementation
(for systems whose own libc is the interface)"] --> K + R3["runner ③"] -. runs the artifact on.-> consumer +``` + +The specification's own rules make the layering safe: an implementation may +declare an interface absent (the conformance suite decides which absences are +legal), a C library above the interface never sits above an implementation +that would call back into it (layer-cycle refuses that graph at resolution), +and a program states which ABI it speaks by depending on one. + +## 3. What the merged round established + +Measured, not aspirational (each item landed with its gate green): + +1. **Rows as the only place targets differ.** Eighteen identities build from + one engine with zero per-target code; the identity that was missing + arrived as one table row plus one base package. +2. **Base packages.** Where a row names one, the payload is the target's C + library: the driver gets `--sysroot`, the payload's own `-B/-L`, a + loader folded sysroot-relative, and the compiler's runtime directory — + and the host's link words, runtime directories and closure rules yield + entirely. A cross needs nothing from the machine it stands on. +3. **Foreign-kernel execution.** An artifact of another system, of this + machine's architecture, runs as a child: its death by signal is named, + its status returned unchanged, and the runner declaration follows it. +4. **ABI members as data.** `abi longjmp` states how a jump crosses the + WebAssembly boundary; the probe renders the flag on both sides, and a + row that is not the member's subject is refused rather than ignored. +5. **Corrective revisions.** A resource whose bytes must change without a + version change is re-published under the same key with a raised + revision; the updater channel enforces that a raise is total — every + platform, one sha per url, shapes compared. +6. **One file, several systems.** A pack can hold the programs of several + systems: a PE leads with a POSIX script in its DOS header, and every + POSIX system's program rides behind it. + +## 4. What the next systems consume + +The same planes, with no engine work: + +| Scenario | What it uses | What is left to build | +|---|---|---| +| A POSIX-family kernel with its own libc | the POSIX-library implementation of the ABI (already above every such libc), rows, a cross toolchain payload | the toolchain payload; a disk-image asset; an emulator runner | +| Firmware (UEFI, SBI) | the existing firmware implementations; an ordinary PE row and three link flags state a UEFI application — measured, no new target kind | an OVMF/emulator runner entry; a status convention for environments without exit codes | +| A system's official media, booted | the disk-image asset class (`-disk-image`): official media assembled into a bootable image, serial console, a marker protocol that separates the program's output from the machine's noise, exit codes carried through | the generic emulator harness (overlay isolation, expect/send primitives, the marker protocol); the naming and attribution conventions | +| Freestanding and bare-metal targets | the freestanding rows; a C-only, no-module path | the conformance hook-ups | +| Any corrective republish | the revision channel | nothing | + +The one boundary this architecture states out loud: the **semantic plane** — +a process model, a terminal — is the program's own. The infrastructure +answers that a target can be built, started and judged; it does not port the +program. + +## 5. Naming (index side) + +- Generic assets and toolchains carry no prefix; a tool whose contract is a + build-system session carries that system's prefix. +- A bootable-image asset is named `-disk-image` — the thing, not a + mechanism, not a format; the asset's file names carry the format. +- Official media is attributed to its publisher in the recipe; repacked + bytes are provenance-recorded, double-mirrored, sha-identical. + +These are conventions of the index, not engine vocabulary; the engine knows +none of the names. From d6abf3a7550e59ea56981796995dedcc21c05ffd Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:43:58 +0900 Subject: [PATCH 09/15] 2026.11.12.1: the release the ecosystem round lands in; the three-plane architecture as its reference The version the PR will release under is dated for its merge day, and every comment that cites its introducing release follows it. The architecture record names what the round established and what the families after it consume: rows, payloads and runners as data; one C library above one kernel-ABI specification, ported once; layer vocabulary the rows, payloads and requirement side already share; corrective revisions; the disk-image asset class and the harness that boots it; the semantic plane stated as the program's own. It adds no behaviour -- it names what the gates already hold green. --- ...6-11-12-three-plane-platform-architecture-design.md | 10 ++++++++-- CHANGELOG.md | 2 +- docs/10-pack-and-release.md | 2 +- docs/22-target-side.md | 2 +- docs/zh/22-target-side.md | 2 +- mcpp.toml | 2 +- modules/manifest/src/toml.cppm | 2 +- modules/manifest/src/types.cppm | 2 +- modules/toolchain-model/data/builtin.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/build/flags.cppm | 2 +- src/build/prepare/base.cpp | 2 +- src/build/prepare/toolchain.cpp | 2 +- src/build/prepare/windows_resources.cpp | 2 +- src/build/resources.cppm | 2 +- src/build/runtime_validation.cppm | 2 +- src/pack/pack.cppm | 6 +++--- src/toolchain/registry.cppm | 2 +- tests/unit/test_pack_modes.cpp | 2 +- tests/unit/test_pack_polyglot.cpp | 2 +- tests/unit/test_runner_lookup.cpp | 2 +- tests/unit/test_toolchain_triple.cpp | 2 +- 22 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md index ad4851af1..83ce7aab8 100644 --- a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -39,7 +39,7 @@ flowchart TB M --> A K --> M KF["firmware implementations
(UEFI boot services, RISC-V SBI)"] --> K - KP["kernel implementations
(the reference one, a BSD one,") --> K + KP["kernel implementations
(the reference kernel's own interface,
a BSD kernel's, a Windows kernel's)"] --> K KX["a POSIX-library implementation
(for systems whose own libc is the interface)"] --> K R3["runner ③"] -. runs the artifact on.-> consumer ``` @@ -75,6 +75,12 @@ Measured, not aspirational (each item landed with its gate green): 6. **One file, several systems.** A pack can hold the programs of several systems: a PE leads with a POSIX script in its DOS header, and every POSIX system's program rides behind it. +7. **Layer vocabulary.** `kernel-abi` is a layer the rows, the payloads and + the requirement side all speak (`layers.kernel-abi`, `requires`), an + implementation written above a C library never sits beneath one it would + call back into — the resolution refuses that graph before anything is + compiled — and a payload's kernel-abi is the target's system, so a + requirement stated on it holds across every route to the row. ## 4. What the next systems consume @@ -83,7 +89,7 @@ The same planes, with no engine work: | Scenario | What it uses | What is left to build | |---|---|---| | A POSIX-family kernel with its own libc | the POSIX-library implementation of the ABI (already above every such libc), rows, a cross toolchain payload | the toolchain payload; a disk-image asset; an emulator runner | -| Firmware (UEFI, SBI) | the existing firmware implementations; an ordinary PE row and three link flags state a UEFI application — measured, no new target kind | an OVMF/emulator runner entry; a status convention for environments without exit codes | +| Firmware (UEFI, SBI) | the existing firmware implementations; an ordinary PE row and three link flags state a UEFI application — measured, no new target kind; a runner's exit status is the program's (the contract already carries firmware: the harness states the status) | an OVMF/emulator runner entry | | A system's official media, booted | the disk-image asset class (`-disk-image`): official media assembled into a bootable image, serial console, a marker protocol that separates the program's output from the machine's noise, exit codes carried through | the generic emulator harness (overlay isolation, expect/send primitives, the marker protocol); the naming and attribution conventions | | Freestanding and bare-metal targets | the freestanding rows; a C-only, no-module path | the conformance hook-ups | | Any corrective republish | the revision channel | nothing | diff --git a/CHANGELOG.md b/CHANGELOG.md index 81913d6a9..bd30f93da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. -## [2026.11.11.1] - 2026-11-11 +## [2026.11.12.1] - 2026-11-12 ### Added diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index e0fb56e5a..e14d83dfe 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -857,7 +857,7 @@ names no `windows-aarch64`, so `aarch64-windows-gnu`, a row this mcpp builds, gets no runner from it). An image built on a Windows host is not the bytes of one built elsewhere (#850): pack trees on Linux or macOS. -**What `all` names is data, not a list (mcpp 2026.11.11.1+).** The machine's +**What `all` names is data, not a list (mcpp 2026.11.12.1+).** The machine's own system, every system a platform defines a row of for the architecture, and every system the platform's runner states a host of for it (`runner.hosts`): a platform whose runner exists for `freebsd-` gets FreeBSD's runners, diff --git a/docs/22-target-side.md b/docs/22-target-side.md index f79be1572..1c6f0f413 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1363,7 +1363,7 @@ package needs. |---|---|---|---| | `threads` | boolean | `-pthread` on a target that is neither PE nor freestanding; nothing on PE and on freestanding targets | the standard library module prebuild, the dependency scan, every C and C++ translation unit of every package, and the link | | `exceptions` *(mcpp 2026.9.12.3+)* | boolean, or `"wasm"` / `"js"` *(2026.10.30.1+)* | on `os = "emscripten"` only, on the compile line through the dialect flags and on the link: `"wasm"` is `-fwasm-exceptions`, WebAssembly's own exception handling; `"js"` and `true` are `-fexceptions`, the JavaScript-based model. Nothing on every other target, where exceptions are already the default | the same set `threads` reaches | -| `longjmp` *(mcpp 2026.11.11.1+)* | `"wasm"` or `"js"` | on `os = "emscripten"` only, on the compile line of every C and C++ translation unit and on the link: `"wasm"` is `-sSUPPORT_LONGJMP=wasm`, WebAssembly's own stack switching; `"js"` is `-sSUPPORT_LONGJMP=emscripten`, the JavaScript model and the toolchain's default. Declared on any other row, it is refused. An absent member renders nothing | the same set `threads` reaches | +| `longjmp` *(mcpp 2026.11.12.1+)* | `"wasm"` or `"js"` | on `os = "emscripten"` only, on the compile line of every C and C++ translation unit and on the link: `"wasm"` is `-sSUPPORT_LONGJMP=wasm`, WebAssembly's own stack switching; `"js"` is `-sSUPPORT_LONGJMP=emscripten`, the JavaScript model and the toolchain's default. Declared on any other row, it is refused. An absent member renders nothing | the same set `threads` reaches | All three members enter the dependency cache key through the flags they render, so a dependency built without one is never reused by a build with it, and one built diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 2d803403d..b68daabf1 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -1147,7 +1147,7 @@ WebAssembly 上,每个目标文件、预编译的标准库模块与链接必 |---|---|---|---| | `threads` | 布尔 | 在既非 PE 也非 freestanding 的目标上为 `-pthread`;在 PE 与 freestanding 目标上不产生任何 flag | 标准库模块的预构建、依赖扫描、所有包的每个 C 与 C++ 翻译单元,以及链接 | | `exceptions` *(mcpp 2026.9.12.3+)* | 布尔,或 `"wasm"` / `"js"` *(2026.10.30.1+)* | 只在 `os = "emscripten"` 上,经方言 flag 进入编译行,也进入链接行:`"wasm"` 为 `-fwasm-exceptions`,即 WebAssembly 自身的异常处理;`"js"` 与 `true` 为 `-fexceptions`,即基于 JavaScript 的模型。在其余每个目标上什么都不产生,因为那里异常本来就是默认开启的 | 与 `threads` 相同的那一套 | -| `longjmp` *(mcpp 2026.11.11.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `threads` 相同的那一套 | +| `longjmp` *(mcpp 2026.11.12.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `threads` 相同的那一套 | 两个成员都经由方言 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。 diff --git a/mcpp.toml b/mcpp.toml index e31c70818..dc39019df 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.11.1" +version = "2026.11.12.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 8f913df36..b235a3389 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -4489,7 +4489,7 @@ std::expected parse_document(const t::Document& documen // 2026-10-29 §6): "wasm" or "js"; `true` keeps "js". const bool model = ak == "exceptions" && av.is_string() && (av.as_string() == "wasm" || av.as_string() == "js"); - // `longjmp` is a model only (2026.11.11.1): "wasm" or + // `longjmp` is a model only (2026.11.12.1): "wasm" or // "js", no boolean -- the engine's default is the // toolchain's own ("js"), and an absent member is that // default rather than a switch turned off. diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d1673b65c..21453c223 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1173,7 +1173,7 @@ struct BuildConfig : BuildInputs { // "wasm" (`-fwasm-exceptions`), "js" (`-fexceptions`), or empty for // `exceptions = true`, which keeps its meaning since 2026.9.12.3: "js". std::string abiExceptionsModel; - // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // `[target..abi] longjmp` -- the third member (2026.11.12.1): // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary on an // Emscripten row. A model, not a switch: "wasm" (`-sSUPPORT_LONGJMP=wasm`, // WebAssembly's own stack switching) or "js" (`-sSUPPORT_LONGJMP=emscripten`, diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 38368dc87..8130c137f 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -431,7 +431,7 @@ name = "glibc" serves = ["aarch64-linux-gnu"] priority = 100 # The payload: native on each Linux arch; the cross from x86_64 takes the C -# library from a sysroot payload the row names (2026.11.11.1, G1) -- llvm +# library from a sysroot payload the row names (2026.11.12.1, G1) -- llvm # alone carries no glibc of another architecture, and the host's is not the # target's. The payload's package root IS the sysroot (glibc 2.44 with the # kernel headers, libgcc and libc++ inside), so the row is served wherever diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 1ca3d45db..769ff0148 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.11.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.12.1"; } // namespace mcpp diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 85df28235..37cc75a60 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -2733,7 +2733,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // A BASE PACKAGE'S RUN-TIMES (design 2026-11-10 E5). f.ld += base_link_words(true); - // The base's run-time directory reaches the C link too (2026.11.11.1): + // The base's run-time directory reaches the C link too (2026.11.12.1): // a hosted cross over a sysroot payload links its compiler run-time from // the payload's own resource directory, and the C program's link is the // one that had nowhere to find it. diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index c12712e6d..8a4cb7e5d 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -540,7 +540,7 @@ std::expected step2_resolve_base(PrepareState& state) { } // A LOADER INSIDE A CROSS BASE IS THE SYSROOT'S, NOT THIS MACHINE'S - // (2026.11.11.1): a --sysroot link names it sysroot-relative + // (2026.11.12.1): a --sysroot link names it sysroot-relative // (/lib/ld-linux-aarch64.so.1), and so does the artifact's PT_INTERP -- // the form qemu -L , a runner declaration and the ld scripts // beside it (the base's own libc.so GROUPs /lib/...) all resolve. An diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index f5edafbbc..4479df961 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1428,7 +1428,7 @@ static std::expected step1_device_axis_and_layer_merge(Prepar state.add_once(state.m->buildConfig.ldflags, flag); state.add_once(state.m->buildConfig.graphLdflags, flag); } - // `[target..abi] longjmp` -- the third member (2026.11.11.1): + // `[target..abi] longjmp` -- the third member (2026.11.12.1): // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary. The flag // changes the CODE emcc emits for the two calls, so it reaches both // sides -- every translation unit's flags (C and C++ alike; the C library diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index a632dec3a..d0bc179ad 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -74,7 +74,7 @@ import mcpp.bmi_cache; namespace mcpp::build { -// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.11.1, G3): a row whose +// THE WINDOWS HEADERS EVERY PE ROW SHARES (2026.11.12.1, G3): a row whose // system the graph supplies -- `x86_64-windows-musl` over openkal -- has no // paired payload, and its llvm binary carries no Windows headers, so // `winres.h` reaches neither of the include sources the toolchain states. The diff --git a/src/build/resources.cppm b/src/build/resources.cppm index 4dbe50470..47bad2cf9 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -101,7 +101,7 @@ std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple); // gcc's own) `-w64-mingw32/include` (design 2026-11-10 E3). One list for the // compile's `/I` and for the scan of what the script reads. // -// `mingw_headers` is the third source (2026.11.11.1, G3): the include +// `mingw_headers` is the third source (2026.11.12.1, G3): the include // directory of the MinGW-w64 sysroot payload the platform's rows declare, // which is where the Windows headers of a row whose system the graph supplies // -- `x86_64-windows-musl`, no paired payload, an llvm binary that carries no diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index fe7b326e6..475cf0ef9 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -686,7 +686,7 @@ ValidationReport validate_changed_artifacts( } // A ROW'S BASE PACKAGE IS THE TARGET'S C LIBRARY (the gnu-sysroot form, - // 2026.11.11.1): the artifact's loader and libc are the payload's, and + // 2026.11.12.1): the artifact's loader and libc are the payload's, and // the process these rules describe -- one process on this machine with // this machine's binding -- is not the one that would load it. The row's // declaration is the statement, exactly as an SDK's is above; the diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 1b769fa27..2798662ce 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -569,7 +569,7 @@ std::string unbuilt_row(std::string_view system, std::string_view arch); // §4.1): an ``, an `-` of that architecture, or every system // for `all`; this machine's system when it names none. // -// `all` is the DATA's answer (2026.11.11.1, G5), not a list spelled here: +// `all` is the DATA's answer (2026.11.12.1, G5), not a list spelled here: // this machine's system, every system a platform defines a row of for the // architecture (mcpp#911 G10), and every system the platform's runner states // a host of for it (`runner.hosts`) -- which is where FreeBSD enters, a @@ -2399,7 +2399,7 @@ ProgramRow program_row(std::string_view system, std::string_view arch, bool sing if (t && t->os == system && t->arch == arch) return { t->str(), "static", platform_ref_of(*P) }; } - // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.11.1, + // FREEBSD'S ROW IS THE MUSL IDENTITY THE PLATFORM SERVES (2026.11.12.1, // G5), as FreeBSD's own request takes it (design 2026-11-10 E4): // `*-freebsd-musl`, openkal-musl over openkal-freebsd, one static // program with no C library of the system beneath -- built over the @@ -3540,7 +3540,7 @@ std::vector systems_for(const std::vector& stated, std // This machine's system, every system the platforms of this // process define a row of for the architecture (mcpp#911 G10), // and every system the carrier's runner states a host of for it - // (2026.11.11.1, G5) -- the rows the platform itself says its + // (2026.11.12.1, G5) -- the rows the platform itself says its // runner exists for, FreeBSD's among them. add(std::string(detail::host_system())); for (auto const* r : mcpp::toolchain::target::rows()) diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index ed668ce06..a1b0de2ae 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -1368,7 +1368,7 @@ bool host_can_serve(const triple::Triple& target) { if (target.is_musl()) return !crossArch || mcpp::toolchain::platform::host_arch == "x86_64"; // A CROSS WHOSE ROW NAMES A SYSROOT PAYLOAD IS SERVED WHEREVER - // THAT PAYLOAD IS PUBLISHED (2026.11.11.1, G1). llvm alone carries + // THAT PAYLOAD IS PUBLISHED (2026.11.12.1, G1). llvm alone carries // no glibc of another architecture, which is what `return // !crossArch` answered: no payload here produces it. The row's // own declaration is the better answer -- a provider that serves diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index 228e898bc..eb6815594 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -53,7 +53,7 @@ UTest dispatched_format_stages_without_it{"PackClosureUnavailableOutcome/Dispatc expect(eq(closure_unavailable_outcome(Format::Dispatched), ClosureUnavailableOutcome::StageWithoutClosure)); }}; -// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.11.1 answers +// `--systems` (design 2026-10-29 §4.1; `all` since 2026.11.12.1 answers // with the data): `` for every architecture of the legs, `-` // for one; unstated, this machine's. `all` is this machine's system, the // systems the builtin rows define for the architecture, and -- with a diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 5ce8bfc09..07d2a8940 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -68,7 +68,7 @@ UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [ expect(!(pg::assemble("app", {}).has_value())); expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - // FreeBSD is a POSIX system of the file since 2026.11.11.1 (G5): its + // FreeBSD is a POSIX system of the file since 2026.11.12.1 (G5): its // program rides behind the script head beside Linux's. What still has // no place is a system the script names no `uname` pattern for. expect(!(pg::assemble("app", { program("plan9", "x86_64", "x") }).has_value())); diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 4ef7d5271..5e86587a7 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -149,7 +149,7 @@ UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbati expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice }}; -// The classification of a death by signal (2026.11.11.1, G6): the three +// The classification of a death by signal (2026.11.12.1, G6): the three // signals the mwrap of the C library's macros carries that an artifact of // another system's kernel interface dies by on its first kernel call, and // nothing else — a normal exit, an exit by a signal the classification diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index e529ba405..748cb3c85 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -637,7 +637,7 @@ UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTie // the LLVM 23.1.3/glibc default, its deployed pack, GNU self-host, // all 147 suites and four real index members, from source. The tier // stays `preview` until the released engine is consumed on a clean - // ARM64 host (SPEC-009 §10.7). From 2026.11.11.1 the row names the + // ARM64 host (SPEC-009 §10.7). From 2026.11.12.1 the row names the // sysroot payload its C library comes from (G1) -- llvm alone // carries no glibc of another architecture -- and the linkage is // glibc's own dynamic one: the compiler run-time is the payload's From ff990b2e750e983a4c1b934c25726972adb5eacf Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:44:33 +0900 Subject: [PATCH 10/15] docs: the architecture record carries its front matter, as the gate requires --- .../2026-11-12-three-plane-platform-architecture-design.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md index 83ce7aab8..9c8e0fb22 100644 --- a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -1,3 +1,8 @@ +--- +subject: architecture +status: active +--- + # The three-plane platform architecture: mcpp × openkal × xim Date: 2026-11-12 From 891e1f5896be5710031297e1155209fbcdda3c41 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:44:52 +0900 Subject: [PATCH 11/15] docs: the agents index lists the new record --- .agents/docs/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.agents/docs/README.md b/.agents/docs/README.md index f7cd9cdd8..06873dfa9 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,12 +18,16 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -358 records. +359 records. ## By subject Records that declare one. Everything else is listed by date below. +### architecture + +- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — active + ### build - [大型工作区的构建空间与复用(mcpp#874)](2026-10-31-workspace-space-and-reuse-design.md) — active @@ -188,6 +192,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-11 +- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — active - [测试与依赖解析补充方案:mcpp、mcpp test、mtest、mcpp-ide](2026-11-10-test-and-resolution-supplement-design.md) — active - [生态方案:跨平台通用基础设施(mcpp + openkal + xim),以一个 C89 工程为验证](2026-11-10-cross-platform-ecosystem-design.md) — active - [目标与提供者:统一的目标模型(结合 mcpp#911)](2026-11-09-target-model-design.md) — active From 262357aa935638e453e55731c16988e45ba27e68 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 08:59:00 +0900 Subject: [PATCH 12/15] 1076 runs again: the index carries the sysroot payload it skips without The exception belonged to the window where no hosted shard could install xim:aarch64-linux-gnu-sysroot; the index PR is merged, every shard that declares the payload's capabilities runs the test for real, and a test that runs does not belong in the table. --- tests/e2e/coverage-exceptions.tsv | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/coverage-exceptions.tsv b/tests/e2e/coverage-exceptions.tsv index 6e346fca3..13a168b7a 100644 --- a/tests/e2e/coverage-exceptions.tsv +++ b/tests/e2e/coverage-exceptions.tsv @@ -2,4 +2,3 @@ # One `\t` line each. A test that runs on some shard, or that a # workflow names, does not belong here, and the check fails if it is listed. 658_a_program_runs_on_an_attached_android_device.sh needs an Android device attached over adb; the android job of ci-linux-e2e.yml runs `mcpp test` on an emulator instead -1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh needs the xim:aarch64-linux-gnu-sysroot payload installed; no hosted shard installs it, and the test skips rather than guesses until the index carries the payload From e603382021afb6d56d80934554d3cad02609346f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 09:25:17 +0900 Subject: [PATCH 13/15] root_position_key keeps its cross-version shape; abi longjmp invalidates through the flags it renders The key is a contract between releases: an engine of one release finds a project's graph current through it, and 1068's inner build -- the subos's own engine -- is that reader by design. A field for the new member made every plan another release wrote look foreign, and the self-host shard measured it twice (1068, 1069). The member needs no field: it reaches the compile line through dialect_cxxflags, which the key already lists, and 1079 round-trips it unchanged. Threads and exceptions keep their fields -- they are switches that render no flag of their own. --- src/project.cppm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/project.cppm b/src/project.cppm index a96f8d347..4a34f1392 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,7 +1095,12 @@ export std::string root_position_key( auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; std::string abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" + b.abiExceptionsModel : "0") : "-"; - std::string abiLongjmp = b.abiLongjmpDeclared ? b.abiLongjmpModel : "-"; + // `abi longjmp` needs no field of its own here: it reaches the compile + // line through `dialect_cxxflags` (prepare adds the flag there), and this + // key is a CROSS-VERSION contract -- a project's plan stays current for an + // engine of another release reading it (1068's inner build, the subos's + // own engine, must find the graph current). Threads and exceptions have + // fields because they are booleans that render no flag of their own. if (applies) for (auto const& cc : m.conditionalConfigs) { if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared @@ -1105,7 +1110,9 @@ export std::string root_position_key( dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" + cc.abiExceptionsModel : "0"; - if (cc.abiLongjmpDeclared) abiLongjmp = cc.abiLongjmpModel; + // abi longjmp: rendered into dialect_cxxflags by prepare; adding + // a field here would orphan every plan a differently-released + // engine wrote (measured: 1068 on a self-host shard, twice). } list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); @@ -1119,7 +1126,6 @@ export std::string root_position_key( field("ios_deployment_target", b.iosDeploymentTarget); field("abi_threads", abiThreads); field("abi_exceptions", abiExceptions); - field("abi_longjmp", abiLongjmp); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); From cf1492ff55be35ac85bbb4a3f3fea71b33ba3de5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 12:04:10 +0900 Subject: [PATCH 14/15] split the per-subject manifest policy out of step13_windows_resources: the clang-tidy 400-line gate under src/build/prepare (the predicates the block already named) --- src/build/prepare/windows_resources.cpp | 75 +++++++++++++++++-------- 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index d0bc179ad..7d70ebb5b 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -214,6 +214,53 @@ static std::vector resource_subjects(PrepareState& state, BuildContext& return subjects; } + +// ─── The per-subject manifest policy: code page and long paths ──────────── +// +// The predicates step13 applies to every subject, as functions so the gate's +// 400-line budget holds (split at the boundary the block already had). + +static bool subject_declares_own_manifest(const mcpp::manifest::Manifest& M, + const Subject& S, bool hostToolBuild) +{ + return hostToolBuild + && std::ranges::any_of(M.resources.files, [&](const auto& f) { + const auto abs = (f.is_absolute() ? f : (S.dir / f)).lexically_normal(); + return mcpp::build::resources::scan_rc(abs).declaresManifest; + }); +} + +static std::string_view target_code_page(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + if (!t.windowsCodePage.empty()) return t.windowsCodePage; + return (hostToolBuild && t.is_program() && !ownManifest) ? "utf-8" : "legacy"; +} + +// `windows_long_paths` (design 2026-10-16 §11), with the same default: a +// host tool is long-path aware unless it says otherwise. +static bool target_long_paths(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + if (const auto& stated = t.windowsLongPaths) return *stated; + return hostToolBuild && t.is_program() && !ownManifest; +} + +// A test program carries the code page `[test] windows_code_page` gave it, +// and nothing by default: the host-tool default is a program's. +static bool target_takes_code_page(const mcpp::manifest::Target& t) +{ + return t.is_program() || t.kind == mcpp::manifest::Target::TestBinary; +} + +// Whether an image carries an application manifest of mcpp's: a UTF-8 code +// page or long paths asked for. +static bool target_manifested(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + return target_code_page(t, hostToolBuild, ownManifest) == "utf-8" + || target_long_paths(t, hostToolBuild, ownManifest); +} std::expected step13_windows_resources(PrepareState& state, BuildContext& ctx) { // ─── Windows resources: [resources] → a tracked link input (mcpp#365) ── // @@ -271,32 +318,16 @@ std::expected step13_windows_resources(PrepareState& state, B && (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary); }); }; - const bool ownManifest = hostToolBuild - && std::ranges::any_of(M.resources.files, [&](const auto& f) { - const auto abs = (f.is_absolute() ? f : (S.dir / f)).lexically_normal(); - return mcpp::build::resources::scan_rc(abs).declaresManifest; - }); - auto codePageOf = [&](const mcpp::manifest::Target& t) -> std::string_view { - if (!t.windowsCodePage.empty()) return t.windowsCodePage; - return (hostToolBuild && t.is_program() && !ownManifest) ? "utf-8" : "legacy"; + const bool ownManifest = subject_declares_own_manifest(M, S, hostToolBuild); + auto codePageOf = [&](const mcpp::manifest::Target& t) { + return target_code_page(t, hostToolBuild, ownManifest); }; - // `windows_long_paths` (design 2026-10-16 §11), with the same - // defaults: a host tool is long-path aware unless it says otherwise. auto longPathsOf = [&](const mcpp::manifest::Target& t) { - const auto& stated = t.windowsLongPaths; - if (stated) return *stated; - return hostToolBuild && t.is_program() && !ownManifest; - }; - // A test program carries the code page `[test] windows_code_page` - // gave it, and nothing by default: the host-tool default is a - // program's. - auto takesCodePage = [](const mcpp::manifest::Target& t) { - return t.is_program() || t.kind == mcpp::manifest::Target::TestBinary; + return target_long_paths(t, hostToolBuild, ownManifest); }; - // Whether an image carries an application manifest of mcpp's: a - // UTF-8 code page or long paths asked for. + auto takesCodePage = target_takes_code_page; auto manifested = [&](const mcpp::manifest::Target& t) { - return codePageOf(t) == "utf-8" || longPathsOf(t); + return target_manifested(t, hostToolBuild, ownManifest); }; const bool anyUtf8Image = std::ranges::any_of(M.targets, [&](const auto& t) { return active_target(t) && takesCodePage(t) && manifested(t); From 0819c80eccd64095f24b0a36bc3f22112b34397b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 12:08:09 +0900 Subject: [PATCH 15/15] keep the long-path lookup's if off the windowsLongPaths line: the ownership gate reads substrings --- src/build/prepare/windows_resources.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 7d70ebb5b..0d00e37c0 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -242,7 +242,8 @@ static std::string_view target_code_page(const mcpp::manifest::Target& t, static bool target_long_paths(const mcpp::manifest::Target& t, bool hostToolBuild, bool ownManifest) { - if (const auto& stated = t.windowsLongPaths) return *stated; + const auto& stated = t.windowsLongPaths; + if (stated) return *stated; return hostToolBuild && t.is_program() && !ownManifest; }