diff --git a/.agents/docs/2026-10-23-target-base-and-execution-host-design.md b/.agents/docs/2026-10-23-target-base-and-execution-host-design.md new file mode 100644 index 000000000..c20b87f66 --- /dev/null +++ b/.agents/docs/2026-10-23-target-base-and-execution-host-design.md @@ -0,0 +1,832 @@ +--- +subject: toolchain +status: landed +--- + +# 目标基座与执行宿主:sysroot 的通用模型、运行器契约、自备输入,以及同批的发布产物路径(#832) + +日期:2026-10-23。分析基线:`4c80028c`(mcpp 2026.10.22.1)。实现:mcpp 2026.10.23.1。 + +**修订记录** + +- rev 1–4(评审前):见附录 A。 +- rev 5(实现):按实现修订以下几处,理由写在各节。 + - §3.2:每行合法的写法由三元组的平台类别决定(`mcpp.toolchain.base::row_base`),没有给 `kKnownTargets` 加两列。同一类别的行接受同样的写法,按类别写一张表,目标表中没有的行也能得到回答。 + - §3.1:去掉 `[target..sysroot.c-abi]`。一个写在 `*-linux-gnu` 行上的 sysroot 呈现的就是该行的 C 环境;呈现别的环境,就是另一个目标行。`[c-abi]` 块留给由依赖图构建的 C 库(§11)。 + - §4.1:`Toolchain` 的五组基座字段迁入成员 `tc.base`(类型 `base::Resolution`)。Apple 交叉行的 SDK 记在 `tc.base.apple`,原生 macOS 不陈述时仍取 `xcrun` 的回答,以保证默认构建不变。 + - §4.12:`fallback/sysroot_complete` 没有删除,改为只作用于生态自己的 subos 视图(与载荷同在一个 home 之下)。按路径命名的工具链报告的厂商 sysroot 不再被写入。 + - §3.3:`--sysroot` 是本次调用的 `MCPP_SYSROOT`,在 build、run、test、pack、why 与 emit 上声明,与 `--toolchain` 同形。 + - §15.4:`ndebug = false` 是唯一的改回方式,没有给 `defines` 引入 `!NAME` 语法。`defines` 是集合,增加取反语法会让这一节扩大成清单语法的修改。 + - §15.3:cl.exe 不做探测,直接如实报告"本次未裁剪"(`diag::note`,不会被 `--strict` 升级为错误,因为构建的行为没有改变),没有使用未文档化的开关。 + - §15.5 R4:检查门匹配构建机的检出目录(`GITHUB_WORKSPACE`),不用 `/(home|tmp)/`:二进制本身含有 `/tmp/`、`/home/` 字面量,动态链接的本地构建的 loader 与运行路径指向工具链存储(那是 pack 改写的对象,不是源码路径)。本地实测:旧发布的二进制命中 `/home/runner/work/mcpp/mcpp/modules/libs/src/json/json.hpp`,新引擎构建的 mcpp 不再含检出目录。它作用于 linux-x86_64(由本次发布的引擎在 pack 时重建)与 macOS(第二阶段自举);Windows 与 aarch64 由引导用的旧版引擎构建,在引导版本前移后自然得到裁剪。 + - pack 的下限写为 `HOST-REQUIREMENTS` 中的 `capability=c-abi.glibc>=X discovery=PT_INTERP`,并打印 `Requires glibc >= X`。 + - 新增拒绝码 `target-base`。 + - §15.2:R3(e2e 932)在 CI 上实测出链路上的第二处缺陷,位于 xlings:脚本类型包的别名写作未加引号的 `xlings script <路径>`,shim 按空白切分它(`failed to load script: package file not found: /tmp/…/mc`)。处理分两处:xim-pkgindex#1035 让 gcc 的 hook 以 argv 形式直接运行 `xlings script <脚本>`,不经别名,对已发布的所有 xlings 都成立,本次发布因此不依赖 xlings 的发布;openxlings/xlings#699 给别名中的路径加引号,修正根因。"不需要改 xlings"这一判断在实测后改为"本次发布不依赖 xlings 的修复,但 xlings 有一个需要修的缺陷"。索引侧另需修复的调用有 53 处,多于 §15.2 原先估计的范围。 + - §15.5 R3:实测确认,空格路径可以构建;非 ASCII 字符(`家`)使 GCC 16 的 module mapper(libcody)在预编译 std 模块时失败(`malformed string … INCLUDE-TRANSLATE`)。这是上游限制:同一份 gcc 放在仅含空格的目录下可以构建。mcpp 在该错误之后点名原因与出路(ASCII 路径的 home,或 llvm);R3 改为空格 home 必须能构建,非 ASCII home 要么构建、要么给出这条诊断。docs/20 记为已知工具链限制。 + - §15.3:`trim_paths` 不作用于 `mcpp test`(以及与之同面的 configure 过程)。CI 实测:mcpp 自己的单测在 release 默认档下借 `__FILE__` 定位仓库,被裁剪成 `workspace@0.0.0/src` 后全部找不到。测试二进制在构建处运行,不发布;`ndebug` 照常作用于测试(与 Cargo 的 release 关闭 `debug_assertions` 一致)。 + - §4.5:clean specs 不恢复 `%(sysroot_spec)`。回归 e2e 220 实测:链接器拿到 `--sysroot` 后,会把 sysroot 前缀加到每个输入共享库的 DT_RUNPATH 上,SubOS farm 中 `libEGL.so` 的 RUNPATH 以绝对路径指向载荷,于是 `libGLdispatch` 找不到。hello 程序逐字节相同的测量只覆盖了没有这类输入的链接。改为:被陈述的 gnu-sysroot 基座由渲染器给 gcc 显式加上 `-Wl,--sysroot=`(clang 的驱动自己把 `--sysroot` 交给链接器),生态缺省视图不变。SPEC-006 §4.3 按此改写。 + +关联:mcpp#755(按路径命名的工具链,`sysroot` 键由此引入)、mcpp#378(clean link specs)、 +mcpp#400(运行时闭包校验)、mcpp#392(运行时绑定先于工具链解析)、**mcpp#832**(home 路径含空格; +发布产物残留构建机路径,本批的 §15); +[SPEC-002](../../docs/specs/target-side.md)、[SPEC-004](../../docs/specs/manifest-semantics.md)、 +[SPEC-006](../../docs/specs/toolchain-management.md)。issue:待建(sysroot 部分)。 + +复现:[2026-10-23-target-base-and-execution-host-repro.sh](2026-10-23-target-base-and-execution-host-repro.sh) +(只用 mcpp home 中已有的载荷,场景 A–K;`FETCH_DEBIAN=1` 时另取一份 Debian 11 glibc 2.31)。 + +读者:评审与实现者。本文由一次讨论收敛而来,从"自定义 sysroot 带自己的 glibc"的实测开始, +扩展到全平台 sysroot 的含义、Linux 上的 Apple SDK 与模拟器、引擎与插件的边界。 +问答与结论见附录 A。 + +**本设计的边界**: + +- **核心知识判据(D18)**:引擎只持有两类知识。一是通用的抽象与契约;二是**核心内置目标行所在平台的官方约定**,例如 glibc 的 loader 名与 Debian multiarch、Apple 自己的 `SDKROOT` 与 `xcrun`、微软的 vswhere。第三方生态与工具的知识(Swift SDK 包、qemu、xtool、ssh、simctl……)一律不进核心,通过通用接口由包和插件接入。 +- **用户首先面对的是 `mcpp.toml`**:所有新键都可以写在工程里,`config.toml` 只是默认值。路径或工具缺失时,给出友好、可操作的提示。 +- mcpp 与官方索引不分发受厂商许可约束的内容,只提供让开发者自备的标准接口。 + +## 0. 结论 + +| # | 决定 | +|---|---| +| D1 | **sysroot 是交付形式,不是层**:它是目标侧下层的**预制供给者**,称为**目标基座**,清单键仍叫 `sysroot`。它供给 `c-abi`,通常还有 `kernel-abi`,可能还有 `c++-abi`、`compiler-runtime` 与平台库 | +| D2 | **四个正交属性**:来源、地址、供给的层、运行时语义 | +| D3 | **全平台同一套写法**:`[target.].sysroot` 接受生态包、本机定位器、`{ path }`、`""`,或者不写。定位器族名用业界词汇:`glibc`、`musl`、`msvc`,以及 xcrun 的 SDK 名 `macosx`、`iphoneos`、`iphonesimulator`。某行接受哪些写法是目标表里的数据 | +| D4 | **一次解析,所有读者读同一个结果**(编译、扫描、std、驱动与 ld 的链接、产物运行时、闭包校验、hermetic、缓存键与指纹、报告、pack、运行器)。七处散落的决定合成一个 `BaseResolution` | +| D5 | **"工具链跑在什么上"(宿主)与"产物绑定什么"(目标)分开回答**。前者不变;#392 的时序约束只属于前者 | +| D6 | **c-abi 独占律**:一个目录作为 `--sysroot` 寻址,当且仅当它供给 c-abi。可叠加层(比如内核头文件)由引擎接线;基座的 c-abi 与依赖图的 c-abi 并存时,按规则一报错 | +| D7 | **不新增 `libc` / `c-abi` 键**,用已有的 `provides`、`requires`、`version-floor` | +| D8 | **规则二改为可测量**:编译器运行时与预制库要求的 C 库版本,不得高于基座的版本 | +| D9 | **运行时语义三种**:binding、floor、none。开发构建时 binding,pack 时改为 floor | +| D10 | **不写入、不修补外部树**。布局问驱动,实现与版本问预处理器;唯一的布局知识是 Debian multiarch(有规范定义,且实测需要) | +| D11 | **宿主基座 `glibc@system` / `musl@system`**(`{ path = "/" }` 归一到它)。用于为发行版打包、链接宿主的库 | +| D12 | **执行宿主**:工程写 `runner`(用什么跑),`run_under` 写"经过什么到达执行宿主",执行时两者组合。本地、虚拟机、远程真机在引擎里是同一个抽象 | +| D13 | **运行器契约标准化**:一组环境变量、一份运行闭包清单、运行器可嵌套、可能被并发调用、退出状态透传、物理上无法执行时在执行前拒绝 | +| D14 | **值的层次**:**`mcpp.toml` 为主**(工程与团队的约定);`~/.mcpp/config.toml` 与系统级 `/etc/mcpp/config.toml` 提供**默认值**;环境变量与命令行负责**覆盖**。顺序沿用 #755:覆盖 > 工程 > 机器默认 > 引擎默认。缺失时给出友好提示,列出三种让它存在的办法:xpkg 分发、仓库内相对路径、团队约定路径 | +| D15 | **自备输入**写成规范条款:只按公开标准定位、记录身份,从不获取、托管或分发 | +| D16 | **目标行与厂商的硬件路线无关**:x86_64 的 Apple 行由 SDK 里有没有对应架构切片来决定(测量),执行由执行宿主决定。`x86_64-ios-sim` 由 CI 验证:arm64 job 构建,Intel job 运行(§2.6) | +| D17 | 一并修正的缺陷(§1.2):clean specs 丢了 ld 的 `--sysroot`;声明的 sysroot 没有 loader 与 rpath;rule B 拿生态 glibc 比对;`ensure_sysroot_complete` 写入外部树;基座身份不进指纹;工具链阶段静默丢弃键;非 Apple 宿主完全不能定位 Apple SDK | +| D18 | **核心知识判据**(见开头)。据此:Swift SDK 包(SE-0387)和 `QEMU_LD_PREFIX` **不进核心**;`SDKROOT` 进核心,并且在所有宿主上读取 | +| D19 | **同批修复 #832 中属于 mcpp 的部分**(§15):建议命令中的路径加引号;release / dist 档默认做路径裁剪(`trim_paths`,根包与依赖都映射成 `@`),mcpp 自己的发布产物接入检查门;release / dist 档默认定义 `NDEBUG`(`ndebug` 键);home 路径含空格由 xim-pkgindex 修复(不改 xlings),mcpp 加 e2e | + +## 1. 问题与证据 + +### 1.1 实测(Linux x86_64,宿主 glibc 2.39,mcpp 2026.10.22.1) + +| 场景 | 写法 | 结果 | +|---|---|---| +| A | `[toolchain.linux] { path }`,自定义 gcc 16.1.0 + binutils | ✅ 用生态 glibc 2.44.3 | +| B | A 加 `prefix` | ✅ 同 A。注释说"用自己携带的 sysroot",实际不是 | +| C | A 加 `sysroot = ` | ❌ `ld: cannot find /lib64/libm.so.6` | +| D | C 加 `-Wl,--sysroot` | ❌ PT_INTERP 是宿主的 loader,RPATH 末尾是 subos farm,运行时报 `symbol lookup error … GLIBC_PRIVATE`;rule B 拦下了,但建议的是"选一个 SubOS" | +| E | D 再补上 loader 与 rpath | 产物本身正确,**rule B 仍然拒绝** | +| F | 托管 gcc 加 `[target] sysroot = "<目录>"` | ❌ "is not an xpkg reference" | +| G、H | 交叉 `aarch64-linux-musl-gcc` | ✅ | +| I | 加 Debian 11 glibc 2.31 sysroot | ❌ 报错落在 `std.cc` 里;真正的原因是 libstdc++ 要求 GLIBC_2.38,没有说出来 | +| J | 生态 gcc 加 `--sysroot=/`(Ubuntu) | ❌ `cannot find crti.o`;补上 multiarch 目录后 ✅ | +| K | 生态 clang 加 `--sysroot=/` | ✅(clang 自己识别 multiarch) | + +A、D、E 三个工程的输出目录指纹相同(`17bd18d28b33e658`):基座不同,却没有被区分。 + +### 1.2 缺陷与设计 + +时间线: + +- **#378**(08-08)引入 clean specs,`sysroot_mode` 显式补 loader 与 rpath。当时的前提是:GCC 的 sysroot 只可能是 subos,subos 链接脚本里写的是 payload 的绝对路径。 +- **#400**(08-10)引入闭包校验 rule B。 +- **#755**(10-02)复用 `sysroot_mode`,同时清空了 `payloadPaths`,没有写测试。 + +| 现象 | 位置 | 裁定 | +|---|---|---| +| ld 拿不到 `--sysroot`(gcc 在读 `-specs=` 之前插入的 `%(sysroot_spec)` 被覆盖掉) | `src/toolchain/post_install.cppm:375` | **缺陷**(字段注释写的是"for the compiler and the linker";违反 SPEC-006 §4.3) | +| 声明的 sysroot 没有 loader 与 rpath | `local_toolchain.cpp:305`、`linkmodel.cppm:515` | **缺陷**(#378 的注释明言这是缺陷) | +| rule B 拿生态 glibc 比对 | `prepare/toolchain.cpp:151-165` | **缺陷**(违反 SPEC-006 §3.1) | +| 工具链阶段 `spec` 加其他键时静默丢弃 | `local_toolchain.cpp:140` | **缺陷** | +| 往外部 sysroot 里建符号链接 | `detect.cppm:139` → `fallback/sysroot_complete.cppm` | **缺陷**(违反 SPEC-006 §2.2.1) | +| 基座身份不进指纹 | `fingerprint.cppm` 第 10 字段 | **缺陷** | +| 非 Apple 宿主不能定位 Apple SDK | `macos.cppm` 的 `sdk_path()` 整段在 `#if defined(__APPLE__)` 里,`SDKROOT` 也读不到 | **缺陷**(对本设计而言) | +| `[target].sysroot` 只接受 xpkg 引用 | `toml.cppm:844` | 设计,本次扩展 | +| `{ path }` 不写 sysroot 时用生态 glibc | `local_toolchain.cpp:260` | 设计,保留 | +| 清单的 `cfg(...)` 只能判断目标,不能判断宿主 | SPEC-004 §3 | 设计,保留(影响 §3.7) | + +### 1.3 根因 + +`Toolchain` 里装着五组属于基座的字段(`sysroot`、`payloadPaths`、`targetSysroot*`、`appleSdk*`、`msvc*` / `windowsSdk*`), +由七条代码路径各自填写: + +- Linux 托管、Linux 声明、裸机的 multilib 块; +- Apple 的四处 `sdk_path()`; +- `bind_msvc_sysroot`; +- `has_own_sysroot`(9 个文件); +- `bind_windows_ucrt` 的回流。 + +缺陷都出在这些路径的接缝上。 + +### 1.4 本轮不实测的部分 + +通过本机 macOS 客体运行 `x86_64-ios-sim` 这条路,本轮不测(附录 A.8)。核对过的前提: + +- 满足:`/dev/kvm` 可读写;32 核 / 62G;系统 QEMU 8.2.2;本地配方 `localtest:osx-kvm` / `macos-vm` / `xcode-in-vm` 都在(2026-09-12,头注释写明"仅本地,永不发布")。 +- 不满足:**`/home` 只剩 7.1G**,需要 100G 以上;没有客体磁盘;没有 Xcode.xip。 + +它的验证由 CI 的 Intel runner 替代(§2.6)。 + +## 2. 模型 + +### 2.1 定义与四个属性 + +> **目标基座(sysroot)**:目标侧下层的预制供给者。它是一个按目标驱动约定组织的目录,供给 `c-abi`,通常还有 `kernel-abi`,可能还有 `c++-abi`、`compiler-runtime` 与平台库。 + +层是"供给了什么",五层闭集不变;基座是"以什么形式预制供给"。SPEC-002 §1.3 的 `prebuilt` +放宽为"被点名、按内容识别的预制目录"。出处类别沿用 #755 的 `SourceClass`,**不新增层来源,也不新增出处类别**。 + +| 属性 | 取值 | 由谁回答 | +|---|---|---| +| 来源 | 覆盖、工程、机器默认、引擎默认(§3.3) | 优先级 | +| 地址 | `gnu-sysroot`(驱动与 ld)、`apple-isysroot`、`msvc-roots`、`multilib`、`driver-internal` | 目标表一列 | +| 供给的层 | `{层 → 实现, 版本, 位置}`,加上事实(如 `glibc=2.31`、`iphoneos.sdk=18.5`、架构切片) | 测量(§4.3),必要时声明 | +| 运行时语义 | binding、floor、none | c-abi 的实现,加上"是否本机" | + +### 2.2 全平台语义 + +| 目标行 | 基座实物 | 供给的层 | 地址 | 运行时语义 | 可用写法 | 不写时 | +|---|---|---|---|---|---|---| +| `*-linux-gnu` | glibc 加内核头文件(可能带库);宿主的 `/` | kernel-abi、c-abi | gnu-sysroot(加 multiarch) | 本机 binding;交叉 floor | `xim:…`、`glibc@system`、`{path}`、`""` | 生态 glibc 绑定 | +| `*-linux-musl` | 工具链里的 `/`;Alpine 的 `/` | kernel-abi、c-abi | gnu-sysroot | 静态 none;动态本机 binding | `xim:…`、`musl@system`、`{path}`、`""` | 工具链携带 | +| `*-none-*` | picolibc / newlib 的 multilib 目录 | c-abi | multilib | none | `xim:…`、`{path}`、`""` | 行的 `sysroot` 列 | +| `*-macos` | MacOSX SDK | kernel-abi、c-abi(libSystem 桩)、C++ 头文件、frameworks | apple-isysroot | floor | `macosx@system`、`macosx@<版本>`、`{path}` | `macosx@system` | +| `*-ios` / `*-ios-sim` | iPhoneOS / iPhoneSimulator SDK | 同上 | apple-isysroot | floor | `iphoneos@…` / `iphonesimulator@…`、`{path}` | 对应的 `@system` | +| `*-windows-msvc` | VC toolset 加 Windows SDK | c++-abi、compiler-runtime、c-abi(UCRT)、kernel-abi | msvc-roots | UCRT 是 floor | `msvc@system`、`msvc@<版本>`、`xim:msvc@…`、`{path}`(仅 clang 行) | `msvc@system` | +| `*-windows-gnu`、`*-android`、`wasm32-emscripten` | SDK 工具链内建 | 视 SDK 而定 | driver-internal | floor 或 none | 不可写 | 工具链携带 | + +定位器族名必须与目标行一致。`glibc@<版本>` 不带前缀时,等同于 `xim:glibc@<版本>`(SPEC-006 §2.3)。 + +### 2.3 两个运行时问题 + +| 问题 | 轴 | 回答者 | +|---|---|---| +| 编译器自己运行在什么 C 库上 | 宿主 | 提供者为工具链钉定(`pin_runtime`),不变 | +| 产物针对什么编译、运行时绑定什么 | 目标 | `BaseResolution` | + +默认构建两问答案相同,所以输出不变。托管 gcc 配自定义基座时:编译器运行在生态 glibc 2.44 上,产物绑定基座的 2.31。 + +### 2.4 c-abi 独占律 + +- **独占**:c-abi 决定驱动在哪里找 `crt*.o`、`libc.so` 与 loader,不能从基座里拆出去。 +- **可叠加**:内核头文件、第三方头文件和库,由引擎用 `-isystem` / `-L` 接线。 +- 基座里自带的其他库,来源可以追溯,允许使用,并在链接闭包中标明来自基座。宿主基座下,宿主的库就是基座的库,不再需要 `allow_host_libs`。 +- 不供给 c-abi 的目录是库前缀,按依赖处理,不能当 sysroot。 + +### 2.5 执行宿主 + +| 执行宿主 | 用途 | 引擎看到的 | +|---|---|---| +| 本地 | 本机目标 | 不包装 | +| qemu-user / binfmt | Linux 交叉目标 | 运行器(qemu 的包提供,它把 `MCPP_SYSROOT` 转成 `-L` 或 `QEMU_LD_PREFIX`) | +| 本机虚拟机的客体(如 macOS 客体加 simctl) | 在 Linux 上执行 x86_64 的 Apple 产物 | `run_under` 加运行器 | +| 远程真机(Mac、云 Mac、开发板) | arm64 的 Apple 行、开发板 | `run_under` 加运行器 | +| 设备 | iOS / Android 真机 | 运行器(插件提供) | + +在本地构建,在执行宿主上执行;mcpp 本身不需要运行在执行宿主上。 + +### 2.6 目标行、硬件路线与 `x86_64-ios-sim` + +- **目标行**能否构建,取决于有没有一份带对应架构切片的基座。切片可以测量(`libSystem.tbd` 的 `targets`),引擎里不写截止日期。新版 SDK 去掉 x86_64 后,用户可以用 `{ path }` 或 `macosx@<旧版本>` 固定一份旧 SDK。 +- **执行**取决于执行宿主。GitHub 当前的 runner 镜像中有 x64 的 `macos-15-intel` 与 `macos-26-intel`,而 Xcode 27 的镜像只有 arm64。由此可以确认: + - macOS 26 仍然支持 Intel。本地 `macos-vm` 配方头注释里"Tahoe 完全放弃 Intel"的说法有误。 + - Intel 的终点大约在 Xcode 27 这一代。 +- **`x86_64-ios-sim` 的真实需求**: + - 在 Linux 虚拟机里运行它的需求很低。 + - 构建它的需求真实存在:给 Intel Mac 或 Intel CI 的使用方提供 arm64 + x86_64 的模拟器通用切片(XCFramework 的标准形态),虽然 mcpp 目前还不产出 XCFramework。 + - 这一行已经在发布(preview),但**从没运行过**。 +- **验证(纳入本 PR)**:arm64 的 job 用 mcpp 构建,把产物交给 Intel 的 job,后者只执行 `xcrun simctl spawn`。Intel 一侧不需要 mcpp(mcpp 也没有 Intel macOS 的二进制)。这是"在这里构建、在那里执行"的实证,传输方式是 CI 的 artifact。S15 通过后,按层级规则把该行升为 verified,同时更新 README 的目标表。 + +### 2.7 行业对照 + +| 系统 | sysroot | 运行器 / 执行 | 机器默认 | +|---|---|---|---| +| GCC / Clang | `--sysroot`;clang `/winsysroot` | — | — | +| CMake | `CMAKE_SYSROOT` / `CMAKE_OSX_SYSROOT`(可写 SDK 名) | `CMAKE_CROSSCOMPILING_EMULATOR` | 用户 presets | +| Cargo | — | `target..runner` | `~/.cargo/config.toml`、`CARGO_TARGET__*` | +| Bazel | `cc_toolchain` | `--run_under` | `.bazelrc` | +| Meson | `sys_root` | `exe_wrapper` | 机器文件 | +| Xcode | `SDKROOT`、`iphoneos18.2` | `simctl` / `devicectl` | — | + +## 3. 清单与配置语义 + +### 3.1 写法 + +```toml +[target.] +sysroot = "xim:glibc@2.39" # 生态包;团队私有索引也行:"acme:sysroot-rhel8@1.0" +sysroot = "glibc@system" # 本机定位器(glibc / musl / macosx / iphoneos / iphonesimulator / msvc) +sysroot = "iphoneos@18.5" # 定位器加版本匹配 +sysroot = { path = "third_party/sysroot" } # 按路径命名:相对路径相对于写它的那个文件 +sysroot = "" # 不用预制 C 库 +sysroot = { path = "/opt/exotic", provides = ["mcpp:c-abi=exotic"] } +``` + +- 表只有 `path` 与 `provides` 两个键;`{ path = "/" }` 归一为该行的 `@system`;`~` 会展开。 +- 陌生的 c-abi 用 `provides = ["mcpp:c-abi="]` 声明;不支持 `[c-abi]` 块(rev 5,见修订记录)。 +- **团队让基座"存在"的三种办法**(文档与诊断都按这个顺序推荐): + 1. **xpkg 分发**(包括团队私有索引):有版本、有身份、可复现,首选; + 2. **仓库内相对路径**(子模块或 LFS); + 3. **团队约定的路径**(如 `/opt/acme/sysroot`)。 + +### 3.2 每行合法的写法是数据 + +每行接受的写法与地址由该行的平台类别决定:`mcpp.toolchain.base::row_base` 按三元组返回地址、接受的写法位集与定位器族名,`refusal` 据此拒绝并列出该行接受的写法。清单解析只做语法检查(是不是某种写法),因为清单模块不解析三元组;某行接受与否在目标已知处判定。 + +### 3.3 优先级:四档 + +| 档 | 来源 | 出处类别(#755) | +|---|---|---| +| 1 覆盖 | `--sysroot` / `MCPP_SYSROOT`;`MCPP_TARGET__SYSROOT` | 按写法 | +| 2 **工程** | ① `mcpp.toml` 的 `[target.].sysroot`(含工作空间层);② `[toolchain] { path, sysroot }`;③ 工具链阶段陈述的 `sysroot`;④ 工具链携带、经探测得到 | ①② 按写法;③ `program`;④ 跟随该工具链 | +| 3 机器默认 | `~/.mcpp/config.toml`,其次 `/etc/mcpp/config.toml` 的 `[target.].sysroot` | 按写法 | +| 4 引擎默认 | ① 目标表该行的 `sysroot` 列(仅当依赖图中没人供给 c-abi);② 平台默认 | `managed`,默认输出不变 | + +写法与出处类别的对应: + +| 写法 | 出处类别 | +|---|---| +| 生态包 | `managed`;工程写了版本时为 `pinned` | +| `{ path }` | `custom` | +| `*@system` | `host` | +| `*@<版本>` | `pinned` | +| 工具链阶段 | `program` | + +- 环境变量里的 `none` 表示"不用预制 C 库"(对应清单里的 `""`);空字符串等于没设置。 +- 同一档内,更具体的陈述胜出,并报告覆盖。 +- `--managed-only` 拒绝一切非默认的类别。 +- 与 `[xlings] subos` 同时出现时,subos 只供给宿主工具,并报告一行。 + +### 3.4 `provides`、`requires` 与版本下限 + +- 测量能回答时,`provides` 用于核对,不一致就拒绝;测量回答不了时,`provides` 就是声明;两者都没有就拒绝。 +- 断言实现:`requires = ["mcpp:c-abi=glibc"]`。 +- 版本下限:`[[runtime.requirements]] kind = "version-floor"`。基座把测量到的事实以 `runtime.provides` 的同形发布。 + +### 3.5 工具链陈述与工具链阶段 + +- `[toolchain] { path, sysroot }` 的含义是"这套工具链携带的基座"。 +- **工具链阶段可以同时陈述 `spec` 与 `sysroot`**,表示由程序陈述目标行的基座(类别 `program`)。这是插件定位基座的接口。 +- `spec` 加 `prefix` / `launcher` / `tool.*` / `family` 时按名拒绝,修正现在静默丢弃的缺陷。 + +### 3.6 依赖、工作空间与宿主图 + +- 键类别不变(Configuration,从根读取)。 +- 依赖用 `requires` 与 `version-floor` 表达要求。 +- **宿主图永远不读目标基座,也不读 `run_under`。** + +### 3.7 `run_under`:写在工程里为主,机器默认为辅 + +```toml +# mcpp.toml —— 团队约定,所有人都经同一台共享 Mac 执行 +[target.aarch64-ios-sim] +runner = ["simctl-run"] +run_under = ["remote-run", "team-mac", "--"] # team-mac 是团队 ssh 配置里的主机别名 +``` + +```toml +# ~/.mcpp/config.toml —— 只在这台机器上需要(混合团队:Mac 上的同事不需要它) +[target.x86_64-ios-sim] +run_under = ["remote-run", "macvm", "--"] +``` + +- **层次与 `sysroot` 相同**:覆盖(`MCPP_TARGET__RUN_UNDER`,值为 `none` 时清除)> `mcpp.toml` > `config.toml` > 无。 +- 执行形如 ` <产物> <参数>`;`--no-runner` 表示"这个三元组在本机是原生的",所以 `runner` 与 `run_under` 都去掉。 +- **团队怎么选**:全员同一种执行环境,写在 `mcpp.toml`;混合环境(有人在 Mac 上、有人在 Linux 上),写在各自的 `config.toml`;个别机器的例外,用环境变量清除或替换。 +- 我们**不**给清单的 `cfg` 加宿主谓词:清单的条件只判断目标(SPEC-004 §3),三层配置已经够用(§11)。 +- 机器默认不能放宽工程的约束:`requires` / `version-floor` 照样核对。 + +### 3.8 缺失时的友好提示 + +| 缺什么 | 提示里给出 | +|---|---| +| `{ path }` 不存在 | 文件与行号、解析后的绝对路径;三种让它存在的办法(§3.1);如何在本机覆盖(`MCPP_TARGET_…_SYSROOT`、`config.toml`) | +| 定位器找不到 | 查过哪些位置;生成方法(例如 Apple SDK 在 Linux 上可以用 xtool 从自己的 Xcode.xip 生成,然后设 `SDKROOT` 或写 `{ path }`) | +| `run_under` / `runner` 的程序找不到 | 现有的"程序、查过的目录、errno";怎么通过 `[xlings.workspace]` 装上它;本机覆盖或 `--no-runner` | +| 物理上无法执行(如 Linux 上的 iOS 模拟器产物,又没有 `run_under`) | 执行前就拒绝,说明原因,并给出 `run_under` 的写法 | + +### 3.9 拒绝的写法 + +| 写法 | 理由 | +|---|---| +| `sysroot = { path, libc = … }` | 第二种语法;用 `provides` | +| 基座的 c-abi 与依赖图的 c-abi 并存 | 规则一 | +| `{ path }` 下既测量不到 c-abi,也没有声明 | 是库前缀,不是基座 | +| SDK 工具链行写基座 | 基座就是那个 SDK | +| cl.exe 行写了不同的 toolset,或写 `{ path }` | SPEC-006 §3.5 | +| 定位器族名与目标行不符 | 会描述另一个平台 | +| `xim:` 前缀加 Apple 定位器 | 没有生态包 | +| 工具链阶段 `spec` 加路径专属的键 | §3.5 | + +### 3.10 场景 + +| 场景 | 之后的写法 | +|---|---| +| 用旧 glibc 构建可分发的二进制 | `sysroot = "acme:sysroot-glibc228@1"`(团队索引),或 `{ path }`;`pack` 时报"需要 glibc ≥ 2.28" | +| 为发行版打包;链接宿主的库 | `glibc@system` | +| 厂商交叉 SDK | `[toolchain] { path, prefix, sysroot }` | +| 托管编译器配公司的 sysroot | 目标行写 `{ path }` 或团队 xpkg | +| Yocto / Buildroot SDK | 同上 | +| 固定一份 Apple SDK | `iphoneos@18.5` 或 `{ path }` | +| Linux 上构建 iOS / macOS | 自备 SDK:`SDKROOT`,或 `{ path }` | +| Linux 上运行 x86_64 的 iOS 模拟器或 macOS 产物 | `run_under = ["remote-run", "<客体或远程 Mac>", "--"]`,写在 `mcpp.toml` 或 `config.toml` | +| 解包的 VS 或 winsysroot 目录 | clang 行写 `{ path }`,两种布局自动识别 | +| CI 或发行版打包时换基座 | `MCPP_SYSROOT=path:/sysroots/rhel8` | + +## 4. 引擎架构 + +### 4.1 一个解析结果 + +纯数据部分放在 `modules/toolchain-model/src/base.cppm`,定位、测量与安装放在 `src/toolchain/base.cppm`。 + +```cpp +struct BaseStatement { Form form; std::string value; std::vector provides; SourceDecision origin; }; +struct BaseResolution { + SourceClass source; AddressKind address; std::filesystem::path root; + std::map layers; std::vector> facts; + bool multiarch = false; RuntimeSemantics runtime; + std::filesystem::path loader; std::vector runtimeDirs; + std::string identity; MsvcRoots msvc; AppleSdk apple; +}; +``` + +`Toolchain` 收缩为编译器本身,五组字段迁入 `BaseResolution`。cl.exe 行的 toolset 记为 `via toolchain`。 + +### 4.2 解析时序 + +| 步 | 内容 | +|---|---| +| 1 | 由覆盖、工程、机器默认、目标表得出 `BaseStatement`(工具链阶段的陈述在第二遍 prepare 时进入) | +| 2 | 定位(Package 走 fetcher;Locator 走 §4.4;Path 检查存在;Default 走现有的 `resolve_runtime_binding`) | +| 3 | 宿主侧:工具链运行库的钉定(不变,#392 的时序在这里) | +| 4 | 工具链解析(不变) | +| 5 | 测量(§4.3) | +| 6 | 产物运行时由基座推导(`bind_windows_ucrt` 的回流并入这一步) | +| 7 | 层间下限检查 | +| 8 | 依赖图解析之后,目标侧读取基座的层(检查规则一、规则三) | + +### 4.3 测量 + +- **布局问驱动**:`-print-file-name=crt1.o / libc.so / libc.a`、`-print-search-dirs`。 +- **Debian multiarch**:驱动找不到 `crt1.o`,且存在 `/usr/lib/`(tuple 取自 Debian MultiarchTuples 数据表)时,补上 `-B`、`-L`、`-isystem` 与 `-Wl,-rpath-link`,再问驱动一次,并记录 `multiarch = true`。驱动自己能处理(clang)时不补(实测 J、K)。 +- **实现与版本**:驱动 `-E -dM` 预处理一个探针翻译单元,对照数据表: + +| 实现 | 判据 | 版本 | 运行时语义 | 事实名 | +|---|---|---|---|---| +| glibc | `__GLIBC__` | `__GLIBC__.__GLIBC_MINOR__` | 本机 binding / 交叉 floor | `glibc` | +| bionic | `__BIONIC__` | `__ANDROID_API__` | floor | `android.api` | +| picolibc | `__PICOLIBC__` | `__PICOLIBC_VERSION__` | none | `picolibc` | +| newlib | `_NEWLIB_VERSION` | 同名宏 | none | `newlib` | +| emscripten | `__EMSCRIPTEN__` | `__EMSCRIPTEN_major__…` | none | `emscripten` | +| musl | 以上都不是,且能找到 `ld-musl-*` 或 `libc.a` | 读不出 | 静态 none / 动态 binding | `musl` | +| ucrt | 定位器 | SDK 版本 | floor | `ucrt` | +| libSystem | 定位器 | `SDKSettings.json`;切片取自 `libSystem.tbd` 的 `targets` | floor | `.sdk` | + +- 读不出的版本不参与比较。 +- 结果按"驱动身份 × 基座身份"缓存;生态默认基座不执行探针。 + +### 4.4 定位器(只用平台的官方约定,D18) + +| 族 | 查找顺序(报告用了哪个) | +|---|---| +| `glibc@system` / `musl@system` | `/`;在 Luban 系统上,`/` 按已有的 `declared_system_root` 规则处理 | +| `macosx` / `iphoneos` / `iphonesimulator` `@system` | ① `SDKROOT`(**所有宿主**;仅当它指向该平台的 SDK,沿用现有的 `sdkroot_answers`);② `xcrun --sdk <名>`;③ `xcode-select` 与固定位置(②③ 只在 Apple 宿主上)。`sdk_path()` 不再整段包在 `__APPLE__` 里 | +| `…@<版本>` | 同上,只接受版本匹配的那一个 | +| `msvc@…` | 现有规则,不变 | +| MSVC 行的 `{ path }` | toolset 取 `/VC/Tools/MSVC/`;SDK 在 `/Windows Kits/10` 存在时用它(clang `/winsysroot` 布局),否则走机器的 SDK 查找(VS / `xim:msvc` 布局),并报告用了哪个 | + +**Swift SDK 包(SE-0387)不在核心里**:Linux 用户用 xtool 等工具生成之后,设一次 `SDKROOT`,或者写 `{ path }`。 +插件或包可以提供一个辅助命令打印路径(§6)。 + +### 4.5 地址的渲染 + +每种地址一个渲染器,取代 `ToolchainLinkModel` 中描述 C 库的部分与 `hostflags` 片段。 + +- **gnu-sysroot**:驱动**与 ld** 都拿 `--sysroot`(被陈述的基座由 gcc 的 `-Wl,--sysroot=` 给出,见 rev 5);binding 时加 loader 与 RUNPATH;需要时加 multiarch。 +- **apple-isysroot**:`-isysroot` 加 deployment target。 +- **msvc-roots**:`-Xmicrosoft-*` 与 `-fms-compatibility-version`;cl.exe 用 INCLUDE / LIB。 +- **multilib**:`-isystem` / `-L` 指向 `/{include,lib}/`。 +- **driver-internal**:只给 `--target`。 + +### 4.6 产物运行时 + +- **binding**:PT_INTERP 与 RUNPATH 指向基座;契约哈希包含基座身份;subos farm 只在生态默认基座时追加;rule B 以基座为参照,按来源给出修复建议。 +- **floor**:loader 用目标的标准路径;`runtimeId` 作为下限。 +- **none**:没有运行时。 + +### 4.7 运行器契约 + +执行形如 ` <产物> <参数>`。引擎不认识任何运行器的名字。 + +| 项 | 规定 | +|---|---| +| 定位 | 先在声明的载荷 `bin/` 里找,再到 `PATH` 上找;`run_under` 的第一个元素同样处理 | +| 环境变量 | `MCPP_RUN_TRIPLE`、`MCPP_RUN_KIND`(`exe` / `bundle`)、`MCPP_RUN_MODE`(`run` / `test`)、`MCPP_RUN_ID`、`MCPP_RUN_CLOSURE`、`MCPP_SYSROOT` | +| 运行闭包清单 | 一份带 schema 版本的 JSON:产物、它运行时需要的文件(来自 `runtime_placement`)、资源目录、基座根 | +| 嵌套 | 运行器可以包装运行器,argv 原样传递 | +| 并发 | 可能被并发调用,用 `MCPP_RUN_ID` 区分 | +| 退出状态 | 运行器的退出码即程序的退出码;拿不到程序自己的状态的运行器,必须在运行前说明 | +| 物理边界 | 没有任何执行路径时,执行前拒绝(§3.8) | + +`QEMU_LD_PREFIX` 这类工具专属的变量**不由引擎设置**(D18),由对应运行器的包从 `MCPP_SYSROOT` 转换。 + +### 4.8 层间下限检查 + +- 编译器载荷的 `libstdc++.so.6` / `libgcc_s.so.1` 的 verneed 中最高的 `GLIBC_x.y`,与基座事实比较,编译前拒绝。 +- 预制库走同一个比较器。 +- Apple:deployment target ≤ SDK 版本,且 SDK 带目标架构的切片。 + +### 4.9 身份、指纹与快速路径 + +- **身份**:生态包取包身份;定位器取版本加根目录;路径与宿主基座取根目录加测量所依据文件的路径、大小、修改时间。 +- 身份进入契约哈希,因此进入指纹第 10 字段;同时写入 stamp;跨工程缓存键也加上它。 +- 生态默认基座的身份就是现在的契约哈希,升级不会让缓存失效。 + +### 4.10 报告 + +- 非默认的基座与 `run_under` 各打印一行:`Using sysroot glibc 2.31 ← third_party/sysroot [custom · mcpp.toml:12]`、`Running via remote-run team-mac [mcpp.toml:15]`。 +- 默认情况下输出逐字节不变。 +- 目标侧报告中,基座供给的层显示为 `prebuilt`。 +- `resolution.json` 新增 `base` 与 `execution`;`sources` 新增 `sysroot`、`run_under`;新增 `mcpp why sysroot`。 + +### 4.11 pack + +- `vendored` / `system` 模式:binding 改成发行版路径,并写入基座下限。 +- `self-contained` 模式:随包带的是基座的 loader 与 C 库。 +- §15 的路径裁剪默认作用于 pack 所用的 dist 档。 + +### 4.12 删除与收敛 + +| 删除或改写 | 去向 | +|---|---| +| `Toolchain` 的五组基座字段 | `BaseResolution` | +| `fallback/sysroot_complete.cppm` | 保留,只作用于与载荷同在一个 home 下的 subos 视图(rev 5) | +| `payloadPaths.reset()` 一带 | 改为陈述基座 | +| 裸机 multilib 块 | Package 加 multilib | +| 四处 `sdk_path()` 与 `__APPLE__` 的整段包裹 | Apple 定位器 | +| `bind_msvc_sysroot` / `check_cl_row_sysroot` | MSVC 定位器 | +| `bind_windows_ucrt` 回流 | §4.2 第 6 步 | +| `has_own_sysroot()` 的 9 处读取 | `address == driver-internal` | +| `sysroot_refusal` 的拼写分支 | 目标表 | + +## 5. 自备输入(BYO) + +### 5.1 条款 + +对受厂商许可约束、或者属于某台机器某个人的输入,引擎: + +1. 只按公开标准定位(`SDKROOT`、xcrun、`/winsysroot` 布局、`{ path }`、`config.toml`); +2. 记录身份; +3. 从不获取、托管或分发,官方索引不收录; +4. 缺失时给出友好提示(§3.8); +5. 生成它们的工具在许可证与服务条款允许时可以是 xpkg; +6. 不接触任何凭据。 + +### 5.2 范围 + +| 输入 | 谁生成 | 怎么接入 | +|---|---|---| +| Apple SDK | Xcode;在 Linux 上由 xtool / xcross 用用户自己的 Xcode.xip 生成 | `SDKROOT`、`{ path }` | +| macOS 客体、远程 Mac | 用户 | `run_under` | +| 签名证书、描述文件、账号 | 用户 | 插件 | +| 非生态的 VS / Windows SDK | 用户 | MSVC 定位器 | + +### 5.3 许可证 + +mcpp 只为统一平台提供中性机制,不规避许可证。文档以远程 Apple 执行宿主为主要例子; +CI 不运行 macOS 虚拟机,Intel 验证用 GitHub 的 Intel runner(§2.6)。 + +## 6. 生态侧配套(引擎之外) + +引擎 PR 不依赖这些;它的 e2e 用测试目录里的假运行器验证契约。 + +| 项 | 归属 | 说明 | +|---|---|---| +| `remote-run` | xim-pkgindex(可公开) | ssh 传输:读闭包清单、rsync、执行、带回退出状态、按 `MCPP_RUN_ID` 清理 | +| qemu 的运行器包装 | xim-pkgindex(`qemu-user-*` 的配方) | 把 `MCPP_SYSROOT` 转成 `-L` 或 `QEMU_LD_PREFIX` | +| Swift SDK 包路径辅助 | 包或插件 | 读 `swift-sdk.json` 并打印 `sdkRootPath`,供用户设 `SDKROOT` 或写 `{ path }` | +| `simctl-run` | 不需要改 | 由 `remote-run` 远程调用 | +| `apple-device-tools` 的 Linux 后端 | xim-pkgindex | pymobiledevice3 或 xtool;退出状态参考 xcross 的 gdb-remote 附加做法 | +| `dist-apple` 在 Linux 上签名 | mcpp-plugins | rcodesign | +| `xim:xtool` | xim-pkgindex | MIT;SDK 生成工具与设备后端(生成 SDK 是否需要 Swift,未实测) | +| xcross | 不联动 | AGPL-3.0、没有通用入口、凭据风险;借鉴它的思路 | +| 虚拟机配方 | 只在本地或用户自己的索引 | 永不进官方索引 | + +## 7. 诊断(示例) + +``` +error: mcpp.toml:12: [target.x86_64-linux-gnu].sysroot = { path = "third_party/sysroot" } + resolves to /work/proj/third_party/sysroot, which does not exist. + A sysroot a project names is the project's to provide. Common ways: + · publish it as a package in your index: sysroot = "acme:sysroot@1.0" + · keep it in the repository (submodule/LFS) at that relative path + · agree on a path for the team (e.g. /opt/acme/sysroot) + To use another one on this machine only: MCPP_TARGET_X86_64_LINUX_GNU_SYSROOT=path: + +error: [target.x86_64-linux-gnu].sysroot: gcc 16.1.0's runtime requires glibc >= 2.38 + (libstdc++.so.6: GLIBC_2.38), and this sysroot provides glibc 2.31. + +error: target aarch64-ios needs the iphoneos SDK, which this machine does not provide. + Looked in: SDKROOT (unset). It is not redistributable, so mcpp locates it: + on macOS install Xcode; elsewhere produce one from your own Xcode.xip (e.g. with xtool) + and set SDKROOT, or write [target.aarch64-ios] sysroot = { path = "…" }. + +error: cannot run x86_64-ios-sim artifacts on this host: the iOS Simulator runs only on macOS. + Name an execution host: [target.x86_64-ios-sim] run_under = ["remote-run", "", "--"] + (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine). + +error: the C library is supplied twice: sysroot glibc 2.44 ← … and graph musl ← openkal-musl@0.3.3. +``` + +## 8. 规范修订(条文草案) + +**SPEC-006** + +- §1 "sysroot" 改为 §2.1 的定义,附规范性附录(§2.2 的表)。 +- 新 §3.5 "目标基座",共五款:一次解析;写法与优先级;测量与声明(布局知识仅限 multiarch);不写入;运行时语义。 +- 新 §3.6 "自备输入"。 +- 新 §3.7 "核心知识判据"(D18)。 +- §2.2.1:`sysroot` 是工具链携带的基座;工具链阶段可以陈述 `spec` 加 `sysroot`。 +- §3.1 补全读者名单;§3.3 增加主体 `sysroot` / `run_under`。 +- §4.3 增加:gcc 的 `-specs=` **必须**保留 `%(sysroot_spec)`。 + +**SPEC-002** + +- §1.3 放宽 `prebuilt`;§2.4 标为已实现。 +- 规则一、二、三各增一款(基座是一个供给者;可测量的下限;可叠加层)。 +- 新 §6 "目标侧执行":运行器契约、`run_under` 的组合、物理边界。 + +**SPEC-004** + +- §2 平面:值的来源写明四档"覆盖 > 工程 > 机器默认(用户级、系统级 `config.toml`)> 引擎默认";`sysroot` 与 `run_under` 两个键都可以出现在工程与机器默认中。 +- §4.2:写法与表写法。 +- 键登记表:`target..sysroot`(允许表值)、`target..sysroot.c-abi`、`target..run_under`。 + +**构建档案(docs/04 §2.9,以及它对应的规范条款)**:`trim_paths`(§15.3)与 `ndebug`(§15.4)两个档案键,以及它们在内置档案中的默认值。 + +**用户文档**:docs/20、04、22、10、09、50、25,中英文同步。 + +## 9. 兼容与无感升级 + +- 现有拼写的含义全部不变。 +- 生态默认构建的命令行、产物、输出、指纹逐字节不变。例外有两个: + - gcc 的 ld 新拿到 `--sysroot=`,需要用 CI 逐项比对产物来确认; + - **release / dist 档默认开启 `trim_paths`**,产物里的源文件路径与调试路径会变(§15.3);需要绝对路径的人可以写 `trim_paths = false`。 + - **release / dist 档默认定义 `NDEBUG`**,这些档里的 `assert` 会消失(§15.4);需要保留的人可以写 `ndebug = false`。 + - 这两项都会让 release / dist 档在升级后重建一次,dev 档不受影响;两项都写进 changelog,并在升级后第一次构建时提示一次。 +- 运行器多出来的环境变量,读不懂的运行器会忽略。 +- 新增的拒绝:工具链阶段 `spec` 加路径专属的键(发布前在 mcpp-plugins 与 mcpplibs 里检索用法)。 + +## 10. 多视角自审 + +第一轮与第二轮审查的结论保留在下表的前半部分;第三轮(本次修订)新增的放在后半部分。 + +| 视角 | 发现 | 修订 | +|---|---|---| +| 规范一致 | 新造了出处类别 `carried` / `system` | 沿用 #755 的 `SourceClass` | +| 简洁 | 优先级平铺六级 | 收为四档 | +| 跨平台 | Linux 上 Apple 行被整段编译掉 | 列为缺陷;定位器跨宿主 | +| 跨平台 | 宿主基座下生态 gcc 直接失败(J) | multiarch 一条 | +| 场景 | 宿主基座可有可无? | 生态 glibc 高于多数发行版,所以正式支持 | +| 插件边界 | 工具链阶段拒绝 `spec` 加 `sysroot`,堵住了插件入口 | 改为允许 | +| 正确性 | 远程运行器不知道要传什么;会被并发调用 | 闭包清单与 `MCPP_RUN_ID` | +| **用户心智(第三轮)** | 上一稿把 `run_under` 限定在机器层,用户要学两个文件,团队的约定又写不进工程 | **工程为主,机器为默认,环境变量为覆盖**,两个键同一套层次(D14、§3.7) | +| 用户心智 | 混合团队里,工程写了 `run_under`,Mac 上的同事怎么办? | 混合团队写在各自的 `config.toml`;个别例外用 `…_RUN_UNDER=none`;不给 `cfg` 加宿主谓词(§11) | +| 一致性 | 环境变量里"空"是什么意思,`sysroot` 与 `run_under` 不一致 | 统一:空字符串等于没设置,`none` 表示显式清除 | +| 可用性 | 路径不存在时只报"不存在",开发者不知道怎么办 | §3.8 提示表:三种让它存在的办法,加本机覆盖 | +| 核心边界 | SE-0387 与 `QEMU_LD_PREFIX` 是第三方工具的知识,却放进了核心 | 按 D18 移出,由包提供;`SDKROOT` 是平台约定,保留,并在所有宿主上读取 | +| 事实核对 | 本地配方说"Tahoe 放弃了 Intel" | GitHub 有 `macos-26-intel` 镜像,说明这个说法有误;时间线改为以 Xcode 27 为界(§2.6) | +| 需求真实性 | `x86_64-ios-sim` 值不值得验证? | 构建有真实需求;运行验证可以用 CI 的 Intel job 低成本完成,纳入本 PR;本地虚拟机不做 | +| 范围 | #832 只记在附录,与发布流程脱节 | 纳入同批(§15),有自己的 checkpoint 与验收;它与本设计共享"路径占位符"这套机制(§15.3 与缓存键的相对化同源) | +| 跨仓库 | 上一稿把 xlings 的 argv 形式 exec 当成前置依赖,多了一个仓库的发布 | 索引自己就有 `libs/` 共享库机制,修复只需在 xim-pkgindex 里完成;xlings 的那一项降为可选的优化(§15.2) | +| 测试 | R2 原本用 `assert` 验证路径裁剪,而 release 默认定义 `NDEBUG` 后 `assert` 不再展开 | R2 改用 `__FILE__` 与 `std::source_location`,`NDEBUG` 单独验收(R2b) | +| 风险 | PR 改动面更大了 | 十三个 checkpoint;§15 的几个 checkpoint 与基座部分互相独立,可以单独回滚 | +| 安全 | `run_under` 写进工程,就能执行任意程序 | 与工程的 `runner`、构建程序同等可信,没有新增攻击面;`--no-runner` 可以去掉它 | +| 可测试 | Linux CI 上怎么测 Apple 定位器? | 用假 SDK 目录加 `SDKROOT` 做 plan-only 解析 | +| 遗留 | 生态侧的外部工具都未实测 | 不在引擎 PR 的验收里,列为附录 A 的开放问题 | + +## 11. 不采纳的方案 + +| 方案 | 理由 | +|---|---| +| 只修 §1.2 的缺陷 | 不解决目标轴、托管编译器配自定义基座、多处解析这几个问题 | +| `{ libc = …, path }` | 把形式与层揉在一起;会构成规则一冲突 | +| 新增层来源或出处类别 | 已有的维度够用 | +| 规范化任意布局 | 写入外部树;与驱动行为分叉 | +| 本机自定义基座默认 floor | 本机能否运行就取决于宿主 | +| 废弃 `[toolchain] { sysroot }` | 厂商成对交付是真实存在的形态 | +| 定位器族名叫 `apple-sdk` | 不是业界词汇 | +| **`run_under` 只允许写在机器层**(上一稿) | 工程才是开发者首先面对的地方;团队约定需要写进工程 | +| 机器默认单独放一个文件 | `config.toml` 本来就是机器层,并且分为用户级与系统级 | +| 给清单的 `cfg` 加宿主谓词 | 扩大了条件语法(SPEC-004 §3 "条件化的唯一形状");三层配置已经能表达 | +| **核心读取 Swift SDK 包(SE-0387)** | 第三方生态的格式(D18) | +| **核心设置 `QEMU_LD_PREFIX`** | 工具专属(D18) | +| `[target..sysroot.c-abi]` 块 | sysroot 写在哪一行,呈现的就是那一行的 C 环境;`[c-abi]` 块属于由依赖图构建、呈现非默认环境的 C 库(rev 5) | +| 给 `defines` 加 `!NAME` 取反语法 | 只为改回 `NDEBUG` 扩大清单语法;`ndebug = false` 足够(rev 5) | +| 定位器插件机制(`sysroot = { command = … }`) | 每次构建都要执行外部程序,身份与缓存都会变复杂;自备 SDK 本来就是一次性的手工步骤 | +| 引擎内建虚拟机或 ssh 支持 | 具体绑定 | +| 在 macOS 客体里运行 mcpp | 需要 Intel macOS 的 mcpp 二进制,而且没有必要 | +| 与 xcross 做 xpkg 联动 | AGPL、没有通用入口、凭据风险 | +| 通过 xim 分发 Apple SDK 或 macOS 镜像 | 违反许可证 | +| 用本地虚拟机验证 `x86_64-ios-sim` | 要 100G 磁盘、手工安装、许可证灰区;CI 的 Intel job 更便宜也更干净 | + +## 12. 验收 + +### 12.1 不变量 + +- 三个平台的默认构建:命令行、`resolution.json`(除新增对象外)、输出、指纹逐字节不变(`trim_paths` 只影响 release / dist,单独验收)。 +- gcc 加上 `%(sysroot_spec)` 前后,产物的 DT_NEEDED / PT_INTERP / RUNPATH 逐项相同。 + +### 12.2 单测 + +- 写法与每行拒绝;四档优先级(`sysroot` 与 `run_under` 各一套);环境变量的空值与 `none`。 +- 测量数据表;multiarch;运行时语义。 +- Apple 定位器(假 SDK 目录加 `SDKROOT`,非 Apple 宿主上也能运行);MSVC `{ path }` 的两种布局。 +- 身份与指纹;源码门禁(`src/` 中不出现针对外部树的 `create_*symlink`)。 +- 运行器 argv 的组合;`--no-runner`。 +- §15:前缀映射表的生成与排序;建议命令的引号。 + +### 12.3 e2e + +| 编号 | 内容 | +|---|---| +| S1 | 自定义 gcc 加 custom glibc 基座:构建、运行(用的是基座的 glibc)、pack 下限 | +| S2 | 托管 gcc 加 custom 基座(相对路径写法) | +| S3 | 下限拒绝 | +| S4 | 原地改写基座后重建;默认快速路径不变 | +| S5 | 基座与依赖图的 c-abi 冲突 | +| S6 | 非基座目录被拒;路径不存在时的友好提示 | +| S7 | 覆盖(`MCPP_SYSROOT`、`…_SYSROOT=none`)、`config.toml` 默认、`--managed-only` | +| S8 | 交叉 musl 携带的基座 | +| S9 | 工具链阶段:`spec` 加 `sysroot` 生效;`spec` 加 `prefix` 被拒 | +| S10 | `glibc@system`:gcc(走 multiarch)与 clang | +| S11 | 运行器契约:假运行器经 `run_under`(分别写在 `mcpp.toml` 与 `config.toml`)加 `runner` 嵌套调用;argv、环境变量、闭包清单、退出码;`…_RUN_UNDER=none`;`--no-runner` | +| S12 | Linux 上的 Apple 定位器:假 SDK 加 `SDKROOT` 做 plan-only;缺失时的提示 | +| S13(macOS CI) | `macosx@system`、`@<版本>`、`{ path }` 一致;`iphonesimulator@system` | +| S14(Windows CI) | `msvc@system` 与 `{ path }`(clang 行)一致;cl.exe 行 `{ path }` 被拒 | +| **S15(CI,跨 job)** | macOS arm64 job 用 mcpp 构建 `x86_64-ios-sim` 的测试程序(以 artifact 交出)→ `macos-15-intel` job 用 `xcrun simctl spawn` 运行,断言程序自己的退出码与输出;arm64 job 同时断言 SDK 测量出 x86_64 切片 | +| R1–R4 | §15.5 | + +另外,挑出与基座有关的既有用例(134、179、201、219、295、296、778、875、889、891 等),在新旧二进制上比对。 + +### 12.4 本地 + +用复现脚本跑 A–K。本地虚拟机路线不测(§1.4)。 + +## 13. 单 PR 与 commit checkpoint + +1. 数据:测量表、multiarch 表、目标表的两列、基座数据类型,加单测。 +2. 解析、优先级、定位、测量、缓存;`Toolchain` 字段迁移(行为不变)。 +3. 渲染器、`%(sysroot_spec)`、multiarch。 +4. 产物运行时、rule B、farm 规则。 +5. 下限检查。 +6. 清单与 `config.toml`:写法、`provides`、`c-abi` 块、覆盖、`run_under` 的层次、缺失提示;工具链阶段的修订;拒绝表。 +7. Apple 与 MSVC 定位器,删除 `__APPLE__` 的整段包裹。 +8. 运行器契约(环境变量、闭包清单、组合、物理边界)。 +9. 删除 `sysroot_complete` 与回流特例;报告、`resolution.json`、`mcpp why`、`--managed-only`、pack。 +10. CI:S15 的跨 job 验证。 +11. **§15.1**:建议命令的引号。 +12. **§15.3、§15.4**:`trim_paths`(档案键、映射、渲染、调试映射文件、缓存键)与 `ndebug`;mcpp 自身发布的检查门。 +13. 规范与文档(中英文),e2e S1–S14、R1–R4;**§15.2** 的 e2e 在索引版本跟进之后打开。 + +## 14. 评审决议 + +前几轮的决策点都已经定下,记录如下: + +| # | 问题 | 决议 | +|---|---|---| +| 1 | 机器层包装前缀的键名 | `run_under`(沿用 Bazel 的语义) | +| 2 | 机器默认放在哪里 | 现有的 `config.toml`(用户级与系统级),不另开文件;**`mcpp.toml` 为主** | +| 3 | Swift SDK 包是否进核心 | 不进(D18) | +| 4 | `NDEBUG` | 跟随业界惯例:release / dist 默认定义,`ndebug` 键可以改回(§15.4) | +| 5 | `trim_paths` 中根包的映射 | `@`,与依赖同形(§15.3) | +| 6 | `x86_64-ios-sim` 的层级 | S15 通过后,按层级规则升为 verified | +| 7 | #832 中 home 路径含空格 | 只改 xim-pkgindex(`libs/shell.lua` 加 lint);xlings 的 argv 形式 exec 属于优化,不是依赖(§15.2) | + +目前没有待决的事项。 + +## 15. 同批:mcpp#832 的 mcpp 侧 + +#832 报告了两个问题(发布产物残留 CI 路径;home 路径含空格时失败)。下面是分析后属于 mcpp 的部分, +它们与本设计在同一个发布里交付。 + +### 15.1 建议命令里的路径没加引号 + +`src/home.cppm:273` 的提示把未加引号的路径拼进了建议命令(`mcpp self init --force --home /tmp/mc probe/home`)。 + +- 修正:所有**给用户照抄**的命令都经过同一个辅助函数生成,按平台的 shell 规则(`mcpp::platform::shell::quote`)加引号。 +- 审计:扫一遍 `src/` 中拼进诊断里的命令行。mcpp 自己执行的命令大多已经加了引号,例如 `probe.cppm`。 + +### 15.2 home 路径含空格时,工具链的 hook 失败 + +- **根因不在 mcpp**:xim-pkgindex 的 `pkgs/g/gcc.lua:662` 用 `system.exec(string.format("%s %s --dynamic-linker %s …"))` 拼接字符串,路径没加引号。索引中有 48 个配方使用 `system.exec(string.format(…))`,共 86 处插值,需要逐个审计其中插入路径的那些。 +- **只改 xim-pkgindex,不需要改 xlings**: + - 索引已经有共享库机制:`libs/*.lua`,以 `xim.pkgindex.<名>` 导入,例如 `sysroot` 被 102 个配方使用。`glibc.lua` 里也已经有一个私有的 `__sh_quote`。 + - 新增 `libs/shell.lua`(`xim.pkgindex.shell`),提供 `quote(s)` 与 `exec(argv)`,按宿主的 shell 规则加引号后调用 `system.exec`。 + - 迁移 `gcc.lua` 以及其他插入路径的调用点;`glibc.lua` 的私有实现改为使用它。 + - 新增 `tools/lint_exec_quoting.sh`:`system.exec` / `os.exec` 的格式串中,`%s` 必须经过 `quote()`。 +- **xlings 的 argv 形式 exec 属于优化,不是依赖**:它能彻底绕过 shell,不必再操心各平台的引号规则,可以在 xlings 的路线里单独做;本批不依赖它。 +- **发布顺序**:xim-pkgindex 合入并发布 → mcpp 跟进索引版本 → 打开 R3。 +- **mcpp 侧**:现有 e2e 179 只测了"项目路径带空格"。新增 R3:**全新 home,路径同时含空格与非 ASCII 字符**,三个平台各跑一次默认构建。由于本地沙箱的限制,它只在 CI 上运行。 + +### 15.3 发布产物里的路径:`trim_paths` + +mcpp 自己的发布产物 `strings` 之后能看到 `/home/runner/work/mcpp/mcpp/modules/libs/src/json/json.hpp`, +来源是 json.hpp 中 `assert` 展开的 `__FILE__`。这不只是 mcpp 自己发布配置的问题:**mcpp 作为构建工具, +release / dist 档没有任何路径裁剪**,每个用户的发布产物都会带上他们机器上的绝对路径。 +2026-07-30 的缓存设计里把它列为 S8,后来没有实施,当时的顾虑是调试器需要 `substitute-path`。 + +- **档案键** `trim_paths = true | false`。默认值:`dev` / `debug` 为 false,`release` / `dist` 为 true。对照:Go 的 `-trimpath`、Cargo 的 `trim-paths`(RFC 3127)。 +- **映射表**,与缓存键的 `` / `` 相对化同源(`cache_key.cppm`),从同一处生成: + +| 源前缀 | 映射为 | +|---|---| +| 依赖图中每个包的根 | `@` | +| 根包的根 | `@`(与依赖同形,见下文) | +| 存储根 | `/mcpp/store` | +| home | `/mcpp/home` | +| 构建输出目录 | `/mcpp/build` | +| 工具链根 | `/mcpp/toolchain` | +| **目标基座根** | `/mcpp/sysroot` | + + 规则按最长前缀优先排列。 + +- **渲染**: + - GCC / Clang 用 `-ffile-prefix-map==`,它同时覆盖 `__FILE__`、`std::source_location` 与调试信息; + - clang-cl 用 `/clang:-ffile-prefix-map=`; + - cl.exe 先用探针确认可用的开关(例如 `/d1trimfile:`),没有可用开关时**报告一次"本次未裁剪"**,不能悄悄假装裁剪过。 +- **调试**:开启裁剪、同时带调试信息时,在构建目录写出 `source-map.gdbinit` 与 `source-map.lldbinit`,把映射反过来,消除当初 S8 的顾虑。 +- **缓存键**:映射后的旗标进入缓存键。占位符与机器无关,跨 home 的缓存共享因此更好。 +- **mcpp 自己的发布**:mcpp 的发布用 dist / release 档,自动得到裁剪;发布 CI 增加检查门 `strings -a bin/mcpp | grep -E '/(home|Users|runner|tmp)/'`,有输出就失败。 + +- **根包的映射(已定)**:根包的源码同样映射成 `@`,与依赖同形。`__FILE__`、`assert` 的消息、`std::source_location`、调试信息里的路径都会变成这个样子: + + | 位置 | 不裁剪(dev) | 裁剪后(release / dist) | + |---|---|---| + | 根包的源码 | `/home/alice/work/app/src/main.cpp` | `app@1.2.0/src/main.cpp` | + | 依赖的源码 | `/home/alice/.mcpp/registry/…/fmt/11.0.2/src/format.cc` | `fmt@11.0.2/src/format.cc` | + | sysroot 的头文件 | `/opt/acme-sysroot/usr/include/stdio.h` | `/mcpp/sysroot/usr/include/stdio.h` | + + 这样有三点好处: + - 产物不暴露开发者的目录与用户名; + - 同一份源码在不同目录检出、在不同机器上构建,嵌入的路径完全相同,这是可复现构建的前提; + - 用户看到的崩溃信息或断言里,`app@1.2.0/…` 直接指明是哪个包、哪个版本、哪个文件,比 Cargo 那种相对路径 `./src/main.cpp` 更能分辨根包与依赖。 + + 调试器要找回源码时,加载生成的 `source-map.gdbinit` / `.lldbinit` 即可(每一行就是上表的反向映射)。工作空间的每个成员都是一个包,按各自的 `@` 映射。 + +### 15.4 `NDEBUG`(已定:跟随业界惯例) + +json.hpp 的路径之所以出现,是因为 release 档没有定义 `NDEBUG`,`assert` 还在。路径裁剪能独立解决泄漏问题; +`NDEBUG` 另行决定,结论是跟随业界惯例: + +- 档案键 `ndebug = true | false`。默认值:`dev` / `debug` 为 false;`release` / `dist` 为 true,即定义 `-DNDEBUG`(cl.exe 上是 `/DNDEBUG`)。对照:CMake 的 Release、RelWithDebInfo、MinSizeRel 都定义它;Meson 的 `b_ndebug=if-release`;Cargo 的 release 关闭 `debug_assertions`。 +- 在档里写 `ndebug = false` 即可取消(rev 5:不为 `defines` 引入取反语法)。 +- 这是行为改变:现有 release 构建里的 `assert` 会消失。写进 changelog,并在升级后第一次 release 构建时提示一次("release 档现在定义 NDEBUG;要保留 assert,在 `[profile.release]` 里写 `ndebug = false`")。 +- 它会让 release / dist 档的编译旗标变化,因而指纹变化,升级后这两个档会重建一次。dev 档不受影响。 +- mcpp 自己的发布产物同样受这条规则约束(mcpp.toml 的 `default-profile = "release"`)。 + +### 15.5 验收 + +| 编号 | 内容 | +|---|---| +| R1 | 建议命令:home 路径含空格时,提示中的命令照抄执行能成功(单测加 e2e) | +| R2 | `trim_paths`:release 构建一个用了 `__FILE__`、`std::source_location` 的程序,产物中不出现工程、存储、home、基座的绝对路径,根包显示为 `@/…`;同一份源码在两个不同目录构建,嵌入的路径相同;dev 构建中保留绝对路径;gdb 加载生成的 `source-map.gdbinit` 后能找到源文件;cl.exe 上如实报告 | +| R2b | `ndebug`:release / dist 中 `assert(false)` 不触发、dev 中触发;`ndebug = false` 能改回;升级后的提示只出现一次 | +| R3 | 全新 home 路径含空格与非 ASCII(CI,三平台),在索引版本跟进之后打开 | +| R4 | mcpp 自身发布产物的路径检查门(`.github/tools/check_release_paths.sh`,linux-x86_64 与 macOS) | + +## 附录 A. 讨论记录与结论 + +| # | 问题 | 结论 | +|---|---|---| +| A.1 | sysroot 带自己的 glibc 时能不能用? | 不能:链接失败;补齐后 loader 与 rpath 错误,rule B 误拒(§1) | +| A.2 | 是设计还是 bug? | bug:#755 复用了前提已不成立的 `sysroot_mode`,没有测试 | +| A.3 | sysroot 一定带 libc 吗?要不要 `{ libc, path }`? | 几乎都带,但不只有 libc;不加新键(D1、D7) | +| A.4 | 定位器族名 | 业界词汇:xcrun 的 SDK 名加 `glibc` / `musl` / `msvc`(D3) | +| A.5 | MSVC `{ path }` 的布局 | 两种都支持,按标志目录自动识别(§4.4) | +| A.6 | 运行器的环境变量 | 引擎只给 `MCPP_SYSROOT`;`QEMU_LD_PREFIX` 由 qemu 的包转换(D18) | +| A.7 | `{ path = "/" }` | 支持,写作 `glibc@system`;需要 multiarch(D11) | +| A.8 | Linux 上的 macOS / iOS 模拟器 | 经 macOS 客体或远程 Mac 可以运行;mcpp 只做支持、不分发;本地本轮不测(§1.4) | +| A.9 | x86 是否随 Apple 一起到期 | 不会:目标行看 SDK 切片,执行看执行宿主(D16) | +| A.10 | xtool | 可以做 xpkg,作为 SDK 生成工具与设备后端(§6) | +| A.11 | xcross | 借鉴思路,不联动(§6) | +| A.12 | 引擎与插件的边界 | 核心知识判据(D18) | +| A.13 | 许可证 | 中性机制,不规避(§5.3) | +| A.14 | `config.toml` 还是 `mcpp.toml`? | **`mcpp.toml` 为主**,`config.toml`(用户级与系统级)为默认,环境变量为覆盖,不另开文件;缺失时友好提示(D14、§3.7、§3.8) | +| A.15 | Swift SDK 包要不要进核心? | 不进;`SDKROOT` 跨宿主读取,`{ path }` 兜底,路径辅助由包提供(D18) | +| A.16 | `x86_64-ios-sim` 有没有真实需求? | 运行需求低,构建需求真实;用 CI 的 Intel job 低成本验证,纳入本 PR(§2.6、S15) | +| A.17 | #832 的发布问题有没有放进方案? | 放进同批(§15):引号、`trim_paths`、`ndebug`、home 含空格的 e2e | +| A.18 | 必须改 xlings 吗? | 本次发布不依赖(rev 5:实测发现 xlings 脚本别名未加引号的缺陷,由 xlings#699 修正,索引侧 #1035 让本次链路不经别名)。原结论:不必。`config.toml` 的 `[target.]` 由 mcpp 自己的 `src/config.cppm` 读取,与 xlings 无关;home 路径含空格只需改 xim-pkgindex(索引已有 `libs/` 共享库机制);xlings 的 argv 形式 exec 是可以单独做的优化(§15.2) | +| A.19 | `NDEBUG` | 跟随业界惯例(§15.4) | +| A.20 | 根包映射成 `@` 有什么用? | 产物不暴露开发者的目录;不同目录、不同机器构建出的路径相同(可复现的前提);崩溃与断言信息直接指明包和版本;调试时由生成的 source-map 映射回来(§15.3) | + +**开放问题**(生态侧,不阻塞引擎 PR): + +- xtool 生成 SDK 是否需要 Swift; +- pymobiledevice3 建隧道是否需要 root; +- 用 gdb-remote 附加能否在真机上拿到退出状态; +- 本地虚拟机路线的实际可行性。 diff --git a/.agents/docs/2026-10-23-target-base-and-execution-host-repro.sh b/.agents/docs/2026-10-23-target-base-and-execution-host-repro.sh new file mode 100755 index 000000000..193d8f210 --- /dev/null +++ b/.agents/docs/2026-10-23-target-base-and-execution-host-repro.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Repro for 2026-10-23-target-base-and-execution-host-design.md §1. +# +# Builds, from the payloads an mcpp home already holds: +# toolchain/ gcc 16.1.0 + binutils 2.42.1 in one prefix, pristine specs +# (a "vendor" gcc: not a path inside mcpp's registry) +# xgcc/ aarch64-linux-musl-gcc 16.1.0 (a cross toolchain carrying a sysroot) +# sysroot-glibc244/ glibc 2.44.3 + linux-headers, crosstool-ng layout +# sysroot-glibc231/ Debian 11 glibc 2.31 (only with FETCH_DEBIAN=1; needs network) +# and runs the scenario matrix A-K against $MCPP (J and K drive the compilers directly: +# mcpp has no spelling for a host sysroot yet). +# +# Usage: MCPP=/path/to/mcpp [FETCH_DEBIAN=1] [WORK=/tmp/sysroot-demo] bash this.sh +set -uo pipefail +MCPP=${MCPP:?set MCPP to the mcpp binary under test} +WORK=${WORK:-/tmp/sysroot-demo} +R=${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs +GCC=$R/xim-x-gcc/16.1.0 +BINUTILS=$(ls -d $R/xim-x-binutils/* | tail -1) +GLIBC=$R/xim-x-glibc/2.44.3 +HEADERS=$(ls -d $R/xim-x-linux-headers/* | tail -1) +XGCC=$(ls -d $R/xim-x-aarch64-linux-musl-gcc/* | tail -1) +export MCPP_NO_AUTO_INSTALL=1 MCPP_JOBS=${MCPP_JOBS:-8} + +mkdir -p "$WORK" +T=$WORK/toolchain S=$WORK/sysroot-glibc244 X=$WORK/xgcc + +# ── toolchains ───────────────────────────────────────────────────────────── +rm -rf "$T" "$X" +cp -a "$GCC" "$T"; chmod -R u+w "$T" +rm -f "$T"/lib/gcc/x86_64-linux-gnu/*/specs # the registry copy carries baked specs +mkdir -p "$T/x86_64-linux-gnu/bin" +for t in as ld ld.bfd ar nm ranlib objcopy objdump strip readelf; do + cp -a "$BINUTILS/bin/$t" "$T/bin/$t" + ln -sf "../../bin/$t" "$T/x86_64-linux-gnu/bin/$t" +done +cp -a "$XGCC" "$X"; chmod -R u+w "$X" + +# ── sysroots (crosstool-ng layout: usr/include, usr/lib64, lib64 -> usr/lib64) ─ +layout() { mkdir -p "$1/usr/include" "$1/usr/lib64" "$1/lib" "$1/usr/lib"; ln -sfn usr/lib64 "$1/lib64"; } +relink_scripts() { # $1 sysroot, $2 sed expression; text-only files (-I) + for f in $(grep -lI "GROUP\|INPUT" "$1"/usr/lib64/*.so 2>/dev/null); do sed -i "$2" "$f"; done +} +rm -rf "$S"; layout "$S" +cp -a "$GLIBC/include/." "$HEADERS/include/." "$S/usr/include/" +cp -a "$GLIBC/lib/." "$S/usr/lib64/"; chmod -R u+w "$S" +relink_scripts "$S" "s#$GLIBC/lib/#/lib64/#g" + +if [ "${FETCH_DEBIAN:-0}" = 1 ]; then + O=$WORK/sysroot-glibc231; rm -rf "$O"; mkdir -p "$O/x" "$WORK/dl" + for f in g/glibc/libc6_2.31-13+deb11u11_amd64.deb g/glibc/libc6-dev_2.31-13+deb11u11_amd64.deb \ + l/linux/linux-libc-dev_5.10.223-1_amd64.deb; do + [ -f "$WORK/dl/${f##*/}" ] || curl -sSfL -o "$WORK/dl/${f##*/}" "https://deb.debian.org/debian/pool/main/$f" + dpkg-deb -x "$WORK/dl/${f##*/}" "$O/x" + done + layout "$O" + cp -a "$O/x/usr/include/." "$O/x/usr/include/x86_64-linux-gnu/." "$O/usr/include/" + cp -a "$O/x/lib/x86_64-linux-gnu/." "$O/x/usr/lib/x86_64-linux-gnu/." "$O/usr/lib64/" + rm -rf "$O/x" + relink_scripts "$O" "s#/usr/lib/x86_64-linux-gnu/#/lib64/#g; s#/lib/x86_64-linux-gnu/#/lib64/#g" + for l in $(find "$O/usr/lib64" -maxdepth 1 -type l); do + t=$(readlink "$l"); case $t in /lib/x86_64-linux-gnu/*) ln -sfn "$(basename "$t")" "$l";; esac + done +fi + +# ── sources ──────────────────────────────────────────────────────────────── +GLIBC_MAIN='#include +#include +import std; +int main() { + std::println("compiled against glibc {}.{}", __GLIBC__, __GLIBC_MINOR__); + std::println("running on glibc {}", gnu_get_libc_version()); +#if __GLIBC_PREREQ(2, 42) + std::println("pthread_gettid_np = {}", pthread_gettid_np(pthread_self())); // GLIBC_2.42 +#endif +}' +PLAIN_MAIN='import std; +int main() { std::println("hello from a {}-bit target", sizeof(void*) * 8); }' + +scenario() { # name, main, manifest-tail, [build args] + local d=$WORK/m/$1; rm -rf "$d"; mkdir -p "$d/src" + printf '%s\n' "$2" > "$d/src/main.cpp" + printf '[package]\nname = "m%s"\nversion = "0.1.0"\n\n%s\n' "$1" "$3" > "$d/mcpp.toml" + echo "================ $1 ${4:-}"; sed -n '5,$p' "$d/mcpp.toml" + ( cd "$d" && nice -n 10 "$MCPP" build ${4:-} 2>&1 | grep -v '^\s*\(Index\|Inferred\)' | tail -6 ) + local b; b=$(ls "$d"/target/*/*/bin/m$1 2>/dev/null | head -1) + [ -n "$b" ] || return 0 + file "$b" | cut -d, -f1-2 + readelf -lW "$b" 2>/dev/null | grep -o 'interpreter: [^]]*' + readelf -dW "$b" 2>/dev/null | grep -i 'rpath\|runpath' + file "$b" | grep -q x86-64 && { echo "-- run:"; "$b" 2>&1 | head -3; } +} + +scenario A "$GLIBC_MAIN" "[toolchain.linux] +path = \"$T\"" +scenario B "$GLIBC_MAIN" "[toolchain.linux] +path = \"$T\" +prefix = \"x86_64-linux-gnu-\"" +scenario C "$GLIBC_MAIN" "[toolchain.linux] +path = \"$T\" +sysroot = \"$S\"" +scenario D "$GLIBC_MAIN" "[toolchain.linux] +path = \"$T\" +sysroot = \"$S\" + +[build] +ldflags = [\"-Wl,--sysroot=$S\"]" +scenario E "$GLIBC_MAIN" "[toolchain.linux] +path = \"$T\" +sysroot = \"$S\" + +[build] +ldflags = [\"-Wl,--sysroot=$S\", \"-Wl,--dynamic-linker=$S/lib64/ld-linux-x86-64.so.2\", \"-Wl,-rpath,$S/lib64\"]" +scenario F "$GLIBC_MAIN" "[toolchain] +linux = \"gcc@16.1.0\" + +[target.x86_64-linux-gnu] +sysroot = \"$S\"" +scenario G "$PLAIN_MAIN" "[toolchain.linux] +path = \"$X\" +prefix = \"aarch64-linux-musl-\"" +scenario H "$PLAIN_MAIN" "[toolchain.linux] +path = \"$X\" +prefix = \"aarch64-linux-musl-\" +sysroot = \"$X/aarch64-linux-musl\"" +if [ -d "$WORK/sysroot-glibc231" ]; then + scenario I "$PLAIN_MAIN" "[toolchain.linux] +path = \"$T\" +sysroot = \"$WORK/sysroot-glibc231\"" +fi + +# ── J / K: the host's own root as the sysroot (Debian/Ubuntu multiarch) ───── +# Not through mcpp: `glibc@system` is what the design adds. The ecosystem gcc is +# built without multiarch (-print-multiarch is empty), so J fails until the +# multiarch directories are wired; clang (K) resolves them itself. +LLVM=$(ls -d $R/xim-x-llvm/* 2>/dev/null | tail -1) +TUPLE=$(/usr/bin/gcc -print-multiarch 2>/dev/null); [ -n "$TUPLE" ] || TUPLE=$(uname -m)-linux-gnu +mkdir -p "$WORK/m/J"; printf '%s\n' '#include ' '#include ' \ + 'int main(){printf("compiled against %d.%d, running on %s\n",__GLIBC__,__GLIBC_MINOR__,gnu_get_libc_version());}' > "$WORK/m/J/c.c" +echo "================ J (gcc --sysroot=/)" +"$T/bin/gcc" --sysroot=/ "$WORK/m/J/c.c" -o "$WORK/m/J/plain" 2>&1 | tail -2 +echo "-- with the multiarch directories wired:" +"$T/bin/gcc" --sysroot=/ -B/usr/lib/$TUPLE -L/usr/lib/$TUPLE -L/lib/$TUPLE -isystem /usr/include/$TUPLE \ + -Wl,-rpath-link,/lib/$TUPLE "$WORK/m/J/c.c" -o "$WORK/m/J/wired" 2>&1 | tail -2 && "$WORK/m/J/wired" +if [ -n "$LLVM" ]; then + echo "================ K (clang --sysroot=/)" + "$LLVM/bin/clang" --no-default-config --sysroot=/ "$WORK/m/J/c.c" -o "$WORK/m/J/clang" 2>&1 | tail -2 && "$WORK/m/J/clang" +fi diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 0f7141755..0958a5743 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -335 records. +336 records. ## By subject @@ -108,6 +108,7 @@ Records that declare one. Everything else is listed by date below. ### toolchain +- [目标基座与执行宿主:sysroot 的通用模型、运行器契约、自备输入,以及同批的发布产物路径(#832)](2026-10-23-target-base-and-execution-host-design.md) — landed - [LLVM 23.1.3 Part 2:Linux aarch64 默认工具链与 glibc 生态闭环](2026-10-08-llvm-2313-linux-aarch64-ecosystem-part2-design.md) — active - [LLVM 23.1.3 全平台统一默认:跨仓库联动方案(mcpp × xim-pkgindex)](2026-10-07-llvm-2313-unified-default-cross-repo-design.md) — active @@ -138,6 +139,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-10 +- [目标基座与执行宿主:sysroot 的通用模型、运行器契约、自备输入,以及同批的发布产物路径(#832)](2026-10-23-target-base-and-execution-host-design.md) — landed - [依赖 image 的资源归属、模块扫描边界与锁文件稳定写入](2026-10-21-resource-ownership-module-scan-and-lock-write-design.md) — active - [首次初始化、home 重定向与网络诊断优化设计](2026-10-19-first-run-home-and-network-design.md) — landed - [升级的连续性:打包修订不在使用路径上替换载荷、替换失败不拆掉可用状态、home 中的悬空记录自愈、真实的升级 lane](2026-10-17-upgrade-continuity-design.md) — active diff --git a/.github/tools/check_release_paths.sh b/.github/tools/check_release_paths.sh new file mode 100755 index 000000000..b1d1a1bfb --- /dev/null +++ b/.github/tools/check_release_paths.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# check_release_paths.sh — a released mcpp names no directory of the machine +# that built it (mcpp#832; design 2026-10-23 §15.3, R4). +# +# mcpp's release profile trims the source paths an artifact carries +# (`trim_paths`), so `__FILE__` in an `assert` of a vendored header reads +# `libs@0.1.0/src/json/json.hpp` rather than +# `/home/runner/work/mcpp/mcpp/modules/libs/src/json/json.hpp`. This gate reads +# the shipped binary and fails on the build machine's checkout -- the source +# paths #832 found. It names exactly that directory: the binary legitimately +# carries `/tmp/` and `/home/` as string literals of its own code, and a +# dynamically linked build's loader and run path name the toolchain store, +# which is what `mcpp pack` rewrites, not a source path. +# +# Usage: bash .github/tools/check_release_paths.sh +set -uo pipefail +bin="${1:?usage: check_release_paths.sh }" +[[ -f "$bin" ]] || { echo "FAIL: no binary at $bin"; exit 1; } +roots=() +[[ -n "${GITHUB_WORKSPACE:-}" ]] && roots+=("$GITHUB_WORKSPACE") +[[ -n "${CHECKOUT:-}" ]] && roots+=("$CHECKOUT") +(( ${#roots[@]} > 0 )) || { echo "FAIL: neither GITHUB_WORKSPACE nor CHECKOUT names the checkout"; exit 1; } +fail=0 +for r in "${roots[@]}"; do + hits=$(strings -a "$bin" | grep -F -- "$r" | head -5) + if [[ -n "$hits" ]]; then + echo "FAIL: $bin carries the build machine's path $r:" + sed 's/^/ /' <<<"$hits" + fail=1 + fi +done +[[ $fail -eq 0 ]] && echo "OK: $bin carries no path of the machine that built it (${roots[*]})" +exit $fail diff --git a/.github/workflows/ci-macos-ios.yml b/.github/workflows/ci-macos-ios.yml index 38863e54e..7fe28452c 100644 --- a/.github/workflows/ci-macos-ios.yml +++ b/.github/workflows/ci-macos-ios.yml @@ -367,12 +367,67 @@ jobs: fi echo "ok: mcpp run --target aarch64-ios-sim printed 1-2-3 and exited 0" - # THE THIRD ROW, ON A HOST THAT CANNOT RUN IT. A simulator runs the host's - # architecture and this runner is Apple silicon, so the claim is the - # artefact alone -- which is exactly the `preview` tier the row carries. - - name: "x86_64-ios-sim: the artefact, on a host that cannot run it" + # THE THIRD ROW, BUILT HERE AND RUN ELSEWHERE (design 2026-10-23 §2.6, + # S15). A simulator runs the host's architecture and this runner is + # Apple silicon, so this job proves the artefact and the SDK's slice; the + # `ios-intel-run` job below executes it on an Intel Mac. The row can be + # built because the SDK carries the slice -- a fact read from the SDK, + # not from a vendor's hardware roadmap. + - name: "x86_64-ios-sim: the SDK carries the slice, and the artefact is built" run: | set -euo pipefail cd /tmp/iostest + sdk=$(xcrun --sdk iphonesimulator --show-sdk-path) + grep -E '^targets:.*x86_64-ios-simulator' "$sdk/usr/lib/libSystem.tbd" \ + || { echo "FAIL: the simulator SDK carries no x86_64 slice"; exit 1; } "$MCPP_FRESH" build --target x86_64-ios-sim /tmp/assert-artefact.sh x86_64-ios-sim x86_64 7 18.0 + mkdir -p /tmp/ios-x86 && cp /tmp/iostest/target/x86_64-ios-sim/*/bin/iostest /tmp/ios-x86/ + - uses: actions/upload-artifact@v4 + with: + name: x86_64-ios-sim-iostest + path: /tmp/ios-x86/iostest + retention-days: 3 + + # S15: AN ARTEFACT BUILT ON APPLE SILICON RUNS ON AN INTEL MAC'S SIMULATOR. + # + # "Built here, executed there", with CI's artifact store as the transport. + # This job runs no mcpp -- there is no Intel macOS mcpp, and none is needed: + # it boots an iOS simulator of the host's architecture and spawns the + # program, comparing its own output and exit status. Passing it is what the + # row's `verified` tier states. + ios-intel-run: + name: iOS - x86_64-ios-sim runs on an Intel Mac + needs: ios-engine + runs-on: macos-15-intel + timeout-minutes: 30 + steps: + - uses: actions/download-artifact@v4 + with: + name: x86_64-ios-sim-iostest + path: /tmp/ios-x86 + - name: "x86_64-ios-sim: the simulator runs the artefact and returns its status" + run: | + set -euo pipefail + art=/tmp/ios-x86/iostest + chmod +x "$art" + file "$art" + udid=$(xcrun simctl list devices available -j | python3 -c ' + import json, sys + d = json.load(sys.stdin)["devices"] + for runtime, devs in sorted(d.items(), reverse=True): + if "iOS-18" not in runtime and "iOS-2" not in runtime: continue + for dev in devs: + if dev.get("isAvailable") and "iPhone" in dev["name"]: + print(dev["udid"]); sys.exit(0) + sys.exit(1)') + echo "device $udid" + xcrun simctl boot "$udid" 2>/dev/null || true + xcrun simctl bootstatus "$udid" -b + set +e + out=$(xcrun simctl spawn "$udid" "$art" 2>&1); rc=$? + set -e + printf '%s\n' "$out" + [ "$rc" -eq 0 ] || { echo "FAIL: the program exited $rc in the simulator"; exit 1; } + grep -qx '1-2-3' <<<"$out" || { echo "FAIL: the program's output line '1-2-3' is absent"; exit 1; } + echo "ok: x86_64-ios-sim built on arm64 ran on an Intel simulator and printed 1-2-3" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31b0f3b80..bb05f651a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,6 +221,9 @@ jobs: env -u MCPP_HOME "$ROOT/mcpp" --version | grep -q "$VERSION" # MCPP_HOME should auto-resolve to the extracted root. out=$(env -u MCPP_HOME "$ROOT/bin/mcpp" self env) + # R4 (mcpp#832): the binary pack rebuilt with this release's own + # engine trims the paths it carries. + bash .github/tools/check_release_paths.sh "$ROOT/bin/mcpp" echo "$out" | grep -q "MCPP_HOME *= *$ROOT" - name: Generate source tarball + xpkg.lua via mcpp publish @@ -612,6 +615,9 @@ jobs: tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" "$SMOKE/$WRAPPER/bin/mcpp" --version "$SMOKE/$WRAPPER/mcpp" --version | grep -q "$VERSION" + # R4 (mcpp#832): stage 2 is this release's own engine, and it trims + # the paths the binary carries. + bash .github/tools/check_release_paths.sh "$SMOKE/$WRAPPER/bin/mcpp" - name: Upload macOS artifacts to release uses: softprops/action-gh-release@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index f905a106d..bbd374f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,80 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.10.23.1] - 2026-10-23 + +### Added + +- **The target base.** `[target.].sysroot` takes a package, a locator + (`glibc@system`, `musl@system`, `macosx|iphoneos|iphonesimulator@system|`, + `msvc@…`), a table `{ path = "", provides = [...] }` relative to the file + that writes it, or `""`. One statement is resolved once and every reader uses + it: compile and link lines, the scan, the `std` module, the artifact's loader + and run path, closure validation, the fingerprint, the reports, `mcpp pack` and + the runner. Statements come from `--sysroot` / `MCPP_TARGET__SYSROOT` / + `MCPP_SYSROOT`, then the project, then `[target.]` in `config.toml` + (the user's, then the system's), then the target row. +- The C library a base supplies is measured: the driver says where the C runtime + is, the preprocessor which library and version. Debian multiarch is supplied to + a gcc that does not know it, so `glibc@system` builds with gcc and clang alike. + A base older than the compiler's runtime requires is refused before compiling, + naming both versions (`libstdc++.so.6: GLIBC_2.38` against glibc 2.31). +- A native artifact binds its base: PT_INTERP and RUNPATH name the base's loader + and C library, and closure validation checks against it. The base's identity + enters the fingerprint, so a sysroot rewritten in place rebuilds. For such a + build `mcpp pack` states the highest glibc the program and its packed + libraries need and writes it to `HOST-REQUIREMENTS`. +- `Using sysroot glibc 2.31 ← third_party/sysroot [custom · mcpp.toml:12]`, + `mcpp why sysroot`, and `base` / `execution` in `resolution.json`. + `--managed-only` names a stated base. A new refusal code, `target-base`. +- `SDKROOT` names an Apple SDK on every host, so an SDK produced from one's own + Xcode on Linux is located as one in Xcode is. An SDK whose `libSystem.tbd` + lacks the row's slice is refused naming it. The clang `*-windows-msvc` row + takes `{ path }` for an unpacked Visual Studio tree or a `/winsysroot` layout. +- **The execution host.** `[target.].run_under` (also in `config.toml`, + overridden by `MCPP_TARGET__RUN_UNDER`, `none` clears) is composed in + front of the runner. Runners receive `MCPP_RUN_TRIPLE`, `MCPP_RUN_KIND`, + `MCPP_RUN_MODE`, `MCPP_RUN_ID`, `MCPP_RUN_CLOSURE` (a versioned JSON closure) + and `MCPP_SYSROOT`; their exit status is the program's. An Apple artifact on a + host that is not macOS is refused before anything runs, naming `run_under`. +- A toolchain phase may state `sysroot` beside `spec`. +- Profile keys `ndebug` and `trim_paths`, on by default for `release` and `dist`. + `trim_paths` maps each package's sources to `@/…` and the store, + home, build directory, toolchain and sysroot to `/mcpp/…`, and writes + `source-map.gdbinit` / `.lldbinit` when there is debug information. It + applies to what `build`, `run` and `pack` produce; `mcpp test` keeps paths, + because a test binary finds its fixtures through `__FILE__`. +- `x86_64-ios-sim` is `verified`: CI builds it on Apple silicon and runs it on + an Intel Mac's simulator. + +### Changed + +- **`release` and `dist` define `NDEBUG` and trim source paths.** `assert` + compiles out there, and an artifact no longer carries the directories of the + machine that built it (mcpp#832). Write `ndebug = false` or `trim_paths = + false` under the profile to keep the previous artifact; the first shipping + build after the upgrade says so once. Both profiles rebuild once; `dev` does + not. + +### Fixed + +- A stated sysroot hands the linker `--sysroot` (gcc's clean link specs drop + the driver's), so a sysroot's linker scripts resolve inside it (`cannot find + /lib64/libm.so.6`). The ecosystem's default build is unchanged. +- A stated sysroot no longer produces an artifact that loads the host's loader + and the SubOS farm's C library, and closure validation no longer compares a + stated base with the ecosystem's glibc. +- mcpp no longer writes into a sysroot it does not own (a toolchain named by + path reports its vendor's). +- A toolchain phase that states `spec` with `prefix`, `launcher`, `family` or + `tool.` is refused by name; those keys were dropped without a word. +- A command printed for pasting quotes a path that holds a space + (`mcpp self init --force --home '/tmp/mc probe/home'`, mcpp#832). +- A fresh home whose path holds a space builds the default project (with + openxlings/xim-pkgindex#1032 and #1035). Under a path outside ASCII, gcc's + module mapper cannot precompile `std`; mcpp now names that cause and the two + ways out after the error. + ## [2026.10.22.1] - 2026-10-22 ### Fixed diff --git a/README.md b/README.md index c1152f5fb..57f8bd514 100644 --- a/README.md +++ b/README.md @@ -458,7 +458,7 @@ list` reports for this machine): | `aarch64-linux-android` | the same payload and the same build; ran under qemu-user over the system image's own bionic, which the platform emulator cannot do from an x86_64 host | verified | | `aarch64-ios-sim` | llvm 23 plus the machine's iPhoneSimulator SDK, which mcpp locates rather than installs; ran on a simulator through `simctl-run` ³ | verified | | `aarch64-ios` | the same split for the device; the artefact names the iOS platform, and running it off a device needs a signature the developer owns ³ | preview | -| `x86_64-ios-sim` | the same build; nothing ran it, because a simulator runs the host's architecture and the machine measured was Apple silicon ³ | preview | +| `x86_64-ios-sim` | the same build, on Apple silicon; the artefact ran on an Intel Mac's simulator, handed over by CI ³ | verified | `verified` an image has been built **and run** for the row, qemu and wine included · `preview` it builds and links, and no emulator run has been recorded diff --git a/README.zh-CN.md b/README.zh-CN.md index ac706281f..93ca76c9f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -408,7 +408,7 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`( | `aarch64-linux-android` | 同一个 payload、同样的构建;已在 qemu-user 上配合系统镜像自带的 bionic 运行,平台模拟器无法在 x86_64 宿主上做到这一点 | verified | | `aarch64-ios-sim` | llvm 23 加上本机的 iPhoneSimulator SDK(mcpp 定位该 SDK,不安装它);已通过 `simctl-run` 在模拟器上运行 ³ | verified | | `aarch64-ios` | 真机采用同样的分工;产物标记为 iOS 平台,在设备上运行需要开发者自己的签名 ³ | preview | -| `x86_64-ios-sim` | 同一次构建;尚未运行过,因为模拟器运行宿主的架构,而测量所用的机器是 Apple 芯片 ³ | preview | +| `x86_64-ios-sim` | 同一次构建,在 Apple 芯片上完成;产物由 CI 交给一台 Intel Mac,在其模拟器上运行 ³ | verified | `verified`:该行的镜像已构建**并运行**过,qemu 与 wine 均计入 · `preview`:可以构建和链接,尚无模拟器运行记录 · `planned`:已在词表中登记,尚未接通。针对这类目标的构建会被拒绝,而不会被尝试。 diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index 23c1cffe5..23a38df15 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -1352,6 +1352,39 @@ Exit code 1 keeps its meaning — a test ran and failed — and 0 means every te ran and passed. `--message-format json` carries `"status":"not_run"` and a `reason` on each record, and `not_run` / `not_run_reason` on the summary record (see [50 — Machine-Readable Output](50-machine-output.md)). +#### 2.7.3.2 `sysroot` and `run_under` (2026.10.23.1+) + +Two more keys of `[target.]` describe what the target is built against +and where its artifacts execute: + +```toml +[target.x86_64-linux-gnu] +sysroot = { path = "third_party/sysroot" } # or "xim:glibc@2.39", "glibc@system", "" + +[target.aarch64-ios-sim] +runner = ["simctl-run"] +run_under = ["remote-run", "team-mac", "--"] # reach the machine the artifact runs on +``` + +`sysroot` names the **target base** -- the prebuilt C library and its neighbours +-- as a package, a locator, `{ path, provides }` or `""`; its semantics are in +[20 — Toolchains](20-toolchains.md). `run_under` +is the argv that reaches the **execution host**, composed in front of the +runner: ` ` (Bazel's `--run_under`). +Both keys may also be written in `~/.mcpp/config.toml` as this machine's +default, and overridden per invocation with `MCPP_TARGET__SYSROOT` / +`MCPP_TARGET__RUN_UNDER` (`none` clears; an empty value is unset). A +team that all executes the same way writes `run_under` here; a mixed team +writes it in each machine's `config.toml`. `--no-runner` drops both the runner +and `run_under`. + +A runner reached this way receives `MCPP_RUN_TRIPLE`, `MCPP_RUN_KIND` (`exe` / +`bundle`), `MCPP_RUN_MODE` (`run` / `test`), `MCPP_RUN_ID`, `MCPP_RUN_CLOSURE` +(a versioned JSON naming the artifact, the files it carries and the base) and, +when a base is stated, `MCPP_SYSROOT`. Its exit status is the program's. An +Apple artifact on a host that is not macOS, with no `run_under`, is refused +before anything is spawned, naming this key. + ### 2.8 `[features]` — Features Moved to [06 — Features and Capabilities](06-features-and-capabilities.md), @@ -1446,6 +1479,8 @@ opt = 3 # -O level (a number, or the string "s"/"z") debug = false # -g lto = true # -flto; /GL + /LTCG on cl.exe (note: some packaged gcc builds ship without the LTO plugin) strip = true # -s at link time +ndebug = true # -DNDEBUG for every unit of the graph (default: on for release/dist) +trim_paths = true # -ffile-prefix-map for every unit (default: on for release/dist) # passthrough escape hatch (fixed keys, open values): cflags = ["-fno-plt"] cxxflags = ["-fno-plt"] @@ -1465,6 +1500,22 @@ ldflags = [] - Built-in profiles: `release` (-O2) / `dev`, `debug` (-O0 -g) / `dist` (-O3 + strip; **LTO is not enabled by default**). `[profile.]` can override a built-in definition wholesale. +- **`ndebug` and `trim_paths` (2026.10.23.1+)** follow the industry's convention: on + for `release` and `dist`, off for `dev`, `debug` and a custom profile that does not + state them. `ndebug` defines `NDEBUG` for every unit, so `assert` compiles out, as in + CMake's Release and Meson's `b_ndebug=if-release`. `trim_paths` maps every absolute + prefix an artifact would carry -- in `__FILE__`, `std::source_location`, assertion + messages and debug information -- to what it is: each package's sources to + `@/…` (the root package's too, so a report reads + `app@1.2.0/src/main.cpp`), the store, home, build directory, toolchain and sysroot to + `/mcpp/store`, `/mcpp/home`, `/mcpp/build`, `/mcpp/toolchain` and `/mcpp/sysroot`. Two + checkouts of one tree then embed identical paths. It applies to what `build`, `run` + and `pack` produce, not to `mcpp test`: a test binary runs where it was built and + commonly finds its fixtures through `__FILE__`. A build with debug information + also writes `source-map.gdbinit` and `source-map.lldbinit` beside its outputs, which + map the paths back for a debugger. cl.exe has no documented equivalent and the build + says so once. Write `ndebug = false` or `trim_paths = false` under the profile to keep + the previous artifact; the first shipping build after the upgrade says this once. - **Each profile owns its own build directory.** The resolved profile knobs participate in the fingerprint, so `target//` holds one hash directory per profile and switching between them is incremental instead of a full diff --git a/docs/09-commands-by-scenario.md b/docs/09-commands-by-scenario.md index afef5b656..070743685 100644 --- a/docs/09-commands-by-scenario.md +++ b/docs/09-commands-by-scenario.md @@ -155,6 +155,21 @@ sources: considered: payload xim:cmake@>=3.31 (not installed: overridden) ``` +`mcpp why sysroot` reports the target base (2026.10.23.1+): what was stated +and where, what was measured there, what an artifact loads, and the execution +host's wrapper when there is one: + +``` +$ mcpp why sysroot +sysroot: path:/work/app/third_party/sysroot + address=gnu-sysroot runtime=binding root=/work/app/third_party/sysroot + c-abi=glibc 2.31 + loader=/work/app/third_party/sysroot/lib64/ld-linux-x86-64.so.2 + runtime dir /work/app/third_party/sysroot/lib64 + identity=3d9bbfa57f6acda6 + source: custom · mcpp.toml:12 +``` + `mcpp why deps` lists the resolved dependency graph before the lines of `mcpp.lock` (2026.9.14.2+): every package, the key and the table each request was written with, and a library's link form with the reason for it. A `path` diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index ebb9053fe..d7ac9056f 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -122,6 +122,17 @@ capability=opengl.glx.driver discovery=rpath-of-dispatch satisfying one does not satisfy another. It is written only when there is something to say: an empty file would be a *claim* that nothing is needed. +A package built against a stated target base that loads the target's own C +library (`vendored`, `system`) also states the lowest one it runs on +(2026.10.23.1+): the highest `GLIBC_x.y` the +program and the libraries packed with it need, printed as +`Requires glibc >= 2.38 …` and written as +`capability=c-abi.glibc>=2.38 discovery=PT_INTERP`. The base the program was +built against becomes that floor; `self-contained` ships the base's own loader +and C library instead. The `release` and `dist` profiles that `mcpp pack` +builds trim the source paths the artifact carries and define `NDEBUG` +([04 — `[profile]`](04-mcpp-toml.md)). + ### Mode name compatibility Canonical names are shown above. The old names remain **permanent aliases**: diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index 77ed724df..0c0f0a695 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -877,14 +877,109 @@ Xcode and is not redistributable. So mcpp **locates** it, through `xcrun --sdk --show-sdk-path`, exactly as it has always located the macOS SDK. -That is why these rows carry no `sysroot` entry. That column names a package, -and a located directory is not one. +That is why these rows carry no `sysroot` column entry: that column names a +package, and a located directory is not one. A project names the SDK through +the row's `sysroot` key instead -- `iphonesimulator@system` (the default), +`iphonesimulator@`, or `{ path = "..." }` -- and `SDKROOT` is read on +every host, so an SDK a developer produced from their own Xcode.xip on Linux +(with xtool, for example) is located the way one inside Xcode is. mcpp locates +it and never distributes it. The SDK's own `libSystem.tbd` says which +architectures it carries, and a row whose slice is missing is refused naming it. ```bash mcpp build --target aarch64-ios # resolves llvm@23.1.3 + the iPhoneOS SDK mcpp build --target aarch64-ios-sim # resolves llvm@23.1.3 + the Simulator SDK ``` +## The target base: `sysroot` (2026.10.23.1+) + +A sysroot is not a layer. It is the **prebuilt supplier** of a target's lower +layers ([docs/22](22-target-side.md)): a directory organised the way the target's +driver expects, supplying `c-abi`, usually `kernel-abi`, and possibly `c++-abi`, +`compiler-runtime` and platform libraries. mcpp calls it the **target base** and +keeps the key name `sysroot`. One statement of it is resolved once, and every +reader uses that result: the compile and link lines, the dependency scan, the +`std` module, the artifact's loader and run path, closure validation, the +fingerprint, the reports, `mcpp pack` and the runner. + +### Spellings + +```toml +[target.x86_64-linux-gnu] +sysroot = "xim:glibc@2.39" # a package (a team's private index works too) +sysroot = "glibc@system" # this machine's own root +sysroot = { path = "third_party/sysroot" } # a directory, relative to this file +sysroot = { path = "/opt/exotic", provides = ["mcpp:c-abi=glibc"] } +sysroot = "" # no prebuilt C library + +[target.aarch64-ios-sim] +sysroot = "iphonesimulator@18.5" # a located SDK of a given version +``` + +The locator families are the industry's words: `glibc`, `musl`, `msvc`, and +xcrun's SDK names `macosx`, `iphoneos`, `iphonesimulator`. A family must agree +with the row (`musl@system` on a `-gnu` row is refused, naming the spellings the +row accepts). `glibc@` without a namespace is the package +`xim:glibc@`. `{ path = "/" }` is the same statement as `@system`. +The table has two keys, `path` and `provides`. Which spellings each row takes is +listed in [SPEC-006 appendix A](specs/toolchain-management.md). + +### Precedence of statements + +The first of four tiers applies: + +| Tier | Source | +|---|---| +| override | `--sysroot`; `MCPP_TARGET__SYSROOT`; `MCPP_SYSROOT` (`none` = `""`, empty = unset) | +| project | `[target.].sysroot` in `mcpp.toml`; then `[toolchain] { path, sysroot }`, or a toolchain phase that states `sysroot` | +| machine default | `[target.].sysroot` in `~/.mcpp/config.toml`, then the system `config.toml` | +| engine default | the target row's own column, or the ecosystem's base | + +A team states its base in `mcpp.toml`. A sysroot a project names is the project's +to provide, and when it is missing the error says so and lists three ways to make +it exist: publish it as a package in the team's index, keep it in the repository +at that relative path, or agree on a path for the team. A machine that needs a +different one sets `MCPP_TARGET__SYSROOT` or writes it in its own +`config.toml`. Build programs and host tools are built on the host's own base and +never read a target's. + +### Measurement + +The driver is asked where the C runtime is (`-print-file-name=crt1.o`), and the +preprocessor which C library and which version (`__GLIBC__`, `__BIONIC__`, +`__PICOLIBC__`, `_NEWLIB_VERSION`, `__EMSCRIPTEN__`; musl by its loader). The one +layout rule mcpp knows is Debian multiarch: a gcc built without multiarch support +finds no `crt1.o` in `/usr/lib/`, and mcpp then supplies the tuple's +directories to that driver only. `provides` checks a measurement, or stands in for +one that cannot be made; a directory with neither is a library prefix, and is +refused. + +The compiler's own runtime has a floor. gcc 16's `libstdc++.so.6` needs +`GLIBC_2.38`, so a glibc 2.31 base is refused before anything compiles: + +``` +error: [target.x86_64-linux-gnu].sysroot (mcpp.toml:7): gcc 16.1.0's runtime requires glibc >= 2.38 + (libstdc++.so.6: GLIBC_2.38), and this sysroot provides glibc 2.31. +``` + +### The artifact's runtime + +A native, dynamically linked artifact **binds** its base: PT_INTERP is the base's +loader and RUNPATH names the base's C library directory, so the program runs on +the C library it was compiled against. `glibc@system` binds through the host's +standard loader and adds no RUNPATH. A cross target and the Apple and MSVC rows +treat the base as a **floor**. The base's identity -- its statement, its root and +the size and time of the files it was measured by -- enters the fingerprint, so a +sysroot rewritten in place rebuilds. For such a build `mcpp pack` reports the highest glibc the +program and its packed libraries need and writes it to `HOST-REQUIREMENTS`. + +``` + Using sysroot glibc 2.31 ← third_party/sysroot [custom · mcpp.toml:12] +``` + +`mcpp why sysroot` prints the whole resolution; `resolution.json` carries it as +`base`. mcpp never writes into a sysroot it did not create. + ## The source of each tool (2026.10.1.3+) A build uses a toolchain, the payloads its plugins declare, and the tools those @@ -1409,6 +1504,22 @@ In `cxxflags` the std module's precompile does not see it. `i686-windows-msvc` is not a row of the target table; a project that builds for it declares `[target.i686-windows-msvc]`, as for any custom triple. +## Known Toolchain Limitation: GCC Modules Under a Non-ASCII Path (gcc 16) + +GCC's module mapper protocol (libcody) cannot parse its own encoding of a byte +outside ASCII. When the gcc toolchain lives under such a path -- an mcpp home +under `/home/张三/` or `…/家/` -- every `#include` of the standard library fails +while the `std` module is precompiled: + +``` +error: cannot determine '#include' translation of …/家/…/bits/stdc++.h: + malformed string 'ERROR 'unrecognized \'INCLUDE-TRANSLATE… +``` + +A path with a space builds. mcpp names the cause after that error (2026.10.23.1+); +the remedies are a home on an ASCII path (`mcpp self init --home `) or the +llvm toolchain, whose modules do not use that protocol. + ## The C++ runtime contract (`cxx_runtime`) `cxx_runtime` states what the produced artifact promises about the machine that diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 5aa1f0322..6163b16a2 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -518,7 +518,7 @@ other's rows. | `x86_64-linux-android` | verified | `android-ndk@30.0.16248370` | payload | payload | payload | — | | `aarch64-ios` | preview | `llvm@23.1.3` | — | — | SDK | — | | `aarch64-ios-sim` | verified | `llvm@23.1.3` | — | — | SDK | — | -| `x86_64-ios-sim` | preview | `llvm@23.1.3` | — | — | SDK | — | +| `x86_64-ios-sim` | verified | `llvm@23.1.3` | — | — | SDK | — | | `wasm32-emscripten` | verified | `emsdk@6.0.9` | payload | payload | payload | payload | `payload` a toolchain payload here produces it · `graph` no payload, but a diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 4d613d8b5..7d8d60f69 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1128,15 +1128,24 @@ only what the project and its dependencies supply. `#include ` stops resolving. A kernel or a bootloader wants exactly that, and collapsing the two cases would silently hand such a project the target's C library back. -The value is an xpkg reference or the empty string; a bare name is rejected when -the manifest is parsed, because accepting it would install nothing and then fail -much later naming a missing libc. +The value is a package reference, a locator (`glibc@system`, `iphoneos@18.5`), +a table `{ path = "", provides = [...] }`, or the empty string +(2026.10.23.1+); a bare name is rejected when the manifest is parsed, because +accepting it would install nothing and then fail much later naming a missing +libc. A stated base is a supplier of `c-abi` under rule one: a graph package +that also provides `mcpp:c-abi` is refused, naming both. The payload's C++ +runtime stays usable over a base measured to supply the C library it was +configured for, at a version its runtime accepts (rule two's measurable floor). +[20 — Toolchains](20-toolchains.md) gives the spellings each row takes, their +precedence and what is measured. On an MSVC-ABI row (`*-windows-msvc`) the sysroot is the MSVC toolset, and the value is an msvc spelling (2026.9.24.1+): `msvc@system` (the default when the key is absent), `msvc@` (an installed toolset of that version, else -the payload), or `xim:msvc@` (the payload only). Any other value on -such a row is rejected when the manifest is parsed. See +the payload), or `xim:msvc@` (the payload only). With clang the row +also takes `{ path = "" }` (2026.10.23.1+): an unpacked Visual Studio tree +or clang's `/winsysroot` layout, whose `Windows Kits/10` is used when present. +Any other value on such a row is rejected. See [20 — Clang on the MSVC ABI](20-toolchains.md#clang-on-the-msvc-abi-the-toolset-is-the-sysroot). A build program can ask which C library **payload** supplies the sysroot: diff --git a/docs/25-home-indices-and-store.md b/docs/25-home-indices-and-store.md index cc10dbb78..645e2c780 100644 --- a/docs/25-home-indices-and-store.md +++ b/docs/25-home-indices-and-store.md @@ -183,6 +183,25 @@ the user's that resolves a newer version installs it into the user's home. On Luban OS the system configuration names `/xlings` as well, so a payload the machine already has exists on it once. +`config.toml` (the user's, then the system's) may also state a machine's +defaults for one target (2026.10.23.1+): its base and how its artifacts reach +the machine that runs them. A project's `mcpp.toml` outranks both, and +`MCPP_TARGET__SYSROOT` / `_RUN_UNDER` override all three; a relative +`path` is relative to the `config.toml` that writes it. + +```toml +# ~/.mcpp/config.toml +[target.x86_64-ios-sim] +run_under = ["remote-run", "macvm", "--"] # only this machine reaches a Mac this way + +[target.x86_64-linux-gnu] +sysroot = { path = "/opt/acme/sysroot" } # the team's agreed path, on a machine without it in the repo +``` + +The table takes `sysroot` and `run_under` and nothing else: everything else +about a target belongs to the project. See [20 — Toolchains](20-toolchains.md) +and [04 — mcpp.toml](04-mcpp-toml.md). + ## 5. The provider program `[xlings] binary` chooses the xlings mcpp runs: `"bundled"` (the one mcpp diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 7fb52c80a..1a3c0771c 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -392,7 +392,27 @@ Each entry is `{subject, value, class, origin, decidedFor, considered[]}`: ``` Every build writes the same record into -`target///resolution.json` under `sources`. +`target///resolution.json` under `sources`. The subject +`sysroot` records the target base's statement; `run_under` records the +execution host's wrapper. `mcpp why sysroot --format json` is +`mcpp why sources sysroot` (2026.10.23.1+). + +`resolution.json` also carries two objects that describe the target base and +the execution host (2026.10.23.1+), on every build: + +```jsonc +"base": { "form": "path", "statement": "path:/work/app/third_party/sysroot", + "address": "gnu-sysroot", "runtime": "binding", + "root": "/work/app/third_party/sysroot", + "c_abi": { "impl": "glibc", "version": "2.31" }, "multiarch": "", + "loader": "/work/app/third_party/sysroot/lib64/ld-linux-x86-64.so.2", + "runtime_dirs": ["/work/app/third_party/sysroot/lib64"], + "identity": "3d9bbfa57f6acda6" }, +"execution": { "run_under": ["remote-run", "team-mac", "--"], "origin": "mcpp.toml:15" } +``` + +`form` is `default` for the ecosystem's own base, and then the other fields +describe what the engine resolved by default. ### `mcpp.why.build-programs` and `mcpp.why.compat` *(mcpp 2026.10.11.1+)* @@ -511,7 +531,8 @@ a program classifying the outcome reads `reason`: | `payload-override` | an `[xlings.overrides]` entry names a path that does not exist, a version a requirement refuses, or is stated by a dependency *(2026.10.1.3+)* | | `payload-request` | a build program asked for a payload no manifest of this build declares `provision = "on-request"`, or asked again in three consecutive runs *(2026.10.1.3+)* | | `lto-export-discovery` | a PE shared library whose exports are discovered would be compiled by cl.exe into `/GL` objects, which hold no symbol table: `windows_auto_export = true` stated under `lto = true`, or `/GL` written into the flags of a package it links *(2026.10.5.2+)* | -| `local-toolchain` | a toolchain named by path, or stated by a build program's toolchain phase, cannot be used: no driver, a contradicting family, a missing tool or sysroot *(2026.10.1.3+)* | +| `local-toolchain` | a toolchain named by path, or stated by a build program's toolchain phase, cannot be used: no driver, a contradicting family, a missing tool, or `spec` stated with a key only a toolchain named by path has *(2026.10.1.3+)* | +| `target-base` | the target base (`sysroot`) cannot serve this build: a spelling the row does not take, a path that does not exist, a directory that supplies no C library, a C library older than the compiler's runtime requires, a native base without its loader, or a second C library supplier in the graph *(2026.10.23.1+)* | | `convention-unreplaced` | the convention was overridden and nothing replaced it | | `os-mismatch` | the requested and resolved triples name different systems | | `layer-requirement` | a package requires a layer the resolution did not give it | diff --git a/docs/specs/README.md b/docs/specs/README.md index 8bde8f09f..1868d1b23 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -31,11 +31,11 @@ | 编号 | 标题 | 状态 | 最后修改 | 对应实现 | |---|---|---|---|---| | [SPEC-001](package-identity.md) | 包身份(`package.namespace` / `package.name`)、`[dependencies]` 选择器与匹配机制 | 评审中 v1.1 | 2026-08-03 | mcpp >= 0.0.106 | -| [SPEC-002](target-side.md) | 目标侧模型与能力声明(`mcpp:` 保留命名空间、五层、三条规则) | 评审中 v1.0 | 2026-08-24 | mcpp >= 2026.8.24.2 | +| [SPEC-002](target-side.md) | 目标侧模型与能力声明(`mcpp:` 保留命名空间、五层、三条规则、目标侧执行) | 评审中 v1.2 | 2026-10-23 | mcpp >= 2026.8.24.2;§2.4 与 §6 mcpp >= 2026.10.23.1 | | [SPEC-003](exit-codes.md) | 退出码契约(分类、语义、稳定性承诺) | 评审中 v1.0 | 2026-09-01 | mcpp >= 2026.9.1.1 | -| [SPEC-004](manifest-semantics.md) | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | 草案 v1.15 | 2026-10-11 | 条件化形状 mcpp >= 2026.8.29.1;目标轴 mcpp >= 2026.9.6.4;`linkage` 默认值 mcpp >= 2026.9.15.2;链接 flag 的词读法 mcpp >= 2026.9.26.2;条件化的 `dialect_cxxflags` 与 `-p` 的包身份 mcpp >= 2026.9.28.1;条件表按具体程度生效 mcpp >= 2026.9.28.2;平台前缀规约 mcpp >= 2026.10.5.1;根包的继承、根位置的 profile、链接 flag 的作用域、链接期优化与导出发现 mcpp >= 2026.10.5.2;架构段的 `x86` 即 `i686` mcpp >= 2026.10.5.3;前缀即语义与工作空间层 mcpp >= 2026.10.10.1;依赖的库与配置(§12) mcpp >= 2026.10.11.1 | +| [SPEC-004](manifest-semantics.md) | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | 草案 v1.17 | 2026-10-23 | 条件化形状 mcpp >= 2026.8.29.1;目标轴 mcpp >= 2026.9.6.4;`linkage` 默认值 mcpp >= 2026.9.15.2;链接 flag 的词读法 mcpp >= 2026.9.26.2;条件化的 `dialect_cxxflags` 与 `-p` 的包身份 mcpp >= 2026.9.28.1;条件表按具体程度生效 mcpp >= 2026.9.28.2;平台前缀规约 mcpp >= 2026.10.5.1;根包的继承、根位置的 profile、链接 flag 的作用域、链接期优化与导出发现 mcpp >= 2026.10.5.2;架构段的 `x86` 即 `i686` mcpp >= 2026.10.5.3;前缀即语义与工作空间层 mcpp >= 2026.10.10.1;依赖的库与配置(§12) mcpp >= 2026.10.11.1;值的四档层次与 `run_under` mcpp >= 2026.10.23.1 | | [SPEC-005](build-database.md) | 构建数据库:`mcpp emit build-database` 的内容、取值规则与不写工程目录的保证 | 评审中 v1.8 | 2026-10-11 | mcpp >= 2026.9.15.1;v1.3 条款 mcpp >= 2026.9.26.2;v1.4 条款 mcpp >= 2026.9.27.1;v1.5 条款 mcpp >= 2026.9.28.1;v1.6 条款 mcpp >= 2026.9.29.5;v1.7 条款 mcpp >= 2026.9.30.2;v1.8 条款 mcpp >= 2026.10.11.1 | -| [SPEC-006](toolchain-management.md) | 工具链管理:身份、来源、选择与载荷契约 | 草案 v0.7 | 2026-10-09 | 逐条标注;已实现条款 mcpp >= 2026.9.24.1;§3.7 mcpp >= 2026.9.28.1;§3.7.1 mcpp >= 2026.9.28.2;§2.2.1 与 §3.3 的非缺省来源 mcpp >= 2026.10.1.3;§3.8 mcpp >= 2026.10.8.1 | +| [SPEC-006](toolchain-management.md) | 工具链管理:身份、来源、选择与载荷契约 | 草案 v0.10 | 2026-10-23 | 逐条标注;已实现条款 mcpp >= 2026.9.24.1;§3.7 mcpp >= 2026.9.28.1;§3.7.1 mcpp >= 2026.9.28.2;§2.2.1 与 §3.3 的非缺省来源 mcpp >= 2026.10.1.3;§3.8 mcpp >= 2026.10.8.1;§3.9 至 §3.11 mcpp >= 2026.10.23.1 | | [SPEC-007](build-plugins.md) | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | 草案 v0.9 | 2026-10-11 | 逐条标注;mcpp >= 2026.9.26.2;v0.3 条款 mcpp >= 2026.9.27.1;v0.4 条款 mcpp >= 2026.9.28.1;v0.5 条款 mcpp >= 2026.9.28.2;v0.6(§9)mcpp >= 2026.9.28.3;R3.9、R3.10 mcpp >= 2026.10.10.1;§10 mcpp >= 2026.10.11.1 | | [SPEC-008](library-interface.md) | 库的接口:公开模块、发布闭包与两种形态的一致 | 草案 v0.2 | 2026-10-11 | 第一阶段(只警告)mcpp >= 2026.9.28.3;v0.2 mcpp >= 2026.10.11.1 | | [SPEC-009](toolchain-maintenance.md) | 工具链的支持与维护:版本线、默认值、来源、移动与退役 | 草案 v0.6 | 2026-10-09 | 逐条标注;本版只有规范,多数条款未实现;§6.4 mcpp >= 2026.10.8.1 | diff --git a/docs/specs/manifest-semantics.md b/docs/specs/manifest-semantics.md index 04ed787c4..19e1b1edb 100644 --- a/docs/specs/manifest-semantics.md +++ b/docs/specs/manifest-semantics.md @@ -5,11 +5,11 @@ | **规范编号** | SPEC-004 | | **标题** | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | | **状态** | **草案(Draft)** | -| **版本** | 1.16 | -| **最后修改** | 2026-10-21 | +| **版本** | 1.17 | +| **最后修改** | 2026-10-23 | | **最低实现版本** | 条件化形状:mcpp **2026.8.29.1**(`[target..build-dependencies]` 起齐备);目标轴:mcpp **2026.9.6.4** | | **作者/维护** | mcpp-community | -| **相关设计文档** | `.agents/docs/2026-09-07-mcpp-toml-unified-semantics-design.md`
`.agents/docs/2026-06-04-manifest-schema-ownership.md`
`.agents/docs/2026-09-03-xlings-workspace-as-the-one-table.md`
`.agents/docs/2026-09-25-issue-690-workspace-build-inheritance-consistency.md`
`.agents/docs/2026-09-27-eight-reports-by-home-and-one-optimisation-plan.md`
`.agents/docs/2026-10-05-std-module-pair-msvc-lto-and-export-discovery-design.md`
`.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md`
`.agents/docs/2026-10-10-one-source-one-meaning-design.md`
`.agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md` | +| **相关设计文档** | `.agents/docs/2026-09-07-mcpp-toml-unified-semantics-design.md`
`.agents/docs/2026-06-04-manifest-schema-ownership.md`
`.agents/docs/2026-09-03-xlings-workspace-as-the-one-table.md`
`.agents/docs/2026-09-25-issue-690-workspace-build-inheritance-consistency.md`
`.agents/docs/2026-09-27-eight-reports-by-home-and-one-optimisation-plan.md`
`.agents/docs/2026-10-05-std-module-pair-msvc-lto-and-export-discovery-design.md`
`.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md`
`.agents/docs/2026-10-10-one-source-one-meaning-design.md`
`.agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md`
`.agents/docs/2026-10-23-target-base-and-execution-host-design.md` | | **相关使用文档** | [docs/04 —— mcpp.toml 字段参考](../04-mcpp-toml.md) | ## 规范用语 @@ -64,6 +64,12 @@ 谓词,既不改变作用域,也不改变来源。键的类别只陈述一次(`mcpp.manifest.key_registry`), 解析器的已知键检查从中取得。 +**值的层次。** 同一个键的值按四档取第一个:覆盖(命令行与环境变量)、工程(`mcpp.toml`, +含工作空间层)、机器默认(`~/.mcpp/config.toml`,其次系统级 `config.toml`)、引擎默认。 +`[target.<三元组>].sysroot` 与 `[target.<三元组>].run_under` 两个键**可以**同时出现在工程与 +机器默认中;其余键只属于工程。环境变量中空值视为未设置,`none` 表示显式清除。 +机器默认**禁止**放宽工程的约束:`requires` 与版本下限照样核对。 + **库依赖与工具是两个平面,不是一个。** 库有模块与 ABI,参与解析与链接;载荷是可执行 的工具或被编译对着的输入,不参与模块图。二者的解析规则不同(§4),因此**禁止**把工具 写进 `[dependencies]`,也**禁止**把 mcpp 包写进 `[xlings]`。 @@ -184,10 +190,17 @@ feature-deps feature-xlings ← 限定词是门 | 顶层 `[xlings]`,值写成平台键对象 `{ linux=…, macosx=…, windows=…, default=… }` | 宿主 | **已实现** | | `[target..xlings…]` | 目标 | **已实现**(2026.9.6.4) | | `[target.].sysroot` | 目标 | **已实现** | +| `[target.].run_under` | 目标(执行宿主) | **已实现**(2026.10.23.1) | 顶层平台键对象**不是**遗留拼法,它是宿主轴**正确**的写法:工具必须能在这台机器上 执行。`[target.].sysroot` 是目标轴 xpkg 引用的既有先例。 +`sysroot` 的值**可以**是字符串(生态包、定位器或 `""`),也**可以**是表 +`{ path = "<目录>", provides = ["mcpp:c-abi=<实现>"] }`;表只有这两个键,`path` 相对于写它的 +文件,`~` 展开为用户目录。各写法的语义与每行接受的写法见 SPEC-006 §3.9 与附录 A。 +`run_under` 是字符串数组,在 `runner` 之前组合(SPEC-002 §6)。两个键都是配置类键,从构建的根读取; +宿主轴的子构建**禁止**读取它们。 + ### 4.3 两条轴的写法 `[target..xlings.workspace]` 与 @@ -740,3 +753,4 @@ mcpp 2026.10.10.1,#785、#786;第 14 至 16 条与第 13 条的依赖位置为 m | 1.14 | 2026-10-10 | mcpp 2026.10.10.1(#785、#786):§2 陈述键的作用域与来源;§9 新增第 12 条前缀即语义(工作空间层 `[workspace.X]`、按键合并、W7)与第 13 条按位置读取的兼容期;第 7 条发布写出工作空间层;第 8 条改为 `[workspace.xlings]`;第 10 条按求值后的配置分组。 | | 1.15 | 2026-10-11 | mcpp 2026.10.11.1(#797):新增 §12 依赖的库与配置(两条轴、实例、一个包一个版本、按配置激活特性、`host-module` 的含义与默认值、构建程序图、构建插件、兼容读法);§9 第 13 条的移除版本改为 mcpp 2027.1,并补依赖位置;新增 §9 第 14 至 16 条(`[xlings.overrides]` 属于构建的根、`[workspace.dependencies]` 的继承、虚拟根旁的 `build.mcpp`)。 | | 1.16 | 2026-10-21 | 新增 §13,明确依赖 EXE、DLL 的资源只进入其所属包的 image,且按计划内目标校验 manifest 冲突(#826)。 | +| 1.17 | 2026-10-23 | mcpp 2026.10.23.1:§2 陈述值的四档层次,`sysroot` 与 `run_under` 可出现在工程与机器默认中;§4.2 增加 `run_under`,陈述 `sysroot` 的表写法。 | diff --git a/docs/specs/target-side.md b/docs/specs/target-side.md index 9ddfb59aa..0b099ba9b 100644 --- a/docs/specs/target-side.md +++ b/docs/specs/target-side.md @@ -4,10 +4,10 @@ |---|---| | 规范编号 | SPEC-002 | | 标题 | 目标侧模型与能力声明(`mcpp:` 保留命名空间) | -| 状态 | 评审中 v1.0 | -| 最后修改 | 2026-08-24 | +| 状态 | 评审中 v1.2 | +| 最后修改 | 2026-10-23 | | 对应实现 | mcpp >= 2026.8.24.2 | -| 相关设计文档 | `.agents/docs/2026-08-24-target-side-design.md` | +| 相关设计文档 | `.agents/docs/2026-08-24-target-side-design.md`、`.agents/docs/2026-10-23-target-base-and-execution-host-design.md` | | 使用文档 | [docs/22 - 目标侧](../zh/22-target-side.md) | 本规范定义一次构建的目标侧由哪些层构成、每一层可以由谁供给、 @@ -46,7 +46,7 @@ | 来源 | 报告用词 | 可知时刻 | |---|---|---| | 编译器载荷 | `payload` | 依赖解析之前 | -| 被点名的预制载荷 | `prebuilt` | 依赖解析之前 | +| 被点名、按内容识别的预制目录(目标基座,SPEC-006 §3.9) | `prebuilt` | 依赖解析之前 | | 依赖图中的包 | `graph` | 依赖解析之后 | | 无 | `—` | — | @@ -92,14 +92,13 @@ mcpp:<层名>[=<实现名>] **部分实现**:`std-module` 与 `std-compat-module` 是单一路径,不可条件化; 仅 `std-module-flags` 参与条件合并。 -### 2.4 预制载荷的声明 未实现 +### 2.4 预制目录的声明 已实现 -预制载荷的描述符**应当**能够声明它供给哪一层、该层在载荷内的位置、 -以及该层的真实版本。当前实现从载荷的包名切出接口名, -因此一个名为 `musl-gcc` 的载荷在供给 `c-abi` 时被报告为 `musl-gcc` 而非 `musl`。 - -在其落地之前,预制载荷供给的层由 `[target.<三元组>].sysroot` 与目标表的 -`sysroot` 列点名,接口名取包名。 +一个预制目录供给哪一层、实现与版本,**必须**由测量回答(SPEC-006 §3.9.3): +驱动预处理探针翻译单元得到 C 库的实现与版本。测量回答不了时, +`[target.<三元组>].sysroot = { path = "…", provides = ["mcpp:c-abi=<实现>"] }` +的 `provides` 作为声明;测量回答了时,`provides` 用于核对。 +由包名点名的预制载荷,接口名仍取包名。 --- @@ -113,11 +112,16 @@ mcpp:<层名>[=<实现名>] 同一个包在 `provides` 中为同一层给出多条条目时,视为一个供给者; 其中命名了实现名的一条**必须**优先。 +目标基座是一个供给者:它供给 `c-abi` 时,依赖图中再有 `c-abi` 的供给者,**必须**按本规则报错。 +一个目录作为 `--sysroot` 寻址,当且仅当它供给 `c-abi`(c-abi 独占律)。 + ### 3.2 规则二:为其下方的层配置过 部分实现 一个实现**必须**在它曾被配置的层之上使用。引擎**必须**执行: -- 编译器载荷的 `c++-abi`,仅在 `c-abi` 同样来自该载荷时可用; +- 编译器载荷的 `c++-abi`,仅在 `c-abi` 同样来自该载荷,或来自一个被测量为同一实现的目标基座时可用; +- 后一种情形下,编译器运行库所需的 C 库版本(其 version need 中最高的 `GLIBC_x.y`)**禁止**高于基座的版本, + 违反时**必须**在编译前拒绝并说出两个版本(可测量的下限); - §2.2 声明的每一条 `requires`。 「`compiler-runtime` 必须与 `compiler` 同族」当前**依赖供给者自行声明 @@ -132,6 +136,9 @@ mcpp:<层名>[=<实现名>] 当前实现覆盖「两层均来自图」与「两层均来自载荷」。 「一层预制、一层来自图」的接线尚不完整。 +可叠加的层(内核头文件、第三方头文件与库)由引擎以 `-isystem` / `-L` 接线;本机的目标基座缺少 +内核头文件时,引擎**必须**补上生态的内核头文件,而**禁止**写入基座。 + `c-abi` 层来自图时,链接的库搜索同样属于图:引擎**禁止**让驱动器从宿主推导库目录, 这样的链接上每一个 `-L` **必须**位于工具链存储、构建目录或图中某个包之内; `[build] allow_host_libs = true` 解除这一约束。由 clang 链接的 ELF 目标已实现 @@ -219,9 +226,45 @@ mcpp:<层名>[=<实现名>] --- +## 6. 目标侧执行 已实现 + +### 6.1 组合 + +一个产物**必须**按 ` <产物> <参数>` 执行。`runner` 说明用什么执行(模拟器、 +运行器包),`run_under` 说明经过什么到达执行宿主(Bazel `--run_under` 的语义)。`run_under` 的值按 +覆盖(`MCPP_TARGET_<三元组>_RUN_UNDER`,空值视为未设置,`none` 表示清除)、工程(`[target.<三元组>].run_under`)、 +机器默认(`config.toml` 的 `[target.<三元组>].run_under`)取第一个;没有引擎默认。`--no-runner` 表示该三元组 +在本机是原生的,`runner` 与 `run_under` 都**必须**去掉。两者的程序都**必须**先在声明的载荷 `bin/` 中查找, +再在 `PATH` 上查找。 + +### 6.2 运行器契约 + +经过 `runner` 或 `run_under` 执行时,引擎**必须**设置: + +| 变量 | 含义 | +|---|---| +| `MCPP_RUN_TRIPLE` | 目标,mcpp 的规范拼写 | +| `MCPP_RUN_KIND` | `exe`;产物是目录时为 `bundle` | +| `MCPP_RUN_MODE` | `run` 或 `test` | +| `MCPP_RUN_ID` | 每次执行唯一;运行器可能被并发调用 | +| `MCPP_RUN_CLOSURE` | 运行闭包清单的路径:带 `schema` 版本的 JSON,含产物、它从自身目录加载或读取的文件、基座的根 | +| `MCPP_SYSROOT` | 被陈述的目标基座的根,没有时不设置 | +| `MCPP_RUNTIME_FILES` | 既有的文件清单(TSV),保留 | + +运行器**可以**包装运行器,argv **必须**原样传递;运行器的退出码即程序的退出码。工具专属的变量 +(例如 `QEMU_LD_PREFIX`)**禁止**由引擎设置,由对应运行器的包从 `MCPP_SYSROOT` 转换(SPEC-006 §3.11)。 + +### 6.3 物理边界 + +本机没有任何路径能执行的产物,**必须**在执行前被拒绝,并给出 `run_under` 的写法。当前的判据: +非 macOS 宿主上的 Apple 产物,且没有 `run_under`。`mcpp test` 把这种情形报告为未运行。 + +--- + ## 变更记录 | 版本 | 日期 | 变更 | |---|---|---| | v1.0 | 2026-08-24 | 初版。五层闭集、`provides`/`requires` 语法、三条规则、报告与兼容性条款。 | | v1.1 | 2026-09-26 | 规则三增补:`c-abi` 来自图时,链接不搜索宿主库目录(ELF 已实现,mcpp#696)。 | +| v1.2 | 2026-10-23 | 随 mcpp 2026.10.23.1:§1.3 `prebuilt` 放宽为被点名、按内容识别的预制目录;§2.4 改为由测量回答、`provides` 声明或核对,标为已实现;规则一增加基座是一个供给者与 c-abi 独占律;规则二增加基座上的编译器运行库与可测量的下限;规则三增加可叠加层;新增 §6 目标侧执行(组合、运行器契约、物理边界)。 | diff --git a/docs/specs/toolchain-management.md b/docs/specs/toolchain-management.md index 5231cbcc5..ff8925b59 100644 --- a/docs/specs/toolchain-management.md +++ b/docs/specs/toolchain-management.md @@ -4,11 +4,11 @@ |---|---| | 规范编号 | SPEC-006 | | 标题 | 工具链管理:身份、来源、选择与载荷契约 | -| 状态 | 草案 v0.9 | -| 最后修改 | 2026-10-17 | +| 状态 | 草案 v0.10 | +| 最后修改 | 2026-10-23 | | 对应实现 | 逐条标注;标为「已实现」的条款对应 mcpp >= 2026.9.24.1。标为「未实现」的条款计划与下一批 LLVM 工具链一同落地,届时按实测修订本规范 | -| 相关设计文档 | `.agents/docs/2026-09-24-toolchain-selection-and-payload-trust-design.md`、`.agents/docs/2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md`、`.agents/docs/2026-09-28-ecosystem-design-and-optimisation-plan.md`、`.agents/docs/2026-10-02-pr-ci-acceleration-and-the-toolchain-specification-design.md` | -| 相关 issue | mcpp#685、mcpp#687、mcpp#718、mcpp#755 | +| 相关设计文档 | `.agents/docs/2026-10-23-target-base-and-execution-host-design.md`、`.agents/docs/2026-09-24-toolchain-selection-and-payload-trust-design.md`、`.agents/docs/2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md`、`.agents/docs/2026-09-28-ecosystem-design-and-optimisation-plan.md`、`.agents/docs/2026-10-02-pr-ci-acceleration-and-the-toolchain-specification-design.md` | +| 相关 issue | mcpp#685、mcpp#687、mcpp#718、mcpp#755、mcpp#832 | | 使用文档 | [docs/20 - 工具链](../zh/20-toolchains.md)、[docs/32 - 编写载荷](../zh/32-authoring-a-payload.md)、[docs/91 - 工具链内部](../zh/91-toolchain-internals.md) | 本规范定义 mcpp 对工具链的命名、选择和使用方式,以及一个工具链载荷在发布前必须满足的条件。 @@ -24,8 +24,9 @@ |---|---| | 工具链 | 执行编译的程序及其随附部分:编译器驱动、编译器运行时(builtins 与展开器)、随编译器发布的 C++ 标准库、汇编器、链接器与归档工具 | | 载荷 | 由 xlings 从索引安装的预构建目录树,身份为命名空间、名字与版本 | -| 来源 | 工具链或 sysroot 的出处:`managed`(生态包)或 `system`(在本机定位) | -| sysroot | 编译时所针对的目标环境。Linux 目标上是 C 库载荷;MSVC ABI 目标上是 MSVC toolset(STL、vcruntime、CRT)及其 Windows SDK | +| 来源 | 工具链或目标基座的出处,取 mcpp#755 的出处类别:`managed`、`pinned`、`custom`、`program`、`host` | +| 目标基座(sysroot) | 目标侧下层的预制供给者:一个按目标驱动约定组织的目录,供给 `c-abi`,通常还有 `kernel-abi`,可能还有 `c++-abi`、`compiler-runtime` 与平台库。它是一种交付形式,不是 [SPEC-002](target-side.md) 的层。清单键名为 `sysroot`。各目标行的含义见附录 A | +| 执行宿主 | 产物被执行的机器:本机、本机上的虚拟机客体、远程机器或设备。由 `runner` 与 `run_under` 描述(SPEC-002 §6) | | 构建环境 | 产出一个载荷的机器或容器,以及其中的 C 库 | --- @@ -45,7 +46,8 @@ ### 2.2.1 按路径命名 已实现 工具链**可以**写成一个表,由路径命名本机已有的一份: -`[toolchain] <键> = { path = "<目录>", prefix, sysroot, family, launcher, tools }`, +`[toolchain] <键> = { path = "<目录>", prefix, sysroot, family, launcher, tools }`。 +其中 `sysroot` 是这套工具链携带的目标基座,按 §3.9 的第二档参与基座的解析。 或 `MCPP_TOOLCHAIN=path:<目录>`。`<目录>/bin` 中的驱动决定族(`clang++` 为 llvm, `g++` 为 gcc);其余属性由探测该驱动得到。 @@ -61,7 +63,9 @@ `[toolchain] bootstrap = "<族>@<版本>"` 命名编译并运行构建程序的工具链;未写时按 §3 的 规则决定。`[toolchain] <键> = { configure = "build.mcpp" }` 把构建工具链的选择交给根构建 -程序的工具链阶段(SPEC-007 R9.9)。 +程序的工具链阶段(SPEC-007 R9.9)。工具链阶段**可以**同时陈述 `spec` 与 `sysroot`,由程序 +陈述托管工具链的目标基座(出处类别 `program`);`spec` 与 `prefix`、`launcher`、`family`、 +`tool.<角色>` 同时出现时**必须**被拒绝,拒绝信息点名这些键。 **状态:已实现**(mcpp 2026.10.1.3,mcpp#755)。 @@ -87,7 +91,8 @@ ### 3.1 一次选择 已实现 -一次构建中,工具链及其 sysroot **必须**只解析一次;编译、依赖扫描、标准库模块、链接与缓存键**必须**读取同一个结果。 +一次构建中,工具链及其目标基座**必须**只解析一次;编译、依赖扫描、标准库模块、驱动与链接器的链接、 +产物的运行时、运行时闭包校验、hermetic 检查、缓存键与指纹、报告、`mcpp pack` 与运行器**必须**读取同一个结果(§3.9)。 clang 以 MSVC ABI 为目标时,所选 toolset 与 SDK 以 `-Xmicrosoft-visualc-tools-root`、`-Xmicrosoft-windows-sdk-root`、 `-Xmicrosoft-windows-sdk-version` 显式交给编译器驱动,`std.ixx` 取自同一个 toolset。 @@ -109,7 +114,8 @@ clang 以 MSVC ABI 为目标时,所选 toolset 与 SDK 以 `-Xmicrosoft-visualc- 程序)**必须**各自以一行陈述,写出它是什么、从哪里来、以及陈述它的位置;`Finished` 行 **必须**汇总它们;`resolution.json` 的 `sources` 记录每一条。全部来源都是生态缺省时,输出 **必须**与没有本机制时相同。`--managed-only`(或 `MCPP_MANAGED_ONLY`)**必须**拒绝任何 -非缺省来源,并逐条点名(mcpp#755)。 +非缺省来源,并逐条点名(mcpp#755)。主体 `sysroot` 记录目标基座的陈述(§3.9),主体 `run_under` +记录执行宿主的包装前缀;后者不是构建的输入,不参与 `Finished` 行的汇总与 `--managed-only`。 MSVC ABI 目标上:SDK 以 `ucrt@<版本>` 进入运行时身份;clang 行的 toolset 与 SDK 写入 `resolution.json` 的 `msvc_toolset` 与 `windows_sdk`,toolset 目录与 SDK 版本进入缓存键,`stdlibVersion` 记为 toolset 的版本。 @@ -137,8 +143,10 @@ MSVC ABI 目标上:SDK 以 `ucrt@<版本>` 进入运行时身份;clang 行的 to 在 `*-windows-msvc` 目标上,编译器是工具链,MSVC toolset 是 sysroot。 - `[target.<三元组>].sysroot` 在这些行上**可以**写作 `msvc@system`、`msvc@<版本>` 或 `xim:msvc@<版本>`,含义同 §2。 +- 编译器为 clang 时,**可以**写作 `{ path = "<目录>" }`:toolset 取 `<目录>/VC/Tools/MSVC/<版本>` 中最新的一个; + `<目录>/Windows Kits/10` 存在时 SDK 取自其中(clang `/winsysroot` 布局),否则取本机的 SDK。实现**必须**报告取用了哪一个。 - 未写时**必须**等同于 `msvc@system`。 -- 编译器为 `cl.exe` 时,sysroot **必须**是该编译器所属的 toolset;另写一个不同的 sysroot,**必须**被拒绝。 +- 编译器为 `cl.exe` 时,sysroot **必须**是该编译器所属的 toolset;另写一个不同的 sysroot 或写 `{ path }`,**必须**被拒绝。 - SDK **必须**跟随 toolset 的来源:生态包 toolset 用随其安装的 `windows-sdk` 载荷,本机 toolset 用本机扫描的结果。 - 目标侧报告([SPEC-002](target-side.md))中,c-abi 与 c++ 两层记为预制来源;具体的 toolset 与 SDK 记入 `resolution.json`(§3.3)。 - 该值**禁止**作为 C 库包进入项目环境的安装集合。 @@ -217,6 +225,62 @@ GCC 以 xlings subos 为 `--sysroot`,没有可用 subos 而退回载荷布局时 当前:clang 一侧由 `ToolchainLinkModel::compile_tokens` 与安装后重新生成的驱动配置文件给出;GCC 的退回路径由同一函数给出 `-isysroot`(mcpp 2026.10.8.1)。e2e 891 覆盖 clang 一侧的未找到、显式写出与依赖图提示的边界。 +### 3.9 目标基座 已实现 + +本节定义 `sysroot` 在全部目标行上的统一语义。附录 A 给出各行的取值。 + +**3.9.1 一次解析。** 目标基座**必须**只解析一次,结果是一个记录(下称解析结果),包含四个正交属性: +来源(出处类别)、地址(`gnu-sysroot`、`apple-isysroot`、`msvc-roots`、`multilib`、`driver-internal` 之一)、 +供给的层与事实(例如 `glibc 2.31`、SDK 版本)、运行时语义(`binding`、`floor`、`none` 之一)。§3.1 所列读者 +**必须**只读取这一结果。 + +**3.9.2 写法与优先级。** 陈述**可以**是生态包(`xim:glibc@2.39`,团队私有索引的包亦同)、本机定位器 +(`glibc@system`、`musl@system`、`macosx|iphoneos|iphonesimulator@system|<版本>`、`msvc@…`)、 +`{ path = "<目录>", provides = [...] }` 或 `""`(不用预制 C 库)。定位器族名**必须**与目标行一致; +某行接受哪些写法是目标表的数据,拒绝信息**必须**列出该行接受的写法。`{ path = "/" }` **必须**归一为该行的 +`@system`。陈述按四档取第一个: + +| 档 | 来源 | +|---|---| +| 覆盖 | `--sysroot`;`MCPP_TARGET_<三元组>_SYSROOT`;`MCPP_SYSROOT`。空值视为未设置,`none` 表示 `""` | +| 工程 | `mcpp.toml` 的 `[target.<三元组>].sysroot`;其次 `[toolchain] { path, sysroot }` 或工具链阶段的陈述 | +| 机器默认 | `~/.mcpp/config.toml`,其次系统级 `config.toml` 的 `[target.<三元组>].sysroot` | +| 引擎默认 | 目标表该行的 `sysroot` 列,或生态的缺省基座 | + +宿主图(构建程序与宿主工具的子构建)**禁止**读取目标基座。一个工程写明的目录不存在时,诊断**必须**给出文件与 +行号、解析后的绝对路径,以及让它存在的三种办法(生态包、仓库内相对路径、团队约定的路径)和本机覆盖的写法。 + +**3.9.3 测量与声明。** 布局**必须**问驱动(`-print-file-name=crt1.o`);C 库的实现与版本**必须**由驱动预处理 +一个探针翻译单元得到(`__GLIBC__`、`__BIONIC__`、`__PICOLIBC__`、`_NEWLIB_VERSION`、`__EMSCRIPTEN__`; +都不是而存在 musl 的 loader 时为 musl)。引擎持有的布局知识**仅限**Debian multiarch:驱动找不到 `crt1.o` +而存在 `<根>/usr/lib/<元组>` 时,实现**必须**补上该元组的头文件(`-idirafter`)与库目录(`-B`、`-L`、 +`-Wl,-rpath-link`)。`provides` 在测量能回答时用于核对,不一致**必须**拒绝;测量回答不了时作为声明; +两者都没有时该目录是库前缀而非基座,**必须**拒绝。编译器运行库所需的 glibc 版本(其 `libstdc++.so.6`、 +`libgcc_s.so.1` 等的 version need 中最高的 `GLIBC_x.y`)高于基座的版本时,**必须**在编译前拒绝并说出两个版本。 + +**3.9.4 不写入。** 实现**禁止**写入或修补任何不由 mcpp 管理的目录树,包括被陈述的基座与按路径命名的工具链 +所报告的 sysroot。 + +**3.9.5 运行时语义。** 本机目标的动态产物为 `binding`:PT_INTERP 与 RUNPATH **必须**指向基座的 loader 与 +C 库目录,SubOS 的 farm 只在生态缺省基座时追加,运行时闭包校验(rule B)**必须**以基座为参照; +`glibc@system` 经宿主的标准 loader 绑定,不写 RUNPATH。交叉目标与 Apple、MSVC 行为 `floor`:不改写 +loader,基座是下限。静态产物与裸机为 `none`。被陈述基座的内容身份(陈述、根目录,以及测量所依据文件的 +路径、大小与修改时间)**必须**进入运行时契约哈希,从而进入指纹;生态缺省基座的契约**必须**不变。对陈述了基座的构建,`mcpp pack` +**必须**报告产物与随包库所需的最高 glibc 版本,并写入 `HOST-REQUIREMENTS`。 + +### 3.10 自备输入 已实现 + +对受厂商许可约束或属于某台机器、某个人的输入(Apple SDK、非生态的 Visual Studio 与 Windows SDK、 +远程执行宿主、签名材料),实现**必须**只按公开标准定位它们(`SDKROOT`、`xcrun`、`/winsysroot` 布局、 +`{ path }`、`config.toml`),记录其身份;**禁止**获取、托管或分发它们,官方索引**禁止**收录;缺失时 +**必须**给出可操作的提示。`SDKROOT` **必须**在所有宿主上读取;`xcrun` 与开发者目录只在 Apple 宿主上询问。 + +### 3.11 核心知识判据 已实现 + +引擎**只**持有通用的抽象与契约,以及内置目标行所在平台的官方约定(glibc 的 loader 名与 Debian multiarch、 +Apple 的 `SDKROOT` 与 `xcrun`、微软的 vswhere 与 `/winsysroot` 布局)。第三方生态与工具的知识(例如 +Swift SDK 包、`QEMU_LD_PREFIX`)**禁止**进入引擎,由包与插件经 §3.9 与 SPEC-002 §6 的通用接口接入。 + ## 4. 载荷契约 ### 4.1 可重定位 部分实现 @@ -239,6 +303,7 @@ gcc 载荷的 `lib/gcc/<三元组>/<版本>/include-fixed/` **禁止**含带 fix | 载荷**必须**只由提供者在安装时写入;此后**禁止**任何程序改写它,包括 mcpp(ELF 头、`specs`、clang 的 `.cfg`、标记文件) | 已实现(mcpp 2026.10.16.1;`check_provider_boundary.py`) | | 工具链**必须**运行在提供者安装时为其绑定的运行库上;mcpp 经提供者 home 的运行库钉版本选定该运行库 | 已实现 | | 产物的加载器、运行路径与 sysroot **必须**来自构建自己的命令行,不依赖载荷中的配置 | 已实现(gcc:`-specs=` 文件;clang:`--no-default-config`) | +| 以 gnu-sysroot 寻址的被陈述基座,链接器**必须**收到 `--sysroot`:gcc 在读入 `-specs=` 之前把 `%(sysroot_spec)` 拼在 `*link:` 之前,重写 `*link:` 的文件会丢掉它,因此由构建显式给出 `-Wl,--sysroot`。生态缺省的 SubOS 视图**禁止**这样做:链接器会把 sysroot 前缀加到每个输入共享库的 DT_RUNPATH 上,而 farm 中库的 RUNPATH 以绝对路径指向载荷 | 已实现(mcpp 2026.10.23.1) | | 早期 mcpp 改写过的载荷**可以**继续使用;其标记文件不再被读取 | 已实现 | ### 4.4 完整性 部分实现 @@ -343,4 +408,22 @@ doctor 如实报告这一点。它同时报告 mcpp 的包 home 中指向不存 | v0.6 | 2026-10-02 | §7 移入 SPEC-009(工具链的支持与维护):移动默认版本或 C 库绑定的顺序、门与撤销由 SPEC-009 §10 规定,本节改为引用。 | | v0.7 | 2026-10-09 | 随 mcpp 2026.10.8.1:新增 §3.8,使用受管 C 库的构建不隐式搜索宿主的系统头文件;需要时由项目显式写出。 | | v0.8 | 2026-10-16 | 随 mcpp 2026.10.16.1:§4.3 改为「安装后不改写」,载荷只由提供者写入;工具链运行在提供者按 home 的运行库钉版本绑定的运行库上。 | +| v0.10 | 2026-10-23 | 随 mcpp 2026.10.23.1:§1 定义目标基座与执行宿主;新增 §3.9 目标基座(一次解析、四档写法与优先级、测量与声明、不写入、运行时语义)、§3.10 自备输入、§3.11 核心知识判据;§2.2.1 工具链阶段可陈述 `spec` 与 `sysroot`,`spec` 与路径专属键同时出现时拒绝;§3.1 补全读者名单;§3.3 增加主体 `sysroot` 与 `run_under`;§3.5 clang 行接受 `{ path }`;§4.3 要求被陈述的 gnu-sysroot 基座把 `--sysroot` 交给链接器;新增附录 A。 | | v0.9 | 2026-10-17 | 随 mcpp 2026.10.17.1:新增 §4.7,已安装的修订在构建中继续使用,`floor` 决定是否必须替换,`toolchain install` 取当前修订;§6.4 doctor 只读,报告构建实际使用的工具链与包 home 的一致性。 | + +--- + +## 附录 A(规范性):各目标行的基座 + +| 目标行 | 基座实物 | 供给的层 | 地址 | 运行时语义 | 可用写法 | 不写时 | +|---|---|---|---|---|---|---| +| `*-linux-gnu` | glibc 加内核头文件;宿主的 `/` | kernel-abi、c-abi | gnu-sysroot(加 multiarch) | 本机 binding;交叉 floor | 生态包、`glibc@system`、`{ path }`、`""` | 生态 glibc | +| `*-linux-musl` | 工具链里的 `<三元组>/`;Alpine 的 `/` | kernel-abi、c-abi | gnu-sysroot | 静态 none;动态本机 binding | 生态包、`musl@system`、`{ path }`、`""` | 工具链携带 | +| `*-none-*` | picolibc / newlib 的 multilib 目录 | c-abi | multilib | none | 生态包、`{ path }`、`""` | 行的 `sysroot` 列 | +| `*-macos` | MacOSX SDK | kernel-abi、c-abi、C++ 头文件、frameworks | apple-isysroot | floor | `macosx@system`、`macosx@<版本>`、`{ path }` | `macosx@system` | +| `*-ios`、`*-ios-sim` | iPhoneOS / iPhoneSimulator SDK | 同上 | apple-isysroot | floor | `iphoneos@…` / `iphonesimulator@…`、`{ path }` | 对应的 `@system` | +| `*-windows-msvc` | VC toolset 加 Windows SDK | c++-abi、compiler-runtime、c-abi(UCRT)、kernel-abi | msvc-roots | UCRT 为 floor | `msvc@system`、`msvc@<版本>`、`xim:msvc@…`、`{ path }`(仅 clang) | `msvc@system` | +| `*-windows-gnu`、`*-android`、`wasm32-emscripten` | SDK 工具链内建 | 视 SDK 而定 | driver-internal | floor 或 none | 生态包与 `""` 沿用既有含义,不接受新写法 | 工具链携带 | + +Apple 行能否构建由 SDK 中是否有该架构的切片决定(`libSystem.tbd` 的 `targets`),实现**必须**在缺少切片时 +点名拒绝;与厂商的硬件路线无关。 diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index 1121e2ff3..2b38452fb 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -1261,6 +1261,32 @@ error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot exe `"status":"not_run"` 与一个 `reason`,摘要记录上则是 `not_run` / `not_run_reason`(见[50 —— 机器可读输出](50-machine-output.md))。 +#### 2.7.3.2 `sysroot` 与 `run_under`(2026.10.23.1+) + +`[target.<三元组>]` 另有两个键,分别描述目标针对什么构建、产物在哪里执行: + +```toml +[target.x86_64-linux-gnu] +sysroot = { path = "third_party/sysroot" } # 或 "xim:glibc@2.39"、"glibc@system"、"" + +[target.aarch64-ios-sim] +runner = ["simctl-run"] +run_under = ["remote-run", "team-mac", "--"] # 到达产物执行所在的机器 +``` + +`sysroot` 点名**目标基座**——预制的 C 库及其相邻的层——写作生态包、定位器、`{ path, provides }` +或 `""`;语义见 [20 —— 工具链](20-toolchains.md)。`run_under` 是到达**执行宿主**的 argv,组合在 +runner 之前:` <产物> <参数>`(Bazel 的 `--run_under`)。两个键也可以写在 +`~/.mcpp/config.toml` 中作为本机的默认值,并可按调用以 `MCPP_TARGET_<三元组>_SYSROOT` / +`MCPP_TARGET_<三元组>_RUN_UNDER` 覆盖(`none` 表示清除,空值视为未设置)。全员以同一方式执行的团队 +把 `run_under` 写在这里;混合环境的团队写在各自机器的 `config.toml` 中。`--no-runner` 同时去掉 +runner 与 `run_under`。 + +经此到达的运行器收到 `MCPP_RUN_TRIPLE`、`MCPP_RUN_KIND`(`exe` / `bundle`)、`MCPP_RUN_MODE` +(`run` / `test`)、`MCPP_RUN_ID`、`MCPP_RUN_CLOSURE`(一份带版本的 JSON,列出产物、它随带的文件与基座), +以及陈述了基座时的 `MCPP_SYSROOT`;其退出码即程序的退出码。非 macOS 宿主上的 Apple 产物在没有 +`run_under` 时于执行前被拒绝,拒绝信息点名本键。 + ### 2.8 `[features]` —— Feature 已移至 [06 —— Feature 与能力](06-features-and-capabilities.md),连同 @@ -1350,6 +1376,8 @@ opt = 3 # -O level (a number, or the string "s"/"z") debug = false # -g lto = true # -flto; /GL + /LTCG on cl.exe (note: some packaged gcc builds ship without the LTO plugin) strip = true # -s at link time +ndebug = true # 整个图的每个单元 -DNDEBUG(release/dist 默认开) +trim_paths = true # 整个图的每个单元 -ffile-prefix-map(release/dist 默认开) # passthrough escape hatch (fixed keys, open values): cflags = ["-fno-plt"] cxxflags = ["-fno-plt"] @@ -1371,6 +1399,17 @@ ldflags = [] - 内置 profile:`release`(-O2)/ `dev`、`debug`(-O0 -g)/ `dist` (-O3 + strip;**默认不启用 LTO**)。`[profile.<内置名>]` 可以整体 覆盖一个内置定义。 +- **`ndebug` 与 `trim_paths`(2026.10.23.1+)** 跟随业界惯例:`release` 与 `dist` 默认开启, + `dev`、`debug` 以及未陈述它们的自定义 profile 默认关闭。`ndebug` 为每个单元定义 `NDEBUG`, + `assert` 因此不再展开,与 CMake 的 Release、Meson 的 `b_ndebug=if-release` 相同。`trim_paths` + 把产物会携带的每个绝对前缀——`__FILE__`、`std::source_location`、断言消息与调试信息中的—— + 映射为它所代表的东西:每个包的源码映射为 `@/…`(根包同样如此,报告读作 + `app@1.2.0/src/main.cpp`),存储、home、构建目录、工具链与 sysroot 分别映射为 `/mcpp/store`、 + `/mcpp/home`、`/mcpp/build`、`/mcpp/toolchain` 与 `/mcpp/sysroot`。同一棵树的两个检出因此嵌入 + 相同的路径。它作用于 `build`、`run` 与 `pack` 的产物,不作用于 `mcpp test`:测试二进制在构建处运行, + 且常借 `__FILE__` 找到它的夹具。带调试信息的构建还在输出旁写出 `source-map.gdbinit` 与 `source-map.lldbinit`, + 供调试器把路径映射回来。cl.exe 没有文档化的等价开关,构建会报告一次。在 profile 下写 + `ndebug = false` 或 `trim_paths = false` 可保留以前的产物;升级后第一次发布档构建会提示一次。 - **每个 profile 拥有自己的构建目录。** 解析出的 profile 旋钮参与 指纹,所以 `target//` 下每个 profile 各有一个哈希目录,在 它们之间切换是增量的,不是一次完整重建。这也意味着磁盘开销随实际 diff --git a/docs/zh/09-commands-by-scenario.md b/docs/zh/09-commands-by-scenario.md index 6aa3a2f13..1b8b9e2a1 100644 --- a/docs/zh/09-commands-by-scenario.md +++ b/docs/zh/09-commands-by-scenario.md @@ -137,6 +137,20 @@ sources: considered: payload xim:cmake@>=3.31 (not installed: overridden) ``` +`mcpp why sysroot` 报告目标基座(2026.10.23.1+):陈述了什么、在哪里陈述、在那里测量到什么、产物加载 +什么,以及存在时执行宿主的包装前缀: + +``` +$ mcpp why sysroot +sysroot: path:/work/app/third_party/sysroot + address=gnu-sysroot runtime=binding root=/work/app/third_party/sysroot + c-abi=glibc 2.31 + loader=/work/app/third_party/sysroot/lib64/ld-linux-x86-64.so.2 + runtime dir /work/app/third_party/sysroot/lib64 + identity=3d9bbfa57f6acda6 + source: custom · mcpp.toml:12 +``` + `mcpp why deps` 在 `mcpp.lock` 各行之前列出解析出的依赖图(2026.9.14.2+): 每个包、每条请求书写时用的键与所在的表,以及库的链接形态与其原因。锁文件 不记录的 `path` 依赖同样列出: diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 488ab49a0..82e555fab 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -110,6 +110,13 @@ capability=opengl.glx.driver discovery=rpath-of-dispatch `discovery` 是可操作的那一半 —— 各机制彼此独立,满足其一不代表满足另一个。该 文件只在确有内容需要说明时才写出:一个空文件等于**声称**什么都不需要。 +针对一个被陈述的目标基座构建、且加载目标自身 C 库的包(`vendored`、`system`)还陈述它能运行的最低 +C 库(2026.10.23.1+):程序与 +随包库所需的最高 `GLIBC_x.y`,打印为 `Requires glibc >= 2.38 …`,并写为 +`capability=c-abi.glibc>=2.38 discovery=PT_INTERP`。程序构建时所针对的基座即成为这一下限; +`self-contained` 则随包带上基座自己的 loader 与 C 库。`mcpp pack` 使用的 `release` 与 `dist` 档 +会裁剪产物携带的源码路径并定义 `NDEBUG`(见 [04 —— `[profile]`](04-mcpp-toml.md))。 + ### 模式名的兼容性 上表给出规范名称。旧名称仍是**永久性的兼容别名**:`bundle-project` = diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index cf04bd879..9373bb719 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -796,14 +796,92 @@ iPhoneSimulator 的 SDK:它在 Xcode 里,而且不可再分发。所以 mcpp 它,通过 `xcrun --sdk --show-sdk-path`,与它一直以来定位 macOS SDK 的方式完全相同。 -这也是这三行不带 `sysroot` 条目的原因。那一列命名的是一个包,而一个被定位的 -目录不是包。 +这也是这三行在目标表中不带 `sysroot` 列条目的原因:那一列命名的是一个包,而一个被定位的 +目录不是包。工程改以该行的 `sysroot` 键点名 SDK:`iphonesimulator@system`(缺省)、 +`iphonesimulator@<版本>` 或 `{ path = "…" }`;`SDKROOT` 在所有宿主上读取,因此开发者在 Linux +上用自己的 Xcode.xip(例如借助 xtool)生成的 SDK,与 Xcode 中的 SDK 以同样方式被定位。mcpp 只定位, +从不分发。SDK 自己的 `libSystem.tbd` 说明它带哪些架构,缺少某行切片时,构建被拒绝并点名该切片。 ```bash mcpp build --target aarch64-ios # resolves llvm@23.1.3 + the iPhoneOS SDK mcpp build --target aarch64-ios-sim # resolves llvm@23.1.3 + the Simulator SDK ``` +## 目标基座:`sysroot`(2026.10.23.1+) + +sysroot 不是层。它是目标侧下层([docs/22](22-target-side.md))的**预制供给者**: +一个按目标驱动约定组织的目录,供给 `c-abi`,通常还有 `kernel-abi`,可能还有 `c++-abi`、 +`compiler-runtime` 与平台库。mcpp 称之为**目标基座**,清单键名仍为 `sysroot`。一个陈述只解析 +一次,所有读者使用同一个结果:编译行与链接行、依赖扫描、`std` 模块、产物的 loader 与运行路径、 +闭包校验、指纹、报告、`mcpp pack` 与运行器。 + +### 写法 + +```toml +[target.x86_64-linux-gnu] +sysroot = "xim:glibc@2.39" # 生态包(团队私有索引的包亦可) +sysroot = "glibc@system" # 本机自己的根 +sysroot = { path = "third_party/sysroot" } # 一个目录,相对于写它的文件 +sysroot = { path = "/opt/exotic", provides = ["mcpp:c-abi=glibc"] } +sysroot = "" # 不用预制 C 库 + +[target.aarch64-ios-sim] +sysroot = "iphonesimulator@18.5" # 一个被定位的、指定版本的 SDK +``` + +定位器的族名取业界词汇:`glibc`、`musl`、`msvc`,以及 xcrun 的 SDK 名 `macosx`、 +`iphoneos`、`iphonesimulator`。族名必须与目标行一致(`-gnu` 行上写 `musl@system` 会被拒绝, +拒绝信息列出该行接受的写法)。不带命名空间的 `glibc@<版本>` 即生态包 `xim:glibc@<版本>`。 +`{ path = "/" }` 与 `@system` 是同一个陈述。表只有 `path` 与 `provides` 两个键。各目标行 +接受的写法见 [SPEC-006 附录 A](../specs/toolchain-management.md)。 + +### 陈述的优先级 + +按四档取第一个: + +| 档 | 位置 | +|---|---| +| 覆盖 | `--sysroot`;`MCPP_TARGET_<三元组>_SYSROOT`;`MCPP_SYSROOT`(`none` 即 `""`,空值视为未设置) | +| 工程 | `mcpp.toml` 的 `[target.<三元组>].sysroot`;其次 `[toolchain] { path, sysroot }`,或陈述了 `sysroot` 的工具链阶段 | +| 机器默认 | `~/.mcpp/config.toml` 的 `[target.<三元组>].sysroot`,其次系统级 `config.toml` | +| 引擎默认 | 目标行自己的那一列,或生态的缺省基座 | + +团队把基座写在 `mcpp.toml` 里。工程点名的 sysroot 由工程负责提供;它缺失时,错误信息说明这一点, +并列出让它存在的三种办法:作为包发布到团队索引、以该相对路径放在仓库中、约定一个团队共用的路径。 +个别机器需要另一份时,设置 `MCPP_TARGET_<三元组>_SYSROOT`,或写在本机的 `config.toml` 中。 +构建程序与宿主工具在宿主自己的基座上构建,从不读取目标的基座。 + +### 测量 + +C 运行时的位置问驱动(`-print-file-name=crt1.o`),C 库的实现与版本问预处理器(`__GLIBC__`、 +`__BIONIC__`、`__PICOLIBC__`、`_NEWLIB_VERSION`、`__EMSCRIPTEN__`;musl 由其 loader 识别)。 +mcpp 持有的唯一布局规则是 Debian multiarch:不支持 multiarch 的 gcc 在 `/usr/lib/<元组>` 中找不到 +`crt1.o`,此时 mcpp 只为该驱动补上该元组的目录。`provides` 用于核对测量,或在无法测量时代替测量; +两者都没有的目录是库前缀,会被拒绝。 + +编译器自己的运行库有下限。gcc 16 的 `libstdc++.so.6` 需要 `GLIBC_2.38`,因此 glibc 2.31 的基座 +在编译开始之前即被拒绝: + +``` +error: [target.x86_64-linux-gnu].sysroot (mcpp.toml:7): gcc 16.1.0's runtime requires glibc >= 2.38 + (libstdc++.so.6: GLIBC_2.38), and this sysroot provides glibc 2.31. +``` + +### 产物的运行时 + +本机目标的动态产物**绑定**其基座:PT_INTERP 是基座的 loader,RUNPATH 指向基座的 C 库目录, +程序运行在它编译时所用的 C 库上。`glibc@system` 经宿主的标准 loader 绑定,不写 RUNPATH。 +交叉目标以及 Apple 与 MSVC 行把基座视为**下限**。基座的身份(陈述、根目录、测量所依据文件的 +大小与修改时间)进入指纹,因此原地改写的 sysroot 会触发重建。对这样的构建,`mcpp pack` 报告程序及随包库所需的 +最高 glibc 版本,并写入 `HOST-REQUIREMENTS`。 + +``` + Using sysroot glibc 2.31 ← third_party/sysroot [custom · mcpp.toml:12] +``` + +`mcpp why sysroot` 打印完整的解析结果;`resolution.json` 以 `base` 记录。mcpp 从不写入一个 +不是它创建的 sysroot。 + ## 每个工具的来源(2026.10.1.3+) 一次构建用到一个工具链、它的插件声明的载荷,以及那些插件运行的工具。每一项都有一个来源, @@ -1282,6 +1360,20 @@ dialect_cxxflags = ["-D__cpp_impl_coroutine=201902L"] `i686-windows-msvc` 不是目标表中的一行;为它构建的项目需要声明 `[target.i686-windows-msvc]`,与其他自定义三元组相同。 +## 已知工具链限制:非 ASCII 路径下的 GCC 模块(gcc 16) + +GCC 的 module mapper 协议(libcody)无法解析它自己对 ASCII 以外字节的编码。gcc 工具链位于这样的 +路径下时——mcpp home 在 `/home/张三/` 或 `…/家/` 之下——预编译 `std` 模块时,标准库的每个 +`#include` 都失败: + +``` +error: cannot determine '#include' translation of …/家/…/bits/stdc++.h: + malformed string 'ERROR 'unrecognized \'INCLUDE-TRANSLATE… +``` + +含空格的路径可以构建。mcpp 在该错误之后点名原因(2026.10.23.1+);出路是把 home 放在 ASCII 路径上 +(`mcpp self init --home `),或使用 llvm 工具链,它的模块不经过这一协议。 + ## C++ 运行时契约(`cxx_runtime`) `cxx_runtime` 声明的是产物对运行它的那台机器做出的承诺。它是**分发**属性, diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index c8ed6f823..583f1c7b7 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -487,7 +487,7 @@ docs/22。 | `x86_64-linux-android` | verified | `android-ndk@30.0.16248370` | 载荷 | 载荷 | 载荷 | — | | `aarch64-ios` | preview | `llvm@23.1.3` | — | — | SDK | — | | `aarch64-ios-sim` | verified | `llvm@23.1.3` | — | — | SDK | — | -| `x86_64-ios-sim` | preview | `llvm@23.1.3` | — | — | SDK | — | +| `x86_64-ios-sim` | verified | `llvm@23.1.3` | — | — | SDK | — | | `wasm32-emscripten` | verified | `emsdk@6.0.9` | 载荷 | 载荷 | 载荷 | 载荷 | `载荷` 这里有工具链 payload 产出它 · `图` 没有 payload,但依赖能供给 diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 28e5c96d7..0d3790131 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -948,13 +948,18 @@ sysroot = "" # no C library at all 与其依赖提供的内容,`#include ` 不再解析。内核与 bootloader 要的正是这一档,而把两种情形合并会让这类工程静默地把目标的 C 库拿回去。 -取值是一个 xpkg 引用或空字符串;裸名在解析清单时即被拒绝,因为接受它会 -导致什么都不安装,然后在很晚的时候以「缺少 libc」失败。 +取值是一个包引用、一个定位器(`glibc@system`、`iphoneos@18.5`)、一个表 +`{ path = "<目录>", provides = [...] }` 或空字符串(2026.10.23.1+);裸名在解析清单时即被拒绝, +因为接受它会导致什么都不安装,然后在很晚的时候以「缺少 libc」失败。被陈述的基座在规则一下是 +`c-abi` 的一个供给者:依赖图中另有包提供 `mcpp:c-abi` 时,构建被拒绝并点名两者。基座被测量为 +供给载荷 C++ 运行时所配置的那种 C 库、且版本不低于该运行时的要求时,载荷的 C++ 运行时仍可使用 +(规则二的可测量下限)。各行接受的写法、优先级与测量内容见 [20 —— 工具链管理](20-toolchains.md)。 在 MSVC ABI 行(`*-windows-msvc`)上,sysroot 是 MSVC toolset,取值是 msvc 的 写法(2026.9.24.1+):`msvc@system`(键缺席时的默认)、`msvc@`(已安装的 同版本 toolset,没有时为载荷)或 `xim:msvc@`(只要载荷)。这类行上的 -其他取值在解析清单时即被拒绝。见 [20 —— 工具链管理](20-toolchains.md) 中 +其他取值被拒绝。编译器为 clang 时,该行还接受 `{ path = "<目录>" }`(2026.10.23.1+): +一棵解包的 Visual Studio 目录树或 clang 的 `/winsysroot` 布局,其中存在 `Windows Kits/10` 时使用它。见 [20 —— 工具链管理](20-toolchains.md) 中 「MSVC ABI 上的 clang:toolset 就是 sysroot」一节。 构建程序可以询问供给 sysroot 的是哪个 C 库**载荷**:`mcpp::target_libc()` diff --git a/docs/zh/25-home-indices-and-store.md b/docs/zh/25-home-indices-and-store.md index 96ae666a7..182d8ee26 100644 --- a/docs/zh/25-home-indices-and-store.md +++ b/docs/zh/25-home-indices-and-store.md @@ -148,6 +148,22 @@ default_target = "x86_64-linux-gnu" 在 Luban OS 上,系统级配置同时写出 `/xlings`,因此机器上已有的 payload 只存在一份。 +`config.toml`(用户级,其次系统级)还可以陈述本机对某个目标的默认值(2026.10.23.1+):它的基座, +以及它的产物如何到达执行它的机器。工程的 `mcpp.toml` 优先于二者,`MCPP_TARGET_<三元组>_SYSROOT` / +`_RUN_UNDER` 覆盖三者;相对的 `path` 相对于写它的那个 `config.toml`。 + +```toml +# ~/.mcpp/config.toml +[target.x86_64-ios-sim] +run_under = ["remote-run", "macvm", "--"] # 只有这台机器经此到达一台 Mac + +[target.x86_64-linux-gnu] +sysroot = { path = "/opt/acme/sysroot" } # 团队约定的路径,用于仓库中没有它的机器 +``` + +该表只接受 `sysroot` 与 `run_under`:目标的其余属性都属于工程。见 [20 —— 工具链管理](20-toolchains.md) +与 [04 —— mcpp.toml](04-mcpp-toml.md)。 + ## 5. 提供者程序 `[xlings] binary` 选择 mcpp 运行的 xlings:`"bundled"`(mcpp 随附的,默认)、`"system"`(`PATH` 上的)或一个路径。 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 7db462f0d..d1487353c 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -353,7 +353,24 @@ mcpp why sources|tool |payload --format json ``` 每次构建把同一份记录写入 -`target///resolution.json` 的 `sources`。 +`target///resolution.json` 的 `sources`。主体 `sysroot` 记录目标基座的 +陈述,`run_under` 记录执行宿主的包装前缀。`mcpp why sysroot --format json` 等同于 +`mcpp why sources sysroot`(2026.10.23.1+)。 + +`resolution.json` 还在每次构建中携带描述目标基座与执行宿主的两个对象(2026.10.23.1+): + +```jsonc +"base": { "form": "path", "statement": "path:/work/app/third_party/sysroot", + "address": "gnu-sysroot", "runtime": "binding", + "root": "/work/app/third_party/sysroot", + "c_abi": { "impl": "glibc", "version": "2.31" }, "multiarch": "", + "loader": "/work/app/third_party/sysroot/lib64/ld-linux-x86-64.so.2", + "runtime_dirs": ["/work/app/third_party/sysroot/lib64"], + "identity": "3d9bbfa57f6acda6" }, +"execution": { "run_under": ["remote-run", "team-mac", "--"], "origin": "mcpp.toml:15" } +``` + +生态缺省基座的 `form` 为 `default`,其余字段描述引擎缺省解析出的结果。 ### `mcpp.why.build-programs` 与 `mcpp.why.compat` *(mcpp 2026.10.11.1+)* @@ -463,7 +480,8 @@ replaced}` —— `origin` 与构建的状态行使用的是同一句话 | `payload-override` | 一条 `[xlings.overrides]` 点名的路径不存在、版本被某条要求拒绝,或它由依赖写出 *(2026.10.1.3+)* | | `payload-request` | 构建程序请求了本次构建中没有任何清单声明 `provision = "on-request"` 的载荷,或连续三次运行都在请求 *(2026.10.1.3+)* | | `lto-export-discovery` | 导出需要被发现的 PE 共享库会被 cl.exe 编译成没有符号表的 `/GL` 对象:在 `lto = true` 下陈述了 `windows_auto_export = true`,或在它所链接的包的 flag 中写了 `/GL` *(2026.10.5.2+)* | -| `local-toolchain` | 由路径命名的工具链,或构建程序工具链阶段陈述的工具链无法使用:没有驱动、族与驱动矛盾、缺少某个工具或 sysroot *(2026.10.1.3+)* | +| `local-toolchain` | 由路径命名的工具链,或构建程序工具链阶段陈述的工具链无法使用:没有驱动、族与驱动矛盾、缺少某个工具,或 `spec` 与只属于路径工具链的键一同陈述 *(2026.10.1.3+)* | +| `target-base` | 目标基座(`sysroot`)无法服务本次构建:该行不接受的写法、不存在的路径、不供给 C 库的目录、低于编译器运行库要求的 C 库、缺少 loader 的本机基座,或依赖图中的第二个 C 库供给者 *(2026.10.23.1+)* | | `convention-unreplaced` | 约定被推翻了,而没有任何东西接替它 | | `os-mismatch` | 请求的三元组与解析出的三元组命名不同的系统 | | `layer-requirement` | 某个包要求的层,解析结果没有提供 | diff --git a/mcpp.toml b/mcpp.toml index 44f0e4d78..bdea64e54 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.22.1" +version = "2026.10.23.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/manifest/src/key_registry.cppm b/modules/manifest/src/key_registry.cppm index bce160d0d..e137af2c4 100644 --- a/modules/manifest/src/key_registry.cppm +++ b/modules/manifest/src/key_registry.cppm @@ -83,6 +83,7 @@ inline constexpr KeyClass kKeyRegistry[] = { {"target.", "cxx_runtime", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "linkage", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "min_api_level", KeyScope::Configuration, KeyMerge::Scalar, true, true }, + {"target.", "run_under", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "runner", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "sysroot", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "toolchain", KeyScope::Configuration, KeyMerge::Scalar, true, true }, diff --git a/modules/manifest/src/targetside_model.cppm b/modules/manifest/src/targetside_model.cppm index d50199502..12004348f 100644 --- a/modules/manifest/src/targetside_model.cppm +++ b/modules/manifest/src/targetside_model.cppm @@ -309,6 +309,13 @@ struct TargetSide { // configuration (`mcpp.toolchain.cenv`). std::optional cAbiDecl; + // THE PAYLOAD'S C++ RUNTIME OVER A PREBUILT BASE, MEASURED TO FIT (design + // 2026-10-23 D8). Rule two holds for a base that supplies the C library + // the payload's C++ runtime was configured for (glibc under libstdc++) at + // a version no lower than that runtime requires -- a fact the base + // resolution measured, not one assumed. + bool cxxOverBaseMeasured = false; + // 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. @@ -597,6 +604,14 @@ struct Inputs { // caller from `[target.X].sysroot` over the target table's column. bool sysrootDeclaredEmpty = false; // `sysroot = ""` — the zero-libc tier std::string sysrootXpkg; // an xpkg reference, or empty + // The C library a base named by path or by a locator was measured to + // supply ("glibc"): what the report names it, since there is no package + // name to derive it from. Empty for a package. + std::string sysrootLibc; + // That base supplies the C library the payload's C++ runtime was + // configured for, at a version its runtime accepts (measured by the base + // resolution's floor check). + bool sysrootFitsPayloadCxx = false; // What the payload would contribute, for display only. std::string payloadSystemRef; @@ -716,7 +731,9 @@ inline TargetSide resolve(const Inputs& in) { else if (in.sysrootDeclaredEmpty) ts.cAbi = { Origin::None, {}, {}, false }; else if (!in.sysrootXpkg.empty()) - ts.cAbi = { Origin::Xpkg, xpkg_interface(in.sysrootXpkg), in.sysrootXpkg, false }; + ts.cAbi = { Origin::Xpkg, + in.sysrootLibc.empty() ? xpkg_interface(in.sysrootXpkg) : in.sysrootLibc, + in.sysrootXpkg, false }; else if (in.freestandingTarget) ts.cAbi = { Origin::None, {}, {}, false }; else @@ -727,7 +744,8 @@ inline TargetSide resolve(const Inputs& in) { if (in.cxxAbi) ts.cxx = { Origin::Graph, in.cxxAbi->display_interface(), in.cxxAbi->id(), !in.cxxAbi->hasStdModule }; - else if (ts.cAbi.origin == Origin::Payload) + else if (ts.cAbi.origin == Origin::Payload + || (ts.cAbi.origin == Origin::Xpkg && in.sysrootFitsPayloadCxx)) // THE LAYERING RULE, AS STRUCTURE RATHER THAN AS A LATER CHECK. // // An implementation must have been configured for the layer beneath it. @@ -740,6 +758,7 @@ inline TargetSide resolve(const Inputs& in) { ts.cxx = { Origin::Payload, in.payloadCxxInterface, in.payloadCxxRef, false }; else ts.cxx = { Origin::None, {}, {}, false }; + ts.cxxOverBaseMeasured = ts.cAbi.origin == Origin::Xpkg && in.sysrootFitsPayloadCxx; return ts; } @@ -765,7 +784,7 @@ inline const Layer& layer_of(const TargetSide& ts, CapLayer l) { // rule is stated again here for that path. inline std::optional check_layering(const TargetSide& ts) { if (ts.cxx.origin == Origin::Payload && ts.cAbi.origin != Origin::Payload - && ts.cAbi.origin != Origin::None) + && ts.cAbi.origin != Origin::None && !ts.cxxOverBaseMeasured) return std::format( "the toolchain payload's C++ runtime cannot be used with a C " "library that does not come from the payload.\n" diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index b5fe00296..df44c6ed5 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -841,6 +841,24 @@ parse_c_abi_absent(const t::Value& v, return std::nullopt; } +// A `{ path = "…" }` sysroot, absolute: relative to the directory of the file +// that wrote it, with `~` standing for the home directory. +std::filesystem::path absolute_sysroot_path(const std::filesystem::path& origin, + const std::string& written) { + std::filesystem::path p(written); + if (written == "~" || written.starts_with("~/")) { + const char* h = std::getenv(mcpp::platform::is_windows ? "USERPROFILE" : "HOME"); + if (h && *h) p = std::filesystem::path(h) / (written.size() > 2 ? written.substr(2) : std::string{}); + } + if (p.is_relative()) { + std::error_code ec; + auto dir = origin.parent_path(); + if (dir.is_relative()) dir = std::filesystem::absolute(dir, ec); + p = dir / p; + } + return p.lexically_normal(); +} + // `[target.].sysroot`: the refusal for a value the row cannot take, or // nullopt. Kept out of `parse_string` for the reason the helper above states. // @@ -864,16 +882,22 @@ std::optional sysroot_refusal(std::string_view triple, std::string_ return std::format( "[target.{}].sysroot = '{}': on an MSVC-ABI row the sysroot is an " "MSVC toolset, written \"msvc@system\", \"msvc@\" (an " - "installed toolset first, then the package) or " - "\"xim:msvc@\" (the package only).", + "installed toolset first, then the package), " + "\"xim:msvc@\" (the package only) or, for clang, " + "{{ path = \"\" }}.", triple, value); } - if (!value.empty() && value.find(':') == std::string_view::npos) { + // A package (`:`), a locator (`@system` or + // `@`), `path:`, or "". Whether THIS row takes the + // form is decided where the target is known; this only refuses a value + // that is no form at all. + if (!value.empty() && value.find(':') == std::string_view::npos + && value.find('@') == std::string_view::npos) { return std::format( - "[target.{}].sysroot = '{}' is not an xpkg reference; " - "expected `:[@]` (e.g. " - "\"xim:picolibc-riscv@1.8.12\"), or \"\" for a target " - "that takes no prebuilt C library directory.", + "[target.{}].sysroot = '{}' is not a sysroot. A sysroot is a package " + "(\"xim:glibc@2.39\"), a locator (\"glibc@system\", \"musl@system\", " + "\"macosx@system\", \"iphoneos@18.5\", \"msvc@system\"), " + "{{ path = \"\" }}, or \"\" for no prebuilt C library.", triple, value); } return std::nullopt; @@ -1131,6 +1155,14 @@ std::expected parse_document(const t::Document& documen pr.strip = it->second.as_bool(); pr.stripDeclared = true; } + if (auto it = tt.find("ndebug"); it != tt.end() && it->second.is_bool()) { + pr.ndebug = it->second.as_bool(); + pr.ndebugDeclared = true; + } + if (auto it = tt.find("trim_paths"); it != tt.end() && it->second.is_bool()) { + pr.trimPaths = it->second.as_bool(); + pr.trimPathsDeclared = true; + } auto read_list = [&](const char* key, std::vector& out) { if (auto it = tt.find(key); it != tt.end() && it->second.is_array()) for (auto& v : it->second.as_array()) @@ -3609,12 +3641,74 @@ std::expected parse_document(const t::Document& documen // the target row) stays distinguishable from "the key is present and // empty". Collapsing the two is how a kernel project would silently // get picolibc back. - if (auto it = body.find("sysroot"); it != body.end() && it->second.is_string()) { - std::string s = it->second.as_string(); - if (auto why = sysroot_refusal(triple, s)) - return std::unexpected(error(origin, *why)); - e.sysroot = std::move(s); - e.sysrootDeclared = true; + // + // THE TABLE FORM NAMES A DIRECTORY (design 2026-10-23 §3.1): + // `{ path = "…", provides = [...] }`. A relative path is relative + // to this file, so it is made absolute here, where the file is + // known, and travels as `path:` -- the spelling the + // environment uses too. Which forms a row accepts is decided where + // the target is known (`mcpp.toolchain.base::refusal`); this + // parser decides only that the value is a sysroot at all. + if (auto it = body.find("sysroot"); it != body.end()) { + e.sysrootLine = static_cast(it->second.position.line); + if (it->second.is_string()) { + std::string s = it->second.as_string(); + if (auto why = sysroot_refusal(triple, s)) + return std::unexpected(error(origin, *why)); + e.sysroot = std::move(s); + e.sysrootDeclared = true; + } else if (it->second.is_table()) { + std::string path; + for (auto& [k, v] : it->second.as_table()) { + if (k == "path" && v.is_string()) { + path = v.as_string(); + } else if (k == "provides" && v.is_array()) { + for (auto& el : v.as_array()) { + if (!el.is_string()) + return std::unexpected(error(origin, std::format( + "[target.{}].sysroot.provides must contain only strings, " + "e.g. provides = [\"mcpp:c-abi=glibc\"]", triple))); + e.sysrootProvides.push_back(el.as_string()); + } + } else { + return std::unexpected(error(origin, std::format( + "[target.{}].sysroot: '{}' is not a key of a sysroot table; " + "the keys are `path` (a string) and `provides` (an array of " + "capabilities)", triple, k))); + } + } + if (path.empty()) + return std::unexpected(error(origin, std::format( + "[target.{}].sysroot is a table without `path`; write " + "{{ path = \"\" }}, or a string for a package or a locator", + triple))); + e.sysroot = "path:" + absolute_sysroot_path(origin, path).generic_string(); + e.sysrootDeclared = true; + } else { + return std::unexpected(error(origin, std::format( + "[target.{}].sysroot must be a string or {{ path = \"\" }}", + triple))); + } + } + // `run_under` -- the wrapper that reaches the execution host + // (design 2026-10-23 §3.7). An array, like `runner`, and composed + // in front of it: ` `. + if (auto it = body.find("run_under"); it != body.end()) { + if (!it->second.is_array()) + return std::unexpected(error(origin, std::format( + "[target.{}].run_under must be an array of strings, " + "e.g. run_under = [\"remote-run\", \"team-mac\", \"--\"]", triple))); + for (auto& el : it->second.as_array()) { + if (!el.is_string()) + return std::unexpected(error(origin, std::format( + "[target.{}].run_under must contain only strings", triple))); + e.runUnder.push_back(el.as_string()); + } + if (e.runUnder.empty()) + return std::unexpected(error(origin, std::format( + "[target.{}].run_under is empty; leave it out to run on this " + "machine", triple))); + e.runUnderLine = static_cast(it->second.position.line); } // `min_api_level` — the oldest OS release the artifact must run @@ -3742,7 +3836,7 @@ std::expected parse_document(const t::Document& documen static constexpr std::string_view kKnownTargetScalars[] = { "cxx_runtime", "linkage", "min_api_level", "sysroot", "toolchain", }; - static constexpr std::string_view kKnownTargetArrays[] = { "runner" }; + static constexpr std::string_view kKnownTargetArrays[] = { "run_under", "runner" }; static constexpr std::string_view kKnownTargetTables[] = { "abi", "build", "build-dependencies", "dependencies", "dev-dependencies", "feature-deps", "feature-requires-abi", @@ -3754,6 +3848,12 @@ std::expected parse_document(const t::Document& documen if (std::ranges::find(kKnownTargetTables, key) != std::end(kKnownTargetTables)) continue; + // A scalar key whose value may also be written as a table + // (`sysroot = { path = ... }`) is a value, not a section; + // it was parsed, or refused, above. + if (std::ranges::find(kKnownTargetScalars, key) + != std::end(kKnownTargetScalars)) + continue; std::string tables; for (auto name : kKnownTargetTables) { if (!tables.empty()) tables += ", "; @@ -3771,7 +3871,7 @@ std::expected parse_document(const t::Document& documen if (std::ranges::find(known, key) != known.end()) continue; m.schemaWarnings.push_back(std::format( "[target.{}] has unsupported key '{}' (ignored). Supported keys: " - "cxx_runtime, linkage, min_api_level, runner, sysroot, toolchain, " + "cxx_runtime, linkage, min_api_level, run_under, runner, sysroot, toolchain, " "plus the [target..runners] table for named runners. " "Per-role contracts go in [build].cxx_runtime's table form.", triple, key)); diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index bf8f79798..432e80135 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1104,6 +1104,11 @@ struct BuildConfig : BuildInputs { // the build summary, which says `+ lto (partial)` rather than `+ lto`. bool ltoPartial = false; bool strip = false; // link -s + // `-DNDEBUG` for every unit of the graph (the profile's `ndebug`). + bool ndebug = false; + // `-ffile-prefix-map` for every unit (the profile's `trim_paths`): the + // paths an artifact carries name packages and roles, not this machine. + bool trimPaths = false; // `[build].default-profile` (alias: `profile`) — the project's DEFAULT // profile when no --profile/--dev/--release is passed. The global convention // default stays "release"; this lets a project opt its plain `mcpp build` @@ -1489,6 +1494,18 @@ struct TargetEntry { // Two plain members carry the same information and instantiate nothing. std::string sysroot; bool sysrootDeclared = false; + // The table form's `provides` (`{ path = "…", provides = [...] }`): what a + // directory supplies when measurement cannot say (design 2026-10-23 + // §3.4). The path itself is in `sysroot`, absolute, as `path:`, + // because a relative path is relative to the file that wrote it. + std::vector sysrootProvides; + int sysrootLine = 0; + // THE EXECUTION HOST'S WRAPPER (`run_under`, Bazel's word): the argv that + // reaches the machine an artifact runs on, prefixed to the runner. Empty + // = run here. A project states it for a team; `config.toml` states it for + // one machine; `MCPP_TARGET__RUN_UNDER` overrides both. + std::vector runUnder; + int runUnderLine = 0; // THE OLDEST OS RELEASE THIS ARTIFACT MUST RUN ON, for a target whose // compiler takes it as part of the triple. // @@ -1969,6 +1986,14 @@ struct Profile { bool debugDeclared = false; bool ltoDeclared = false; bool stripDeclared = false; + // `ndebug` and `trim_paths` (design 2026-10-23 §15.3-§15.4). Their + // defaults depend on the profile's NAME -- on for `release` and `dist`, + // off for `dev` and `debug` -- so "not written" is kept apart from + // either value and the default is applied where the name is known. + bool ndebug = false; + bool ndebugDeclared = false; + bool trimPaths = false; + bool trimPathsDeclared = false; // `dependency_linkage`, per profile (#519). // // DECLARED-OR-NOT IS LOAD-BEARING, and that is why there are two members diff --git a/modules/platform/src/env.cppm b/modules/platform/src/env.cppm index bcd015790..8a47c70c3 100644 --- a/modules/platform/src/env.cppm +++ b/modules/platform/src/env.cppm @@ -9,6 +9,7 @@ module; #include // _putenv_s #else #include // setenv / unsetenv +extern char** environ; #endif export module mcpp.platform.env; @@ -28,6 +29,12 @@ void set(const std::string& key, const std::string& value); // are one state there, and `get` answers nullopt for both on every platform. void unset(const std::string& key); +// Every variable whose name starts with `prefix`, as `NAME=value` in the +// order the process environment holds them, empty values left out. For an +// identity that has to change when any of a family of variables does +// (`MCPP_TARGET__SYSROOT`), where the names are not known in advance. +std::vector with_prefix(std::string_view prefix); + // ── Network policy: offline mode ────────────────────────────────────────── // // One process-wide knob answering "may mcpp reach the network at all?", read @@ -155,6 +162,24 @@ std::optional get(std::string_view key) { return std::string(v); } +std::vector with_prefix(std::string_view prefix) { + std::vector out; +#if defined(_WIN32) + char** env = _environ; +#else + char** env = environ; +#endif + for (; env && *env; ++env) { + std::string_view e(*env); + if (!e.starts_with(prefix)) continue; + auto eq = e.find('='); + if (eq == std::string_view::npos || eq + 1 == e.size()) continue; + out.emplace_back(e); + } + std::ranges::sort(out); + return out; +} + void set(const std::string& key, const std::string& value) { #if defined(_WIN32) _putenv_s(key.c_str(), value.c_str()); diff --git a/modules/platform/src/macos/macos.cppm b/modules/platform/src/macos/macos.cppm index 7b51243cc..96e1ffee8 100644 --- a/modules/platform/src/macos/macos.cppm +++ b/modules/platform/src/macos/macos.cppm @@ -69,6 +69,19 @@ std::string sdk_name_of_root(const std::filesystem::path& p); bool sdkroot_answers(const std::filesystem::path& candidate, std::string_view sdk); +// The version an SDK directory says it is, from its own `SDKSettings.json` +// (`"Version": "18.5"`). Empty when the file is absent or says nothing. Pure +// file reading, so it answers on every host. +std::string sdk_settings_version(const std::filesystem::path& sdkRoot); + +// Whether an SDK carries a slice for `target` (`x86_64-ios-simulator`, +// `arm64-macos`), read from the `targets:` list of its `libSystem.tbd`. The +// slice is what decides whether a row can be built against this SDK; a +// vendor's hardware roadmap does not (D16). nullopt when the stub cannot be +// read. +std::optional sdk_has_target(const std::filesystem::path& sdkRoot, + std::string_view target); + // Discover an Apple SDK path via `xcrun --show-sdk-path`. // Returns the SDK path if found, or nullopt. The default argument is // today's behaviour for today's callers. @@ -231,13 +244,18 @@ std::optional sdk_version(std::string_view sdk) { std::optional sdk_path(std::string_view sdk) { auto layout = sdk_layout(sdk); if (!layout) return std::nullopt; -#if defined(__APPLE__) // 1. Explicit override wins (matches clang's own SDKROOT handling) -- but // only for a request it answers. See `sdkroot_answers`. + // + // ON EVERY HOST. `SDKROOT` is Apple's own convention for naming an SDK, + // and an SDK a developer produced from their own Xcode on Linux is + // named the same way (design 2026-10-23 §4.4, D18). The steps below + // ask Apple's tools, which exist only on an Apple host. if (const char* env = std::getenv("SDKROOT"); env && *env) { std::filesystem::path p(env); if (std::filesystem::exists(p) && sdkroot_answers(p, sdk)) return p; } +#if defined(__APPLE__) // 2. xcrun — the canonical query. The named form is the only one that can // answer for a specific SDK; the generic form returns the ACTIVE // default, so it is tried only for the default request, where it is @@ -276,6 +294,45 @@ std::optional sdk_path(std::string_view sdk) { return std::nullopt; } +std::string sdk_settings_version(const std::filesystem::path& sdkRoot) { + std::ifstream is(sdkRoot / "SDKSettings.json", std::ios::binary); + if (!is) return {}; + std::string text((std::istreambuf_iterator(is)), std::istreambuf_iterator()); + auto key = text.find("\"Version\""); + if (key == std::string::npos) return {}; + auto q1 = text.find('"', text.find(':', key)); + if (q1 == std::string::npos) return {}; + auto q2 = text.find('"', q1 + 1); + if (q2 == std::string::npos) return {}; + return text.substr(q1 + 1, q2 - q1 - 1); +} + +std::optional sdk_has_target(const std::filesystem::path& sdkRoot, + std::string_view target) { + std::ifstream is(sdkRoot / "usr" / "lib" / "libSystem.tbd", std::ios::binary); + if (!is) return std::nullopt; + std::string line; + while (std::getline(is, line)) { + if (!line.starts_with("targets:")) continue; + // `targets: [ x86_64-ios-simulator, arm64-ios-simulator ]`, possibly + // wrapped onto following lines until the closing bracket. + std::string list = line; + while (list.find(']') == std::string::npos && std::getline(is, line)) list += line; + std::size_t pos = 0; + while ((pos = list.find(target, pos)) != std::string::npos) { + const auto end = pos + target.size(); + const bool startOk = pos == 0 || !(std::isalnum(static_cast(list[pos - 1])) + || list[pos - 1] == '-' || list[pos - 1] == '_'); + const bool endOk = end >= list.size() || !(std::isalnum(static_cast(list[end])) + || list[end] == '-' || list[end] == '_'); + if (startOk && endOk) return true; + pos = end; + } + return false; + } + return std::nullopt; +} + std::vector runtime_lib_dirs(const std::filesystem::path& toolchain_root) { std::vector dirs; diff --git a/modules/platform/src/shell.cppm b/modules/platform/src/shell.cppm index d620120bf..5807a7576 100644 --- a/modules/platform/src/shell.cppm +++ b/modules/platform/src/shell.cppm @@ -27,6 +27,13 @@ std::string quote(std::string_view s); std::string quote_windows(std::string_view s); std::string quote_posix(std::string_view s); +// AN ARGUMENT OF A COMMAND PRINTED FOR A PERSON TO PASTE (mcpp#832). Left as +// it is when the shell would read it as one word anyway, so the common +// message reads as it always did; quoted by this host's shell rules when it +// would not -- a space, a quote, a glob or a shell metacharacter. Every +// suggested command that carries a path builds the argument with this. +std::string paste_arg(std::string_view s); + // An argument that must survive TWO parsers: cmd.exe's, and then the child // program's own argv parsing. // @@ -116,6 +123,20 @@ std::string quote_windows(std::string_view s) { return out; } +std::string paste_arg(std::string_view s) { +#if defined(_WIN32) + constexpr bool is_windows = true; +#else + constexpr bool is_windows = false; +#endif + const bool plain = !s.empty() && std::ranges::all_of(s, [](unsigned char c) { + return std::isalnum(c) || c == '/' || c == '.' || c == '_' || c == '-' || c == ':' + || c == '@' || c == '+' || c == '=' || c == ',' || c == '%' || c == '~' + || (c == '\\' && is_windows) || c >= 0x80; + }); + return plain ? std::string(s) : quote(s); +} + std::string quote_posix(std::string_view s) { std::string out; out.reserve(s.size() + 2); diff --git a/modules/toolchain-model/src/base.cppm b/modules/toolchain-model/src/base.cppm new file mode 100644 index 000000000..3478f5a8a --- /dev/null +++ b/modules/toolchain-model/src/base.cppm @@ -0,0 +1,462 @@ +// mcpp.toolchain.base — the target base: what the manifest key `sysroot` +// names, as data (SPEC-006 §3.5; design 2026-10-23, "target base and +// execution host"). +// +// A SYSROOT IS A DELIVERY FORM, NOT A LAYER. It is the prebuilt supplier of a +// target's lower layers: it supplies `c-abi`, usually `kernel-abi`, and may +// supply `c++-abi`, `compiler-runtime` and platform libraries. The layers stay +// the closed five of SPEC-002; this module describes the form that supplies +// them, along four orthogonal attributes: +// +// source who stated it (override, project, machine default, engine +// default), in #755's source classes +// address how a driver is told about it — one renderer per kind +// layers what it supplies, measured (§4.3) or declared (`provides`) +// runtime what a produced artifact does with it: binding, floor, none +// +// Pure data and pure functions. Locating, measuring and installing live in +// `src/toolchain/base_probe.cppm`; deciding which statement applies lives in +// prepare. Every one of the readers that used to answer "which sysroot" for +// itself reads the `Resolution` this module defines. + +export module mcpp.toolchain.base; + +import std; +import mcpp.toolchain.triple; + +export namespace mcpp::toolchain::base { + +// The spelling a statement used. `Default` is the absence of a statement. +enum class Form : std::uint8_t { + Default = 0, + Package = 1, // `xim:glibc@2.39`, `acme:sysroot-rhel8@1.0` + Locator = 2, // `glibc@system`, `iphoneos@18.5`, `msvc@14.44` + Path = 3, // `{ path = "…" }`, `path:` in the environment + Empty = 4, // `""`: no prebuilt C library +}; + +enum class Address : std::uint8_t { + None, + GnuSysroot, // `--sysroot` for the driver and the linker + AppleIsysroot, // `-isysroot` and a deployment target + MsvcRoots, // the toolset and Windows SDK roots + Multilib, // `/{include,lib}/` of a bare-metal C library + DriverInternal, // an SDK toolchain that resolves its own system +}; + +enum class Runtime : std::uint8_t { + None, // nothing is loaded at run time (static, bare metal, wasm) + Binding, // PT_INTERP and RUNPATH name the base (a native build) + Floor, // the target's standard loader; the base is a lower bound +}; + +constexpr std::string_view form_name(Form f) { + switch (f) { + case Form::Default: return "default"; + case Form::Package: return "package"; + case Form::Locator: return "locator"; + case Form::Path: return "path"; + case Form::Empty: return "empty"; + } + return "default"; +} + +constexpr std::string_view address_name(Address a) { + switch (a) { + case Address::None: return "none"; + case Address::GnuSysroot: return "gnu-sysroot"; + case Address::AppleIsysroot: return "apple-isysroot"; + case Address::MsvcRoots: return "msvc-roots"; + case Address::Multilib: return "multilib"; + case Address::DriverInternal: return "driver-internal"; + } + return "none"; +} + +constexpr std::string_view runtime_name(Runtime r) { + switch (r) { + case Runtime::None: return "none"; + case Runtime::Binding: return "binding"; + case Runtime::Floor: return "floor"; + } + return "none"; +} + +// THE LOCATOR FAMILIES ARE THE INDUSTRY'S WORDS, NOT mcpp'S: the C library +// names a Linux row is built on, xcrun's SDK names, and the MSVC toolset. A +// family names a platform, so it must agree with the row it is written on. +inline constexpr std::string_view kLocatorFamilies[] = { + "glibc", "musl", "macosx", "iphoneos", "iphonesimulator", "msvc", +}; + +constexpr bool is_locator_family(std::string_view f) { + for (auto k : kLocatorFamilies) if (k == f) return true; + return false; +} + +constexpr bool is_apple_family(std::string_view f) { + return f == "macosx" || f == "iphoneos" || f == "iphonesimulator"; +} + +// One statement of a base, wherever it was written. +// +// `text` is the canonical spelling every reader and every report uses: +// `xim:glibc@2.39`, `glibc@system`, `path:/abs/dir`, or empty for the empty +// form. A table `{ path = "…" }` arrives here already absolute, as +// `path:`, because a relative path is relative to the file that wrote it +// and only the reader of that file knows which file that was. +struct Statement { + Form form = Form::Default; + std::string text; + std::string family; // Locator: one of kLocatorFamilies + std::string version; // Locator: "system" or a version + std::filesystem::path path; // Path + std::vector provides; // `mcpp:c-abi=` and the like + // Who said so, in #755's vocabulary: `env`, `manifest`, `toolchain`, + // `build-program`, `config`, `default`. + std::string originKind = "default"; + std::string originFile; + int originLine = 0; + std::string originKey; + + bool stated() const { return form != Form::Default; } +}; + +// The string grammar, shared by the manifest, `config.toml`, the environment +// and a toolchain phase. Errors name the accepted spellings. +// +// "" Empty +// path: Path +// :[@] Package +// @system Locator +// glibc@ / musl@ Package `xim:@` (SPEC-006 §2.3) +// macosx|iphoneos|iphonesimulator|msvc@ Locator with a version +inline std::expected parse_text(std::string_view text) { + Statement s; + s.text = std::string(text); + if (text.empty()) { s.form = Form::Empty; return s; } + if (text.starts_with("path:")) { + auto p = text.substr(5); + if (p.empty()) + return std::unexpected(std::string("`path:` names no directory")); + s.form = Form::Path; + s.path = std::filesystem::path(std::string(p)).lexically_normal(); + s.text = "path:" + s.path.generic_string(); + return s; + } + const auto at = text.find('@'); + const auto colon = text.find(':'); + if (colon != std::string_view::npos && (at == std::string_view::npos || colon < at)) { + // A package. Apple SDKs and the host's own C library are located, so + // the package spelling of a locator names something that cannot exist. + auto ns = text.substr(0, colon); + auto rest = text.substr(colon + 1); + auto name = rest.substr(0, rest.find('@')); + if (ns.empty() || name.empty()) + return std::unexpected(std::format( + "'{}' is not a package reference; expected `:[@]`", text)); + if (is_apple_family(name)) + return std::unexpected(std::format( + "'{}': an Apple SDK is located, not installed, so it has no package; " + "write \"{}@system\", \"{}@\" or {{ path = \"…\" }}", text, name, name)); + if (rest.ends_with("@system")) + return std::unexpected(std::format( + "'{}': `@system` is a locator and takes no namespace; write \"{}\"", + text, rest)); + s.form = Form::Package; + return s; + } + if (at != std::string_view::npos) { + auto family = text.substr(0, at); + auto version = text.substr(at + 1); + if (is_locator_family(family) && !version.empty()) { + if (version != "system" && (family == "glibc" || family == "musl")) { + s.form = Form::Package; + s.text = std::format("xim:{}", text); + return s; + } + s.form = Form::Locator; + s.family = std::string(family); + s.version = std::string(version); + return s; + } + } + return std::unexpected(std::format( + "'{}' is not a sysroot. A sysroot is a package (\"xim:glibc@2.39\"), a locator " + "(\"glibc@system\", \"musl@system\", \"macosx@system\", \"iphoneos@18.5\", " + "\"msvc@system\"), {{ path = \"\" }}, or \"\" for no prebuilt C library", text)); +} + +// ── The rows ──────────────────────────────────────────────────────────────── +// +// WHICH SPELLINGS A ROW ACCEPTS IS DATA, keyed on the row's platform class. +// Every row of one class takes the same base; a table per canonical name +// would repeat one entry thirty times and miss the rows a manifest names +// without the target vocabulary knowing them. + +constexpr std::uint8_t bit(Form f) { return static_cast(1u << static_cast(f)); } + +struct RowBase { + Address address = Address::None; + std::uint8_t forms = 0; // bit(Form) set + std::string_view family; // the row's locator family, or empty + std::string_view accepted; // the accepted spellings, for a refusal +}; + +inline RowBase row_base(const triple::Triple& t) { + const auto pkg = bit(Form::Package), loc = bit(Form::Locator), + path = bit(Form::Path), empty = bit(Form::Empty); + if (t.has_own_sysroot()) + return {Address::DriverInternal, static_cast(pkg | empty), {}, + "nothing new: the SDK toolchain carries its own system"}; + if (t.is_freestanding()) + return {Address::Multilib, static_cast(pkg | path | empty), {}, + "a package (\"xim:picolibc-riscv@1.8.12\"), { path = \"\" } or \"\""}; + // The Apple rows took a package and "" before they took a locator, and + // those spellings keep their meaning (§9). + if (t.os == "macos") + return {Address::AppleIsysroot, static_cast(loc | path | pkg | empty), "macosx", + "\"macosx@system\", \"macosx@\" or { path = \"\" }"}; + if (t.is_ios_simulator()) + return {Address::AppleIsysroot, static_cast(loc | path | pkg | empty), "iphonesimulator", + "\"iphonesimulator@system\", \"iphonesimulator@\" or { path = \"\" }"}; + if (t.is_ios()) + return {Address::AppleIsysroot, static_cast(loc | path | pkg | empty), "iphoneos", + "\"iphoneos@system\", \"iphoneos@\" or { path = \"\" }"}; + if (t.os == "windows" && t.is_msvc_env()) + return {Address::MsvcRoots, static_cast(pkg | loc | path), "msvc", + "\"msvc@system\", \"msvc@\", \"xim:msvc@\" or, for clang, " + "{ path = \"\" }"}; + if (t.os == "windows") + return {Address::DriverInternal, static_cast(pkg | empty), {}, + "a package or \"\": the MinGW toolchain carries its own system"}; + if (t.os == "linux" && t.is_musl()) + return {Address::GnuSysroot, static_cast(pkg | loc | path | empty), "musl", + "a package, \"musl@system\", { path = \"\" } or \"\""}; + if (t.os == "linux") + return {Address::GnuSysroot, static_cast(pkg | loc | path | empty), "glibc", + "a package (\"xim:glibc@2.39\"), \"glibc@system\", { path = \"\" } or \"\""}; + return {Address::GnuSysroot, static_cast(pkg | path | empty), {}, + "a package, { path = \"\" } or \"\""}; +} + +// The refusal for a statement this row cannot take, or nullopt. +inline std::optional refusal(const triple::Triple& t, const Statement& s) { + if (!s.stated()) return std::nullopt; + const auto row = row_base(t); + const auto shown = s.form == Form::Path + ? std::format("{{ path = \"{}\" }}", s.path.generic_string()) + : std::format("\"{}\"", s.text); + if ((row.forms & bit(s.form)) == 0) + return std::format("[target.{}].sysroot = {}: this row takes {}", + t.str(), shown, row.accepted); + if (s.form == Form::Locator && s.family != row.family) + return std::format( + "[target.{}].sysroot = {}: `{}` describes another platform; this row takes {}", + t.str(), shown, s.family, row.accepted); + if (row.address == Address::MsvcRoots && s.form == Form::Package + && !s.text.starts_with("xim:msvc@")) + return std::format("[target.{}].sysroot = {}: this row takes {}", + t.str(), shown, row.accepted); + return std::nullopt; +} + +// `{ path = "/" }` IS THE HOST'S C LIBRARY, AND IT HAS A NAME. Normalised to the +// row's `@system` so that the two spellings of one statement cannot +// resolve differently. +inline Statement normalise(const triple::Triple& t, Statement s) { + if (s.form != Form::Path || s.path != std::filesystem::path("/")) return s; + const auto row = row_base(t); + if (row.family != "glibc" && row.family != "musl") return s; + s.form = Form::Locator; + s.family = std::string(row.family); + s.version = "system"; + s.text = s.family + "@system"; + s.path.clear(); + return s; +} + +// ── Measurement data (§4.3) ──────────────────────────────────────────────── +// +// THE IMPLEMENTATION AND ITS VERSION ARE ANSWERED BY THE PREPROCESSOR, never by +// a directory layout: a C library defines its own identity macros, and the +// driver that will compile against it is the one that reads them. + +struct LibcFact { + std::string impl; // "glibc", "musl", "bionic", "picolibc", "newlib", "emscripten" + std::string version; // "2.31", or empty when the library does not say + std::string factName; // the key a report and a floor use +}; + +inline std::optional +classify_macros(const std::map>& m) { + auto get = [&](std::string_view k) -> std::string { + auto it = m.find(k); + return it == m.end() ? std::string{} : it->second; + }; + if (m.contains("__GLIBC__")) + return LibcFact{"glibc", std::format("{}.{}", get("__GLIBC__"), get("__GLIBC_MINOR__")), "glibc"}; + if (m.contains("__BIONIC__")) + return LibcFact{"bionic", get("__ANDROID_API__"), "android.api"}; + if (m.contains("__PICOLIBC__")) { + auto v = get("__PICOLIBC_VERSION__"); + if (v.size() >= 2 && v.front() == '"') v = v.substr(1, v.size() - 2); + return LibcFact{"picolibc", v, "picolibc"}; + } + if (m.contains("_NEWLIB_VERSION")) { + auto v = get("_NEWLIB_VERSION"); + if (v.size() >= 2 && v.front() == '"') v = v.substr(1, v.size() - 2); + return LibcFact{"newlib", v, "newlib"}; + } + if (m.contains("__EMSCRIPTEN__")) + return LibcFact{"emscripten", + std::format("{}.{}.{}", get("__EMSCRIPTEN_major__"), + get("__EMSCRIPTEN_minor__"), get("__EMSCRIPTEN_tiny__")), + "emscripten"}; + return std::nullopt; +} + +// `#define NAME VALUE` lines, as `-E -dM` prints them. +inline std::map> parse_defines(std::string_view out) { + std::map> m; + std::size_t pos = 0; + while (pos < out.size()) { + auto end = out.find('\n', pos); + auto line = out.substr(pos, end == std::string_view::npos ? std::string_view::npos : end - pos); + pos = end == std::string_view::npos ? out.size() : end + 1; + constexpr std::string_view kDef = "#define "; + if (!line.starts_with(kDef)) continue; + line.remove_prefix(kDef.size()); + auto sp = line.find(' '); + auto name = line.substr(0, sp); + if (name.find('(') != std::string_view::npos) continue; // function-like + auto value = sp == std::string_view::npos ? std::string_view{} : line.substr(sp + 1); + while (!value.empty() && (value.back() == '\r' || value.back() == ' ')) value.remove_suffix(1); + m.emplace(std::string(name), std::string(value)); + } + return m; +} + +// THE DEBIAN MULTIARCH TUPLE, from Debian's MultiarchTuples table. This is the +// one piece of layout knowledge the engine carries (D10): it is specified, and +// a gcc built without multiarch support cannot find `crt1.o` in a Debian or +// Ubuntu root without it (measured, scenario J). Empty for an unknown arch. +inline std::string multiarch_tuple(const triple::Triple& t) { + const bool musl = t.is_musl(); + struct Row { std::string_view arch, gnu, musl; }; + static constexpr Row kRows[] = { + {"x86_64", "x86_64-linux-gnu", "x86_64-linux-musl"}, + {"aarch64", "aarch64-linux-gnu", "aarch64-linux-musl"}, + {"i686", "i386-linux-gnu", "i386-linux-musl"}, + {"i386", "i386-linux-gnu", "i386-linux-musl"}, + {"riscv64", "riscv64-linux-gnu", "riscv64-linux-musl"}, + {"loongarch64", "loongarch64-linux-gnu", "loongarch64-linux-musl"}, + {"powerpc64le", "powerpc64le-linux-gnu", "powerpc64le-linux-musl"}, + {"s390x", "s390x-linux-gnu", "s390x-linux-musl"}, + {"armv7", "arm-linux-gnueabihf", "arm-linux-musleabihf"}, + }; + for (auto const& r : kRows) + if (t.arch == r.arch) return std::string(musl ? r.musl : r.gnu); + return {}; +} + +// Dotted numeric versions, compared numerically: "2.9" < "2.31". Anything +// that is not a number compares as 0, so a version that cannot be read never +// produces a refusal on its own. +inline int compare_versions(std::string_view a, std::string_view b) { + auto next = [](std::string_view& s) -> unsigned { + unsigned v = 0; + std::size_t i = 0; + while (i < s.size() && s[i] >= '0' && s[i] <= '9') { v = v * 10 + unsigned(s[i] - '0'); ++i; } + while (i < s.size() && s[i] != '.') ++i; + s.remove_prefix(i < s.size() ? i + 1 : i); + return v; + }; + while (!a.empty() || !b.empty()) { + auto x = next(a), y = next(b); + if (x != y) return x < y ? -1 : 1; + } + return 0; +} + +// The highest `GLIBC_x.y` a list of version needs names, or empty. +inline std::string highest_glibc(const std::vector& needs) { + std::string best; + for (auto const& n : needs) { + if (!n.starts_with("GLIBC_")) continue; + auto v = n.substr(6); + if (v.empty() || !(v[0] >= '0' && v[0] <= '9')) continue; + if (best.empty() || compare_versions(v, best) > 0) best = std::move(v); + } + return best; +} + +// ── The resolution ──────────────────────────────────────────────────────── + +struct MsvcRoots { + std::filesystem::path toolsDir; // /VC/Tools/MSVC/ + std::string toolsVersion; // "14.44.35207" + std::string compilerVersion; // cl.exe's file version (mcpp#746) + std::string origin; // "system" | "managed" | "path" + std::string product; // for the one printed line + std::filesystem::path sdkRoot; // /10 or the payload + std::string sdkVersion; // "10.0.26100.0" + std::filesystem::path redistDir; // the toolset's redistributable CRT +}; + +struct AppleSdk { + std::filesystem::path root; // the located SDK + std::string name; // "iphoneos" + std::string version; // "18.5", from SDKSettings.json or xcrun + bool cxxHeaders = false; // the C++ headers are the SDK's +}; + +// A toolchain-supplied glibc in the ecosystem's payload layout: headers and +// libraries in separate payloads, assembled onto the command line by hand. +struct PayloadPaths { + std::filesystem::path glibcInclude; // glibc headers (features.h, bits/) + std::filesystem::path glibcLib; // glibc runtime (libc.so, crt*.o, ld-linux) + std::filesystem::path linuxInclude; // linux kernel headers (linux/, asm/) +}; + +// THE ONE ANSWER. Every reader of "which sysroot" reads a field of this — the +// compile and link renderers, the std module, the scanner, the artifact's +// runtime, closure validation, hermeticity, the fingerprint, reports, pack and +// the runner. Defaults describe the ecosystem's base, so a build that states +// nothing renders exactly what it rendered before this existed. +struct Resolution { + Form form = Form::Default; + Address address = Address::None; + Runtime runtime = Runtime::None; + std::string text; // the statement's canonical spelling + // The directory a gnu-sysroot address hands the driver: the stated root, + // or the root a toolchain was built with (`-print-sysroot`). + std::filesystem::path root; + // The ecosystem's glibc in payload layout (the default base on Linux). + std::optional payload; + // A bare-metal C library in multilib layout. + std::filesystem::path multilibRoot; + std::filesystem::path multilibInclude; + std::filesystem::path multilibLib; + std::string package; // its package name (`picolibc-riscv`) + AppleSdk apple; + MsvcRoots msvc; + // Measured (§4.3). + std::string libc; // "glibc", "musl", … + std::string libcVersion; // "2.31" + bool multiarch = false; + std::string multiarchTuple; + // What a binding artifact loads: the base's dynamic loader and the + // directories its RUNPATH names. + std::filesystem::path loader; + std::vector runtimeDirs; + // Kernel headers the base lacks, supplied by the engine (§2.4, additive). + std::vector addedIncludes; + // Content identity: enters the runtime contract and so the fingerprint. + // Empty for the ecosystem default, whose identity is the contract itself. + std::string identity; + + bool stated() const { return form != Form::Default; } +}; + +} // namespace mcpp::toolchain::base diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index 75eb95116..c97ed9cf0 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -83,6 +83,15 @@ struct ToolchainLinkModel { // linux-headers supplement for a sysroot that lacks them). std::vector systemIncludes; + // DEBIAN MULTIARCH, FOR A DRIVER THAT DOES NOT KNOW IT (design 2026-10-23 + // §4.3, scenario J). gcc built without multiarch support finds neither + // `crt1.o` nor `bits/` in a Debian or Ubuntu root; the measurement records + // the tuple and these name its directories. Headers go AFTER the system + // directories (`-idirafter`), so libstdc++'s `#include_next` chains are + // untouched. Empty unless measured -- clang resolves multiarch itself. + std::vector multiarchIncludes; // -idirafter + std::vector multiarchLibDirs; // -B, -L, -rpath-link + // Rendering knobs derived from the toolchain at resolve time. // CLANG ON THE MSVC ABI: the toolset and SDK the build compiles against, // copied from the toolchain. Rendered by `msvc_driver_tokens`, not by @@ -93,6 +102,10 @@ struct ToolchainLinkModel { std::string winSdkVersion; std::string msvcCompilerVersion; + // The linker is handed `--sysroot` by the driver flag above only on + // clang; on gcc a stated base adds it explicitly (`link_tokens`). + bool linkerSysroot = false; + bool clangDriver = false; // clang: -isystem headers; gcc: -idirafter // gcc: -idirafter (…#include_next), -B/-L only bool clangWithCfg = false; // sibling .cfg exists (bundled LLVM) @@ -120,6 +133,8 @@ struct ToolchainLinkModel { ? "-isystem" : "-idirafter"; for (auto& inc : systemIncludes) out.push_back(incFlag + esc(inc)); + for (auto& inc : multiarchIncludes) + out.push_back("-idirafter" + esc(inc)); return out; } @@ -170,6 +185,18 @@ struct ToolchainLinkModel { std::vector out; if (mode == CLibMode::Sysroot) { out.push_back("--sysroot=" + esc(sysroot)); + // A STATED base tells the linker too (SPEC-006 §4.3). gcc's + // clean link specs drop `%(sysroot_spec)`, so without this ld + // never hears the sysroot and a sysroot's linker scripts + // (`GROUP ( /lib64/libc.so.6 ... )`) resolve on the host, or not + // at all (`cannot find /lib64/libm.so.6`, scenario C). clang hands + // its `--sysroot` to the linker itself. + if (linkerSysroot) out.push_back("-Wl,--sysroot=" + esc(sysroot)); + for (auto& dir : multiarchLibDirs) { + out.push_back("-B" + esc(dir)); + out.push_back("-L" + esc(dir)); + out.push_back("-Wl,-rpath-link," + esc(dir)); + } // Sysroot mode still needs the interpreter named explicitly when // we know it. mcpp replaces gcc's `*link:` to stop the payload's // accumulated rpath entries reaching artifacts, and the pristine @@ -445,10 +472,10 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { // cross-compile resolves by what it builds FOR. if (is_msvc_target(tc) || is_mingw_target(tc)) { if (is_msvc_target(tc) && tc.compiler == CompilerId::Clang) { - lm.msvcToolsDir = tc.msvcToolsDir; - lm.winSdkRoot = tc.windowsSdkRoot; - lm.winSdkVersion = tc.windowsSdkVersion; - lm.msvcCompilerVersion = tc.msvcCompilerVersion; + lm.msvcToolsDir = tc.base.msvc.toolsDir; + lm.winSdkRoot = tc.base.msvc.sdkRoot; + lm.winSdkVersion = tc.base.msvc.sdkVersion; + lm.msvcCompilerVersion = tc.base.msvc.compilerVersion; } return lm; } @@ -499,7 +526,7 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { }; auto payload_first = [&] { - auto& pp = *tc.payloadPaths; + auto& pp = *tc.base.payload; lm.mode = CLibMode::PayloadFirst; lm.crtDir = pp.glibcLib; lm.libDirs.push_back(pp.glibcLib); @@ -515,6 +542,30 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { auto sysroot_mode = [&](const std::filesystem::path& root) { lm.mode = CLibMode::Sysroot; lm.sysroot = root; + // A STATED BASE IS THE C LIBRARY, AND IT SAYS WHAT A BINDING ARTIFACT + // LOADS (design 2026-10-23 §4.5-§4.6). Its loader and the directories + // that hold its libc, measured where it was resolved, replace the + // ecosystem payload's -- which this artifact does not touch. A floor + // (a cross target) names neither: the pristine spec's standard loader + // path is the right one on the machine the artifact will run on. + // #755 reused this branch for a stated sysroot with the payload + // pointers cleared, so the artifact got the HOST's loader and the + // SubOS farm's libc (scenario D); that is what this replaces. + if (tc.base.stated()) { + lm.linkerSysroot = !lm.clangDriver; + if (tc.base.runtime == base::Runtime::Binding) { + lm.loader = tc.base.loader; + for (auto const& d : tc.base.runtimeDirs) lm.libDirs.push_back(d); + } + if (tc.base.multiarch && !tc.base.multiarchTuple.empty()) { + lm.multiarchIncludes.push_back(root / "usr" / "include" / tc.base.multiarchTuple); + lm.multiarchLibDirs.push_back(root / "usr" / "lib" / tc.base.multiarchTuple); + lm.multiarchLibDirs.push_back(root / "lib" / tc.base.multiarchTuple); + } + add_compiler_runtime_dir(); + for (auto const& inc : tc.base.addedIncludes) lm.systemIncludes.push_back(inc); + return; + } // Name the interpreter even here: see link_tokens. The payload is the // address (R6 — artifacts bind the payload, never the mutable view), // and it is known whenever a runtime binding resolved. @@ -535,35 +586,45 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { // below already says this about headers; it holds for libraries and // the loader too. if (!is_musl_target(tc) - && tc.payloadPaths && !tc.payloadPaths->glibcLib.empty()) { - lm.loader = resolve_loader(tc.payloadPaths->glibcLib, tc.targetTriple); - lm.libDirs.push_back(tc.payloadPaths->glibcLib); + && tc.base.payload && !tc.base.payload->glibcLib.empty()) { + lm.loader = resolve_loader(tc.base.payload->glibcLib, tc.targetTriple); + lm.libDirs.push_back(tc.base.payload->glibcLib); } add_compiler_runtime_dir(); // Supplement kernel headers when the sysroot lacks them (glibc's // local_lim.h needs ). Self-contained musl sysroots // ship their own; a cross target must not see host-arch headers. - if (!is_musl_target(tc) && tc.payloadPaths - && !tc.payloadPaths->linuxInclude.empty() + if (!is_musl_target(tc) && tc.base.payload + && !tc.base.payload->linuxInclude.empty() && !std::filesystem::exists(root / "usr" / "include" / "linux" / "limits.h")) - lm.systemIncludes.push_back(tc.payloadPaths->linuxInclude); + lm.systemIncludes.push_back(tc.base.payload->linuxInclude); }; if (lm.clangWithCfg) { // Bundled LLVM: payload first (PR #62 principle — the sysroot comes // from the toolchain payload, not from an environment directory), // then the macOS SDK, then a probed sysroot. - if (tc.payloadPaths) payload_first(); - else if (auto sdk = mcpp::platform::macos::sdk_path()) { + if (tc.base.payload) payload_first(); + else if (tc.base.stated() && tc.base.address == base::Address::AppleIsysroot) { + // A stated macOS SDK (`macosx@`, `{ path }`), located + // once by the base resolution. + lm.mode = CLibMode::Sysroot; + lm.sysroot = tc.base.root; + } + else if (auto sdk = mcpp::platform::is_macos ? mcpp::platform::macos::sdk_path() + : std::nullopt) { + // `macosx@system`: the SDK `xcrun` answers on an Apple host. Only + // there -- `sdk_path` reads `SDKROOT` on every host now, and an + // SDK must not become the sysroot of a Linux build. lm.mode = CLibMode::Sysroot; lm.sysroot = *sdk; } - else if (!tc.sysroot.empty()) sysroot_mode(tc.sysroot); - } else if (!tc.sysroot.empty()) { + else if (!tc.base.root.empty()) sysroot_mode(tc.base.root); + } else if (!tc.base.root.empty()) { // GCC (or clang without cfg): --sysroot is required for GCC's // include-fixed headers (stdlib.h wrapper). - sysroot_mode(tc.sysroot); - } else if (tc.payloadPaths) { + sysroot_mode(tc.base.root); + } else if (tc.base.payload) { payload_first(); } return lm; diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index c3cd904ff..b92d2272e 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -4,6 +4,7 @@ export module mcpp.toolchain.model; import std; import mcpp.toolchain.triple; +export import mcpp.toolchain.base; export namespace mcpp::toolchain { @@ -49,11 +50,7 @@ struct EnvVar { // ("glibc@2.39"). Resolved before payload probing from the root-selected // RuntimeBinding snapshot. Empty means no libc payload applies, and payload // resolution then declines rather than guessing. -struct PayloadPaths { - std::filesystem::path glibcInclude; // glibc headers (features.h, bits/) - std::filesystem::path glibcLib; // glibc runtime (libc.so, crt*.o, ld-linux) - std::filesystem::path linuxInclude; // linux kernel headers (linux/, asm/) -}; +using PayloadPaths = base::PayloadPaths; struct Toolchain { CompilerId compiler = CompilerId::Unknown; @@ -189,58 +186,14 @@ struct Toolchain { // implementation needs no engine change. bool kernelAbiIsOpenkal = false; - std::filesystem::path sysroot; // -print-sysroot output (or empty) - std::optional payloadPaths; // fine-grained sysroot from xpkgs - // The TARGET's C library, for targets whose row in kKnownTargets names one - // (today: bare metal). Resolved during prepare, where the config is - // already open, rather than in the flag builder — computing it there would - // put a config load on a path that runs per build and answers a question - // prepare has already answered. - // - // Empty for every hosted target: they get their libc from the compiler - // payload or from PayloadPaths, which is why nobody ever had to write one - // down for them. - std::filesystem::path targetSysrootRoot; - std::filesystem::path targetSysrootInclude; - // The C library's package NAME (`picolibc-riscv`), recorded beside its - // paths so a build program can be told which C library it is without - // reverse-engineering the store layout from the path above. Empty on the - // zero-libc tier and on hosted targets. - std::string targetSysrootPkg; - std::filesystem::path targetSysrootLib; - // THE APPLE SDK THIS TARGET WAS LOCATED AGAINST, and it is a different - // axis from the three paths above. - // - // `targetSysroot*` name a PACKAGE's directories -- a C library this - // ecosystem ships and can therefore version. An Apple SDK is located, not - // installed: the iPhoneOS and iPhoneSimulator headers and stub libraries - // ship inside Xcode and are not redistributable, so there is nothing for - // a package to name and the row carries no `sysroot` entry. What it - // carries instead is this, resolved once where the target is known. - // - // Empty for every non-Apple target AND for a native macOS build, which - // needs none: that build reads the payload's own `clang++.cfg`, and - // `post_install.cppm` wrote the located macOS SDK into it. An Apple CROSS - // target suppresses that cfg (`--no-default-config`) precisely because it - // names the wrong platform -- measured 2026-09-11 on macos-15, where an - // iOS-simulator link with the right `-isysroot` on the command line still - // picked the cfg's SDK: - // - // ld64.lld: error: .../MacOSX.sdk/usr/lib/libc++.tbd(...) is - // incompatible with arm64 (iOS Simulator18.0.0) - // - // so the SDK has to travel explicitly, on the compile side as well as the - // link side. - std::filesystem::path appleSdkRoot; - // THE C++ HEADERS ARE THE SDK'S, on an Apple cross target whose C++ - // runtime is the SDK's libc++ and whose graph does not import `std`. - // Decided by prepare, read by the compile-flag builder: with no graph - // package the runtime is the SDK's by construction, and the headers - // follow the runtime. When the graph imports `std` the payload's module - // and headers stay in use over the SDK's dylib, which is the pairing - // that fails at link on the first inline path the older dylib lacks, and - // prepare reports that once rather than refusing what built yesterday. - bool appleSdkCxxHeaders = false; + // THE TARGET BASE: what this build compiles and links against below the + // compiler -- the sysroot, the ecosystem's glibc payload, a bare-metal C + // library, an Apple SDK or an MSVC toolset and Windows SDK. Resolved once + // by prepare (design 2026-10-23, §4) and read by every renderer, the + // artifact's runtime, closure validation, the fingerprint and the reports. + // These were five groups of fields here, written by seven code paths, and + // the defects of mcpp#755's sysroot were at their seams. + base::Resolution base; std::vector compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools std::vector linkRuntimeDirs; // -L/-rpath dirs for produced binaries // Environment the toolchain's tools need when invoked (set on the ninja @@ -275,47 +228,7 @@ struct Toolchain { // GCC/libc++ answer 20; MSVC answers 20 from cl 19.38 (VS 2022 17.8, // microsoft/STL#3977) and 23 below that. int importStdMinLevel = 0; - // The Windows SDK this toolchain compiles and links against - // ("10.0.26100.0"), once resolved. Empty everywhere else — and empty on - // Windows too when no SDK was found, which detection tolerates so that - // toolchain SELECTION still works on an SDK-less box. - // - // Recorded rather than re-derived because two different questions read it - // and they must not be able to disagree: the compile environment - // (INCLUDE/LIB) and the runtime identity (`ucrt@`, which enters - // the runtime contract hash). Deriving the second from a second search is - // how the SDK came to have no identity in the first place. - std::string windowsSdkVersion; - - // THE MSVC TOOLSET A CLANG `*-windows-msvc` BUILD COMPILES AGAINST, and the - // SDK that follows it: the row's sysroot. Chosen once by prepare from - // `[target.].sysroot` (default `msvc@system`) and handed to the - // driver explicitly by the link model, so clang does not search the - // machine a second time. Empty on every other row, where nothing changes; - // the cl.exe row carries the same answer in its own path and envOverrides. - std::filesystem::path msvcToolsDir; // /VC/Tools/MSVC/ - std::string msvcToolsVersion; // "14.44.35207" - // The version of that toolset's cl.exe ("19.44.35211"), from its file - // version: what clang would otherwise read from the same file itself, and - // what a toolset directory name does not determine (mcpp#746). - std::string msvcCompilerVersion; - std::string msvcOrigin; // "system" | "managed" - std::string msvcProduct; // for the one printed line - std::filesystem::path windowsSdkRoot; // /10 or the payload - // THE TOOLSET'S OWN REDISTRIBUTABLE CRT DIRECTORY - // (`VC\Redist\MSVC\\\Microsoft.VC*.CRT`), on an MSVC-ABI row of - // EITHER driver — cl.exe or clang++ targeting `*-windows-msvc`. - // - // Its own field rather than a reading of `linkRuntimeDirs`, because that - // field means something else on the two rows: for cl it happens to hold - // exactly this directory, but for clang it holds the LLVM payload's OWN - // runtime directories (`discover_link_runtime_dirs`, clang.cppm), and - // reading it for staging there would copy LLVM's files into a Windows - // program's `bin/`. Empty when the resolved toolset ships no - // redistributable (`msvc_abi_default_contract` then defaults to - // host-coupled rather than toolchain-coupled, and an explicit - // toolchain-coupled contract is refused, naming this field empty). - std::filesystem::path msvcRedistDir; + // Something about HOW this toolchain was resolved that the user has to be // told, but which is not a failure. Non-empty ⇒ the caller MUST surface it. // diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index f3c2d8f8b..4517022d0 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -802,8 +802,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // an Intel one needs `x86_64`. A single row would describe a simulator half // the machines cannot run. // - // ONE `verified` AND ONE `preview`, AND THE DIFFERENCE IS THE HOST'S - // ARCHITECTURE RATHER THAN ANYTHING ABOUT THE ROWS. + // BOTH `verified`, AND THE SECOND ONE WAS RUN ON ANOTHER MACHINE. // // `aarch64-ios-sim` was built AND RUN. Measured 2026-09-11 on macos-15 // (Xcode 16.4, iPhoneSimulator 18.5, `ios_deployment_target = "18.0"`): @@ -816,12 +815,15 @@ inline constexpr TargetInfo kKnownTargets[] = { // made `simctl-run` a boot-and-spawn wrapper rather than a bundle builder. // // `x86_64-ios-sim` builds and its artefact is correct -- Mach-O x86_64, - // the same `platform 7`, the same floor -- and nothing ran it, because a - // SIMULATOR RUNS THE HOST'S ARCHITECTURE and the runner is Apple silicon. - // That is a property of the machine the measurement was taken on, so the - // row stays `preview` until an Intel host takes it. + // the same `platform 7`, the same floor -- and a SIMULATOR RUNS THE HOST'S + // ARCHITECTURE, so the Apple-silicon runner that builds it cannot run it. + // ci-macos-ios.yml builds it there and hands the artefact to a + // `macos-15-intel` job, which spawns it on that host's simulator and reads + // `1-2-3` and status 0 (design 2026-10-23 §2.6, S15). The row can be built + // because the SDK carries an x86_64 slice, which is measured; whether the + // vendor still sells the hardware is not a property of the row. { "aarch64-ios-sim", "verified", "", "llvm@23.1.3","", false }, - { "x86_64-ios-sim", "preview", "", "llvm@23.1.3","", false }, + { "x86_64-ios-sim", "verified", "", "llvm@23.1.3","", false }, // WEB IS THE OUTLIER, AND IT IS THE ONLY ONE OF THE THREE THAT CHANGES THE // MODEL RATHER THAN EXTENDING A TABLE. A new arch (`wasm32`), a new os diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 74ebc5127..99a16a6a3 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.10.22.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.23.1"; } // namespace mcpp diff --git a/src/build/build_database.cppm b/src/build/build_database.cppm index a34401492..fbdcdabb8 100644 --- a/src/build/build_database.cppm +++ b/src/build/build_database.cppm @@ -245,7 +245,7 @@ nlohmann::json toolchain_json(const mcpp::toolchain::Toolchain& tc, {"target", std::string(compilerTriple)}, {"config-files", config_files(tc, invocations)}, }; - if (!tc.sysroot.empty()) j["sysroot"] = native_string(tc.sysroot); + if (!tc.base.root.empty()) j["sysroot"] = native_string(tc.base.root); if (!tc.stdlibId.empty()) { nlohmann::json stdlib{{"name", std::string(stdlib_name(tc.stdlibId))}}; if (!tc.stdlibVersion.empty()) stdlib["version"] = tc.stdlibVersion; diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index dbca7f03a..96b7f48e9 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -186,6 +186,8 @@ struct BuildAxes { bool debug = false; bool lto = false; bool strip = false; + bool ndebug = false; + bool trimPaths = false; // Position-independent code, and it is here because it was MISSING. // // `-fPIC` is whole-build: one shared link unit anywhere and every object @@ -337,6 +339,8 @@ nlohmann::json to_json(const BuildAxes& b, const PackageAxes& p) { {"debug", b.debug}, {"lto", b.lto}, {"strip", b.strip}, + {"ndebug", b.ndebug}, + {"trim_paths", b.trimPaths}, {"pic", b.pic}, {"crt", b.crt}, }; @@ -386,6 +390,9 @@ std::string key_hex(const BuildAxes& b, const PackageAxes& p) { put(s, "debug", b.debug ? "1" : "0"); put(s, "lto", b.lto ? "1" : "0"); put(s, "strip", b.strip ? "1" : "0"); + // Only when on, so a key computed before these existed is unchanged. + if (b.ndebug) put(s, "ndebug", "1"); + if (b.trimPaths) put(s, "trim", "1"); put(s, "pic", b.pic ? "1" : "0"); put(s, "crt", b.crt); // D @@ -550,6 +557,8 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, b.debug = rootManifest.buildConfig.debug; b.lto = rootManifest.buildConfig.lto; b.strip = rootManifest.buildConfig.strip; + b.ndebug = rootManifest.buildConfig.ndebug; + b.trimPaths = rootManifest.buildConfig.trimPaths; return b; } diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 97bc94dc2..3b23073e1 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -140,13 +140,37 @@ std::string toolchain_request_identity() { std::string cli; if (const char* e = std::getenv("MCPP_TOOLCHAIN"); e) cli = e; std::string machineDefault; + // THE TARGET BASE AND THE EXECUTION HOST, FROM WHEREVER THEY CAN BE STATED + // OUTSIDE THE MANIFEST (design 2026-10-23 §3.3, §3.7): the overrides and + // this machine's `[target.]` defaults. A fast path replays a build + // without resolving its base, so a change to any of them is a miss. + std::string targets; std::error_code ec; const auto configFile = mcpp::home::root() / "config.toml"; + auto target_table = [&](const std::filesystem::path& file) { + auto doc = mcpp::libs::toml::parse_file(file); + if (!doc || !doc->get_table("target")) return; + std::ifstream is(file, std::ios::binary); + std::string text((std::istreambuf_iterator(is)), std::istreambuf_iterator()); + std::uint64_t h = 1469598103934665603ull; + for (unsigned char c : text) { h ^= c; h *= 1099511628211ull; } + targets += std::format("{:016x}", h); + }; if (std::filesystem::exists(configFile, ec)) { if (auto doc = mcpp::libs::toml::parse_file(configFile)) machineDefault = doc->get_string("toolchain.default").value_or(""); + target_table(configFile); } - return std::format("cli={};default={}", cli, machineDefault); + if (std::filesystem::exists(mcpp::home::system_config_file(), ec)) + target_table(mcpp::home::system_config_file()); + std::string overrides; + for (auto const& e : mcpp::platform::env::with_prefix("MCPP_TARGET_")) + if (e.find("_SYSROOT=") != std::string::npos || e.find("_RUN_UNDER=") != std::string::npos) + overrides += e + ";"; + if (const char* s = std::getenv("MCPP_SYSROOT"); s && *s) overrides += std::format("MCPP_SYSROOT={};", s); + if (overrides.empty() && targets.empty()) + return std::format("cli={};default={}", cli, machineDefault); + return std::format("cli={};default={};base={}{}", cli, machineDefault, overrides, targets); } // THE ENGINE THAT WROTE A BUILD RECORD. @@ -886,6 +910,141 @@ write_runtime_files_list( return listPath; } +// THE RUNNER CONTRACT (design 2026-10-23 §4.7; SPEC-002 §6). +// +// An artifact is executed as ` `. The +// engine knows no runner and no execution host by name: it hands every one +// of them the same facts, and a runner that reads none of them is unaffected. +// +// MCPP_RUN_TRIPLE the target, in mcpp's canonical spelling +// MCPP_RUN_KIND `exe`, or `bundle` for a directory +// MCPP_RUN_MODE `run` or `test` +// MCPP_RUN_ID unique per execution: runners may run concurrently +// MCPP_RUN_CLOSURE a JSON manifest of what the execution needs +// MCPP_SYSROOT the stated target base, when there is one +// +// The closure manifest is versioned (`schema`), names the artifact, every +// file it loads or reads from its own directory (the entries of +// MCPP_RUNTIME_FILES, which stays for runners written against it), and the +// base's root -- what a runner that moves the artifact to another machine has +// to move with it. +constexpr int kRunClosureSchema = 1; + +std::expected +write_run_closure(const mcpp::build::BuildContext& ctx, + const std::filesystem::path& artifact, + const std::vector>& carried, + std::string_view mode, std::string_view triple) { + namespace fs = std::filesystem; + std::error_code ec; + auto rel = artifact.lexically_normal().lexically_relative(ctx.outputDir.lexically_normal()); + if (rel.empty() || *rel.begin() == "..") rel = fs::path("distributable") / artifact.filename(); + auto path = ctx.outputDir / ".mcpp-runtime-files" / rel; + path += ".closure.json"; + fs::create_directories(path.parent_path(), ec); + auto quote = [](std::string_view s) { + std::string o = "\""; + for (char c : s) { + if (c == '"' || c == '\\') { o += '\\'; o += c; } + else if (static_cast(c) < 0x20) o += std::format("\\u{:04x}", static_cast(c)); + else o += c; + } + return o + "\""; + }; + std::string j = std::format("{{\n \"schema\": {},\n \"triple\": {},\n \"mode\": {},\n" + " \"artifact\": {},\n \"kind\": {},\n \"files\": [", + kRunClosureSchema, quote(triple), quote(mode), + quote(artifact.generic_string()), + quote(fs::is_directory(artifact, ec) ? "bundle" : "exe")); + for (std::size_t i = 0; i < carried.size(); ++i) + j += std::format("{}\n {{ \"dest\": {}, \"source\": {} }}", i ? "," : "", + quote(carried[i].first), quote(carried[i].second.generic_string())); + j += std::format("{}],\n \"sysroot\": {}\n}}\n", carried.empty() ? "" : "\n ", + quote(ctx.tc.base.stated() ? ctx.tc.base.root.generic_string() : std::string{})); + std::ofstream os(path, std::ios::binary | std::ios::trunc); + os << j; + os.flush(); + if (!os) + return std::unexpected(std::format( + "could not write the run closure '{}' a runner receives as MCPP_RUN_CLOSURE", + path.string())); + return path; +} + +std::vector> +run_contract_env(const mcpp::build::BuildContext& ctx, const std::filesystem::path& artifact, + std::string_view mode, std::string_view triple, + const std::filesystem::path& closure) { + static std::atomic counter{0}; + std::error_code ec; + std::vector> env{ + {"MCPP_RUN_TRIPLE", std::string(triple)}, + {"MCPP_RUN_KIND", std::filesystem::is_directory(artifact, ec) ? "bundle" : "exe"}, + {"MCPP_RUN_MODE", std::string(mode)}, + {"MCPP_RUN_ID", std::format("{:x}-{}", + static_cast( + std::chrono::system_clock::now().time_since_epoch().count()), + counter.fetch_add(1))}, + {"MCPP_RUN_CLOSURE", closure.string()}, + }; + if (ctx.tc.base.stated() && !ctx.tc.base.root.empty()) + env.emplace_back("MCPP_SYSROOT", ctx.tc.base.root.string()); + return env; +} + +// `run_under`, its program located as a runner's is (the payloads the project +// declared, then PATH). An empty result with an empty error is "this +// machine". +struct ExecutionHost { + std::vector prefix; + std::string error; +}; + +ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string_view tripleKey) { + ExecutionHost h; + if (ctx.runUnder.empty()) return h; + h.prefix = ctx.runUnder; + const char* pathEnv = std::getenv("PATH"); + auto found = mcpp::build::runner_lookup::locate(h.prefix.front(), ctx.xlingsDepBinDirs, + pathEnv ? pathEnv : ""); + if (!found.program) { + h.error = std::format( + "{}\n `run_under` comes from {}. Install the program (for example through " + "[xlings.workspace]), override it on this machine with {}=, or clear it " + "with {}=none.", + mcpp::build::runner_lookup::not_found_message(tripleKey, h.prefix.front(), found.searched), + ctx.runUnderOrigin, std::format("MCPP_TARGET_{}_RUN_UNDER", [&] { + std::string s; + for (char c : tripleKey) + s += (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A') + : ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_'; + return s; + }()), "the same variable"); + h.prefix.clear(); + return h; + } + h.prefix.front() = found.program->string(); + return h; +} + +// THE PHYSICAL BOUNDARY (§3.8): an artifact no path from this machine can +// execute is refused before anything is spawned, naming the key that adds one. +// Today that is an Apple artifact on a host that is not macOS: the simulator +// and the OS run only there. `--no-runner` states that the triple is native +// here, and is believed. +std::optional unreachable_execution(const mcpp::build::BuildContext& ctx, + std::string_view tripleKey, bool noRunner) { + if (noRunner || !ctx.runUnder.empty() || mcpp::platform::is_macos) return std::nullopt; + auto t = mcpp::toolchain::triple::parse(tripleKey); + if (!t || !t->is_apple()) return std::nullopt; + return std::format( + "cannot run {} artifacts on this host: {} runs only on macOS.\n" + " Name an execution host: [target.{}] run_under = [\"remote-run\", \"\", \"--\"]\n" + " (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine).", + tripleKey, t->is_ios_simulator() ? "the iOS Simulator" : t->is_ios() ? "iOS" : "macOS", + tripleKey); +} + // Compile a prepared BuildContext. Shared between `mcpp build` and `mcpp run` // so the latter doesn't call prepare_build twice (and re-print the toolchain // resolution banner). @@ -1159,7 +1318,7 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache, ctx.depSourceRoots, // #544: the run fast path declines an entry whose // target has a runner declared — see the field. - !choose_runner(ctx).tmpl.empty(), + !choose_runner(ctx).tmpl.empty() || !ctx.runUnder.empty(), // …and one written by a build that left a run-tier // tool uninstalled, for the same reason. ctx.runTierPending, @@ -1924,6 +2083,11 @@ export std::optional try_fast_build(const std::filesystem::path& projectRoo } } if (!match) return fast_path_declined("build", "no recorded build matches this request"); + // A STATED TARGET BASE IS A TREE THIS PATH CANNOT SEE CHANGE (design + // 2026-10-23 §4.9): its identity is measured by prepare, and a sysroot + // rewritten in place is a different base. + if (match->runtimeBinding && !match->runtimeBinding->baseIdentity.empty()) + return fast_path_declined("build", "a stated target base is measured on the full path"); // The gates every fast path shares (see admit_recorded_build); what is left // here is the project's own tree. @@ -2148,6 +2312,8 @@ std::optional try_fast_run(const std::filesystem::path& projectRoot, // other door wraps it would make the second `mcpp run` behave differently // from the first. The entry records the fact; the fast path declines. if (match->runnerDeclared) return fast_path_declined("run", "a runner is declared"); + if (match->runtimeBinding && !match->runtimeBinding->baseIdentity.empty()) + return fast_path_declined("run", "a stated target base is measured on the full path"); // The same reasoning one axis over: this entry was written by a verb that // installed less than a run needs, so taking it would execute with a // declared tool absent. prepare_build provisions the difference. @@ -2345,7 +2511,24 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, format_name, format_name, choice.tripleKey, format_name); return 126; } + if (isRunSlot) { + if (auto why = unreachable_execution(ctx, choice.tripleKey, no_runner)) { + std::println(stderr, "error: {}", *why); + return 2; + } + } + // The execution host's wrapper, in front of everything (§3.7). + // `--no-runner` states the triple is native here: neither applies. + ExecutionHost host; + if (!no_runner) { + host = locate_run_under(ctx, choice.tripleKey); + if (!host.error.empty()) { + std::println(stderr, "error: {}", host.error); + return 2; + } + } std::optional runtimeFilesList; + std::vector> carriedFiles; if (!choice.tmpl.empty()) { // The program is located by mcpp, not by posix_spawnp: a declared // payload's bin/ and then its root, then PATH — see runner_lookup for the shim @@ -2369,10 +2552,9 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, for (auto& a : passthrough) argv.push_back(a); // What the runner has to carry with the artifact. A distributable // holds its own files, so its list is empty and still exists. - auto listed = write_runtime_files_list( - ctx, exe, - format_name.empty() ? runtime_files_for(ctx, exe) - : std::vector>{}); + carriedFiles = format_name.empty() ? runtime_files_for(ctx, exe) + : std::vector>{}; + auto listed = write_runtime_files_list(ctx, exe, carriedFiles); if (!listed) { std::println(stderr, "error: {}", listed.error()); return 1; @@ -2397,6 +2579,22 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, mcpp::ui::status("Running", std::format("`{}`", mcpp::ui::shorten_path(exe, pathCtx))); } + std::optional runClosure; + if (!host.prefix.empty()) { + if (choice.tmpl.empty()) carriedFiles = runtime_files_for(ctx, exe); + argv.insert(argv.begin(), host.prefix.begin(), host.prefix.end()); + std::string shown; + for (auto const& a : ctx.runUnder) shown += (shown.empty() ? "" : " ") + a; + mcpp::ui::status("Via", std::format("{} [{}]", shown, ctx.runUnderOrigin)); + } + if (!host.prefix.empty() || !choice.tmpl.empty()) { + auto closure = write_run_closure(ctx, exe, carriedFiles, "run", choice.tripleKey); + if (!closure) { + std::println(stderr, "error: {}", closure.error()); + return 1; + } + runClosure = *closure; + } run_separator(); std::vector> childEnv; @@ -2407,6 +2605,9 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, for (auto& kv : compute_subos_env(ctx.plan)) childEnv.push_back(std::move(kv)); if (runtimeFilesList) childEnv.emplace_back(std::string(kRuntimeFilesEnv), runtimeFilesList->string()); + if (runClosure) + for (auto& kv : run_contract_env(ctx, exe, "run", choice.tripleKey, *runClosure)) + childEnv.push_back(std::move(kv)); // Direct exec (no /bin/sh): the loader env reaches ONLY the target child, // never mcpp or a host shell. Fixes the bundled-glibc-vs-host-libtinfo @@ -2422,7 +2623,7 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, const int rc = mcpp::platform::process::run_foreground(argv, childEnv, &spawnErr); if (spawnErr != 0) { using namespace mcpp::build::runner_lookup; - if (!choice.tmpl.empty()) + if (!choice.tmpl.empty() || !host.prefix.empty()) std::println(stderr, "error: {}", spawn_failed_message(argv.front(), spawnErr)); else if (classify(spawnErr) == SpawnClass::Unloadable) std::println(stderr, "error: {}", @@ -2939,6 +3140,8 @@ struct TestBuild { // (#544): every test of one build shares a target. RunnerChoice runnerChoice; std::vector runnerTmpl; + // The execution host's wrapper (`run_under`), located once (§3.7). + std::vector runUnderPrefix; // Non-empty ⇒ no test is spawned; every one is reported NotRun with it. std::string invocationNotRunReason; // Non-zero: there is nothing to execute the tests with, and this is the @@ -3145,6 +3348,14 @@ static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool tb.runnerFatal = 2; return; } + // Not run, with the reason, as a missing runner is: the tests built. + if (auto why = unreachable_execution(*ctx, runnerChoice.tripleKey, testOpts.noRunner)) + tb.invocationNotRunReason = *why; + if (!testOpts.noRunner) { + auto host = locate_run_under(*ctx, runnerChoice.tripleKey); + if (!host.error.empty()) tb.invocationNotRunReason = host.error; + tb.runUnderPrefix = std::move(host.prefix); + } tb.runnerTmpl = runnerChoice.tmpl; auto& runnerTmpl = tb.runnerTmpl; if (!runnerTmpl.empty()) { @@ -3498,6 +3709,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, if (runnerTmpl.empty()) argv.push_back(exe.string()); else argv = mcpp::freestanding::expand(runnerTmpl, exe); for (auto& a : passthrough) argv.push_back(a); + argv.insert(argv.begin(), tb.runUnderPrefix.begin(), tb.runUnderPrefix.end()); std::vector> childEnv; if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty()) @@ -3509,13 +3721,18 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // ...and, through a runner, the files the test carries with it, as // `mcpp run` hands them over (see `runtime_files_for`). Written here, // in the single-threaded pass, one list per test program. - if (!runnerTmpl.empty()) { + if (!runnerTmpl.empty() || !tb.runUnderPrefix.empty()) { std::vector> carried; with_member(*ctx, owner, [&] { carried = runtime_files_for(*ctx, exe, owner); }); if (auto listed = write_runtime_files_list(*ctx, exe, carried)) childEnv.emplace_back(std::string(kRuntimeFilesEnv), listed->string()); else if (invocationNotRunReason.empty()) invocationNotRunReason = listed.error(); + if (auto closure = write_run_closure(*ctx, exe, carried, "test", runnerChoice.tripleKey)) + for (auto& kv : run_contract_env(*ctx, exe, "test", runnerChoice.tripleKey, *closure)) + childEnv.push_back(std::move(kv)); + else if (invocationNotRunReason.empty()) + invocationNotRunReason = closure.error(); } // Prepend the toolchain's environment's bin to the CHILD PATH so test diff --git a/src/build/flags.cppm b/src/build/flags.cppm index b86eddb49..bd0296134 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -645,7 +645,7 @@ std::string program_cxx_runtime(const mcpp::manifest::Manifest& m, mcpp::toolchain::is_msvc_target(tc) ? std::optional(dist::msvc_abi_default_contract( mcpp::toolchain::msvc_wants_static_crt(bc.linkage, bc.cxxRuntime), - !tc.msvcRedistDir.empty())) + !tc.base.msvc.redistDir.empty())) : std::nullopt; const auto contracts = dist::role_contracts( dist::ContractStatement{ @@ -884,7 +884,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // READ, NOT RE-DERIVED. `prepare` located this once, where the target // and the machine's Xcode are both known, and refused there if it was // absent -- so a second `sdk_path()` call here could only disagree. - hopt.appleSdkRoot = plan.toolchain.appleSdkRoot; + hopt.appleSdkRoot = plan.toolchain.base.apple.root; // THE SAME EXPRESSION THE LINK SIDE ASKS, twenty lines further down // (`plan.targetSide.cAbi.prebuilt()`). Reading one value at both sites // is what makes it impossible for them to disagree — which they did, @@ -894,7 +894,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // in for. The two differ on a hosted target whose C library is a // located SDK while a package supplies libc++ (mcpp#630, §5). hopt.cxxFromGraph = plan.targetSide.cxx.fromGraph(); - hopt.appleSdkCxxHeaders = plan.toolchain.appleSdkCxxHeaders; + hopt.appleSdkCxxHeaders = plan.toolchain.base.apple.cxxHeaders; compile_toolchain_flags = mcpp::toolchain::render_tokens( mcpp::toolchain::host_compile_tokens(plan.toolchain, hopt, ninjaEsc)); for (auto const& w : mcpp::toolchain::apple_float_macro_words(plan.toolchain)) @@ -1142,6 +1142,28 @@ CompileFlags compute_flags(const BuildPlan& plan) { // `/LTCG` on the link and the archive; before, it was not spelled at all, // and a build with `lto = true` reported `+ lto` while doing none. if (prof.lto) opt_flag += isMsvcDialect ? " /GL" : " -flto"; + // `ndebug` (design 2026-10-23 §15.4): the profiles that produce what is + // shipped define NDEBUG for every unit of the graph, as CMake's Release, + // Meson's `b_ndebug=if-release` and Cargo's release do. + if (prof.ndebug) opt_flag += isMsvcDialect ? " /DNDEBUG" : " -DNDEBUG"; + // `trim_paths` (§15.3, mcpp#832): every absolute prefix an artifact would + // carry -- in `__FILE__`, `std::source_location`, assertion messages and + // debug information -- replaced by what it IS. One flag covers all of + // them on gcc and clang. cl.exe has no documented equivalent, and a build + // that claimed to trim and did not would be worse than one that says so. + if (prof.trimPaths && !plan.trimMap.empty()) { + if (isMsvcDialect) { + // A note, not a degradation: nothing the build does changes, and + // `--strict` must not start refusing every cl.exe release build. + mcpp::diag::note("build/trim_paths", + "trim_paths is on, and cl.exe has no documented switch that maps source " + "paths: the artifact keeps the absolute paths it was built from (clang on " + "`*-windows-msvc` trims them)"); + } else { + for (auto const& [from, to] : plan.trimMap) + opt_flag += " -ffile-prefix-map=" + escape_path(from) + "=" + to; + } + } // MSVC baseline: /nologo /EHsc /utf-8 (dialect alwaysFlags) + the CRT // model — /MD by default, /MT when either knob asks for the static CRT @@ -1339,7 +1361,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { const std::optional msvcAbiDefault = mcpp::toolchain::is_msvc_target(plan.toolchain) ? std::optional(dist::msvc_abi_default_contract( - msvcAbiWantsStaticCrt, !plan.toolchain.msvcRedistDir.empty())) + msvcAbiWantsStaticCrt, !plan.toolchain.base.msvc.redistDir.empty())) : std::nullopt; const dist::RoleContracts contracts = dist::role_contracts( dist::ContractStatement{ @@ -1410,7 +1432,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // this target IS the statement "this is an Apple cross"; deriving it // again from the triple here would be a second answer to a question // already settled where the machine's Xcode was consulted. - mi.appleCrossTarget = !plan.toolchain.appleSdkRoot.empty(); + mi.appleCrossTarget = !plan.toolchain.base.apple.root.empty(); mi.format = format; // A SECOND C++ RUNTIME ON THIS LINE. // @@ -1674,11 +1696,11 @@ CompileFlags compute_flags(const BuildPlan& plan) { d.origin == Origin::Derived ? rp::Kind::Derived : rp::Kind::Declared}); if (msvcAbi && policy != rp::CrtPolicy::System - && !plan.toolchain.msvcRedistDir.empty()) { + && !plan.toolchain.base.msvc.redistDir.empty()) { std::vector sources; std::error_code ec; for (auto const& e : std::filesystem::directory_iterator( - plan.toolchain.msvcRedistDir, ec)) { + plan.toolchain.base.msvc.redistDir, ec)) { if (!e.is_regular_file(ec)) continue; auto ext = e.path().extension().string(); std::ranges::transform(ext, ext.begin(), @@ -1992,7 +2014,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // which SDK, and which deployment-target flag. `appleSdkRoot` being // non-empty is what says the target is the cross one; prepare resolves // it and refuses there when the SDK cannot be located. - const bool appleCross = !plan.toolchain.appleSdkRoot.empty(); + const bool appleCross = !plan.toolchain.base.apple.root.empty(); std::string version_min; // THE iOS DEPLOYMENT TARGET IS NOT A FLAG HERE, AND THAT IS ONE @@ -2031,7 +2053,10 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string macos_sdk; if (appleCross) { macos_sdk = " -isysroot " - + escape_path(plan.toolchain.appleSdkRoot); + + escape_path(plan.toolchain.base.apple.root); + } else if (plan.toolchain.base.stated() + && plan.toolchain.base.address == mcpp::toolchain::base::Address::AppleIsysroot) { + macos_sdk = " -isysroot " + escape_path(plan.toolchain.base.root); } else if (auto sdk = mcpp::platform::macos::sdk_path()) { macos_sdk = " -isysroot " + escape_path(*sdk); } @@ -2333,9 +2358,9 @@ CompileFlags compute_flags(const BuildPlan& plan) { // it goes into LinkInputs below, because the freestanding link // line is replaced wholesale and anything added to f.ld here is // discarded. - if (!plan.toolchain.targetSysrootInclude.empty()) { + if (!plan.toolchain.base.multilibInclude.empty()) { const auto flag = - " -isystem " + ninjaEsc(plan.toolchain.targetSysrootInclude); + " -isystem " + ninjaEsc(plan.toolchain.base.multilibInclude); f.cxx += flag; f.cc += flag; f.as += flag; } @@ -2345,7 +2370,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // it, which then dies with 'unrecognised emulation mode'. // Reproduced on this toolchain 2026-08-19. in.lld = mcpp::freestanding::resolve_lld(plan.toolchain.binaryPath); - in.sysrootLib = plan.toolchain.targetSysrootLib; + in.sysrootLib = plan.toolchain.base.multilibLib; // `--no-default-config` FIRST, and it is not hygiene. // // The llvm payload ships bin/clang++.cfg with an unconditional diff --git a/src/build/hermetic.cppm b/src/build/hermetic.cppm index 9c37fb809..27ee65942 100644 --- a/src/build/hermetic.cppm +++ b/src/build/hermetic.cppm @@ -138,7 +138,7 @@ std::expected verify_hermetic_link( if (!ec) if (auto upstream = mcpp::provider::store_of(real)) allowed.push_back(*upstream); } - if (!tc.sysroot.empty()) allowed.push_back(tc.sysroot); + if (!tc.base.root.empty()) allowed.push_back(tc.base.root); if (const char* e = std::getenv("MCPP_ALLOW_HOST_LIBS"); e && *e && *e != '0') allowHostLibs = true; diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 5c942c2c4..646eb9f85 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -2266,8 +2266,8 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // same across the plans of one build. std::string crt = " --crt " + flags.runtimeCrtPolicy; if (flags.runtimeCrtPolicy != "system" && flags.runtimeCrtPolicy != "not-applicable" - && !plan.toolchain.msvcRedistDir.empty()) - crt += " --toolset-crt " + ninja_command_word(plan.toolchain.msvcRedistDir.string()); + && !plan.toolchain.base.msvc.redistDir.empty()) + crt += " --toolset-crt " + ninja_command_word(plan.toolchain.base.msvc.redistDir.string()); append(" command = $mcpp place-dlls --output $out --depfile $out.d" + crt + " $in" + dirs + "\n"); append(" depfile = $out.d\n"); @@ -3625,7 +3625,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // host that is not Windows the directory is inert on PATH, and the // same graph is then assertable on every host. const bool crtOnPath = mcpp::toolchain::is_msvc_target(plan.toolchain) - && !plan.toolchain.msvcRedistDir.empty(); + && !plan.toolchain.base.msvc.redistDir.empty(); const bool named = !a.env.empty() || !a.cwd.empty() || crtOnPath; if (stamped || named) { // `mcpp_exe_path()`, not `self_exe_path()` directly: this file @@ -3647,7 +3647,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, for (auto const& e : a.env) wrapped += " --env " + q(e); if (!a.cwd.empty()) wrapped += " --cwd " + q(a.cwd); if (crtOnPath) - wrapped += " --path-prepend " + q(plan.toolchain.msvcRedistDir.string()); + wrapped += " --path-prepend " + q(plan.toolchain.base.msvc.redistDir.string()); // Before the stamp list, so the wrapper can tell the flag from a // stamp path without an allowlist of extensions. `directives.cppm` // refuses a `prepare` action with no `output_dir`, so this is diff --git a/src/build/plan.cppm b/src/build/plan.cppm index ef1ef5ab5..86ea1ec0f 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -26,6 +26,7 @@ import mcpp.platform.runtime_env_contract; import mcpp.platform.runtime_search; import mcpp.provider; import mcpp.provider.select; +import mcpp.home; export namespace mcpp::build { @@ -396,6 +397,13 @@ struct BuildPlan { // `-L` (openkal-musl's empty archives), and a path dependency's root can // be anywhere on the machine, not only under the store. std::vector packageRoots; + // THE PATHS AN ARTIFACT CARRIES, MAPPED (`trim_paths`; design 2026-10-23 + // §15.3, mcpp#832): each package's root to `@`, and the + // store, the home, the build directory, the toolchain and the target base + // to fixed placeholders. Ordered by prefix length, shortest first, so that + // a driver that tries the last-given mapping first takes the longest + // match. Empty unless the profile trims. + std::vector> trimMap; mcpp::toolchain::Toolchain toolchain; mcpp::toolchain::Fingerprint fingerprint; // Where the target's platform interface, C library and C++ runtime come @@ -1532,6 +1540,39 @@ std::string exports_without_discovery(const mcpp::manifest::Target& t, t.name, package); } +// The `trim_paths` map (see `BuildPlan::trimMap`). A package's identity is +// its name and version -- the root package's too, so a crash report names +// `app@1.2.0/src/main.cpp` wherever it was built, and two checkouts of one +// tree embed the same paths. +std::vector> +trim_map(const std::vector& packages, + const mcpp::toolchain::Toolchain& tc, + const std::filesystem::path& outputDir, + const std::vector& storeRoots) { + std::vector> out; + auto add = [&](std::filesystem::path from, std::string to) { + if (from.empty() || !from.is_absolute()) return; + from = from.lexically_normal(); + while (from.has_relative_path() && from.filename().empty()) from = from.parent_path(); + if (from == from.root_path()) return; // never map `/` itself + if (std::ranges::any_of(out, [&](auto const& e) { return e.first == from; })) return; + out.emplace_back(std::move(from), std::move(to)); + }; + add(mcpp::home::root(), "/mcpp/home"); + for (auto const& s : storeRoots) add(s, "/mcpp/store"); + add(tc.localRoot.empty() ? tc.binaryPath.parent_path().parent_path() + : std::filesystem::path(tc.localRoot), "/mcpp/toolchain"); + if (tc.base.stated()) add(tc.base.root, "/mcpp/sysroot"); + add(outputDir, "/mcpp/build"); + for (auto const& p : packages) + if (!p.manifest.package.name.empty()) + add(p.root, std::format("{}@{}", p.manifest.package.name, + p.manifest.package.version.empty() ? std::string("0") + : p.manifest.package.version)); + std::ranges::stable_sort(out, {}, [](auto const& e) { return e.first.native().size(); }); + return out; +} + std::expected make_plan(const mcpp::manifest::Manifest& manifest, const mcpp::toolchain::Toolchain& tc, @@ -1550,6 +1591,8 @@ make_plan(const mcpp::manifest::Manifest& manifest, plan.toolchain = tc; plan.fingerprint = fp; for (auto const& p : packages) plan.packageRoots.push_back(p.root); + if (manifest.buildConfig.trimPaths) + plan.trimMap = trim_map(packages, tc, outputDir, storeRoots); // The ROOT package's extension table. Only the synthesized entry main // needs it — every scanned unit arrives with its kind already set by the @@ -1769,8 +1812,8 @@ make_plan(const mcpp::manifest::Manifest& manifest, // `linkRuntimeDirs` above, so this is a harmless duplicate there) and the // LLVM row (where `linkRuntimeDirs` holds LLVM's own directories instead // and would otherwise never mention it). - if (!tc.msvcRedistDir.empty()) - append_unique_path(out.runtimeLibraryDirs, tc.msvcRedistDir); + if (!tc.base.msvc.redistDir.empty()) + append_unique_path(out.runtimeLibraryDirs, tc.base.msvc.redistDir); // The private glibc payload exists here for ONE reason: a dlopen()'d // library, whose own DT_NEEDED closure never consults the main // executable's RUNPATH, must still resolve the same libc the executable @@ -1797,9 +1840,9 @@ make_plan(const mcpp::manifest::Manifest& manifest, // carries it and the dlopen() it performs, and nothing else. if constexpr (mcpp::platform::publishes_via_environment( mcpp::platform::kPrivateLibcSearchScope)) { - if (tc.payloadPaths && !out.depRuntimeLibraryDirs.empty()) { + if (tc.base.payload && !out.depRuntimeLibraryDirs.empty()) { append_unique_path(out.runtimeLibraryDirs, - tc.payloadPaths->glibcLib); + tc.base.payload->glibcLib); } } }; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index fbad6fc0d..81719eb86 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -518,6 +518,13 @@ export struct BuildContext { // declared but not installed contributes nothing, and the lookup then // continues to PATH. std::vector xlingsDepBinDirs; + // THE EXECUTION HOST'S WRAPPER (`run_under`, design 2026-10-23 §3.7): + // decided once by prepare from the override, the project and this + // machine's defaults, and composed in front of the runner by every verb + // that executes an artifact. Empty = this machine. `runUnderOrigin` says + // where it was stated, for the line that announces it. + std::vector runUnder; + std::string runUnderOrigin; // The payload directory of every `[xlings]` address resolved above, for // the fast path's presence check (#716). See BuildCacheEntry::xlingsPayloads. std::vector xlingsPayloads; diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp new file mode 100644 index 000000000..c5ee49503 --- /dev/null +++ b/src/build/prepare/base.cpp @@ -0,0 +1,541 @@ +// base.cpp -- the target base: which statement of `sysroot` applies, and the +// one resolution every reader reads (design 2026-10-23, "target base and +// execution host", §3.3 and §4.2). Declared in `:state`. +// +// FOUR TIERS, IN #755'S ORDER: an override (`--sysroot`, `MCPP_TARGET__ +// SYSROOT`, `MCPP_SYSROOT`) outranks the project (`[target.].sysroot`, then +// `[toolchain] { path, sysroot }` or a toolchain phase), which outranks this +// machine's defaults (`config.toml`, the user's then the system's), which +// outrank the engine's (the target row). The host graph reads none of them: +// a build program runs here, on the host's own base. + +module mcpp.build.prepare; +import :state; + +import std; +import mcpp.build.refusal; +import mcpp.config; +import mcpp.fetcher; +import mcpp.fetcher.progress; +import mcpp.manifest; +import mcpp.platform; +import mcpp.provider; +import mcpp.runtime.binding; +import mcpp.toolchain.base; +import mcpp.toolchain.base_probe; +import mcpp.toolchain.triple; +import mcpp.ui; + +namespace mcpp::build { + +namespace fs = std::filesystem; +namespace tb = mcpp::toolchain::base; + +std::string target_env_name(const mcpp::toolchain::triple::Triple& t, std::string_view key) { + std::string out = "MCPP_TARGET_"; + for (char c : t.str()) + out += (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A') + : ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_'; + out += '_'; + out += key; + return out; +} + +namespace { + +// An environment or command-line value: empty is unset, `none` is the empty +// form, a filesystem path is a path (relative to where mcpp was started), and +// anything else is the manifest's string grammar. +std::optional> +override_value(std::string_view v) { + if (v.empty()) return std::nullopt; + if (v == "none") return tb::parse_text(""); + if (!v.starts_with("path:") && names_a_file_path(v)) { + std::error_code ec; + auto p = fs::absolute(fs::path(std::string(v)), ec).lexically_normal(); + return tb::parse_text("path:" + p.generic_string()); + } + return tb::parse_text(v); +} + +std::string shown_statement(const tb::Statement& s, const fs::path& relativeTo) { + if (s.form == tb::Form::Empty) return "\"\" (no prebuilt C library)"; + if (s.form != tb::Form::Path) return s.text; + if (!relativeTo.empty()) { + std::error_code ec; + auto rel = fs::relative(s.path, relativeTo, ec); + if (!ec && !rel.empty() && !rel.generic_string().starts_with("..")) + return rel.generic_string(); + } + return s.path.generic_string(); +} + +SourceClass class_of(const tb::Statement& s) { + if (s.originKind == "build-program") return SourceClass::Program; + switch (s.form) { + case tb::Form::Path: return SourceClass::Custom; + case tb::Form::Locator: return s.version == "system" ? SourceClass::Host : SourceClass::Pinned; + case tb::Form::Package: return s.text.find('@') != std::string::npos ? SourceClass::Pinned + : SourceClass::Managed; + case tb::Form::Empty: return SourceClass::Custom; + case tb::Form::Default: return SourceClass::Managed; + } + return SourceClass::Managed; +} + +std::string origin_text(const tb::Statement& s) { + if (s.originKind == "env") return s.originKey; + if (s.originFile.empty()) return s.originKey; + return s.originLine > 0 ? std::format("{}:{}", s.originFile, s.originLine) : s.originFile; +} + +// A sysroot the project names and that is not there (design §3.8): where the +// statement is, what it resolved to, the three ways a team makes it exist, +// and how to use another one on this machine only. +std::string missing_path_message(const mcpp::toolchain::triple::Triple& t, const tb::Statement& s) { + return std::format( + "{}: [target.{}].sysroot = {{ path = ... }}\n" + " resolves to {}, which does not exist.\n" + " A sysroot a project names is the project's to provide. Common ways:\n" + " · publish it as a package in your index: sysroot = \"acme:sysroot@1.0\"\n" + " · keep it in the repository (submodule/LFS) at that relative path\n" + " · agree on a path for the team (e.g. /opt/acme/sysroot)\n" + " To use another one on this machine only: {}=path:, or\n" + " [target.{}] sysroot = {{ path = \"\" }} in ~/.mcpp/config.toml", + origin_text(s), t.str(), s.path.string(), target_env_name(t, "SYSROOT"), t.str()); +} + +} // namespace + +std::expected +base_statement(PrepareState& state, const mcpp::toolchain::triple::Triple& t) { + const auto key = t.str(); + if (state.baseStatement && state.baseStatementFor == key) return *state.baseStatement; + auto settle = [&](std::expected s) + -> std::expected { + if (!s) return s; + *s = tb::normalise(t, std::move(*s)); + if (auto why = tb::refusal(t, *s)) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format("{} (stated in {})", *why, origin_text(*s))); + } + state.baseStatement = *s; + state.baseStatementFor = key; + return s; + }; + auto stamped = [&](std::expected s, std::string kind, + std::string file, int line, std::string originKey) + -> std::expected { + if (!s) return std::unexpected(std::format("{}{}: {}", file.empty() ? originKey : file, + line > 0 ? std::format(":{}", line) : "", s.error())); + s->originKind = std::move(kind); + s->originFile = std::move(file); + s->originLine = line; + s->originKey = std::move(originKey); + return s; + }; + // The host graph never reads the target's base (§3.6). + if (state.overrides.host_target || state.overrides.tool_depth > 0) + return settle(tb::Statement{}); + + // 1. Overrides (`--sysroot` is `MCPP_SYSROOT`). + for (auto name : {target_env_name(t, "SYSROOT"), std::string("MCPP_SYSROOT")}) { + const char* e = std::getenv(name.c_str()); + if (auto v = override_value(e ? std::string_view(e) : std::string_view{})) + return settle(stamped(std::move(*v), "env", {}, 0, name)); + } + // 2. The project: the target row, then the toolchain's own statement. + if (auto* e = find_target_entry(*state.m, t); e && e->sysrootDeclared) { + auto s = stamped(tb::parse_text(e->sysroot), "manifest", state.m->sourcePath.string(), + e->sysrootLine, std::format("[target.{}].sysroot", key)); + if (s) s->provides = e->sysrootProvides; + return settle(std::move(s)); + } + if (state.localToolchain && !state.localToolchain->sysroot.empty()) { + auto const& o = state.localToolchainOrigin; + return settle(stamped(tb::parse_text("path:" + state.localToolchain->sysroot), + o.originKind == "build-program" ? "build-program" : "toolchain", + o.originFile, o.originLine, "[toolchain] sysroot")); + } + if (!state.programSysroot.empty()) { + auto const& o = state.programSysrootOrigin; + auto text = state.programSysroot; + auto parsed = names_a_file_path(text) && !text.starts_with("path:") + ? tb::parse_text("path:" + fs::path(text).lexically_normal().generic_string()) + : tb::parse_text(text); + return settle(stamped(std::move(parsed), "build-program", o.originFile, o.originLine, + "mcpp:toolchain=sysroot")); + } + // 3. This machine's defaults. + if (auto cfg = state.get_cfg(false); cfg) { + for (auto const& [k, d] : (*cfg)->targetDefaults) { + if (!d.sysrootDeclared) continue; + auto kt = mcpp::toolchain::triple::parse(k); + if (!(kt ? kt->str() == key : k == key)) continue; + auto s = stamped(tb::parse_text(d.sysroot), "config", d.sysrootFile.string(), + d.sysrootLine, std::format("[target.{}].sysroot", key)); + if (s) s->provides = d.sysrootProvides; + return settle(std::move(s)); + } + } + // 4. The engine's: the row, read where it always was (`effective_sysroot`). + return settle(tb::Statement{}); +} + +const std::string* base_statement_text(PrepareState& state, const mcpp::toolchain::triple::Triple& t) { + auto s = base_statement(state, t); + if (!s || !s->stated()) return nullptr; + // A `std::string` that outlives the call: the memoised statement's. + return &state.baseStatement->text; +} + +namespace { + +// The driver and the flags that choose its target, without any sysroot: what +// a measurement asks. +std::vector driver_base(const mcpp::toolchain::Toolchain& tc) { + std::vector v{tc.binaryPath.string()}; + if (mcpp::toolchain::is_clang(tc)) v.push_back("--no-default-config"); + if (!tc.crossTargetFlag.empty()) v.push_back(tc.crossTargetFlag); + return v; +} + +bool provides_c_abi(const tb::Statement& s, std::string* impl) { + for (auto const& p : s.provides) { + constexpr std::string_view k = "mcpp:c-abi="; + if (p.starts_with(k)) { if (impl) *impl = p.substr(k.size()); return true; } + } + return false; +} + +// The directory a package statement names, installed through the channel a +// row's toolchain pin uses (and `MCPP_NO_AUTO_INSTALL` / offline are the +// fetcher's to decide, as they are there). +std::expected +package_dir(PrepareState& state, const std::string& ref) { + auto cfg = state.get_cfg(true); + if (!cfg) return std::unexpected(cfg.error()); + mcpp::fetcher::Fetcher f(**cfg); + mcpp::fetcher::InstallProgressHandler progress; + if (auto p = f.resolve_xpkg_path(ref, mcpp::fetcher::PayloadIntent::Use, &progress)) + return p->root; + auto xl = mcpp::config::make_xlings_env(**cfg); + if (auto d = mcpp::provider::payload(*mcpp::provider::open(xl), mcpp::provider::parse_ref(ref))) + return *d; + return std::unexpected(std::format("the package {} could not be installed", ref)); +} + +} // namespace + +// THE ONE RESOLUTION (§4.2 steps 5-7) for the rows whose base the engine +// renders itself: a gnu-sysroot (Linux), and the identity and report of every +// other stated base. The Apple, MSVC and multilib rows locate theirs where +// they always did, now from the same statement; this records what they found. +std::expected step2_resolve_base(PrepareState& state) { + if (!state.tc) return {}; + auto& tc = *state.tc; + auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); + if (!tt) return {}; + const auto row = tb::row_base(*tt); + tc.base.address = row.address; + auto stmt = base_statement(state, *tt); + if (!stmt) return std::unexpected(stmt.error()); + if (!stmt->stated()) return {}; + tc.base.form = stmt->form; + tc.base.text = stmt->text; + // A NAMED SubOS AND A STATED BASE ANSWER DIFFERENT QUESTIONS (§3.3): the + // SubOS supplies what the compiler and the build's tools run on, the base + // what the artifact is built against. Said once, so neither is mistaken + // for having been ignored. + if (state.runtimeBindingSnapshot.selection.mode + == mcpp::provider::runtime::RuntimeSelection::Mode::NamedSubos + && row.address == tb::Address::GnuSysroot) + mcpp::ui::info("note", std::format( + "[xlings] subos '{}' supplies the host's tools; the artifact is built against " + "the stated sysroot {}", state.runtimeBindingSnapshot.selection.subosName, + stmt->text)); + + const auto root = state.root ? *state.root : fs::path{}; + SourceDecision d; + d.subject = "sysroot"; + d.value = shown_statement(*stmt, root); + d.cls = class_of(*stmt); + d.originKind = stmt->originKind; + d.originFile = stmt->originFile; + d.originLine = stmt->originLine; + d.originKey = stmt->originKey; + + // The identity a non-rendered base contributes: its statement and where it + // was found. The Linux branch below replaces it with a measured one. + auto record_identity = [&](std::string measured, std::vector evidence, + std::vector hostDirs = {}) { + tc.base.identity = tb::identity_of(tc.base.text, tc.base.root, evidence, measured); + auto target = state.targetRuntimeBinding.value_or(state.runtimeBindingSnapshot); + // The host's base loads through the host's loader, which searches the + // host's directories itself: the binding names no private loader, so + // closure validation models those directories. + const bool hostBase = !hostDirs.empty(); + mcpp::platform::runtime::bind_target_base(target, { + .runtimeId = tc.base.runtime == tb::Runtime::Binding && tc.base.libc == "glibc" + && !tc.base.libcVersion.empty() + ? std::format("glibc@{}", tc.base.libcVersion) : std::string{}, + .loader = tc.base.loader, + .libraryDirs = hostBase ? hostDirs : tc.base.runtimeDirs, + .identity = tc.base.identity, + .binding = tc.base.runtime == tb::Runtime::Binding, + .host = hostBase, + }); + state.targetRuntimeBinding = std::move(target); + tc.runtimeContractHash = state.targetRuntimeBinding->contractHash; + }; + + if (row.address == tb::Address::AppleIsysroot) { + if (stmt->form != tb::Form::Locator && stmt->form != tb::Form::Path) { + // A package or "" on an Apple row: the meaning it always had, + // which names no SDK. Stated, so reported; not a located base. + tc.base.form = tb::Form::Default; + tc.base.text.clear(); + record_source(state, std::move(d)); + return {}; + } + tc.base.runtime = tb::Runtime::Floor; + const bool cross = !tc.base.apple.root.empty(); + tc.base.root = cross ? tc.base.apple.root : state.appleSdkLocated.value_or(fs::path{}); + tc.base.libc = "libSystem"; + d.detail = std::format("{} {}", tc.base.apple.name.empty() ? std::string(row.family) : tc.base.apple.name, + tc.base.apple.version); + record_identity(tc.base.apple.version, {tc.base.root / "SDKSettings.json"}); + record_source(state, std::move(d)); + return {}; + } + if (row.address == tb::Address::MsvcRoots) { + tc.base.runtime = tb::Runtime::Floor; + tc.base.root = tc.base.msvc.toolsDir; + d.detail = std::format("msvc {}", tc.base.msvc.toolsVersion); + record_identity(tc.base.msvc.toolsVersion + " " + tc.base.msvc.sdkVersion, + {tc.base.msvc.toolsDir, tc.base.msvc.sdkRoot}); + record_source(state, std::move(d)); + return {}; + } + if (row.address == tb::Address::Multilib) { + tc.base.runtime = tb::Runtime::None; + tc.base.root = tc.base.multilibRoot; + d.detail = tc.base.package; + record_identity(tc.base.package, {tc.base.multilibInclude, tc.base.multilibLib}); + record_source(state, std::move(d)); + return {}; + } + if (row.address != tb::Address::GnuSysroot || stmt->form == tb::Form::Empty) { + // The empty form, and a package on a row whose SDK resolves its own + // system: accepted as they always were, and stated. + record_source(state, std::move(d)); + return {}; + } + + // ── gnu-sysroot ──────────────────────────────────────────────────────── + fs::path dir; + std::error_code ec; + if (stmt->form == tb::Form::Locator) { + dir = "/"; + } else if (stmt->form == tb::Form::Path) { + dir = stmt->path; + if (!fs::is_directory(dir, ec)) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(missing_path_message(*tt, *stmt)); + } + } else { + auto p = package_dir(state, stmt->text); + if (!p) return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", + tt->str(), stmt->text, p.error())); + dir = *p; + } + const auto ecosystemLinux = tc.base.payload ? tc.base.payload->linuxInclude : fs::path{}; + const bool native = tt->os == "linux" && tt->arch == mcpp::platform::host_arch + && mcpp::platform::is_linux; + const bool staticLink = state.m->buildConfig.linkage == "static"; + const auto probeDir = state.workRoot / "target" / ".mcpp-base"; + + tc.base.libc.clear(); + tc.base.libcVersion.clear(); + std::vector evidence; + std::vector hostLibcDirs; + std::string impl; + if (tb::is_payload_layout(dir)) { + // The ecosystem's own layout: a glibc package (`xim:glibc@2.39`). It + // is linked as the default base is, from its own directories. + tc.base.payload = tb::payload_layout_paths(dir, ecosystemLinux); + tc.base.root.clear(); + auto v = driver_base(tc); + v.push_back("-isystem" + tc.base.payload->glibcInclude.string()); + fs::create_directories(probeDir, ec); + { std::ofstream os(probeDir / "mcpp-base-probe.c"); os << "#include \n"; } + v.insert(v.end(), {"-E", "-dM", "-x", "c", (probeDir / "mcpp-base-probe.c").string()}); + auto r = mcpp::platform::process::capture_stdout(v); + if (r.exit_code == 0) + if (auto f = tb::classify_macros(tb::parse_defines(r.output))) { + tc.base.libc = f->impl; + tc.base.libcVersion = f->version; + } + tc.base.loader = mcpp::toolchain::resolve_loader(tc.base.payload->glibcLib, tt->str()); + tc.base.runtimeDirs = {tc.base.payload->glibcLib}; + evidence = {tc.base.payload->glibcInclude / "features.h", tc.base.loader, + tc.base.payload->glibcLib / "libc.so.6"}; + } else { + auto m = tb::measure_gnu(driver_base(tc), *tt, dir, + mcpp::toolchain::is_clang(tc), probeDir); + if (!m.cAbiFound && !provides_c_abi(*stmt, &impl)) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot = {} ({}): {} supplies no C library the compiler can find " + "(no crt1.o), so it is a library prefix rather than a sysroot.\n" + " A library prefix is a dependency; a sysroot supplies the C library. If this " + "directory does supply one in a layout the driver does not know, say which:\n" + " sysroot = {{ path = \"{}\", provides = [\"mcpp:c-abi=\"] }}", + tt->str(), d.value, origin_text(*stmt), dir.string(), dir.generic_string())); + } + tc.base.payload.reset(); + tc.base.root = dir; + tc.base.multiarch = m.multiarch; + tc.base.multiarchTuple = m.tuple; + if (m.fact) { tc.base.libc = m.fact->impl; tc.base.libcVersion = m.fact->version; } + tc.base.loader = m.loader; + tc.base.runtimeDirs = m.runtimeDirs; + evidence = m.evidence; + // The host's own root binds through the standard loader path and the + // loader's own search: nothing to name in RUNPATH, and nothing private + // about the loader. The libc directory is kept for closure validation. + if (stmt->form == tb::Form::Locator) { + hostLibcDirs = tc.base.runtimeDirs; + tc.base.runtimeDirs.clear(); + if (auto std = mcpp::toolchain::distro_loader_path(tt->str()); + !std.empty() && fs::exists(std, ec)) + tc.base.loader = std; + } + // Kernel headers are additive (§2.4): the engine supplies the + // ecosystem's for a native base that lacks them. + if (native && !ecosystemLinux.empty() + && !fs::exists(dir / "usr" / "include" / "linux" / "limits.h", ec)) + tc.base.addedIncludes.push_back(ecosystemLinux); + } + // A declaration checks a measurement and stands in for one that cannot be + // made (§3.4). + if (std::string declared; provides_c_abi(*stmt, &declared)) { + if (!tc.base.libc.empty() && declared != tc.base.libc) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot ({}) provides mcpp:c-abi={}, and the C library measured there " + "is {} {}", tt->str(), origin_text(*stmt), declared, tc.base.libc, tc.base.libcVersion)); + } + if (tc.base.libc.empty()) tc.base.libc = declared; + } + const auto rowLibc = std::string(row.family); + if (!rowLibc.empty() && !tc.base.libc.empty() && tc.base.libc != rowLibc + && (tc.base.libc == "glibc" || tc.base.libc == "musl")) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot ({}) supplies {} {}; a {} row is built on {}. Name the row that " + "matches the C library (e.g. --target {}-linux-{}).", + tt->str(), origin_text(*stmt), tc.base.libc, tc.base.libcVersion, tt->str(), rowLibc, + tt->arch, tc.base.libc == "glibc" ? "gnu" : "musl")); + } + tc.base.runtime = !native || staticLink ? (native ? tb::Runtime::None : tb::Runtime::Floor) + : tb::Runtime::Binding; + if (tc.base.runtime == tb::Runtime::Binding && tc.base.loader.empty()) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot = {} ({}): no dynamic loader ({}) was found in {}, and a " + "native dynamic artifact loads its C library through the sysroot's own loader.\n" + " Link statically ([target.{}] linkage = \"static\"), or use a sysroot that " + "carries its loader.", + tt->str(), d.value, origin_text(*stmt), mcpp::toolchain::loader_filename(tt->str()), + dir.string(), tt->str())); + } + + // THE LAYERS BELOW MUST FIT (§4.8): the compiler's own runtime requires a + // C library version, and a base older than that cannot serve it. Refused + // here, naming both, rather than inside `std.cc` (scenario I). + if (tc.base.libc == "glibc" && !tc.base.libcVersion.empty()) { + auto floor = tb::compiler_runtime_glibc_floor(driver_base(tc)); + if (!floor.version.empty() && tb::compare_versions(floor.version, tc.base.libcVersion) > 0) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot ({}): {} {}'s runtime requires glibc >= {}\n" + " ({}: GLIBC_{}), and this sysroot provides glibc {}.\n" + " Use a compiler built against glibc {} or older, or a newer sysroot.", + tt->str(), origin_text(*stmt), tc.compiler_name(), tc.version, floor.version, + floor.from.filename().string(), floor.version, tc.base.libcVersion, + tc.base.libcVersion)); + } + } + + d.detail = tc.base.libcVersion.empty() ? tc.base.libc + : std::format("{} {}", tc.base.libc, tc.base.libcVersion); + record_identity(std::format("{} {} {} {}", tc.base.libc, tc.base.libcVersion, + tc.base.multiarch ? tc.base.multiarchTuple : "", + tb::runtime_name(tc.base.runtime)), + std::move(evidence), std::move(hostLibcDirs)); + record_source(state, std::move(d)); + return {}; +} + +// THE EXECUTION HOST'S WRAPPER (§3.7): the same tiers as the base, minus the +// engine's (an execution host is never a default). In the environment, empty +// is unset and `none` clears what the project or the machine said. +std::expected step2_resolve_run_under(PrepareState& state) { + state.runUnder.clear(); + if (!state.tc || state.overrides.host_target || state.overrides.tool_depth > 0) return {}; + auto tt = mcpp::toolchain::triple::parse(state.tc->targetTriple); + if (!tt) return {}; + const auto key = tt->str(); + SourceDecision d; + d.subject = "run_under"; + d.cls = SourceClass::Custom; + d.announce = false; // announced when something is run through it + auto decide = [&](std::vector argv) { + state.runUnder = std::move(argv); + if (state.runUnder.empty()) return; + std::string shown; + for (auto const& a : state.runUnder) shown += (shown.empty() ? "" : " ") + a; + d.value = std::move(shown); + state.runUnderOrigin = d; + record_source(state, std::move(d)); + }; + const auto envName = target_env_name(*tt, "RUN_UNDER"); + if (const char* e = std::getenv(envName.c_str()); e && *e) { + std::string_view v(e); + if (v == "none") return {}; + std::vector argv; + std::string cur; + for (char c : v) { + if (c == ' ' || c == '\t') { if (!cur.empty()) argv.push_back(std::exchange(cur, {})); } + else cur += c; + } + if (!cur.empty()) argv.push_back(cur); + d.originKind = "env"; + d.originKey = envName; + decide(std::move(argv)); + return {}; + } + if (auto* e = find_target_entry(*state.m, *tt); e && !e->runUnder.empty()) { + d.originKind = "manifest"; + d.originFile = state.m->sourcePath.string(); + d.originLine = e->runUnderLine; + decide(e->runUnder); + return {}; + } + if (auto cfg = state.get_cfg(false); cfg) { + for (auto const& [k, td] : (*cfg)->targetDefaults) { + if (td.runUnder.empty()) continue; + auto kt = mcpp::toolchain::triple::parse(k); + if (!(kt ? kt->str() == key : k == key)) continue; + d.originKind = "config"; + d.originFile = td.runUnderFile.string(); + d.originLine = td.runUnderLine; + decide(td.runUnder); + return {}; + } + } + return {}; +} + +} // namespace mcpp::build diff --git a/src/build/prepare/local_toolchain.cpp b/src/build/prepare/local_toolchain.cpp index 0ce1a163d..305c51858 100644 --- a/src/build/prepare/local_toolchain.cpp +++ b/src/build/prepare/local_toolchain.cpp @@ -23,6 +23,7 @@ import mcpp.home; import mcpp.manifest; import mcpp.platform; import mcpp.toolchain.registry; +import mcpp.toolchain.triple; import mcpp.toolchain.post_install; import mcpp.ui; @@ -84,6 +85,26 @@ read_statement(const std::vector& lines, std::string& spec, if (!spec.empty() && !lt.path.empty()) return std::unexpected(std::string( "the toolchain phase stated both `spec` and `path`; a build toolchain is one of them")); + // A MANAGED TOOLCHAIN HAS NO TREE TO DESCRIBE. `prefix`, `launcher`, + // `tool.` and `family` describe a toolchain named by path, and + // stated beside `spec` they were dropped without a word. `sysroot` is the + // exception: it is the TARGET's base, which a program may state for a + // managed compiler too (design 2026-10-23 §3.5). + if (!spec.empty()) { + std::vector pathOnly; + if (!lt.prefix.empty()) pathOnly.push_back("prefix"); + if (!lt.launcher.empty()) pathOnly.push_back("launcher"); + if (!lt.family.empty()) pathOnly.push_back("family"); + for (auto const& [role, _] : lt.tools) pathOnly.push_back("tool." + role); + if (!pathOnly.empty()) { + std::string names; + for (auto const& n : pathOnly) names += (names.empty() ? "" : ", ") + n; + return std::unexpected(std::format( + "the toolchain phase stated `spec` with {}; those describe a toolchain named by " + "`path`, and a managed toolchain ({}) has no tree for them to describe. State " + "`path` instead, or drop them (`sysroot` may accompany either)", names, spec)); + } + } return lt; } @@ -140,6 +161,16 @@ std::expected step1_local_toolchain(PrepareState& state) { state.tcSpec = spec; d.cls = SourceClass::Pinned; d.value = spec; + if (!lt->sysroot.empty()) { + // The program stated the target's base beside a managed + // compiler: a path relative to the project, or a spelling. + auto text = lt->sysroot; + if (names_a_file_path(text) && !text.starts_with("path:")) + text = absolute_against(*state.root, text).generic_string(); + state.programSysroot = std::move(text); + state.programSysrootOrigin = d; + state.programSysrootOrigin.subject = "sysroot"; + } state.localToolchainOrigin = std::move(d); return {}; } @@ -254,14 +285,22 @@ step2_use_local_toolchain(PrepareState& state, const mcpp::toolchain::ToolchainS "through clang's `--ld-path`, which gcc has no counterpart for. Either " "name an llvm toolchain, or place a program called `ld` in the tree " "gcc searches", p)); - if (!lt.sysroot.empty() && !fs::is_directory(lt.sysroot, ec)) - return refuse(std::format("sysroot '{}' is not a directory", lt.sysroot)); state.explicit_compiler = driver; // THE C LIBRARY A NATIVE BUILD LINKS IS THE ECOSYSTEM'S, AS FOR A MANAGED // TOOLCHAIN: the binding's payload, linked by mcpp's own link model. A - // toolchain that states its sysroot, or a prefixed (cross) toolchain that - // carries one, links what it carries instead. - if (lt.sysroot.empty() && lt.prefix.empty()) { + // toolchain that states its sysroot, a target whose base is stated + // anywhere else, or a prefixed (cross) toolchain that carries one, links + // that instead -- and needs no payload of the ecosystem's. + const auto tt = state.overrides.target_triple.empty() + ? std::optional{mcpp::toolchain::triple::host_triple()} + : mcpp::toolchain::triple::parse(state.overrides.target_triple); + bool baseStated = !lt.sysroot.empty(); + if (tt && !baseStated) { + auto s = base_statement(state, *tt); + if (!s) return std::unexpected(s.error()); + baseStated = s->stated(); + } + if (!baseStated && lt.prefix.empty()) { mcpp::toolchain::XimToolchainPackage pkg; pkg.ximName = family; pkg.family = family == "llvm" ? mcpp::toolchain::Family::Llvm : mcpp::toolchain::Family::Gcc; @@ -302,12 +341,8 @@ std::expected step2_apply_local_toolchain(PrepareState& state } tc.launcher = l.generic_string(); } - if (!lt.sysroot.empty()) { - // A STATED SYSROOT IS THE C LIBRARY: the link model's sysroot mode, - // not the binding's payload. - tc.sysroot = lt.sysroot; - tc.payloadPaths.reset(); - } + // A stated sysroot is the target's base, resolved with every other + // statement of it (base.cpp), not here. tc.driverIdent += "\nmcpp-local-toolchain " + local_identity(tc); // Announced beside the resolution, with what the probe found. auto d = state.localToolchainOrigin; diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index ad82c10e1..9373cc851 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -647,7 +647,7 @@ static std::expected step13_cxx_process_runtime(PrepareState& ? std::optional(dist::msvc_abi_default_contract( mcpp::toolchain::msvc_wants_static_crt( bc.linkage, bc.cxxRuntime), - !state.tc->msvcRedistDir.empty())) + !state.tc->base.msvc.redistDir.empty())) : std::nullopt; const auto contracts = dist::role_contracts( dist::ContractStatement{ @@ -666,7 +666,7 @@ static std::expected step13_cxx_process_runtime(PrepareState& // cannot be met is an error, never a downgrade with a warning — the // same rule `mcpp pack`'s mode contradiction follows. if (mcpp::toolchain::is_msvc_target(*state.tc) - && state.tc->msvcRedistDir.empty()) { + && state.tc->base.msvc.redistDir.empty()) { struct { dist::Contract c; bool stated; std::string_view role; } rows[] = { {contracts.program, contracts.programStated, "distributable"}, {contracts.tests, contracts.testsStated, "test"}, @@ -903,9 +903,12 @@ static void step13_graph_and_schedule(PrepareState& state, BuildContext& ctx) { ctx.plan.scheduleTag, decision.reason)); } - ctx.plan.runtimeBinding = state.runtimeBindingSnapshot; - mcpp::build::merge_runtime_binding_contract( - ctx.plan, state.runtimeBindingSnapshot); + // THE ARTIFACT'S RUNTIME: the stated base's, when one re-derived it + // (design 2026-10-23 §4.6), else the binding the compiler runs on. + const auto& artifactRuntime = state.targetRuntimeBinding + ? *state.targetRuntimeBinding : state.runtimeBindingSnapshot; + ctx.plan.runtimeBinding = artifactRuntime; + mcpp::build::merge_runtime_binding_contract(ctx.plan, artifactRuntime); ctx.plan.compileDbPath = state.workRoot / "compile_commands.json"; // GCC: a clean `*link:` for this build, so the payload's specs cannot // inject other homes' rpath entries into the artifact. AFTER the plan is @@ -918,6 +921,23 @@ static void step13_graph_and_schedule(PrepareState& state, BuildContext& ctx) { if (state.tc->compiler == mcpp::toolchain::CompilerId::GCC && !state.overrides.plan_only) ctx.plan.gccCleanSpecs = mcpp::toolchain::write_clean_link_specs( state.tc->binaryPath, ctx.outputDir); + // THE WAY BACK FROM A TRIMMED PATH (design 2026-10-23 §15.3): a debugger + // loads these and finds the sources the debug information names by + // `@/…`. Written with the build that produced them, and + // only when there is debug information for them to serve. + if (!ctx.plan.trimMap.empty() && state.m->buildConfig.debug && !state.overrides.plan_only) { + std::error_code ec; + std::filesystem::create_directories(ctx.outputDir, ec); + std::ofstream gdb(ctx.outputDir / "source-map.gdbinit", std::ios::binary | std::ios::trunc); + std::ofstream lldb(ctx.outputDir / "source-map.lldbinit", std::ios::binary | std::ios::trunc); + gdb << "# mcpp trim_paths: the trimmed source paths, mapped back. `source `\n"; + lldb << "# mcpp trim_paths: the trimmed source paths, mapped back. `command source `\n"; + for (auto it = ctx.plan.trimMap.rbegin(); it != ctx.plan.trimMap.rend(); ++it) { + gdb << std::format("set substitute-path {} {}\n", it->second, it->first.generic_string()); + lldb << std::format("settings append target.source-map {} \"{}\"\n", it->second, + it->first.generic_string()); + } + } } static std::expected step13_build_graph_actions(PrepareState& state, BuildContext& ctx) { @@ -1887,7 +1907,14 @@ std::expected phase13_finish(PrepareState& state) { ctx.tc = *state.tc; ctx.fp = state.fp; ctx.runtimeSelection = state.runtimeSelection; - ctx.runtimeBinding = state.runtimeBindingSnapshot; + ctx.runtimeBinding = state.targetRuntimeBinding ? *state.targetRuntimeBinding + : state.runtimeBindingSnapshot; + ctx.runUnder = state.runUnder; + if (!state.runUnder.empty()) { + auto const& o = state.runUnderOrigin; + ctx.runUnderOrigin = o.originKind == "env" ? o.originKey + : o.originLine > 0 ? std::format("{}:{}", o.originFile, o.originLine) : o.originFile; + } ctx.profile = state.effectiveProfile; ctx.activeFeatureRequest = state.workspacePlan() ? state.requestedFeatures : state.overrides.features; diff --git a/src/build/prepare/records.cpp b/src/build/prepare/records.cpp index 6c05f468a..21108006d 100644 --- a/src/build/prepare/records.cpp +++ b/src/build/prepare/records.cpp @@ -547,17 +547,42 @@ void step13_resolution_json(PrepareState& state, BuildContext& ctx) { // THE MSVC SYSROOT OF THE CLANG ROW: which toolset and SDK the build // compiled against, and where each came from. Absent on every other // row, so a reader can tell "not this row" from "not recorded". - if (!ctx.plan.toolchain.msvcToolsDir.empty()) { + if (!ctx.plan.toolchain.base.msvc.toolsDir.empty()) { const auto& tcr = ctx.plan.toolchain; j["msvc_toolset"] = { - {"version", tcr.msvcToolsVersion}, - {"origin", tcr.msvcOrigin}, - {"product", tcr.msvcProduct}, - {"root", tcr.msvcToolsDir.generic_string()}, + {"version", tcr.base.msvc.toolsVersion}, + {"origin", tcr.base.msvc.origin}, + {"product", tcr.base.msvc.product}, + {"root", tcr.base.msvc.toolsDir.generic_string()}, }; j["windows_sdk"] = { - {"version", tcr.windowsSdkVersion}, - {"root", tcr.windowsSdkRoot.generic_string()}, + {"version", tcr.base.msvc.sdkVersion}, + {"root", tcr.base.msvc.sdkRoot.generic_string()}, + }; + } + // THE TARGET BASE AND THE EXECUTION HOST (design 2026-10-23 §4.10). + // Present on every build, so "the ecosystem's base" is a value a + // reader sees rather than an absence it has to interpret. + { + namespace tb = mcpp::toolchain::base; + const auto& b = ctx.plan.toolchain.base; + nlohmann::json dirs = nlohmann::json::array(); + for (auto const& d : b.runtimeDirs) dirs.push_back(d.generic_string()); + j["base"] = { + {"form", std::string(tb::form_name(b.form))}, + {"statement", b.text}, + {"address", std::string(tb::address_name(b.address))}, + {"runtime", std::string(tb::runtime_name(b.runtime))}, + {"root", b.root.generic_string()}, + {"c_abi", {{"impl", b.libc}, {"version", b.libcVersion}}}, + {"multiarch", b.multiarch ? b.multiarchTuple : std::string{}}, + {"loader", b.loader.generic_string()}, + {"runtime_dirs", std::move(dirs)}, + {"identity", b.identity}, + }; + j["execution"] = { + {"run_under", ctx.runUnder}, + {"origin", ctx.runUnderOrigin}, }; } // WHERE EACH SOURCE CAME FROM (mcpp#755): the decision record, one diff --git a/src/build/prepare/scan.cpp b/src/build/prepare/scan.cpp index 50a919f6f..4e4a9e5e7 100644 --- a/src/build/prepare/scan.cpp +++ b/src/build/prepare/scan.cpp @@ -561,7 +561,7 @@ step11_package_std_module_source(PrepareState& state) { hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always; hopt.cAbiPrebuilt = true; hopt.cxxFromGraph = true; - hopt.appleSdkRoot = state.tc->appleSdkRoot; + hopt.appleSdkRoot = state.tc->base.apple.root; // Target-keyed, not host-keyed (#685) — see `min_platform_version`. const bool cAbiTargetIsMacos = [&] { auto cAbiTt = mcpp::toolchain::triple::parse(state.tc->targetTriple); @@ -609,10 +609,10 @@ step11_apple_sdk_cxx_runtime(PrepareState& state, bool needsStdModule) { // It is REPORTED ONCE rather than refused: refusing would break a // program that built yesterday, and the report names the two lines // that make the hazard disappear. - if (state.tc && !state.tc->appleSdkRoot.empty() && state.targetSideResolved + if (state.tc && !state.tc->base.apple.root.empty() && state.targetSideResolved && !state.resolvedTargetSide.cxx.fromGraph()) { if (!needsStdModule) { - state.tc->appleSdkCxxHeaders = true; + state.tc->base.apple.cxxHeaders = true; state.tc->clear_std_modules(); } else { mcpp::diag::degraded("target/cxx-runtime", std::format( diff --git a/src/build/prepare/sources.cpp b/src/build/prepare/sources.cpp index ad70a2157..aaf265f55 100644 --- a/src/build/prepare/sources.cpp +++ b/src/build/prepare/sources.cpp @@ -633,6 +633,8 @@ std::optional managed_only_refusal(const PrepareState& state) { const auto root = state.root ? *state.root : fs::path{}; for (auto const& d : state.sources) { if (source_class_is_default(d.cls)) continue; + // Where an artifact is executed is not a source of the build. + if (d.subject == "run_under") continue; list += std::format("\n {} ← {} [{}]", subject_shown(d), d.value, source_tag(d, root)); } @@ -648,6 +650,7 @@ std::string sources_summary(const std::vector& sources) { std::map> byClass; for (auto const& d : sources) { if (source_class_is_default(d.cls)) continue; + if (d.subject == "run_under") continue; // A tool whose source is its payload's is already summarised by that // payload: one statement, one entry. if (!d.payload.empty() && std::ranges::any_of(sources, [&](auto const& o) { diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 7bcaac840..20b4e8436 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -40,6 +40,7 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.base; import mcpp.build.linkage_form; // #519 — which form each dependency takes import mcpp.build.plan; import mcpp.build.flags; // compute_flags — the per-role contracts (#418) @@ -300,6 +301,7 @@ struct PrepareState { // ── P0: manifest and workspace resolution ─────────────────────────────── std::string unservedTargetDiagnosis; std::optional appleSdkLocated; + std::optional appleSdkFound; bool iosFloorFromSdk = false; std::string pinReplacedDefault; std::optional hostSpecBeforeRowPin; @@ -624,6 +626,21 @@ struct PrepareState { // the description the resolver uses, and the decision it is reported as. std::optional localToolchain; SourceDecision localToolchainOrigin; + // A toolchain phase that stated `spec` and `sysroot`: a program stating + // the target's base (design 2026-10-23 §3.5), with where it said so. + std::string programSysroot; + SourceDecision programSysrootOrigin; + // THE TARGET BASE STATEMENT, decided once per target (base.cpp) and read + // by every site that used to read `[target.].sysroot` itself. + std::optional baseStatement; + std::string baseStatementFor; + // The artifact's runtime when a stated base re-derived it (§4.2 step 6). + // `runtimeBindingSnapshot` stays what the COMPILER runs on and what a + // host sub-build inherits; the plan and the context read this one. + std::optional targetRuntimeBinding; + // The execution host's wrapper for this target (§3.7), with its origin. + std::vector runUnder; + SourceDecision runUnderOrigin; // `[toolchain] = { configure = "build.mcpp" }` and this is the first // pass: the toolchain resolved is the bootstrap, and the root build // program runs its toolchain phase once host modules are registered. @@ -899,18 +916,26 @@ bool graph_or_targets_import(const mcpp::modgraph::Graph& graph, const std::vector& packages, const std::function& wanted); +// base.cpp: the target base -- which statement applies, and its resolution +std::string target_env_name(const mcpp::toolchain::triple::Triple& t, std::string_view key); +std::expected +base_statement(PrepareState& state, const mcpp::toolchain::triple::Triple& t); +// The statement's canonical text when one applies, nullptr otherwise: what +// the readers that predate the statement (`effective_sysroot`) take. +const std::string* base_statement_text(PrepareState& state, + const mcpp::toolchain::triple::Triple& t); +std::expected step2_resolve_base(PrepareState& state); +// The execution host's wrapper (`run_under`), decided once (§3.7). +std::expected step2_resolve_run_under(PrepareState& state); + // toolchain_env.cpp: target rows, sysroots, the MSVC binding, build-program environments -const std::string* -sysroot_override(const mcpp::manifest::Manifest& m, - const mcpp::toolchain::triple::Triple& t); std::expected bind_msvc_sysroot(mcpp::toolchain::Toolchain& tc, - const mcpp::manifest::Manifest& m, + const std::string* declared, const std::function()>& cfgOf); std::expected -check_cl_row_sysroot(const mcpp::toolchain::Toolchain& tc, - const mcpp::manifest::Manifest& m); +check_cl_row_sysroot(const mcpp::toolchain::Toolchain& tc, const std::string* declared); void fill_package_build_env(mcpp::build::BuildProgramEnv& e, const mcpp::manifest::Manifest& m); void fill_target_build_env(mcpp::build::BuildProgramEnv& e, diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index c8a9c46f4..7bead627f 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -281,15 +281,35 @@ step9_gather_target_side_candidates(PrepareState& state) { // On an MSVC-ABI row the key names the MSVC toolset, which the // toolchain binding consumes (`bind_msvc_sysroot`); it is not a // C library package for this model to report as one. + // + // The statement is the base's (base.cpp): from the project, + // an override or this machine's defaults alike. A base named + // by path or by a locator is reported by the C library it was + // measured to supply. if (tt->is_msvc_env()) ; - else if (auto const* ovr = sysroot_override(*state.m, *tt); ovr && ovr->empty()) + else if (auto const* ovr = base_statement_text(state, *tt); ovr && ovr->empty()) in.sysrootDeclaredEmpty = true; - else - in.sysrootXpkg = mcpp::toolchain::triple::effective_sysroot( - *tt, sysroot_override(*state.m, *tt)); + // A LOCATED Apple SDK is the platform's, not a C library + // package: the layers it supplies are reported as they were. + else if (tt->is_apple() && ovr + && (ovr->starts_with("path:") || ovr->find(':') == std::string::npos)) + ; + else { + in.sysrootXpkg = mcpp::toolchain::triple::effective_sysroot(*tt, ovr); + if (ovr && (ovr->starts_with("path:") || ovr->find(':') == std::string::npos)) + in.sysrootLibc = state.tc->base.libc.empty() ? std::string("sysroot") + : state.tc->base.libc; + // A hosted base whose C library is the row's own, measured + // and floor-checked by `step2_resolve_base`. + const auto& b = state.tc->base; + in.sysrootFitsPayloadCxx = b.stated() + && b.address == mcpp::toolchain::base::Address::GnuSysroot + && ((b.libc == "glibc" && tt->env == "gnu") + || (b.libc == "musl" && tt->env == "musl")); + } } - in.payloadLibcRef = state.tc->targetSysrootPkg; + in.payloadLibcRef = state.tc->base.package; in.payloadCxxInterface = state.tc->stdlibId; // The compiler is a layer, and it is the one layer no package can // supply. It enters here so that a requirement has something to be @@ -307,7 +327,7 @@ step9_gather_target_side_candidates(PrepareState& state) { // said earlier (mcpp#630). The engine never looks in Xcode for the // archive: a compiler runtime the payload lacks is a graph // package, as it is on the bare rows. - if (!state.tc->appleSdkRoot.empty()) { + if (!state.tc->base.apple.root.empty()) { if (auto tt = mcpp::toolchain::triple::parse(state.tc->targetTriple); tt && tt->is_ios()) { const std::string archive = std::format( @@ -941,6 +961,28 @@ step9_layering_and_requirement_checks(PrepareState& state, TargetSideGather& gat refusal::record(refusal::Code::LayerOrdering); return std::unexpected(*why); } + // RULE ONE, FOR A STATED BASE (design 2026-10-23 §2.4, SPEC-002 §3.1): + // a base that supplies the C library is a supplier, and a second one + // in the graph is two C libraries in one image. The row's own column + // stands down for a graph supplier (§3.5 of SPEC-002); a STATED base + // does not, so the two are named and the build refused. + if (const auto& b = state.tc->base; b.stated() && gather.in.cAbi + && (b.form == mcpp::toolchain::base::Form::Path + || b.form == mcpp::toolchain::base::Form::Locator + || b.form == mcpp::toolchain::base::Form::Package) + && b.address != mcpp::toolchain::base::Address::MsvcRoots + && b.address != mcpp::toolchain::base::Address::AppleIsysroot) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "the C library is supplied twice: sysroot {}{} ← {} and graph {} ← {}@{}.\n" + " One image has one C library. Drop the sysroot statement, or the package " + "that provides `mcpp:c-abi`.", + b.libc.empty() ? std::string("(unmeasured)") : b.libc, + b.libcVersion.empty() ? std::string{} : " " + b.libcVersion, b.text, + gather.in.cAbi->interfaceName.empty() ? gather.in.cAbi->name + : gather.in.cAbi->interfaceName, + gather.in.cAbi->name, gather.in.cAbi->version)); + } // REQUIREMENTS ARE CHECKED BEFORE ANYTHING IS COMPILED, WHICH IS THE // WHOLE POINT OF DECLARING THEM. The combination this rejects — a C++ // runtime configured for one compiler family being handed to another — diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index dfebc34bb..d3987b664 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -32,6 +32,9 @@ import mcpp.freestanding.linkline; // the ISA profile, for the std module comman import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.base; +import mcpp.toolchain.base_probe; +import mcpp.home; import mcpp.build.plan; import mcpp.build.flags; // compute_flags — the per-role contracts (#418) import mcpp.build.graph_shape; // #407: the graph says which mode wrote it @@ -255,6 +258,37 @@ static std::expected step1_define_early_toolchain_closures(Pr state.m->buildConfig.debug = pr.debug; state.m->buildConfig.lto = pr.lto; state.m->buildConfig.strip = pr.strip; + // `ndebug` and `trim_paths` follow the industry's convention for a + // profile that does not state them (design 2026-10-23 §15.3-§15.4): + // on for the profiles that produce what is shipped, off for the ones + // a developer debugs. A custom profile states what it wants. + const bool shipping = pname == "release" || pname == "dist"; + state.m->buildConfig.ndebug = pr.ndebugDeclared ? pr.ndebug : shipping; + // NOT FOR THE TEST SURFACE. A test binary runs where it was built, + // is never shipped, and commonly finds its fixtures through + // `__FILE__` -- measured on mcpp's own unit tests under its release + // default, which then looked for `workspace@0.0.0/src`. What + // `mcpp test` (and the configure pass that mirrors it) builds keeps + // its paths; what build, run and pack produce is trimmed. + state.m->buildConfig.trimPaths = (pr.trimPathsDeclared ? pr.trimPaths : shipping) + && !state.includeDevDeps; + // SAID ONCE PER HOME, the first time a shipping profile takes the new + // defaults without stating them: the artifact changed, and the line + // that changes it back is one key. + if (shipping && !(pr.ndebugDeclared && pr.trimPathsDeclared) && !state.overrides.plan_only + && state.overrides.tool_depth == 0 && !state.overrides.host_target) { + std::error_code ec; + const auto marker = mcpp::home::root() / "cache" / "notes" / "profile-ndebug-trim-paths"; + if (!std::filesystem::exists(marker, ec)) { + mcpp::ui::info("note", std::format( + "the {} profile now defines NDEBUG and trims the source paths an artifact " + "carries (`app@1.2.0/src/main.cpp`). To keep `assert` or absolute paths, write " + "`ndebug = false` or `trim_paths = false` under [profile.{}]. Said once.", + pname, pname)); + std::filesystem::create_directories(marker.parent_path(), ec); + std::ofstream(marker) << "2026.10.23.1\n"; + } + } state.m->buildConfig.cflags.insert(state.m->buildConfig.cflags.end(), pr.cflags.begin(), pr.cflags.end()); state.m->buildConfig.cxxflags.insert(state.m->buildConfig.cxxflags.end(), @@ -655,11 +689,50 @@ step1_apple_sdk_check(PrepareState& state, TargetOverrideCtx& ctx) { // Declaring a toolchain says which compiler; it says nothing about // where the headers and stub libraries are, and every compiler needs // them. + // + // FROM THE BASE STATEMENT (design 2026-10-23 §4.4): `iphoneos@system` by + // default, `iphoneos@`, or `{ path }`, located by Apple's own + // conventions -- `SDKROOT` on every host, `xcrun` and the developer + // directory on an Apple one. A native macOS build that states its SDK is + // located the same way; one that states nothing keeps `xcrun`'s answer. + if (ctx.parsed && ctx.parsed->os == "macos") { + auto stmt = base_statement(state, *ctx.parsed); + if (!stmt) return std::unexpected(stmt.error()); + if (stmt->form == mcpp::toolchain::base::Form::Locator + || stmt->form == mcpp::toolchain::base::Form::Path) { + auto sdk = mcpp::toolchain::base::locate_apple(*stmt, mcpp::platform::macos::sdk_macos); + if (!sdk) { + refusal::record(refusal::Code::AppleSdkAbsent); + return std::unexpected(std::format( + "target {} names the macosx SDK {}, which this machine does not provide: {}", + ctx.parsed->str(), stmt->text, sdk.error())); + } + state.appleSdkLocated = sdk->root; + state.appleSdkFound = *sdk; + } + } if (ctx.parsed && ctx.parsed->is_ios()) { const auto which = ctx.parsed->is_ios_simulator() ? mcpp::platform::macos::sdk_iphonesim : mcpp::platform::macos::sdk_iphoneos; - state.appleSdkLocated = mcpp::platform::macos::sdk_path(which); + auto stmt = base_statement(state, *ctx.parsed); + if (!stmt) return std::unexpected(stmt.error()); + mcpp::toolchain::base::Statement asked = *stmt; + // Only a locator and a path name an SDK; a package or "" keep the + // meaning they had on this row, and the SDK is the default one. + if (asked.form != mcpp::toolchain::base::Form::Locator + && asked.form != mcpp::toolchain::base::Form::Path) { + asked.form = mcpp::toolchain::base::Form::Locator; + asked.family = std::string(which); + asked.version = "system"; + asked.text = std::format("{}@system", which); + } + auto sdk = mcpp::toolchain::base::locate_apple(asked, which); + std::string why = sdk ? std::string{} : sdk.error(); + if (sdk) { + state.appleSdkLocated = sdk->root; + state.appleSdkFound = *sdk; + } // AN UNSET FLOOR IS THE LOCATED SDK'S VERSION, READ RATHER THAN // LEFT TO THE DRIVER. `docs/20` promised that an unversioned // triple meant the SDK's own default; measured on macos-15 with @@ -671,29 +744,53 @@ step1_apple_sdk_check(PrepareState& state, TargetOverrideCtx& ctx) { // that the fingerprint slot, the effective triple and every // report read one value. if (state.appleSdkLocated && state.m->buildConfig.iosDeploymentTarget.empty()) { - if (auto v = mcpp::platform::macos::sdk_version(which)) { - state.m->buildConfig.iosDeploymentTarget = *v; + if (!state.appleSdkFound->version.empty()) { + state.m->buildConfig.iosDeploymentTarget = state.appleSdkFound->version; state.iosFloorFromSdk = true; } } + // THE SLICE DECIDES THE ROW (D16). An SDK that no longer carries the + // target's architecture is refused naming the slice, not left to fail + // at the first link. + if (state.appleSdkLocated) { + const auto slice = std::format("{}-ios{}", + ctx.parsed->arch == "aarch64" ? "arm64" : ctx.parsed->arch, + ctx.parsed->is_ios_simulator() ? "-simulator" : ""); + if (auto has = mcpp::platform::macos::sdk_has_target(*state.appleSdkLocated, slice); + has && !*has) { + refusal::record(refusal::Code::AppleSdkAbsent); + return std::unexpected(std::format( + "target {}: the {} SDK at {} carries no {} slice (its libSystem.tbd " + "does not list it). Name an SDK that does: [target.{}] sysroot = " + "\"{}@\" or {{ path = \"…\" }}.", + ctx.parsed->str(), which, state.appleSdkLocated->string(), slice, + ctx.parsed->str(), which)); + } + } if (!state.appleSdkLocated) { // A CODE, BECAUSE THE MATRIX COMPARES REASONS AND NOT ONLY // OUTCOMES. A refusal with no code is recorded as `other`, // which `check_matrix_reasons.sh` refuses on the ground that // it freezes an unnamed branch into the expected table. refusal::record(refusal::Code::AppleSdkAbsent); + if (stmt->stated()) + return std::unexpected(std::format( + "target {} names the {} SDK {} ({}), which this machine does not " + "provide: {}", ctx.parsed->str(), which, asked.text, + stmt->originFile.empty() ? stmt->originKey : stmt->originFile, why)); return std::unexpected(std::format( "target {} needs the {} SDK, which this machine does not " "provide.\n" " It is not redistributable, so mcpp LOCATES it " - "rather than installing it: `xcrun --sdk {} " - "--show-sdk-path` must answer, which needs Xcode on macOS " - "(not the Command Line Tools alone -- those ship the " - "macOS SDK only).\n" - " Check `xcode-select -p`, and note that the " - "compiler is not what is missing: these rows pin " + "rather than installing it. {}.\n" + " On macOS install Xcode (not the Command Line Tools alone -- " + "those ship the macOS SDK only) so that `xcrun --sdk {} " + "--show-sdk-path` answers. Elsewhere, produce one from your own " + "Xcode.xip (e.g. with xtool) and set SDKROOT, or write " + "[target.{}] sysroot = {{ path = \"…\" }}.\n" + " The compiler is not what is missing: these rows pin " "`xim:llvm`, which every other Apple row also uses.", - ctx.parsed->str(), which, which)); + ctx.parsed->str(), which, why, which, ctx.parsed->str())); } } return {}; @@ -1980,7 +2077,11 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { "gate that locates it did not run for this " "request", want->str())); } - state.tc->appleSdkRoot = *state.appleSdkLocated; + state.tc->base.apple.root = *state.appleSdkLocated; + if (state.appleSdkFound) { + state.tc->base.apple.name = state.appleSdkFound->name; + state.tc->base.apple.version = state.appleSdkFound->version; + } auto sdk = state.appleSdkLocated; // AND THE std MODULE'S OWN COMMAND, WHICH IS A SEPARATE // CHANNEL. Same reason the Android rows set it: the @@ -2038,10 +2139,29 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { // 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(*state.m, *want)); - !want_sysroot.empty()) { + const std::string want_sysroot = + mcpp::toolchain::triple::effective_sysroot( + *want, base_statement_text(state, *want)); + // A C library named by path (design 2026-10-23 §3.1): the same + // `/{include,lib}/` layout, nothing to install. + if (want_sysroot.starts_with("path:")) { + const std::filesystem::path dir = want_sysroot.substr(5); + std::error_code ec2; + if (!std::filesystem::is_directory(dir, ec2)) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot = {{ path = \"{}\" }} does not exist", want->str(), + dir.generic_string())); + } + if (auto spec = mcpp::freestanding::resolve(*want)) { + state.tc->base.multilibRoot = dir; + state.tc->base.package = dir.filename().string(); + const auto inc = dir / "include" / std::string(spec->libdir); + const auto lib = dir / "lib" / std::string(spec->libdir); + if (std::filesystem::is_directory(inc, ec2)) state.tc->base.multilibInclude = inc; + if (std::filesystem::is_directory(lib, ec2)) state.tc->base.multilibLib = lib; + } + } else if (!want_sysroot.empty()) { if (auto cfg3 = state.get_cfg(true); cfg3) { auto ref = mcpp::provider::parse_ref(want_sysroot); auto xl = mcpp::config::make_xlings_env(**cfg3); @@ -2086,12 +2206,12 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { const auto lib = *dir / "lib" / std::string(spec->libdir); std::error_code ec2; - state.tc->targetSysrootRoot = *dir; - state.tc->targetSysrootPkg = ref.name; + state.tc->base.multilibRoot = *dir; + state.tc->base.package = ref.name; if (std::filesystem::is_directory(inc, ec2)) - state.tc->targetSysrootInclude = inc; + state.tc->base.multilibInclude = inc; if (std::filesystem::is_directory(lib, ec2)) - state.tc->targetSysrootLib = lib; + state.tc->base.multilibLib = lib; } } } @@ -2105,12 +2225,17 @@ static std::expected step2_bind_msvc_toolset(PrepareState& state) { // THE MSVC TOOLSET OF THE CLANG ROW, chosen once and recorded before the // runtime identity below reads its SDK version. See bind_msvc_sysroot. + const std::string* declared = nullptr; + if (auto tt = mcpp::toolchain::triple::parse(state.tc->targetTriple)) { + if (auto s = base_statement(state, *tt); !s) return std::unexpected(s.error()); + declared = base_statement_text(state, *tt); + } if (state.tc->compiler == mcpp::toolchain::CompilerId::Clang && mcpp::toolchain::is_msvc_target(*state.tc)) { - auto bound = bind_msvc_sysroot(*state.tc, *state.m, [&] { return state.get_cfg(true); }); + auto bound = bind_msvc_sysroot(*state.tc, declared, [&] { return state.get_cfg(true); }); if (!bound) return std::unexpected(bound.error()); } else if (state.tc->compiler == mcpp::toolchain::CompilerId::MSVC) { - if (auto ok = check_cl_row_sysroot(*state.tc, *state.m); !ok) + if (auto ok = check_cl_row_sysroot(*state.tc, declared); !ok) return std::unexpected(ok.error()); } return {}; @@ -2129,10 +2254,17 @@ step2_windows_runtime_identity(PrepareState& state) { // `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.runtime.binding. - if (!state.tc->windowsSdkVersion.empty()) { + if (!state.tc->base.msvc.sdkVersion.empty()) { mcpp::platform::runtime::bind_windows_ucrt( - state.runtimeBindingSnapshot, state.tc->windowsSdkVersion); + state.runtimeBindingSnapshot, state.tc->base.msvc.sdkVersion); state.tc->runtimeContractHash = state.runtimeBindingSnapshot.contractHash; + // And the artifact's runtime, when a stated base re-derived it: + // the floor and the base's identity are both part of its contract. + if (state.targetRuntimeBinding) { + mcpp::platform::runtime::bind_windows_ucrt( + *state.targetRuntimeBinding, state.tc->base.msvc.sdkVersion); + state.tc->runtimeContractHash = state.targetRuntimeBinding->contractHash; + } } } @@ -2341,6 +2473,8 @@ std::expected phase2_define_toolchain_resolver(PrepareState& if (auto r = step2_apply_local_toolchain(state); !r) return std::unexpected(r.error()); if (auto r = step2_retarget_for_retargetable_driver(state); !r) return std::unexpected(r.error()); if (auto r = step2_bind_msvc_toolset(state); !r) return std::unexpected(r.error()); + if (auto r = step2_resolve_base(state); !r) return std::unexpected(r.error()); + if (auto r = step2_resolve_run_under(state); !r) return std::unexpected(r.error()); step2_windows_runtime_identity(state); if (auto r = step2_msvc_abi_without_msvc_repair(state); !r) return std::unexpected(r.error()); step2_musl_default_static_linkage(state); diff --git a/src/build/prepare/toolchain_env.cpp b/src/build/prepare/toolchain_env.cpp index 870a557d2..bd2df2fe5 100644 --- a/src/build/prepare/toolchain_env.cpp +++ b/src/build/prepare/toolchain_env.cpp @@ -64,15 +64,6 @@ namespace mcpp::build { // index is first opened, which can be hundreds of lines earlier; the message // that STOPS the build has to carry the cause, because that is the one a user // reads. See mcpp::pm::unusable_index_hint. -// The project's `[target.].sysroot`, or nullptr when it declared none. -// The row is found by `find_target_entry` (prepare_inputs), as every row is. -const std::string* -sysroot_override(const mcpp::manifest::Manifest& m, - const mcpp::toolchain::triple::Triple& t) { - auto* e = find_target_entry(m, t); - return (e && e->sysrootDeclared) ? &e->sysroot : nullptr; -} - // THE MSVC TOOLSET A CLANG `*-windows-msvc` BUILD COMPILES AGAINST. // // On an MSVC-ABI row the compiler is the toolchain and the MSVC toolset -- its @@ -90,14 +81,56 @@ sysroot_override(const mcpp::manifest::Manifest& m, // already names the alternatives. std::expected bind_msvc_sysroot(mcpp::toolchain::Toolchain& tc, - const mcpp::manifest::Manifest& m, + const std::string* declared, const std::function()>& cfgOf) { namespace msvc = mcpp::toolchain::msvc; auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); if (!tt) return {}; - const std::string* declared = sysroot_override(m, *tt); const std::string text = declared ? *declared : std::string("msvc@system"); + // A TOOLSET NAMED BY PATH (design 2026-10-23 §4.4): an unpacked Visual + // Studio tree, or clang's `/winsysroot` layout. The toolset is the newest + // under `VC/Tools/MSVC`; the SDK is the layout's own `Windows Kits/10` + // when it has one, and the machine's otherwise -- and the line printed + // below says which. + if (text.starts_with("path:")) { + const std::filesystem::path root = text.substr(5); + auto choice = msvc::toolset_in_root(root); + if (!choice) + return std::unexpected(std::format( + "[target.{}].sysroot = {{ path = \"{}\" }} holds no MSVC toolset: " + "expected `VC/Tools/MSVC//include` under it (a Visual Studio " + "tree, or a clang /winsysroot layout)", tt->str(), root.generic_string())); + std::optional sdk; + std::string sdkFrom = "the layout's Windows Kits/10"; + std::error_code ec; + if (std::filesystem::is_directory(root / "Windows Kits" / "10", ec)) + sdk = msvc::sdk_in_root(root / "Windows Kits" / "10"); + if (!sdk) { sdk = msvc::find_windows_sdk(); sdkFrom = "this machine's"; } + tc.base.msvc.toolsDir = choice->toolsDir; + tc.base.msvc.toolsVersion = choice->version; + tc.base.msvc.compilerVersion = msvc::compiler_version_in_tools_dir(choice->toolsDir, tt->arch); + tc.base.msvc.origin = "path"; + tc.base.msvc.product = choice->product; + tc.base.msvc.redistDir = msvc::vc_redist_dir_for_tools_dir(choice->toolsDir, tt->arch); + if (sdk) { + tc.base.msvc.sdkRoot = sdk->root; + tc.base.msvc.sdkVersion = sdk->version; + } + tc.stdlibVersion = choice->version; + const auto ixx = choice->toolsDir / "modules" / "std.ixx"; + if (std::filesystem::exists(ixx, ec)) { + const auto compat = ixx.parent_path() / "std.compat.ixx"; + tc.set_std_modules(ixx, std::filesystem::exists(compat, ec) + ? compat : std::filesystem::path{}); + tc.importStdMinLevel = msvc::std_module_min_level_for_stl(ixx); + } + mcpp::ui::info("Resolved", std::format( + "sysroot {} → MSVC {}{}", root.generic_string(), choice->version, + sdk ? std::format(" · Windows SDK {} ({})", sdk->version, sdkFrom) + : std::string(" · no Windows SDK found"))); + return {}; + } auto spec = mcpp::toolchain::parse_toolchain_spec(text); if (!spec) return std::unexpected(std::format( @@ -190,21 +223,21 @@ bind_msvc_sysroot(mcpp::toolchain::Toolchain& tc, auto sdk = msvc::resolve_sdk_for(choice->toolsDir / "bin"); if (!sdk.note.empty()) mcpp::ui::info("note", sdk.note); - tc.msvcToolsDir = choice->toolsDir; - tc.msvcToolsVersion = choice->version; + tc.base.msvc.toolsDir = choice->toolsDir; + tc.base.msvc.toolsVersion = choice->version; // The version of this toolset's cl.exe, read from the file (this row runs // no cl.exe) and said to the driver by the link model, so it is on every // command and in every key rather than chosen by the driver (mcpp#746). - tc.msvcCompilerVersion = msvc::compiler_version_in_tools_dir(choice->toolsDir, tt->arch); - tc.msvcOrigin = origin; - tc.msvcProduct = choice->product; + tc.base.msvc.compilerVersion = msvc::compiler_version_in_tools_dir(choice->toolsDir, tt->arch); + tc.base.msvc.origin = origin; + tc.base.msvc.product = choice->product; // The toolset's own redistributable CRT (#718), reached from the // sysroot rather than from a cl.exe path — this row runs no cl.exe. - tc.msvcRedistDir = msvc::vc_redist_dir_for_tools_dir( + tc.base.msvc.redistDir = msvc::vc_redist_dir_for_tools_dir( choice->toolsDir, tt->arch); if (sdk.sdk) { - tc.windowsSdkRoot = sdk.sdk->root; - tc.windowsSdkVersion = sdk.sdk->version; + tc.base.msvc.sdkRoot = sdk.sdk->root; + tc.base.msvc.sdkVersion = sdk.sdk->version; } // The STL is this toolset's, so its version is the standard library's. tc.stdlibVersion = choice->version; @@ -231,9 +264,9 @@ bind_msvc_sysroot(mcpp::toolchain::Toolchain& tc, mcpp::ui::info("Resolved", std::format( "sysroot {} → MSVC {} ({}: {}){}", spec->spec_str(), choice->version, origin, choice->product, - tc.windowsSdkVersion.empty() + tc.base.msvc.sdkVersion.empty() ? std::string{} - : std::format(" · Windows SDK {}", tc.windowsSdkVersion))); + : std::format(" · Windows SDK {}", tc.base.msvc.sdkVersion))); return {}; } @@ -242,12 +275,19 @@ bind_msvc_sysroot(mcpp::toolchain::Toolchain& tc, // statement of the compiler's toolset, and one that names a different // toolset is refused rather than silently ignored. std::expected -check_cl_row_sysroot(const mcpp::toolchain::Toolchain& tc, - const mcpp::manifest::Manifest& m) { +check_cl_row_sysroot(const mcpp::toolchain::Toolchain& tc, const std::string* declared) { auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); if (!tt) return {}; - const std::string* declared = sysroot_override(m, *tt); if (!declared) return {}; + // A directory is clang's to compile against; cl.exe's toolset is the + // compiler itself (SPEC-006 §3.5). + if (declared->starts_with("path:")) + return std::unexpected(std::format( + "[target.{}].sysroot = {{ path = \"{}\" }}: with cl.exe the compiler is its own " + "sysroot, so a directory cannot be its toolset. Build with clang to compile " + "against an unpacked toolset (a Visual Studio tree or a /winsysroot layout), or " + "pin the toolset in the toolchain (`msvc@`).", + tt->str(), declared->substr(5))); auto spec = mcpp::toolchain::parse_toolchain_spec(*declared); if (!spec || spec->version.empty() || spec->version == "system") return {}; // /bin/Host//cl.exe → is named by the toolset. @@ -344,8 +384,8 @@ build_information(const mcpp::manifest::Manifest& m, const mcpp::toolchain::Tool if (msvcAbi) { const auto arch = msvc_arch_of(tc.targetTriple); clBin = msvcCompiler ? tc.binaryPath.parent_path() - : (tc.msvcToolsDir.empty() ? std::filesystem::path{} - : msvc_bin_dir(tc.msvcToolsDir, arch)); + : (tc.base.msvc.toolsDir.empty() ? std::filesystem::path{} + : msvc_bin_dir(tc.base.msvc.toolsDir, arch)); if (!clBin.empty()) { const auto cl = clBin / "cl.exe"; put("MCPP_ABI_TOOL_CXX", cl); @@ -370,9 +410,9 @@ build_information(const mcpp::manifest::Manifest& m, const mcpp::toolchain::Tool info["MCPP_TOOL_ENV"] = env; info["MCPP_TOOLSET_IDENTITY"] = std::format( "msvc {}; sdk {}", - tc.msvcToolsVersion.empty() ? clBin.parent_path().parent_path() + tc.base.msvc.toolsVersion.empty() ? clBin.parent_path().parent_path() .parent_path().filename().string() - : tc.msvcToolsVersion, + : tc.base.msvc.toolsVersion, sdk.sdk->version); } // A Visual Studio instance, when the toolset came from one: the @@ -380,7 +420,7 @@ build_information(const mcpp::manifest::Manifest& m, const mcpp::toolchain::Tool const auto tools = clBin.parent_path().parent_path().parent_path(); const auto vc = tools.parent_path().parent_path().parent_path(); std::error_code ec; - if (tc.msvcOrigin != "managed" && vc.filename() == "VC" + if (tc.base.msvc.origin != "managed" && vc.filename() == "VC" && std::filesystem::exists(vc / "Auxiliary" / "Build", ec)) put("MCPP_MSVC_INSTANCE_DIR", vc.parent_path()); } @@ -434,13 +474,13 @@ void fill_target_build_env(mcpp::build::BuildProgramEnv& e, } e.toolchainDir = (tc && !tc->binaryPath.empty()) ? tc->binaryPath.parent_path().parent_path().string() : std::string{}; - e.targetSysroot = tc ? tc->targetSysrootRoot.string() : std::string{}; + e.targetSysroot = tc ? tc->base.multilibRoot.string() : std::string{}; e.compilerId = !tc ? std::string{} : tc->compiler == mcpp::toolchain::CompilerId::GCC ? "gcc" : tc->compiler == mcpp::toolchain::CompilerId::Clang ? "clang" : tc->compiler == mcpp::toolchain::CompilerId::MSVC ? "msvc" : std::string{}; - e.targetLibc = tc ? tc->targetSysrootPkg : std::string{}; + e.targetLibc = tc ? tc->base.package : std::string{}; // Read from the toolchain the engine resolved, the same field the cache // key, the ABI tag and the toolchain fingerprint read. Not re-derived from // `compilerId`: clang answers "libc++" or "libstdc++" depending on how the diff --git a/src/build/prepare/xlings.cpp b/src/build/prepare/xlings.cpp index 8e0b66acc..bc18afbeb 100644 --- a/src/build/prepare/xlings.cpp +++ b/src/build/prepare/xlings.cpp @@ -264,8 +264,8 @@ step3_define_host_tc_closures_and_refresh_index(PrepareState& state) { // fine-grained glibc/linux-headers xpkg directories `resolve_link_model` // attaches as explicit `-isystem` rows — from the SECOND argument, and // does so only when it is given; passing only `frontend` leaves - // `tc.payloadPaths` unset, so `host_base_flags`/`host_compile_tokens` - // fell back to `tc.sysroot` alone (from the payload's own + // `tc.base.payload` unset, so `host_base_flags`/`host_compile_tokens` + // fell back to `tc.base.root` alone (from the payload's own // `*sysroot_spec: --sysroot=%R`, `%R` being wherever the fixup pointed // it — nothing, on a sandbox with no leaked subos sysroot to fill it // in by accident). @@ -434,9 +434,17 @@ std::expected phase3_xlings_before_graph(PrepareState& state) // `bind_msvc_sysroot` locates or installs itself -- an installed // toolset of the pinned version must win over a download, and // `msvc@system` is not a package at all. - if (tt && !tt->is_msvc_env()) - targetSysroot = mcpp::toolchain::triple::effective_sysroot( - *tt, sysroot_override(*state.m, *tt)); + // + // AND ONLY A PACKAGE IS MATERIALISED. A path, a locator and the empty + // form name nothing to install (design 2026-10-23 §3.1). + if (tt && !tt->is_msvc_env()) { + auto stmt = base_statement(state, *tt); + if (!stmt) return std::unexpected(stmt.error()); + if (!stmt->stated()) targetSysroot = mcpp::toolchain::triple::effective_sysroot(*tt, nullptr); + else if (stmt->form == mcpp::toolchain::base::Form::Package + && tt->is_freestanding()) + targetSysroot = stmt->text; + } } const bool materializeRootRuntime = !state.overrides.inherited_runtime_binding diff --git a/src/build/prepare_inputs.cppm b/src/build/prepare_inputs.cppm index aaa7abcd7..2ed984355 100644 --- a/src/build/prepare_inputs.cppm +++ b/src/build/prepare_inputs.cppm @@ -562,6 +562,9 @@ std::string canonical_configuration_flags(const mcpp::manifest::Manifest& root, s += " debug="; s += root.buildConfig.debug ? "1" : "0"; s += " lto="; s += root.buildConfig.lto ? "1" : "0"; s += " strip="; s += root.buildConfig.strip ? "1" : "0"; + // Only when on: a profile that has neither keeps the identity it had. + if (root.buildConfig.ndebug) s += " ndebug=1"; + if (root.buildConfig.trimPaths) s += " trim_paths=1"; if (!root.buildConfig.dependencyLinkage.empty()) { s += " deplinkage="; s += root.buildConfig.dependencyLinkage; diff --git a/src/build/refusal.cppm b/src/build/refusal.cppm index f66303a5b..b9a95261f 100644 --- a/src/build/refusal.cppm +++ b/src/build/refusal.cppm @@ -171,6 +171,13 @@ enum class Code { PayloadOverride, PayloadRequest, LocalToolchain, + // The target base (`sysroot`) cannot serve this build: a spelling the row + // does not take, a path that does not exist, a directory that supplies no + // C library, a C library older than the compiler's runtime requires, or a + // native base without its loader (design 2026-10-23). Distinct from + // LocalToolchain, which is about the compiler: this one is about what it + // builds against. + TargetBase, // A PE shared library whose exports are discovered (`windows_auto_export` // stated `true`, or `/GL` written into its sources' flags) would be // compiled by cl.exe into `/GL` objects, which hold no symbol table to @@ -229,6 +236,7 @@ constexpr std::string_view name(Code c) { case Code::PayloadOverride: return "payload-override"; case Code::PayloadRequest: return "payload-request"; case Code::LocalToolchain: return "local-toolchain"; + case Code::TargetBase: return "target-base"; case Code::LtoExportDiscovery: return "lto-export-discovery"; case Code::Other: return "other"; } diff --git a/src/cli.cppm b/src/cli.cppm index e4ea4218a..66029520c 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -260,6 +260,14 @@ int run(int argc, char** argv) { if (i + 1 < argc) mcpp::platform::env::set("MCPP_TOOLCHAIN", argv[++i]); } else if (a.starts_with("--toolchain=")) mcpp::platform::env::set("MCPP_TOOLCHAIN", std::string(a.substr(12))); + // --sysroot is `MCPP_SYSROOT` for this invocation (design 2026-10-23 + // §3.3), for the reason --toolchain is: its consumer is the base + // statement deep in prepare, and the fast paths read the variable to + // know a build's base changed. + else if (a == "--sysroot") { + if (i + 1 < argc) mcpp::platform::env::set("MCPP_SYSROOT", argv[++i]); + } + else if (a.starts_with("--sysroot=")) mcpp::platform::env::set("MCPP_SYSROOT", std::string(a.substr(10))); else if (a.starts_with("-j") && a.size() > 2) mcpp::platform::env::set("MCPP_JOBS", std::string(a.substr(2))); // --play-game rides the same channel: its value is optional @@ -426,6 +434,7 @@ int run(int argc, char** argv) { .option(cl::Option("managed-only") .help("Fail if a toolchain, payload or plugin tool does not come from the ecosystem") .global()) + // Answers "what do you speak" without spawning a command that might // fail. An optimisation, NOT the client's detection rule: on any mcpp // predating it this is itself an unknown option, so a client must @@ -456,6 +465,8 @@ int run(int argc, char** argv) { .help("Concurrent compiles: a number, or 'auto' to size from cores + free RAM")) .option(cl::Option("toolchain").takes_value().value_name("SPEC") .help("Build with this toolchain for one build, e.g. llvm@22.1.8")) + .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") + .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) .option(cl::Option("no-cache") .help("Deprecated alias for --cache=off (also clears the build dir)")) .option(cl::Option("target").takes_value().help( @@ -524,6 +535,8 @@ int run(int argc, char** argv) { // unknown option (#634 A10). .option(cl::Option("toolchain").takes_value().value_name("SPEC") .help("Build with this toolchain for one invocation, e.g. llvm@22.1.8")) + .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") + .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) .option(cl::Option("cache").takes_value().value_name("MODE") .help("Global dependency cache: global (default) | local | off")) .option(cl::Option("no-cache") @@ -620,6 +633,8 @@ int run(int argc, char** argv) { .help("Run the tests of the named workspace member (namespace.name or package name, then directory); repeat to test several")) .option(cl::Option("toolchain").takes_value().value_name("SPEC") .help("Build the tests with this toolchain for one invocation, e.g. llvm@22.1.8")) + .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") + .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) .option(cl::Option("cache").takes_value().value_name("MODE") .help("Global dependency cache: global (default) | local | off")) .option(cl::Option("no-cache") @@ -643,7 +658,7 @@ int run(int argc, char** argv) { .action(wrap_rc(cmd_clean))) .subcommand(cl::App("why") .description("Explain how the toolchain / runtime / deps / runners / sources were resolved") - .arg(cl::Arg("topic").help("toolchain | runtime | deps | runners | sources | tool | payload | build-programs | compat (default: all)")) + .arg(cl::Arg("topic").help("toolchain | sysroot | runtime | deps | runners | sources | tool | payload | build-programs | compat (default: all)")) .arg(cl::Arg("subject").help("for `tool` and `payload`: the name to look up")) // `--target` / `--toolchain` make this a QUERY rather than a // report on the current directory's default: "what would a build @@ -653,6 +668,8 @@ int run(int argc, char** argv) { .help("Ask about this target instead of the project's default")) .option(cl::Option("toolchain").takes_value() .help("Ask about this toolchain, e.g. llvm@22.1.8")) + .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") + .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) .option(cl::Option("features").takes_value().value_name("LIST") .help("Resolve with these features, as `build --features` does (root features and /)")) .option(cl::Option("format").takes_value().value_name("json") @@ -733,6 +750,8 @@ int run(int argc, char** argv) { .option(cl::Option("dev").help("Shorthand for --profile dev")) .option(cl::Option("toolchain").takes_value().value_name("SPEC") .help("Build with this toolchain for one invocation, e.g. llvm@22.1.8")) + .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") + .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) .option(cl::Option("features").takes_value().value_name("LIST") .help("Activate root-package features, and a dependency's as /, for every build pass of the pack (comma-separated)")) // `--format` names the PACKAGE format here, so machine output is @@ -787,6 +806,8 @@ int run(int argc, char** argv) { .help("Describe the build for (same axis as `mcpp build --target`)")) .option(cl::Option("toolchain").takes_value().value_name("SPEC") .help("Describe the build with this toolchain, e.g. llvm@22.1.8")) + .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") + .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) .option(cl::Option("accel").takes_value().value_name("SPEC") .help("Device backends and architectures (same axis as `mcpp build --accel`)")) .option(cl::Option("no-accel") diff --git a/src/cli/cmd_self.cppm b/src/cli/cmd_self.cppm index 5097cffdf..8419dca67 100644 --- a/src/cli/cmd_self.cppm +++ b/src/cli/cmd_self.cppm @@ -107,6 +107,9 @@ export int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { return 2; } const std::string topic = parsed.positional(0); + if (topic == "sysroot") + return mcpp::doctor::why_sources_json("sources", "sysroot", + parsed.option_or_empty("features").value()); if (topic == "sources" || topic == "tool" || topic == "payload") return mcpp::doctor::why_sources_json(topic, parsed.positional(1), parsed.option_or_empty("features").value()); diff --git a/src/config.cppm b/src/config.cppm index 21d663f95..5bd25bbc3 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -155,6 +155,22 @@ struct GlobalConfig { }; std::map payloadOverrides; + // From config.toml [target.] (design 2026-10-23 §3.3, §3.7): THIS + // MACHINE'S DEFAULTS for a target's base and execution host. A project's + // `mcpp.toml` outranks them and the environment overrides both. The + // user's file outranks the system's (`/etc/mcpp/config.toml`), key by key. + struct TargetDefault { + std::string sysroot; // canonical text; `path:` for a table + bool sysrootDeclared = false; + std::vector sysrootProvides; + int sysrootLine = 0; + std::vector runUnder; + int runUnderLine = 0; + std::filesystem::path sysrootFile; // the config.toml that said so + std::filesystem::path runUnderFile; + }; + std::map targetDefaults; + // Resolved xlings home (registryDir unless overridden) std::filesystem::path xlingsHome() const { return xlingsHomeOverride.empty() ? registryDir : xlingsHomeOverride; @@ -341,6 +357,15 @@ void print_env(const GlobalConfig& cfg); std::expected, std::string> parse_payload_overrides(const mcpp::libs::toml::Document& doc); +// `[target.]` of a parsed config.toml: `sysroot` (a string, or +// `{ path, provides }` with `path` relative to the file) and `run_under`. +// Merged into `into` below what it already holds, so the user's file is read +// first and the system's fills the gaps. +std::expected +parse_target_defaults(const mcpp::libs::toml::Document& doc, + const std::filesystem::path& file, + std::map& into); + // Normalize legacy index naming in a loaded config (exported for tests): // org migration mcpp-community/mcpp-index -> mcpplibs/mcpp-index, index name // mcpp-index -> mcpplibs, then name+url dedup. Order matters: URL first, so @@ -686,6 +711,80 @@ void canonicalize_legacy_index_names(GlobalConfig& cfg) { cfg.indexRepos = std::move(normalized); } +std::expected +parse_target_defaults(const mcpp::libs::toml::Document& doc, + const std::filesystem::path& file, + std::map& into) { + auto* tt = doc.get_table("target"); + if (!tt) return {}; + for (auto const& [triple, body] : *tt) { + if (!body.is_table()) + return std::unexpected(std::format( + "{} [target.{}] must be a table with `sysroot` and/or `run_under`", + file.string(), triple)); + auto& d = into[triple]; + for (auto const& [k, v] : body.as_table()) { + if (k == "sysroot") { + if (d.sysrootDeclared) continue; // a nearer file said it + std::string text; + std::vector provides; + if (v.is_string()) { + text = v.as_string(); + } else if (v.is_table()) { + std::string path; + for (auto const& [tk, tv] : v.as_table()) { + if (tk == "path" && tv.is_string()) path = tv.as_string(); + else if (tk == "provides" && tv.is_array()) { + for (auto const& el : tv.as_array()) + if (el.is_string()) provides.push_back(el.as_string()); + } else + return std::unexpected(std::format( + "{} [target.{}].sysroot: '{}' is not a key of a sysroot table; " + "the keys are `path` and `provides`", file.string(), triple, tk)); + } + if (path.empty()) + return std::unexpected(std::format( + "{} [target.{}].sysroot is a table without `path`", file.string(), triple)); + std::filesystem::path p(path); + if (path == "~" || path.starts_with("~/")) + if (auto* h = std::getenv(mcpp::platform::is_windows ? "USERPROFILE" : "HOME"); h && *h) + p = std::filesystem::path(h) / (path.size() > 2 ? path.substr(2) : std::string{}); + if (p.is_relative()) p = file.parent_path() / p; + text = "path:" + p.lexically_normal().generic_string(); + } else { + return std::unexpected(std::format( + "{} [target.{}].sysroot must be a string or {{ path = \"\" }}", + file.string(), triple)); + } + d.sysroot = std::move(text); + d.sysrootProvides = std::move(provides); + d.sysrootDeclared = true; + d.sysrootLine = static_cast(v.position.line); + d.sysrootFile = file; + } else if (k == "run_under") { + if (!d.runUnder.empty()) continue; + if (!v.is_array()) + return std::unexpected(std::format( + "{} [target.{}].run_under must be an array of strings", file.string(), triple)); + for (auto const& el : v.as_array()) { + if (!el.is_string()) + return std::unexpected(std::format( + "{} [target.{}].run_under must contain only strings", file.string(), triple)); + d.runUnder.push_back(el.as_string()); + } + d.runUnderLine = static_cast(v.position.line); + d.runUnderFile = file; + } else { + return std::unexpected(std::format( + "{} [target.{}]: '{}' is not a key of a machine's target defaults; " + "the keys are `sysroot` and `run_under` (everything else about a " + "target belongs in the project's mcpp.toml)", file.string(), triple, k)); + } + } + } + return {}; +} + std::expected, std::string> parse_payload_overrides(const mcpp::libs::toml::Document& doc) { std::map out; @@ -869,6 +968,13 @@ std::expected load_or_init( cfg.payloadOverrides = std::move(*overrides); else return std::unexpected(ConfigError{overrides.error()}); + // [target.]: this machine's defaults, the user's first. + if (auto r = parse_target_defaults(*doc, cfg.configFile, cfg.targetDefaults); !r) + return std::unexpected(ConfigError{r.error()}); + if (systemDoc) + if (auto r = parse_target_defaults(*systemDoc, mcpp::home::system_config_file(), + cfg.targetDefaults); !r) + return std::unexpected(ConfigError{r.error()}); // A read-only report must not create its log directory either. if (initialization != Initialization::ReadOnly) { diff --git a/src/doctor.cppm b/src/doctor.cppm index b8bd2da03..a4d466fca 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -507,7 +507,7 @@ export int doctor_report() { std::error_code cwdEc; auto project = std::filesystem::current_path(cwdEc); if (mcpp::fallback::sysroot_is_foreign( - tc->sysroot, (*cfg).registryDir, + tc->base.root, (*cfg).registryDir, cwdEc ? std::filesystem::path{} : project)) warn(std::format( "sysroot {} belongs to neither this mcpp home ({}) nor " @@ -515,7 +515,7 @@ export int doctor_report() { "here declared. mcpp remaps a baked sysroot when it can " "find the equivalent under the registry; seeing it here " "means it could not", - tc->sysroot.string(), (*cfg).registryDir.string())); + tc->base.root.string(), (*cfg).registryDir.string())); } if (!cfg) { err(cfg.error().message); @@ -1284,9 +1284,9 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) {"llvm", ts.llvmTriple}, }; - // THE C LIBRARY MODEL, NOT `tc.sysroot`. + // THE C LIBRARY MODEL, NOT `tc.base.root`. // - // `tc.sysroot` is what the driver reports for `-print-sysroot`, and it is + // `tc.base.root` is what the driver reports for `-print-sysroot`, and it is // frequently empty for a toolchain that nonetheless receives an explicit // `--sysroot` on every command line. The first version of this reported it // and disagreed with the build: the query said `none` while build.ninja @@ -1533,12 +1533,12 @@ export int why_report(const std::string& topic, const std::string& features, const std::string& subject = {}) { const bool all = topic.empty() || topic == "all"; static constexpr std::string_view kTopics[] = { - "all", "toolchain", "runtime", "deps", "runners", "sources", "tool", "payload", + "all", "toolchain", "sysroot", "runtime", "deps", "runners", "sources", "tool", "payload", "build-programs", "compat", }; if (!topic.empty() && std::ranges::find(kTopics, std::string_view(topic)) == std::end(kTopics)) { std::println(stderr, "error: '{}' is not a topic of `mcpp why`; the topics are " - "toolchain, runtime, deps, runners, sources, tool , " + "toolchain, sysroot, runtime, deps, runners, sources, tool , " "payload , build-programs and compat", topic); return 2; } @@ -1594,6 +1594,31 @@ export int why_report(const std::string& topic, const std::string& features, std::println(" declared accelerators: {} (CI matrix hint)", as); } } + // THE TARGET BASE (design 2026-10-23 §4.10): what was stated, where, and + // what was measured there -- the one resolution every reader used. + if (all || topic == "sysroot") { + namespace tb = mcpp::toolchain::base; + const auto& b = tc.base; + std::println("sysroot: {}", b.stated() ? b.text : std::string("(the ecosystem's default)")); + std::println(" address={} runtime={}{}", tb::address_name(b.address), + tb::runtime_name(b.runtime), + b.root.empty() ? std::string{} : " root=" + b.root.generic_string()); + if (!b.libc.empty()) + std::println(" c-abi={}{}{}", b.libc, b.libcVersion.empty() ? "" : " " + b.libcVersion, + b.multiarch ? " multiarch=" + b.multiarchTuple : std::string{}); + if (!b.loader.empty()) std::println(" loader={}", b.loader.generic_string()); + for (auto const& d : b.runtimeDirs) std::println(" runtime dir {}", d.generic_string()); + if (!b.identity.empty()) std::println(" identity={}", b.identity); + if (auto it = std::ranges::find_if(ctx->sources, + [](const mcpp::build::SourceDecision& d) { return d.subject == "sysroot"; }); + it != ctx->sources.end()) + std::println(" source: {}", mcpp::build::source_tag(*it, ctx->projectRoot)); + if (!ctx->runUnder.empty()) { + std::string shown; + for (auto const& a : ctx->runUnder) shown += (shown.empty() ? "" : " ") + a; + std::println("run_under: {} [{}]", shown, ctx->runUnderOrigin); + } + } if (all || topic == "sources" || topic == "tool" || topic == "payload") print_sources(ctx->sources, all ? std::string_view("sources") : std::string_view(topic), subject, ctx->projectRoot); diff --git a/src/home.cppm b/src/home.cppm index 7f34c7098..eba473112 100644 --- a/src/home.cppm +++ b/src/home.cppm @@ -270,7 +270,10 @@ std::expected choose(const std::filesystem::path& target auto before = inspect(*original); if (!before && !force) return std::unexpected(before.error()); if (before && !same_path(before->effective, *effective) && initialized(before->effective) && !force) - return std::unexpected(std::format("a home is already initialized at '{}'; use `mcpp self init --force --home {}` to select '{}'", spelling(before->effective), spelling(*effective), spelling(*effective))); + // The command is for pasting into a shell, so the path in it is + // quoted by the shell's rules (mcpp#832: a home under `/tmp/mc probe` + // printed a command that selected `/tmp/mc` and passed `probe` on). + return std::unexpected(std::format("a home is already initialized at '{}'; use `mcpp self init --force --home {}` to select '{}'", spelling(before->effective), mcpp::platform::shell::paste_arg(spelling(*effective)), spelling(*effective))); if (!same_path(*original, *effective) && (contains_path(*original, *effective) || contains_path(*effective, *original))) return std::unexpected("the original and selected home must not contain each other"); Selection result{.original = *original, .effective = *effective, diff --git a/src/modgraph/p1689.cppm b/src/modgraph/p1689.cppm index 4e6e7e556..c51d36e46 100644 --- a/src/modgraph/p1689.cppm +++ b/src/modgraph/p1689.cppm @@ -26,6 +26,7 @@ import mcpp.platform; import mcpp.source_kind; import mcpp.toolchain.detect; import mcpp.toolchain.model; +import mcpp.toolchain.linkmodel; export namespace mcpp::modgraph::p1689 { @@ -351,8 +352,14 @@ scan_file(const std::filesystem::path& source, auto obj = base; obj += ".o"; std::string sysroot_flag; - if (!tc.sysroot.empty()) { - sysroot_flag = std::format(" --sysroot={}", shell_escape(tc.sysroot)); + if (tc.base.stated()) { + // A stated target base: the compile side the link model renders for + // it (its sysroot, multiarch, or payload headers), as every compile + // of this build is given. + sysroot_flag = mcpp::toolchain::resolve_link_model(tc).compile_flags( + [](const std::filesystem::path& p) { return shell_escape(p); }); + } else if (!tc.base.root.empty()) { + sysroot_flag = std::format(" --sysroot={}", shell_escape(tc.base.root)); } std::string std_flag = cppStandardFlag.empty() ? std::string("-std=c++23") diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 71114cc95..79fbbd2f6 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -52,6 +52,9 @@ import mcpp.provider; import mcpp.provider.select; import mcpp.manifest; import mcpp.toolchain.triple; +import mcpp.toolchain.base; +import mcpp.ui; +import mcpp.runtime.elf; export namespace mcpp::pack { @@ -96,6 +99,9 @@ ClosureUnavailableOutcome closure_unavailable_outcome(Format format) { struct Options { Mode mode = Mode::BundleProject; + // The build stated its target base (design 2026-10-23 §4.11): the base it + // was built against becomes the C library floor the package states. + bool baseStated = false; Format format = Format::Tar; // The `--format` value when `format == Dispatched`. Empty otherwise. // @@ -2532,6 +2538,37 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) if (!unresolvedText.empty()) return finish_without_closure(plan, unresolvedText, std::move(needs)); + // THE C LIBRARY THE MACHINE THAT RUNS IT MUST HAVE (design 2026-10-23 + // §4.11). A package that loads the target's own C library binds no base: + // the base it was built against becomes a FLOOR, the highest `GLIBC_x.y` + // the program and every library shipped beside it need. Said once on the + // terminal and written to HOST-REQUIREMENTS, where a reader of the bundle + // looks for what the target must provide. + // + // Only for a build that STATED its base: that is the case whose floor is a + // decision someone made (an old glibc, for distributions older than this + // machine). The ecosystem's own base packs exactly as it did. + if (plan.opts.baseStated + && (plan.opts.mode == Mode::BundleProject || plan.opts.mode == Mode::None)) { + std::vector needs; + auto consider = [&](const std::filesystem::path& p) { + if (auto f = mcpp::platform::elf::inspect_elf_runtime(p)) + needs.insert(needs.end(), f->requiredGlibcVersions.begin(), + f->requiredGlibcVersions.end()); + }; + consider(bundledBinary); + for (auto const& [staged, source] : bundledCopies) consider(staged); + if (auto floor = mcpp::toolchain::base::highest_glibc(needs); !floor.empty()) { + mcpp::ui::info("Requires", std::format( + "glibc >= {} on the machine that runs it (the highest GLIBC_ version " + "the program and the libraries packed with it need)", floor)); + auto reqs = plan.hostRequirements; + reqs.push_back({std::format("c-abi.glibc>={}", floor), "PT_INTERP", true}); + std::ofstream out(plan.stagingRoot / std::filesystem::path(kFileName)); + out << render(reqs); + } + } + // Output. if (plan.opts.format == Format::Tar) { if (auto r = make_tarball(plan.stagingRoot, plan.archivePath); !r) diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index a384ef9ac..6a30360a8 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -500,8 +500,8 @@ std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { opts.carryToolchainRuntime = false; } opts.toolchainRuntimeDirs = ctx.plan.toolchain.linkRuntimeDirs; - if (!ctx.plan.toolchain.msvcRedistDir.empty()) - opts.toolchainRuntimeDirs.push_back(ctx.plan.toolchain.msvcRedistDir); + if (!ctx.plan.toolchain.base.msvc.redistDir.empty()) + opts.toolchainRuntimeDirs.push_back(ctx.plan.toolchain.base.msvc.redistDir); // Where a third-party dependency's shared library may be found. Both // channels, because they answer for different things: the runtime // library dirs are what `mcpp run` puts on the loader's path, and the @@ -559,6 +559,7 @@ std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { } // ─── Build the plan ────────────────────────────────────────────── + opts.baseStated = ctx.tc.base.stated(); auto plan = mcpp::pack::make_plan(ctx.manifest, *run.cfg, opts, mainBinary, ctx.projectRoot, ctx.tc.targetTriple, // From the RESOLVED graph. `mcpp why runtime` on a real imgui project diff --git a/src/project.cppm b/src/project.cppm index 1f71f3877..d8c1cd603 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -348,6 +348,11 @@ void merge_layer_root_position(mcpp::manifest::Manifest& member, if (!to.debugDeclared && from.debugDeclared) { to.debug = from.debug; to.debugDeclared = true; } if (!to.ltoDeclared && from.ltoDeclared) { to.lto = from.lto; to.ltoDeclared = true; } if (!to.stripDeclared && from.stripDeclared) { to.strip = from.strip; to.stripDeclared = true; } + if (!to.ndebugDeclared && from.ndebugDeclared) { to.ndebug = from.ndebug; to.ndebugDeclared = true; } + if (!to.trimPathsDeclared && from.trimPathsDeclared) { + to.trimPaths = from.trimPaths; + to.trimPathsDeclared = true; + } if (!to.dependencyLinkageDeclared && from.dependencyLinkageDeclared) { to.dependencyLinkage = from.dependencyLinkage; to.dependencyLinkageDeclared = true; @@ -367,6 +372,12 @@ void merge_layer_root_position(mcpp::manifest::Manifest& member, if (!to.sysrootDeclared && from.sysrootDeclared) { to.sysroot = from.sysroot; to.sysrootDeclared = true; + to.sysrootProvides = from.sysrootProvides; + to.sysrootLine = from.sysrootLine; + } + if (to.runUnder.empty()) { + to.runUnder = from.runUnder; + to.runUnderLine = from.runUnderLine; } if (to.minApiLevel == 0) to.minApiLevel = from.minApiLevel; if (to.runner.empty()) to.runner = from.runner; @@ -1018,12 +1029,14 @@ export std::string root_position_key( // this build's target is not this build's. const bool written = !e.toolchain.empty() || !e.linkage.empty() || !e.cxxRuntime.empty() || e.sysrootDeclared || e.minApiLevel != 0 || !e.runner.empty() - || !e.namedRunners.empty(); + || !e.namedRunners.empty() || !e.runUnder.empty(); if (!written || (applies && !applies(triple))) continue; field("target." + triple + ".toolchain", e.toolchain); field("target." + triple + ".linkage", e.linkage); field("target." + triple + ".cxx_runtime", e.cxxRuntime); field("target." + triple + ".sysroot", e.sysroot); + list("target." + triple + ".sysroot.provides", e.sysrootProvides); + list("target." + triple + ".run_under", e.runUnder); field("target." + triple + ".min_api", std::to_string(e.minApiLevel)); list("target." + triple + ".runner", e.runner); for (auto const& [name, argv] : e.namedRunners) diff --git a/src/runtime/binding.cppm b/src/runtime/binding.cppm index 70ef5c797..de8349de7 100644 --- a/src/runtime/binding.cppm +++ b/src/runtime/binding.cppm @@ -88,6 +88,12 @@ struct RuntimeBinding { // be satisfied, so it is reported rather than degraded. bool declared = false; + // THE TARGET BASE THIS ARTIFACT BINDS, when one was stated (design + // 2026-10-23 §4.6, §4.9): its content identity. Empty for the ecosystem's + // own base, so a build that states nothing keeps the contract, and the + // output directory, it had. + std::string baseIdentity; + // Why something degraded. Non-empty ⇒ the caller MUST surface it. Never // an error: "it did not happen" and "it succeeded" producing identical // output is the property that made mcpp#352 expensive. @@ -99,7 +105,10 @@ struct RuntimeBinding { // payload, the HOST loader's built-in default directories are not part of // the search path, and modelling them is how a binary that cannot start // was reported as valid. - bool hermetic() const { return loader.has_value(); } + // + // The host's own base (`glibc@system`) names the host's loader, which is + // not private: it searches the host's directories, and so must the model. + bool hermetic() const { return loader.has_value() && provenance != "host-sysroot"; } }; // The PROVIDER half of a runtime identity: "glibc" from "glibc@2.39", "ucrt" @@ -138,6 +147,26 @@ std::string_view runtime_provider(std::string_view runtimeId) { // declare. void bind_windows_ucrt(RuntimeBinding& binding, std::string_view sdkVersion); +// THE ARTIFACT'S RUNTIME, DERIVED FROM A STATED BASE (design 2026-10-23 §4.2 +// step 6, §4.6) -- the same backward flow `bind_windows_ucrt` is, generalised. +// +// The binding resolved before the toolchain answers "what does the COMPILER +// run on", and that answer stays. A stated base answers "what does the +// ARTIFACT bind": with `binding` set, its loader and runtime directories +// replace the ecosystem's and the SubOS farm leaves the artifact's search +// path (it belongs to the ecosystem's base, not to this one); with it unset +// (a floor, or a cross target), only the identity is recorded. Either way the +// identity enters the contract hash, and through it the fingerprint. +struct TargetBase { + std::string runtimeId; // "glibc@2.31"; empty = keep + std::filesystem::path loader; + std::vector libraryDirs; + std::string identity; + bool binding = false; + bool host = false; // `glibc@system`: the host's loader +}; +void bind_target_base(RuntimeBinding& binding, const TargetBase& base); + namespace detail { std::string hash_contract(std::string_view data) { @@ -198,6 +227,10 @@ std::string canonical_contract(const RuntimeBinding& binding) { append_field(out, p.generic_string()); for (auto const& provider : binding.providerBindings) append_field(out, provider); + // Only when present: a binding with no stated base hashes exactly as it + // did before the field existed. + if (!binding.baseIdentity.empty()) + append_field(out, "base=" + binding.baseIdentity); for (auto const& d : binding.environment) { append_field(out, d.var); append_field(out, d.op); @@ -486,6 +519,27 @@ void bind_windows_ucrt(RuntimeBinding& binding, std::string_view sdkVersion) { detail::hash_contract(detail::canonical_contract(binding)); } +void bind_target_base(RuntimeBinding& binding, const TargetBase& base) { + if (base.binding) { + if (!base.runtimeId.empty()) { + binding.runtimeId = base.runtimeId; + binding.libc = base.runtimeId; + } + if (base.loader.empty()) binding.loader.reset(); + else binding.loader = base.loader; + binding.libraryDirs = base.libraryDirs; + binding.searchDirs.clear(); + // The providers and artifacts the SubOS described are the ecosystem + // base's; this artifact loads none of them. + binding.runtimeProviders.clear(); + binding.runtimeArtifacts.clear(); + binding.provenance = base.host ? "host-sysroot" : "sysroot"; + } + binding.baseIdentity = base.identity; + binding.contractHash = + detail::hash_contract(detail::canonical_contract(binding)); +} + std::string serialize_runtime_binding(const RuntimeBinding& binding) { nlohmann::json j; j["schema"] = binding.schema; @@ -541,6 +595,7 @@ std::string serialize_runtime_binding(const RuntimeBinding& binding) { }); } j["provenance"] = binding.provenance; + if (!binding.baseIdentity.empty()) j["base_identity"] = binding.baseIdentity; j["subos_dir"] = binding.subosDir.generic_string(); j["selection"] = { {"mode", binding.selection.mode @@ -576,6 +631,7 @@ deserialize_runtime_binding(std::string_view encoded) { out.libraryDirs.emplace_back(v.get()); out.declared = j.value("declared", false); out.note = j.value("note", ""); + out.baseIdentity = j.value("base_identity", ""); if (auto it = j.find("search_dirs"); it != j.end() && it->is_array()) for (auto const& v : *it) if (v.is_string()) out.searchDirs.emplace_back(v.get()); diff --git a/src/runtime/elf.cppm b/src/runtime/elf.cppm index 0ce3f04da..f56426493 100644 --- a/src/runtime/elf.cppm +++ b/src/runtime/elf.cppm @@ -1259,7 +1259,11 @@ RuntimeVerdict validate_runtime_artifact( std::filesystem::path{}); std::filesystem::path expectedLibDir; if (!expectedLoader.empty()) expectedLibDir = expectedLoader.parent_path(); - if (expectedLibDir.empty() && !binding.libraryDirs.empty()) + // The host's base: `/lib64/ld-linux-x86-64.so.2` is the standard name of a + // loader that lives beside the C library somewhere else (Debian multiarch), + // so the binding names the C library's directory itself. + if ((expectedLibDir.empty() || binding.provenance == "host-sysroot") + && !binding.libraryDirs.empty()) expectedLibDir = binding.libraryDirs.front(); if (!facts.interp.empty()) { diff --git a/src/toolchain/base_probe.cppm b/src/toolchain/base_probe.cppm new file mode 100644 index 000000000..fbbf01e5b --- /dev/null +++ b/src/toolchain/base_probe.cppm @@ -0,0 +1,274 @@ +// mcpp.toolchain.base_probe — locating and measuring a target base (design +// 2026-10-23 §4.3-§4.4, §4.8, §4.9). The data it fills is +// `mcpp.toolchain.base`; the decision of which statement applies is prepare's. +// +// LAYOUT IS ASKED OF THE DRIVER, IMPLEMENTATION AND VERSION OF THE +// PREPROCESSOR (D10). Nothing here recognises a vendor's directory tree. The +// one piece of layout knowledge is Debian multiarch, which is specified and +// which a gcc built without multiarch support needs to be told (scenario J). +// Nothing here writes into the directory it measures. + +export module mcpp.toolchain.base_probe; + +import std; +import mcpp.platform; +import mcpp.platform.macos; +import mcpp.toolchain.base; +import mcpp.toolchain.linkmodel; +import mcpp.toolchain.model; +import mcpp.toolchain.triple; +import mcpp.runtime.elf; + +export namespace mcpp::toolchain::base { + +namespace fs = std::filesystem; + +// The ecosystem's payload layout (`/include/features.h`, `/lib64` +// or `/lib`) rather than a sysroot's (`/usr/include`). A glibc +// package is the first; a crosstool-ng or Debian root is the second. +inline bool is_payload_layout(const fs::path& root) { + std::error_code ec; + if (fs::exists(root / "usr" / "include", ec)) return false; + if (!fs::exists(root / "include" / "features.h", ec)) return false; + return fs::exists(root / "lib64" / "libc.so.6", ec) || fs::exists(root / "lib" / "libc.so.6", ec) + || fs::exists(root / "lib64" / "libc.so", ec) || fs::exists(root / "lib" / "libc.so", ec); +} + +inline PayloadPaths payload_layout_paths(const fs::path& root, const fs::path& linuxInclude) { + PayloadPaths pp; + std::error_code ec; + pp.glibcInclude = root / "include"; + pp.glibcLib = fs::exists(root / "lib64" / "libc.so.6", ec) || fs::exists(root / "lib64" / "libc.so", ec) + ? root / "lib64" : root / "lib"; + pp.linuxInclude = fs::exists(root / "include" / "linux" / "limits.h", ec) ? fs::path{} : linuxInclude; + return pp; +} + +// The directories of a gnu-sysroot a loader searches, in the order the +// layouts put them: crosstool-ng and Fedora (`lib64`, `usr/lib64`), the +// generic `lib`, then Debian multiarch. +inline std::vector library_dirs(const fs::path& root, std::string_view tuple) { + std::vector out{root / "lib64", root / "usr" / "lib64", root / "lib", root / "usr" / "lib"}; + if (!tuple.empty()) { + out.push_back(root / "lib" / std::string(tuple)); + out.push_back(root / "usr" / "lib" / std::string(tuple)); + } + return out; +} + +namespace detail { + +inline std::string trim(std::string s) { + while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back(); + return s; +} + +// `-print-file-name=`: an absolute path when the driver found it, the +// name echoed back when it did not. +inline fs::path print_file_name(const std::vector& base, std::string_view name) { + auto argv = base; + argv.push_back(std::format("-print-file-name={}", name)); + auto r = mcpp::platform::process::capture_stdout(argv); + if (r.exit_code != 0) return {}; + fs::path p(trim(r.output)); + std::error_code ec; + if (!p.is_absolute() || !fs::exists(p, ec)) return {}; + return p; +} + +inline void mix(std::uint64_t& h, std::string_view s) { + for (unsigned char c : s) { h ^= c; h *= 1099511628211ull; } + h ^= 0x1f; h *= 1099511628211ull; +} + +inline void stamp(std::uint64_t& h, const fs::path& p) { + std::error_code ec; + mix(h, p.generic_string()); + auto real = fs::canonical(p, ec); + if (ec) { mix(h, "absent"); return; } + mix(h, real.generic_string()); + mix(h, std::format("{}", static_cast(fs::file_size(real, ec)))); + auto t = fs::last_write_time(real, ec); + mix(h, std::format("{}", static_cast(t.time_since_epoch().count()))); +} + +} // namespace detail + +// What a gnu-sysroot directory is, measured with the driver that will use it. +struct GnuMeasurement { + bool cAbiFound = false; // the driver found the C runtime + bool multiarch = false; + std::string tuple; + std::optional fact; + fs::path loader; + std::vector runtimeDirs; + std::vector evidence; // the files the identity stamps +}; + +// `driverBase` is the driver and the flags that select its target +// (`--target=`, `--no-default-config`), without any sysroot. `probeDir` is a +// writable directory for the probe translation unit. +inline GnuMeasurement measure_gnu(const std::vector& driverBase, + const triple::Triple& t, + const fs::path& root, + bool clangDriver, + const fs::path& probeDir) { + GnuMeasurement m; + std::error_code ec; + auto base = driverBase; + base.push_back("--sysroot=" + root.string()); + // The C runtime, as the driver looks for it. + auto crt = detail::print_file_name(base, "crt1.o"); + if (crt.empty()) crt = detail::print_file_name(base, "Scrt1.o"); + std::vector extra; + const auto tuple = multiarch_tuple(t); + if (crt.empty() && !tuple.empty() + && (fs::exists(root / "usr" / "lib" / tuple / "crt1.o", ec) + || fs::exists(root / "usr" / "lib" / tuple / "Scrt1.o", ec))) { + // Debian multiarch, and a driver that does not know it. + m.multiarch = true; + m.tuple = tuple; + crt = root / "usr" / "lib" / tuple / "crt1.o"; + extra.push_back(std::string(clangDriver ? "-isystem" : "-idirafter") + + (root / "usr" / "include" / tuple).string()); + } else if (!tuple.empty() && fs::is_directory(root / "usr" / "lib" / tuple, ec)) { + // The driver found it through multiarch itself (clang); the + // directories still hold the libraries a loader is pointed at. + m.tuple = tuple; + } + m.cAbiFound = !crt.empty(); + if (!crt.empty()) m.evidence.push_back(crt); + + // The implementation and its version, from the preprocessor. + fs::create_directories(probeDir, ec); + const auto probe = probeDir / "mcpp-base-probe.c"; + { + std::ofstream os(probe, std::ios::binary | std::ios::trunc); + os << "#include \n"; + } + auto pp = base; + for (auto const& e : extra) pp.push_back(e); + pp.insert(pp.end(), {"-E", "-dM", "-x", "c", probe.string()}); + auto r = mcpp::platform::process::capture_stdout(pp); + if (r.exit_code == 0) m.fact = classify_macros(parse_defines(r.output)); + if (!m.fact && m.cAbiFound) { + // musl defines nothing that names it; a musl loader or a `libc.a` + // with no glibc beside it is the answer (§4.3's last measurable row). + for (auto const& d : library_dirs(root, m.tuple)) + for (auto it = fs::directory_iterator(d, ec); !ec && it != fs::directory_iterator{}; it.increment(ec)) + if (it->path().filename().string().starts_with("ld-musl-")) { + m.fact = LibcFact{"musl", "", "musl"}; + break; + } + if (!m.fact && t.is_musl()) m.fact = LibcFact{"musl", "", "musl"}; + } + for (auto name : {"usr/include/features.h", "usr/include/gnu/libc-version.h", + "usr/include/stdio.h"}) + if (fs::exists(root / name, ec)) m.evidence.push_back(root / name); + + // The loader and the directories a binding artifact's RUNPATH names. + const bool glibc = m.fact && m.fact->impl == "glibc"; + for (auto const& d : library_dirs(root, m.tuple)) { + if (m.loader.empty()) + if (auto l = resolve_loader(d, t.str()); !l.empty()) m.loader = l; + const bool hasLibc = glibc ? fs::exists(d / "libc.so.6", ec) + : (fs::exists(d / "libc.so", ec) || fs::exists(d / "libc.a", ec)); + if (hasLibc && std::ranges::find(m.runtimeDirs, d) == m.runtimeDirs.end()) { + // A symlinked `lib64 -> usr/lib64` is one directory. + auto real = fs::canonical(d, ec); + bool dup = false; + for (auto const& seen : m.runtimeDirs) + if (!ec && fs::canonical(seen, ec) == real) dup = true; + if (!dup) m.runtimeDirs.push_back(d); + } + } + if (!m.loader.empty()) m.evidence.push_back(m.loader); + for (auto const& d : m.runtimeDirs) + m.evidence.push_back(d / (glibc ? "libc.so.6" : "libc.so")); + return m; +} + +// THE BASE'S CONTENT IDENTITY (§4.9): the statement, the root, and the path, +// size and modification time of each file the measurement rested on. A base +// rewritten in place gets a new identity, and so a new output directory, +// without hashing a gigabyte of libraries on every build. +inline std::string identity_of(std::string_view text, const fs::path& root, + const std::vector& evidence, + std::string_view measured) { + std::uint64_t h = 1469598103934665603ull; + detail::mix(h, text); + detail::mix(h, root.generic_string()); + detail::mix(h, measured); + for (auto const& f : evidence) detail::stamp(h, f); + return std::format("{:016x}", h); +} + +// THE C LIBRARY THE COMPILER'S OWN RUNTIME REQUIRES (§4.8): the highest +// `GLIBC_x.y` named by the version needs of the runtime libraries a produced +// artifact links. Asked of the driver without the base, because these are the +// compiler's own. Empty when none is found or none names glibc. +struct RuntimeFloor { + std::string version; // "2.38" + fs::path from; // the library that requires it +}; + +inline RuntimeFloor compiler_runtime_glibc_floor(const std::vector& driverBase) { + RuntimeFloor best; + for (auto name : {"libstdc++.so.6", "libgcc_s.so.1", "libc++.so.1", "libc++abi.so.1", + "libunwind.so.1", "libatomic.so.1"}) { + auto p = detail::print_file_name(driverBase, name); + if (p.empty()) continue; + std::error_code ec; + auto real = fs::canonical(p, ec); + if (ec) continue; + auto facts = mcpp::platform::elf::inspect_elf_runtime(real); + if (!facts) continue; + auto v = highest_glibc(facts->requiredGlibcVersions); + if (!v.empty() && (best.version.empty() || compare_versions(v, best.version) > 0)) + best = RuntimeFloor{v, p}; + } + return best; +} + +// ── Apple (§4.4): the platform's own conventions, on every host ──────────── + +// The SDK a statement names, or the reason there is none. `name` is the row's +// xcrun SDK name. `SDKROOT` is honoured on every host; `xcrun` and the +// developer directory only exist on an Apple one (inside `sdk_path`). +inline std::expected +locate_apple(const Statement& s, std::string_view name) { + namespace mac = mcpp::platform::macos; + AppleSdk sdk; + sdk.name = std::string(name); + std::error_code ec; + if (s.form == Form::Path) { + if (!fs::is_directory(s.path, ec)) + return std::unexpected(std::format("{} does not exist", s.path.string())); + if (!fs::exists(s.path / "SDKSettings.json", ec) && !fs::exists(s.path / "usr" / "include", ec)) + return std::unexpected(std::format( + "{} is not an Apple SDK: it has neither SDKSettings.json nor usr/include", + s.path.string())); + sdk.root = s.path; + } else { + auto found = mac::sdk_path(name); + if (!found) { + const char* env = std::getenv("SDKROOT"); + return std::unexpected(std::format( + "looked in: SDKROOT ({}){}", env && *env ? env : "unset", + mcpp::platform::is_macos ? ", xcrun, xcode-select and the standard locations" : "")); + } + sdk.root = *found; + } + sdk.version = mac::sdk_settings_version(sdk.root); + if (sdk.version.empty()) + if (auto v = mac::sdk_version(name)) sdk.version = *v; + if (s.form == Form::Locator && s.version != "system" && !s.version.empty() + && !(sdk.version == s.version || sdk.version.starts_with(s.version + "."))) + return std::unexpected(std::format( + "the {} SDK found at {} is version {}, and {}@{} was asked for", + name, sdk.root.string(), sdk.version.empty() ? "unknown" : sdk.version, + name, s.version)); + return sdk; +} + +} // namespace mcpp::toolchain::base diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index 0e1b148ed..bc77c6f69 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -12,6 +12,7 @@ import mcpp.toolchain.msvc; import mcpp.provider; import mcpp.provider.select; import mcpp.platform; +import mcpp.fallback.probe_sysroot; export namespace mcpp::toolchain { @@ -127,17 +128,28 @@ detect(const std::filesystem::path& explicit_compiler, mcpp::toolchain::clang::enrich_toolchain(tc, envPrefix); } - tc.sysroot = probe_sysroot(tc.binaryPath, envPrefix); + tc.base.root = probe_sysroot(tc.binaryPath, envPrefix); // Probe fine-grained payload paths from sibling xpkgs (glibc, linux-headers). // When available, flags are assembled from these paths instead of --sysroot. - tc.payloadPaths = probe_payload_paths(tc.binaryPath, tc.runtimeBinding); + tc.base.payload = probe_payload_paths(tc.binaryPath, tc.runtimeBinding); // For GCC: ensure the probed sysroot has complete headers by symlinking // missing content (linux kernel headers, glibc) from payload xpkgs. // This makes mcpp self-sufficient — not dependent on xlings subos init. - if (tc.payloadPaths && !tc.sysroot.empty()) - ensure_sysroot_complete(tc.sysroot, *tc.payloadPaths); + // + // ONLY A VIEW THIS ECOSYSTEM OWNS. A compiler named by path reports the + // sysroot it was built with, which is the vendor's tree, and mcpp does not + // write into a tree it does not own (SPEC-006 §2.2.1; design 2026-10-23 + // D10): its headers are completed on the command line instead. The view + // is owned when it lives in the same home as the payloads it is completed + // from. + if (tc.base.payload && !tc.base.root.empty()) { + auto store = mcpp::provider::store_of(tc.base.payload->glibcInclude); + if (store && mcpp::fallback::path_is_under( + tc.base.root, mcpp::provider::layout().home_of_store(*store))) + ensure_sysroot_complete(tc.base.root, *tc.base.payload); + } return tc; } diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 036532e8f..78472a477 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -300,6 +300,14 @@ struct WindowsSdk { std::optional find_windows_sdk( std::span extraRoots = {}); +// The SDK inside ONE root (`/10`), the highest usable version: what a +// `/winsysroot` layout's `Windows Kits/10` holds. No search beyond it. +std::optional sdk_in_root(const std::filesystem::path& root); + +// The newest toolset under `/VC/Tools/MSVC`, or nullopt: what a Visual +// Studio tree or a `/winsysroot` layout named by path holds. +std::optional toolset_in_root(const std::filesystem::path& root); + // The machine's Windows SDK roots, in the order step 3 above searches them // (D15): the root the SDK installer recorded (`Installed Roots\KitsRoot10`, // either registry view), then `Windows Kits\10` under %ProgramFiles(x86)% @@ -1258,6 +1266,31 @@ std::string declared_sdk_version() { } // namespace +std::optional sdk_in_root(const std::filesystem::path& root) { + return pick_sdk_in(root, {}); +} + +std::optional toolset_in_root(const std::filesystem::path& root) { + std::error_code ec; + const auto dir = root / "VC" / "Tools" / "MSVC"; + std::string best; + for (auto it = std::filesystem::directory_iterator(dir, ec); + !ec && it != std::filesystem::directory_iterator{}; it.increment(ec)) { + if (!it->is_directory(ec)) continue; + auto v = it->path().filename().string(); + if (!std::filesystem::exists(it->path() / "include", ec)) continue; + if (best.empty() || compare_toolset_versions(v, best) > 0) best = v; + } + if (best.empty()) return std::nullopt; + ToolsetChoice c; + c.vsRoot = root; + c.version = best; + c.toolsDir = dir / best; + c.product = "path:" + root.generic_string(); + c.via = "path"; + return c; +} + std::optional find_windows_sdk( std::span extraRoots) { // Highest version dir under `root/Include` that actually carries the UCRT @@ -1766,7 +1799,7 @@ std::expected enrich_toolchain_from_cl(Toolchain& tc) { if (choice.sdk) { tc.envOverrides = build_env_for_cl(tc.binaryPath, parsed->second, *choice.sdk); - tc.windowsSdkVersion = choice.sdk->version; + tc.base.msvc.sdkVersion = choice.sdk->version; } if (!choice.note.empty()) tc.resolutionNote = choice.note; @@ -1790,7 +1823,7 @@ std::expected enrich_toolchain_from_cl(Toolchain& tc) { // staging gate read THIS rather than `linkRuntimeDirs`, which on the // LLVM row holds LLVM's own directories instead — one name for "the // toolset's redistributable" that means the same thing on both rows. - tc.msvcRedistDir = redist; + tc.base.msvc.redistDir = redist; } return {}; } diff --git a/src/toolchain/post_install.cppm b/src/toolchain/post_install.cppm index 44d945242..c9885e01c 100644 --- a/src/toolchain/post_install.cppm +++ b/src/toolchain/post_install.cppm @@ -381,7 +381,23 @@ std::filesystem::path write_clean_link_specs( auto out = outputDir / "mcpp-clean-link.specs"; // Idempotent: the pristine spec is a property of the compiler binary, so // regenerating it every build would spawn a process for a constant. - if (std::filesystem::exists(out, ec)) return out; + // + // `%(sysroot_spec)` IS NOT RESTORED HERE, and that is measured. gcc puts + // it before `*link:` ahead of any `-specs=` file, so this file drops it + // and ld hears no `--sysroot` from the driver. For the ecosystem's own + // view that is what keeps the link working: ld prefixes a sysroot onto the + // DT_RUNPATH of every shared library it reads, and a SubOS farm library's + // RUNPATH names the payload store by absolute path (e2e 220: libEGL.so's + // libGLdispatch, not found). A STATED base hands ld its `--sysroot` + // explicitly instead (`ToolchainLinkModel::link_tokens`). + if (std::filesystem::exists(out, ec)) { + std::ifstream is(out); + std::string head, body; + std::getline(is, head); + std::getline(is, body); + if (!body.starts_with("%(sysroot_spec) ")) return out; + // Written by a pre-release build of 2026.10.23.1; regenerated once. + } // An argument vector: the command-string form carried `2>/dev/null`, which // cmd.exe cannot open, so on a Windows host this probe printed a spurious diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 78eec3e94..9ac7da141 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -124,9 +124,24 @@ std::expected run_capture_command( if (r.exit_code != 0) { // Include the command: its --sysroot/-isystem flags are the first // thing needed to diagnose header-resolution failures. + std::string note; + // GCC'S MODULE MAPPER CANNOT CARRY A NON-ASCII PATH. Its protocol + // (libcody) quotes a header's path into an `INCLUDE-TRANSLATE` + // request and cannot parse its own encoding of a byte outside ASCII, + // so every `#include` of the standard library fails with + // `malformed string` when the toolchain lives under such a path -- + // measured with gcc 16.1.0 under `/tmp/家/` (a path with a space + // builds). The cause is the path, and the message says so. + if (r.output.find("malformed string") != std::string::npos + && r.output.find("INCLUDE-TRANSLATE") != std::string::npos + && std::ranges::any_of(cmd, [](char c) { return static_cast(c) >= 0x80; })) + note = "\nnote: gcc's module mapper cannot carry a path outside ASCII, and this " + "toolchain's path has one (the mcpp home, or the tree it was named by).\n" + " Put the home on an ASCII path (`mcpp self init --home `), or build " + "with llvm, whose modules do not go through that protocol."; return std::unexpected(StdModError{ - std::format("std module precompile failed (rc={}):\n{}\ncommand: {}", - r.exit_code, r.output, cmd)}); + std::format("std module precompile failed (rc={}):\n{}\ncommand: {}{}", + r.exit_code, r.output, cmd, note)}); } return r.output; } diff --git a/tests/e2e/134_target_sysroot_and_queries.sh b/tests/e2e/134_target_sysroot_and_queries.sh index 812bd3827..0626764fb 100755 --- a/tests/e2e/134_target_sysroot_and_queries.sh +++ b/tests/e2e/134_target_sysroot_and_queries.sh @@ -168,7 +168,7 @@ manifest ' sysroot = "newlib"' if "$MCPP" build > badref.log 2>&1; then cat badref.log; echo "a bare sysroot name should have been rejected"; exit 1; fi -grep -q 'xpkg reference' badref.log || { +grep -q 'is not a sysroot. A sysroot is a package' badref.log || { cat badref.log; echo "rejection did not explain the expected form"; exit 1; } echo "PASS: [target.X].sysroot override, zero-libc tier, and the three target queries" diff --git a/tests/e2e/330_runner_hosted_targets.sh b/tests/e2e/330_runner_hosted_targets.sh index f35c0619a..f4eae9a69 100644 --- a/tests/e2e/330_runner_hosted_targets.sh +++ b/tests/e2e/330_runner_hosted_targets.sh @@ -169,7 +169,7 @@ fi printf '\n[target.%s]\nrunnerX = ["x"]\n' "$HOST" >> mcpp.toml out=$("$MCPP" build 2>&1) grep -q "unsupported key 'runnerX'" <<<"$out" || fail "array typo not reported: $out" -grep -q "Supported keys: cxx_runtime, linkage, min_api_level, runner, sysroot, toolchain" <<<"$out" \ +grep -q "Supported keys: cxx_runtime, linkage, min_api_level, run_under, runner, sysroot, toolchain" <<<"$out" \ || fail "runner missing from the supported-keys list: $out" echo "PASS: 330_runner_hosted_targets" diff --git a/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh b/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh new file mode 100755 index 000000000..7c1d4fedd --- /dev/null +++ b/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# requires: gcc elf unix-shell python3 +# A stated sysroot is the C library the artifact is built against AND runs on +# (design 2026-10-23, "target base and execution host"; S1, S2, S4, S6, S7). +# +# WHY IT IS ASSERTED ON THE ARTIFACT AND NOT ON THE BUILD'S SUCCESS +# +# Before this release a stated sysroot failed in two ways that read as one. +# The link could not find `/lib64/libm.so.6` (gcc's clean link specs dropped +# `--sysroot` for ld), and once that was worked around the artifact loaded the +# HOST's loader and the SubOS farm's libc while being compiled against the +# sysroot's -- a binary that links and dies at `symbol lookup error`. So every +# case below reads PT_INTERP and RUNPATH, not only the exit status. +set -e + +MCPP="${MCPP:-mcpp}" +source "$(dirname "$0")/_toolchain_env.sh" +source "$(dirname "$0")/_sysroot_fixture.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +mkdir -p "$work/app/src" +cd "$work/app" +cat > src/main.cpp <<'CPP' +#include +import std; +int main() { std::println("built {}.{} ran {}", __GLIBC__, __GLIBC_MINOR__, gnu_get_libc_version()); } +CPP +manifest() { # $1: the [target.x86_64-linux-gnu] body, $2: anything else + cat > mcpp.toml <&1)" || fail "the default build failed" +default_dir=$(ls -d target/x86_64-linux-gnu/*/ | head -1) +make_glibc_sysroot "$work/app/third_party/sysroot" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 0; } +SR="$work/app/third_party/sysroot" + +artifact() { ls target/x86_64-linux-gnu/*/bin/app | xargs ls -t | head -1; } +interp() { readelf -l "$1" | sed -n 's/.*Requesting program interpreter: \(.*\)]/\1/p'; } +runpath() { readelf -d "$1" | sed -n 's/.*R\(UN\)\{0,1\}PATH).*\[\(.*\)\]/\2/p'; } + +# ── S2: a managed compiler, the sysroot named by a path relative to the file ── +manifest 'sysroot = { path = "third_party/sysroot" }' "" +out="$("$MCPP" run 2>&1)" || fail "S2: the build or the run failed" +grep -q "Using sysroot glibc $SYSROOT_GLIBC ← third_party/sysroot \[custom · mcpp.toml:" <<<"$out" \ + || fail "S2: no Using line naming the sysroot, its C library and where it was stated" +grep -q "built $SYSROOT_GLIBC ran $SYSROOT_GLIBC" <<<"$out" || fail "S2: the program did not run on the sysroot's glibc" +a=$(artifact) +[[ "$(interp "$a")" == "$SR/lib64/ld-linux-x86-64.so.2" ]] || fail "S2: PT_INTERP is $(interp "$a")" +rp=$(runpath "$a") +[[ "$rp" == "$SR/lib64:"* ]] || fail "S2: RUNPATH does not start with the sysroot: $rp" +grep -q "subos" <<<"$rp" && fail "S2: the SubOS farm reached a stated base's RUNPATH: $rp" +[[ "$(dirname "$(dirname "$a")")/" != "$default_dir" ]] || fail "S2: the stated base reused the default output directory" +python3 - "$(dirname "$(dirname "$a")")/resolution.json" <<'PY' || fail "S2: resolution.json does not describe the base" +import json, sys +b = json.load(open(sys.argv[1]))["base"] +assert b["form"] == "path" and b["runtime"] == "binding" and b["c_abi"]["impl"] == "glibc", b +assert b["identity"], b +PY +out="$("$MCPP" why sysroot 2>&1)" || fail "S2: mcpp why sysroot failed" +grep -q "address=gnu-sysroot runtime=binding" <<<"$out" || fail "S2: why sysroot omits the address and runtime" + +# ── S4: the base rewritten in place is a different base ─────────────────────── +first=$(dirname "$(dirname "$(artifact)")") +touch -d '2001-01-01' "$SR/usr/include/features.h" +out="$("$MCPP" build 2>&1)" || fail "S4: the rebuild failed" +second=$(ls -td target/x86_64-linux-gnu/*/ | head -1) +[[ "${second%/}" != "$first" ]] || fail "S4: a sysroot rewritten in place reused the output directory" + +# ── S7: overrides outrank the project, the machine's default fills a gap ────── +make_glibc_sysroot "$work/other" >/dev/null +out="$(MCPP_SYSROOT="path:$work/other" "$MCPP" build 2>&1)" || fail "S7: MCPP_SYSROOT failed" +grep -q "Using sysroot glibc .* ← $work/other \[custom · env MCPP_SYSROOT\]" <<<"$out" \ + || fail "S7: MCPP_SYSROOT did not outrank the manifest" +out="$(MCPP_TARGET_X86_64_LINUX_GNU_SYSROOT="path:$work/other" "$MCPP" build 2>&1)" || fail "S7: the per-target variable failed" +grep -q "env MCPP_TARGET_X86_64_LINUX_GNU_SYSROOT" <<<"$out" || fail "S7: the per-target variable was not used" +out="$("$MCPP" --managed-only build 2>&1 || true)" +grep -q "sysroot ← third_party/sysroot" <<<"$out" || fail "S7: --managed-only did not name the sysroot" +cfgdir="$work/home-cfg"; mkdir -p "$cfgdir" +manifest "" "" +home="${MCPP_HOME:-$HOME/.mcpp}" +cp "$home/config.toml" "$work/config.toml.bak" 2>/dev/null || true +restore() { if [[ -f "$work/config.toml.bak" ]]; then cp "$work/config.toml.bak" "$home/config.toml"; fi; } +trap 'restore; rm -rf "$work"' EXIT +printf '\n[target.x86_64-linux-gnu]\nsysroot = { path = "%s" }\n' "$work/other" >> "$home/config.toml" +out="$("$MCPP" build 2>&1)"; rc=$? +restore +[[ $rc -eq 0 ]] || fail "S7: the machine default failed" +grep -q "Using sysroot glibc .* ← $work/other \[custom · .*config.toml" <<<"$out" \ + || fail "S7: config.toml's [target.].sysroot was not the default" + +# ── S6: what is not a sysroot, and what is not there ───────────────────────── +manifest 'sysroot = { path = "does/not/exist" }' "" +out="$("$MCPP" build 2>&1 || true)" +grep -q "which does not exist" <<<"$out" || fail "S6: a missing path was not named" +grep -q "publish it as a package in your index" <<<"$out" || fail "S6: the message omits how to make it exist" +grep -q "MCPP_TARGET_X86_64_LINUX_GNU_SYSROOT=path:" <<<"$out" || fail "S6: the message omits the local override" +mkdir -p "$work/app/libs-only/usr/include" +manifest 'sysroot = { path = "libs-only" }' "" +out="$("$MCPP" build 2>&1 || true)" +grep -q "library prefix rather than a sysroot" <<<"$out" || fail "S6: a directory with no C library was accepted" + +# ── S3: the compiler's runtime requires more than the base supplies ─────────── +make_glibc_sysroot "$work/old" >/dev/null +sed -i 's/^\(#define[[:space:]]*__GLIBC_MINOR__[[:space:]]*\)[0-9]*/\120/' "$work/old/usr/include/features.h" +manifest "sysroot = { path = \"$work/old\" }" "" +out="$("$MCPP" build 2>&1 || true)" +grep -q "runtime requires glibc >= " <<<"$out" && grep -q "this sysroot provides glibc 2.20" <<<"$out" \ + || fail "S3: a base older than the compiler's runtime was not refused naming both versions" +grep -q "error:.*std.cc" <<<"$out" && fail "S3: the refusal arrived as a std module error" + +# ── S1: a toolchain named by path, with the sysroot it carries ──────────────── +manifest "" "[toolchain.linux] +path = \"$MCPP_E2E_GCC_ROOT\" +sysroot = \"$SR\"" +sed -i '/^default = /d' mcpp.toml +out="$("$MCPP" run 2>&1)" || fail "S1: [toolchain] { path, sysroot } failed" +grep -q "built $SYSROOT_GLIBC ran $SYSROOT_GLIBC" <<<"$out" || fail "S1: the program did not run on the carried sysroot" +[[ "$(interp "$(artifact)")" == "$SR/lib64/ld-linux-x86-64.so.2" ]] || fail "S1: PT_INTERP is not the carried sysroot's" +# `--no-strip`: a tree named by path need not carry binutils, and pack +# refuses to ship debug information it was not told to keep. +out="$("$MCPP" pack --no-strip 2>&1)" || fail "S1: pack failed" +grep -q "Requires glibc >= " <<<"$out" || fail "S1: pack did not state the C library floor" + +echo "PASS: a stated sysroot is the C library the artifact is built against and runs on" diff --git a/tests/e2e/926_the_hosts_own_root_is_a_base.sh b/tests/e2e/926_the_hosts_own_root_is_a_base.sh new file mode 100755 index 000000000..bf130f0d9 --- /dev/null +++ b/tests/e2e/926_the_hosts_own_root_is_a_base.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# requires: gcc llvm elf unix-shell +# `glibc@system`: the host's own root as the target base (design 2026-10-23 +# D11, S10) -- for a build that packages for the distribution it runs on, or +# links the host's libraries. +# +# THE TWO DRIVERS DIFFER, AND THAT IS THE POINT OF RUNNING BOTH. A gcc built +# without Debian multiarch support finds no `crt1.o` in `/usr/lib/` +# (`--sysroot=/` fails with `cannot find crti.o`, measured as scenario J); +# clang knows the layout itself (scenario K). The engine measures which driver +# needs the multiarch directories and supplies them to that one only. +set -e + +MCPP="${MCPP:-mcpp}" +source "$(dirname "$0")/_toolchain_env.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +tuple="$(uname -m)-linux-gnu" +if [[ ! -e "/usr/lib/$tuple/crt1.o" ]]; then + echo "SKIP: this host is not a Debian multiarch layout (/usr/lib/$tuple/crt1.o)"; exit 0 +fi +host_glibc=$(getconf GNU_LIBC_VERSION | awk '{print $2}') + +mkdir -p "$work/app/src" +cd "$work/app" +cat > src/main.cpp <<'CPP' +#include +import std; +int main() { std::println("built {}.{} ran {}", __GLIBC__, __GLIBC_MINOR__, gnu_get_libc_version()); } +CPP +for tc in "gcc@${GCC_VERSION}" "llvm@${LLVM_VERSION}"; do + cat > mcpp.toml <&1)" || fail "$tc: glibc@system failed" + grep -q "Using sysroot glibc .* ← glibc@system \[host · mcpp.toml:" <<<"$out" \ + || fail "$tc: no Using line for the host's base" + grep -q "ran $host_glibc" <<<"$out" || fail "$tc: the program did not run on the host's glibc" + a=$(ls target/*/*/bin/app | head -1) + readelf -d "$a" | grep -q "subos" && fail "$tc: the SubOS farm reached a host-based artifact" + readelf -l "$a" | grep -q "Requesting program interpreter: /lib" || fail "$tc: PT_INTERP is not the host's loader" + why="$("$MCPP" why sysroot 2>&1)" + if [[ "$tc" == gcc@* ]]; then + grep -q "multiarch=$tuple" <<<"$why" || fail "gcc: the multiarch directories were not measured" + else + grep -q "multiarch=" <<<"$why" && fail "llvm: multiarch was supplied to a driver that knows it" + fi + echo " ok: $tc builds and runs on the host's own base" +done +echo "PASS: glibc@system is the host's base, multiarch supplied only where measured" diff --git a/tests/e2e/927_the_runner_contract.sh b/tests/e2e/927_the_runner_contract.sh new file mode 100755 index 000000000..da17464bc --- /dev/null +++ b/tests/e2e/927_the_runner_contract.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell +# The runner contract and `run_under` (design 2026-10-23 §3.7, §4.7, S11). +# +# An artifact runs as ` `. The engine +# knows no runner by name; what it owes every one of them is the same argv, +# the same environment (`MCPP_RUN_*`), a closure manifest a runner that moves +# the artifact can read, and the program's own exit status back. A fake runner +# records all of it. +set -e + +MCPP="${MCPP:-mcpp}" +source "$(dirname "$0")/_toolchain_env.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +mkdir -p "$work/app/src" "$work/bin" +cd "$work/app" +cat > "$work/bin/via-host" <<'SH' +#!/bin/sh +{ + echo "argv: $*" + echo "triple=$MCPP_RUN_TRIPLE kind=$MCPP_RUN_KIND mode=$MCPP_RUN_MODE" + echo "id=$MCPP_RUN_ID" + echo "closure=$MCPP_RUN_CLOSURE" +} >> "$RECORD" +shift # the host name +exec "$@" +SH +chmod +x "$work/bin/via-host" +export PATH="$work/bin:$PATH" RECORD="$work/record" +cat > src/main.cpp <<'CPP' +int main(int argc, char**) { return argc == 2 ? 7 : 3; } +CPP +triple="$(uname -m)-linux-gnu" +manifest() { + cat > mcpp.toml <&1)"; rc=$?; set -e +[[ $rc -eq 7 ]] || fail "the program's exit status did not come back through run_under (got $rc)" +grep -q "Via via-host team-mac \[.*mcpp.toml:7\]" <<<"$out" || fail "no Via line naming where run_under was stated" +grep -q "argv: team-mac .*/bin/app one" "$RECORD" || fail "run_under was not composed in front of the artifact" +grep -q "triple=$triple kind=exe mode=run" "$RECORD" || fail "the contract variables are missing" +closure=$(sed -n 's/^closure=//p' "$RECORD") +[[ -f "$closure" ]] || fail "MCPP_RUN_CLOSURE names no file" +grep -q '"schema": 1' "$closure" || fail "the closure manifest carries no schema" +grep -q '"artifact": ".*/bin/app"' "$closure" || fail "the closure manifest does not name the artifact" + +# Cleared on this machine, and dropped by --no-runner. +rm -f "$RECORD" +var="MCPP_TARGET_$(echo "$triple" | tr 'a-z-' 'A-Z_')_RUN_UNDER" +set +e; env "$var=none" "$MCPP" run -- one >/dev/null 2>&1; rc=$?; set -e +[[ $rc -eq 7 && ! -f "$RECORD" ]] || fail "\`..._RUN_UNDER=none\` did not clear run_under" +set +e; "$MCPP" run --no-runner -- one >/dev/null 2>&1; rc=$?; set -e +[[ $rc -eq 7 && ! -f "$RECORD" ]] || fail "--no-runner did not drop run_under" + +# Stated as this machine's default. +manifest '' +home="${MCPP_HOME:-$HOME/.mcpp}" +cp "$home/config.toml" "$work/config.bak" +trap 'cp "$work/config.bak" "$home/config.toml"; rm -rf "$work"' EXIT +printf '\n[target.%s]\nrun_under = ["via-host", "my-vm"]\n' "$triple" >> "$home/config.toml" +rm -f "$RECORD" +set +e; out="$("$MCPP" run -- one 2>&1)"; rc=$?; set -e +cp "$work/config.bak" "$home/config.toml" +[[ $rc -eq 7 ]] || fail "config.toml's run_under failed (got $rc)" +grep -q "argv: my-vm " "$RECORD" || fail "config.toml's [target.].run_under was not the default" + +# A program nobody installed is named before anything runs. +manifest 'run_under = ["no-such-wrapper", "x"]' +set +e; out="$("$MCPP" run 2>&1)"; rc=$?; set -e +[[ $rc -eq 2 ]] || fail "a missing run_under program did not fail with 2 (got $rc)" +grep -q "no-such-wrapper" <<<"$out" || fail "the missing program was not named" + +# Tests run through it too. +manifest 'run_under = ["via-host", "team-mac"]' +mkdir -p tests +cat > tests/t.cpp <<'CPP' +int main() { return 0; } +CPP +rm -f "$RECORD" +out="$("$MCPP" test 2>&1)" || fail "mcpp test through run_under failed" +grep -q "mode=test" "$RECORD" || fail "a test was not run through run_under with MCPP_RUN_MODE=test" + +echo "PASS: run_under composes in front of the runner and the contract reaches it" diff --git a/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh b/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh new file mode 100755 index 000000000..5c2656b6e --- /dev/null +++ b/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# requires: unix-shell +# Apple's own convention for naming an SDK, `SDKROOT`, is read on every host +# (design 2026-10-23 §4.4, D18, S12). An SDK a developer produced from their +# own Xcode on Linux is located the way one in Xcode is, and its slices decide +# which rows it can serve (D16). Nothing is downloaded: mcpp supports the SDK +# and does not distribute it. +# +# Linux only: on macOS `xcrun` answers first, which is the other arm, covered +# by the iOS workflow. +set -e + +MCPP="${MCPP:-mcpp}" +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: the non-Apple arm"; exit 0; } +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 MCPP_NO_AUTO_INSTALL=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +sdk="$work/iPhoneSimulator18.5.sdk" +mkdir -p "$sdk/usr/include" "$sdk/usr/lib" +printf '{"CanonicalName":"iphonesimulator18.5","Version":"18.5"}\n' > "$sdk/SDKSettings.json" +printf -- '--- !tapi-tbd\ntbd-version: 4\ntargets: [ arm64-ios-simulator ]\ninstall-name: /usr/lib/libSystem.B.dylib\n' \ + > "$sdk/usr/lib/libSystem.tbd" +mkdir -p "$work/app/src" && cd "$work/app" +printf 'int main() { return 0; }\n' > src/main.cpp +printf '[package]\nname = "app"\nversion = "0.1.0"\n' > mcpp.toml + +# Without it, the refusal says where it looked and how to provide one. +out="$(env -u SDKROOT "$MCPP" build --target aarch64-ios-sim 2>&1 || true)" +grep -q "needs the iphonesimulator SDK, which this machine does not provide" <<<"$out" || fail "no refusal without an SDK" +grep -q "SDKROOT (unset)" <<<"$out" || fail "the refusal does not say SDKROOT was looked at" +grep -q "xtool" <<<"$out" || fail "the refusal does not say how to produce one off macOS" + +# With it, the SDK gate opens: whatever stops the build next, it is not that. +out="$(SDKROOT="$sdk" "$MCPP" build --target aarch64-ios-sim 2>&1 || true)" +grep -q "which this machine does not provide" <<<"$out" && fail "SDKROOT was not honoured off macOS" + +# The slice decides the row: this SDK has no x86_64 simulator slice. +out="$(SDKROOT="$sdk" "$MCPP" build --target x86_64-ios-sim 2>&1 || true)" +grep -q "carries no x86_64-ios-simulator slice" <<<"$out" || fail "a missing slice was not named" + +# A version is matched, and a mismatch says what was found. +printf '[package]\nname = "app"\nversion = "0.1.0"\n[target.aarch64-ios-sim]\nsysroot = "iphonesimulator@17.0"\n' > mcpp.toml +out="$(SDKROOT="$sdk" "$MCPP" build --target aarch64-ios-sim 2>&1 || true)" +grep -q "is version 18.5, and iphonesimulator@17.0 was asked for" <<<"$out" || fail "a version mismatch was not named" + +# And the family names the platform: an iphoneos SDK is not a simulator's. +printf '[package]\nname = "app"\nversion = "0.1.0"\n[target.aarch64-ios-sim]\nsysroot = "iphoneos@system"\n' > mcpp.toml +out="$("$MCPP" build --target aarch64-ios-sim 2>&1 || true)" +grep -q "describes another platform" <<<"$out" || fail "a locator of another platform was accepted" + +echo "PASS: an Apple SDK is located by SDKROOT on any host, and its slices decide the row" diff --git a/tests/e2e/929_release_trims_paths_and_defines_ndebug.sh b/tests/e2e/929_release_trims_paths_and_defines_ndebug.sh new file mode 100755 index 000000000..7e4547b44 --- /dev/null +++ b/tests/e2e/929_release_trims_paths_and_defines_ndebug.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# requires: gcc elf unix-shell +# `trim_paths` and `ndebug` (design 2026-10-23 §15.3-§15.4; mcpp#832; R2, R2b). +# +# A release artifact named the machine that built it: `__FILE__` in an +# `assert` reached mcpp's own released binary as +# `/home/runner/work/mcpp/mcpp/modules/libs/src/json/json.hpp`. The shipping +# profiles now map every absolute prefix to what it is -- the root package's +# sources to `@` -- and define NDEBUG, as CMake's Release does. +set -e + +MCPP="${MCPP:-mcpp}" +source "$(dirname "$0")/_toolchain_env.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +mk() { + mkdir -p "$1/src" + cat > "$1/mcpp.toml" < "$1/src/main.cpp" <<'CPP' +#include +import std; +int main() { + std::cout << "file " << __FILE__ << ' ' << std::source_location::current().file_name() + << std::endl; // flushed: the assert below aborts the process + assert(false && "assert fired"); +} +CPP +} + +mk "$work/a" "" +cd "$work/a" +out="$("$MCPP" run --release 2>&1)" || fail "R2b: release ran the assert" +grep -q "file app@1.2.0/src/main.cpp app@1.2.0/src/main.cpp" <<<"$out" || fail "R2: release did not trim __FILE__ and source_location" +a=$(ls -t target/*/*/bin/app | head -1) +strings -a "$a" | grep -qF "$work" && fail "R2: the release artifact still carries the project's path" + +# The same sources, another directory: the same embedded paths. +mk "$work/b" "" +(cd "$work/b" && "$MCPP" build --release >/dev/null 2>&1) || fail "R2: the second checkout failed" +b=$(ls -t "$work"/b/target/*/*/bin/app | head -1) +diff <(strings -a "$a" | grep "app@1.2.0") <(strings -a "$b" | grep "app@1.2.0") \ + || fail "R2: two checkouts embed different paths" + +# dev keeps both. +set +e; out="$("$MCPP" run 2>&1)"; rc=$?; set -e +[[ $rc -ne 0 ]] || fail "R2b: dev did not run the assert" +grep -qF "file $work/a/src/main.cpp" <<<"$out" || fail "R2: dev trimmed a path" + +# Both are profile keys, and `false` restores the previous artifact. +mk "$work/c" '[profile.release] +ndebug = false +trim_paths = false' +cd "$work/c" +set +e; out="$("$MCPP" run --release 2>&1)"; rc=$?; set -e +[[ $rc -ne 0 ]] || fail "R2b: ndebug = false did not keep the assert" +grep -qF "file $work/c/src/main.cpp" <<<"$out" || fail "R2: trim_paths = false still trimmed" + +# A debug build with trimmed paths carries the way back for a debugger. +mk "$work/d" '[profile.release] +debug = true' +cd "$work/d" +"$MCPP" build --release >/dev/null 2>&1 || fail "release with debug failed" +g=$(ls target/*/*/source-map.gdbinit | head -1) +grep -q "set substitute-path app@1.2.0 $work/d" "$g" || fail "source-map.gdbinit does not map the root package back" + +echo "PASS: release trims the paths an artifact carries and defines NDEBUG; dev keeps both" diff --git a/tests/e2e/930_a_toolchain_phase_states_the_base.sh b/tests/e2e/930_a_toolchain_phase_states_the_base.sh new file mode 100755 index 000000000..8ecbb806d --- /dev/null +++ b/tests/e2e/930_a_toolchain_phase_states_the_base.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# requires: llvm elf unix-shell +# A toolchain phase states the target base beside a managed compiler (design +# 2026-10-23 §3.5, S9): `spec` and `sysroot` together, the program class -- +# the seam through which a plugin that LOCATES a base hands it to the engine. +# And `spec` with a key that only describes a toolchain named by path is +# refused by name; it used to be dropped without a word. +set -e + +MCPP="${MCPP:-mcpp}" +source "$(dirname "$0")/_llvm_env.sh" +source "$(dirname "$0")/_sysroot_fixture.sh" +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0; } +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64 Linux"; exit 0; } +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +make_glibc_sysroot "$work/sr" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 0; } +mkdir -p "$work/app/src" +cd "$work/app" +cat > mcpp.toml <\nimport std;\nint main() { std::println("ran {}", gnu_get_libc_version()); }\n' > src/main.cpp +phase() { + cat > build.mcpp <&1)" || fail "spec + sysroot from the phase failed" +grep -q "Using sysroot glibc .* ← $work/sr \[program · build.mcpp:4\]" <<<"$out" \ + || fail "the base stated by the phase is not reported as the program's" +a=$(ls target/*/*/bin/app | head -1) +readelf -l "$a" | grep -q "interpreter: $work/sr/" || fail "the artifact does not load the phase's base" + +phase "mcpp::toolchain(\"spec\", \"llvm@$LLVM_VERSION\"); mcpp::toolchain(\"prefix\", \"x-\");" +rm -rf target +out="$("$MCPP" build 2>&1 || true)" +grep -q "stated \`spec\` with prefix" <<<"$out" || fail "spec + prefix was not refused by name" + +echo "PASS: a toolchain phase states the base; spec with a path-only key is refused" diff --git a/tests/e2e/931_a_suggested_command_quotes_its_path.sh b/tests/e2e/931_a_suggested_command_quotes_its_path.sh new file mode 100755 index 000000000..c720bcd6d --- /dev/null +++ b/tests/e2e/931_a_suggested_command_quotes_its_path.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# requires: unix-shell fresh-sandbox +# A command printed for a person to paste quotes the paths in it (mcpp#832, +# R1). A home under `/tmp/mc probe` printed +# `mcpp self init --force --home /tmp/mc probe/home`, which selects `/tmp/mc` +# and hands `probe/home` to the parser. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +export HOME="$work/user" +mkdir -p "$HOME" +unset MCPP_HOME +first="$work/first home" +second="$work/second home" +"$MCPP" self init --home "$first" >/dev/null 2>&1 || fail "the first home could not be initialised" +out="$("$MCPP" self init --home "$second" 2>&1 || true)" +# The path as mcpp spells it, which on macOS is the resolved `/private/var/…` +# form of `$TMPDIR`: what matters is that it arrives as one quoted word. +grep -q -- "--home '[^']*/second home'" <<<"$out" || fail "the suggested command does not quote the spaced path" +cmd=$(sed -n "s/.*use \`\(mcpp self init --force --home .*\)\` to select.*/\1/p" <<<"$out") +[[ -n "$cmd" ]] || fail "no suggested command found" +eval "\"$MCPP\" ${cmd#mcpp }" >/dev/null 2>&1 || fail "the suggested command, pasted, failed" +echo "PASS: a suggested command quotes the path it carries, and pastes" diff --git a/tests/e2e/932_a_fresh_home_with_a_space_and_non_ascii_builds.sh b/tests/e2e/932_a_fresh_home_with_a_space_and_non_ascii_builds.sh new file mode 100755 index 000000000..884fa0478 --- /dev/null +++ b/tests/e2e/932_a_fresh_home_with_a_space_and_non_ascii_builds.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell fresh-sandbox +# A fresh home whose path holds a space builds the default project; one whose +# path also holds a non-ASCII character either builds or is refused naming +# gcc's module mapper (mcpp#832, R3). +# +# The failure #832 reported was the gcc payload's install hook: the index's +# recipe interpolated the home's path into a shell command unquoted, so the +# loader path it passed was split at the space. The recipe now quotes every +# interpolated path (xim-pkgindex `libs/shell.lua`, its lint), and this test +# holds the whole chain -- home, provider, payload hooks, link -- to a path +# that is not a single plain word. +set -e + +MCPP="${MCPP:-mcpp}" +source "$(dirname "$0")/_toolchain_env.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export NO_COLOR=1 +fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } + +# 1. A space: the chain must hold end to end. +home="$work/mc probe/home" +mkdir -p "$home" +export MCPP_HOME="$home" +out="$("$MCPP" self init 2>&1)" || fail "self init in a spaced home failed" +(cd "$work" && "$MCPP" new app >/dev/null 2>&1) || fail "mcpp new failed" +out="$(cd "$work/app" && "$MCPP" run 2>&1)" || fail "the default build in a spaced home failed" +grep -q "Hello" <<<"$out" || fail "the program did not run" +echo " ok: a fresh home at '$home' builds and runs the default project" + +# 2. A non-ASCII character as well. GCC's module mapper (libcody) cannot +# parse its own encoding of a byte outside ASCII, so `import std` with gcc +# cannot be built from a toolchain under such a path -- an upstream limit, +# measured with gcc 16.1.0. What mcpp owes is a build that works, or a +# refusal that names the cause. +home2="$work/mc probe/家" +mkdir -p "$home2" +export MCPP_HOME="$home2" +out="$("$MCPP" self init 2>&1)" || fail "self init in a non-ASCII home failed" +rm -rf "$work/app/target" +if out="$(cd "$work/app" && "$MCPP" run 2>&1)"; then + grep -q "Hello" <<<"$out" || fail "the program did not run" + echo " ok: the non-ASCII home builds too" +else + grep -q "gcc's module mapper cannot carry a path outside ASCII" <<<"$out" \ + || fail "the non-ASCII home failed without naming the cause" + echo " ok: the non-ASCII home is refused naming gcc's module mapper" +fi +echo "PASS: a fresh spaced home builds; a non-ASCII one builds or names the cause" diff --git a/tests/e2e/_sysroot_fixture.sh b/tests/e2e/_sysroot_fixture.sh new file mode 100755 index 000000000..04ef4baf0 --- /dev/null +++ b/tests/e2e/_sysroot_fixture.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# _sysroot_fixture.sh — a glibc sysroot built from what this home already has. +# +# A test of a stated target base needs a directory that IS one: a C library in +# a sysroot layout, foreign to mcpp. Downloading a distribution's would make +# the suite depend on a mirror, so the fixture assembles one from the +# ecosystem's own glibc and linux-headers payloads, in crosstool-ng's layout +# (`usr/include`, `usr/lib64`, `lib64 -> usr/lib64`), with the linker scripts' +# absolute paths made sysroot-relative -- the shape a vendor sysroot has. It +# copies only the top-level libraries and objects, not the locale data. +# +# Usage: source "$(dirname "$0")/_sysroot_fixture.sh" +# make_glibc_sysroot # 0 on success, 1 when no payload is installed +# SYSROOT_GLIBC # the payload's version ("2.44"), set on success + +_sysroot_store="${MCPP_HOME:-${HOME}/.mcpp}/registry/data/xpkgs" + +make_glibc_sysroot() { + local dest="$1" g h + g=$(ls -d "$_sysroot_store"/xim-x-glibc/*/ 2>/dev/null | sort -V | tail -1) + h=$(ls -d "$_sysroot_store"/xim-x-linux-headers/*/ 2>/dev/null | sort -V | tail -1) + [[ -n "$g" && -n "$h" && -d "$g/include" ]] || return 1 + g=${g%/}; h=${h%/} + rm -rf "$dest" + mkdir -p "$dest/usr/include" "$dest/usr/lib64" "$dest/lib" "$dest/usr/lib" + ln -sfn usr/lib64 "$dest/lib64" + cp -a "$g/include/." "$h/include/." "$dest/usr/include/" + find "$g/lib/" -maxdepth 1 \( -type f -o -type l \) -exec cp -a {} "$dest/usr/lib64/" \; + chmod -R u+w "$dest" + local f + for f in $(grep -lI "GROUP\|INPUT" "$dest"/usr/lib64/*.so 2>/dev/null); do + sed -i "s#$g/lib64/#/lib64/#g; s#$g/lib/#/lib64/#g" "$f" + done + SYSROOT_GLIBC=$(sed -n 's/^#define[[:space:]]*__GLIBC_MINOR__[[:space:]]*\([0-9]*\).*/2.\1/p' \ + "$dest/usr/include/features.h" | head -1) + return 0 +} diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index 1bb3ae339..5d7b33f8c 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -582,7 +582,7 @@ TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { tc.version = "16.1.0"; tc.driverIdent = "gcc-16.1.0"; tc.targetTriple = "x86_64-linux-gnu"; - tc.sysroot = home / "subos" / "default"; + tc.base.root = home / "subos" / "default"; mcpp::manifest::Manifest m; m.package.standard = "c++23"; @@ -597,7 +597,7 @@ TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { // Two subos on ONE home are still two keys — the relativization must not // flatten the distinction it exists to preserve. auto tc2 = tc; - tc2.sysroot = home / "subos" / "musl"; + tc2.base.root = home / "subos" / "musl"; const auto b = ck::build_axes(tc2, m, "-std=c++23", {}, "", store); EXPECT_NE(a.targetHeaderSet, b.targetHeaderSet); EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(b, pkg())); @@ -701,9 +701,9 @@ TEST(CacheKey, TheMsvcToolsetOfTheClangRowChangesTheKey) { tc.version = "20.1.7"; tc.driverIdent = "clang-20.1.7"; tc.targetTriple = "x86_64-pc-windows-msvc"; - tc.msvcToolsDir = "C:/VS/VC/Tools/MSVC/14.44.35207"; - tc.windowsSdkRoot = "C:/Kits/10"; - tc.windowsSdkVersion = "10.0.26100.0"; + tc.base.msvc.toolsDir = "C:/VS/VC/Tools/MSVC/14.44.35207"; + tc.base.msvc.sdkRoot = "C:/Kits/10"; + tc.base.msvc.sdkVersion = "10.0.26100.0"; mcpp::manifest::Manifest m; m.package.standard = "c++23"; @@ -714,12 +714,12 @@ TEST(CacheKey, TheMsvcToolsetOfTheClangRowChangesTheKey) { EXPECT_TRUE(present) << "the toolset directory is part of the header set"; auto tc2 = tc; - tc2.msvcToolsDir = "C:/VS/VC/Tools/MSVC/14.51.36014"; + tc2.base.msvc.toolsDir = "C:/VS/VC/Tools/MSVC/14.51.36014"; EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(ck::build_axes(tc2, m, "-std=c++23", {}, ""), pkg())); auto tc3 = tc; - tc3.windowsSdkVersion = "10.0.22621.0"; + tc3.base.msvc.sdkVersion = "10.0.22621.0"; EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(ck::build_axes(tc3, m, "-std=c++23", {}, ""), pkg())); } diff --git a/tests/unit/test_hostflags.cpp b/tests/unit/test_hostflags.cpp index 2d965d61a..acc504fd9 100644 --- a/tests/unit/test_hostflags.cpp +++ b/tests/unit/test_hostflags.cpp @@ -921,9 +921,9 @@ TEST(HostFlags, ClangOnTheMsvcAbiCompilesAndLinksAgainstTheChosenToolset) { mcpp::toolchain::Toolchain t; t.compiler = CompilerId::Clang; t.targetTriple = "x86_64-pc-windows-msvc"; - t.msvcToolsDir = "C:/VS/VC/Tools/MSVC/14.44.35207"; - t.windowsSdkRoot = "C:/Kits/10"; - t.windowsSdkVersion = "10.0.26100.0"; + t.base.msvc.toolsDir = "C:/VS/VC/Tools/MSVC/14.44.35207"; + t.base.msvc.sdkRoot = "C:/Kits/10"; + t.base.msvc.sdkVersion = "10.0.26100.0"; HostFlagOptions opt; auto find_pair = [](const std::vector& v, std::string_view flag, std::string_view value) { @@ -942,7 +942,7 @@ TEST(HostFlags, ClangOnTheMsvcAbiCompilesAndLinksAgainstTheChosenToolset) { // Without a recorded toolset nothing is said, which is the behaviour of // every build before a toolset is bound. - t.msvcToolsDir.clear(); + t.base.msvc.toolsDir.clear(); for (auto const& w : mcpp::toolchain::host_compile_tokens(t, opt, mcpp::toolchain::no_escape)) EXPECT_EQ(w.find("-Xmicrosoft"), std::string::npos) << w; } diff --git a/tests/unit/test_link_model_runtime_dirs.cpp b/tests/unit/test_link_model_runtime_dirs.cpp index 164c8b90f..a01914481 100644 --- a/tests/unit/test_link_model_runtime_dirs.cpp +++ b/tests/unit/test_link_model_runtime_dirs.cpp @@ -67,8 +67,8 @@ struct Payload { tc::PayloadPaths pp; pp.glibcLib = glibcLib; pp.glibcInclude = glibcLib.parent_path() / "include"; - t.payloadPaths = pp; - if (withSysroot) t.sysroot = sysroot; + t.base.payload = pp; + if (withSysroot) t.base.root = sysroot; return t; } }; diff --git a/tests/unit/test_linkmodel.cpp b/tests/unit/test_linkmodel.cpp index 109f7c156..bf5710b30 100644 --- a/tests/unit/test_linkmodel.cpp +++ b/tests/unit/test_linkmodel.cpp @@ -103,7 +103,7 @@ struct FakeClangWithPayload { t.compiler = tc::CompilerId::Clang; t.binaryPath = llvm / "bin" / "clang++"; t.targetTriple = "x86_64-unknown-linux-gnu"; - t.payloadPaths = tc::PayloadPaths{ + t.base.payload = tc::PayloadPaths{ .glibcInclude = glibc / "include", .glibcLib = glibcLib, .linuxInclude = {}, @@ -150,8 +150,8 @@ TEST(LinkModel, GccSysrootWinsOverPayload) { t.compiler = tc::CompilerId::GCC; t.binaryPath = dir.path / "bin" / "g++"; t.targetTriple = "x86_64-linux-gnu"; - t.sysroot = sysroot; - t.payloadPaths = tc::PayloadPaths{ + t.base.root = sysroot; + t.base.payload = tc::PayloadPaths{ .glibcInclude = dir.path / "glibc" / "include", .glibcLib = dir.path / "glibc" / "lib64", .linuxInclude = dir.path / "linux" / "include", @@ -174,8 +174,8 @@ TEST(LinkModel, GccSysrootSupplementsMissingKernelHeaders) { tc::Toolchain t; t.compiler = tc::CompilerId::GCC; t.targetTriple = "x86_64-linux-gnu"; - t.sysroot = sysroot; - t.payloadPaths = tc::PayloadPaths{ + t.base.root = sysroot; + t.base.payload = tc::PayloadPaths{ .glibcInclude = dir.path / "glibc" / "include", .glibcLib = dir.path / "glibc" / "lib64", .linuxInclude = dir.path / "linux" / "include", @@ -193,7 +193,7 @@ TEST(LinkModel, GccPayloadEmitsIdirafterAndItsOwnAddressing) { tc::Toolchain t; t.compiler = tc::CompilerId::GCC; t.targetTriple = "x86_64-linux-gnu"; - t.payloadPaths = tc::PayloadPaths{ + t.base.payload = tc::PayloadPaths{ .glibcInclude = dir.path / "glibc" / "include", .glibcLib = glibcLib, .linuxInclude = {}, @@ -289,9 +289,9 @@ tc::Toolchain clang_msvc() { tc::Toolchain t; t.compiler = tc::CompilerId::Clang; t.targetTriple = "x86_64-pc-windows-msvc"; - t.msvcToolsDir = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207"; - t.windowsSdkRoot = "C:/Program Files (x86)/Windows Kits/10"; - t.windowsSdkVersion = "10.0.26100.0"; + t.base.msvc.toolsDir = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207"; + t.base.msvc.sdkRoot = "C:/Program Files (x86)/Windows Kits/10"; + t.base.msvc.sdkVersion = "10.0.26100.0"; return t; } } // namespace @@ -300,8 +300,8 @@ TEST(LinkModel, ClangOnTheMsvcAbiNamesItsToolsetAndSdk) { auto t = clang_msvc(); auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); const std::vector expected = { - "-Xmicrosoft-visualc-tools-root", t.msvcToolsDir.string(), - "-Xmicrosoft-windows-sdk-root", t.windowsSdkRoot.string(), + "-Xmicrosoft-visualc-tools-root", t.base.msvc.toolsDir.string(), + "-Xmicrosoft-windows-sdk-root", t.base.msvc.sdkRoot.string(), "-Xmicrosoft-windows-sdk-version", "10.0.26100.0", }; EXPECT_EQ(words, expected) << "separate words: the driver rejects the `=` form"; @@ -316,20 +316,20 @@ TEST(LinkModel, ClangOnTheMsvcAbiNamesItsToolsetAndSdk) { // that saw only the directory served one image's std module to the other. TEST(LinkModel, TheCompilerVersionIsSaidWithTheToolset) { auto t = clang_msvc(); - t.msvcCompilerVersion = "19.44.35211"; + t.base.msvc.compilerVersion = "19.44.35211"; auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); ASSERT_FALSE(words.empty()); EXPECT_EQ(words.back(), "-fms-compatibility-version=19.44.35211"); // A second build of cl.exe under the same directory is a different word. auto other = t; - other.msvcCompilerVersion = "19.44.35217"; + other.base.msvc.compilerVersion = "19.44.35217"; EXPECT_NE(tc::resolve_link_model(other).msvc_driver_tokens(ident), words); } TEST(LinkModel, TheToolsetWithoutAnSdkOmitsTheSdkWords) { auto t = clang_msvc(); - t.windowsSdkRoot.clear(); - t.windowsSdkVersion.clear(); + t.base.msvc.sdkRoot.clear(); + t.base.msvc.sdkVersion.clear(); auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); ASSERT_EQ(words.size(), 2u); EXPECT_EQ(words[0], "-Xmicrosoft-visualc-tools-root"); @@ -347,6 +347,6 @@ TEST(LinkModel, NoOtherRowReceivesMsvcDriverWords) { EXPECT_TRUE(tc::resolve_link_model(mingw).msvc_driver_tokens(ident).empty()); auto unbound = clang_msvc(); - unbound.msvcToolsDir.clear(); + unbound.base.msvc.toolsDir.clear(); EXPECT_TRUE(tc::resolve_link_model(unbound).msvc_driver_tokens(ident).empty()); } diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 2a82c38e2..13a137094 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -4540,7 +4540,46 @@ sysroot = "newlib" )"; auto m = mcpp::manifest::parse_string(src); ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("xpkg reference"), std::string::npos); + EXPECT_NE(m.error().format().find("is not a sysroot"), std::string::npos); +} + +// The table form names a directory, absolute against the manifest's own +// directory, and carries what it supplies (design 2026-10-23 §3.1). +TEST(Manifest, TargetSysrootTableNamesADirectory) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[target.x86_64-linux-gnu] +sysroot = { path = "third_party/sysroot", provides = ["mcpp:c-abi=glibc"] } +run_under = ["remote-run", "team-mac", "--"] +)"; + // An absolute origin on this host (a drive letter on Windows). + const auto proj = (std::filesystem::temp_directory_path() / "proj").lexically_normal(); + auto m = mcpp::manifest::parse_string(src, proj / "mcpp.toml"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + auto it = m->targetOverrides.find("x86_64-linux-gnu"); + ASSERT_NE(it, m->targetOverrides.end()); + EXPECT_TRUE(it->second.sysrootDeclared); + EXPECT_EQ(it->second.sysroot, + "path:" + (proj / "third_party" / "sysroot").lexically_normal().generic_string()); + ASSERT_EQ(it->second.sysrootProvides.size(), 1u); + EXPECT_EQ(it->second.runUnder, (std::vector{"remote-run", "team-mac", "--"})); + for (auto const& w : m->schemaWarnings) + EXPECT_EQ(w.find("sysroot"), std::string::npos) << w; +} + +TEST(Manifest, TargetSysrootTableRefusesOtherKeys) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[target.x86_64-linux-gnu] +sysroot = { path = "/opt/s", libc = "glibc" } +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_FALSE(m.has_value()); + EXPECT_NE(m.error().format().find("`path`"), std::string::npos) << m.error().format(); } // The unknown-scalar sweep must not report a key it now honours — the exact @@ -4604,11 +4643,12 @@ TEST(Manifest, MsvcRowSysrootRefusesWhatIsNotAnMsvcToolset) { } } -TEST(Manifest, TheMsvcSpellingIsNotASysrootElsewhere) { - // On every other row the key keeps its xpkg-reference grammar. +TEST(Manifest, TheMsvcSpellingIsAcceptedByTheGrammarElsewhere) { + // On every other row the spelling is a locator the grammar knows; which + // row takes it is decided where the target is known + // (`mcpp.toolchain.base::refusal`, tests/unit/test_target_base.cpp). auto m = msvc_row("x86_64-windows-gnu", "msvc@14.44.35207"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("xpkg reference"), std::string::npos); + ASSERT_TRUE(m.has_value()) << m.error().format(); } // ── Dependency version requirements are checked where they are written ─────── @@ -5012,7 +5052,7 @@ runnerX = ["qemu-aarch64-static"] // tests/unit/test_target_scalar_keys.cpp for the test that keeps this // string (and the array beside it) from drifting from the parser again. EXPECT_NE(m->schemaWarnings[0].find( - "Supported keys: cxx_runtime, linkage, min_api_level, runner, " + "Supported keys: cxx_runtime, linkage, min_api_level, run_under, runner, " "sysroot, toolchain"), std::string::npos) << m->schemaWarnings[0]; } diff --git a/tests/unit/test_ninja_backend.cpp b/tests/unit/test_ninja_backend.cpp index a4a9434d5..08381514e 100644 --- a/tests/unit/test_ninja_backend.cpp +++ b/tests/unit/test_ninja_backend.cpp @@ -1683,7 +1683,7 @@ BuildPlan msvc_plan_with_redist(const FakeRedistDir& redist, plan.toolchain.binaryPath = "cl.exe"; plan.toolchain.targetTriple = "x86_64-pc-windows-msvc"; plan.toolchain.linkRuntimeDirs = {redist.path}; - plan.toolchain.msvcRedistDir = redist.path; + plan.toolchain.base.msvc.redistDir = redist.path; plan.manifest.buildConfig.cxxRuntime = std::string(cxxRuntime); plan.linkUnits.push_back({ .targetName = "app", diff --git a/tests/unit/test_target_base.cpp b/tests/unit/test_target_base.cpp new file mode 100644 index 000000000..9a6eb42e9 --- /dev/null +++ b/tests/unit/test_target_base.cpp @@ -0,0 +1,276 @@ +#include + +import std; +import mcpp.toolchain.base; +import mcpp.toolchain.base_probe; +import mcpp.toolchain.linkmodel; +import mcpp.toolchain.model; +import mcpp.toolchain.triple; +import mcpp.platform; +import mcpp.platform.macos; + +// The target base (design 2026-10-23, "target base and execution host"): the +// string grammar of `sysroot`, which spellings each row takes, the +// measurement tables, and what the link model renders for a stated base. +// Everything here runs on every host -- including the Apple locator, which +// reads `SDKROOT` on all of them. + +namespace { + +namespace tb = mcpp::toolchain::base; +namespace tc = mcpp::toolchain; +using mcpp::toolchain::triple::parse; + +struct Tmp { + std::filesystem::path path; + Tmp() { + path = std::filesystem::temp_directory_path() + / std::format("mcpp_target_base_{}", std::random_device{}()); + std::filesystem::create_directories(path); + } + ~Tmp() { + std::error_code ec; + std::filesystem::remove_all(path, ec); + } +}; + +void touch(const std::filesystem::path& p, std::string_view text = "x") { + std::filesystem::create_directories(p.parent_path()); + std::ofstream(p, std::ios::binary) << text; +} + +std::string ident(const std::filesystem::path& p) { return p.string(); } + +bool has(const std::vector& v, std::string_view s) { + return std::ranges::find(v, s) != v.end(); +} + +} // namespace + +TEST(TargetBaseGrammar, EveryForm) { + auto e = tb::parse_text(""); + ASSERT_TRUE(e); + EXPECT_EQ(e->form, tb::Form::Empty); + + auto p = tb::parse_text("path:/opt/acme//sysroot"); + ASSERT_TRUE(p); + EXPECT_EQ(p->form, tb::Form::Path); + EXPECT_EQ(p->text, "path:/opt/acme/sysroot"); + + auto k = tb::parse_text("acme:sysroot-rhel8@1.0"); + ASSERT_TRUE(k); + EXPECT_EQ(k->form, tb::Form::Package); + + auto l = tb::parse_text("glibc@system"); + ASSERT_TRUE(l); + EXPECT_EQ(l->form, tb::Form::Locator); + EXPECT_EQ(l->family, "glibc"); + EXPECT_EQ(l->version, "system"); + + // A versioned C library without a namespace is the ecosystem's package + // (SPEC-006 §2.3); a versioned SDK is a locator with a version. + auto g = tb::parse_text("glibc@2.39"); + ASSERT_TRUE(g); + EXPECT_EQ(g->form, tb::Form::Package); + EXPECT_EQ(g->text, "xim:glibc@2.39"); + auto i = tb::parse_text("iphoneos@18.5"); + ASSERT_TRUE(i); + EXPECT_EQ(i->form, tb::Form::Locator); + EXPECT_EQ(i->version, "18.5"); +} + +TEST(TargetBaseGrammar, RefusesWhatNamesNothing) { + EXPECT_FALSE(tb::parse_text("sysroot")); + EXPECT_FALSE(tb::parse_text("path:")); + EXPECT_FALSE(tb::parse_text("bogus@1.0")); + // An Apple SDK has no package; `@system` takes no namespace. + EXPECT_FALSE(tb::parse_text("xim:iphoneos@18.5")); + EXPECT_FALSE(tb::parse_text("xim:glibc@system")); +} + +TEST(TargetBaseRows, EachRowTakesItsOwnForms) { + auto linux_gnu = *parse("x86_64-linux-gnu"); + EXPECT_FALSE(tb::refusal(linux_gnu, *tb::parse_text("glibc@system"))); + EXPECT_FALSE(tb::refusal(linux_gnu, *tb::parse_text("path:/x"))); + EXPECT_FALSE(tb::refusal(linux_gnu, *tb::parse_text("xim:glibc@2.39"))); + // A family names a platform, so it must agree with the row. + EXPECT_TRUE(tb::refusal(linux_gnu, *tb::parse_text("musl@system"))); + EXPECT_TRUE(tb::refusal(linux_gnu, *tb::parse_text("iphoneos@system"))); + + auto sim = *parse("aarch64-ios-sim"); + EXPECT_FALSE(tb::refusal(sim, *tb::parse_text("iphonesimulator@system"))); + EXPECT_FALSE(tb::refusal(sim, *tb::parse_text("path:/sdk"))); + EXPECT_TRUE(tb::refusal(sim, *tb::parse_text("iphoneos@system"))); + // The spellings the row took before it took a locator keep their meaning. + EXPECT_FALSE(tb::refusal(sim, *tb::parse_text(""))); + + auto msvc = *parse("x86_64-windows-msvc"); + EXPECT_FALSE(tb::refusal(msvc, *tb::parse_text("msvc@system"))); + EXPECT_FALSE(tb::refusal(msvc, *tb::parse_text("xim:msvc@14.44"))); + EXPECT_TRUE(tb::refusal(msvc, *tb::parse_text("acme:vs@1"))); + + // The MSVC spelling on a MinGW row names another platform's toolset. + EXPECT_TRUE(tb::refusal(*parse("x86_64-windows-gnu"), *tb::parse_text("msvc@14.44.35207"))); + + // An SDK toolchain's row carries its own system: no new form. + auto android = *parse("aarch64-linux-android"); + EXPECT_TRUE(tb::refusal(android, *tb::parse_text("path:/ndk"))); + EXPECT_EQ(tb::row_base(android).address, tb::Address::DriverInternal); + + EXPECT_EQ(tb::row_base(*parse("riscv64-none-elf")).address, tb::Address::Multilib); + EXPECT_EQ(tb::row_base(*parse("aarch64-macos")).address, tb::Address::AppleIsysroot); +} + +TEST(TargetBaseRows, PathSlashIsTheHostLocator) { + auto s = tb::normalise(*parse("x86_64-linux-gnu"), *tb::parse_text("path:/")); + EXPECT_EQ(s.form, tb::Form::Locator); + EXPECT_EQ(s.text, "glibc@system"); + auto m = tb::normalise(*parse("x86_64-linux-musl"), *tb::parse_text("path:/")); + EXPECT_EQ(m.text, "musl@system"); +} + +TEST(TargetBaseMeasure, ImplementationFromThePreprocessor) { + auto glibc = tb::classify_macros(tb::parse_defines( + "#define __GLIBC__ 2\n#define __GLIBC_MINOR__ 31\n#define FOO(x) x\n")); + ASSERT_TRUE(glibc); + EXPECT_EQ(glibc->impl, "glibc"); + EXPECT_EQ(glibc->version, "2.31"); + + auto pico = tb::classify_macros(tb::parse_defines( + "#define __PICOLIBC__ 1\n#define __PICOLIBC_VERSION__ \"1.8.12\"\n")); + ASSERT_TRUE(pico); + EXPECT_EQ(pico->version, "1.8.12"); + + auto bionic = tb::classify_macros(tb::parse_defines( + "#define __BIONIC__ 1\n#define __ANDROID_API__ 24\n")); + ASSERT_TRUE(bionic); + EXPECT_EQ(bionic->factName, "android.api"); + + EXPECT_FALSE(tb::classify_macros(tb::parse_defines("#define __linux__ 1\n"))); +} + +TEST(TargetBaseMeasure, VersionsCompareNumerically) { + EXPECT_LT(tb::compare_versions("2.9", "2.31"), 0); + EXPECT_GT(tb::compare_versions("2.38", "2.31"), 0); + EXPECT_EQ(tb::compare_versions("2.31", "2.31.0"), 0); + EXPECT_EQ(tb::highest_glibc({"GLIBC_2.2.5", "GLIBC_2.38", "GLIBC_PRIVATE", "GLIBC_2.34"}), + "2.38"); +} + +TEST(TargetBaseMeasure, MultiarchTuplesAreDebiansTable) { + EXPECT_EQ(tb::multiarch_tuple(*parse("x86_64-linux-gnu")), "x86_64-linux-gnu"); + EXPECT_EQ(tb::multiarch_tuple(*parse("aarch64-linux-gnu")), "aarch64-linux-gnu"); + EXPECT_EQ(tb::multiarch_tuple(*parse("x86_64-linux-musl")), "x86_64-linux-musl"); +} + +TEST(TargetBaseMeasure, PayloadLayoutIsNotASysroot) { + Tmp t; + touch(t.path / "pkg" / "include" / "features.h"); + touch(t.path / "pkg" / "lib64" / "libc.so.6"); + EXPECT_TRUE(tb::is_payload_layout(t.path / "pkg")); + touch(t.path / "root" / "usr" / "include" / "features.h"); + touch(t.path / "root" / "lib64" / "libc.so.6"); + EXPECT_FALSE(tb::is_payload_layout(t.path / "root")); +} + +// The identity moves when a file the measurement rested on is rewritten in +// place, and only then. +TEST(TargetBaseIdentity, FollowsTheEvidence) { + Tmp t; + const auto lib = t.path / "lib64" / "libc.so.6"; + touch(lib, "one"); + const auto a = tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31"); + EXPECT_EQ(a, tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31")); + touch(lib, "a longer file"); + EXPECT_NE(a, tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31")); + EXPECT_NE(a, tb::identity_of("path:/y", t.path, {lib}, "glibc 2.31")); +} + +// Apple's own convention, on every host: `SDKROOT` names the SDK, its +// `SDKSettings.json` its version, its `libSystem.tbd` its slices. +TEST(TargetBaseApple, SdkrootOnEveryHost) { + Tmp t; + const auto sdk = t.path / "iPhoneSimulator18.5.sdk"; + touch(sdk / "SDKSettings.json", R"({"CanonicalName": "iphonesimulator18.5", "Version": "18.5"})"); + touch(sdk / "usr" / "include" / "stdio.h"); + touch(sdk / "usr" / "lib" / "libSystem.tbd", + "--- !tapi-tbd\ntbd-version: 4\ntargets: [ x86_64-ios-simulator, arm64-ios-simulator ]\n"); + mcpp::platform::env::ScopedEnv env("SDKROOT", sdk.string()); + auto found = tb::locate_apple(*tb::parse_text("iphonesimulator@system"), "iphonesimulator"); + ASSERT_TRUE(found) << found.error(); + EXPECT_EQ(found->root, sdk); + EXPECT_EQ(found->version, "18.5"); + EXPECT_FALSE(tb::locate_apple(*tb::parse_text("iphonesimulator@17.0"), "iphonesimulator")); + // SDKROOT names one SDK and does not answer for another. (On an Apple + // host `xcrun` answers next, and Xcode's iPhoneOS SDK is found there.) + if (!mcpp::platform::is_macos) + EXPECT_FALSE(tb::locate_apple(*tb::parse_text("iphoneos@system"), "iphoneos")); + EXPECT_EQ(mcpp::platform::macos::sdk_has_target(sdk, "x86_64-ios-simulator"), true); + EXPECT_EQ(mcpp::platform::macos::sdk_has_target(sdk, "x86_64-ios"), false); +} + +TEST(TargetBaseApple, PathMustBeAnSdk) { + Tmp t; + auto none = tb::locate_apple(*tb::parse_text("path:" + (t.path / "nothing").string()), "macosx"); + EXPECT_FALSE(none); + touch(t.path / "empty" / "readme"); + EXPECT_FALSE(tb::locate_apple(*tb::parse_text("path:" + (t.path / "empty").string()), "macosx")); +} + +// THE LINKER IS GIVEN THE SYSROOT, AND A BINDING ARTIFACT LOADS THE BASE: the +// two defects #755's sysroot had (scenarios C and D). +TEST(TargetBaseLinkModel, StatedBindingBaseNamesItsLoaderAndDirs) { + tc::Toolchain t; + t.compiler = tc::CompilerId::GCC; + t.targetTriple = "x86_64-linux-gnu"; + t.binaryPath = "/tc/bin/g++"; + t.base.form = tb::Form::Path; + t.base.address = tb::Address::GnuSysroot; + t.base.runtime = tb::Runtime::Binding; + t.base.root = "/sr"; + t.base.loader = "/sr/lib64/ld-linux-x86-64.so.2"; + t.base.runtimeDirs = {"/sr/lib64"}; + auto lm = tc::resolve_link_model(t); + EXPECT_EQ(lm.mode, tc::CLibMode::Sysroot); + auto link = lm.link_tokens(ident); + EXPECT_TRUE(has(link, "--sysroot=/sr")); + EXPECT_TRUE(has(link, "-Wl,--sysroot=/sr")); + EXPECT_TRUE(has(link, "-Wl,--dynamic-linker=/sr/lib64/ld-linux-x86-64.so.2")); + EXPECT_TRUE(has(link, "-Wl,-rpath,/sr/lib64")); + + // A floor (a cross target) names no loader: the pristine spec's standard + // path is the one the target machine has. + t.base.runtime = tb::Runtime::Floor; + auto floor = tc::resolve_link_model(t).link_tokens(ident); + EXPECT_FALSE(std::ranges::any_of(floor, [](auto const& s) { + return s.starts_with("-Wl,--dynamic-linker"); })); +} + +TEST(TargetBaseLinkModel, MultiarchForADriverThatDoesNotKnowIt) { + tc::Toolchain t; + t.compiler = tc::CompilerId::GCC; + t.targetTriple = "x86_64-linux-gnu"; + t.binaryPath = "/tc/bin/g++"; + t.base.form = tb::Form::Locator; + t.base.address = tb::Address::GnuSysroot; + t.base.runtime = tb::Runtime::Binding; + t.base.root = "/"; + t.base.multiarch = true; + t.base.multiarchTuple = "x86_64-linux-gnu"; + auto lm = tc::resolve_link_model(t); + // Spelled as the host spells a path: the renderer joins with `/` + // operators, so a Windows host writes its own separator. + namespace fs = std::filesystem; + const auto root = fs::path("/"); + EXPECT_TRUE(has(lm.compile_tokens(ident), + "-idirafter" + (root / "usr" / "include" / "x86_64-linux-gnu").string())); + auto link = lm.link_tokens(ident); + EXPECT_TRUE(has(link, "-B" + (root / "usr" / "lib" / "x86_64-linux-gnu").string())); + EXPECT_TRUE(has(link, "-Wl,-rpath-link," + (root / "lib" / "x86_64-linux-gnu").string())); +} + +TEST(PasteArg, QuotesOnlyWhatTheShellWouldSplit) { + EXPECT_EQ(mcpp::platform::shell::paste_arg("/tmp/home"), "/tmp/home"); + EXPECT_NE(mcpp::platform::shell::paste_arg("/tmp/mc probe/home"), "/tmp/mc probe/home"); + EXPECT_NE(mcpp::platform::shell::paste_arg("/tmp/a'b"), "/tmp/a'b"); +} diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 685304836..d6a8114da 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -768,7 +768,7 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { // NOT contain these three. for (auto [name, tier] : {std::pair{"aarch64-ios", "preview"}, std::pair{"aarch64-ios-sim", "verified"}, - std::pair{"x86_64-ios-sim", "preview"}}) { + std::pair{"x86_64-ios-sim", "verified"}}) { auto t = parse(name); ASSERT_TRUE(t.has_value()) << name; EXPECT_EQ(t->str(), name);