diff --git a/.gitignore b/.gitignore index 116bb5f7..1850f27a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ bench-child.log # to be expanded (`> $binDir`) and was not. Listed so the same slip is caught # next time rather than reviewed again. binDir +examples/*/target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd4cd40..735e96f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,50 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.8.24.3] — 2026-08-24 + +### 修复 + +- **⚠️ 三元组是请求,而解析把「未指定」抹掉了。** + + ``` + $ mcpp build --target x86_64-linux # 我写的是「不指定 C 库」 + Target x86_64-linux-gnu → x86_64-unknown-linux-gnu ← 被改写 + c-abi musl (openkal-musl@0.3.3, graph) ← 名字自相矛盾 + ``` + + 三元组同时充当**身份**(输出目录、缓存键、`cfg()` 的主语)与**请求**。 + 身份必须是全的,请求必须能说「没指定」;`parse` 用自动填充让身份变全, + 代价是请求消失 —— 两态在下游不可区分。 + + ⚠️ 修法是窄的:保留填充,另记 `Triple::envExplicit`。而请求**必须在规范化 + 之前捕获** —— `str()` 渲染的是填好的身份,之后再 parse 就分不出来了。 + + 未指定 ⇒ 报告显示工程写的那个拼写;写了且与图矛盾 ⇒ **拒绝**。 + +- **⚠️ 目标行的约定在图之前就被应用,而它要回答的问题在图之后才有答案。** + + `x86_64-linux-musl → gcc@16.1.0` 说的不是「偏好 gcc」,是「musl-gcc 载荷 + 供给这个目标的 C 库」。工程的 C 库若来自依赖图,该载荷根本不被使用。 + + ⚠️ 早决定被**双向实测**否掉:无条件应用会替换用户用 `mcpp toolchain default` + 设下的工具链;不应用会让一个零依赖的交叉构建从可用变为不可用。 + + ⭐ 判据换成它本来就该是的那个:**图供给 `kernel-abi` 或 `c-abi` 时,约定不适用。** + 工具链解析因此移到依赖解析之后。 + + ⚠️ 代码不搬,只搬执行时机 —— 原地包成 lambda,在图已知处调用。 + 先前记录的「39 处读写挡着」是**没测就写下的**:实测依赖解析段读 `tc` 仅 1 处, + 而那一处要的是三元组不是编译器。 + + 实测三格: + + | 场景 | 结果 | + |---|---| + | openkal 工程 + 全局 llvm 默认 + `--target x86_64-linux-musl` | `Resolved llvm@22.1.8` ✅ | + | 无依赖工程 + 同一目标 | `Resolved gcc@16.1.0`(约定生效)✅ | + | 无依赖工程 + `x86_64-windows-gnu` | `Resolved gcc@16.1.0` ✅ | + ## [2026.8.24.2] — 2026-08-24 ### 新增 diff --git a/docs/14-target-side.md b/docs/14-target-side.md index 1dd0fc3e..4303a1c0 100644 --- a/docs/14-target-side.md +++ b/docs/14-target-side.md @@ -120,6 +120,12 @@ interface. The environment field states a request for a C library; it is a request rather than the answer, and the resolved value is reported by the build. +Omitting the field declines to state one: `x86_64-linux` asks for whatever +supplies that layer, and `x86_64-linux-musl` asks for musl. When the dependency +graph supplies a different one the graph decides, and the build reports that the +name is inaccurate together with the spelling to use instead. The request is +ignored rather than violated, so the artifact is the same either way. + ### The Toolchain `mcpp toolchain default @`, `[toolchain]` in the manifest, or @@ -128,8 +134,9 @@ layer, which is the one layer no package may supply. A target row may carry a convention — a toolchain whose payload supplies that target's C library. The convention applies when the manifest states nothing for -that target. When it replaces a default set with `mcpp toolchain default`, the -status line reports the substitution and names the one-line override. +that target AND nothing in the dependency graph supplies the target's system. +The second condition is knowable only after resolution, so the toolchain is +resolved there rather than before it. ### Dependencies diff --git a/docs/15-openkal-cross.md b/docs/15-openkal-cross.md new file mode 100644 index 00000000..f15b4865 --- /dev/null +++ b/docs/15-openkal-cross.md @@ -0,0 +1,250 @@ +# Cross-Compilation Over openkal + +Conventional cross-compilation is served by a payload. A toolchain is built for +one target, its driver has exactly one answer, and reaching a second target +means obtaining a second toolchain. The number of payloads a distribution must +publish is therefore the number of host-target pairs it supports. + +openkal changes what is being crossed. The target side — the platform interface, +the C library, the compiler runtime and the C++ runtime — becomes a set of +packages resolved from the dependency graph and compiled from source by whichever +compiler is running. What remains for the compiler is code generation, and one +Clang binary emits every object format it was built with. + +This document states the model, what a project writes, what the ecosystem +supplies, and the limits that have been measured. + +## The Claim + +An ecosystem of N platforms and M architectures requires N implementations of +one interface rather than N×M toolchains. The count follows from where the +target side lives: a package built from source is built for whatever target the +compiler is asked to emit, so a platform implementation is written once and +reaches every architecture the compiler supports. + +The claim is verified by a matrix of three hosts and three targets, each cell +building one source and running the result. + +## What A Project Writes + +```toml +[dependencies] +openkal-llvm-runtime = "0.1.1" + +[toolchain] +default = "llvm@22.1.8" +``` + +Two lines. The first selects three layers of the target side; the second names +a compiler and says nothing about where anything else comes from. + +Targets are given on the command line: + +```bash +mcpp build --target x86_64-linux +mcpp build --target aarch64-macos +mcpp build --target x86_64-windows-gnu +``` + +No `[target.]` section is required for a hosted target, and no +preprocessor directive is required in the source. A worked example is +[examples/06-openkal-cross](../examples/06-openkal-cross). + +## What The Ecosystem Supplies + +| Package | Layer | Content | +|---|---|---| +| `openkal` | — | the specification, and the C++ modules that declare it | +| `openkal-linux` | `kernel-abi` | the reference implementation, on Linux system calls | +| `openkal-macos` | `kernel-abi` | on the macOS system-call surface | +| `openkal-windows` | `kernel-abi` | on Win32 and the object manager, using no C runtime symbol | +| `openkal-opensbi` | `kernel-abi` | on the RISC-V Supervisor Binary Interface, no operating system | +| `openkal-uefi` | `kernel-abi` | on UEFI Boot Services, before an operating system exists | +| `openkal-musl` | `c-abi` | musl redirected onto openkal, ported once | +| `openkal-llvm-runtime` | `compiler-runtime`, `c++-abi` | compiler-rt builtins, libunwind, libc++abi and libc++, configured for openkal-musl | + +A project names the last of these. The others follow from its dependencies. + +## Why The Compiler Must Be LLVM + +`openkal-llvm-runtime` declares the requirement rather than leaving it to be +discovered: + +```toml +requires = ["mcpp:compiler=llvm"] +``` + +Its sources are libc++'s, and its `std` module source in particular is compiled +by Clang. Handing that source to GCC fails inside libc++'s own headers, in a +message naming a file the reader has never opened: + +``` +fatal error: __config: No such file or directory +``` + +With the requirement declared, the build refuses the combination before it +compiles anything, and names the command that selects a compiler which satisfies +it. + +## How The Target Is Chosen + +The target row of mcpp's own vocabulary may carry a toolchain convention. That +convention names the payload which supplies **that target's C library**, and it +applies only when two conditions hold: the manifest states nothing for the +target, and nothing in the dependency graph supplies the target's system. + +The second condition is knowable only after the graph is resolved. A project +whose C library comes from `openkal-musl` therefore keeps the compiler it asked +for, while a project with no dependencies still receives the payload the row +names. Both behaviours were measured; deciding either way in advance was wrong +for the other. + +## The Environment Segment + +On Linux the third segment of a target triple names the C library. Under openkal +the C library comes from the graph, so a triple that names one states a request +the graph may not honour: + +``` +mcpp build --target x86_64-linux-gnu # asks for glibc + c-abi musl (openkal-musl@0.3.3, graph) +``` + +The graph decides. Omitting the segment states no request and produces the same +artifact: + +``` +mcpp build --target x86_64-linux +``` + +The build reports the mismatch when the segment is present and disagrees. It is +a report rather than a refusal, because the segment is ignored rather than +violated. Measured on one host, `x86_64-linux` against `x86_64-linux-musl`: the +two executables differ, and after stripping they are byte-identical. What +differs is the debug information, which records the output directory, and the +directory is named after the triple. The code is the same code. + +On Windows the same segment names the object ABI instead — `gnu` for PE with the +GNU ABI, `msvc` for PE with Microsoft's — and both are compatible with more than +one C library. The mismatch report is therefore scoped to platforms where the +segment names a C library. + +## Bare Metal + +A target with no operating system is the same model with the platform layer +supplied by firmware rather than by a kernel. `riscv64-none-elf` over OpenSBI +runs the same source as a hosted target, including `import std`, because the +standard library it uses is the one the graph supplied rather than the +compiler's own. + +Two things must be declared, both properties of the board rather than defaults: + +```toml +[target.riscv64-none-elf] +sysroot = "" +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +`sysroot = ""` selects the zero-libc tier. Which machine model and which +firmware mode to use are board facts, and an engine that guesses one is an +engine a different board has to fight. + +### The Source Is The Same, The Program Is Not + +"The same source" is a claim about the toolchain and the standard library, and +it holds: `import std` works, the C++ runtime is the one the graph supplied, and +no `#if` distinguishes the targets. It is not a claim that any given program +builds for any given target, and the specification is explicit about why. + +A bare-metal backend provides some interfaces and not others. `openkal-opensbi` +provides `abort`, `stream`, `memory`, `env` and `time`; it provides no +filesystem and no tasks, because the machine has none. Clause 6.1 makes that +absence a link-time fact: + +> An interface that an implementation does not provide is absent as a link-time +> definition, and a consumer that uses it fails to link. + +A capability word therefore answers a narrower question than it first appears +to. It says how an implementation behaves *within an interface it provides* — +whether names are compared case-sensitively, what the granularity of a clock is. +Whether the interface exists at all is answered before that, by the dependency +graph, and failing that by the linker. + +The distinction is easy to lose, because the query is an inline function over a +data object, so a program that merely asks whether a filesystem exists takes the +address of `kal_fs_props` and fails to link with no filesystem call anywhere in +it. Defining that word as zero in the backend removes the error and is the one +remedy the clause forbids: the program then proceeds past the point the linker +existed to stop it at. It was tried, published as `openkal-opensbi@0.1.3`, and +retracted. + +### Two Routes To A Bare x86_64 Machine + +An x86_64 machine with no operating system is reached in two different ways, and +the difference is what loads the program. + +| Route | Target | Platform layer | Entry | +|---|---|---|---| +| UEFI application | `x86_64-windows-gnu` | `openkal-uefi` | firmware, with Boot Services available | +| Kernel, or raw bare metal | `x86_64-none-elf` | none, or `openarch` | the reset vector, with nothing beneath | + +A UEFI application is PE/COFF entered through the Microsoft x64 calling +convention. Both are properties the LLVM toolchain already has, so its target is +the same triple as a Windows program and firmware function pointers are called +directly. What distinguishes it from a Windows build is which implementation of +the platform interface the graph resolved, together with three link flags that +select `IMAGE_SUBSYSTEM_EFI_APPLICATION`. + +A kernel has no firmware services to call. Its target is `x86_64-none-elf`, the +zero-libc tier: no C library on the compile line, no library directory on the +link, and `#include ` does not resolve. The program is entered at its +own `_start` and reaches hardware directly. + +`openarch` is the layer such a program builds on. It is not a platform interface +and does not answer to `mcpp:kernel-abi`; it is the architecture mechanism — +execution contexts, traps, per-CPU state and address spaces — presented as one +interface over several instruction sets, with a backend package per instruction +set. A kernel depends on it and supplies its own platform layer, or none. + +### Why x86_64 Bare Metal Required Engine Work + +`riscv64-none-elf` and `aarch64-none-elf` are rows in a table and nothing more: +Clang has a BareMetal toolchain for both, drives their links itself and reaches +`ld.lld`. It has none for x86_64, so that triple falls through to the generic +GCC toolchain, whose linker is the host's `g++`: + +``` +g++: error: unrecognized command-line option '-fuse-ld=…/ld.lld' +``` + +Measured for every spelling of a bare x86_64 triple, and not correctable by any +flag. The row therefore carries a linker emulation and mcpp invokes `ld.lld` +itself, which is also why the host toolchain must be shown not to participate in +such a link. + +## Measured Limits + +Three, recorded because each was found by building rather than by reading. + +**A backend must define every capability word.** The specification's queries are +inline functions over property objects, so a program that merely asks whether a +filesystem exists takes the address of `kal_fs_props`. A backend that omits the +words for layers it lacks makes the question fail to link on exactly the class of +machine the question exists for. + +**Two suppliers of one layer is an error rather than a choice.** A C library, a +platform interface and a C++ runtime are mutually exclusive. Selecting the wrong +one does not fail the link; it produces a program that runs and intermittently +does not. + +**A payload's C++ runtime cannot sit above a foreign C library.** Its +`__config_site` records the configuration it was built with. The resolver's +structure prevents the combination on the default path, and a diagnostic covers +the paths where a project overrides the contract explicitly. + +## Reference + +[docs/14 — The Target Side](14-target-side.md) for the five layers, the four +origins and the rules. [SPEC-002](spec/target-side.md) for the normative +statement of the capability grammar. diff --git a/docs/README.md b/docs/README.md index 83d07c44..1d3d3c72 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,7 @@ - [12 - Distributing a Prebuilt Library](12-binary-distribution.md) - [13 - Bare-Metal and Freestanding Targets](13-baremetal.md) - [14 - The Target Side](14-target-side.md) +- [15 - Cross-Compilation Over openkal](15-openkal-cross.md) ## Specifications diff --git a/docs/spec/target-side.md b/docs/spec/target-side.md index 5445c020..05bf8e72 100644 --- a/docs/spec/target-side.md +++ b/docs/spec/target-side.md @@ -132,6 +132,32 @@ mcpp:<层名>[=<实现名>] ⚠️ 当前实现覆盖「两层均来自图」与「两层均来自载荷」。 「一层预制、一层来自图」的接线尚不完整。 +### 3.4 规则四:三元组是请求 ✅ 已实现 + +三元组的 env 段**必须**被当作对 `c-abi` 的请求,而非其答案。 + +- 段缺席 ⇒ 未作陈述,任何供给者都不与之矛盾; +- 段存在且与解析出的 `c-abi` 不同,且后者来自图 ⇒ 引擎**必须**报出该不一致, + 并**必须**给出不含该段的目标拼写;**禁止**据此使构建失败。 + +⚠️ 拒绝曾被实现并被实测否掉:它打破了每一个把宿主目标拼作 `x86_64-linux-gnu` +的工程与 CI 配置 —— 而那正是 `mcpp toolchain list` 打印的拼写。 +判据是该请求**不改变任何东西**:图两种写法下都供给同一个 C 库, +因此该段是被忽略而非被违反。 + +⚠️ 规范化会把 `x86_64-linux` 写成 `x86_64-linux-gnu`,因为身份必须是全的。 +请求**必须**在规范化之前捕获;报告**应当**显示工程书写的拼写。 + +### 3.5 目标表的约定何时生效 ✅ 已实现 + +目标行的 `pin` 命名的是**供给该目标 C 库的载荷**,不是偏好的编译器。 +它**必须**仅在两个条件同时成立时生效:清单对该目标未作陈述, +且依赖图中无人供给 `kernel-abi` 或 `c-abi`。 + +⚠️ 第二个条件在依赖解析之后才可知,因此工具链**必须**在其之后解析。 +过早决定被双向实测否掉:无条件应用会替换用户用 `mcpp toolchain default` +设下的工具链;不应用会让一个零依赖的交叉构建从可用变为不可用。 + --- ## 4. 报告 diff --git a/docs/zh/14-target-side.md b/docs/zh/14-target-side.md index 29c0133e..f23ee673 100644 --- a/docs/zh/14-target-side.md +++ b/docs/zh/14-target-side.md @@ -100,6 +100,11 @@ C 库、平台接口与 C++ 运行时是互斥的选择,而非可叠加的贡献 `--target <三元组>`,或 `[build] target`。OS 段选择平台接口。 env 段陈述一条对 C 库的请求;它是请求而非答案,解析出的值由构建报告。 +省略该段即为不陈述:`x86_64-linux` 请求「供给该层的任何实现」, +`x86_64-linux-musl` 请求 musl。依赖图供给了另一个时以图为准, +构建会报出该名字不准确并给出应当使用的拼写。该请求是被忽略而非被违反, +因此产物两种写法下相同。 + ### 工具链 `mcpp toolchain default <族>@<版本>`、清单中的 `[toolchain]`, @@ -107,8 +112,9 @@ env 段陈述一条对 C 库的请求;它是请求而非答案,解析出的值 唯一一个任何包都不能供给的层。 目标表的行可以携带一条约定,即其载荷供给该目标 C 库的工具链。 -该约定在清单对该目标未作陈述时生效。当它替换了由 `mcpp toolchain default` -设定的默认时,状态行报出该替换并给出一行覆盖写法。 +该约定在两个条件同时成立时生效:清单对该目标未作陈述,**且**依赖图中没有任何 +东西供给该目标的系统。第二个条件只有在解析之后才可知, +因此工具链在那之后解析,而不在那之前。 ### 依赖 diff --git a/docs/zh/15-openkal-cross.md b/docs/zh/15-openkal-cross.md new file mode 100644 index 00000000..eadb9e5e --- /dev/null +++ b/docs/zh/15-openkal-cross.md @@ -0,0 +1,209 @@ +# 基于 openkal 的交叉构建 + +传统的交叉构建由载荷承担。一份工具链为一个目标而构建,它的驱动只有一个答案, +到达第二个目标意味着获取第二份工具链。因此一个发行方必须发布的载荷数, +等于它支持的宿主-目标对数。 + +openkal 改变了「被交叉的是什么」。目标侧 —— 平台接口、C 库、编译器运行时与 +C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编译器从源码构建的包。 +留给编译器的是代码生成,而一个 Clang 二进制发出它被构建时所包含的每一种对象格式。 + +本文陈述该模型、工程需要书写的内容、生态所供给的内容,以及已被实测的界限。 + +## 论断 + +一个由 N 个平台与 M 个架构构成的生态,需要的是一个接口的 N 份实现, +而不是 N×M 份工具链。这个计数来自目标侧所在的位置:一个从源码构建的包, +是为编译器被要求发出的那个目标而构建的,因此一份平台实现只写一次, +就到达编译器支持的每一个架构。 + +该论断由一个三宿主 × 三目标的矩阵验证,每一格构建同一份源码并运行其结果。 + +## 工程书写的内容 + +```toml +[dependencies] +openkal-llvm-runtime = "0.1.1" + +[toolchain] +default = "llvm@22.1.8" +``` + +两行。第一行选定目标侧的三个层;第二行命名一个编译器, +并且对其余一切来自何处只字未提。 + +目标在命令行上给出: + +```bash +mcpp build --target x86_64-linux +mcpp build --target aarch64-macos +mcpp build --target x86_64-windows-gnu +``` + +宿主目标不需要 `[target.<三元组>]` 段,源码中也不需要任何预处理指令。 +可运行的示例见 [examples/06-openkal-cross](../../examples/06-openkal-cross)。 + +## 生态供给的内容 + +| 包 | 层 | 内容 | +|---|---|---| +| `openkal` | — | 规范,以及声明它的那些 C++ 模块 | +| `openkal-linux` | `kernel-abi` | 参考实现,建立在 Linux 系统调用之上 | +| `openkal-macos` | `kernel-abi` | 建立在 macOS 的系统调用面之上 | +| `openkal-windows` | `kernel-abi` | 建立在 Win32 与对象管理器之上,不使用任何 C 运行时符号 | +| `openkal-opensbi` | `kernel-abi` | 建立在 RISC-V 的 SBI 之上,无操作系统 | +| `openkal-uefi` | `kernel-abi` | 建立在 UEFI Boot Services 之上,在操作系统存在之前 | +| `openkal-musl` | `c-abi` | 被重定向到 openkal 的 musl,只移植一次 | +| `openkal-llvm-runtime` | `compiler-runtime`、`c++-abi` | compiler-rt builtins、libunwind、libc++abi 与 libc++,为 openkal-musl 配置 | + +一个工程命名其中最后一个。其余由它的依赖推出。 + +## 编译器为何必须是 LLVM + +`openkal-llvm-runtime` 把这项要求声明出来,而不是留待被发现: + +```toml +requires = ["mcpp:compiler=llvm"] +``` + +它的源码是 libc++ 的,其中 `std` 模块源尤其由 Clang 编译。 +把那份源码交给 GCC,会在 libc++ 自己的头文件深处失败, +其消息命名一个读者从未打开过的文件: + +``` +fatal error: __config: No such file or directory +``` + +有了这项声明,构建在编译任何东西之前拒绝该组合, +并指出选择一个满足它的编译器的那条命令。 + +## 目标如何被选定 + +mcpp 自身词表的目标行可以携带一条工具链约定。该约定命名的是 +**供给该目标 C 库的那份载荷**,并且仅在两个条件同时成立时生效: +清单对该目标未作陈述,且依赖图中没有任何东西供给该目标的系统。 + +第二个条件只有在图被解析之后才可知。因此一个 C 库来自 `openkal-musl` 的工程 +保留它所要求的编译器,而一个没有依赖的工程仍然收到该行所命名的载荷。 +两种行为均经实测;提前按任一方向决定,对另一方向都是错的。 + +## 环境段 + +在 Linux 上,目标三元组的第三段命名 C 库。在 openkal 之下 C 库来自图, +因此一个命名了 C 库的三元组陈述的是一项图未必兑现的请求: + +``` +mcpp build --target x86_64-linux-gnu # 请求 glibc + c-abi musl (openkal-musl@0.3.3, graph) +``` + +以图为准。省略该段即不作请求,并产出相同的产物: + +``` +mcpp build --target x86_64-linux +``` + +该段存在且不一致时,构建报出这一点。它是报出而非拒绝, +因为该段是被忽略而非被违反。在一台宿主上实测 `x86_64-linux` 与 `x86_64-linux-musl`:两个可执行文件不同,strip 之后逐字节相同。差异在调试信息里,它记录了输出目录,而目录以三元组命名。代码是同一份代码。 + +在 Windows 上,同一段命名的是对象 ABI —— `gnu` 是 PE 加 GNU ABI, +`msvc` 是 PE 加微软的 —— 而两者都与不止一种 C 库相容。 +因此该项报出被限定在该段命名 C 库的那些平台上。 + +## 裸机 + +一个没有操作系统的目标,是同一个模型,只是平台层由固件而非内核供给。 +`riscv64-none-elf` 之上的 OpenSBI 运行与宿主目标相同的源码,含 `import std`, +因为它所使用的标准库是图供给的那份,而不是编译器自带的那份。 + +有两样东西必须声明,两者都是板子的性质而非默认值: + +```toml +[target.riscv64-none-elf] +sysroot = "" +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +`sysroot = ""` 选定零 libc 档。使用哪个机器模型与哪种固件模式是板子的事实, +而一个去猜测它的引擎,是另一块板子必须与之搏斗的引擎。 + +### 源码是同一份,程序不是 + +「同一份源码」是关于工具链与标准库的断言,而它成立:`import std` 可用, +C++ 运行时是图供给的那份,没有任何 `#if` 区分目标。它不是「任何程序都能 +为任何目标构建」的断言,而规范把原因写得很清楚。 + +一个裸机后端提供一部分接口而不提供另一部分。`openkal-opensbi` 提供 +`abort`、`stream`、`memory`、`env` 与 `time`;它不提供文件系统也不提供任务, +因为这台机器没有。6.1 条把这种缺席定为链接期的事实: + +> 实现不提供的接口,作为链接期定义是缺席的,使用它的消费者链接失败。 + +因此能力字回答的问题比它初看上去更窄。它说的是一个实现**在它提供的接口内** +如何表现 —— 名字是否区分大小写、一个时钟的粒度是多少。接口是否存在, +由更早的东西回答:依赖图,以及退而求其次的链接器。 + +⚠️ 这个区分容易丢,因为查询是数据对象上的内联函数:一个程序**只是提问** +「有没有文件系统」,就取了 `kal_fs_props` 的地址,于是在整份源码没有任何 +文件系统调用的情况下链接失败。在后端把那个字定义为零可以消掉这个错误, +而它恰是该条禁止的唯一补法 —— 程序随后越过了链接器存在的意义。 +这条路走过,发布为 `openkal-opensbi@0.1.3`,并已撤回。 + +### 到达一台裸 x86_64 机器的两条路线 + +一台没有操作系统的 x86_64 机器有两种到达方式,区别在于**谁加载这个程序**。 + +| 路线 | 目标 | 平台层 | 入口 | +|---|---|---|---| +| UEFI 应用 | `x86_64-windows-gnu` | `openkal-uefi` | 固件,Boot Services 可用 | +| 内核,或裸机 | `x86_64-none-elf` | 无,或 `openarch` | 复位向量,其下一无所有 | + +UEFI 应用是 PE/COFF,经微软 x64 调用约定进入。两者都是 LLVM 工具链已有的性质, +因此它的目标与一个 Windows 程序是同一个三元组,而固件函数指针被直接调用。 +把它与一次 Windows 构建区分开的,是图解析出的平台接口实现, +以及三个选定 `IMAGE_SUBSYSTEM_EFI_APPLICATION` 的链接 flag。 + +一个内核没有固件服务可以调用。它的目标是 `x86_64-none-elf`,即零 libc 档: +编译行上没有 C 库,链接上没有库目录,`#include ` 不解析。 +程序在它自己的 `_start` 被进入,并直接触达硬件。 + +`openarch` 是这类程序所建立于其上的层。它不是平台接口,也不应答 +`mcpp:kernel-abi`;它是**架构机制** —— 执行上下文、陷阱、每 CPU 状态与地址空间 —— +作为一个跨若干指令集的接口呈现,每个指令集一个后端包。 +一个内核依赖它,并供给自己的平台层,或者不供给。 + +### x86_64 裸机为何需要引擎侧的工作 + +`riscv64-none-elf` 与 `aarch64-none-elf` 是表中的行,除此之外别无他物: +Clang 对两者都有 BareMetal 工具链,自行驱动它们的链接并到达 `ld.lld`。 +它对 x86_64 没有,于是那个三元组落到通用的 GCC 工具链,而后者的链接器是宿主的 `g++`: + +``` +g++: error: unrecognized command-line option '-fuse-ld=…/ld.lld' +``` + +对裸 x86_64 三元组的每一种拼写都实测过,且无法由任何 flag 纠正。 +因此该行携带一个链接器 emulation,由 mcpp 自己调用 `ld.lld` —— +这也是为什么必须证明宿主工具链没有参与这样一次链接。 + +## 已实测的界限 + +三条,记录在此是因为每一条都由构建而非由阅读发现。 + +**一个后端必须定义每一个能力字。** 规范的查询是属性对象上的 inline 函数, +因此一个仅仅询问是否存在文件系统的程序,会取 `kal_fs_props` 的地址。 +一个省略了自身所缺层的能力字的后端,使该问题恰恰在这个问题为之存在的那类机器上 +链接失败。 + +**同一层的两个供给者是错误而非选择。** C 库、平台接口与 C++ 运行时互斥。 +选错不会使链接失败,它产出一个能够运行且间歇性不能运行的程序。 + +**载荷的 C++ 运行时不能位于外来的 C 库之上。** 它的 `__config_site` 记录了 +它被构建时的配置。解析器的结构在默认路径上阻止该组合, +而一条诊断覆盖工程显式覆写该契约的那些路径。 + +## 参考 + +[docs/14 — 目标侧](14-target-side.md) 给出五个层、四种来源与规则。 +[SPEC-002](../spec/target-side.md) 给出能力语法的规范性陈述。 diff --git a/docs/zh/README.md b/docs/zh/README.md index e1c949af..ab490139 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -17,6 +17,7 @@ - [12 - 分发预编译库](12-binary-distribution.md) - [13 - 裸机与 freestanding 目标](13-baremetal.md) - [14 - 目标侧](14-target-side.md) +- [15 - 基于 openkal 的交叉构建](15-openkal-cross.md) ## 规范文档 diff --git a/examples/06-openkal-cross/README.md b/examples/06-openkal-cross/README.md new file mode 100644 index 00000000..bf9d2ca2 --- /dev/null +++ b/examples/06-openkal-cross/README.md @@ -0,0 +1,137 @@ +# 06 — One Source, Several Machines + +A program that asks each machine what it is, built for three targets from any +host without being edited. + +```bash +mcpp run # this machine +mcpp build --target x86_64-linux # Linux, from any host +mcpp build --target aarch64-macos # macOS, from any host +mcpp build --target x86_64-windows-gnu # Windows, from any host +``` + +`src/main.cpp` contains no preprocessor directive and no branch on a target +name. What differs between the builds is which packages the dependency graph +resolved. + +## What The Manifest States + +```toml +[dependencies] +openkal-llvm-runtime = "0.1.1" + +[toolchain] +default = "llvm@22.1.8" +``` + +One dependency supplies the compiler runtime and the C++ runtime, and depends in +turn on a C library, which depends on whichever implementation of the platform +interface matches the target being built. One line therefore selects three of +the five target-side layers. + +The toolchain line names a compiler and nothing else. Where the headers, the C +library, the C++ runtime and the platform implementation come from is not stated +in the manifest at all; the build reports what it resolved: + +``` + Target x86_64-windows-gnu → x86_64-w64-windows-gnu + kernel-abi openkal (openkal-windows@0.1.3, graph) + c-abi musl (openkal-musl@0.3.3, graph) + c++-abi libc++ (openkal-llvm-runtime@0.1.1, graph) +``` + +## What The Program Demonstrates + +The output is a table of facts about the machine the program landed on. The code +path producing it is identical everywhere; the answers are not. + +Measured, same binary source, two targets built on one Linux host: + +| Subject | `x86_64-linux` | `x86_64-windows-gnu` | +|---|---|---| +| preopened directories | 2 | 5 | +| case-sensitive paths | yes | no | +| monotonic granularity (ns) | 1 | 100 | + +A portable program written against a conventional stack is a program that +compiles under several sets of `#if`s. A program written against a named +interface is one source that queries what it landed on. The queries in +`src/main.cpp` are chosen so that each exercises a different layer of the target +side, and so that a build which silently reached the host's own libraries +instead of the resolved ones would answer differently. + +The unwinding check is the strictest of them. Unwinding is the one part of a C++ +runtime that links successfully whether or not it works: a program whose +unwinder is absent still builds, and fails only when something is thrown. +Running a destructor during the unwind separates the two. + +## What This Program Does Not Ask + +A capability word says how an implementation behaves **within an interface it +provides**. Whether it provides the interface at all is a different question, +and the specification answers it earlier and by other means: + +| Time | Mechanism | Question | +|---|---|---| +| dependency resolution | the package declares what it provides | may this program be built against this implementation | +| link | an undefined symbol | was an interface used that the implementation does not provide | +| run | a capability word | how does this implementation behave within an interface it provides | + +So this program may ask a filesystem how it compares names, and may not ask a +machine whether it has a filesystem. Building it for `riscv64-none-elf`, whose +backend implements `abort`, `stream`, `memory`, `env` and `time` and nothing +else, produces: + +``` +ld.lld: error: undefined symbol: kal_fs_props +>>> referenced by fs.cppm:136 +>>> obj/main.o:(kal::fs::properties@openkal.fs()) +``` + +The reference comes from the question rather than from any filesystem call, and +the error is clause 6.1 working: "an interface that an implementation does not +provide is absent as a link-time definition, and a consumer that uses it fails +to link." Defining the word as zero in the backend was tried, and it is the one +thing the clause forbids — the program then proceeds past the point the linker +existed to stop it at. + +## What A Bare-Metal Project Writes Instead + +A different program, in a different directory, asking only what that machine +provides. Two things must be declared, both properties of the board rather than +defaults: + +```toml +[target.riscv64-none-elf] +sysroot = "" +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +`sysroot = ""` selects the zero-libc tier. The emulator is named without a path, +and must stay that way: committing an absolute path puts one machine's layout +into a file every other machine reads. + +## Bare Metal On x86 + +Two routes, and neither belongs in this manifest. + +A **UEFI application** is PE/COFF entered through the Microsoft x64 calling +convention, so its target is `x86_64-windows-gnu` — the same triple as a Windows +program — distinguished by which implementation of the platform interface the +graph resolved. One manifest cannot resolve two different implementations for one +triple, so it needs its own. See `mcpplibs/openkal-uefi`. + +A **kernel** has no firmware services to call. Its target is `x86_64-none-elf`, +the zero-libc tier, entered at its own `_start`, reaching hardware directly. +There is no platform interface beneath it to depend on; what such a program +builds on is `mcpplibs/openarch`, the architecture-mechanism layer. + +Both routes are described in +[docs/15 — Cross-Compilation Over openkal](../../docs/15-openkal-cross.md). + +## Reference + +[docs/15 — Cross-Compilation Over openkal](../../docs/15-openkal-cross.md) for +the model, and [docs/14 — The Target Side](../../docs/14-target-side.md) for the +five layers and the rules that govern them. diff --git a/examples/06-openkal-cross/mcpp.toml b/examples/06-openkal-cross/mcpp.toml new file mode 100644 index 00000000..828f4ab1 --- /dev/null +++ b/examples/06-openkal-cross/mcpp.toml @@ -0,0 +1,30 @@ +[package] +name = "portable-report" +version = "0.1.0" +description = "One source, three hosted targets, over openkal" + +# There is no `[build] target` here, and its absence is what the directory is +# for. The target is given on the command line so that the same file can be +# built three ways without being edited. + +[dependencies] +# One line selects three of the five target-side layers. `openkal-llvm-runtime` +# supplies the compiler runtime and the C++ runtime, and depends in turn on +# `openkal-musl` for the C library, which depends on whichever implementation of +# openkal matches the target being built. +openkal-llvm-runtime = "0.1.1" + +[toolchain] +# A compiler, and nothing else. Where the headers, the C library, the C++ +# runtime and the platform implementation come from is not stated in this file; +# it follows from the dependency above and is resolved after the graph exists. +# The build reports what it resolved. +default = "llvm@22.1.8" + +# ⚠️ NO BARE-METAL TARGET SECTION, AND ITS ABSENCE IS DELIBERATE. +# +# This program asks about a filesystem and about tasks. An implementation that +# provides neither is absent as a link-time definition — clause 6.1 of the +# openkal specification — so the question itself does not link, which is the +# mechanism rather than a defect. A bare-metal project is a different project, +# and the README says what it writes instead. diff --git a/examples/06-openkal-cross/src/main.cpp b/examples/06-openkal-cross/src/main.cpp new file mode 100644 index 00000000..12cdd153 --- /dev/null +++ b/examples/06-openkal-cross/src/main.cpp @@ -0,0 +1,163 @@ +// One source, three machines, and a program that asks each of them what it is. +// +// mcpp run this machine +// mcpp build --target x86_64-linux Linux, any host +// mcpp build --target aarch64-macos macOS, any host +// mcpp build --target x86_64-windows Windows, any host +// +// Nothing below is conditional on a platform. There is no preprocessor +// directive in this file, and no branch on a target name. What differs between +// the three builds is which packages the dependency graph resolved, and the +// only trace of that difference in the source is that the program ASKS about +// capabilities instead of assuming them. +// +// That distinction is the subject of the example. A portable program written +// against a conventional stack is a program that compiles under several sets of +// `#if`s; a program written against a named interface is one source that +// queries what it landed on. The queries below are chosen so that each one +// exercises a different layer of the target side, and so that a build which +// silently reached the host's own libraries instead of the resolved ones would +// answer differently. +// +// ⚠️ WHAT IS ASKED HERE AND WHAT IS NOT. +// +// A capability word says how an implementation behaves WITHIN an interface it +// provides. Whether it provides the interface at all is a different question, +// answered earlier and by something else: the dependency graph, and failing +// that the linker. So this file may ask a filesystem how it compares names, and +// may not ask a machine whether it has a filesystem — on one that does not, +// `kal::fs::properties()` is an undefined symbol, and that is clause 6.1 of the +// specification working rather than failing. A program for such a machine is a +// different program, and the README says what it writes instead. + +import std; + +import openkal.env; +import openkal.fs; +import openkal.task; +import openkal.time; +import openkal.types; + +namespace { + +// A row of the report. Collected into a container first rather than printed as +// it is produced, because the container is what requires the allocator, and the +// allocator is `openkal.memory` rather than the host's. +struct fact { + std::string subject; + std::string value; +}; + +// Records that its destructor ran. Unwinding is the one part of the C++ runtime +// that links successfully whether or not it works: a program whose unwinder is +// absent still builds, and fails only when something is thrown. Running a +// destructor during the unwind is what separates the two. +struct scope_marker { + bool* ran; + ~scope_marker() { *ran = true; } +}; + +struct unwound {}; + +bool destructor_ran_during_unwind() { + bool ran = false; + try { + scope_marker marker{&ran}; + throw unwound{}; + } catch (const unwound&) { + } + return ran; +} + +std::string yes_no(bool b) { return b ? "yes" : "no"; } + +// The first command-line argument, or a placeholder. `kal::env` reports the +// argument vector the platform actually delivered, which on Windows is derived +// from a single command-line string and on Linux from the stack the kernel +// prepared. The program does not need to know which. +std::string program_name() { + if (kal::env::arg_count() == 0) return "(none)"; + kal_uintptr len = 0; + const char* p = kal::env::arg(0, &len); + if (p == nullptr || len == 0) return "(empty)"; + return std::string(p, static_cast(len)); +} + +} // namespace + +int main() { + std::vector facts; + + // ── The platform interface ────────────────────────────────────────────── + // + // A preopened directory is the only filesystem root a program is given on a + // capability-oriented platform. The count is a property of an implementation + // that HAS a filesystem — how many roots it handed over — and not a way of + // discovering whether there is one. + facts.push_back({"preopened directories", + std::format("{}", kal::fs::preopen_count())}); + facts.push_back({"case-sensitive paths", + yes_no(kal::fs::has(kal::fs::case_sensitive))}); + facts.push_back({"symbolic links", + yes_no(kal::fs::has(kal::fs::links))}); + facts.push_back({"atomic rename", + yes_no(kal::fs::has(kal::fs::atomic_rename))}); + + // ── Time ──────────────────────────────────────────────────────────────── + // + // Two clocks with different guarantees. A wall clock may be absent, which + // is the ordinary state of a machine that has just been powered on and has + // no battery-backed counter; the monotonic clock is always present because + // the specification requires it. + facts.push_back({"wall clock", + yes_no(kal::time::has(kal::time::wall_available))}); + facts.push_back({"monotonic granularity (ns)", + std::format("{}", kal::time::granularity())}); + + const auto before = kal::time::monotonic(); + kal::time::sleep(1'000'000); // one millisecond + const auto after = kal::time::monotonic(); + facts.push_back({"monotonic advanced", yes_no(after > before)}); + + // ── Concurrency ───────────────────────────────────────────────────────── + // + // Reported rather than used. Every implementation reached here provides + // `openkal.task`; what varies is whether its scheduler preempts and whether + // anything runs in parallel. A program that spawns a thread without asking + // does not fail to compile on a machine with one core and a cooperative + // scheduler; it fails to return. + facts.push_back({"preemptive scheduling", + yes_no(kal::task::has(kal::task::preemptive))}); + facts.push_back({"parallel execution", + yes_no(kal::task::has(kal::task::parallel))}); + facts.push_back({"thread-local storage", + yes_no(kal::task::has(kal::task::thread_local_storage))}); + + // ── The C++ runtime ───────────────────────────────────────────────────── + facts.push_back({"unwinding runs destructors", + yes_no(destructor_ran_during_unwind())}); + + // ── The standard library ──────────────────────────────────────────────── + // + // A sort and a fold, present so that the report is not the only thing the + // allocator and the algorithm headers are asked to do. + std::vector sample{9, 3, 7, 1, 8, 2}; + std::ranges::sort(sample); + const int total = std::accumulate(sample.begin(), sample.end(), 0); + facts.push_back({"sorted sample", + std::format("{} (sum {})", sample, total)}); + + // ── Output ────────────────────────────────────────────────────────────── + // + // One column width for every row, computed rather than hard-coded, so that + // the output of the three builds can be compared line by line. + std::size_t width = 0; + for (const auto& f : facts) width = std::max(width, f.subject.size()); + + std::println("{}", program_name()); + std::println("{}", std::string(width + 22, '-')); + for (const auto& f : facts) + std::println("{:<{}} {}", f.subject, width, f.value); + + return 0; +} diff --git a/mcpp.toml b/mcpp.toml index ad5f2a34..295e1aa5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.8.24.2" +version = "2026.8.24.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 88097f4c..a3d18225 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -915,6 +915,19 @@ prepare_build(bool print_fingerprint, // learned by experiment — writing the same value a second time in // `[target.]` and observing that it works. std::string pinReplacedDefault; + // The C library the target triple asked for, taken before the triple is + // canonicalised. Empty when the project declined to name one. + std::string requestedCAbi; + // The target as the project spelled it, when that differs from the + // canonical identity. Report only; empty means they coincide. + std::string targetDisplayName; + // The target row's toolchain convention, held until the graph is known. + // Empty when the row names none or the project named its own. + std::string targetPinCandidate; + // Whether the resolved toolchain spec names the machine's own Visual + // Studio. Decided inside `resolve_target_toolchain`, read by + // `host_tc_for_build_program`, which is why it is declared out here. + bool tcSpecIsMsvc = false; auto root = overrides.project_root.empty() ? mcpp::project::find_manifest_root(std::filesystem::current_path()) @@ -1559,6 +1572,28 @@ prepare_build(bool print_fingerprint, servable.empty() ? "(nothing — `mcpp toolchain list`)" : servable, parsed->str()); } + // ⚠️ CAPTURED BEFORE CANONICALISATION, BECAUSE CANONICALISATION IS + // EXACTLY WHAT DESTROYS IT. + // + // `str()` renders the filled-in identity, so `x86_64-linux` becomes + // `x86_64-linux-gnu` here and every later `parse` of that string reports + // an env segment the project never wrote. The request has to be taken + // from the ONLY triple that still knows the difference: this one. + if (parsed && parsed->envExplicit) requestedCAbi = parsed->env; + // ⚠️ AND THE SPELLING THE PROJECT USED, FOR THE REPORT ONLY. + // + // The canonical form is the identity — the output directory, the cache + // key, the subject of a `cfg()` — and it must stay filled. The REPORT is + // a different thing: it says what was asked for and what resolved, and + // heading it `x86_64-linux-gnu` above a line reading `c-abi musl` states + // a contradiction the build does not actually contain. A project that + // declined to name a C library is shown as having declined. + if (parsed && !parsed->envExplicit && !parsed->env.empty()) { + auto asWritten = *parsed; + asWritten.env.clear(); + targetDisplayName = asWritten.str(); + } + // Canonical from here on: cfg evaluation, spec attachment and the // target/ output directory all see one spelling. if (parsed) overrides.target_triple = parsed->str(); @@ -1605,23 +1640,14 @@ prepare_build(bool print_fingerprint, // a project does not use. The narrower reading of this guard was // patched with an openkal-specific exception; stating the rule // correctly removes the need for one. - const bool pinWouldOverruleUser = tc_origin_is_user_explicit(tcOrigin); + // ⚠️ RECORDED, NOT APPLIED. The convention answers "which payload + // supplies this target's C library", and whether it is needed depends on + // whether the dependency graph supplies one instead. That is knowable + // only after resolution, so the decision waits for + // `resolve_target_toolchain` and only the candidate is kept here. if (known && !hasToolchainOverride && !known->pin.empty() - && !pinWouldOverruleUser) { - // ⚠️ AND WHEN IT REPLACES SOMETHING THE USER WROTE DOWN, SAY SO. - // - // `mcpp toolchain default llvm` prints the change back and then a - // cross build silently used a different compiler. The substitution - // is correct — the row names the payload that supplies this target's - // C library — but a status line reporting only the outcome left the - // reader to discover the rule by writing the same value a second - // time in `[target.]` and observing that it worked. - if (tcOrigin == TcOrigin::GlobalDefault && tcSpec.has_value() - && *tcSpec != known->pin) - pinReplacedDefault = *tcSpec; - tcSpec = std::string(known->pin); - if (!tc_origin_is_user_explicit(tcOrigin)) - tcOrigin = TcOrigin::TargetPin; + && !tc_origin_is_user_explicit(tcOrigin)) { + targetPinCandidate = std::string(known->pin); } if (known && known->defaultStatic && m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static"; @@ -1679,9 +1705,30 @@ prepare_build(bool print_fingerprint, // Studio. `Origin::Managed` is everything else, including a VERSIONED // msvc spec, and that is the point: what the manifest says is what gets // used, on every machine, instead of whatever this one happens to have. - std::optional parsedSpec; - auto tcOriginAxis = mcpp::toolchain::Origin::Managed; - if (tcSpec.has_value() && *tcSpec != "system") { + // ⚠️ RESOLVED HERE, RUN AFTER THE DEPENDENCY GRAPH — AND THE SPLIT IS THE + // WHOLE POINT. + // + // A target row's convention does not name a preferred compiler. It names + // the payload that supplies THAT TARGET'S C library. Whether the user's own + // toolchain can serve the target instead depends on whether something ELSE + // supplies the target side — and that is knowable only once the graph is + // resolved, which is after this point in the function. + // + // Deciding early was measured to be wrong in both directions. Applying the + // convention unconditionally replaced a toolchain the user had set with + // `mcpp toolchain default`, for a payload their project never used. NOT + // applying it turned a working zero-dependency cross build into a failing + // one, because clang alone carries no C runtime for `x86_64-windows-gnu` + // while the payload the row names does. + // + // ⚠️ The body does not MOVE; only its execution does. Everything between + // here and the call site was measured to read `tc` exactly once, and that + // one read wanted the target triple rather than the compiler. + std::optional tc; + auto resolve_target_toolchain = [&]() -> std::expected { + std::optional parsedSpec; + auto tcOriginAxis = mcpp::toolchain::Origin::Managed; + if (tcSpec.has_value() && *tcSpec != "system") { // A parse FAILURE is not the same as an unparseable spec being // absent: `gcc@system` now fails here by name (see // parse_toolchain_spec), and swallowing that would put the error back @@ -1691,11 +1738,14 @@ prepare_build(bool print_fingerprint, "[toolchain].{} = '{}': {}", kCurrentPlatform, *tcSpec, s.error())); parsedSpec = std::move(*s); tcOriginAxis = mcpp::toolchain::origin_of(*parsedSpec); - } - const bool tcSpecIsMsvc = + } + // ⚠️ ASSIGNED, NOT DECLARED. `host_tc_for_build_program` reads it and is + // defined outside this lambda, so the declaration lives in the enclosing + // scope; the value is still decided here, where the spec is parsed. + tcSpecIsMsvc = parsedSpec && tcOriginAxis == mcpp::toolchain::Origin::SystemMsvc; - if (tcSpecIsMsvc) { + if (tcSpecIsMsvc) { if (!mcpp::platform::is_windows) { return std::unexpected(std::format( "toolchain '{}' is only available on Windows hosts", *tcSpec)); @@ -1708,7 +1758,7 @@ prepare_build(bool print_fingerprint, mcpp::ui::info("Resolved", std::format( "msvc@system → msvc {} ({})", inst->display_version(), inst->clPath.string())); - } else if (parsedSpec) { + } else if (parsedSpec) { auto spec = parsedSpec; if (spec->version.empty()) { return std::unexpected(std::format( @@ -1807,9 +1857,9 @@ prepare_build(bool print_fingerprint, mcpp::fetcher::make_path_ctx(&**get_cfg(), *root)), chosenBy)); } - } else if (tcSpec.has_value() && *tcSpec == "system") { + } else if (tcSpec.has_value() && *tcSpec == "system") { // Explicit user opt-in to system PATH compiler — kept as escape hatch. - } else if (mcpp::platform::env::offline_mode() + } else if (mcpp::platform::env::offline_mode() || mcpp::platform::env::no_auto_install()) { // CI / offline / test opt-out: hard-error instead of silently // pulling ~800 MB of toolchain. Preserves the original M5.5 @@ -1858,7 +1908,7 @@ prepare_build(bool print_fingerprint, " {}", pins::kSuggestGccMusl, pins::kFirstRunLinuxOther, release)); } - } else { + } else { // First-run UX: no project-level [toolchain], no global default, // and the user just ran `mcpp build` (or similar). Auto-install // the platform's canonical default so the user gets a working @@ -1961,14 +2011,14 @@ prepare_build(bool print_fingerprint, // not the running build. tcSpec = defaultSpec; tcOrigin = TcOrigin::FirstRun; - } - - // Windows first run that got diverted to winlibs GCC: announce it and - // persist BOTH axes, so the next invocation is silent and - // `mcpp toolchain list` shows the same pair the build actually used. - // Persisting only the target would leave the toolchain axis implicit - // (derived from the vocabulary pin) and the two views would disagree. - if (windowsGnuFirstRun && tcSpec.has_value()) { + } + + // Windows first run that got diverted to winlibs GCC: announce it and + // persist BOTH axes, so the next invocation is silent and + // `mcpp toolchain list` shows the same pair the build actually used. + // Persisting only the target would leave the toolchain axis implicit + // (derived from the vocabulary pin) and the two views would disagree. + if (windowsGnuFirstRun && tcSpec.has_value()) { mcpp::ui::info("First run", std::format("no toolchain configured and no Visual Studio found — " "using {} for {} (MinGW-w64, self-contained)", @@ -1982,288 +2032,292 @@ prepare_build(bool print_fingerprint, std::format("set to {} → {}", *tcSpec, overrides.target_triple)); } tcOrigin = TcOrigin::FirstRun; - } - - auto tc = mcpp::toolchain::detect( - explicit_compiler, runtimePayload, runtimeBindingSnapshot.contractHash); - if (!tc) return std::unexpected(tc.error().message); - - // Something about the resolution the user has to be told, but which is - // not a failure. Today's only producer is the Windows SDK axis: a managed - // toolset binds the SDK it was installed with, so a `WindowsSdkDir` in - // the environment does not apply — and an override that is ignored - // SILENTLY is indistinguishable from one that was never set. - if (!tc->resolutionNote.empty()) - mcpp::ui::info("note", tc->resolutionNote); - - // ── A retargetable driver has to be TOLD what it is targeting ──────── - // - // `tc.targetTriple` comes from `-dumpmachine`, and for every cross target - // that worked before this it was right for a reason that does not - // generalise: those targets use a DISTINCT compiler binary - // (`x86_64-w64-mingw32-g++`, `aarch64-linux-musl-g++`), whose own - // -dumpmachine reports the cross triple. Clang is ONE binary that emits - // every target it was built with, so -dumpmachine always answers with the - // host — and nothing downstream ever learns otherwise. - // - // Measured before this line existed: - // - // $ mcpp build --target riscv64-none-elf - // Resolved llvm@22.1.8 → riscv64-none-elf → …/bin/clang++ - // Finished dev [unoptimized + debuginfo] in 0.47s - // $ ls target/ - // x86_64-linux-gnu/ ← an ELF for the host, reported as riscv64 - // - // That is E1: success reported, host artifact produced. The output - // directory, the fingerprint, the cache key and the flag layer all read - // `tc.targetTriple`, so correcting it here corrects all of them at once — - // which is the point of there being one field rather than five answers. - // - // ⚠️ THIS USED TO BE SCOPED TO FREESTANDING, WITH THIS REASON: - // - // The hosted cross targets already resolve a per-target binary, and - // overwriting their probed triple would replace a measured fact with - // an assumed one for no gain. - // - // ⭐⭐ That was true while every hosted cross was served by a payload. It - // stops being true when the TARGET SIDE comes from the dependency graph: - // the C library, the C++ runtime and the platform's own implementation are - // then packages built from source, and the compiler is an ordinary clang — - // whose `-dumpmachine` answers the host, exactly as the paragraph above - // describes for freestanding. - // - // ⚠️ Measured 2026-08-23, with an explicit `[target.aarch64-macos] - // toolchain = "llvm@…"`. The manifest's cfg evaluation used the REQUESTED - // target, so the C library's aarch64 headers were on the command line; the - // toolchain's own triple was still the host's, so code generation was - // x86_64. Two answers to one question, in one command: - // - // okm_float_assert.c: the C library and the compiler disagree about - // LDBL_DIG ('33 == 18') 33 = aarch64 binary128, 18 = x87 - // - // ⇒ The condition is now the property the first paragraph of this comment - // already names: a RETARGETABLE driver has to be told. gcc is not one — a - // gcc payload IS its target — so the mingw and musl-gcc crosses keep - // answering from `-dumpmachine`, which for them remains a measured fact. - if (!overrides.target_triple.empty()) { - if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); - want && (want->is_freestanding() - || tc->compiler == mcpp::toolchain::CompilerId::Clang)) - { - tc->targetTriple = want->str(); - - // And the flag that says it to the driver — for a HOSTED target - // only. Freestanding already emits its own `--target`, together - // with the ISA flags that must accompany it - // (freestanding/target.cppm); a second one here would be the same - // decision in two places. - if (!want->is_freestanding() - && tc->compiler == mcpp::toolchain::CompilerId::Clang) { - tc->crossTargetFlag = - "--target=" + want->llvm_triple( - mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget)); - } - } - if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); - want && want->is_freestanding()) - { - // `import std` is structurally hosted, and turning it off is the - // SAME fact as the line above, not a second policy: libc++'s - // std.cppm is one module over the whole library, including the - // parts that are threads, filesystem and iostreams. There is no - // subset of it to precompile. - // - // Left on, the failure is neither early nor legible — measured: - // - // error: std module precompile failed (rc=1): - // .../include/c++/v1/__config:13:10: fatal error: - // '__config_site' file not found - // - // which reads as a broken toolchain payload and says nothing about - // the target. The freestanding std subset a user actually wants is - // an ordinary package (`mcpplibs.std.freestanding`), so mcpp's job - // here is to stop pretending the hosted one exists and to say - // where the other one is. - tc->hasImportStd = false; - tc->stdModuleSource.clear(); - tc->stdCompatSource.clear(); - - // ── The target's C library, resolved like its compiler ───────── - // - // The row in kKnownTargets names it, exactly as it names the - // toolchain pin, and it is installed through the same channel a - // project's `[xlings] deps` use (see the materialization above). - // Resolved HERE because the config is already open; the flag - // builder only reads the result. - // - // Absent is not an error at this point: the install happens - // earlier in this function and may legitimately not have run yet - // on a first pass. What follows would then simply not add the - // paths, and the link fails naming the missing libc — which is the - // truthful message either way. - if (const std::string want_sysroot = - mcpp::toolchain::triple::effective_sysroot( - *want, sysroot_override(*m, *want)); - !want_sysroot.empty()) { - if (auto cfg3 = get_cfg(); cfg3) { - auto ref = mcpp::xlings::paths::parse_xpkg_ref(want_sysroot); - auto xl = mcpp::config::make_xlings_env(**cfg3); - if (auto dir = mcpp::xlings::paths::xpkg_payload(xl, ref)) { - if (auto spec = mcpp::freestanding::resolve(*want)) { - const auto inc = - *dir / "include" / std::string(spec->libdir); - const auto lib = - *dir / "lib" / std::string(spec->libdir); - std::error_code ec2; - tc->targetSysrootRoot = *dir; - tc->targetSysrootPkg = ref.name; - if (std::filesystem::is_directory(inc, ec2)) - tc->targetSysrootInclude = inc; - if (std::filesystem::is_directory(lib, ec2)) - tc->targetSysrootLib = lib; - } - } - } - } - } - } - - // The Windows runtime identity, flowing BACK into the contract. - // - // Everything else about the runtime is known before a toolchain is - // resolved, and deliberately so (see the RuntimeBinding block above). The - // Windows SDK is the exception: it is a property of the toolchain, and - // until it reached the contract hash the version axis simply did not - // exist one layer below the compiler — two SDKs produced one cache key. - // - // `ucrt@` is a COMPATIBILITY FLOOR, not a payload binding like - // `glibc@`: ucrtbase.dll is an OS component and mcpp ships no - // redistributable for it. See mcpp.platform.runtime_binding. - if (!tc->windowsSdkVersion.empty()) { - mcpp::platform::runtime::bind_windows_ucrt( - runtimeBindingSnapshot, tc->windowsSdkVersion); - tc->runtimeContractHash = runtimeBindingSnapshot.contractHash; - } - - // ── Targeting the MSVC ABI without a usable MSVC ───────────────────── - // - // One judgement, one place. This used to be two separate concerns and - // only one of them was implemented: `msvc@system` with no Windows SDK - // was caught here, while clang-targeting-MSVC on a machine with no - // Visual Studio at all — the default on every bare Windows box — fell - // straight through to clang's own "'vector' file not found", from which - // no user could infer that a working alternative was one flag away. - // Deriving the same judgement in two places is how the second case went - // unnoticed, so they are now one condition with two outcomes. - const bool targetsMsvcAbi = - tc->compiler == mcpp::toolchain::CompilerId::MSVC - || mcpp::toolchain::is_msvc_target(*tc); - if (targetsMsvcAbi && !msvc_usable_either_origin()) { - // Native cl.exe is ALWAYS a deliberate choice: mcpp never selects - // msvc@system on its own — it cannot install one — so the only way it - // reaches config.toml is a user typing `mcpp toolchain default msvc`. - // Without this, that user (who evidently wants MSVC and is probably - // just missing the SDK component) would be silently moved to MinGW - // instead of being told which component to install. - // - // The residual imprecision is deliberate and bounded: a *global* - // default of llvm@20.1.7 is indistinguishable from the one mcpp used - // to write itself, so an explicitly-typed one gets repaired too. The - // value is identical either way and the machine cannot build with it; - // a user who wants that failure can pin it in mcpp.toml, which is - // honoured exactly. - const bool userChoseMsvcItself = - tc->compiler == mcpp::toolchain::CompilerId::MSVC; - const bool mayRepair = - !tc_origin_is_user_explicit(tcOrigin) - && !userChoseMsvcItself - && !mcpp::platform::env::offline_mode() - && !mcpp::platform::env::no_auto_install() - && mcpp::platform::is_windows; - if (!mayRepair) { - return std::unexpected(msvc_unavailable_guidance(*tc)); - } - // mcpp chose this default itself and it cannot work on this machine. - // Revise it — including for users who already have `llvm@20.1.7` - // persisted by an older mcpp: the first-run branch never fires again - // for them, so this gate (which runs on EVERY build) is what repairs - // them without a single manual command. - namespace pins = mcpp::toolchain::triple::pins; - mcpp::ui::info("Toolchain", - std::format("{} targets the MSVC ABI but no Visual Studio " - "(MSVC STL + Windows SDK) was found — switching to {} → {}", - tcSpec.value_or("the configured default"), - pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget)); - - overrides.target_triple = std::string(pins::kFirstRunWinGnuTarget); - // The x86_64-windows-gnu row is defaultStatic; the target block that - // normally applies that already ran, so mirror just this one field. - if (m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static"; - - auto gnuSpec = mcpp::toolchain::parse_toolchain_spec( - std::string(pins::kFirstRunWinGnu)); - if (!gnuSpec) return std::unexpected(gnuSpec.error()); - if (auto t = mcpp::toolchain::triple::parse(overrides.target_triple)) - gnuSpec->target = *t; - auto gnuPkg = mcpp::toolchain::to_xim_package(*gnuSpec); - - auto cfgR = get_cfg(); - if (!cfgR) return std::unexpected(cfgR.error()); - mcpp::fetcher::Fetcher fetcherR(**cfgR); - mcpp::fetcher::InstallProgressHandler progressR; - auto payloadR = fetcherR.resolve_xpkg_path(gnuPkg.target(), - /*autoInstall=*/true, &progressR); - if (!payloadR) { - return std::unexpected(std::format( - "switching to the MinGW-w64 toolchain ({}) failed: {}\n" - " install it manually with:\n" - " mcpp toolchain install {} --target {}", - pins::kFirstRunWinGnu, payloadR.error().message, - pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget)); - } - explicit_compiler = - mcpp::toolchain::toolchain_frontend(payloadR->binDir, gnuPkg); - if (!std::filesystem::exists(explicit_compiler)) { - return std::unexpected(std::format( - "MinGW-w64 payload {} has no known C++ frontend in {}", - gnuPkg.target(), payloadR->binDir.string())); - } - if (auto fixed = mcpp::toolchain::ensure_post_install_fixup( - **cfgR, payloadR->root, gnuPkg, - runtimeBindingSnapshot.runtimeId, runtimeLibDir); !fixed) - return std::unexpected(std::format( - "MinGW toolchain post-install fixup: {}", fixed.error())); - else report_fixup(*fixed, payloadR->root); - - // Persist both axes so the repair happens once, not on every build. - if (mcpp::config::write_default_toolchain(**cfgR, pins::kFirstRunWinGnu)) - (*cfgR)->defaultToolchain = std::string(pins::kFirstRunWinGnu); - if (mcpp::config::write_default_target(**cfgR, overrides.target_triple)) - (*cfgR)->defaultTarget = overrides.target_triple; - - tcSpec = std::string(pins::kFirstRunWinGnu); - tcOrigin = TcOrigin::FirstRun; - tc = mcpp::toolchain::detect( - explicit_compiler, runtimePayload, - runtimeBindingSnapshot.contractHash); - if (!tc) return std::unexpected(tc.error().message); - } - - // For musl-gcc the toolchain is fully self-contained - // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). - // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. - bool isMuslTc = mcpp::toolchain::is_musl_target(*tc); - - // A musl toolchain only really makes sense with static linkage — - // dynamic-musl binaries depend on a system /lib/ld-musl-x86_64.so.1 - // that most distros don't ship. Default linkage to "static" when - // the resolved toolchain is musl, unless the user has already opted - // out via `--static` or [target.].linkage. (There is no - // [build].linkage — the parser only reads it under a target section.) - if (isMuslTc && m->buildConfig.linkage.empty()) { - m->buildConfig.linkage = "static"; - } + } + + auto detected = mcpp::toolchain::detect( + explicit_compiler, runtimePayload, runtimeBindingSnapshot.contractHash); + if (!detected) return std::unexpected(detected.error().message); + tc = std::move(*detected); + + // Something about the resolution the user has to be told, but which is + // not a failure. Today's only producer is the Windows SDK axis: a managed + // toolset binds the SDK it was installed with, so a `WindowsSdkDir` in + // the environment does not apply — and an override that is ignored + // SILENTLY is indistinguishable from one that was never set. + if (!tc->resolutionNote.empty()) + mcpp::ui::info("note", tc->resolutionNote); + + // ── A retargetable driver has to be TOLD what it is targeting ──────── + // + // `tc.targetTriple` comes from `-dumpmachine`, and for every cross target + // that worked before this it was right for a reason that does not + // generalise: those targets use a DISTINCT compiler binary + // (`x86_64-w64-mingw32-g++`, `aarch64-linux-musl-g++`), whose own + // -dumpmachine reports the cross triple. Clang is ONE binary that emits + // every target it was built with, so -dumpmachine always answers with the + // host — and nothing downstream ever learns otherwise. + // + // Measured before this line existed: + // + // $ mcpp build --target riscv64-none-elf + // Resolved llvm@22.1.8 → riscv64-none-elf → …/bin/clang++ + // Finished dev [unoptimized + debuginfo] in 0.47s + // $ ls target/ + // x86_64-linux-gnu/ ← an ELF for the host, reported as riscv64 + // + // That is E1: success reported, host artifact produced. The output + // directory, the fingerprint, the cache key and the flag layer all read + // `tc.targetTriple`, so correcting it here corrects all of them at once — + // which is the point of there being one field rather than five answers. + // + // ⚠️ THIS USED TO BE SCOPED TO FREESTANDING, WITH THIS REASON: + // + // The hosted cross targets already resolve a per-target binary, and + // overwriting their probed triple would replace a measured fact with + // an assumed one for no gain. + // + // ⭐⭐ That was true while every hosted cross was served by a payload. It + // stops being true when the TARGET SIDE comes from the dependency graph: + // the C library, the C++ runtime and the platform's own implementation are + // then packages built from source, and the compiler is an ordinary clang — + // whose `-dumpmachine` answers the host, exactly as the paragraph above + // describes for freestanding. + // + // ⚠️ Measured 2026-08-23, with an explicit `[target.aarch64-macos] + // toolchain = "llvm@…"`. The manifest's cfg evaluation used the REQUESTED + // target, so the C library's aarch64 headers were on the command line; the + // toolchain's own triple was still the host's, so code generation was + // x86_64. Two answers to one question, in one command: + // + // okm_float_assert.c: the C library and the compiler disagree about + // LDBL_DIG ('33 == 18') 33 = aarch64 binary128, 18 = x87 + // + // ⇒ The condition is now the property the first paragraph of this comment + // already names: a RETARGETABLE driver has to be told. gcc is not one — a + // gcc payload IS its target — so the mingw and musl-gcc crosses keep + // answering from `-dumpmachine`, which for them remains a measured fact. + if (!overrides.target_triple.empty()) { + if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); + want && (want->is_freestanding() + || tc->compiler == mcpp::toolchain::CompilerId::Clang)) + { + tc->targetTriple = want->str(); + + // And the flag that says it to the driver — for a HOSTED target + // only. Freestanding already emits its own `--target`, together + // with the ISA flags that must accompany it + // (freestanding/target.cppm); a second one here would be the same + // decision in two places. + if (!want->is_freestanding() + && tc->compiler == mcpp::toolchain::CompilerId::Clang) { + tc->crossTargetFlag = + "--target=" + want->llvm_triple( + mcpp::platform::macos::deployment_target( + m->buildConfig.macosDeploymentTarget)); + } + } + if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); + want && want->is_freestanding()) + { + // `import std` is structurally hosted, and turning it off is the + // SAME fact as the line above, not a second policy: libc++'s + // std.cppm is one module over the whole library, including the + // parts that are threads, filesystem and iostreams. There is no + // subset of it to precompile. + // + // Left on, the failure is neither early nor legible — measured: + // + // error: std module precompile failed (rc=1): + // .../include/c++/v1/__config:13:10: fatal error: + // '__config_site' file not found + // + // which reads as a broken toolchain payload and says nothing about + // the target. The freestanding std subset a user actually wants is + // an ordinary package (`mcpplibs.std.freestanding`), so mcpp's job + // here is to stop pretending the hosted one exists and to say + // where the other one is. + tc->hasImportStd = false; + tc->stdModuleSource.clear(); + tc->stdCompatSource.clear(); + + // ── The target's C library, resolved like its compiler ───────── + // + // The row in kKnownTargets names it, exactly as it names the + // toolchain pin, and it is installed through the same channel a + // project's `[xlings] deps` use (see the materialization above). + // Resolved HERE because the config is already open; the flag + // builder only reads the result. + // + // Absent is not an error at this point: the install happens + // earlier in this function and may legitimately not have run yet + // on a first pass. What follows would then simply not add the + // paths, and the link fails naming the missing libc — which is the + // truthful message either way. + if (const std::string want_sysroot = + mcpp::toolchain::triple::effective_sysroot( + *want, sysroot_override(*m, *want)); + !want_sysroot.empty()) { + if (auto cfg3 = get_cfg(); cfg3) { + auto ref = mcpp::xlings::paths::parse_xpkg_ref(want_sysroot); + auto xl = mcpp::config::make_xlings_env(**cfg3); + if (auto dir = mcpp::xlings::paths::xpkg_payload(xl, ref)) { + if (auto spec = mcpp::freestanding::resolve(*want)) { + const auto inc = + *dir / "include" / std::string(spec->libdir); + const auto lib = + *dir / "lib" / std::string(spec->libdir); + std::error_code ec2; + tc->targetSysrootRoot = *dir; + tc->targetSysrootPkg = ref.name; + if (std::filesystem::is_directory(inc, ec2)) + tc->targetSysrootInclude = inc; + if (std::filesystem::is_directory(lib, ec2)) + tc->targetSysrootLib = lib; + } + } + } + } + } + } + + // The Windows runtime identity, flowing BACK into the contract. + // + // Everything else about the runtime is known before a toolchain is + // resolved, and deliberately so (see the RuntimeBinding block above). The + // Windows SDK is the exception: it is a property of the toolchain, and + // until it reached the contract hash the version axis simply did not + // exist one layer below the compiler — two SDKs produced one cache key. + // + // `ucrt@` is a COMPATIBILITY FLOOR, not a payload binding like + // `glibc@`: ucrtbase.dll is an OS component and mcpp ships no + // redistributable for it. See mcpp.platform.runtime_binding. + if (!tc->windowsSdkVersion.empty()) { + mcpp::platform::runtime::bind_windows_ucrt( + runtimeBindingSnapshot, tc->windowsSdkVersion); + tc->runtimeContractHash = runtimeBindingSnapshot.contractHash; + } + + // ── Targeting the MSVC ABI without a usable MSVC ───────────────────── + // + // One judgement, one place. This used to be two separate concerns and + // only one of them was implemented: `msvc@system` with no Windows SDK + // was caught here, while clang-targeting-MSVC on a machine with no + // Visual Studio at all — the default on every bare Windows box — fell + // straight through to clang's own "'vector' file not found", from which + // no user could infer that a working alternative was one flag away. + // Deriving the same judgement in two places is how the second case went + // unnoticed, so they are now one condition with two outcomes. + const bool targetsMsvcAbi = + tc->compiler == mcpp::toolchain::CompilerId::MSVC + || mcpp::toolchain::is_msvc_target(*tc); + if (targetsMsvcAbi && !msvc_usable_either_origin()) { + // Native cl.exe is ALWAYS a deliberate choice: mcpp never selects + // msvc@system on its own — it cannot install one — so the only way it + // reaches config.toml is a user typing `mcpp toolchain default msvc`. + // Without this, that user (who evidently wants MSVC and is probably + // just missing the SDK component) would be silently moved to MinGW + // instead of being told which component to install. + // + // The residual imprecision is deliberate and bounded: a *global* + // default of llvm@20.1.7 is indistinguishable from the one mcpp used + // to write itself, so an explicitly-typed one gets repaired too. The + // value is identical either way and the machine cannot build with it; + // a user who wants that failure can pin it in mcpp.toml, which is + // honoured exactly. + const bool userChoseMsvcItself = + tc->compiler == mcpp::toolchain::CompilerId::MSVC; + const bool mayRepair = + !tc_origin_is_user_explicit(tcOrigin) + && !userChoseMsvcItself + && !mcpp::platform::env::offline_mode() + && !mcpp::platform::env::no_auto_install() + && mcpp::platform::is_windows; + if (!mayRepair) { + return std::unexpected(msvc_unavailable_guidance(*tc)); + } + // mcpp chose this default itself and it cannot work on this machine. + // Revise it — including for users who already have `llvm@20.1.7` + // persisted by an older mcpp: the first-run branch never fires again + // for them, so this gate (which runs on EVERY build) is what repairs + // them without a single manual command. + namespace pins = mcpp::toolchain::triple::pins; + mcpp::ui::info("Toolchain", + std::format("{} targets the MSVC ABI but no Visual Studio " + "(MSVC STL + Windows SDK) was found — switching to {} → {}", + tcSpec.value_or("the configured default"), + pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget)); + + overrides.target_triple = std::string(pins::kFirstRunWinGnuTarget); + // The x86_64-windows-gnu row is defaultStatic; the target block that + // normally applies that already ran, so mirror just this one field. + if (m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static"; + + auto gnuSpec = mcpp::toolchain::parse_toolchain_spec( + std::string(pins::kFirstRunWinGnu)); + if (!gnuSpec) return std::unexpected(gnuSpec.error()); + if (auto t = mcpp::toolchain::triple::parse(overrides.target_triple)) + gnuSpec->target = *t; + auto gnuPkg = mcpp::toolchain::to_xim_package(*gnuSpec); + + auto cfgR = get_cfg(); + if (!cfgR) return std::unexpected(cfgR.error()); + mcpp::fetcher::Fetcher fetcherR(**cfgR); + mcpp::fetcher::InstallProgressHandler progressR; + auto payloadR = fetcherR.resolve_xpkg_path(gnuPkg.target(), + /*autoInstall=*/true, &progressR); + if (!payloadR) { + return std::unexpected(std::format( + "switching to the MinGW-w64 toolchain ({}) failed: {}\n" + " install it manually with:\n" + " mcpp toolchain install {} --target {}", + pins::kFirstRunWinGnu, payloadR.error().message, + pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget)); + } + explicit_compiler = + mcpp::toolchain::toolchain_frontend(payloadR->binDir, gnuPkg); + if (!std::filesystem::exists(explicit_compiler)) { + return std::unexpected(std::format( + "MinGW-w64 payload {} has no known C++ frontend in {}", + gnuPkg.target(), payloadR->binDir.string())); + } + if (auto fixed = mcpp::toolchain::ensure_post_install_fixup( + **cfgR, payloadR->root, gnuPkg, + runtimeBindingSnapshot.runtimeId, runtimeLibDir); !fixed) + return std::unexpected(std::format( + "MinGW toolchain post-install fixup: {}", fixed.error())); + else report_fixup(*fixed, payloadR->root); + + // Persist both axes so the repair happens once, not on every build. + if (mcpp::config::write_default_toolchain(**cfgR, pins::kFirstRunWinGnu)) + (*cfgR)->defaultToolchain = std::string(pins::kFirstRunWinGnu); + if (mcpp::config::write_default_target(**cfgR, overrides.target_triple)) + (*cfgR)->defaultTarget = overrides.target_triple; + + tcSpec = std::string(pins::kFirstRunWinGnu); + tcOrigin = TcOrigin::FirstRun; + auto redetected = mcpp::toolchain::detect( + explicit_compiler, runtimePayload, + runtimeBindingSnapshot.contractHash); + if (!redetected) return std::unexpected(redetected.error().message); + tc = std::move(*redetected); + } + + // For musl-gcc the toolchain is fully self-contained + // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). + // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. + bool isMuslTc = mcpp::toolchain::is_musl_target(*tc); + + // A musl toolchain only really makes sense with static linkage — + // dynamic-musl binaries depend on a system /lib/ld-musl-x86_64.so.1 + // that most distros don't ship. Default linkage to "static" when + // the resolved toolchain is musl, unless the user has already opted + // out via `--static` or [target.].linkage. (There is no + // [build].linkage — the parser only reads it under a target section.) + if (isMuslTc && m->buildConfig.linkage.empty()) { + m->buildConfig.linkage = "static"; + } + return {}; + }; // Sysroot comes from the toolchain payload itself (GCC -print-sysroot, // Clang clang++.cfg). mcpp does not override it — the payload is @@ -2421,9 +2475,21 @@ prepare_build(bool print_fingerprint, // wrong. What it must NOT do is depend on the project having an `[xlings]` // section: a bare-metal project written to the template has none, and the // whole point is that it never mentions a libc. + // ⚠️ FROM THE REQUESTED TRIPLE, NOT FROM THE TOOLCHAIN — AND THE TWO WERE + // THE SAME VALUE ALL ALONG. + // + // This read of `tc->targetTriple` was the ONLY thing tying the compiler's + // resolution to a point before dependency resolution, and it never wanted + // the compiler: `tc->targetTriple` is corrected to the requested triple a + // few lines after the toolchain is detected, so the value here is the one + // `--target` named. Taking it from the request instead lets the toolchain + // be resolved where the information it needs actually exists. std::string targetSysroot; - if (tc) { - if (auto tt = mcpp::toolchain::triple::parse(tc->targetTriple)) + { + auto tt = overrides.target_triple.empty() + ? std::optional{mcpp::toolchain::triple::host_triple()} + : mcpp::toolchain::triple::parse(overrides.target_triple); + if (tt) targetSysroot = mcpp::toolchain::triple::effective_sysroot( *tt, sysroot_override(*m, *tt)); } @@ -4682,6 +4748,45 @@ prepare_build(bool print_fingerprint, computeUsageRequirements(); + // ─── The toolchain, resolved now that the graph exists ────────────────── + // + // ⚠️ THE TARGET AND THE COMPILER ARE NOT BOUND TOGETHER, AND THE ROW'S + // CONVENTION IS A FALLBACK RATHER THAN A RULE. + // + // `x86_64-linux-musl → gcc@16.1.0` does not say "prefer gcc". It says "the + // musl-gcc payload is what supplies this target's C library". A project + // whose C library comes from its dependency graph does not use that payload, + // and for it the convention is not a default but a substitution — measured, + // it replaced a toolchain the user had set with `mcpp toolchain default` and + // said nothing. + // + // The discriminator is whether anything in the graph supplies the system, + // which is what these few lines ask. It is the same question + // `mcpp.targetside` answers in full further down; asked here it needs only + // the answer's shape, so it reads the manifests rather than resolving them. + { + bool graphSuppliesSystem = false; + for (auto const& pkg : packages) { + for (auto const& entry : pkg.manifest.provides) { + auto cap = mcpp::targetside::parse_capability(entry); + if (!cap || !*cap) continue; + if ((*cap)->layer == mcpp::targetside::CapLayer::KernelAbi + || (*cap)->layer == mcpp::targetside::CapLayer::CAbi) { + graphSuppliesSystem = true; + } + } + } + if (!targetPinCandidate.empty() && !graphSuppliesSystem) { + if (tcOrigin == TcOrigin::GlobalDefault && tcSpec.has_value() + && *tcSpec != targetPinCandidate) + pinReplacedDefault = *tcSpec; + tcSpec = targetPinCandidate; + tcOrigin = TcOrigin::TargetPin; + } + if (auto r = resolve_target_toolchain(); !r) + return std::unexpected(r.error()); + } + // ─── Feature activation (Cargo-style, additive) ──────────────────── // activated(pkg) = pkg.[features].default ∪ features requested for it // (root: --features; deps: the root dep spec's `features = [...]`). @@ -5703,6 +5808,20 @@ prepare_build(bool print_fingerprint, in.targetOs = tt->os; in.targetEnv = tt->env; in.freestandingTarget = tt->is_freestanding(); + // ⚠️ NOT `tt->envExplicit`. By this line the triple has been + // canonicalised, and the canonical form of `x86_64-linux` is + // `x86_64-linux-gnu` — re-parsing it reports a segment the + // project never wrote. The request was captured upstream, where + // the distinction still existed. + in.requestedCAbi = requestedCAbi; + if (!requestedCAbi.empty()) { + auto bare = *tt; bare.env.clear(); + in.requestFreeTarget = bare.str(); + } + // Only on Linux does the segment name a C library. On Windows + // it names the object ABI and on bare metal the object format, + // and neither is the axis the graph's C library sits on. + in.envNamesCAbi = tt->os == "linux"; // `sysroot = ""` and "no sysroot key" are different answers and // must not be collapsed: the first says this project wants no @@ -5736,6 +5855,13 @@ prepare_build(bool print_fingerprint, // names a file the reader has never opened and no decision mcpp made. if (auto why = tsd::check_requirements(resolvedTargetSide, requirements)) return std::unexpected(*why); + // ⚠️ A WARNING, NOT A REFUSAL. The graph decides the C library either + // way, so the segment is ignored rather than violated and the artifact + // is the same with or without it. Refusing was tried and broke every + // project spelling the host target `x86_64-linux-gnu` — which is what + // `mcpp toolchain list` prints, and therefore what people write. + if (auto why = tsd::check_request(resolvedTargetSide)) + mcpp::diag::warning("target", *why); // The refusal held since toolchain resolution, released now that the // other half of its question has an answer. A payload on this machine @@ -5810,9 +5936,11 @@ prepare_build(bool print_fingerprint, // prints them all; a diagnostic always does. mcpp::ui::info("Target", tsd::format_report( resolvedTargetSide, - resolvedTargetCanonical.empty() - ? (tc ? tc->targetTriple : std::string{}) - : resolvedTargetCanonical, + !targetDisplayName.empty() + ? targetDisplayName + : (resolvedTargetCanonical.empty() + ? (tc ? tc->targetTriple : std::string{}) + : resolvedTargetCanonical), mcpp::log::is_verbose())); } diff --git a/src/targetside/model.cppm b/src/targetside/model.cppm index f16a751c..4820d1c5 100644 --- a/src/targetside/model.cppm +++ b/src/targetside/model.cppm @@ -138,6 +138,18 @@ struct TargetSide { Layer cAbi; Layer cxx; + // What the triple asked the C library to be, empty when it did not ask. + // Kept beside the resolved value rather than replacing it: the report + // states the outcome, and this exists so a mismatch can be named. + std::string requestedCAbi; + // The same target with that segment removed — the spelling to suggest when + // the request turns out to describe nothing. Built by the caller, which is + // the only place that still holds mcpp's own triple. + std::string requestFreeTarget; + // Whether the env segment names a C library on this platform. See the + // member of the same name on `Inputs`. + bool envNamesCAbi = false; + // The single question the five former derivation sites actually asked. // // It is about the SYSTEM, not about the C++ runtime. A C program over @@ -303,6 +315,29 @@ struct Inputs { std::string compilerFamily; // "llvm" / "gcc" / "msvc" std::string compilerVersion; + // ⚠️ THE C LIBRARY THE TRIPLE ASKED FOR, WHICH IS NOT THE SAME QUESTION AS + // WHICH ONE RESOLVED. + // + // `x86_64-linux-musl` states a request; `x86_64-linux` declines to. The + // parser fills the second one in as `gnu` so that the identity stays + // canonical, so `targetEnv` alone cannot tell the two apart — see + // `Triple::envExplicit`. Empty here means the project said nothing, and a + // build that says nothing cannot be contradicted. + std::string requestedCAbi; + // The same target spelled without that segment, for the suggestion. + std::string requestFreeTarget; + // ⚠️ WHETHER THE ENV SEGMENT NAMES A C LIBRARY ON THIS PLATFORM, WHICH IS + // NOT TRUE EVERYWHERE AND WAS ASSUMED TO BE. + // + // The segment carries a different axis depending on the OS. On Linux it + // names the C library — `gnu` is glibc, `musl` is musl — which is the case + // the request check was written for. On Windows it names the OBJECT ABI: + // `gnu` is PE with the GNU ABI and `msvc` is PE with Microsoft's, and both + // are compatible with more than one C library. Reporting a Windows build as + // "asking for the `gnu` C ABI" describes an axis the name never addressed, + // and the correction it suggested named a target that does not exist. + bool envNamesCAbi = false; + std::optional compilerRuntime; std::optional kernelAbi; std::optional cAbi; @@ -355,7 +390,10 @@ inline std::string xpkg_interface(std::string_view ref) { // ── The resolution ─────────────────────────────────────────────────────────── inline TargetSide resolve(const Inputs& in) { TargetSide ts; - ts.llvmTriple = in.llvmTriple; + ts.llvmTriple = in.llvmTriple; + ts.requestedCAbi = in.requestedCAbi; + ts.requestFreeTarget = in.requestFreeTarget; + ts.envNamesCAbi = in.envNamesCAbi; // compiler — always a payload, never a package. if (!in.compilerFamily.empty()) @@ -532,6 +570,50 @@ check_requirements(const TargetSide& ts, std::span reqs) { return std::nullopt; } +// ── The triple is a request; the target side is the fact ──────────────────── +// +// ⚠️ REPORTED RATHER THAN REFUSED, AND THE SEVERITY WAS DECIDED BY A +// MEASUREMENT RATHER THAN BY THE PRINCIPLE. +// +// The first version refused. It is the semantically clean answer — the name +// says one C library, the artifact contains another, and only one of the two +// can be true. It also broke every project and every CI configuration that +// spells the host target `x86_64-linux-gnu`, which is what `mcpp toolchain +// list` prints and therefore what people write. mcpp's own openkal matrix was +// the first casualty. +// +// What decides the severity is that the request changes NOTHING. The graph +// supplies the C library either way; the segment is ignored, not violated. A +// build that would be identical without the segment is not a build to refuse — +// it is a build whose name misdescribes it, and saying so is the whole +// remedy. +// +// ⚠️ The remedy has to be actionable, which is why `x86_64-linux` had to work +// first. Telling someone their target name is wrong is only useful once there +// is a right one to give them. +inline std::optional check_request(const TargetSide& ts) { + if (!ts.envNamesCAbi) return std::nullopt; + if (ts.requestedCAbi.empty()) return std::nullopt; + if (ts.cAbi.absent()) return std::nullopt; + if (ts.cAbi.interfaceName == ts.requestedCAbi) return std::nullopt; + // A prebuilt or payload C library IS what the request selected — the + // request is how it was selected. Only a supplier chosen by something else + // can disagree with it. + if (!ts.cAbi.fromGraph()) return std::nullopt; + + return std::format( + "the target name asks for the `{}` C ABI and the dependency graph " + "supplies `{}`.\n" + " The graph decides, so the build below uses `{}` — the name is " + "what is inaccurate,\n" + " not the artifact. Drop the segment to say what is actually " + "meant:\n" + " --target {}", + ts.requestedCAbi, ts.cAbi.interfaceName, ts.cAbi.interfaceName, + ts.requestFreeTarget.empty() ? std::string("-") + : ts.requestFreeTarget); +} + // ── Rule one: one supplier per layer ───────────────────────────────────────── // // A C library, a kernel interface and a C++ runtime are MUTUALLY EXCLUSIVE diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm index 54a8def3..5b09a0b1 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -34,6 +34,32 @@ struct Triple { std::string os; // "linux" | "macos" | "windows" std::string env; // "gnu" | "musl" | "msvc" | "" (always empty on macos) + // ⚠️ WHETHER THE ENV SEGMENT WAS WRITTEN, AS OPPOSED TO SUPPLIED BY THIS + // PARSER — AND THE TRIPLE HAS TO CARRY BOTH BECAUSE IT SERVES TWO ROLES. + // + // A triple is an IDENTITY — the output directory's name, part of a cache + // key, the subject of a `cfg()` predicate — and identities must be total + // and canonical. It is also a REQUEST, and a request has to be able to say + // nothing. `parse` makes the identity total by filling `x86_64-linux` in as + // `x86_64-linux-gnu`, and until this flag existed that filling ALSO + // destroyed the request: the two states were indistinguishable downstream. + // + // Measured: a project whose graph supplies musl, built with + // `--target x86_64-linux`, reported + // + // Target x86_64-linux-gnu → x86_64-unknown-linux-gnu + // c-abi musl (openkal-musl@0.3.3, graph) + // + // — a name that contradicts the fact printed two lines under it. The user + // had declined to name a C library; the parser named one for them. + // + // The narrow shape is deliberate. Removing the fill would make `env` empty + // for a hosted target at 22 read sites, ten of which are in this file, and + // every one would need a new answer for a state that never existed before. + // A flag beside the value leaves the identity exactly as it was and gives + // the request somewhere to live. + bool envExplicit = false; + bool empty() const { return arch.empty() && os.empty(); } // Canonical rendering: "arch-os[-env]"; "" for an empty (= host) triple. @@ -126,7 +152,17 @@ struct Triple { return std::nullopt; } - bool operator==(const Triple&) const = default; + // ⚠️ IDENTITY IS THE THREE SEGMENTS, AND `envExplicit` IS DELIBERATELY NOT + // AMONG THEM — WHICH IS WHY THIS IS NOT `= default`. + // + // The flag records where the env segment came from, not what the target is. + // A defaulted comparison would make `x86_64-linux-gnu` written by a user + // unequal to the same triple derived by `host_triple`, and the first thing + // that breaks is the `host` tag in `mcpp toolchain list`, which compares + // exactly those two. + bool operator==(const Triple& o) const { + return arch == o.arch && os == o.os && env == o.env; + } }; // Lenient parse of any recognizable triple spelling into canonical fields. @@ -289,6 +325,9 @@ inline Triple host_triple() { Triple t; t.arch = std::string(mcpp::platform::host_arch); t.os = std::string(mcpp::platform::name); + // Derived from the machine rather than written by anyone, so it states no + // request: a host build must not be refused for "contradicting" a C library + // its own triple never asked for. if (t.os == "linux") t.env = "gnu"; else if (t.os == "windows") t.env = "msvc"; return t; @@ -486,23 +525,23 @@ std::optional parse(std::string_view s) { || starts_with(k, "macos")) { t.os = "macos"; sawOs = true; t.env.clear(); continue; } // "mingw32" is the GNU os segment for ALL MinGW targets (64-bit // included — historical residue); it means windows + gnu env. - if (starts_with(k, "mingw")) { t.os = "windows"; sawOs = true; t.env = "gnu"; continue; } + if (starts_with(k, "mingw")) { t.os = "windows"; sawOs = true; t.env = "gnu"; t.envExplicit = true; continue; } // Bare-metal object-format / ABI segments. Only meaningful with // os=none: `riscv64-none-elf`, `arm-none-eabi`, `arm-none-eabihf`. // Gated on the OS so a hosted triple cannot pick them up by accident. if (t.os == "none") { - if (k == "elf") { t.env = "elf"; continue; } - if (k == "eabihf") { t.env = "eabihf"; continue; } - if (k == "eabi") { t.env = "eabi"; continue; } + if (k == "elf") { t.env = "elf"; t.envExplicit = true; continue; } + if (k == "eabihf") { t.env = "eabihf"; t.envExplicit = true; continue; } + if (k == "eabi") { t.env = "eabi"; t.envExplicit = true; continue; } } if (t.os != "macos") { - if (k == "musl" || starts_with(k, "musleabi")) { t.env = "musl"; continue; } - if (k == "gnu" || starts_with(k, "gnueabi")) { t.env = "gnu"; continue; } + if (k == "musl" || starts_with(k, "musleabi")) { t.env = "musl"; t.envExplicit = true; continue; } + if (k == "gnu" || starts_with(k, "gnueabi")) { t.env = "gnu"; t.envExplicit = true; continue; } // starts_with: clang effective triples can carry a version suffix // on the env segment ("…-windows-msvc19.44.35211"). - if (starts_with(k, "msvc")) { t.env = "msvc"; continue; } + if (starts_with(k, "msvc")) { t.env = "msvc"; t.envExplicit = true; continue; } } // Unrecognized segment (androideabi, wasi, …): not in mcpp's target // language — treat as unparseable rather than guessing. @@ -510,8 +549,14 @@ std::optional parse(std::string_view s) { } if (!sawOs) return std::nullopt; - if (t.os == "macos") t.env.clear(); // macos carries no env segment - if (t.os == "linux" && t.env.empty()) t.env = "gnu"; // "x86_64-linux" alias + // macOS carries no env segment at all, so nothing was declined there. + if (t.os == "macos") { t.env.clear(); t.envExplicit = false; } + // ⚠️ THE FILL STAYS, AND THE FACT THAT IT WAS A FILL IS NOW RECORDED. + // `x86_64-linux` is the canonical identity `x86_64-linux-gnu` — every + // directory name and cache key downstream depends on that — but it is NOT + // the request `x86_64-linux-gnu`, which names a C library. See + // `Triple::envExplicit`. + if (t.os == "linux" && t.env.empty()) t.env = "gnu"; return t; } diff --git a/src/version.cppm b/src/version.cppm index f64c92d1..277558ea 100644 --- a/src/version.cppm +++ b/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.8.24.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.8.24.3"; } // namespace mcpp diff --git a/tests/e2e/282_target_is_a_request.sh b/tests/e2e/282_target_is_a_request.sh new file mode 100755 index 00000000..ff9d3214 --- /dev/null +++ b/tests/e2e/282_target_is_a_request.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# requires: gcc +# The target triple states a request; the target side states the fact. +# +# WHY THIS FILE EXISTS. +# +# Two symptoms, one cause. A triple serves as an IDENTITY — the output +# directory, a cache key, the subject of a `cfg()` — and identities must be +# total, so `parse` fills `x86_64-linux` in as `x86_64-linux-gnu`. It also +# serves as a REQUEST, and a request must be able to say nothing. The filling +# destroyed the second, and the target row's convention was applied before the +# graph that decides whether it is needed even exists. +# +# Measured before this: +# +# Target x86_64-linux-gnu → x86_64-unknown-linux-gnu +# c-abi musl (openkal-musl@0.3.3, graph) +# +# — the name contradicts the line under it, and the build succeeded anyway. +set -e + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +cd "$work" + +mkdir -p libc/src app/src +cat > libc/mcpp.toml <<'TOML' +[package] +namespace = "probe" +name = "tiny-musl" +version = "0.1.0" +provides = ["mcpp:c-abi=musl"] + +[build] +sources = [] +TOML +cat > app/mcpp.toml <<'TOML' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +tiny-musl = { path = "../libc" } +TOML +printf 'int main(){ return 0; }\n' > app/src/main.cpp +cd app + +# ── 1. Declining to name a C library is not naming `gnu` ──────────────────── +# +# ⚠️ `|| true`, and that is the subject rather than a concession: the stand-in +# package declares the layer without supplying one, so the LINK afterwards has +# no C library to find. What this file asserts — the report and the refusal — +# is complete before a single object is compiled. +out="$("$MCPP" build --target x86_64-linux 2>&1 || true)" +grep -q "requests the" <<< "$out" && { + echo "declining to name a C library must not be a contradiction:" + echo "$out"; exit 1; } +grep -qE "Target x86_64-linux( |$)" <<< "$out" || { + echo "the report must show the target as the project spelled it:"; echo "$out"; exit 1; } +grep -q "Target x86_64-linux-gnu" <<< "$out" && { + echo "a segment the project did not write must not appear in the report:" + echo "$out"; exit 1; } + +# ── 2. Naming one the graph disagrees with is reported, not refused ───────── +# +# ⚠️ The severity was decided by a measurement. Refusing is the clean answer — +# only one of the two names can describe the artifact — and it broke every +# project and CI configuration spelling the host target `x86_64-linux-gnu`, +# which is what `mcpp toolchain list` prints and therefore what people write. +# mcpp's own openkal matrix was the first casualty. +# +# What settles it is that the request changes nothing: the graph supplies the C +# library either way, so the segment is ignored rather than violated. +out="$("$MCPP" build --target x86_64-linux-gnu 2>&1 || true)" +grep -q "asks for the .gnu. C ABI" <<< "$out" || { + echo "the mismatch was not reported:"; echo "$out"; exit 1; } +grep -q "musl" <<< "$out" || { + echo "the report does not name what resolved:"; echo "$out"; exit 1; } +grep -q -- "--target x86_64-linux" <<< "$out" || { + echo "the report names no correct spelling to use instead:"; echo "$out"; exit 1; } + +echo "OK" diff --git a/tests/unit/test_targetside.cpp b/tests/unit/test_targetside.cpp index cfc7b4e2..0ab741dd 100644 --- a/tests/unit/test_targetside.cpp +++ b/tests/unit/test_targetside.cpp @@ -401,6 +401,65 @@ TEST(TargetSideCapability, TheGrammarKnowsAllFiveLayers) { EXPECT_FALSE(ts::parse_capability("mcpp:c_abi=musl").has_value()); } +// ── The triple is a request; the target side is the fact ──────────────────── + +TEST(TargetSideRequest, AFilledEnvSegmentStatesNothing) { + auto in = payload_linux(); + in.compilerFamily = "llvm"; + in.cAbi = provider("openkal-musl", "0.3.3", "musl"); + in.envNamesCAbi = true; + // `--target x86_64-linux` — the project declined to name a C library, so + // the parser's fill must not become a claim it can be held to. + in.requestedCAbi.clear(); + EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); +} + +// ⚠️ REPORTED, NOT REFUSED. The graph supplies the C library either way, so the +// segment is ignored rather than violated and the artifact is identical with or +// without it. Refusing was tried and broke every project spelling the host +// target `x86_64-linux-gnu` — which is what `mcpp toolchain list` prints. +TEST(TargetSideRequest, AWrittenEnvSegmentThatDisagreesIsReported) { + auto in = payload_linux(); + in.compilerFamily = "llvm"; + in.cAbi = provider("openkal-musl", "0.3.3", "musl"); + in.requestedCAbi = "gnu"; + in.requestFreeTarget = "x86_64-linux"; + in.envNamesCAbi = true; + auto why = ts::check_request(ts::resolve(in)); + ASSERT_TRUE(why.has_value()); + EXPECT_NE(why->find("`gnu`"), std::string::npos); + EXPECT_NE(why->find("musl"), std::string::npos); + EXPECT_NE(why->find("--target x86_64-linux"), std::string::npos) + << "telling someone their target name is wrong is only useful once " + "there is a right one to give them"; +} + +TEST(TargetSideRequest, APrebuiltCLibraryIsWhatTheRequestSelected) { + auto in = payload_linux(); + in.compilerFamily = "llvm"; + in.requestedCAbi = "musl"; + in.envNamesCAbi = true; + // No graph supplier: the payload's C library IS the request's answer, so + // there is nothing to contradict even when the names differ. + EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); +} + +// ⚠️ On Windows the same segment names the OBJECT ABI — `gnu` is PE with the +// GNU ABI, `msvc` is PE with Microsoft's — and both are compatible with more +// than one C library. Reporting such a build as "asking for the `gnu` C ABI" +// describes an axis the name never addressed, and the correction it suggested +// named a target that does not exist. +TEST(TargetSideRequest, TheSegmentIsOnlyACLibraryWhereItNamesOne) { + auto in = payload_linux(); + in.targetOs = "windows"; + in.compilerFamily = "llvm"; + in.cAbi = provider("openkal-musl", "0.3.3", "musl"); + in.requestedCAbi = "gnu"; + in.requestFreeTarget = "x86_64-windows"; + in.envNamesCAbi = false; + EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); +} + // ── Rule two: declared requirements ────────────────────────────────────────── TEST(TargetSideRequirements, ARequirementIsCheckedAgainstWhatResolved) {