From 9d61a0b010e8c3ffdc2486cb2ba295e0fc1e87d6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 05:34:47 +0900 Subject: [PATCH 01/32] chore: 2026.10.10.1; bootstrap from 2026.10.8.1; the design record --- ...026-10-10-one-source-one-meaning-design.md | 771 ++++++++++++++++++ ...2026-10-10-one-source-one-meaning-repro.sh | 169 ++++ .agents/docs/README.md | 4 +- .xlings.json | 2 +- mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- 6 files changed, 946 insertions(+), 4 deletions(-) create mode 100644 .agents/docs/2026-10-10-one-source-one-meaning-design.md create mode 100644 .agents/docs/2026-10-10-one-source-one-meaning-repro.sh diff --git a/.agents/docs/2026-10-10-one-source-one-meaning-design.md b/.agents/docs/2026-10-10-one-source-one-meaning-design.md new file mode 100644 index 000000000..4f7739228 --- /dev/null +++ b/.agents/docs/2026-10-10-one-source-one-meaning-design.md @@ -0,0 +1,771 @@ +--- +subject: design +status: proposed +--- + +# 一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790) + +- 日期:2026-10-10。状态:修订 5,跨仓库方案(mcpp、xlings、xim-pkgindex/xlings-res、mcpp-plugins),待维护者 review;尚未实施。 +- 基线:`main` `b1652184`(2026.10.8.1)。 +- 证据:本地 Linux x86_64,已发布的 mcpp 2026.10.8.1,gcc 16.1.0 与 llvm 23.1.3。 + 复现脚本 [2026-10-10-one-source-one-meaning-repro.sh](2026-10-10-one-source-one-meaning-repro.sh) + 只打印观察结果,不做断言;下文 R1–R6 引用其中的用例。 +- 外部 PR #787(#786)与 #779(#778)各自只修复报告中的一个实例,本文不以它们为基础(§15)。 + +**修订 5 的变化(review 结论与探测):** +- Android 不换工具链:aarch64 仍用 llvm@23.1.3 + glibc,目标是真实跑通(D21 改为只识别、不改默认值)。 +- §13 改写为跨仓库修复:xlings 的硬链接回退(X1)、失败分类与止损(X2)、阶段内进度(X3);载荷重新打包(P1、P2); + mcpp 的 D28、D29、D22、D23、D27。 +- 探测 round 6、7:Android 14 应用沙箱中,去掉硬链接的 llvm@23.1.3 + glibc 能运行,mcpp 2026.10.8.1 构建 `import std` 成功; + 最新的 xlings 2026.10.10.1 解包同样失败(xlings 自身缺陷);arm64 runner 没有 KVM。 +- 用户的日志证实 Planning 的 35 分钟全部在 xlings 安装 musl-gcc 之中(R4)。 +- D30:xlings 固定到包含 X1–X3 的新版本,不是 2026.10.10.1(它没有 X1)。 + +**修订 4 的变化(review 结论与探测):** +- D17 改写:工具链只要被明确指定过(清单、`--toolchain`、全局默认值),就永不自动换成 MinGW; + 只有什么都没指定时才可以改用 MinGW(§12.2)。 +- §13 增加:LLVM 没有 musl 静态版本;Android 宿主的默认值与以后的静态 LLVM(O9);Termux 的 CI(D27)。 +- 临时探测 PR #791(五轮:run 37974353683、37975117448、37976918574、37977451853、37978204220)测量 O5、O6、O8,结果写入 §5.2、§12.1、§13(修订 5 时又跑了 round 6、7:run 37980130590、37980701891)。 +- 探测发现 Android 上另外三个阻碍:mcpp 写死 `/bin/sh`(D28)、工具链的加载器环境泄漏给宿主 shell(D29)、 + termux-exec 的 linker 执行模式拒绝非 PIE 静态程序(O10)。硬链接问题不止 glibc:gcc、musl-gcc 的载荷同样含有。 +- 新增 §14:mcpp 依赖的 xlings 升级到最新发布版本(D30)。 + +**修订 3 的变化(review 结论):** +- O1:旧位置写法移入 `mcpp.pm.compat` 兼容模块,标注 `COMPAT(workspace-position)` 并给出警告,随兼容模块的退役边界一起移除(§3.6)。 +- O2、O3、O4:按建议采纳,转为 D24–D26(§6)。 +- O5:显式定义 `__cpp_impl_coroutine` 由用户负责,mcpp 不做保证;删除修订 2 的 G3 探测与失败说明的修改(§9)。 + MSVC 下各模块角色的参数仍需实测,因为 D8 依赖它(O5 收窄)。 +- 新增 §11:mcpp 自身与生态(mcpp-plugins、openkal、xim-pkgindex、xlings)的同步更新清单。 +- 新增 §12:Windows 上 Visual Studio 或 Windows SDK 不在 `C:` 时的探测与回退。 +- 新增 §13:Termux(Android)上首次运行失败与 Planning 长时间无输出。 + +**修订 2 的变化:** +- 文件改名(原名 `2026-10-10-root-position-action-outputs-module-role-design.md`)。 +- §2 起改写工作空间语义:以“前缀即语义”取代修订 1 中“`[target.]` 标量行是环境表、 + `.build` 是包表”的划分。那种划分让同一个 `[target.]` 表头下的两部分传递方式不同, + 是理解负担的来源。 +- 键的类别登记表扩展到全部键(D4),整套语义在一个发布中完成,不分步。 +- 新增:`--toolchain` 覆盖清单中的工具链时给出警告(§8);显式启用 `__cpp_impl_coroutine` + 的写法与提示(§9);`.xlings.json` 的引导版本(§10)。 +- review 结论:保留多配置模型(成员可以声明自己的配置);共享只经 `workspace.` 前缀; + 虚拟根上的包表先给出警告。 + +**修订 1:** 初稿,提出 A/B/C 三类问题。 + +## 0. 范围 + +| 项 | 结论 | 理由 | +| --- | --- | --- | +| #786 中以 `dialect_cxxflags` 定义 `__cpp_impl_coroutine` | 使用问题;由用户负责,见 §9 | clang 23 对 32 位 x86 Microsoft ABI 不支持协程,2026.10.8.1 已给出说明 | +| #784 | 不是设计问题 | 剩余阻塞是 `released ARM64 CN SubOS ecosystem` 中 proot 的 signal 11,单独处理 | +| #756 | 已修复 | 2026.10.2.1(#759),已关闭 | + +在范围内的问题,结构都相同:**一个事实有两个来源,或者一个产物有两个写入者**。 + +| 类 | issue | 一个事实,两个来源 | +| --- | --- | --- | +| A 工作空间的传递 | #786、#785 | 同一张表的传递方式由它“写在哪里”与“它是哪一部分”共同决定;根位置值由三张手抄清单分别决定继承、分组与虚拟根 | +| B action 输出 | #778、R2 | 一个输出有两个写入者(prepare 的占位文件与生成器),新旧由不与它同处的 ninja 日志判断 | +| C 模块角色 | #790、R5 | 一个单元是否产出 BMI,由扩展名与模块声明分别回答,规则选择只读前者 | + +## 1. 实测 + +| 用例 | 观察(2026.10.8.1) | 期望 | +| --- | --- | --- | +| R1a 虚拟工作空间根写 `[target.'cfg(os = "linux")'.build] dialect_cxxflags = ["-DDIAL=1"]`,`-p app` | `DIAL=0`,0 条警告 | 有明确的写法使其生效;当前写法应有诊断 | +| R1b 同样的行写在成员中,`-p app` | `DIAL=0` | 2 | +| R1c 同一个成员作为独立包 | `DIAL=2` | 2 | +| R2 生成器输出取决于 `mcpp::profile()`;依次 `build`、`build --release`、`build` | 第三次的 dev 产物打印 `GEN=release`;只有一个 `target/.build-mcpp/out/gen.cpp` | `GEN=dev` | +| R3 #778 的工作空间(Linux):首次构建后移走 `app/target`,保留工作空间的 `target` | 第二次构建失败:`undefined reference to resource()`,`generated.cpp` 为 0 字节 | 生成器重新运行 | +| R4 `src/api_impl.cpp` 写 `module repro:api_impl;`,llvm 23.1.3 | 每次构建都重编该单元并重新链接;`pcm.cache/` 中没有 `repro-api_impl.pcm` | 第二次构建无工作 | +| R5 `src/r5.cpp` 写 `export module r5;`,llvm 23.1.3 | `fatal error: module 'r5' not found` | 构建成功 | +| R6 R4 中由接口单元 `import :api_impl;` | `fatal error: module 'repro:api_impl' not found` | 构建成功 | +| R4–R6 改用 gcc 16.1.0 | 全部成功,第二次构建 0 步 | — | + +## 2. 概念 + +文档与代码中的 “root” 指四种不同的东西。本文起分开使用: + +| 术语 | 含义 | +| --- | --- | +| **工作空间根** | 含 `[workspace]` 的清单所在的目录与清单本身 | +| **根包** | 工作空间根同时含 `[package]` 时的那个包;它是成员 `"."`(2026.10.5.2 起) | +| **构建的配置** | 一次构建中全图共有的值:工具链、目标、标准、方言、链接方式、profile、索引……SPEC-004 §9 第 10 条所说的“根位置”指它 | +| **计划根** | 实现概念:由被选中成员合成的虚拟根;不出现在用户语义中 | + +每个键有两个互相独立的属性: + +- **作用域**,是键自身的属性: + - P 包私有:`cxxflags`、`sources`、`defines`、`private_include_dirs`……只作用于本包的单元; + - U 使用需求:`include_dirs`、`ldflags`、库……从依赖流向消费者; + - C 配置:`standard`、`dialect_cxxflags`、`linkage`、`cxx_runtime`、`target`、`toolchain`、 + `profile`、`indices`、`[target.]` 的标量行、`.abi`……从构建的配置流向全图; + - M 元数据:`version`、`license`……不进入命令。 +- **来源**,是表的属性:命令行;本包的表;工作空间的共享表;引擎默认值。 + +条件(`[target.]`)是第三个维度:它是作用在值上的谓词,**既不改变作用域,也不改变来源**。 + +## 3. A 类:工作空间的传递 + +### 3.1 现状 + +工作空间根的清单今天按**位置**传递一部分表,按**前缀**传递另一部分表: + +| 写在工作空间根 | 今天的传递 | +| --- | --- | +| `[workspace.package]`、`[workspace.build]` | 前缀:每个成员继承 | +| `[workspace.dependencies]` | 前缀:成员以 `x.workspace = true` 逐项选择 | +| `[toolchain]`、`[indices]`、`[profile.]` | 位置:成员作为构建的根时继承,成员自己的声明优先 | +| `[target.]` 的标量行(`toolchain`、`linkage`、`cxx_runtime`、`sysroot`、`min_api_level`、`runner(s)`) | 位置:同上,按 triple 整条替换 | +| `[xlings.workspace]` 与 `[target..xlings.workspace]` | 位置:每个成员逐行继承 | +| `[target..build]`、`[target..abi]`、`[target..runtime]` | **不传递**(R1a,没有诊断) | +| `[build]`、`[targets]`、`[dependencies]`、`[features]`…… | 不传递;属于根包,虚拟根上静默丢弃 | + +于是同一个表头 `[target.]` 下,标量行会传递,`.build` 不会;同一个 `[toolchain]`, +在根包上既是根包自己的声明,也是给成员的默认值。读者必须记住一张按位置、按子表区分的例外表, +#785 已经指出这是理解负担。实现层面,继承(`inherit_workspace_root_position`, +`src/project.cppm:282`;`inherit_workspace_xlings`,`:222`;`inherit_workspace_build`)、 +分组(`root_position_key`,`:743`)与计划根(`virtual_workspace_root`,`:813`)是几张必须保持一致的手抄清单。 +条件 dialect 与条件 abi 在其中全部缺失,结果就是 R1a、R1b(#786)。 + +### 3.2 语义设计:前缀即语义 + +**规则 W1。一份清单中,`workspace.` 之外的每张表只说本包:P、U、M 键作用于本包,C 键是 +“本包作为构建的根时”的配置。`workspace.` 之内的每张表只说成员。任何表都不按位置传递。** + +**规则 W2。可共享的表 `X` 都有镜像 `[workspace.X]`,键集合、子表结构与条件选择器与 `X` 相同, +去掉不可共享的键。** + +| 本包写法 | 工作空间共享写法 | 状态 | +| --- | --- | --- | +| `[package]` 中可共享的元数据与 `standard` | `[workspace.package]` | 已有 | +| `[build]` | `[workspace.build]` | 已有(`kWorkspaceBuildKeys`) | +| `[dependencies]` 的条目 | `[workspace.dependencies]` + `x.workspace = true` | 已有,仍逐项选择加入 | +| `[toolchain]` | `[workspace.toolchain]` | 新增 | +| `[indices]` | `[workspace.indices]` | 新增 | +| `[profile.]` | `[workspace.profile.]` | 新增 | +| `[target.]` 的标量行、`.build`、`.abi`、`.runtime`、`.xlings.workspace` | `[workspace.target.]` 及同名子表 | 新增 | +| `[xlings.workspace]` | `[workspace.xlings.workspace]` | 新增(O2) | +| `[targets]`、`[features]`、`[resources]`、`[test]`、`[target..targets]`、`[target..dependencies]`、`requires_abi` | — | 不可共享:它们描述一个具体的包 | + +**规则 W3。合并规则只由键决定,不由表决定。** 成员的值 = 合并(工作空间层,成员层): + +| 键的形态 | 规则 | +| --- | --- | +| 标量 | 成员**声明了**该键时成员优先,否则取工作空间的值(声明与否被记录,不由值推断) | +| 向量 | 追加,工作空间在前 | +| `defines` | 按宏名构成集合;成员同名条目替换,`!NAME` 删除 | +| 命名表(`profile.`、`target.`) | **逐键**合并,规则同上;不再整表替换 | +| 相对路径 | 以写下它的清单所在目录为基准(#224) | + +条件行逐行继承:工作空间的条件行排在成员的条件行之前,成员内部仍按 SPEC-004 §3.1.1 的选择器特异性排序。 + +**规则 W4。构建的配置**按以下顺序决定,先出现者优先: +1. 命令行(`--target`、`--toolchain`、`--profile`……); +2. 被选中成员合并后的 C 键(W3 的结果),条件行以该构建的目标求值。 + +同一张图中的成员,求值后的配置必须相等;不相等的分到不同的图(多配置模型保留,review 结论 1)。 +依赖包中的 C 键不生效,也不诊断(SPEC-004 §9 第 10 条,不变)。 + +**规则 W5。成员在每个位置得到的继承值都相同,而且只得到一次**(2026.9.25.1 起的不变式): +作为被选中的成员、作为另一个成员的 `path` 依赖、作为通过 `git` 取得的工作空间的成员、作为宿主工具子构建的根。 + +**规则 W6。成员发布时,继承来的值写进它自己的表**:`[workspace.target..build]` 写成 +`[target..build]`,`[workspace.toolchain]` 写成 `[toolchain]`,依此类推。 +已发布的包因此自成一体(SPEC-004 §9 第 7 条)。这样,同一个包作为宿主工具(`tools =`) +从索引取得并成为子构建的根时,它的配置与在工作空间中构建时相同。 + +**规则 W7。** 一份没有 `[package]` 的工作空间根,若写了包表(`[build]`、`[targets]`、 +`[target..build]` 中的包键等),给出警告:这些表在这里不作用于任何包;写到成员中, +或写成 `[workspace...]` 来共享。`--strict` 下为错误。 + +**例子**(#786 的意图,按新规则): + +```toml +# 工作空间根:对所有成员,在 i686-windows-msvc 上 +[workspace.target.i686-windows-msvc.build] +dialect_cxxflags = ["-DARCH_COMPAT=1"] + +# 或只对一个成员:写在成员自己的清单中(今天的写法,修复后在工作空间中同样生效) +[target.i686-windows-msvc.build] +dialect_cxxflags = ["-DARCH_COMPAT=1"] +``` + +用户只需记住一句话:**要共享,就加 `workspace.`;不加,就只说本包。** + +### 3.3 与 #785 B 的关系 + +#785 B 反对把 `[target.]` 改名为 `workspace.`。它的三条理由在本设计下都不成立: + +- 独立工程不受影响:它的 `[toolchain]`、`[target.]` 仍写在原处,含义不变(W1)。 +- 作为宿主工具构建的成员仍用自己的行(W5);发布后由 W6 写出,行为不变。 +- “永久双读”只发生在工作空间根,而工作空间根本身不会被发布;成员发布时继承值已写出(W6)。 + 兼容期之后旧写法即可移除(§3.6)。 + +#785 A 的 `[workspace.target..build]` 是 W2 的一行;#785 C 即 W7。 + +### 3.4 对比 + +| 维度 | Cargo | Meson | Bazel | CMake | mcpp(本设计) | +| --- | --- | --- | --- | --- | --- | +| 配置归谁 | `[profile]`、`[patch]`、`resolver` 只在工作空间根生效,成员的被忽略并警告 | `add_global_arguments` 只能在主项目调用 | 属于本次构建(命令行、platform),不属于包 | 一个构建树一个配置 | 属于本次构建;被选中的成员可以声明,多配置分图 | +| 成员能否有不同配置 | 稳定版不能(`per-package-target` 仍 unstable) | 子项目选项可覆盖 | 能(配置 transition) | 不能 | 能(W4) | +| 共享方式 | 命名表 `[workspace.*]`,逐键选择加入 | 主项目 `default_options` | 不继承 | 目录作用域隐式继承 | 命名表 `[workspace.*]`,默认继承(W2) | +| 虚拟根写包表 | 报错 | — | — | — | 警告,`--strict` 为错误(W7) | +| 条件 | 包级 `cfg()` 依赖;flags 放在环境层 `.cargo/config.toml` | 构建文件中的条件判断 | `select()`,只作用于值 | 生成器表达式 | 只作用于值,不改变作用域与来源 | + +结论: +- 配置不属于包,这一点各家一致; +- mcpp 允许成员有不同配置,这是 C++ 工作空间(固件、宿主工具、测试并存)的真实需要,代价是 W4 中的“按求值后的值分组”; +- 共享经由命名表,与 Cargo 的 `[workspace.*]`、pnpm 的 catalog、Gradle 弃用 `allprojects{}` 的方向一致; + 按位置隐式传递(CMake 目录作用域)是被淘汰的做法。 + +(其他工具的条目来自已有了解,写入规范引用前需逐条核对原文。) + +### 3.5 技术架构 + +1. **键的类别登记表 `KeyRegistry`(`modules/manifest`)。** 每个可写的键一行: + - 表路径与键名; + - 作用域(P/U/C/M); + - 合并形态(标量 / 向量 / 宏集合 / 命名表); + - 是否可共享(W2); + - 是否允许出现在条件行; + - 发布时是否写出(W6); + - 字段访问器(沿用 `kWorkspaceBuildKeys` 的成员指针 variant)。 + + `kWorkspaceBuildKeys` 是它的一个子集,并入其中。 + + 下列逻辑全部由登记表推导,不再各写一份: + - 解析器的已知键检查与错误文本; + - `[workspace.*]` 镜像表的解析; + - 继承; + - 根位置值; + - 发布时写回; + - W7 的“包表”判定; + - 文档中的键类别表(新增 `check_manifest_key_classes.py`,比对登记表与 docs/04、docs/07 的表)。 +2. **分层的声明。** 清单读入后保存两层:本包层与工作空间层(来自 `[workspace.*]`,旧位置写法也在此层,见 §3.6)。 + 条件行是 `ConditionalRow { predicate; 若干 (键, 值) }`,键带着登记表中的类别。 + `ConditionalConfig` 中包所有的部分与根位置的部分由类型分开(`RootPositionRow`)。 +3. **一个合并函数。** `inherit(member, workspaceLayer, registry)` 对不带条件的键与条件行使用同一套规则(W3), + 取代 `inherit_workspace_root_position`、`inherit_workspace_xlings` 与 `inherit_workspace_build` 中的手写逻辑。 + 每个继承来的值记录来源(文件、表、键、选择器),沿用 `inheritedFromWorkspace`(WS3),供诊断与 `mcpp why` 使用。 +4. **`RootPosition`。** `root_position(member, cfgContext)` 从合并后的成员导出全部 C 键,条件行已求值。 + - 分组键 = 它的规范序列化; + - 计划根应用第一个成员的 `RootPosition`,并携带其原始条件行,供 prepare 以同一上下文求值; + - 新的 C 键只需在登记表中标为 C,不必改动这些函数。 +5. **按有效值分组。** prepare 的上下文是 `cfgpred::context_for(目标 triple)`(`toolchain.cpp:1056`); + 目标 triple 取 `--target`,否则取 `[build] target`(`toolchain.cpp:966`),否则取宿主。 + 这三项在 CLI 中已知,不需要解析工具链。`workspace_groups`(`src/cli/selection.cppm:213`) + 调用同一个函数,因此分组与 prepare 的求值在构造上一致。 + +### 3.6 兼容与迁移 + +- 工作空间根上的旧位置写法(`[toolchain]`、`[indices]`、`[profile.]`、`[target.]` 标量行、 + `[xlings.workspace]` 及其条件行)**行为不变**,仍被读入工作空间层,仍按原规则(按 triple 或按名字整表替换)合并; + 每张表给出一次警告 `manifest/workspace-position`,并写出对应的新写法。 +- 警告只在旧规则**真正起作用**时给出: + - 虚拟根上的这几张表只有传给成员这一种含义,总是警告; + - 带 `[package]` 的根上,它们首先是根包自己的声明(W1);只有当某个成员作为构建的根、 + 并且确实经由旧规则得到了值时才警告,警告指出该成员与该表。只构建根包时不警告。 +- **旧写法的读取全部放在兼容模块中。** 新增 `modules/manifest/src/compat/workspace_position.cppm` + (`mcpp.pm.compat.workspace_position`),由 `mcpp.pm.compat` 门面导出: + - 它是唯一知道旧写法的地方:把工作空间根上的这几张表转换为工作空间层,并记录“来自旧位置写法”; + 继承、分组与 `KeyRegistry` 只看到工作空间层,不知道旧写法的存在; + - 每个调用点与转换处标注 `// COMPAT(workspace-position): <移除条件>`,移除时用 grep 即可找全; + - `compat.cppm` 顶部的 DEPRECATION SCHEDULE 表增加一行(旧写法 → `[workspace.X]`),与已有的两行同样在 1.0.0 移除; + - 警告由该模块在转换时发出,措辞列出旧表、新表与迁移示例。 +- 同一张表同时以旧写法与 `[workspace.X]` 写出时报错:同一个事实不能有两种说法。 +- 带 `[package]` 的工作空间根迁移后,根包自己的 `[toolchain]` 等只作用于根包;根包作为成员同样接收 `[workspace.*]`。 +- 旧写法移除:随兼容模块在 1.0.0 移除(DEPRECATION SCHEDULE)。 +- **旧版 mcpp 会静默忽略新写法。** 实测 2026.10.8.1 读到 `[workspace.toolchain]` 时不给任何提示,照常构建 + (于是用的是另一个工具链)。因此: + - 使用新写法的工作空间需要用 `[workspace.package] mcpp = ">=<本版本>"` 声明最低版本,由旧版已有的引擎版本检查拒绝构建; + - 本版本读到新写法、而工作空间没有声明这一下限时,给出一条 note,建议补上; + - 本版本起,`[workspace]` 下的未知表报错。今天未知表被静默忽略,这与“传播表中被静默丢弃的键一律拒绝” + (docs/07 §4.1)矛盾,属于同一类缺陷。 +- 行为变化: + - 成员曾被静默丢弃的条件 dialect 与 abi 开始生效(std 缓存键与 fingerprint 改变,相关包重编一次); + - 条件值求值后不同的成员分到不同的图;只在其他目标上不同的成员仍在同一张图中; + - 虚拟根上无效的包表开始警告。 + +### 3.7 诊断分级 + +| 情形 | 等级 | +| --- | --- | +| 虚拟根上的包表(W7) | `warning manifest/schema`;`--strict` 为错误 | +| 工作空间根上的旧位置写法 | `warning manifest/workspace-position`,附新写法 | +| 同一表的旧写法与 `[workspace.X]` 并存 | 解析错误 | +| `[workspace.X]` 中不可共享的键(如 `allow_host_libs`、`[workspace.target..targets]`) | 解析错误,沿用 `[workspace.build]` 的措辞 | +| `[workspace]` 下的未知表 | 解析错误(今天静默忽略) | +| 使用新写法但未声明 `mcpp` 下限 | note,建议声明下限 | +| 继承与分组结果改变 | 无诊断;CHANGELOG 记录 | + +## 4. B 类:action 输出的所有权 + +### 4.1 现状与根因 + +- prepare 为 Source action 尚不存在的 TU 输出写占位文件(`directives.cppm:1488`),供 prepare 期扫描读取;生成器在构建时覆盖它。 +- `mcpp::out_dir()` 固定为 `<包>/target/.build-mcpp/out`,与配置无关(`build_program.cppm:753`); + 工作空间成员的 out_dir 在成员目录下(`target_side.cpp:2011`),计划的 ninja 日志在工作空间根下。 + +根因有两条: +1. **一个输出有两个写入者。** 占位文件比 action 的输入新;只要某份 ninja 日志里有这条 action 的记录, + ninja 就把占位文件当作最新的生成结果(R3,#778)。#534 是同一结构的前一个实例。 +2. **输出与判断它新旧的日志不在一起。** 多个配置共写一个文件,各自的日志都认为它是最新的(R2); + 成员目录与工作空间日志可以分别被清除(R3)。即使没有占位文件,R2 仍然成立。 + +### 4.2 设计 + +**原则:action 的输出只有一个写入者,即 action 本身;它与判断它新旧的 ninja 日志处于同一配置目录下。** + +- **单一写入者(D6)。** prepare 不再写任何 action 输出。生成的 TU 在 prepare 期以“声明单元”进入扫描: + - 路径与语言来自输出路径和扩展名分类;provides/imports 来自 action 的声明(`.provides()` / `.imports()`); + - 不读文件,构造方式与 `scan_overrides` 分支相同(`scanner.cppm:1222`); + - 声明了 `provides` 的单元标为 `scanOverridden`,构建时由编译器的 P1689 扫描审计(`ninja_backend.cppm:2800`); + - 未声明 `provides` 的单元不作断言,它的 imports 由构建期 dyndep 扫描发现,与今天的空占位文件效果相同。 +- **按配置的 out_dir(D7)。** `out_dir` 改为 `<计划根目录>/target/.build-mcpp/out/<配置标识>/<包>`: + - 计划根目录:独立包为包目录;工作空间计划为工作空间根(成员也是); + - 配置标识:构建程序运行前已知、且会改变程序环境的值的摘要(O3); + - 构建程序的二进制与缓存仍放在不分配置的位置,不重复编译。 + +Cargo 的 `OUT_DIR`(`target//build/-/out`)、Bazel 的 `bazel-out/<配置>/` 与“每个输出只有一个生成 action”, +都是同一原则;CMake 多配置生成器下不带 `$` 的 custom command 输出会在配置间共享,就是 R2 的问题。 + +### 4.3 兼容与诊断 + +- `mcpp::out_dir()`、`${mcpp.out_dir}`、`MCPP_OUT_DIR` 的值改变。按接口使用的构建程序不受影响; + 硬编码 `target/.build-mcpp/out` 的项目需要改用接口(#778 的复现本身就是硬编码)。CHANGELOG 与 docs/30 写明。 +- 构建程序之外的脚本(例如测试脚本)需要知道某个包的生成目录时,从 `resolution.json` 读取: + 每个包新增 `outDir` 字段。这是给外部读取者的稳定出口,不再让它们猜路径(mcpp-plugins 的测试即属此类,§11)。 +- 旧位置不再写入也不再读取,`mcpp clean` 删除;旧版本遗留的零字节占位文件因此失效。 +- configure-only 之后,生成的 TU 在第一次构建前不在磁盘上,`compile_commands.json` 中的条目暂时指向不存在的文件。 +- 无新诊断:声明不符时的构建期失败沿用现有审计报错。O4 的过渡期用 `degraded`。 + +## 5. C 类:模块角色 + +### 5.1 现状与根因 + +- `pick_rule`(`ninja_backend.cppm:2532`)按扩展名分类选择规则:`.cpp` → `cxx_object`,不写 BMI。 +- 扫描器读模块声明,得到 `provides`;edge 据此声明 BMI 输出(`ninja_backend.cppm:2983`)。 +- `cxx_module` 规则内部,`module_lang` 与 `module_output` 已按扫描结果逐 edge 设置(`module_edge_vars`,`:2520`)。 + +同一问题已经修过三次,每次一处:#272、未知扩展名被拒绝(`scanner.cppm:762`,原注释写着 “Two answers to 'is this a module interface' and only one of them read”)、`module_edge_vars`。 +#790 是剩下的一处。根因是扩展名同时回答“什么语言”和“什么模块角色”。GCC 按内容判断,所以 R4–R6 在 GCC 上成功。 + +### 5.2 设计(D8) + +**原则:扩展名决定语言,模块声明决定模块角色。** + +- 每个 C++ 单元的模块角色取自扫描得到的 `ModuleDeclaration`(`CompileUnit::declaration` 已存在)。 +- 规则选择、`bmi_out`、`module_lang`/`module_output`、`--expect-provides`、打包时的接口发布判断只读这一个字段。 + `pick_rule`:语言为 C++ 且角色产出 BMI(`Interface`、`InterfacePartition`、`ImplementationPartition`, + 或 `Unknown` 且声明了 provides)时用 `cxx_module`。 +- `module_extensions` 收窄为“这些扩展名是 C++ 模块源文件,默认走模块规则”;它不再是一个 `.cpp` 成为模块单元的前提。 +- 编译器参数:Clang 为 `-x c++-module` 与 `-fmodule-output=`;GCC 不变;原生 cl.exe 按下表(O5 实测,探测 PR #791,run 37974353683): + + | 角色 | `.cpp` | `.cppm`(cl 不认识的扩展名) | `.ixx` | + | --- | --- | --- | --- | + | 主接口 / 接口分区 | `/interface`(不加则 C3378) | `/interface /TP`(不加则文件被忽略,D9024) | 不需要选项 | + | 实现分区 | `/internalPartition`(不加 C7621;`/interface` 则 C3474) | `/internalPartition /TP`(`/interface /TP` 则 C3474) | — | + | 实现单元(`module m;`) | 不需要选项 | 未测 | — | + + 今天 mcpp 对所有产出 BMI 的单元使用 `/interface /TP`(`module_edge_vars`),所以原生 cl.exe 下 `.cppm` 中的实现分区今天就失败(C3474)。 + D8 让模块角色决定这一参数,同时修复这一缺陷。 +- 若 O5 测得某编译器无法以该扩展名编译某种角色,则在计划阶段拒绝,点名文件、角色与改名建议(CMake 对未放进 `CXX_MODULES` file set 的模块单元同样报错)。 + +## 6. 决定 + +| # | 决定 | 节 | +| --- | --- | --- | +| D1 | 前缀即语义:`workspace.` 之外只说本包,`workspace.` 之内只说成员;不按位置传递(W1) | §3.2 | +| D2 | 可共享的表都有 `[workspace.X]` 镜像,含 `toolchain`、`indices`、`profile`、`target.` 及其子表(W2) | §3.2 | +| D3 | 合并规则只由键决定;命名表逐键合并;条件行逐行继承(W3) | §3.2 | +| D4 | `KeyRegistry` 覆盖全部键;分层声明;一个合并函数;`RootPosition`;文档一致性检查 | §3.5 | +| D5 | 分组以 `context_for(目标 triple)` 求值,与 prepare 用同一个函数(W4) | §3.5 | +| D6 | prepare 不写 action 输出;生成的 TU 以声明单元进入扫描 | §4.2 | +| D7 | `out_dir` 按计划根目录与配置标识分开;`resolution.json` 记录每个包的 `outDir` | §4.2、§4.3 | +| D8 | 模块角色只取自模块声明 | §5.2 | +| D9 | 虚拟根上的包表警告(W7);旧位置写法在兼容模块中读取并警告 | §3.6、§3.7 | +| D10 | 发布时写出继承值,含条件行(W6) | §3.2 | +| D11 | `--toolchain` 覆盖清单声明的工具链时警告 | §8 | +| D12 | 用户定义 `__cpp_impl_coroutine` 由用户负责;mcpp 只保证 `dialect_cxxflags` 一致送达 | §9 | +| D13 | `.xlings.json` 的引导版本更新到 2026.10.8.1 | §10 | +| D14 | 修正 SPEC-004、SPEC build-plugins、docs/04、07、20、30 及中文版 | §16 | +| D15 | Windows SDK 与 vswhere 的探测加入注册表与 `%ProgramFiles%`,不再写死 `C:` | §12 | +| D16 | MSVC 探测返回结构化结果,诊断说出缺的是哪一半、查过哪里 | §12 | +| D17 | 工具链被明确指定过就永不自动换成 MinGW;什么都没指定时才改用 MinGW,并说清楚缺的是什么 | §12 | +| D18 | 显式声明的工具链永不自动切换;MSVC ABI 工具链缺 STL 或 SDK 时在计划阶段拒绝 | §12 | +| D19 | (P1、P2)glibc 2.44.3 r4 等载荷去掉硬链接,索引 CI 拒绝含硬链接的载荷 | §13.4 | +| D20 | (xlings X1)解包时硬链接创建失败则复制目标文件 | §13.4 | +| D21 | 识别 Android 宿主,用于 shell 解析与诊断;**不改**默认工具链(aarch64 仍为 llvm@23.1.3 + glibc) | §13.4 | +| D22 | 安装失败的提示按 xlings 的错误类别给出,不再一律提示“检查网络” | §13.4 | +| D23 | Planning 中显示 xlings 的阶段与进度;verbose 下每 30 s 报告仍在运行的子进程 | §13.4 | +| D24 | `[xlings.workspace]` 的共享写法为 `[workspace.xlings.workspace]`(原 O2) | §3.2 | +| D25 | 配置标识取目标 triple、profile、工具链身份与该包启用的 features(原 O3) | §4.2 | +| D26 | 生成模块接口的 action 必须声明 `.provides()`;发现未声明的写法时给一个版本的 `degraded` 过渡期(原 O4) | §4.2 | +| D27 | Termux 的 CI:termux-docker(aarch64)与 Android 模拟器沙箱(x86_64)两个 job | §13.4 | +| D28 | 子进程使用的 shell 不再写死 `/bin/sh`:依次取 `/bin/sh`、`/system/bin/sh`(Android)、`$PREFIX/bin/sh` | §13.4 | +| D29 | 工具链私有库的加载器环境只给工具链自己的进程,不给 ninja 与它的 `/bin/sh` | §13.4 | +| D30 | mcpp 依赖的 xlings 升级到包含 X1–X3 的新版本,同步所有固定点 | §14 | +| D31 | (xlings X2)解包失败按 errno 分类;失败后不再继续注册程序、不报告 installed | §13.4 | +| D32 | (xlings X3)解包阶段按进度发事件,安装钩子每 10 s 心跳 | §13.4 | + +## 7. 待定 + +- ~~O5:原生 cl.exe 对各模块角色的参数。~~ 已测(§5.2 的表):实现分区必须用 `/internalPartition`。 +- ~~O6:Windows SDK 不在常规路径时的实际行为。~~ 已测(§12.1)。 +- ~~O7:Termux 上 Planning 35 分钟。~~ 用户日志证实在 xlings 安装 musl-gcc 之中(§13.1,R4)。 +- ~~O8:Android 上 llvm + glibc 是否可用。~~ x86_64 已在 Android 14 应用沙箱中端到端验证(§13.3)。 +- ~~O9:musl 静态 LLVM。~~ 不需要(review 结论)。 +- ~~O10:termux-exec 的 linker 执行模式。~~ 未能稳定复现,撤销(§13.5)。 +- **O11:aarch64 真机验证。** GitHub 没有可运行 aarch64 Android 模拟器的 runner。X1、D29 完成后,用 PR 构建的 mcpp 在 aarch64 Termux 真机上 + 完成 `mcpp new hello && mcpp run`(llvm@23.1.3 默认),作为发布前的最后一步。 + +## 8. `--toolchain` 覆盖清单中的工具链(D11) + +**现状。** `--toolchain` 经 `MCPP_TOOLCHAIN` 进入 prepare,优先于清单的 `[toolchain]`(`toolchain.cpp:339`), +也优先于匹配的 `[target.].toolchain`(`step1_apply_target_section`,`:451`)。覆盖是静默的。 +`--toolchain` 由 CLI 写入 `MCPP_TOOLCHAIN`,用户直接设置该环境变量与写 `--toolchain` 等价,提示中写作 `--toolchain (MCPP_TOOLCHAIN)`。 + +**设计。** 当命令行给出的工具链与该构建的配置中由清单声明的工具链(本包或工作空间层,`[toolchain]` 或匹配的 `[target.].toolchain`)**不同**时,给出警告: + +``` +warning: --toolchain llvm@23.1.3 replaces gcc@16.1.0 declared at [toolchain].default (mcpp.toml) +``` + +- 相同时不提示;清单没有声明时不提示; +- 宿主工具子构建(`tcFromConsumer`)与依赖包不提示; +- 用 `diag::warning`,不是 `degraded`:用户的选择被执行了,什么也没少做,因此 `--strict` 不因此失败; +- 工作空间多配置时,每个(声明,覆盖值)组合每个进程只说一次(diag 已去重); +- 来源一列沿用 `tcSpecSource` 的措辞,指向写下它的文件与键。 + +## 9. 用户定义 `__cpp_impl_coroutine`(D12) + +**立场:这是用户自己的声明,mcpp 不做保证,也不特别处理。** 编译器已声明 32 位 x86 Microsoft ABI 上的协程不受支持; +用户定义该宏,相当于替编译器打开一个它声明不支持的功能,结果由用户负责。 + +mcpp 只负责一件事,而这件事属于 A 类修复:**用户写在 `dialect_cxxflags` 中的宏,一致地到达 std/std.compat 预编译、扫描与每个 C++ 单元。** +§3 实施后,下面两种写法都满足这一点: + +```toml +# 一个成员(或独立包) +[target.i686-windows-msvc.build] +dialect_cxxflags = ["-D__cpp_impl_coroutine=201902L"] + +# 工作空间的所有成员 +[workspace.target.i686-windows-msvc.build] +dialect_cxxflags = ["-D__cpp_impl_coroutine=201902L"] +``` + +- 写在 `cxxflags` 中时,std 模块的预编译看不到这个宏,仍会在 `` 中失败。这是 `dialect_cxxflags` 的通用规则,不是协程的特例。 +- 不探测、不验证、不增加提示;`msvc_coroutines` 的失败说明不变(它只在 std 的 `` 失败或协程用法失败时出现, + 用户定义宏之后,前一种失败不再发生)。 +- docs/20 该节保留“不推荐”,补一句:“mcpp 不对此做保证;若坚持,宏必须写在 `dialect_cxxflags` 中,原因见 docs/04 §2.1。” + +## 10. `.xlings.json` 的引导版本(D13) + +仓库的 `.xlings.json` 固定 `"mcpp": "2026.9.24.1"`,它是自举构建使用的 mcpp,落后当前版本十余个发布。按 `check_version_pins.sh` 第 (b) 条,引导版本必须是已发布、且索引已提供的版本; +2026.10.8.1 满足这一条件。发布 PR 中把它改为 `2026.10.8.1`,并运行 `check_version_pins.sh`。 + +验证(Linux x86_64,干净的 worktree,`b1652184`,删除 `.xlings.json`,默认 gcc@16.1.0): + +| 引导版本 | 结果 | +| --- | --- | +| 2026.9.24.1 | 构建成功(102 s),但给出 `warning: [test] has unsupported key 'windows_code_page' (ignored)` | +| 2026.10.8.1 | 构建成功(1 m 42 s),无警告 | + +在这台 Linux 主机上,旧引导版本“无法构建”没有复现。但它会忽略它不认识的键,得到的不是这份清单描述的构建: +上表中 Windows 的测试代码页(2026.10.5.2)被丢弃;macOS、Windows 与 aarch64 上默认的 `llvm@23.1.3` 以及 +2026.10.8.1 的工具链修复也不在旧版本中。引导版本必须理解清单中的每个键,所以更新它; +未在 Windows、macOS、aarch64 上实测旧版本的具体失败,发布 PR 的 CI 会覆盖这几个平台。 + +**对 mcpp 自身迁移的约束。** 引导版本 2026.10.8.1 会静默忽略 `[workspace.toolchain]` 等新写法(§3.6)。 +因此 mcpp 自己的 `mcpp.toml` 在本版本中**不能**改用新写法,否则自举构建会悄悄用错工具链;它保留旧写法(自举时有兼容警告), +在下一个版本把引导版本提升到本版本之后再迁移(§11)。 + +## 11. 同步更新:mcpp 自身与生态 + +| 仓库 | 受影响的部分 | 动作 | 时机 | +| --- | --- | --- | --- | +| mcpp:`mcpp.toml` | 带 `[package]` 的工作空间根,根上有 `[toolchain]`、`[target.aarch64-linux-gnu]`、`[target.x86_64-linux-musl]`、`[target.aarch64-linux-musl]` | 本版本保留旧写法:`mcpp build` 只以根包为根,不触发警告;`mcpp test -p <模块>` 这类以成员为根的命令会警告。下一个版本迁移到 `[workspace.toolchain]`、`[workspace.target.*]` | 下一个版本(§10 的约束) | +| mcpp:e2e | 10 个脚本在工作空间根上写了旧位置的表 | 保留 2 个作为兼容覆盖(断言警告与行为不变),其余迁移到新写法 | 本版本 | +| mcpp:docs、specs、示例 | docs/04、07、20、30 与中文版,SPEC-004、build-plugins | 改写(§16) | 本版本 | +| mcpp-community/mcpp-plugins | `tests/metal-consumer/check-metal.sh`、`tests/apk-consumer/check-apk-features.sh`、`tests/apk-consumer-libraries/check-apk-libraries.sh` 与 `ci.yml` 硬编码 `target/.build-mcpp/out`(D7);`rules/qt.cppm` 的 source action 只生成非模块 TU,不受 D26 影响 | 测试改为从 `resolution.json` 的 `outDir` 读取 | 与本版本同时发布 | +| openxlings/libxpkg | `build.mcpp` 使用 `mcpp::out_dir()` 接口 | 无需修改;以发布前的 CI 验证 | 本版本 | +| mcpplibs/openkal 系列(openkal、-musl、-linux、-llvm-runtime、-emscripten、-opensbi、-macos、-windows、-uefi、std-freestanding-alloc-kal) | 都不是工作空间;独立包的 `[toolchain]`、`[target..build]` 含义不变(W1) | 无需修改;以 mcpp 的 openkal workflow 验证 D6–D8 | 本版本 | +| openxlings/xim-pkgindex、xlings-res | glibc 2.44.3-r3(每个架构 256 个硬链接)、gcc 16.1.0(8)、musl-gcc 15.1.0(13)含硬链接 | P1:重新打包(glibc r4 优先,它在 llvm 默认路径上);P2:索引 CI 拒绝含硬链接的载荷 | mcpp 发布之前 | +| openxlings/xlings | 解包没有硬链接回退(2026.10.10.1 实测);`LocalWriteFailure` 一律报成磁盘满;失败后继续产生连带错误;阶段内无进度 | X1–X3(§13.4);发布新版本;mcpp 固定到它(D30) | mcpp 发布之前 | + +## 12. Windows:Visual Studio 或 Windows SDK 不在 `C:` + +### 12.1 现状(代码) + +- “MSVC 可用”定义为 STL 与 SDK 都找到(`has_usable_msvc`,`msvc.cppm:1345`)。 + - Visual Studio:`VSINSTALLDIR`、vswhere、`VS*COMNTOOLS`、常规路径。vswhere 的路径写死为 + `C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`(`msvc.cppm:1061`)。 + Visual Studio 装在 `D:` 时,安装器与 vswhere 仍在系统盘的 Program Files (x86) 下,因此通常能找到; + Windows 本身不在 `C:` 时找不到。 + - Windows SDK(`find_windows_sdk`,`msvc.cppm:1227`):`WindowsSdkDir` 环境变量、受管载荷、两个写死的路径 + `C:\Program Files (x86)\Windows Kits\10` 与 `C:\Program Files\Windows Kits\10`。**不查注册表。** + SDK 装在 `D:` 且不在开发者命令行中时,找不到。 +- **首次运行**(没有任何工具链声明、没有 `--target`、没有全局默认值)且“MSVC 不可用”时,目标改为 + `x86_64-windows-gnu`,工具链改为 MinGW,提示 “no Visual Studio found”,并**写入全局默认工具链与默认目标** + (`toolchain.cpp:404`、`:1680`)。于是: + - Visual Studio 已安装、只是 SDK 不在 `C:` 时,提示的原因是错的; + - 之后用户修好 SDK,这台机器的默认值仍是 MinGW。 +- **显式声明了工具链**(`[toolchain]`、`[target.].toolchain`、`--toolchain`、全局默认值)时,**不会**自动切换: + 回退条件要求 `tcSpec` 为空。缺 SDK 时在计划阶段拒绝(`msvc_unavailable_guidance`,`options.cpp:40`), + 但措辞不准:除了 cl.exe 一行,其余情况一律说 “MSVC STL + Windows SDK — neither was found”。 + +**实测(探测 PR #791,run 37974353683,`windows-latest`,mcpp 2026.10.8.1)。** 把 `C:\Program Files (x86)\Windows Kits\10` +移到 `C:\MovedKits\10`,并按安装器的做法改写注册表(`Installed Roots\KitsRoot10`、`Microsoft SDKs\Windows\v10.0\InstallationFolder`, +含 WOW6432Node),模拟装在其他盘的 SDK: + +| 场景 | 结果 | +| --- | --- | +| B0 对照:SDK 在原处,`[toolchain] windows = "llvm@23.1.3"` | 成功(`msvc@system → MSVC 14.51.36231 · Windows SDK 10.0.26100.0`) | +| S1 什么都没指定(新的 MCPP_HOME) | 改用 `gcc@16.1.0 → x86_64-windows-gnu`,提示 “no Visual Studio found”(实际有 Visual Studio Enterprise 2026),这条提示还打印了两次;写入全局默认工具链与默认目标;`plan 1m14s` | +| S2 `[toolchain] windows = "llvm@23.1.3"` | **不切换**;计划阶段拒绝(exit 2)。上一行已解析出 `msvc@system → MSVC 14.51.36231`,错误却说 “MSVC STL + Windows SDK — neither was found”,并建议改用 MinGW 默认值 | +| S3 `--toolchain llvm@23.1.3` | 同 S2 | + +结论: +- 明确指定时不切换、缺东西时拒绝,这两点今天已经成立; +- 问题在探测(不查注册表)与措辞(把“只缺 SDK”说成“都没找到”,把“有 Visual Studio”说成“没有”); +- 修订 3 中“缺 SDK 时静默继续”的判断是读代码得出的,实测不成立,已删除。 +- **mcpp 不会自动下载 MSVC。** 受管的 `msvc@`(`xim:msvc` 加上与之同装的 `windows-sdk`)只在用户声明时使用。 + +### 12.2 设计 + +- **D15 探测位置。** SDK 的查找顺序:`WindowsSdkDir` → 受管载荷 → 注册表 + `HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots` 的 `KitsRoot10`(含 WOW6432Node 视图,这是 SDK 安装器记录安装位置的地方) + → 由 `%ProgramFiles(x86)%`、`%ProgramFiles%` 得到的常规路径。vswhere 的路径同样由 `%ProgramFiles(x86)%` 得到。 + `mcpp doctor`、`mcpp self env` 与构建使用同一个函数。 +- **D16 结构化结果。** 探测返回 `{Visual Studio 实例, toolset/STL, SDK, 每一项查过的位置}`。 + 诊断说出缺的是哪一半,例如 “Visual Studio 2022 found at D:\VS\2022, but no Windows SDK: searched …”, + 不再在找到 Visual Studio 时说 “no Visual Studio found”。 +- **D17 只有“什么都没指定”时才可以改用 MinGW。** + - **明确指定**的含义:本包或工作空间层的 `[toolchain]`、匹配的 `[target.].toolchain`、`--toolchain`(`MCPP_TOOLCHAIN`), + 以及全局默认值(`mcpp toolchain default` 写下的值,包括以前的首次运行写下的值)。 + 任何一项存在,mcpp 就使用它,**永不**自动换成 MinGW;要换,只能由用户改 `--toolchain` 或改清单(或改全局默认值)。 + MSVC ABI 的工具链缺 STL 或 SDK 时按 D18 拒绝。 + - **什么都没指定**:可以改用 MinGW。无论是完全没有 MSVC,还是只缺一半(有 Visual Studio 但缺 SDK,或反过来),都改用 MinGW, + 写入全局默认值(与现行首次运行相同,之后的构建保持稳定,不会因为某天补齐了 SDK 而悄悄换 ABI)。 + 提示必须说清楚:找到了什么、缺了什么、查过哪里,以及补齐之后如何改用 MSVC + (`mcpp toolchain default llvm@<版本>`)。不再在找到 Visual Studio 时说 “no Visual Studio found”。 +- **D18 明确指定的工具链缺东西时拒绝。** 今天的行为(S2、S3)就是如此,把它写成不变式并加测试; + 拒绝信息改用 D16 的结构化结果,说清楚缺的是哪一半。错误列出查过的位置与选项:安装 SDK 组件;设置 `WindowsSdkDir`; + `mcpp toolchain install msvc`(受管,自带 SDK);或由用户自己改用 MinGW(`--toolchain` 或改清单)。 +- **不自动下载 MSVC。** 受管 MSVC 体积以 GB 计,且是否使用它是用户的选择;它只作为错误信息中的一个选项出现。 + +## 13. Termux(Android):跨仓库的修复(mcpp、xlings、xim-pkgindex/xlings-res) + +**目标(review 结论):** Android 上不换工具链。aarch64 默认仍是 `llvm@23.1.3` 加受管 glibc(与 GNU/Linux aarch64 相同), +x86_64 仍按 Linux 的规则;要做的是让它在 Termux 中真实跑通。 + +### 13.1 用户提供的证据 + +- **第一次运行(2026.10.8.1,llvm@23.1.3 + glibc):** `extract failed for glibc: write_header(…/libexec/getconf/POSIX_V6_LP64_OFF64): Can't create`, + 随后 llvm 的安装钩子因缺 glibc 拒绝,提示 “check free space and permissions”“check network and retry”。 +- **第二次运行(更早的 mcpp,默认 musl-gcc@15.1.0):** `~/.mcpp/log/mcpp.log` 的最后一行是 `05:06:56` 的 + `xlings: interface install_packages exec: … xim:musl-gcc@15.1.0`,此后没有任何记录;对应的 `mcpp-xlings-*.stderr` 为空。 + 初始化共 171 s(索引 130.8 s、patchelf 22.7 s、ninja 17.7 s),下载 79.2 MB 用时 178.7 s;之后 30 分钟以上全部在 xlings 的这次安装中, + mcpp 只显示 `Planning · 35:22`。 + +### 13.2 根因(全部经探测 PR #791 实测) + +| # | 根因 | 所在 | 证据 | +| --- | --- | --- | --- | +| R1 | **载荷含硬链接,Android 应用沙箱禁止创建硬链接,而 xlings 解包没有回退** | xlings `src/core/xim/extract.cpp`(`archive_write_header` 失败即返回 `LocalWriteFailure`);载荷 | 模拟器(API 34,Enforcing,`runas_app`)中 `ln` → `Permission denied`;mcpp 2026.10.8.1 自带的 xlings 与**最新的 xlings 2026.10.10.1** 都报 `extract failed for glibc … Can't create`(round 7);硬链接条目数:glibc 2.44.3-r3 两个架构各 256(253 个 `share/zoneinfo`、3 个 getconf),gcc 16.1.0 为 8,musl-gcc 15.1.0 为 13,llvm 23.1.3、gcc-runtime、libxml2、zlib、linux-headers 为 0 | +| R2 | **工具链私有库的加载器环境泄漏给 ninja 调用的宿主 shell** | mcpp(ninja 子进程的环境) | termux-docker aarch64:`CANNOT LINK EXECUTABLE "/bin/sh": …/xim-x-llvm/23.1.3/lib/aarch64-unknown-linux-gnu/libc++.so is too small`,mcpp 构建失败;同一环境中只把 `LD_LIBRARY_PATH` 加在 clang 命令上则正常(P3/P4)。x86_64 模拟器上宿主 shell 未受影响(P3 通过) | +| R3 | **mcpp 写死 `/bin/sh`** | mcpp `modules/platform/src/process.cppm` | termux-docker 镜像没有 `/bin`,mcpp 启动的每个子进程 0.2 s 内失败;Android 10 及以上真机有 `/bin -> /system/bin`,不受影响 | +| R4 | **xlings 安装在解包与钩子阶段内部没有进度** | xlings(`Extracting` 只在阶段开始时报告一次);mcpp(只显示 `Planning`) | 用户第二次运行:30 分钟以上没有任何输出 | +| R5 | **失败信息误导** | xlings `installer.cpp:785` 把所有 `LocalWriteFailure` 报成 `E_DISK_FULL` 并提示 “check free space and permissions”;解包失败后仍继续,产生 “installed but registered none of the programs” 的连带错误;mcpp 一律提示 “check network and retry” | 用户报错与模拟器复现一致 | + +R2 的位置说明它与载荷无关:任何把工具链私有库放进环境的宿主都会受影响,Android 的 bionic shell 只是最先暴露出来。 + +### 13.3 修复后能否真实跑通:已验证的部分 + +| 环境 | 做法 | 结果 | +| --- | --- | --- | +| Android 14 应用沙箱,x86_64 模拟器(round 7) | 在 runner 上按设备的绝对路径预装 llvm@23.1.3 + glibc,去掉硬链接后打包推入沙箱(即 R1 修复后的状态) | clang 23.1.3 运行;编译并运行多线程 C++ 程序(`sum=6`);**mcpp 2026.10.8.1 构建 `import std` 程序成功(2.08 s)**;runner 上构建的程序在沙箱中运行 | +| termux-docker aarch64(bionic 用户态,round 6) | mcpp 首次运行安装 llvm@23.1.3 + glibc(容器允许硬链接) | clang 运行,编译并运行多线程程序;mcpp 构建因 R2 失败;把加载器环境只给工具后正常 | +| aarch64 真机 Android | GitHub 的 arm64 runner 没有 KVM(`/dev/kvm` 不存在),不能运行 aarch64 模拟器 | 未验证;由 O11 在设备上完成 | + +结论:R1、R2 修复后,llvm@23.1.3 + glibc 在 Android 上可以真实工作;x86_64 已在真实沙箱中端到端验证,aarch64 由组件级证据支持,需真机确认。 + +### 13.4 设计 + +**xlings(openxlings/xlings)** + +- **X1(D20)硬链接回退。** `extract.cpp` 在把条目交给 `archive_write_header` 之前处理硬链接条目: + 先尝试 `create_hard_link(目标, 路径)`;若失败且 errno 为 `EPERM`、`EACCES`、`EXDEV`、`EMLINK` 或 `ENOTSUP`, + 则复制已解出的目标文件(保留权限与修改时间),跳过该条目的数据,并在该归档内只记录一次 verbose 说明。 + 其他条目仍走 libarchive。tar 规定硬链接的目标出现在前,所以复制时目标已在磁盘上;目标不存在时按归档损坏报错。 + 测试:含硬链接条目的归档,在注入“创建硬链接失败”的情况下解出的内容与权限和正常解包一致。 +- **X2(D31)解包失败的分类与止损。** + - `LocalWriteFailure` 按 errno 细分:`ENOSPC`/`EDQUOT` 才是 `E_DISK_FULL`;权限类给出路径与原因; + - 一个包解包失败后,不再继续“注册程序”等后续步骤,也不报告 `installed`,只报告这一条失败。 +- **X3(D32)阶段内进度。** `Extracting` 阶段按条目数或字节数定期发 `progress` 事件;安装钩子运行期间每 10 s 发一次心跳 + (阶段名、包名、已用时间)。接口格式不变,只是事件更密。 +- 发布一个包含 X1–X3 的 xlings 版本;mcpp 固定到它(D30,§14)。 + +**xim-pkgindex / xlings-res** + +- **P1(D19)重新打包。** glibc 2.44.3(x86_64、aarch64)发布 r4,打包时解引用硬链接(`tar --hard-dereference`); + gcc 16.1.0、musl-gcc 15.1.0/16.1.0 同样处理。zoneinfo 中被解引用的文件都很小,体积增加可忽略(实测前后数据待重新打包时记录)。 +- **P2 载荷检查。** 索引 CI 拒绝含硬链接条目的载荷,新载荷不再引入。 +- 有了 X1,P1 不是必需的;它让旧版 xlings 用户(未升级者)也能在 Android 上安装,并去掉对 X1 的依赖。见决策 Q2。 + +**mcpp** + +- **D29 加载器环境只给工具。** 工具链私有库目录不再放进 ninja 进程的环境;需要它的工具在命令中以 `env LD_LIBRARY_PATH=… ` + 前缀获得(或由 RPATH 解决,二者择一,以实现时的改动面为准)。宿主 shell 与其他命令看不到它。 +- **D28 不写死 `/bin/sh`。** 一处解析:`/bin/sh`,否则 `/system/bin/sh`,否则 `$PREFIX/bin/sh`。ninja 自身写死 `/bin/sh`, + 因此 Android 的支持基线是 Android 10 及以上(有 `/bin -> /system/bin`),写入 docs/20。 +- **D22 按失败类别提示。** 读取 xlings 返回的错误码(X2 之后可区分下载、解包、权限、磁盘、钩子),给出对应提示,不再一律 “check network and retry”。 +- **D23 Planning 不再沉默。** 把 xlings 的阶段与进度(X3)显示在进度行(例如 `Installing musl-gcc@15.1.0: extracting 41%`); + verbose 下记录每个阶段的起止;子进程超过 30 s 无事件时,verbose 每 30 s 输出 “still running”。 +- **D21(改)识别 Android 宿主,但不改默认工具链。** 判断依据:`ANDROID_ROOT` 与 `ANDROID_DATA`,或 `/system/bin/linker64` 存在。 + 用于 D28 的 shell 解析、`mcpp self env` 的报告与诊断措辞。删除 `toolchain.cpp:1555` 关于 “aarch64 → musl … ideal for Termux” 的过时注释。 +- **D27 Termux CI。** `ci-fresh-install.yml` 增加两个 job,安装已发布的 mcpp(以及 PR 构建的 mcpp)完成 `mcpp new hello`、`mcpp run`: + - termux-docker aarch64(先建 `/bin -> /system/bin`):覆盖 aarch64 的默认工具链与 R2; + - Android 14 x86_64 模拟器 + Termux APK + `run-as`:覆盖应用沙箱(R1),约 15 分钟。 + +### 13.5 其他结论 + +- **LLVM 没有 musl 静态版本**(xim-pkgindex 的 llvm 在 Linux 上全部依赖 glibc 与 gcc-runtime)。按 review 结论不需要它,原 O9 撤销。 +- **termux-exec 的 linker 执行模式(原 O10)**:round 4 中一次调用报 `has unexpected e_type: 2`;round 6、7 在同一模拟器、termux-exec 2.3.0 下, + 默认模式与关闭该模式都能运行 mcpp;用户真机也能运行。不作为阻碍,撤销 O10。 + +## 14. mcpp 依赖的 xlings 版本(D30) + +**现状。** mcpp 固定 xlings 2026.10.8.1,固定点: +- `src/xlings/xlings.cppm:117` 的 `kXlingsVersion`(`config.cppm` 的 `kXlingsPinnedVersion` 引用它;发布包内置的 xlings 与 `mcpp self env` 的 “xlings pinned” 都取自这里); +- `.github/workflows/release.yml` 的 `XLINGS_VERSION`(4 处)与 aarch64 资产名(`:396–407`); +- `.github/workflows/ci-fresh-install.yml:155` 的 `quick_install.sh … v2026.10.8.1`。 + +xlings 已发布 2026.10.9.1、2026.10.9.2、2026.10.10.1。其间的改动:SubOS 架构(#641)、xvm 对载荷未提供资产的处理(#649,2026.10.9.2)、 +Luban OS(#650,2026.10.10.1)。 + +**本地兼容性冒烟测试**(Linux x86_64):mcpp 2026.10.8.1 的发布包中替换为 xlings 2026.10.10.1,新的 MCPP_HOME, +`gcc@16.1.0` 的 `import std` hello:初始化、patchelf/ninja 引导、glibc 与 gcc 安装、构建全部成功(1 m 26 s,主要为下载)。 + +**设计。** +- 本版本把 xlings 升级到**包含 X1–X3 的新版本**(2026.10.10.1 之后的下一个发布)。2026.10.10.1 不够:round 7 在 Android 沙箱中实测, + 它解包 glibc 时与 2026.10.8.1 一样失败。 +- 若 X1–X3 赶不上 mcpp 的发布,先固定 2026.10.10.1(Linux x86_64 冒烟测试已通过),并依赖 P1(glibc r4)让 Android 的默认路径可以安装; + 这条路线的取舍见决策 Q2。 +- 所有固定点一次改齐;`check_version_pins.sh` 已扫描 `.github/` 中的 xlings 固定点,补充对 `kXlingsVersion` 的比对, + 让源码与 workflow 中的版本不能不一致。 +- `xlings.cppm` 中按版本记录“为什么需要这个版本”的注释段落增加一条,写明新版本带来的、mcpp 依赖的行为(X1 的解包回退、X2 的错误码、X3 的进度事件,以及 #641 之后 SubOS 的变化中 mcpp 用到的部分)。 +- 验证:发布 PR 的完整 CI(四个平台、openkal、交叉构建),加上 D27 的两个 Termux job。 + +## 15. 外部 PR 的处置 + +- #787:修复了“成员中的条件 dialect”(W4 的一个实例),未覆盖条件 abi、工作空间层、按值分组;按原始谓词分组会过度拆分。 +- #779:修复了 R3,未覆盖 R2;仍保留两个写入者。 +- 两者的分析并入本文;发布 PR 合入后在两个 PR 中说明、致谢并关闭。 + +## 16. 测试、规范与文档 + +**新增 e2e**(从 893 开始编号,每个都必须在 2026.10.8.1 上失败): + +| 测试 | 覆盖 | +| --- | --- | +| 893 前缀即语义 | `[workspace.toolchain]`、`[workspace.profile]`、`[workspace.target.]` 的标量行与 `.build` 到达每个成员与根包、不到达取来的依赖;命名表逐键合并;成员的同名 define 优先;两种写法并存时报错;`[workspace]` 下的未知表报错;W7 警告 | +| 894 旧位置写法的兼容 | 旧写法的行为与 2026.10.8.1 相同;虚拟根总是警告;带 `[package]` 的根只在成员经由旧规则取值时警告,只构建根包时不警告 | +| 895 配置中的条件值 | R1b;只在其他目标上不同的两个成员在同一张图中;条件 dialect 进入 std 缓存键 | +| 896 action 输出只有一个写入者 | R3;configure-only 后不留文件;已有输出(含合法的空文件)不被改动 | +| 897 每个配置有自己的生成输出 | R2;`resolution.json` 的 `outDir` | +| 898 `.cpp` 中的模块单元 | R4 第二次构建无工作;R5、R6 在 Clang 与 GCC 上都成功 | +| 899 `--toolchain` 覆盖提示 | 不同时警告并指向文件与键;相同、未声明、宿主工具时不提示;`--strict` 不失败 | +| 900 发布写出继承值 | `mcpp publish --dry-run` 的清单含 `[target..build]` 与 `[toolchain]` 的继承值 | +| 901 加载器环境不进入宿主 shell | 一个 source action 经 `/bin/sh` 打印 `LD_LIBRARY_PATH`,其中不含工具链私有库目录(D29) | + +Windows 探测(D15–D18)与 Android 识别(D21)改动的是系统环境,不适合写成 e2e: +- 单元测试:注册表与环境变量的读取通过注入的读取函数测试;各种“缺一半”的组合对应的选择与诊断;Android 判断;shell 的解析顺序(D28)。 +- CI:D27 的两个 Termux job。 +- O6 的探测 PR 提供真实环境下的证据。 + +**单元测试**(A–C 类):`KeyRegistry` 的完整性(每个解析器认识的键都有一行);合并规则;`RootPosition` 的序列化与求值; +兼容模块的转换;声明单元的构造。 + +**规范与文档**: +- SPEC-004(`docs/specs/manifest-semantics.md`):§2 术语(构建的配置);§9 第 7 条(发布写出条件行)、第 8 条(xlings)、 + 第 10 条(W1–W4);新增 W7 与旧写法迁移。 +- `docs/specs/build-plugins.md`:单一写入者;`out_dir` 按配置;`resolution.json` 的 `outDir`。 +- docs/04、docs/07(§4 改写为“前缀即语义”,删除 “There is no `[workspace.target.]`” 与按位置继承的表述)、 + docs/20(§9;Windows SDK 探测与首次运行;Android 默认值)、docs/30(占位文件与 `out_dir`),以及对应的 `docs/zh/`。 + +## 17. 自审记录 + +修订 2 写成后逐条核对,改动如下: + +| 发现 | 处理 | +| --- | --- | +| 初稿假定旧版 mcpp 会拒绝未知的 `[workspace.*]` 表;实测 2026.10.8.1 静默忽略 `[workspace.toolchain]` | §3.6 改为依赖 `mcpp` 下限,加 note;本版本起未知表报错 | +| 初稿称 `cxxflags` 中的宏会被方言检查拒绝;该检查只针对特定方言参数 | §9 改为如实描述后果:std 预编译看不到宏 | +| 初稿称“只在其他平台不同的成员不再被拆分”,但今天它们本来就没有被拆分(条件行完全被忽略) | §3.6 改为描述新规则下的结果 | +| `--toolchain` 实际经由 `MCPP_TOOLCHAIN` 进入 | §8 写明两者等价及提示措辞 | +| 命名表改为逐键合并会改变 `[profile]`、`[target.]` 的现有整表替换语义 | 只用于新写法;旧位置写法在兼容期内保持整表替换(§3.6) | + +修订 3 的自审: + +| 发现 | 处理 | +| --- | --- | +| 引导版本 2026.10.8.1 静默忽略新写法,mcpp 自己的 `mcpp.toml` 若在本版本迁移,自举构建会用错工具链 | mcpp 自身推迟到下一个版本迁移(§10、§11) | +| openkal 系列都不是工作空间 | 不需要迁移,只做验证(§11) | +| 带 `[package]` 的根上,旧位置的表首先是根包自己的声明;无条件警告会让 mcpp 自己的每次构建都报警告 | 警告只在成员经由旧规则取值时给出(§3.6) | +| mcpp-plugins 的测试硬编码生成目录 | 新增 `resolution.json` 的 `outDir` 作为稳定出口(D7) | +| Windows:显式声明时不会切换(回退条件要求 `tcSpec` 为空),但缺 SDK 时静默继续 | D18 改为在计划阶段拒绝 | +| Termux 第一次失败的硬链接原因只在 x86_64 r1 载荷上核对过 | §13.1 写明 aarch64 r3 需在重新打包时核对 | +| Termux 第二次运行用的很可能不是 2026.10.8.1 | 列为 O7,结论不基于推测 | + +修订 4 的自审: + +| 发现 | 处理 | +| --- | --- | +| 修订 3 依据读代码称“明确指定的工具链缺 SDK 时静默继续”;实测为计划阶段拒绝 | §12.1 改为实测结果;D18 改为“保持并写成不变式,修正措辞” | +| 修订 3 以为换成 musl gcc(D21)即可规避 Android 的问题;实测 musl-gcc 载荷同样含硬链接 | §13.2 的 R1:硬链接是所有受管工具链的共同问题(修订 5 起不再换工具链) | +| termux-docker 镜像没有 `/bin`,与 Android 10+ 真机不同 | CI 中先建 `/bin -> /system/bin`;同时修 mcpp 写死 `/bin/sh`(D28) | +| 模拟器会话 A 中静态程序无法执行,但用户真机能运行 mcpp | 不据此下结论,列为 O10 | +| cl.exe 表中“`.cppm` 实现单元用 `/TP`”未测 | 表中标为“未测” | +| xlings 2026.10.10.1 的兼容性只在 Linux x86_64 上冒烟测试过 | D30 以发布 PR 的完整 CI 验证 | + +修订 5 的自审: + +| 发现 | 处理 | +| --- | --- | +| 修订 4 以“换成 musl gcc”规避 Android 问题,与 review 结论(保持 llvm 23)不符,且 musl-gcc 载荷同样含硬链接 | D21 改为只识别不改默认;§13 改为修复根因 | +| “直接用最新的 xlings”是否够用,修订 4 只是推测 | round 7 实测 2026.10.10.1 同样失败,并在 xlings 源码中定位(`extract.cpp`、`installer.cpp:785`) | +| R2 只在 aarch64 termux-docker 中出现,x86_64 模拟器的宿主 shell 未受影响 | 两处都写明;修法相同,不依赖于哪种 shell 会失败 | +| x86_64 的端到端验证用了预装、去硬链接的载荷,而不是 xlings 实际安装 | §13.3 写明这是“R1 修复后的状态”;X1 发布后由 D27 的模拟器 job 以真实安装复验 | +| aarch64 没有真实 Android 环境的端到端证据 | O11:发布前在真机上验证 | + +尚未消除的风险: +- `KeyRegistry` 覆盖全部键,是本版本改动最大的部分;完整性由单元测试保证(每个解析器认识的键都必须有一行)。 +- X1–X3、P1、P2 在其他仓库中完成,需要在 mcpp 发布前合入并发布。 +- aarch64 真机只能在设备上验证(O11)。 + +## 18. 实施 + +跨仓库的顺序(前一步完成才能做后一步的,用箭头标出): + +``` +xlings:X1、X2、X3 → 发布 xlings ─┐ +xim-pkgindex / xlings-res:P1(glibc r4 优先)、P2 ─┤ + ├→ mcpp 发布 PR(release/)→ 发布 → 生态验证 +mcpp-plugins:测试改读 resolution.json 的 outDir ──┘ (含 O11 真机验证) +``` + +mcpp 发布 PR 内部顺序: + +1. D13、D30(引导版本与 xlings 版本;D30 等 xlings 发布); +2. D4 的登记表与分层、兼容模块(其余 A 类工作的基础); +3. D1–D3、D5、D9、D10、D24; +4. D8; +5. D6、D7、D25、D26; +6. D11、D15–D18;D21–D23、D28、D29;D27 的两个 CI job; +7. 规范与文档。 + +探测 PR #791 保留到本版本发布,用于复测 X1 之后的真实安装与 D27 的 job 原型。O11 在发布前完成。 diff --git a/.agents/docs/2026-10-10-one-source-one-meaning-repro.sh b/.agents/docs/2026-10-10-one-source-one-meaning-repro.sh new file mode 100644 index 000000000..58dbbaafa --- /dev/null +++ b/.agents/docs/2026-10-10-one-source-one-meaning-repro.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# Reproductions for .agents/docs/2026-10-10-one-source-one-meaning-design.md. +# Prints what each case observes; asserts nothing. +# +# MCPP= bash 2026-10-10-one-source-one-meaning-repro.sh +# +# R1 conditional dialect_cxxflags in three positions (#786, #785) +# R2 action output shared by two profiles (out_dir has no configuration) +# R3 #778 on Linux: member target cleared, workspace ninja log kept +# R4 #790: implementation partition in a .cpp, rebuilt every time +# R5 interface unit (`export module`) in a .cpp +# R6 implementation partition in a .cpp imported by the interface +set -u +MCPP="${MCPP:-mcpp}" +LLVM="${LLVM:-llvm@23.1.3}" +T=$(mktemp -d) +echo "mcpp: $("$MCPP" --version 2>&1 | head -1) work: $T" +binrun() { local b; b=$(find target -path '*/bin/*' -name "$1" -type f -newer "$2" 2>/dev/null | xargs -r ls -t | head -1); [ -n "$b" ] && "$b" || echo "(no binary)"; } + +# ── R1 ──────────────────────────────────────────────────────────────── +main_dial() { cat > "$1" <<'EOF' +#include +#ifndef DIAL +#define DIAL 0 +#endif +int main() { std::printf("DIAL=%d\n", DIAL); } +EOF +} +app_toml() { cat < "$T/r1/ws-root/mcpp.toml" +app_toml "" > "$T/r1/ws-root/app/mcpp.toml"; main_dial "$T/r1/ws-root/app/main.cpp" +printf '[workspace]\nmembers = ["app"]\n' > "$T/r1/member/mcpp.toml" +app_toml "$COND2" > "$T/r1/member/app/mcpp.toml"; main_dial "$T/r1/member/app/main.cpp" +app_toml "$COND2" > "$T/r1/standalone/mcpp.toml"; main_dial "$T/r1/standalone/main.cpp" +for c in ws-root member; do + (cd "$T/r1/$c" && "$MCPP" build -p app > build.log 2>&1; echo "R1 $c: $(binrun app mcpp.toml) warnings: $(grep -ci warn build.log)") +done +(cd "$T/r1/standalone" && "$MCPP" build > build.log 2>&1; echo "R1 standalone: $(binrun app mcpp.toml)") + +# ── R2 ──────────────────────────────────────────────────────────────── +mkdir -p "$T/r2/src"; cd "$T/r2" +cat > mcpp.toml <<'EOF' +[package] +name = "gp" +version = "0.1.0" +[build] +sources = ["src/main.cpp"] +[targets.gp] +kind = "bin" +main = "src/main.cpp" +EOF +cat > src/main.cpp <<'EOF' +#include +const char* gen_profile(); +int main() { std::printf("GEN=%s\n", gen_profile()); } +EOF +printf '#!/bin/sh\nprintf '"'"'const char* gen_profile() { return "%%s"; }\\n'"'"' "$1" > "$2"\n' > gen.sh +cat > build.mcpp <<'EOF' +import std; +import mcpp; +int main() { + const auto root = std::filesystem::path(mcpp::manifest_dir()); + const auto out = (std::filesystem::path(mcpp::out_dir()) / "gen.cpp").generic_string(); + const auto gen = (root / "gen.sh").generic_string(); + mcpp::action a; + a.id = "gen"; + a.role = mcpp::roles::source; + a.arg("sh").arg(gen.c_str()).arg(mcpp::profile()).arg(out.c_str()) + .input(gen.c_str()).output(out.c_str()).submit(); + return 0; +} +EOF +for p in "" --release ""; do + "$MCPP" build $p > build.log 2>&1 || tail -5 build.log + echo "R2 build ${p:-(dev)}: $(binrun gp gen.sh)" +done +echo "R2 out_dir: $(find target -name gen.cpp | head -3 | tr '\n' ' ')" + +# ── R3 ──────────────────────────────────────────────────────────────── +mkdir -p "$T/r3/app"; cd "$T/r3" +printf '[workspace]\nmembers = ["app"]\n' > mcpp.toml +cat > app/mcpp.toml <<'EOF' +[package] +namespace = "repro" +name = "app" +version = "0.0.1" +[build] +sources = [] +[targets.resource_test] +kind = "bin" +main = "main.cpp" +EOF +printf 'extern const char* resource();\nint main() { return resource()[0] == '"'"'R'"'"' ? 0 : 1; }\n' > app/main.cpp +printf '#!/bin/sh\nprintf '"'"'const char* resource() { return "RESOURCE"; }\\n'"'"' > "$1"\n' > app/gen.sh +cat > app/build.mcpp <<'EOF' +import std; +import mcpp; +int main() { + const auto root = std::filesystem::path(mcpp::manifest_dir()); + const auto out = (std::filesystem::path(mcpp::out_dir()) / "generated.cpp").generic_string(); + const auto gen = (root / "gen.sh").generic_string(); + mcpp::action a; + a.id = "generate:resource"; + a.role = mcpp::roles::source; + a.arg("sh").arg(gen.c_str()).arg(out.c_str()).input(gen.c_str()).output(out.c_str()).submit(); + return 0; +} +EOF +"$MCPP" build -p app --release > b1.log 2>&1; echo "R3 first: rc=$? size=$(stat -c %s app/target/.build-mcpp/out/generated.cpp 2>/dev/null)" +mv app/target saved-app-target +"$MCPP" build -p app --release > b2.log 2>&1; echo "R3 after clearing member target: rc=$? size=$(stat -c %s app/target/.build-mcpp/out/generated.cpp 2>/dev/null) $(grep -m1 -o 'undefined reference[^)]*' b2.log)" + +# ── R4–R6 ───────────────────────────────────────────────────────────── +mod_pkg() { # dir, extra-for-repro.cppm +mkdir -p "$1/src" +cat > "$1/mcpp.toml" < "$1/src/repro.cppm" +printf 'export module repro:api;\nexport auto answer() -> int;\n' > "$1/src/api.cppm" +printf 'module repro:api_impl;\nimport :api;\nauto answer() -> int { return 42; }\n' > "$1/src/api_impl.cpp" +printf 'import repro;\nimport std;\nauto main() -> int { std::println("{}", answer()); }\n' > "$1/src/main.cpp" +} +mod_pkg "$T/r4" ""; cd "$T/r4" +for i in 1 2 3; do "$MCPP" build --verbose > b$i.log 2>&1; done +echo "R4 build 2: $(grep -cE '^\[[0-9]+/' b2.log) steps; build 3: $(grep -cE '^\[[0-9]+/' b3.log) steps" +echo "R4 pcm files: $(find target -name '*.pcm' -path '*pcm.cache*' -printf '%f ' 2>/dev/null)" + +mod_pkg "$T/r6" "import :api_impl;"; cd "$T/r6" +"$MCPP" build > b.log 2>&1; echo "R6 import of the .cpp partition: rc=$? $(grep -m1 -o "fatal error: .*" b.log)" + +mkdir -p "$T/r5/src"; cd "$T/r5" +cat > mcpp.toml < int { return 42; }\n' > src/r5.cpp +printf 'import r5;\nimport std;\nauto main() -> int { std::println("{}", answer()); }\n' > src/main.cpp +"$MCPP" build > b1.log 2>&1; r1=$?; "$MCPP" build --verbose > b2.log 2>&1 +echo "R5 interface in .cpp: rc=$r1 $(grep -m1 -oE '(error|fatal error): .*' b1.log) second build: $(grep -cE '^\[[0-9]+/' b2.log) steps" diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 588c89854..c0188c8fa 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 --- ``` -329 records. +330 records. ## By subject @@ -30,6 +30,7 @@ Records that declare one. Everything else is listed by date below. ### design +- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-design.md) — proposed - [2026.10.5.3 发布方案:32 位 x86 的架构词汇、资源编译器的识别与增量(#776 后续)](2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md) — active - [下一个版本的发布方案:标准库模块、原生 MSVC LTO 与导出发现、共享库的链接配置、Windows 参数引号(#768 后续、#770、#771)](2026-10-05-std-module-pair-msvc-lto-and-export-discovery-design.md) — active - [`mcpp run` hands the terminal to the program, and the follow-ups of #761, #763 and #765 (#766)](2026-10-05-run-terminal-handoff-and-766-follow-ups-design.md) — landed @@ -123,6 +124,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-10 +- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-design.md) — proposed - [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 - [2026.10.5.3 发布方案:32 位 x86 的架构词汇、资源编译器的识别与增量(#776 后续)](2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md) — active diff --git a/.xlings.json b/.xlings.json index a48c6307c..4aab7d297 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.9.24.1" + "mcpp": "2026.10.8.1" } } diff --git a/mcpp.toml b/mcpp.toml index 8ae9d310d..ae46ea440 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.8.1" +version = "2026.10.10.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index db19fd795..dc7eef9bd 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.8.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.10.1"; } // namespace mcpp From 7f863faf689cb3f5085b36cb7a3eeb06c9fbd502 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:01:10 +0900 Subject: [PATCH 02/32] feat(workspace): the prefix carries the meaning -- [workspace.X] layer, the position reading in a compat module, W7, grouping by evaluated rows --- modules/manifest/src/compat.cppm | 6 + .../src/compat/workspace_position.cppm | 143 ++++++++ modules/manifest/src/manifest.cppm | 3 + modules/manifest/src/toml.cppm | 207 ++++++++++- modules/manifest/src/types.cppm | 48 ++- src/build/prepare/config.cpp | 9 +- src/build/prepare/manifest.cpp | 34 ++ src/cli/cmd_build.cppm | 6 +- src/cli/cmd_publish.cppm | 2 +- src/cli/selection.cppm | 16 +- src/project.cppm | 342 ++++++++++++++---- 11 files changed, 721 insertions(+), 95 deletions(-) create mode 100644 modules/manifest/src/compat/workspace_position.cppm diff --git a/modules/manifest/src/compat.cppm b/modules/manifest/src/compat.cppm index 9f1f7aaf8..ebe915fbb 100644 --- a/modules/manifest/src/compat.cppm +++ b/modules/manifest/src/compat.cppm @@ -14,6 +14,12 @@ // ├─────────────────────────────────────┼──────────────────────────────┤ // │ [dependencies] │ [dependencies.mcpplibs] │ // │ "mcpplibs.cmdline" = "0.0.2" │ cmdline = "0.0.2" │ +// ├─────────────────────────────────────┼──────────────────────────────┤ +// │ on a workspace root: [toolchain], │ [workspace.toolchain], │ +// │ [indices], [profile.], the rows │ [workspace.indices], ..., │ +// │ of [target.], [xlings] given to │ [workspace.target.], │ +// │ members by position │ [workspace.xlings] │ +// │ (compat/workspace_position.cppm) │ (SPEC-004 §9.10) │ // └─────────────────────────────────────┴──────────────────────────────┘ // // See also: mcpp.pm.dep_spec (kDefaultNamespace definition). diff --git a/modules/manifest/src/compat/workspace_position.cppm b/modules/manifest/src/compat/workspace_position.cppm new file mode 100644 index 000000000..9cdc99d8e --- /dev/null +++ b/modules/manifest/src/compat/workspace_position.cppm @@ -0,0 +1,143 @@ +// mcpp.pm.compat.workspace_position - the workspace root's own tables, given +// to its members by POSITION. +// +// COMPAT(workspace-position): removed with the rest of mcpp.pm.compat in +// mcpp 1.0.0 (DEPRECATION SCHEDULE in compat.cppm). +// +// Before 2026.10.10.1 a workspace root's `[toolchain]`, `[indices]`, +// `[profile.]`, the scalar rows of `[target.]` and its +// `[xlings]` / `[target..xlings]` entries reached every member because +// of WHERE they were written, while `[target..build]` beside them reached +// none (#785, #786). From 2026.10.10.1 the prefix carries the meaning: a +// table outside `workspace.` speaks about its own package, and +// `[workspace.X]` speaks to every member (SPEC-004 §9.10, W1-W3). +// +// Deprecated: Canonical: +// +// [toolchain] [workspace.toolchain] +// default = "llvm@23.1.3" default = "llvm@23.1.3" +// +// [target.x86_64-linux-musl] [workspace.target.x86_64-linux-musl] +// linkage = "static" linkage = "static" +// +// This module is the only place that knows the old reading. It answers which +// tables of a workspace root the old reading still applies to (those written +// at the root position and not also as `[workspace.X]`) and applies the old +// merge to them unchanged: a member's own `[toolchain]` replaces the root's +// whole, a `[target.]` or `[profile.]` row of the same name +// replaces the root's row whole, and `[indices]` is taken only by a member +// that declares none. The caller reports each use as a warning +// (`manifest/workspace-position`) with the canonical spelling. +// +// Exported through the `mcpp.manifest` umbrella rather than the +// `mcpp.pm.compat` facade: it reads `Manifest`, and `mcpp.manifest.types` +// imports that facade. + +export module mcpp.pm.compat.workspace_position; + +import std; +import mcpp.manifest.types; + +export namespace mcpp::pm::compat { + +// The tables of workspace root `ws` the position reading applies to, in a +// fixed order: `toolchain`, `indices`, `profile`, `target`, `xlings` -- +// each written at the root position and not also as `[workspace.X]`. +// `target` covers the scalar rows; its `.xlings` rows travel with `xlings`'s +// merge but are named `target` here. +std::vector position_tables(const mcpp::manifest::Manifest& ws) { + std::vector out; + if (!ws.workspace.present) return out; + auto inLayer = [&](std::string_view t) { + return std::ranges::find(ws.workspace.layerTables, t) != ws.workspace.layerTables.end(); + }; + auto add = [&](std::string_view t, bool written) { + if (written && !inLayer(t)) out.emplace_back(t); + }; + add("toolchain", !ws.toolchain.byPlatform.empty() || !ws.toolchain.bootstrap.empty()); + add("indices", !ws.indices.empty()); + add("profile", !ws.profiles.empty()); + const bool targetXlings = std::ranges::any_of(ws.conditionalConfigs, [](auto const& cc) { + return !cc.xlings.deps.empty() || !cc.xlings.overrides.empty(); + }); + // Every `[target.]` header makes a row; one with no scalar written + // (only `.build`, say) gives a member nothing by position. + const bool targetScalars = std::ranges::any_of(ws.targetOverrides, [](auto const& kv) { + auto const& e = kv.second; + return !e.toolchain.empty() || !e.linkage.empty() || !e.cxxRuntime.empty() + || e.sysrootDeclared || e.minApiLevel != 0 || !e.runner.empty() + || !e.namedRunners.empty(); + }); + add("target", targetScalars || targetXlings); + add("xlings", !ws.xlings.deps.empty() || !ws.xlings.overrides.empty()); + return out; +} + +// The position reading of `toolchain`, `indices`, `profile` and `target` +// (scalar rows) for `member` as the root of a build, for the tables in +// `tables`. Returns the tables that gave the member a value. +// +// `wsRoot` anchors a relative `[indices].path`, which was written against the +// workspace root (#224). +std::vector +inherit_root_position_by_position(mcpp::manifest::Manifest& member, + const mcpp::manifest::Manifest& ws, + const std::filesystem::path& wsRoot, + std::span tables) { + std::vector used; + auto wanted = [&](std::string_view t) { return std::ranges::find(tables, t) != tables.end(); }; + // COMPAT(workspace-position): [toolchain] -- whole, when the member has none. + if (wanted("toolchain") && member.toolchain.byPlatform.empty()) { + member.toolchain = ws.toolchain; + for (auto const& [platform, _] : ws.toolchain.byPlatform) + member.toolchain.fileByPlatform.try_emplace(platform, ws.sourcePath); + used.emplace_back("toolchain"); + } + // COMPAT(workspace-position): [target.] -- per triple, whole. + if (wanted("target")) { + bool any = false; + for (auto const& [triple, entry] : ws.targetOverrides) + if (member.targetOverrides.try_emplace(triple, entry).second) any = true; + if (any) used.emplace_back("target"); + } + // COMPAT(workspace-position): [profile.] -- per name, whole. + if (wanted("profile")) { + bool any = false; + for (auto const& [name, profile] : ws.profiles) + if (member.profiles.try_emplace(name, profile).second) any = true; + if (any) used.emplace_back("profile"); + } + // COMPAT(workspace-position): [indices] -- all of them, when the member has none. + if (wanted("indices") && member.indices.empty() && !ws.indices.empty()) { + member.indices = ws.indices; + for (auto& [_, idx] : member.indices) + if (idx.is_local() && idx.path.is_relative()) + idx.path = (wsRoot / idx.path).lexically_normal(); + used.emplace_back("indices"); + } + return used; +} + +// What the warning for one table says. +std::string position_warning(std::string_view table, std::string_view member) { + std::string_view example = + table == "toolchain" ? "[workspace.toolchain]\n default = \"llvm@23.1.3\"" + : table == "indices" ? "[workspace.indices]\n acme = { path = \"vendor/index\" }" + : table == "profile" ? "[workspace.profile.release]\n opt = 3" + : table == "target" ? "[workspace.target.x86_64-linux-musl]\n linkage = \"static\"" + : "[workspace.xlings.workspace]\n cmake = \"4.0.2\""; + const std::string mirror = table == "profile" ? "[workspace.profile.]" + : table == "target" ? "[workspace.target.]" + : std::format("[workspace.{}]", table); + const std::string original = table == "profile" ? "[profile.]" + : table == "target" ? "[target.]" + : std::format("[{}]", table); + return std::format( + "{} on the workspace root reaches {} by its position; this reading is " + "deprecated and is removed in mcpp 1.0.0. Write it as {} to give it to every " + "member, for example:\n {}", + original, member.empty() ? std::string("every member") : std::format("member '{}'", member), + mirror, example); +} + +} // namespace mcpp::pm::compat diff --git a/modules/manifest/src/manifest.cppm b/modules/manifest/src/manifest.cppm index a213459cd..dc67805e9 100644 --- a/modules/manifest/src/manifest.cppm +++ b/modules/manifest/src/manifest.cppm @@ -11,6 +11,8 @@ // mcpp.manifest.toml mcpp.toml parsing (projects / packages on disk) // mcpp.manifest.xpkg xpkg .lua `mcpp = {}` segment (index descriptors) // mcpp.manifest.flag_words the words an element of a compile-flag list stands for +// mcpp.pm.compat.workspace_position COMPAT(workspace-position): a workspace +// root's own tables given to members by position export module mcpp.manifest; @@ -19,3 +21,4 @@ export import mcpp.manifest.toml; export import mcpp.manifest.xpkg; export import mcpp.manifest.flag_words; export import mcpp.manifest.cfg_selector; +export import mcpp.pm.compat.workspace_position; diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 626ebb2e1..576c1323a 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -198,6 +198,10 @@ export namespace mcpp::manifest { // message can name both files. struct LoadContext { bool insideWorkspace = false; + // The text is the body of a workspace root's `[workspace.toolchain]`, + // `[workspace.target.*]`, ... (the workspace layer): it has no + // `[package]`, and it states nothing about one. + bool workspaceLayer = false; }; // ─── `[xlings]`: mcpp's surface for xlings' local project mechanism ───────── @@ -877,6 +881,12 @@ std::optional sysroot_refusal(std::string_view triple, std::string_ } // namespace +namespace { +std::expected parse_document(const t::Document& document, + const std::filesystem::path& origin, + LoadContext ctx); +} + std::expected parse_string(std::string_view content, const std::filesystem::path& origin, LoadContext ctx) { @@ -884,6 +894,18 @@ std::expected parse_string(std::string_view content, if (!doc) { return std::unexpected(error(origin, doc.error().message, doc.error().where)); } + return parse_document(*doc, origin, ctx); +} + +namespace { +// The reader of one manifest document. `parse_string` gives it the text's +// document; the workspace layer gives it the document under `workspace.` +// (`[workspace.toolchain]` read as `[toolchain]`, ...), whose values keep +// the lines they were written on. +std::expected parse_document(const t::Document& document, + const std::filesystem::path& origin, + LoadContext ctx) { + const t::Document* doc = &document; // Closed-grammar guard: reject any array-of-tables whose dotted path // isn't explicitly allowlisted, BEFORE any section is read. See @@ -927,11 +949,11 @@ std::expected parse_string(std::string_view content, // [package] — required unless [workspace] is present (virtual workspace). auto* pkg_t = doc->get_table("package"); bool has_workspace = (doc->get_table("workspace") != nullptr); - if (!pkg_t && !has_workspace) + if (!pkg_t && !has_workspace && !ctx.workspaceLayer) return std::unexpected(error(origin, "missing required [package] section")); auto name = doc->get_string("package.name"); - if (!name && !has_workspace && !ctx.insideWorkspace) + if (!name && !has_workspace && !ctx.insideWorkspace && !ctx.workspaceLayer) return std::unexpected(error(origin, "missing required field 'package.name'")); if (name) m.package.name = *name; @@ -941,7 +963,7 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_string("package.namespace")) m.package.namespace_ = *v; auto version = doc->get_string("package.version"); - if (!version && !has_workspace && !ctx.insideWorkspace) + if (!version && !has_workspace && !ctx.insideWorkspace && !ctx.workspaceLayer) return std::unexpected(error(origin, "missing required field 'package.version'")); if (version) m.package.version = *version; @@ -1094,10 +1116,20 @@ std::expected parse_string(std::string_view content, if (auto it = tt.find("opt"); it != tt.end()) { if (it->second.is_string()) pr.optLevel = it->second.as_string(); else if (it->second.is_int()) pr.optLevel = std::to_string(it->second.as_int()); + pr.optDeclared = it->second.is_string() || it->second.is_int(); + } + if (auto it = tt.find("debug"); it != tt.end() && it->second.is_bool()) { + pr.debug = it->second.as_bool(); + pr.debugDeclared = true; + } + if (auto it = tt.find("lto"); it != tt.end() && it->second.is_bool()) { + pr.lto = it->second.as_bool(); + pr.ltoDeclared = true; + } + if (auto it = tt.find("strip"); it != tt.end() && it->second.is_bool()) { + pr.strip = it->second.as_bool(); + pr.stripDeclared = true; } - if (auto it = tt.find("debug"); it != tt.end() && it->second.is_bool()) pr.debug = it->second.as_bool(); - if (auto it = tt.find("lto"); it != tt.end() && it->second.is_bool()) pr.lto = it->second.as_bool(); - if (auto it = tt.find("strip"); it != tt.end() && it->second.is_bool()) pr.strip = it->second.as_bool(); 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()) @@ -4436,6 +4468,168 @@ std::expected parse_string(std::string_view content, } } } + + // [workspace.toolchain|indices|profile|target|xlings] — THE WORKSPACE + // LAYER (SPEC-004 §9.10, W1-W3): what every member receives. The + // document under `workspace.` is read by this same reader, so each + // mirror has the keys, subtables and selectors of the table it + // mirrors; what it refuses is stated below, once. + { + const auto* ws = doc->get_table("workspace"); + static constexpr std::string_view kWorkspaceKeys[] = { + "members", "exclude", "package", "build", "dependencies", + "toolchain", "indices", "profile", "target", "xlings", + }; + static constexpr std::string_view kMirrored[] = { + "toolchain", "indices", "profile", "target", "xlings", + }; + // An unknown table under [workspace] used to be dropped without a + // word: a workspace that looked configured and was not, the defect + // a propagating table is refused for (docs/07 §4.1). + for (auto const& [key, value] : *ws) { + (void)value; + if (std::ranges::find(kWorkspaceKeys, key) != std::end(kWorkspaceKeys)) continue; + return std::unexpected(error(origin, std::format( + "[workspace] has no key or table '{}'. It holds members, exclude, " + "and the tables members receive: [workspace.package], " + "[workspace.build], [workspace.dependencies], [workspace.toolchain], " + "[workspace.indices], [workspace.profile.], " + "[workspace.target.], [workspace.xlings]", key))); + } + t::Table layerRoot; + for (auto k : kMirrored) { + auto it = ws->find(k); + if (it == ws->end()) continue; + layerRoot.emplace(std::string(k), it->second); + m.workspace.layerTables.emplace_back(k); + } + if (!layerRoot.empty()) { + // Keys that describe one package are not shared: a member's + // targets, dependencies and the safety gates it turns off are + // its own (W2). + if (auto it = layerRoot.find("target"); it != layerRoot.end() && it->second.is_table()) { + static constexpr std::string_view kPackageOnly[] = { + "targets", "dependencies", "dev-dependencies", "build-dependencies", + "requires_abi", "feature-requires-abi", + }; + for (auto const& [sel, row] : it->second.as_table()) { + if (!row.is_table()) continue; + for (auto const& [key, value] : row.as_table()) { + if (std::ranges::find(kPackageOnly, key) != std::end(kPackageOnly)) + return std::unexpected(error(origin, std::format( + "[workspace.target.{}] cannot hold `{}`: it describes one " + "package. Write it in the member's own [target.{}].", + sel, key, sel), value.position)); + if (key == "build" && value.is_table() + && value.as_table().contains("allow_host_libs")) + return std::unexpected(error(origin, std::format( + "[workspace.target.{}.build] allow_host_libs is not " + "inheritable. It disables the hermetic-link check for a " + "specific artifact, so it belongs in that package's own " + "table where the person turning it off owns the result.", + sel), value.position)); + // The keys of a shared `.build` are those of + // [workspace.build]: sources and per-glob flags + // name one package's files. + if (key == "build" && value.is_table()) { + for (auto const& [bk, bv] : value.as_table()) { + if (std::ranges::any_of(kWorkspaceBuildKeys, + [&](auto const& row) { return row.key == bk; })) + continue; + std::string supported; + for (auto const& row : kWorkspaceBuildKeys) + supported += std::format("{}{}", supported.empty() ? "" : ", ", + row.key); + return std::unexpected(error(origin, std::format( + "[workspace.target.{}.build] has no key '{}' (or it is " + "not inheritable). Supported: {}.", sel, bk, supported), + bv.position)); + } + } + } + } + } + if (auto it = layerRoot.find("xlings"); it != layerRoot.end() + && it->second.is_table() && it->second.as_table().contains("subos")) + return std::unexpected(error(origin, + "[workspace.xlings] cannot hold `subos`: the workspace root's own " + "[xlings] subos is the one every member already runs in.", + it->second.position)); + // On a workspace root without [package], the root-position + // table and its mirror would both speak to the members. + if (!pkg_t) { + for (auto const& name : m.workspace.layerTables) { + bool both = false; + if (name == "xlings") { + if (auto* x = doc->get_table("xlings")) + both = std::ranges::any_of(*x, [](auto const& kv) { + return kv.first != "subos"; + }); + } else { + both = doc->get(name) != nullptr; + } + if (both) + return std::unexpected(error(origin, std::format( + "[{0}] and [workspace.{0}] both state what every member " + "receives; write it once, as [workspace.{0}]", name))); + } + } + std::set> explicitTables; + for (auto const& e : doc->explicit_tables()) { + if (!e.starts_with("workspace.")) continue; + auto rest = std::string_view(e).substr(std::string_view("workspace.").size()); + for (auto k : kMirrored) + if (rest == k || rest.starts_with(std::string(k) + ".")) + explicitTables.emplace(rest); + } + const t::Document layerDoc(std::move(layerRoot), std::move(explicitTables)); + auto layer = parse_document(layerDoc, origin, + LoadContext{.insideWorkspace = true, + .workspaceLayer = true}); + if (!layer) { + auto e = std::move(layer).error(); + e.message = "in the workspace layer ([workspace.*]): " + e.message; + return std::unexpected(std::move(e)); + } + for (auto const& w : layer->schemaWarnings) + m.schemaWarnings.push_back( + w.starts_with("[") ? "[workspace." + w.substr(1) : w); + m.workspace.layer = std::make_shared(std::move(*layer)); + } + } + + // W7: a workspace root without [package] holds no package, so a table + // that describes one acts on nothing here. Recorded; the loader warns, + // and refuses under --strict. + if (!pkg_t && !ctx.workspaceLayer) { + static constexpr std::pair kPackageTables[] = { + {"build", "workspace.build"}, {"targets", ""}, {"dependencies", "workspace.dependencies"}, + {"dev-dependencies", ""}, {"build-dependencies", ""}, {"features", ""}, + {"resources", ""}, {"test", ""}, {"hooks", ""}, {"runtime", ""}, + }; + auto note = [&](std::string table, std::string mirror) { + m.virtualRootPackageTables.push_back(mirror.empty() + ? std::format("[{}] is written on a workspace root without [package], " + "so it acts on no package; write it in a member", table) + : std::format("[{}] is written on a workspace root without [package], " + "so it acts on no package; write it in a member, or as " + "[{}] to give it to every member", table, mirror)); + }; + for (auto const& [table, mirror] : kPackageTables) + if (doc->get(table)) note(std::string(table), std::string(mirror)); + if (auto* targets = doc->get_table("target")) + for (auto const& [sel, row] : *targets) { + if (!row.is_table()) continue; + for (std::string_view sub : {"build", "abi", "runtime", "targets", "dependencies", + "dev-dependencies", "build-dependencies"}) { + if (!row.as_table().contains(sub)) continue; + const bool shared = sub == "build" || sub == "abi" || sub == "runtime"; + note(std::format("target.{}.{}", toml_header_key(sel), sub), + shared ? std::format("workspace.target.{}.{}", toml_header_key(sel), sub) + : std::string()); + } + } + } } // [indices] — custom package index repositories. @@ -4525,6 +4719,7 @@ std::expected parse_string(std::string_view content, return m; } +} // namespace // M5.0: inject defaults and auto-infer targets when fields are absent. // Mutates manifest in-place; called from load() with the project root. diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index a8e4f84f5..cb842a319 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -324,6 +324,11 @@ struct Toolchain { int bootstrapLine = 0; // Where each string entry was written, for the source a build reports. std::map lineByPlatform; + // The manifest that wrote an entry, by the same key, when it is not the + // manifest that holds this table: an entry a member took from its + // workspace (`[workspace.toolchain]`, or the root position) names the + // workspace root's mcpp.toml. + std::map fileByPlatform; // The table behind the entry `for_platform` answers with, by the same key. const LocalToolchain* local_for(std::string_view platform) const { @@ -1803,14 +1808,10 @@ struct WorkspaceInherited { BuildConfig build; bool buildPresent = false; - // THERE IS NO `[workspace.target.]`, DELIBERATELY. - // - // A plain `[target.]` block in the workspace root manifest is - // ALREADY inherited by every member, per triple, member-wins — that - // predates these tables. Adding a second spelling for a capability that - // exists would be surface with no function, and two spellings of one rule - // is how the two acquire different behaviour later. Documented in docs/05 - // rather than implemented here. + // `[workspace.toolchain]`, `[workspace.indices]`, `[workspace.profile.*]`, + // `[workspace.target.]` and `[workspace.xlings]` are not here: they + // are read by the same reader as the tables they mirror, into + // `WorkspaceConfig::layer` (SPEC-004 §9.10, rule W2). }; // THE INHERITABLE SUBSET OF `[build]`, STATED ONCE. @@ -1848,12 +1849,29 @@ inline constexpr std::array kWorkspaceBuildKeys{{ {"ios_deployment_target", &BuildConfig::iosDeploymentTarget}, }}; +struct Manifest; + struct WorkspaceConfig { std::vector members; // relative paths to member dirs std::vector exclude; // paths to exclude std::map dependencies; // [workspace.dependencies] - WorkspaceInherited inherited; // [workspace.package|build|target.*] + WorkspaceInherited inherited; // [workspace.package|build] bool present = false; + + // THE WORKSPACE LAYER: what `[workspace.toolchain]`, `[workspace.indices]`, + // `[workspace.profile.]`, `[workspace.target.]` (with its + // `.build`, `.abi`, `.runtime` and `.xlings` subtables) and + // `[workspace.xlings]` say to every member (SPEC-004 §9.10, W1-W3). + // + // Read by the reader of the tables they mirror -- the text under + // `workspace.` is a manifest of its own, without `[package]` -- so the + // keys, subtables and selectors of `[workspace.X]` are those of `X` by + // construction, not by a second list. Null when none of them is written. + // `layerTables` names the ones that are (`toolchain`, `indices`, + // `profile`, `target`, `xlings`); a workspace root that writes one of + // them also at the root position is refused for that table. + std::shared_ptr layer; + std::vector layerTables; }; // `[hooks]` — project build lifecycle commands (#496). @@ -1944,6 +1962,13 @@ struct Profile { bool debug = false; bool lto = false; bool strip = false; + // Which scalars the table WROTE: a profile inherited from the workspace + // layer is merged key by key (W3), and "the member did not write `opt`" + // cannot be read off a default of "2". + bool optDeclared = false; + bool debugDeclared = false; + bool ltoDeclared = false; + bool stripDeclared = false; // `dependency_linkage`, per profile (#519). // // DECLARED-OR-NOT IS LOAD-BEARING, and that is why there are two members @@ -2039,6 +2064,11 @@ struct Manifest { // manifest is read on two paths before it is planned, and an appended // vector must be appended once. bool inheritedAsRootPackage = false; + // W7: the package tables a workspace root WITHOUT `[package]` wrote + // (`[build]`, `[targets]`, `[dependencies]`, ...). They act on no + // package there; the parser records them, the loader warns (`--strict`: + // refuses). Empty for every other manifest. + std::vector virtualRootPackageTables; std::vector conditionalConfigs; // [target.'cfg(...)'.build], deferred std::map profiles; // [profile.] // [features] — feature name → implied features ("default" = default set). diff --git a/src/build/prepare/config.cpp b/src/build/prepare/config.cpp index 7c235ea96..84d2590c4 100644 --- a/src/build/prepare/config.cpp +++ b/src/build/prepare/config.cpp @@ -622,11 +622,13 @@ unfolded_defines_error(const mcpp::manifest::Manifest& m) { // Three parts of the inheritance matter to a dependency: `[workspace.package]` // (a member may omit `version`), `x.workspace = true` dependency entries // (without the merge the entry reaches resolution with neither version nor -// path), and `[workspace.build]`. They are applied at the dependency's LOAD +// path), `[workspace.build]`, `[workspace.xlings]` and the rows of +// `[workspace.target.]`. They are applied at the dependency's LOAD // site, before the conditional merge and the `defines` fold, which is the // order the root follows; `makePackageRoot` only captures the result (#690). -// `[toolchain]`, `[target.]` and `[indices]` are decided by the root -// for the whole graph and are not applied to a dependency. +// `[workspace.toolchain]`, the scalar rows of `[workspace.target.]`, +// `[workspace.indices]` and `[workspace.profile.*]` describe the build, which +// the root decides for the whole graph; they are not applied to a dependency. // // One function for every way a member is reached: a sibling `path` // dependency, a member of a git-hosted workspace, and a member inside an @@ -641,6 +643,7 @@ inherit_as_workspace_member(mcpp::manifest::Manifest& member, mcpp::project::merge_workspace_deps(member, workspace, workspaceRoot); mcpp::project::inherit_workspace_build(member, workspace, workspaceRoot); mcpp::project::inherit_workspace_xlings(member, workspace); + mcpp::project::inherit_workspace_layer_rows(member, workspace, workspaceRoot); return mcpp::project::workspace_inheritance_error(member, memberDir); } diff --git a/src/build/prepare/manifest.cpp b/src/build/prepare/manifest.cpp index 105b0bb9a..a5dc968d9 100644 --- a/src/build/prepare/manifest.cpp +++ b/src/build/prepare/manifest.cpp @@ -154,10 +154,44 @@ report_manifest_statements(const mcpp::manifest::Manifest& m, bool strict) { // links nothing into the root. A rooted workspace's own package is a member // like any other ("."), so it is the same node, with the same commands, in // every selection that reaches it. +// What the workspace root's own manifest states about itself, reported once +// for the command that plans its members (SPEC-004 §9.10): +// W7 a package table on a root without [package] acts on no package -- +// warning, an error under --strict; +// the workspace layer read by an engine older than this one is ignored by +// it without a word (2026.10.8.1 builds with a different toolchain), so a +// root that writes `[workspace.X]` without stating this engine as its floor +// gets a note. +static std::expected +report_workspace_root(const mcpp::manifest::Manifest& ws, bool strict) { + for (auto const& w : ws.virtualRootPackageTables) { + if (strict) return std::unexpected(w); + mcpp::diag::warning("manifest/schema", w); + } + if (ws.workspace.layer) { + const auto have = mcpp::xpkg_version::parse(mcpp::MCPP_VERSION); + const auto floor = ws.workspace.inherited.mcppFloor; + const auto need = floor.empty() ? std::nullopt : mcpp::xpkg_version::parse(floor); + // The first release that reads the layer. + const auto first = mcpp::xpkg_version::parse("2026.10.10.1"); + if (have && first && (!need || mcpp::xpkg_version::compare(*need, *first) < 0)) { + std::string tables; + for (auto const& t : ws.workspace.layerTables) + tables += std::format("{}[workspace.{}]", tables.empty() ? "" : ", ", t); + mcpp::diag::note("manifest/workspace-layer", std::format( + "{} is read by mcpp 2026.10.10.1 and later; an older mcpp ignores it and " + "builds the members without it. State the floor so an older one refuses " + "instead: [workspace.package] mcpp = \">=2026.10.10.1\"", tables)); + } + } + return {}; +} + static std::expected select_workspace_members(PrepareState& state, const std::filesystem::path& wsRoot, mcpp::manifest::Manifest ws, const std::vector& selection) { + if (auto r = report_workspace_root(ws, state.overrides.strict); !r) return r; const auto all = mcpp::project::workspace_members(ws, wsRoot); { auto first = mcpp::project::load_member_manifest(ws, wsRoot, selection.front()); diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index 750e63106..83b2b77a9 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -253,7 +253,7 @@ export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { mo.package_filter = members.front(); return configure_member(std::move(mo), ""); } - auto groups = workspace_groups((*selection)->root, members); + auto groups = workspace_groups((*selection)->root, members, ov.target_triple); if (!groups) { mcpp::ui::error(std::format("{}", groups.error())); return 2; } std::vector request; for (auto const& g : *groups) @@ -540,7 +540,7 @@ export int cmd_emit_build_database(const mcpplibs::cmdline::ParsedArgs& parsed) wsRoot = (*selection)->root; // A member whose manifest cannot be read has no configuration; the // members are then planned one by one, and it fails alone (R5.2). - auto groups = workspace_groups(wsRoot, (*selection)->members); + auto groups = workspace_groups(wsRoot, (*selection)->members, ov.target_triple); if (!groups) { for (auto const& mp : (*selection)->members) plan_alone(mp); } else { @@ -1087,7 +1087,7 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, // has no configuration: each member is then its own group, and it // fails alone when it is planned. std::vector> groups; - if (auto g = workspace_groups(sel.root, members)) groups = std::move(*g); + if (auto g = workspace_groups(sel.root, members, ov.target_triple)) groups = std::move(*g); else for (auto const& mp : members) groups.push_back({mp}); mcpp::build::WorkspaceTestHooks hooks; diff --git a/src/cli/cmd_publish.cppm b/src/cli/cmd_publish.cppm index b61d3388a..e5f879571 100644 --- a/src/cli/cmd_publish.cppm +++ b/src/cli/cmd_publish.cppm @@ -392,7 +392,7 @@ PackMembers pack_members(const mcpplibs::cmdline::ParsedArgs& parsed) { } return out; } - auto groups = mcpp::cli::workspace_groups(sel.root, packable); + auto groups = mcpp::cli::workspace_groups(sel.root, packable, {}); if (!groups) { mcpp::ui::error(groups.error()); out.rc = 2; diff --git a/src/cli/selection.cppm b/src/cli/selection.cppm index db1165422..4eea1e798 100644 --- a/src/cli/selection.cppm +++ b/src/cli/selection.cppm @@ -20,6 +20,7 @@ import std; import mcpplibs.cmdline; import mcpp.manifest; import mcpp.project; +import mcpp.build.prepare_inputs; export namespace mcpp::cli { @@ -209,8 +210,14 @@ MemberRequest member_request(const mcpplibs::cmdline::ParsedArgs& parsed) { // The workspace a fan-out acts on, and its members grouped by configuration // (workspace design 2026-09-29 §15): members whose root-position values are // equal are planned together, in one graph, in one build directory. +// +// A member's conditional configuration rows are evaluated for the target the +// command builds -- `--target`, else the member's `[build] target`, else the +// host -- with prepare's evaluator, so a group and the plan it becomes agree +// on them by construction (W4). std::expected>, std::string> -workspace_groups(const std::filesystem::path& wsRoot, const std::vector& members) { +workspace_groups(const std::filesystem::path& wsRoot, const std::vector& members, + std::string_view targetTriple) { auto ws = mcpp::manifest::load(wsRoot / "mcpp.toml"); if (!ws) return std::unexpected(ws.error().format()); std::vector> groups; @@ -218,7 +225,12 @@ workspace_groups(const std::filesystem::path& wsRoot, const std::vectorbuildConfig.target) : targetTriple); + const auto key = mcpp::project::root_position_key(*mm, [&](std::string_view predicate) { + return cfgpred::matches(std::string(predicate), ctx); + }); auto [it, fresh] = byKey.try_emplace(key, groups.size()); if (fresh) groups.emplace_back(); groups[it->second].push_back(mp); diff --git a/src/project.cppm b/src/project.cppm index 0906b2d12..ed003b9c7 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -13,6 +13,7 @@ export module mcpp.project; import std; import mcpp.manifest; import mcpp.ui; +import mcpp.diag; namespace mcpp::project { @@ -183,44 +184,52 @@ export void inherit_workspace_package(mcpp::manifest::Manifest& member, if (member.package.mcppFloor.empty()) member.package.mcppFloor = inh.mcppFloor; } -// EVERYTHING A MEMBER INHERITS FROM ITS WORKSPACE ROOT, IN ONE FUNCTION. +// ─── The workspace layer and the position reading (SPEC-004 §9.10) ────────── // -// There are two inheritance SITES in prepare_build — the command issued at the -// workspace root with `-p `, and the command issued inside a member -// directory — and until this function existed they were two hand-copied lists -// of the same merges. A fifth key added to one of them is a defect that -// compiles, which is exactly how `[build]` came to be inherited by neither -// (#527 Bug 2). +// A workspace root speaks to its members through `[workspace.X]` (the layer, +// `WorkspaceConfig::layer`) and nothing else (W1). The merge rule is the key's, +// not the table's (W3): a scalar the member declared is the member's, a list +// appends with the workspace first, a named table (`profile.`, +// `target.`) is merged key by key, conditional rows are inherited row +// by row ahead of the member's own, and a relative path is anchored at the +// manifest that wrote it (#224). // -// The discipline is stated on `WorkspaceInherited`: scalars are taken when the -// member did not DECLARE the key, vectors append with the workspace first, and -// dependencies keep their explicit `.workspace = true` opt-in because they are -// graph edges. `[toolchain]`, `[target.]` and `[indices]` were already -// inherited before these tables existed and keep the behaviour they had. -// -// `wsRoot` anchors relative paths: an `[indices].path` or a -// `[workspace.dependencies] path` was written against the WORKSPACE ROOT, and -// re-anchoring it to the member directory is #224. -// The workspace root's `[xlings.workspace]`, for a member (#713). -// -// An xlings entry describes the environment a build runs in, the same class of -// declaration as `[toolchain]` and `[target.*]`, so it is inherited implicitly -// rather than through an explicit opt-in: only dependencies, which are graph -// edges, need `.workspace = true` (`merge_workspace_deps`). Before this a member -// saw none of the root's entries: they were installed for the workspace, and -// `mcpp::xpkg_dir` in the member's build program still answered "" for them. -// -// The root's entries come first and a member's own declaration of the same -// package wins, which is the "nearer the artifact" rule of SPEC-004 §4.5 -- -// identity is `(namespace, name)`, decided by `package_key`. Conditional -// `[target..xlings.workspace]` rows travel as conditional rows, so they are -// still decided by the selector at merge time. Feature-gated entries do not -// travel: a feature belongs to the package that declares it. Neither does the -// emitter's per-platform view (`workspaceByPlatform`), so a published member's -// descriptor states only what the member itself declared; the `subos` is the -// root's choice already (`select_runtime`). -export void inherit_workspace_xlings(mcpp::manifest::Manifest& member, - const mcpp::manifest::Manifest& workspace) { +// The root's own `[toolchain]`, `[indices]`, `[profile.*]`, `[target.*]` and +// `[xlings]` still reach members by position where they have no +// `[workspace.X]` (mcpp.pm.compat.workspace_position), with the old merge and +// a warning. + +namespace detail { + +bool lists(std::span names, std::string_view name) { + return std::ranges::find(names, name) != names.end(); +} + +// The position reading was used for `table`. A root without `[package]` has +// no other reading of these tables, so every use is reported; a root with one +// reads them as its own package's first, and only a member that actually +// received a value through the old rule is told. +void warn_position(const mcpp::manifest::Manifest& workspace, + const mcpp::manifest::Manifest& member, + std::string_view table, bool received) { + const bool virtualRoot = workspace.package.name.empty(); + if (!virtualRoot && !received) return; + mcpp::diag::warning("manifest/workspace-position", + mcpp::pm::compat::position_warning( + table, virtualRoot ? std::string_view{} : member.package.name)); +} + +// `[xlings]` entries and `[target..xlings]` rows from `top` and `rows`, +// for a member. The workspace's entries come first and a member's own +// declaration of the same package wins, the "nearer the artifact" rule of +// SPEC-004 §4.5; identity is `(namespace, name)`. Feature-gated entries do +// not travel: a feature belongs to the package that declares it. Neither does +// the emitter's per-platform view (`workspaceByPlatform`), so a published +// member's descriptor states only what the member itself declared. Returns +// whether the member received anything. +bool merge_xlings(mcpp::manifest::Manifest& member, + const mcpp::manifest::XlingsConfig* top, + std::span rows) { // `(namespace, name)`, the identity `mcpp.xlings.address_set` defines, // spelled here from the same parser rather than imported. Importing that // module here makes GCC 16.1 fail with an internal compiler error @@ -234,6 +243,7 @@ export void inherit_workspace_xlings(mcpp::manifest::Manifest& member, for (auto const& cc : member.conditionalConfigs) for (auto const& a : cc.xlings.deps) own.insert(package_key(a)); + bool received = false; // Copies the entries of `from` whose package the member does not declare, // with the pin and the tier each address carries. auto take = [&](const mcpp::manifest::XlingsConfig& from, @@ -249,62 +259,185 @@ export void inherit_workspace_xlings(mcpp::manifest::Manifest& member, to.depWhen.try_emplace(a, w->second); if (from.onRequest.contains(a)) to.onRequest.insert(a); } + received = received || !taken.empty(); to.deps.insert(to.deps.begin(), taken.begin(), taken.end()); // `[xlings.overrides]` states where a payload comes from on THIS // machine, so a member built as the root of its own build takes the // workspace's statement unless it makes its own (mcpp#755). - for (auto const& [pkg, o] : from.overrides) to.overrides.try_emplace(pkg, o); + for (auto const& [pkg, o] : from.overrides) + if (to.overrides.try_emplace(pkg, o).second) received = true; }; - take(workspace.xlings, member.xlings); + if (top) take(*top, member.xlings); - std::vector rows; - for (auto const& cc : workspace.conditionalConfigs) { + std::vector taken; + for (auto const& cc : rows) { if (cc.xlings.deps.empty() && cc.xlings.overrides.empty()) continue; mcpp::manifest::ConditionalConfig row; row.predicate = cc.predicate; take(cc.xlings, row.xlings); if (!row.xlings.deps.empty() || !row.xlings.overrides.empty()) - rows.push_back(std::move(row)); + taken.push_back(std::move(row)); + } + member.conditionalConfigs.insert(member.conditionalConfigs.begin(), + std::make_move_iterator(taken.begin()), + std::make_move_iterator(taken.end())); + return received; +} + +// W3 for the tables only a build's root reads: `[workspace.toolchain]`, +// `[workspace.indices]`, `[workspace.profile.]` and the scalar rows of +// `[workspace.target.]`. +void merge_layer_root_position(mcpp::manifest::Manifest& member, + const mcpp::manifest::Manifest& layer, + const std::filesystem::path& wsRoot, + const std::filesystem::path& wsManifest) { + // A platform's entry: the member's own for that platform, else the + // member's own `default`, else the workspace's. The member's `default` + // is a statement about every platform, so it outranks the workspace's + // platform entries too. + auto& tc = member.toolchain; + if (!tc.byPlatform.contains("default")) { + for (auto const& [platform, spec] : layer.toolchain.byPlatform) { + if (tc.byPlatform.contains(platform)) continue; + tc.byPlatform[platform] = spec; + if (auto it = layer.toolchain.localByPlatform.find(platform); + it != layer.toolchain.localByPlatform.end()) + tc.localByPlatform[platform] = it->second; + if (auto it = layer.toolchain.lineByPlatform.find(platform); + it != layer.toolchain.lineByPlatform.end()) + tc.lineByPlatform[platform] = it->second; + tc.fileByPlatform[platform] = wsManifest; + } + } + if (tc.bootstrap.empty() && !layer.toolchain.bootstrap.empty()) { + tc.bootstrap = layer.toolchain.bootstrap; + tc.bootstrapLine = layer.toolchain.bootstrapLine; + } + + for (auto const& [name, idx] : layer.indices) { + auto [it, inserted] = member.indices.try_emplace(name, idx); + if (inserted && it->second.is_local() && it->second.path.is_relative()) + it->second.path = (wsRoot / it->second.path).lexically_normal(); + } + + for (auto const& [name, from] : layer.profiles) { + auto [it, inserted] = member.profiles.try_emplace(name, from); + if (inserted) continue; + auto& to = it->second; + if (!to.optDeclared && from.optDeclared) { to.optLevel = from.optLevel; to.optDeclared = true; } + 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.dependencyLinkageDeclared && from.dependencyLinkageDeclared) { + to.dependencyLinkage = from.dependencyLinkage; + to.dependencyLinkageDeclared = true; + } + to.cflags.insert(to.cflags.begin(), from.cflags.begin(), from.cflags.end()); + to.cxxflags.insert(to.cxxflags.begin(), from.cxxflags.begin(), from.cxxflags.end()); + to.ldflags.insert(to.ldflags.begin(), from.ldflags.begin(), from.ldflags.end()); + } + + for (auto const& [triple, from] : layer.targetOverrides) { + auto [it, inserted] = member.targetOverrides.try_emplace(triple, from); + if (inserted) continue; + auto& to = it->second; + if (to.toolchain.empty()) to.toolchain = from.toolchain; + if (to.linkage.empty()) to.linkage = from.linkage; + if (to.cxxRuntime.empty()) to.cxxRuntime = from.cxxRuntime; + if (!to.sysrootDeclared && from.sysrootDeclared) { + to.sysroot = from.sysroot; + to.sysrootDeclared = true; + } + if (to.minApiLevel == 0) to.minApiLevel = from.minApiLevel; + if (to.runner.empty()) to.runner = from.runner; + for (auto const& [n, argv] : from.namedRunners) to.namedRunners.try_emplace(n, argv); + } +} + +} // namespace detail + +// The workspace's `[xlings]` entries, for a member, at every position it is +// reached (W5): selected, a sibling's `path` dependency, a member of a +// git-hosted workspace, a host tool's sub-build root (#713). +// +// An xlings entry describes the environment a build runs in, so it is +// inherited without an opt-in; only dependencies, which are graph edges, need +// `.workspace = true` (`merge_workspace_deps`). +export void inherit_workspace_xlings(mcpp::manifest::Manifest& member, + const mcpp::manifest::Manifest& workspace) { + static const std::vector none; + if (auto const* layer = workspace.workspace.layer.get()) { + auto const& tables = workspace.workspace.layerTables; + detail::merge_xlings(member, + detail::lists(tables, "xlings") ? &layer->xlings : nullptr, + detail::lists(tables, "target") ? layer->conditionalConfigs : none); + } + // COMPAT(workspace-position): the root's own [xlings] and [target..xlings]. + const auto position = mcpp::pm::compat::position_tables(workspace); + const bool top = detail::lists(position, "xlings"); + const bool rows = detail::lists(position, "target"); + if (!top && !rows) return; + const bool received = detail::merge_xlings(member, top ? &workspace.xlings : nullptr, + rows ? workspace.conditionalConfigs : none); + if (top) detail::warn_position(workspace, member, "xlings", received); +} + +// The rows of `[workspace.target.]` -- `.build`, `.abi`, `.runtime` -- +// for a member, at every position it is reached (W5), ahead of the member's +// own rows (W3). Their `.xlings` part is `inherit_workspace_xlings`'s. +// A relative path in them was written against the workspace root (#224). +export void inherit_workspace_layer_rows(mcpp::manifest::Manifest& member, + const mcpp::manifest::Manifest& workspace, + const std::filesystem::path& wsRoot) { + auto const* layer = workspace.workspace.layer.get(); + if (!layer || layer->conditionalConfigs.empty()) return; + auto anchor = [&](std::vector& dirs) { + for (auto& d : dirs) + if (d.is_relative()) d = (wsRoot / d).lexically_normal(); + }; + std::vector rows; + for (auto cc : layer->conditionalConfigs) { + cc.xlings = {}; + anchor(cc.inputs.includeDirs); + anchor(cc.inputs.includeDirsAfter); + anchor(cc.inputs.privateIncludeDirs); + anchor(cc.linkLibraryDirs); + rows.push_back(std::move(cc)); } member.conditionalConfigs.insert(member.conditionalConfigs.begin(), std::make_move_iterator(rows.begin()), std::make_move_iterator(rows.end())); } -// The keys a member inherits only where it is the ROOT of a build: `[toolchain]`, -// `[target.]`, `[indices]` and `[profile.]`. They choose the -// compiler, the target rows, the indices and the build profile for the whole -// graph, so a member reached as somebody's dependency takes them from that -// build's root instead. A member built as a -// host tool is the root of its own sub-build, which is the second caller -// (#710): without it, `mcpp build -p tool` used the workspace's compiler and -// the same tool built for a consumer used the global default. +// The tables only the root of a build reads -- `toolchain`, `indices`, +// `profile.`, the scalar rows of `target.` -- for a member that +// is the root of one: selected by a command, or the root of a host tool's +// sub-build (#710). A member reached as somebody's dependency takes them from +// that build's root instead. export void inherit_workspace_root_position(mcpp::manifest::Manifest& member, const mcpp::manifest::Manifest& workspace, const std::filesystem::path& wsRoot) { - if (member.toolchain.byPlatform.empty()) - member.toolchain = workspace.toolchain; - for (auto& [triple, entry] : workspace.targetOverrides) - if (!member.targetOverrides.contains(triple)) - member.targetOverrides[triple] = entry; - // A profile is inherited by name, and a member's own table of that name - // replaces it whole, as `[target.]` does. Before 2026.10.5.2 the - // workspace's profiles reached no member: a virtual root's were ignored - // without a word, and a rooted root's applied only when its own package - // was the first one selected. - for (auto& [name, profile] : workspace.profiles) - if (!member.profiles.contains(name)) - member.profiles[name] = profile; - inherit_workspace_indices(member, workspace, wsRoot); + if (auto const* layer = workspace.workspace.layer.get()) + detail::merge_layer_root_position(member, *layer, wsRoot, workspace.sourcePath); + // COMPAT(workspace-position) + const auto position = mcpp::pm::compat::position_tables(workspace); + if (position.empty()) return; + const auto received = mcpp::pm::compat::inherit_root_position_by_position( + member, workspace, wsRoot, position); + for (auto const& table : position) { + if (table == "xlings") continue; + detail::warn_position(workspace, member, table, detail::lists(received, table)); + } } // THE ROOT PACKAGE OF A WORKSPACE IS ONE OF ITS MEMBERS. // // A root that carries `[package]` beside `[workspace]` is selected as member -// `"."`, and it receives `[workspace.package]` and `[workspace.build]` once, -// as every other member does, so that one manifest compiles the same way in -// every selection. Its root-position keys and `[workspace.dependencies]` are -// handled by the caller: they are its own already, or merged explicitly. +// `"."`, and it receives `[workspace.package]`, `[workspace.build]` and the +// workspace layer once, as every other member does, so that one manifest +// compiles the same way in every selection. Its own `[toolchain]`, +// `[target.*]`, ... are its own declarations and come first; its +// `[workspace.dependencies]` are merged explicitly by the caller. export void inherit_as_root_package(mcpp::manifest::Manifest& root, const std::filesystem::path& wsRoot) { if (!root.workspace.present || root.package.name.empty()) return; @@ -313,14 +446,41 @@ export void inherit_as_root_package(mcpp::manifest::Manifest& root, const auto workspace = root; inherit_workspace_package(root, workspace); inherit_workspace_build(root, workspace, wsRoot); + if (auto const* layer = workspace.workspace.layer.get()) { + static const std::vector none; + auto const& tables = workspace.workspace.layerTables; + detail::merge_layer_root_position(root, *layer, wsRoot, workspace.sourcePath); + inherit_workspace_layer_rows(root, workspace, wsRoot); + detail::merge_xlings(root, + detail::lists(tables, "xlings") ? &layer->xlings : nullptr, + detail::lists(tables, "target") ? layer->conditionalConfigs : none); + } } +// EVERYTHING A MEMBER INHERITS FROM ITS WORKSPACE ROOT, IN ONE FUNCTION. +// +// There are two inheritance SITES in prepare_build — the command issued at the +// workspace root with `-p `, and the command issued inside a member +// directory — and until this function existed they were two hand-copied lists +// of the same merges. A fifth key added to one of them is a defect that +// compiles, which is exactly how `[build]` came to be inherited by neither +// (#527 Bug 2). +// +// The discipline is stated on `WorkspaceInherited`: scalars are taken when the +// member did not DECLARE the key, vectors append with the workspace first, and +// dependencies keep their explicit `.workspace = true` opt-in because they are +// graph edges. The workspace layer follows the same rule (W3, above). +// +// `wsRoot` anchors relative paths: an `[indices].path` or a +// `[workspace.dependencies] path` was written against the WORKSPACE ROOT, and +// re-anchoring it to the member directory is #224. export void inherit_workspace_config(mcpp::manifest::Manifest& member, const mcpp::manifest::Manifest& workspace, const std::filesystem::path& wsRoot) { merge_workspace_deps(member, workspace, wsRoot); inherit_workspace_root_position(member, workspace, wsRoot); inherit_workspace_xlings(member, workspace); + inherit_workspace_layer_rows(member, workspace, wsRoot); // The two halves, each with a second caller of its own: a member reached // as a sibling.s `path` dependency needs both, at two different points. @@ -330,6 +490,7 @@ export void inherit_workspace_config(mcpp::manifest::Manifest& member, inherit_workspace_build(member, workspace, wsRoot); } + // The `[workspace.build]` half on its own. // // SEPARATE BECAUSE IT HAS A SECOND CALLER. `inherit_workspace_config` runs for @@ -740,7 +901,17 @@ load_member_manifest(const mcpp::manifest::Manifest& workspace, // keys are equal are planned together; the key is a canonical string of those // values and nothing else, so a member's own flags, sources and dependencies // never separate it from another member. -export std::string root_position_key(const mcpp::manifest::Manifest& m) { +// +// A conditional row's configuration values (`[target..build] +// dialect_cxxflags`, `[target..abi]`) count as they evaluate for this +// build (W4): `applies` answers whether a row's selector holds for the target +// the members are planned for, and it is the evaluator prepare uses +// (`cfgpred::matches` over `cfgpred::context_for`). Two members whose rows +// differ only on other targets therefore share a plan; without `applies` no +// row counts. +export std::string root_position_key( + const mcpp::manifest::Manifest& m, + const std::function& applies = {}) { std::string s; auto field = [&](std::string_view name, std::string_view value) { s += name; s += '='; s += value; s += '\x1f'; @@ -764,7 +935,19 @@ export std::string root_position_key(const mcpp::manifest::Manifest& m) { } field("standard", m.package.standard); auto const& b = m.buildConfig; - list("dialect_cxxflags", b.dialectCxxflags); + auto dialect = b.dialectCxxflags; + auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; + auto abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" : "0") : "-"; + if (applies) + for (auto const& cc : m.conditionalConfigs) { + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + continue; + if (!applies(cc.predicate)) continue; + dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); + if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; + if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" : "0"; + } + list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); field("cxx_runtime_tests", b.cxxRuntimeTests); field("cxx_runtime_shared", b.cxxRuntimeShared); @@ -774,8 +957,8 @@ export std::string root_position_key(const mcpp::manifest::Manifest& m) { field("target", b.target); field("macos_deployment_target", b.macosDeploymentTarget); field("ios_deployment_target", b.iosDeploymentTarget); - field("abi_threads", b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"); - field("abi_exceptions", b.abiExceptionsDeclared ? (b.abiExceptions ? "1" : "0") : "-"); + field("abi_threads", abiThreads); + field("abi_exceptions", abiExceptions); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); @@ -858,6 +1041,23 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, b.dependencyLinkage = f.dependencyLinkage; b.cacheMode = f.cacheMode; b.platformDependencies = f.platformDependencies; + // The member's conditional configuration rows, as rows: prepare evaluates + // them against the target exactly as it would on the member, so a + // `[target..build] dialect_cxxflags` or `[target..abi]` the + // member declares (or inherits) reaches the build when the member is + // planned under a virtual root (#786). Nothing a package owns travels. + for (auto const& cc : first.conditionalConfigs) { + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + continue; + mcpp::manifest::ConditionalConfig row; + row.predicate = cc.predicate; + row.dialectCxxflags = cc.dialectCxxflags; + row.abiThreads = cc.abiThreads; + row.abiThreadsDeclared = cc.abiThreadsDeclared; + row.abiExceptions = cc.abiExceptions; + row.abiExceptionsDeclared = cc.abiExceptionsDeclared; + v.conditionalConfigs.push_back(std::move(row)); + } return v; } From c7df53e09da43f3118a45a47359a10f58b81c6f8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:20:07 +0900 Subject: [PATCH 03/32] feat(manifest): the key registry; publish writes the workspace layer into the member (W6); grouping skips rows that do not hold --- modules/manifest/src/key_registry.cppm | 142 +++++++++++++ modules/manifest/src/manifest.cppm | 1 + modules/manifest/src/toml.cppm | 61 +++--- modules/manifest/src/types.cppm | 30 +-- src/build/prepare/manifest.cpp | 2 +- src/project.cppm | 15 +- src/publish/normalize.cppm | 89 +++++++++ tests/unit/test_key_registry.cpp | 265 +++++++++++++++++++++++++ 8 files changed, 547 insertions(+), 58 deletions(-) create mode 100644 modules/manifest/src/key_registry.cppm create mode 100644 tests/unit/test_key_registry.cpp diff --git a/modules/manifest/src/key_registry.cppm b/modules/manifest/src/key_registry.cppm new file mode 100644 index 000000000..bce160d0d --- /dev/null +++ b/modules/manifest/src/key_registry.cppm @@ -0,0 +1,142 @@ +// mcpp.manifest.key_registry - the class of every manifest key, stated once. +// +// SPEC-004 §2 gives each key two independent properties: its SCOPE, which is +// the key's own (whom its value acts on), and its SOURCE, which is the +// table's (the command line, the package's own table, the workspace layer, +// the engine's default). A condition (`[target.]`) is a predicate on a +// value and changes neither. +// +// P package acts on this package's units (`cxxflags`, `sources`) +// U usage flows from a dependency to its consumers (`include_dirs`) +// C configuration one value per build, for the whole graph (`target`, +// `dialect_cxxflags`, `[toolchain]`); read from the root +// M metadata enters no command (`license`) +// +// A row also says how a member and its workspace layer are merged (W3) and +// whether the key may be shared through `[workspace.X]` (W2) or written under +// a condition. The parser's known-key checks for `[build]` and +// `[target..build]`, the keys `[workspace.target..build]` accepts, +// and the package tables W7 reports on a workspace root without `[package]` +// are read from here; tests/unit/test_key_registry.cpp checks the rest +// (`kWorkspaceBuildKeys`, the grouping key) against it. + +export module mcpp.manifest.key_registry; + +import std; + +export namespace mcpp::manifest { + +enum class KeyScope { Package, Usage, Configuration, Metadata }; + +// How a member's value and its workspace layer's combine (W3). +enum class KeyMerge { + Scalar, // the member's, when it declared the key + List, // appended, the workspace's entries first + Defines, // a set by macro name; the member's same name replaces + Table, // a named table: merged key by key +}; + +struct KeyClass { + std::string_view table; // "build", "target..build", "toolchain", ... + std::string_view key; // "" for the table as a whole + KeyScope scope; + KeyMerge merge; + bool shared; // may be written as [workspace.] (W2) + bool conditional; // may be written under [target.] +}; + +inline constexpr KeyClass kKeyRegistry[] = { + // [build] + {"build", "accel", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "allow_host_libs", KeyScope::Package, KeyMerge::Scalar, false, false}, + {"build", "bmi_schedule", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "build_program_timeout", KeyScope::Package, KeyMerge::Scalar, false, false}, + {"build", "c_standard", KeyScope::Package, KeyMerge::Scalar, true, false}, + {"build", "cache", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "cflags", KeyScope::Package, KeyMerge::List, true, true }, + {"build", "cxxflags", KeyScope::Package, KeyMerge::List, true, true }, + {"build", "cxx_runtime", KeyScope::Configuration, KeyMerge::Scalar, true, false}, + {"build", "default-profile", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "defines", KeyScope::Package, KeyMerge::Defines, true, true }, + {"build", "dependency_linkage", KeyScope::Configuration, KeyMerge::Scalar, true, false}, + {"build", "dialect_cxxflags", KeyScope::Configuration, KeyMerge::List, true, true }, + {"build", "flags", KeyScope::Package, KeyMerge::List, false, true }, + {"build", "include_dirs", KeyScope::Usage, KeyMerge::List, true, true }, + {"build", "include_dirs_after", KeyScope::Usage, KeyMerge::List, true, true }, + {"build", "ios_deployment_target", KeyScope::Configuration, KeyMerge::Scalar, true, false}, + {"build", "jobs", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "ldflags", KeyScope::Usage, KeyMerge::List, true, true }, + {"build", "macos_deployment_target", KeyScope::Configuration, KeyMerge::Scalar, true, false}, + {"build", "module_extensions", KeyScope::Package, KeyMerge::List, false, false}, + {"build", "platform-dependencies", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "private_include_dirs", KeyScope::Package, KeyMerge::List, true, true }, + {"build", "profile", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "sources", KeyScope::Package, KeyMerge::List, false, true }, + {"build", "static_stdlib", KeyScope::Configuration, KeyMerge::Scalar, false, false}, + {"build", "std-compat-module", KeyScope::Package, KeyMerge::Scalar, false, false}, + {"build", "std-module", KeyScope::Package, KeyMerge::Scalar, false, false}, + {"build", "std-module-flags", KeyScope::Package, KeyMerge::List, false, true }, + {"build", "target", KeyScope::Configuration, KeyMerge::Scalar, true, false}, + // `[workspace.build]` accepts `linkage`; `[build]` reads it from the + // target row and the profile. + // [target.] scalar rows + {"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.", "runner", KeyScope::Configuration, KeyMerge::Scalar, true, true }, + {"target.", "sysroot", KeyScope::Configuration, KeyMerge::Scalar, true, true }, + {"target.", "toolchain", KeyScope::Configuration, KeyMerge::Scalar, true, true }, + // [target.] subtables + {"target..abi", "", KeyScope::Configuration, KeyMerge::Table, true, true }, + {"target..runtime", "", KeyScope::Usage, KeyMerge::Table, true, true }, + {"target..xlings", "", KeyScope::Configuration, KeyMerge::Table, true, true }, + {"target..targets", "", KeyScope::Package, KeyMerge::Table, false, true }, + {"target..dependencies", "", KeyScope::Package, KeyMerge::Table, false, true }, + // whole tables + {"toolchain", "", KeyScope::Configuration, KeyMerge::Table, true, false}, + {"indices", "", KeyScope::Configuration, KeyMerge::Table, true, false}, + {"profile.", "", KeyScope::Configuration, KeyMerge::Table, true, false}, + {"xlings", "", KeyScope::Configuration, KeyMerge::Table, true, false}, + {"package", "", KeyScope::Metadata, KeyMerge::Table, true, false}, + {"targets", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"dependencies", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"dev-dependencies", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"build-dependencies", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"features", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"resources", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"test", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"hooks", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"runtime", "", KeyScope::Package, KeyMerge::Table, false, false}, +}; + +// The keys of `table`, in registry order. +inline std::vector registry_keys(std::string_view table) { + std::vector out; + for (auto const& row : kKeyRegistry) + if (row.table == table && !row.key.empty()) out.push_back(row.key); + return out; +} + +// The keys of `[build]` that may be written under `[target..build]`. +inline std::vector conditional_build_keys() { + std::vector out; + for (auto const& row : kKeyRegistry) + if (row.table == "build" && row.conditional) out.push_back(row.key); + return out; +} + +// The keys `[workspace.target..build]` accepts: shared and conditional. +inline std::vector shared_conditional_build_keys() { + std::vector out; + for (auto const& row : kKeyRegistry) + if (row.table == "build" && row.conditional && row.shared) out.push_back(row.key); + return out; +} + +inline const KeyClass* key_class(std::string_view table, std::string_view key) { + for (auto const& row : kKeyRegistry) + if (row.table == table && row.key == key) return &row; + return nullptr; +} + +} // namespace mcpp::manifest diff --git a/modules/manifest/src/manifest.cppm b/modules/manifest/src/manifest.cppm index dc67805e9..a6ba5605b 100644 --- a/modules/manifest/src/manifest.cppm +++ b/modules/manifest/src/manifest.cppm @@ -21,4 +21,5 @@ export import mcpp.manifest.toml; export import mcpp.manifest.xpkg; export import mcpp.manifest.flag_words; export import mcpp.manifest.cfg_selector; +export import mcpp.manifest.key_registry; export import mcpp.pm.compat.workspace_position; diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 576c1323a..292570fe9 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -3,6 +3,7 @@ export module mcpp.manifest.toml; import mcpp.manifest.types; +import mcpp.manifest.key_registry; import mcpp.manifest.cfg_selector; import mcpp.targetside; import std; @@ -2953,24 +2954,10 @@ std::expected parse_document(const t::Document& documen // policy as [targets.] above: a warning, an error under --strict. // // MUST stay in sync with the `doc->get_*("build.")` reads above. - static constexpr std::string_view kKnownBuildKeys[] = { - "accel", - "allow_host_libs", "bmi_schedule", "build_program_timeout", "c_standard", - "cache", "cflags", "cxxflags", "cxx_runtime", "default-profile", "defines", - "dependency_linkage", - "dialect_cxxflags", "flags", "include_dirs", "include_dirs_after", - "private_include_dirs", - "ios_deployment_target", - "jobs", "ldflags", "macos_deployment_target", "module_extensions", - "platform-dependencies", "profile", - "sources", "static_stdlib", "target", - // #540: read a few hundred lines above and, until now, absent here — - // the SECOND drift of this list, and the comment below narrates the - // first. Moved to `[build]` by #494 precisely so their flags could be - // conditioned; a manifest writing the documented spelling was told the - // key had been ignored while it was taking effect. - "std-compat-module", "std-module", "std-module-flags", - }; + // The keys of `[build]` are the registry's (mcpp.manifest.key_registry): + // the third hand-written copy of this list had drifted twice before it + // was derived (#540). + const auto kKnownBuildKeys = registry_keys("build"); if (auto* bt = doc->get_table("build")) { for (auto& [key, _] : *bt) { bool known = false; @@ -4055,11 +4042,7 @@ std::expected parse_document(const t::Document& documen // is not conditionable and would otherwise vanish without a // word, the #296 failure mode. MUST stay in sync with the reads // above and with types.cppm's BuildInputs and ConditionalConfig. - static constexpr std::string_view kKnownConditionalBuildKeys[] = { - "cflags", "cxxflags", "defines", "dialect_cxxflags", "flags", - "include_dirs", "include_dirs_after", "ldflags", - "private_include_dirs", "sources", "std-module-flags", - }; + const auto kKnownConditionalBuildKeys = conditional_build_keys(); for (auto& [key, _] : bt) { bool known = false; for (auto k : kKnownConditionalBuildKeys) @@ -4528,18 +4511,17 @@ std::expected parse_document(const t::Document& documen "specific artifact, so it belongs in that package's own " "table where the person turning it off owns the result.", sel), value.position)); - // The keys of a shared `.build` are those of - // [workspace.build]: sources and per-glob flags - // name one package's files. + // The keys of a shared `.build` are the shared + // keys a condition may carry (the registry): + // sources and per-glob flags name one package's + // files. if (key == "build" && value.is_table()) { for (auto const& [bk, bv] : value.as_table()) { - if (std::ranges::any_of(kWorkspaceBuildKeys, - [&](auto const& row) { return row.key == bk; })) - continue; + const auto shared = shared_conditional_build_keys(); + if (std::ranges::find(shared, bk) != shared.end()) continue; std::string supported; - for (auto const& row : kWorkspaceBuildKeys) - supported += std::format("{}{}", supported.empty() ? "" : ", ", - row.key); + for (auto k : shared) + supported += std::format("{}{}", supported.empty() ? "" : ", ", k); return std::unexpected(error(origin, std::format( "[workspace.target.{}.build] has no key '{}' (or it is " "not inheritable). Supported: {}.", sel, bk, supported), @@ -4594,7 +4576,7 @@ std::expected parse_document(const t::Document& documen for (auto const& w : layer->schemaWarnings) m.schemaWarnings.push_back( w.starts_with("[") ? "[workspace." + w.substr(1) : w); - m.workspace.layer = std::make_shared(std::move(*layer)); + m.layer = std::make_shared(std::move(*layer)); } } @@ -4602,11 +4584,14 @@ std::expected parse_document(const t::Document& documen // that describes one acts on nothing here. Recorded; the loader warns, // and refuses under --strict. if (!pkg_t && !ctx.workspaceLayer) { - static constexpr std::pair kPackageTables[] = { - {"build", "workspace.build"}, {"targets", ""}, {"dependencies", "workspace.dependencies"}, - {"dev-dependencies", ""}, {"build-dependencies", ""}, {"features", ""}, - {"resources", ""}, {"test", ""}, {"hooks", ""}, {"runtime", ""}, - }; + // The package's own tables, from the registry, and [build]. + std::vector> kPackageTables{ + {"build", "workspace.build"}}; + for (auto const& row : kKeyRegistry) + if (row.key.empty() && row.scope == KeyScope::Package + && row.table.find('.') == std::string_view::npos) + kPackageTables.emplace_back(row.table, + row.table == "dependencies" ? "workspace.dependencies" : ""); auto note = [&](std::string table, std::string mirror) { m.virtualRootPackageTables.push_back(mirror.empty() ? std::format("[{}] is written on a workspace root without [package], " diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index cb842a319..7deec175b 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1849,8 +1849,6 @@ inline constexpr std::array kWorkspaceBuildKeys{{ {"ios_deployment_target", &BuildConfig::iosDeploymentTarget}, }}; -struct Manifest; - struct WorkspaceConfig { std::vector members; // relative paths to member dirs std::vector exclude; // paths to exclude @@ -1858,19 +1856,8 @@ struct WorkspaceConfig { WorkspaceInherited inherited; // [workspace.package|build] bool present = false; - // THE WORKSPACE LAYER: what `[workspace.toolchain]`, `[workspace.indices]`, - // `[workspace.profile.]`, `[workspace.target.]` (with its - // `.build`, `.abi`, `.runtime` and `.xlings` subtables) and - // `[workspace.xlings]` say to every member (SPEC-004 §9.10, W1-W3). - // - // Read by the reader of the tables they mirror -- the text under - // `workspace.` is a manifest of its own, without `[package]` -- so the - // keys, subtables and selectors of `[workspace.X]` are those of `X` by - // construction, not by a second list. Null when none of them is written. - // `layerTables` names the ones that are (`toolchain`, `indices`, - // `profile`, `target`, `xlings`); a workspace root that writes one of - // them also at the root position is refused for that table. - std::shared_ptr layer; + // The `[workspace.X]` tables the root writes (`toolchain`, `indices`, + // `profile`, `target`, `xlings`); what they say is `Manifest::layer`. std::vector layerTables; }; @@ -2064,6 +2051,19 @@ struct Manifest { // manifest is read on two paths before it is planned, and an appended // vector must be appended once. bool inheritedAsRootPackage = false; + // THE WORKSPACE LAYER: what `[workspace.toolchain]`, `[workspace.indices]`, + // `[workspace.profile.]`, `[workspace.target.]` (with its + // `.build`, `.abi`, `.runtime` and `.xlings` subtables) and + // `[workspace.xlings]` say to every member (SPEC-004 §9.10, W1-W3). Set + // on a workspace root only; `workspace.layerTables` names the tables. + // + // Read by the reader of the tables they mirror -- the document under + // `workspace.` is a manifest of its own, without `[package]` -- so the + // keys, subtables and selectors of `[workspace.X]` are those of `X` by + // construction, not by a second list. Null when none of them is written. + // A workspace root without `[package]` that writes one of them also at + // the root position is refused for that table. + std::shared_ptr layer; // W7: the package tables a workspace root WITHOUT `[package]` wrote // (`[build]`, `[targets]`, `[dependencies]`, ...). They act on no // package there; the parser records them, the loader warns (`--strict`: diff --git a/src/build/prepare/manifest.cpp b/src/build/prepare/manifest.cpp index a5dc968d9..72de01f36 100644 --- a/src/build/prepare/manifest.cpp +++ b/src/build/prepare/manifest.cpp @@ -168,7 +168,7 @@ report_workspace_root(const mcpp::manifest::Manifest& ws, bool strict) { if (strict) return std::unexpected(w); mcpp::diag::warning("manifest/schema", w); } - if (ws.workspace.layer) { + if (ws.layer) { const auto have = mcpp::xpkg_version::parse(mcpp::MCPP_VERSION); const auto floor = ws.workspace.inherited.mcppFloor; const auto need = floor.empty() ? std::nullopt : mcpp::xpkg_version::parse(floor); diff --git a/src/project.cppm b/src/project.cppm index ed003b9c7..8f98ffbdb 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -366,7 +366,7 @@ void merge_layer_root_position(mcpp::manifest::Manifest& member, export void inherit_workspace_xlings(mcpp::manifest::Manifest& member, const mcpp::manifest::Manifest& workspace) { static const std::vector none; - if (auto const* layer = workspace.workspace.layer.get()) { + if (auto const* layer = workspace.layer.get()) { auto const& tables = workspace.workspace.layerTables; detail::merge_xlings(member, detail::lists(tables, "xlings") ? &layer->xlings : nullptr, @@ -389,7 +389,7 @@ export void inherit_workspace_xlings(mcpp::manifest::Manifest& member, export void inherit_workspace_layer_rows(mcpp::manifest::Manifest& member, const mcpp::manifest::Manifest& workspace, const std::filesystem::path& wsRoot) { - auto const* layer = workspace.workspace.layer.get(); + auto const* layer = workspace.layer.get(); if (!layer || layer->conditionalConfigs.empty()) return; auto anchor = [&](std::vector& dirs) { for (auto& d : dirs) @@ -417,7 +417,7 @@ export void inherit_workspace_layer_rows(mcpp::manifest::Manifest& member, export void inherit_workspace_root_position(mcpp::manifest::Manifest& member, const mcpp::manifest::Manifest& workspace, const std::filesystem::path& wsRoot) { - if (auto const* layer = workspace.workspace.layer.get()) + if (auto const* layer = workspace.layer.get()) detail::merge_layer_root_position(member, *layer, wsRoot, workspace.sourcePath); // COMPAT(workspace-position) const auto position = mcpp::pm::compat::position_tables(workspace); @@ -446,7 +446,7 @@ export void inherit_as_root_package(mcpp::manifest::Manifest& root, const auto workspace = root; inherit_workspace_package(root, workspace); inherit_workspace_build(root, workspace, wsRoot); - if (auto const* layer = workspace.workspace.layer.get()) { + if (auto const* layer = workspace.layer.get()) { static const std::vector none; auto const& tables = workspace.workspace.layerTables; detail::merge_layer_root_position(root, *layer, wsRoot, workspace.sourcePath); @@ -924,6 +924,13 @@ export std::string root_position_key( for (auto const& [platform, spec] : m.toolchain.byPlatform) field("toolchain." + platform, spec); for (auto const& [triple, e] : m.targetOverrides) { + // A row with no scalar written (a `[target..build]`-only header) + // states no configuration; a row whose selector does not hold for + // 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(); + 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); diff --git a/src/publish/normalize.cppm b/src/publish/normalize.cppm index 0a1e8d482..a7bd85f55 100644 --- a/src/publish/normalize.cppm +++ b/src/publish/normalize.cppm @@ -393,6 +393,95 @@ normalize_for_publish(const std::filesystem::path& packageDir, } } + // W6: what the workspace layer gave the member is written into the + // member's own tables -- `[workspace.toolchain]` as `[toolchain]`, + // `[workspace.target..build]` as `[target..build]`, and so on -- + // merged as the build merged it (W3): a key the member wrote is the + // member's, a list has the workspace's entries first. The archive is then + // complete on its own (SPEC-004 §9.7), and the package built as the root of + // a host-tool sub-build is configured as it was in its workspace. A + // relative path was written against the workspace root; it is rewritten + // against the package, and refused when it leaves the package. + if (effective.member && effective.workspace && effective.workspace->layer) { + auto wsDoc = t::parse_file(effective.workspaceRoot / "mcpp.toml"); + if (!wsDoc) return std::unexpected(std::format("{}: {}", + (effective.workspaceRoot / "mcpp.toml").string(), wsDoc.error().message)); + const auto* wsTable = wsDoc->get_table("workspace"); + static constexpr std::string_view kPathKeys[] = { + "include_dirs", "include_dirs_after", "private_include_dirs", "link_library_dirs", "path", + }; + std::function(t::Table&, const t::Table&, const std::string&)> + merge = [&](t::Table& into, const t::Table& from, const std::string& at) + -> std::expected { + for (auto const& [key, value] : from) { + const auto here = at.empty() ? key : at + "." + key; + auto value2 = value; + if (std::ranges::find(kPathKeys, key) != std::end(kPathKeys)) { + auto rebase = [&](const std::string& p) -> std::expected { + std::filesystem::path path(p); + if (path.is_absolute()) return p; + const auto abs = (effective.workspaceRoot / path).lexically_normal(); + if (!inside(packageDir, abs)) + return std::unexpected(std::format( + "{}: [workspace.{}] entry '{}' resolves to '{}', outside this " + "package's directory, and the published archive contains only " + "that directory. Move it into the package, or declare it in the " + "package's own table for its own build only.", + manifestPath.string(), here, p, abs.string())); + return abs.lexically_relative(packageDir.lexically_normal()).generic_string(); + }; + if (value.is_string()) { + auto r = rebase(value.as_string()); + if (!r) return std::unexpected(r.error()); + value2 = t::Value{*r}; + } else if (value.is_array()) { + t::Array a; + for (auto const& e : value.as_array()) { + if (!e.is_string()) { a.push_back(e); continue; } + auto r = rebase(e.as_string()); + if (!r) return std::unexpected(r.error()); + a.emplace_back(*r); + } + value2 = t::Value{std::move(a)}; + } + } + auto it = into.find(key); + if (it == into.end()) { + if (value2.is_table() && wsDoc->has_explicit_table("workspace." + here)) + explicitTables.insert(here); + into.emplace(key, std::move(value2)); + changed = true; + continue; + } + if (it->second.is_table() && value2.is_table()) { + if (auto r = merge(it->second.as_table(), value2.as_table(), here); !r) return r; + } else if (it->second.is_array() && value2.is_array()) { + auto joined = value2.as_array(); + for (auto const& e : it->second.as_array()) joined.push_back(e); + it->second = t::Value{std::move(joined)}; + changed = true; + } + // A scalar the member wrote is the member's. + } + return {}; + }; + if (wsTable) + for (auto const& name : effective.workspace->workspace.layerTables) { + auto src = wsTable->find(name); + if (src == wsTable->end() || !src->second.is_table()) continue; + auto* dst = table_at(root, name, name, explicitTables); + if (!dst) return std::unexpected(std::format( + "{}: `{}` is not a table", manifestPath.string(), name)); + auto from = src->second.as_table(); + // The member's own `default` speaks for every platform, as in + // `merge_layer_root_position`. + if (name == "toolchain" && dst->contains("default")) + std::erase_if(from, [](auto const& kv) { return kv.first != "bootstrap"; }); + if (auto r = merge(*dst, from, name); !r) + return std::unexpected(r.error()); + } + } + // Dependencies: the unconditional sections and every conditional one. Walk walk{ packageDir, effective, *doc, manifestPath }; for (auto section : {"dependencies", "build-dependencies", "dev-dependencies"}) { diff --git a/tests/unit/test_key_registry.cpp b/tests/unit/test_key_registry.cpp new file mode 100644 index 000000000..b096647b2 --- /dev/null +++ b/tests/unit/test_key_registry.cpp @@ -0,0 +1,265 @@ +#include + +import std; +import mcpp.manifest; +import mcpp.project; + +// SPEC-004 §9.10: the key registry, the workspace layer and the merge rules +// (W1-W3, W7). The end-to-end halves are tests/e2e/893-895 and 900. + +namespace mm = mcpp::manifest; + +namespace { + +mm::Manifest parse(std::string_view text) { + auto m = mm::parse_string(text); + if (!m) ADD_FAILURE() << m.error().format(); + return m ? *m : mm::Manifest{}; +} + +std::string parse_error(std::string_view text) { + auto m = mm::parse_string(text); + return m ? std::string{} : m.error().message; +} + +} // namespace + +// The inheritable subset of [build] is stated by two tables: the registry's +// `shared` column and `kWorkspaceBuildKeys`, which also carries the field each +// key fills. They must name the same keys; `linkage` is the one key +// [workspace.build] reads that [build] does not (the target row and the +// profile set it there). +TEST(KeyRegistry, TheSharedBuildKeysAreTheWorkspaceBuildKeys) { + std::set shared, table; + for (auto const& row : mm::kKeyRegistry) + if (row.table == "build" && row.shared) shared.emplace(row.key); + for (auto const& row : mm::kWorkspaceBuildKeys) + if (row.key != "linkage") table.emplace(row.key); + EXPECT_EQ(shared, table); +} + +// Every configuration key of [build] is part of the value members are grouped +// by: two members that differ in one are not one plan. +TEST(KeyRegistry, EveryConfigurationKeyEntersTheGroupingKey) { + const auto key = mcpp::project::root_position_key(mm::Manifest{}); + const std::map spelled = { + {"profile", "default_profile"}, {"default-profile", "default_profile"}, + {"platform-dependencies", "platform_dependencies"}, + }; + for (auto const& row : mm::kKeyRegistry) { + if (row.table != "build" || row.scope != mm::KeyScope::Configuration) continue; + auto it = spelled.find(row.key); + const std::string name(it == spelled.end() ? row.key : it->second); + EXPECT_NE(key.find(name + "="), std::string::npos) << row.key; + } +} + +// The parser accepts every [build] key of the registry without a warning. +TEST(KeyRegistry, TheParserReadsTheRegistrysBuildKeys) { + auto m = parse(R"( +[package] +name = "x" +version = "0.1.0" +[build] +sources = ["src/*.cpp"] +cxxflags = ["-O2"] +dialect_cxxflags = ["-DX=1"] +cache = "local" +)"); + EXPECT_TRUE(m.schemaWarnings.empty()); + auto w = parse(R"( +[package] +name = "x" +version = "0.1.0" +[build] +cxxflagz = ["-O2"] +)"); + ASSERT_EQ(w.schemaWarnings.size(), 1u); + EXPECT_NE(w.schemaWarnings.front().find("cxxflagz"), std::string::npos); +} + +// W2: [workspace.X] is read by the reader of X. +TEST(WorkspaceLayer, MirrorsAreReadByTheReaderOfTheTableTheyMirror) { + auto ws = parse(R"( +[workspace] +members = ["a"] +[workspace.toolchain] +default = "llvm@23.1.3" +[workspace.profile.release] +opt = 3 +[workspace.target.x86_64-linux-musl] +linkage = "static" +[workspace.target.'cfg(os = "linux")'.build] +dialect_cxxflags = ["-DW=1"] +cxxflags = ["-DSHARED"] +[workspace.xlings.workspace] +cmake = "4.0.2" +)"); + ASSERT_TRUE(ws.layer); + auto const& layer = *ws.layer; + EXPECT_EQ(layer.toolchain.for_platform("linux").value_or(""), "llvm@23.1.3"); + ASSERT_TRUE(layer.profiles.contains("release")); + EXPECT_EQ(layer.profiles.at("release").optLevel, "3"); + EXPECT_TRUE(layer.profiles.at("release").optDeclared); + EXPECT_FALSE(layer.profiles.at("release").debugDeclared); + EXPECT_EQ(layer.targetOverrides.at("x86_64-linux-musl").linkage, "static"); + ASSERT_FALSE(layer.conditionalConfigs.empty()); + EXPECT_EQ(ws.workspace.layerTables, + (std::vector{"toolchain", "profile", "target", "xlings"})); + // The root itself states none of them for its own package. + EXPECT_TRUE(ws.toolchain.byPlatform.empty()); + EXPECT_TRUE(ws.targetOverrides.empty()); +} + +TEST(WorkspaceLayer, RefusesWhatDescribesOnePackageAndWhatItDoesNotKnow) { + EXPECT_NE(parse_error(R"( +[workspace] +members = ["a"] +[workspace.tolchain] +default = "gcc@16.1.0" +)").find("[workspace] has no key or table 'tolchain'"), std::string::npos); + EXPECT_NE(parse_error(R"( +[workspace] +members = ["a"] +[workspace.target.x86_64-linux-gnu.dependencies] +fmt = "11.0.0" +)").find("describes one package"), std::string::npos); + EXPECT_NE(parse_error(R"( +[workspace] +members = ["a"] +[workspace.target.x86_64-linux-gnu.build] +sources = ["x.cpp"] +)").find("has no key 'sources'"), std::string::npos); + EXPECT_NE(parse_error(R"( +[workspace] +members = ["a"] +[workspace.target.x86_64-linux-gnu.build] +allow_host_libs = true +)").find("allow_host_libs is not inheritable"), std::string::npos); + // On a root without [package], one table cannot speak to members twice. + EXPECT_NE(parse_error(R"( +[workspace] +members = ["a"] +[toolchain] +default = "gcc@16.1.0" +[workspace.toolchain] +default = "llvm@23.1.3" +)").find("write it once, as [workspace.toolchain]"), std::string::npos); + // With [package], the root-position table is the root package's own. + EXPECT_TRUE(parse_error(R"( +[package] +name = "root" +version = "0.1.0" +[workspace] +members = ["a"] +[toolchain] +default = "gcc@16.1.0" +[workspace.toolchain] +default = "llvm@23.1.3" +)").empty()); +} + +// W7: a package table on a root without [package] acts on nothing. +TEST(WorkspaceLayer, PackageTablesOnAVirtualRootAreRecorded) { + auto ws = parse(R"( +[workspace] +members = ["a"] +[build] +cxxflags = ["-O2"] +[target.'cfg(os = "linux")'.build] +dialect_cxxflags = ["-DX=1"] +)"); + ASSERT_EQ(ws.virtualRootPackageTables.size(), 2u); + EXPECT_NE(ws.virtualRootPackageTables[0].find("[build]"), std::string::npos); + EXPECT_NE(ws.virtualRootPackageTables[1].find("[workspace.target."), std::string::npos); + auto rooted = parse(R"( +[package] +name = "root" +version = "0.1.0" +[workspace] +members = ["a"] +[build] +cxxflags = ["-O2"] +)"); + EXPECT_TRUE(rooted.virtualRootPackageTables.empty()); +} + +// W3: scalars the member declared are the member's, named tables merge key by +// key, lists put the workspace first, conditional rows come first. +TEST(WorkspaceLayer, TheMergeRuleIsTheKeys) { + auto ws = parse(R"( +[workspace] +members = ["a"] +[workspace.toolchain] +linux = "llvm@23.1.3" +macos = "llvm@23.1.3" +[workspace.profile.release] +opt = 3 +lto = true +cxxflags = ["-DWS"] +[workspace.target.x86_64-linux-musl] +linkage = "static" +cxx_runtime = "self-contained" +[workspace.target.'cfg(os = "linux")'.build] +dialect_cxxflags = ["-DW=1"] +)"); + auto member = parse(R"( +[package] +name = "a" +version = "0.1.0" +[toolchain] +linux = "gcc@16.1.0" +[profile.release] +opt = 2 +cxxflags = ["-DMEMBER"] +[target.x86_64-linux-musl] +linkage = "dynamic" +[target.'cfg(os = "linux")'.build] +dialect_cxxflags = ["-DM=1"] +)"); + mcpp::project::inherit_workspace_root_position(member, ws, "/ws"); + mcpp::project::inherit_workspace_layer_rows(member, ws, "/ws"); + EXPECT_EQ(member.toolchain.for_platform("linux").value_or(""), "gcc@16.1.0"); + EXPECT_EQ(member.toolchain.for_platform("macos").value_or(""), "llvm@23.1.3"); + auto const& r = member.profiles.at("release"); + EXPECT_EQ(r.optLevel, "2"); + EXPECT_TRUE(r.lto); + EXPECT_EQ(r.cxxflags, (std::vector{"-DWS", "-DMEMBER"})); + auto const& t = member.targetOverrides.at("x86_64-linux-musl"); + EXPECT_EQ(t.linkage, "dynamic"); + EXPECT_EQ(t.cxxRuntime, "self-contained"); + ASSERT_GE(member.conditionalConfigs.size(), 2u); + EXPECT_EQ(member.conditionalConfigs.front().dialectCxxflags, + (std::vector{"-DW=1"})); + + // A member's own `default` speaks for every platform. + auto own = parse(R"( +[package] +name = "b" +version = "0.1.0" +[toolchain] +default = "gcc@16.1.0" +)"); + mcpp::project::inherit_workspace_root_position(own, ws, "/ws"); + EXPECT_EQ(own.toolchain.for_platform("macos").value_or(""), "gcc@16.1.0"); +} + +// W4: grouping counts a conditional configuration row as it evaluates. +TEST(WorkspaceLayer, GroupingReadsEvaluatedRows) { + auto a = parse(R"( +[package] +name = "a" +version = "0.1.0" +[target.'cfg(os = "windows")'.build] +dialect_cxxflags = ["-DWIN=1"] +)"); + auto b = parse(R"( +[package] +name = "b" +version = "0.1.0" +)"); + auto linux = [](std::string_view p) { return p.find("linux") != std::string_view::npos; }; + auto windows = [](std::string_view p) { return p.find("windows") != std::string_view::npos; }; + EXPECT_EQ(mcpp::project::root_position_key(a, linux), mcpp::project::root_position_key(b, linux)); + EXPECT_NE(mcpp::project::root_position_key(a, windows), mcpp::project::root_position_key(b, windows)); +} From bc289c8e7b25e3df04d68f5c407871730fca5488 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:23:39 +0900 Subject: [PATCH 04/32] fix(modules): the module declaration decides the role, the extension the language (#790); cl.exe implementation partitions get /internalPartition --- modules/toolchain-model/src/model.cppm | 10 ++++++++++ src/build/ninja_backend.cppm | 16 ++++++++++++---- src/build/plan.cppm | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index 032d8787b..c3cd904ff 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -545,6 +545,13 @@ struct BmiTraits { // one compiler and not the other, and the split was one flag wide. std::string_view moduleImplLangFlag; // " -x c++" | " /TP" + // How this compiler is told that a translation unit is an IMPLEMENTATION + // PARTITION (`module M:part;`): it writes a BMI and is not an interface. + // Measured on cl.exe 14.51 (design 2026-10-10 §5.2): `/internalPartition` + // is required -- no option is C7621 for a `.cpp`, `/interface` is C3474. + // Clang and GCC compile one as they compile an interface. + std::string_view moduleImplPartitionLangFlag; // " -x c++" | " -x c++-module" | " /internalPartition /TP" + // Non-empty ⇔ the driver can emit the BMI *and stop*, producing the SAME // BMI an ordinary compile of that TU would have produced. Both halves // matter, and the second one is the trap. @@ -715,6 +722,7 @@ BmiTraits bmi_traits(const Toolchain& tc) { // is the half that says "this is C++". A non-interface unit keeps // the second and drops the first. .moduleImplLangFlag = " /TP", + .moduleImplPartitionLangFlag = " /internalPartition /TP", }; } if (is_clang(tc)) { @@ -733,6 +741,7 @@ BmiTraits bmi_traits(const Toolchain& tc) { .bmiSearchPrefix = " -fprebuilt-module-path=", .moduleInterfaceLangFlag = " -x c++-module", .moduleImplLangFlag = " -x c++", + .moduleImplPartitionLangFlag = " -x c++-module", .bmiOnlyFlags = " --precompile -Xclang -emit-reduced-module-interface", }; } @@ -758,6 +767,7 @@ BmiTraits bmi_traits(const Toolchain& tc) { // The same flag, and for GCC that is not a coincidence: since GCC reads // interface-ness from the content, both answers are "this is C++". .moduleImplLangFlag = " -x c++", + .moduleImplPartitionLangFlag = " -x c++", }; } diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 1267d85da..ed6526f05 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -2518,11 +2518,18 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // inhabitant of a `.cppm` and provides nothing. That second question is // answered here, once, from `cu.providesModule` — the same field `bmi_out` // is bound from, so the flag and the binding can no longer disagree. + // + // An implementation partition (`module M:part;`) writes a BMI but is not + // an interface, and cl.exe is told so: `/internalPartition`, measured + // (design 2026-10-10 §5.2) -- `/interface` on one is C3474 and no option + // is C7621. Clang and GCC compile it as they compile an interface. auto module_edge_vars = [&](const mcpp::build::CompileUnit& cu) -> std::string { if (cu.providesModule.empty()) return std::format(" module_lang ={}\n", traits.moduleImplLangFlag); std::string v = std::format(" module_lang ={}\n", - traits.moduleInterfaceLangFlag); + cu.is_implementation_partition() + ? traits.moduleImplPartitionLangFlag + : traits.moduleInterfaceLangFlag); if (traits.needsExplicitModuleOutput) v += std::format(" module_output ={}{}\n", traits.moduleOutputPrefix, unit_bmi(cu)); @@ -2530,6 +2537,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, }; auto pick_rule = [](const mcpp::build::CompileUnit& cu) -> std::string { + if (cu.uses_module_rule()) return "cxx_module"; switch (cu.kind) { case mcpp::SourceKind::ModuleInterface: return "cxx_module"; case mcpp::SourceKind::C: return "c_object"; @@ -2837,7 +2845,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, if (cu.servedFromCache) continue; if (is_scan_exempt(cu)) continue; if (cu.providesModule.empty()) continue; - if (cu.kind != mcpp::SourceKind::ModuleInterface) continue; + if (!cu.uses_module_rule()) continue; two_phase_ddi.insert( (cu.object.parent_path() / cu.source.filename()).string() + ".ddi"); } @@ -2883,7 +2891,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, std::string rule = pick_rule(cu); if (splitBmi && !cu.providesModule.empty() && - cu.kind == mcpp::SourceKind::ModuleInterface) { + cu.uses_module_rule()) { const auto bmi = unit_bmi(cu); const auto obj = escape_ninja_path(cu.object); const auto slot = obj + ".sched"; @@ -2948,7 +2956,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, } if (twoPhase && !cu.providesModule.empty() && - cu.kind == mcpp::SourceKind::ModuleInterface) { + cu.uses_module_rule()) { const auto bmi = unit_bmi(cu); const auto obj = escape_ninja_path(cu.object); const auto ddi = (cu.object.parent_path() / cu.source.filename()) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index c03dc0710..e4f1aa87c 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -74,6 +74,25 @@ struct CompileUnit { // The module declaration form the scanner read (mcpp.modgraph.graph). // Stated as the unit's role by the build database. mcpp::modgraph::ModuleDeclaration declaration = mcpp::modgraph::ModuleDeclaration::None; + + // THE EXTENSION SAYS WHICH LANGUAGE; THE MODULE DECLARATION SAYS WHICH + // ROLE (D8, #790). A C++ unit that provides a module -- an interface, or + // either kind of partition -- writes a BMI and takes the module rule, + // whatever its extension; a module-extension file takes it as well, for + // its depfile handling, even when it holds an implementation unit. Read + // by every place that chooses a rule or a BMI edge, so the rule, the BMI + // the edge declares and the flags the edge passes cannot disagree. + // Before this the choice was the extension's: a `.cpp` holding + // `module m:part;` declared a BMI its `cxx_object` edge never wrote, and + // was rebuilt by every build (R4); `export module m;` in a `.cpp` was not + // found by its importers under Clang (R5). + bool is_implementation_partition() const { + return declaration == mcpp::modgraph::ModuleDeclaration::ImplementationPartition; + } + bool uses_module_rule() const { + return kind == mcpp::SourceKind::ModuleInterface + || (kind == mcpp::SourceKind::Cxx && !providesModule.empty()); + } // This unit's outputs are already in the global cache: the backend emits // `stage_file` edges from the cache instead of a compile edge (and skips // the P1689 scan for it entirely). The unit itself stays in the plan so From 904664a7e96ba413ce73f682fadef0cae1a15c74 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:30:20 +0900 Subject: [PATCH 05/32] fix(build-program): an action's output has one writer, and out_dir is per configuration (#778); resolution.json records outDir --- modules/buildmcpp/src/directives.cppm | 72 +++++++-------------------- modules/manifest/src/types.cppm | 5 ++ src/build/build_program.cppm | 6 ++- src/build/prepare/features.cpp | 3 +- src/build/prepare/graph.cpp | 41 +++++++++++++-- src/build/prepare/state.cppm | 8 +++ src/build/prepare/target_side.cpp | 14 ++++++ src/modgraph/scanner.cppm | 20 +++++++- 8 files changed, 108 insertions(+), 61 deletions(-) diff --git a/modules/buildmcpp/src/directives.cppm b/modules/buildmcpp/src/directives.cppm index aff2044c6..30bdb1681 100644 --- a/modules/buildmcpp/src/directives.cppm +++ b/modules/buildmcpp/src/directives.cppm @@ -657,35 +657,19 @@ std::string target_directive_error(const mcpp::manifest::Manifest& m, const Dire // both the directive and the declaring package, like `target_directive_error`. std::string deploy_directive_error(const mcpp::manifest::Manifest& m, const Directives& d); -// Resolve an action's paths against `pkgRoot` and make its Source outputs -// exist, so the ordinary source scan can see them. +// Resolve an action's paths against `pkgRoot`. // -// A placeholder rather than a synthesised CompileUnit, because that reuses -// every existing mechanism: the glob finds it, the scanner reads it, the plan -// gives it an object path, and ninja overwrites it with the real content -// before the compile edge runs (the compile depends on the action's output). -// -// For a module interface the placeholder carries the DECLARED interface — -// `export module X;` plus its imports — so the prepare-time scan agrees with -// what the generator will emit. That is the same assertion-plus-verification -// trade `[modules].scan_overrides` makes: the declaration is checked against -// the compiler's own P1689 output at build time, so a wrong one is caught -// rather than silently believed. -// -// Never truncates an existing file: after the first build the real content is -// there, and rewriting it would make ninja think the input changed on every -// prepare. -// -// ONLY FOR OUTPUTS THAT ARE TRANSLATION UNITS, which is why this needs the -// table. A placeholder exists so the SCAN has something to read, and the scan -// never reads a header — but writing one anyway turned "the generator did not -// run" into "the header is empty", and mcpp#534 was diagnosed as a race for -// exactly that reason: the file was on disk, so the action looked like it had -// run. A missing file is the honest report, and after the ordering fix the -// generator runs before anything reads it either way. +// It writes nothing. A Source action's outputs are written by the action and +// by nothing else (D6, design 2026-10-10 §4): the placeholder this used to +// write for the scan was newer than the action's inputs, so a ninja log that +// had recorded the action took the placeholder for its output (#778), and an +// output directory shared by two configurations held one file that both of +// their logs believed current (R2). The units the action generates enter the +// scan from its declaration instead (`Modules::declaredUnits`): what it says +// it provides and imports, checked against the compiler's own P1689 output at +// build time, the trade `[modules].scan_overrides` makes. void prepare_actions(std::vector& actions, - const std::filesystem::path& pkgRoot, - const mcpp::ExtensionTable& extensions); + const std::filesystem::path& pkgRoot); // Does this action output belong in the COMPILE set? // @@ -1448,8 +1432,7 @@ bool is_compilable_output(const fs::path& p, const mcpp::ExtensionTable& t) { } void prepare_actions(std::vector& actions, - const fs::path& pkgRoot, - const mcpp::ExtensionTable& extensions) { + const fs::path& pkgRoot) { for (auto& a : actions) { auto absolutize = [&](std::vector& v) { for (auto& p : v) { @@ -1482,30 +1465,13 @@ void prepare_actions(std::vector& actions, // the action (SPEC-007 R2.2), never the build directory. if (!a.cwd.empty() && a.cwd.find("${mcpp.") == std::string::npos) a.cwd = abs_against(pkgRoot, a.cwd); - if (a.role != mcpp::manifest::BuildAction::Role::Source) continue; - for (auto const& o : a.outputs) { - if (o.find("${mcpp.") != std::string::npos) continue; - // A placeholder exists so the scan has a translation unit to read. - // A header is not one — nothing scans it, and the empty file it - // used to leave behind is what made a generator that never ran - // look like one that had (mcpp#534). - if (!is_compilable_output(o, extensions)) continue; - std::error_code ec; - fs::path p(o); - if (fs::exists(p, ec)) continue; // real content already there - fs::create_directories(p.parent_path(), ec); - std::ofstream os(p, std::ios::trunc); - if (!os) continue; - if (!a.provides.empty()) { - os << "// placeholder — replaced by action '" << a.id - << "' during the build\n"; - for (auto const& imp : a.imports) os << "import " << imp << ";\n"; - os << "export module " << a.provides.front() << ";\n"; - } - // A non-module output needs nothing: an empty TU scans as - // "provides nothing, imports nothing", which is what a plain - // generated .cpp/.cc is. - } + // Nothing is written for a Source action's outputs: the action is the + // only writer of what it outputs (D6). A placeholder written here was + // newer than the action's inputs, so a ninja log that recorded the + // action took it for the generated file (#778), and with an `out_dir` + // shared by configurations each log thought the other's output + // current. The generated units enter the scan as declared units + // (`Modules::declaredUnits`). } } diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index 7deec175b..26f070098 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -123,6 +123,11 @@ struct Modules { bool strict = false; // glob → declared scan result; every glob must match ≥1 source file. std::map scanOverrides; + // The translation units a source action of this package generates, by + // absolute path, with what the action declares they provide and import + // (D6). They enter the scan as declared, never read: the action is the + // only writer of its output, and before the build it may not exist. + std::map declaredUnits; }; // The accepted values of `windows_subsystem` (`subsystem = true`) and diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index b8286f442..41485ccfb 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -289,6 +289,10 @@ struct BuildProgramEnv { // root-project default). Dependencies MUST point this into the CONSUMING // project's tree — a registry package root is shared and may be read-only. std::filesystem::path artifactsDir; + // The program's output directory, `mcpp::out_dir()` (D7). Empty → + // /out, a directory every configuration shares, which is + // what a caller with no configuration (a test) gets. + std::filesystem::path outDir; // Base for resolving relative `mcpp:generated=` paths. Empty → root (the // root-project contract, unchanged). Dependencies point this at OUT_DIR so // a shared package root is never written to. @@ -1225,7 +1229,7 @@ std::expected run_build_program_impl( } fs::path bdir = build_dir(root, env); - fs::path outDir = bdir / "out"; + fs::path outDir = env.outDir.empty() ? bdir / "out" : env.outDir; auto childEnv = contract_env(root, outDir, env, mode != ProgramMode::Compile); std::string ctxHash = contract_hash(childEnv); // THE GRAPH DOCUMENT'S CONTENT, NOT ITS PATH. The path is the same on diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 8d2f294ae..1c38fa2ea 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -1941,7 +1941,8 @@ static std::expected step6_dependency_build_programs(PrepareS bpEnv.features = feature_closure(pkg.manifest, req, depDefaultFeatures); bpEnv.artifactsDir = state.workRoot / "target" / ".build-mcpp" / "deps" / (dirSafe(pkg.manifest.package.name) + "@" + pkg.manifest.package.version); - bpEnv.genBase = bpEnv.artifactsDir / "out"; + bpEnv.outDir = state.programOutDir(pkg.manifest, bpEnv.features); + bpEnv.genBase = bpEnv.outDir; // What the program imports is kept once for the workspace, and in the // global cache where it comes from the engine or the index (#748). bpEnv.moduleStore = state.workRoot / "target" / ".build-mcpp" / "host-modules"; diff --git a/src/build/prepare/graph.cpp b/src/build/prepare/graph.cpp index 4df4cb58c..35e9c9066 100644 --- a/src/build/prepare/graph.cpp +++ b/src/build/prepare/graph.cpp @@ -2159,6 +2159,25 @@ step4b_define_provisioning_closures(PrepareState& state) { // (the scanner walks the legacy modules.sources mirror). ninja overwrites // the placeholder before the compile edge runs, because that compile // depends on the action's output. + state.programOutDir = [&state](const mcpp::manifest::Manifest& pm, + const std::vector& features) { + // What changes the program's environment and is known before it runs. + std::string id = std::format("target={}\nprofile={}\naccel={}\n", + state.resolvedTargetCanonical, state.effectiveProfile, state.resolvedAccel()); + if (state.tc) + id += std::format("toolchain={}@{}\n", state.tc->binaryPath.generic_string(), + state.tc->version); + auto sorted = features; + std::ranges::sort(sorted); + for (auto const& f : sorted) id += "feature=" + f + "\n"; + std::string name = pm.package.namespace_.empty() + ? pm.package.name : pm.package.namespace_ + "." + pm.package.name; + for (auto& c : name) + if (!std::isalnum(static_cast(c)) && c != '.' && c != '-' && c != '_') c = '_'; + return state.workRoot / "target" / ".build-mcpp" / "out" + / mcpp::toolchain::hash_string(id) / (name.empty() ? std::string("root") : name); + }; + state.adoptActionOutputs = [](mcpp::manifest::Manifest& mm, const std::filesystem::path& pkgRoot, std::size_t firstNewAction) { @@ -2169,13 +2188,11 @@ step4b_define_provisioning_closures(PrepareState& state) { mm.buildConfig.actions.end()); // The package that DECLARED the outputs classifies them: a dependency // generating a `.ixx` asks its own manifest, not the root project's. - // Built once per package, not once per output — and BEFORE - // `prepare_actions`, which needs the same table to decide which - // outputs get a placeholder (a header does not; see mcpp#534). + // Built once per package, not once per output. const auto pkgExtTable = mcpp::extension_table_for(mm.buildConfig.moduleExtensions, mm.buildConfig.deviceExtensions); - mcpp::build::directives::prepare_actions(fresh, pkgRoot, pkgExtTable); + mcpp::build::directives::prepare_actions(fresh, pkgRoot); std::copy(fresh.begin(), fresh.end(), mm.buildConfig.actions.begin() + static_cast(firstNewAction)); @@ -2189,6 +2206,22 @@ step4b_define_provisioning_closures(PrepareState& state) { continue; mm.buildConfig.sources.push_back(o); mm.modules.sources.push_back(o); + // A declared unit: the scan takes what the action says the + // file provides and imports, and reads nothing (D6). A unit + // that declares no `provides` asserts nothing; the build's own + // scan finds its imports, as it did with the empty placeholder. + mm.modules.declaredUnits[o] = mcpp::manifest::ScanOverride{a.provides, a.imports}; + // D26: a generated module interface must say which module it + // is. Without `.provides()` nothing writes its BMI, and the + // first importer fails far from the action. + if (a.provides.empty() + && mcpp::classify(o, pkgExtTable) == mcpp::SourceKind::ModuleInterface) + mcpp::diag::degraded("build-program/provides", std::format( + "action '{}' generates the module-interface file '{}' and declares no " + "`.provides(...)`", a.id, o), + "the module it defines has no BMI in this build, so an import of it fails", + "declare the module the generated file defines: " + "mcpp::action(...).provides(\"\")"); } } }; diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 7e71be3bb..359bd28ce 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -621,6 +621,14 @@ struct PrepareState { std::function computeUsageRequirements; std::function adoptActionOutputs; + // A package's build-program output directory (`mcpp::out_dir()`, + // MCPP_OUT_DIR) for this build's configuration (D7, D25): below the plan + // root, one directory per configuration -- the target, the profile, the + // toolchain, the accelerators and the features the package is built + // with -- and per package. The program binary and its cache stay in the + // configuration-independent `.build-mcpp`. + std::function&)> programOutDir; std::function fillXpkgDirs; std::functionsecond.answer.linkage))}, {"reason", form->second.answer.reason}, }; + // D7: where this package's build program writes in this + // configuration (`mcpp::out_dir()`), for a reader outside the build + // -- a test script -- that needs a generated file and must not guess. + { + static const std::vector none; + entry["outDir"] = state.programOutDir(pm, i < state.activeFeaturesByPackage.size() + ? state.activeFeaturesByPackage[i] : none) + .generic_string(); + } if (!forBuildProgram) return entry; std::error_code ec; @@ -1614,6 +1623,7 @@ static std::expected step9_root_build_program(PrepareState& s // contract hash — and therefore the build.mcpp cache — is unchanged // across the move for feature-identical builds. bpEnv.features = feature_closure(*state.m, parse_feature_request(state.overrides.features)); + bpEnv.outDir = state.programOutDir(*state.m, bpEnv.features); // mcpp#241 (root): consumer index 0, same owner as the dep loop. // // AND THE LINK FORM OF EACH DEPENDENCY (#642 E2), to this program only. @@ -1819,6 +1829,9 @@ static std::expected step9_root_build_program(PrepareState& s pkg0.manifest.modules.sources.insert( pkg0.manifest.modules.sources.end(), state.m->modules.sources.begin() + rmodN, state.m->modules.sources.end()); + // The units its actions generate, as they declared them (D6). + for (auto const& [path, unit] : state.m->modules.declaredUnits) + pkg0.manifest.modules.declaredUnits.insert_or_assign(path, unit); // The snapshot's manifest is what later readers of the root's // flags see -- mirror the flag/include tails, as the old // pre-snapshot ordering implicitly did. @@ -2011,6 +2024,7 @@ static std::expected step9_member_build_programs(PrepareState bpEnv.artifactsDir = pkg.root / "target" / ".build-mcpp"; if (i < state.activeFeaturesByPackage.size()) bpEnv.features = state.activeFeaturesByPackage[i]; + bpEnv.outDir = state.programOutDir(pkg.manifest, bpEnv.features); { std::map linkForms; for (auto const& [idx, form] : state.dependencyLinkForms) diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm index 3c2f28acc..7e5736e15 100644 --- a/src/modgraph/scanner.cppm +++ b/src/modgraph/scanner.cppm @@ -1134,6 +1134,10 @@ package_source_files(const std::filesystem::path& root, } } for (auto& p : excluded) all_files.erase(p); + // A unit an action generates is a source whether or not the action has + // run yet (D6): it is scanned from its declaration, not read. + for (auto const& [path, _] : manifest.modules.declaredUnits) + all_files.insert(native_path_from_generic(path)); return all_files; } @@ -1216,6 +1220,15 @@ void scan_one_into(ScanResult& result, for (auto const& f : all_files) { const mcpp::manifest::ScanOverride* ov = nullptr; + // An action's generated unit: what the action declares, never the + // file, which the action alone writes (D6). Without a declared + // `provides` it asserts nothing, and the build's own scan finds what + // it imports. + bool declaredByAction = false; + for (auto const& [path, o] : manifest.modules.declaredUnits) { + if (native_path_from_generic(path) == f) { ov = &o; declaredByAction = true; break; } + } + if (!ov) for (auto const& [glob, o] : manifest.modules.scanOverrides) { if (path_matches_glob(f, root, glob)) { ov = &o; @@ -1231,9 +1244,12 @@ void scan_one_into(ScanResult& result, // root here. u.relPath = std::filesystem::relative(f, root); u.packageName = qualifiedName; - u.scanOverridden = true; + // What is asserted is audited against the build's own scan; an + // action unit that declares no module asserts nothing. + u.scanOverridden = !declaredByAction || !ov->provides.empty(); // The override names modules; it does not carry the declaration. - u.declaration = ModuleDeclaration::Unknown; + u.declaration = declaredByAction && ov->provides.empty() + ? ModuleDeclaration::None : ModuleDeclaration::Unknown; // A declared unit still gets its role from the same classifier — // scan_overrides overrides what was SCANNED, not what the file is. u.kind = mcpp::classify(f, extTable); From ad58c5bcd012e43c0a0ced414e4797713a4f3d5b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:31:14 +0900 Subject: [PATCH 06/32] feat(toolchain): --toolchain replacing a declared toolchain says so (D11) --- src/build/prepare/toolchain.cpp | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 8f0a817b5..afba8544a 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -53,6 +53,26 @@ import mcpp.ui; namespace mcpp::build { +// D11's sentence: which spec, replacing what, declared where. The key is the +// platform entry `for_platform` answered with (`[toolchain].linux`, else +// `.default`), and the file is the manifest that wrote it -- the workspace +// root's, for an entry a member received from `[workspace.toolchain]`. +static void warn_toolchain_override(const std::string& given, const std::string& declared, + const mcpp::manifest::Toolchain& tc, + std::string_view platform, + const std::filesystem::path& manifest) { + const std::string key = tc.byPlatform.contains(std::string(platform)) + ? std::string(platform) : std::string("default"); + auto file = manifest; + if (auto it = tc.fileByPlatform.find(key); it != tc.fileByPlatform.end()) file = it->second; + std::error_code ec; + auto shown = std::filesystem::relative(file, std::filesystem::current_path(), ec); + if (ec || shown.empty() || shown.native().starts_with("..")) shown = file; + mcpp::diag::warning("toolchain/override", std::format( + "--toolchain {} replaces {} declared at [toolchain].{} ({})", + given, declared, key, shown.generic_string())); +} + // STEP FUNCTIONS (mcpp#722 / T6), split at the points where phase1's // own banners mark a new concern: the closures phase1 assigns onto // `state` (each captures only `state`), the target/static override @@ -336,6 +356,9 @@ static std::expected step1_define_early_toolchain_closures(Pr // costs nobody anything and needs no coordination. // // It counts as user-explicit, so mcpp will not quietly revise it. + // What the manifest declared, before the command line replaces it: D11 + // says so when the two differ. + const auto manifestSpec = state.tcSpec; if (const char* tcEnv = std::getenv("MCPP_TOOLCHAIN"); tcEnv && *tcEnv) { state.tcSpec = std::string(tcEnv); state.tcOrigin = TcOrigin::ManifestToolchain; @@ -346,6 +369,15 @@ static std::expected step1_define_early_toolchain_closures(Pr state.tcOrigin = TcOrigin::ManifestToolchain; state.tcFromConsumer = true; } + // D11: `--toolchain` replacing a toolchain the manifest declared is done + // as asked, and said, so a build that ignores `[toolchain]` is never a + // surprise. A warning and not `degraded`: nothing was left undone, so + // `--strict` does not fail on it. Not for a host tool's sub-build, whose + // toolchain is its consumer's decision. + if (state.tcFromCommandLine && !state.tcFromConsumer && manifestSpec + && *manifestSpec != *state.tcSpec) + warn_toolchain_override(*state.tcSpec, *manifestSpec, + state.m->toolchain, kCurrentPlatform, state.m->sourcePath); if (!state.tcSpec.has_value()) { auto cfg = state.get_cfg(true); if (cfg && !(*cfg)->defaultToolchain.empty()) { @@ -449,6 +481,14 @@ struct TargetOverrideCtx { // `--target` -- so it is a named helper rather than a per-call closure. static void step1_apply_target_section(PrepareState& state, const mcpp::manifest::TargetEntry& e) { + if (!e.toolchain.empty() && state.tcFromCommandLine && !state.tcFromConsumer + && state.tcSpec && *state.tcSpec != e.toolchain) + mcpp::diag::warning("toolchain/override", std::format( + "--toolchain {} replaces {} declared at [target.{}].toolchain ({})", + *state.tcSpec, e.toolchain, state.overrides.target_triple.empty() + ? std::string(mcpp::toolchain::triple::host_triple().str()) + : state.overrides.target_triple, + state.m->sourcePath.filename().string())); if (!e.toolchain.empty() && !state.tcFromCommandLine && !state.tcFromConsumer) { state.tcSpec = e.toolchain; state.tcOrigin = TcOrigin::TargetSection; From adddd0f44273bfd81f0f02b512868bebc4410cad Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:45:02 +0900 Subject: [PATCH 07/32] fix(build): the toolchain's library path prefixes its tools, not ninja's environment (D29) --- src/build/ninja_backend.cppm | 43 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index ed6526f05..bd05e457c 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1453,7 +1453,8 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, plan.toolchain.binaryPath, plan.toolchain.targetTriple); if (!fsObjcopy.empty()) { - append("objcopy = " + escape_ninja_path(fsObjcopy) + "\n\n"); + append("objcopy = " + mcpp::platform::linux_::build_clean_ld_library_path_prefix( + plan.toolchain.compilerRuntimeDirs) + escape_ninja_path(fsObjcopy) + "\n\n"); // ── The raw image a flasher takes ────────────────────────────────── // // A SEPARATE EDGE with the ELF as its input, not a second output of @@ -1482,7 +1483,22 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // a link through ccache is passed to the compiler unchanged. const std::string launch = flags.launcher.empty() ? std::string{} : escape_ninja_path(std::filesystem::path(flags.launcher)) + " "; - append(std::format("cxx = {}{}\n", launch, escape_ninja_path(flags.cxxBinary))); + // D29: THE TOOLCHAIN'S PRIVATE LIBRARIES ARE THE TOOLCHAIN'S. A managed + // LLVM's `clang`, `llvm-ar` and `clang-scan-deps` find their own libc++ and + // libLLVM through LD_LIBRARY_PATH, and that path used to be in ninja's + // environment -- so every `/bin/sh -c` ninja starts had it too. A shell + // that links libc++ (Android's bionic `sh`) then loaded the LLVM's, + // built for glibc, and died before running anything: + // CANNOT LINK EXECUTABLE "/bin/sh": .../xim-x-llvm/23.1.3/lib/ + // aarch64-unknown-linux-gnu/libc++.so is too small + // (termux-docker, design 2026-10-10 §13). The path now prefixes the tools + // that need it and reaches nothing else; macOS keeps no such path at all + // (`runtime_library_path_key`), Windows keeps its PATH. + const std::string toolEnv = mcpp::platform::is_linux + ? mcpp::platform::linux_::build_clean_ld_library_path_prefix( + plan.toolchain.compilerRuntimeDirs) + : std::string{}; + append(std::format("cxx = {}{}{}\n", toolEnv, launch, escape_ninja_path(flags.cxxBinary))); // The driver alone, for the dependency scan: `clang-scan-deps` reads its // argv[0] as the compiler (and the resource directory beside it), and a // launcher there is not one. @@ -1509,7 +1525,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // static case, which goes to `ar` and produces an empty archive with exit // 0, it does not touch at all. `check_undefined_ninja_variables` below is // what stops the class. - append(std::format("cc = {}{}\n", launch, escape_ninja_path(flags.ccBinary))); + append(std::format("cc = {}{}{}\n", toolEnv, launch, escape_ninja_path(flags.ccBinary))); if (need_c_rule || need_ios_init_shim) { append(std::format("cflags = {}\n", flags.cc)); } @@ -1537,7 +1553,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // `ar` for cxx_archive. if (!flags.arBinary.empty()) { - append(std::format("ar = {}\n", escape_ninja_path(flags.arBinary))); + append(std::format("ar = {}{}\n", toolEnv, escape_ninja_path(flags.arBinary))); } else { append("ar = ar\n"); } @@ -1553,13 +1569,13 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // hand the link to a foreign one. Bound only when set, so a graph that does // not use it has no variable naming a tool it never runs. if (!flags.ldDriver.empty()) - append(std::format("ld_driver = {}\n", escape_ninja_path(flags.ldDriver))); + append(std::format("ld_driver = {}{}\n", toolEnv, escape_ninja_path(flags.ldDriver))); // `$mcpp` is needed by stage_file in EVERY configuration (dyndep or not), // so the binding cannot live inside the `if (dyndep)` below. append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path()))); if (dyndep) { if (!plan.scanDepsPath.empty()) { - append(std::format("scan_deps = {}\n", escape_ninja_path(plan.scanDepsPath))); + append(std::format("scan_deps = {}{}\n", toolEnv, escape_ninja_path(plan.scanDepsPath))); } } append("\n"); @@ -4457,11 +4473,13 @@ std::expected NinjaBackend::build(const BuildPlan& plan if (!joined.empty()) joined += '\x1f'; joined += k; joined += '='; joined += v; }; - if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) - add(runtimeEnv->first, runtimeEnv->second); + if (!mcpp::platform::is_linux) + if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) + add(runtimeEnv->first, runtimeEnv->second); for (auto& ev : plan.toolchain.envOverrides) add(ev.key, ev.value); r.runtimeEnvValue = std::move(joined); - } else if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) { + } else if (auto runtimeEnv = mcpp::platform::is_linux ? std::nullopt + : runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) { r.runtimeEnvKey = runtimeEnv->first; r.runtimeEnvValue = runtimeEnv->second; } else { @@ -4516,8 +4534,11 @@ std::expected NinjaBackend::build(const BuildPlan& plan // Real env pairs for THIS run (the "@env" cache encoding above is only // for the fast path's later re-creation of the same environment). std::vector> nenv; - if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) - nenv.emplace_back(runtimeEnv->first, runtimeEnv->second); + // On Linux the toolchain's library path prefixes its tools (`$cxx`, ...), + // never ninja's environment (D29). + if (!mcpp::platform::is_linux) + if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) + nenv.emplace_back(runtimeEnv->first, runtimeEnv->second); for (auto& ev : plan.toolchain.envOverrides) nenv.emplace_back(ev.key, ev.value); From 5f42969daa8a07ad74170dcb5b09be37eec4ea84 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:46:56 +0900 Subject: [PATCH 08/32] fix(platform): a command string's shell is resolved, not /bin/sh by assumption (D28) --- modules/platform/src/process.cppm | 27 +++++++++++++++++++++++--- src/build/schedule/detach_codegen.cppm | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/modules/platform/src/process.cppm b/modules/platform/src/process.cppm index 5436b4dc5..c0266c480 100644 --- a/modules/platform/src/process.cppm +++ b/modules/platform/src/process.cppm @@ -63,6 +63,12 @@ struct RunResult { std::string output; }; +// The POSIX shell a command string is handed to (D28): `/bin/sh`, else +// Android's `/system/bin/sh`, else `$PREFIX/bin/sh` (Termux). Resolved once. +// `popen` and `std::system` use the C library's own choice, `/bin/sh`, which +// is why Android below 10 (no `/bin`) is not supported (docs/20). +const std::string& posix_shell(); + // Run `command` via the platform shell, capture stdout. // On POSIX, stdin is automatically redirected from /dev/null. RunResult capture(std::string_view command); @@ -1228,6 +1234,21 @@ int run_exec_deadline(const std::vector& argv, return r.exit_code; } +const std::string& posix_shell() { + static const std::string shell = [] { + namespace fs = std::filesystem; + std::error_code ec; + for (const char* p : {"/bin/sh", "/system/bin/sh"}) + if (fs::exists(p, ec)) return std::string(p); + if (const char* prefix = std::getenv("PREFIX"); prefix && *prefix) { + auto p = fs::path(prefix) / "bin" / "sh"; + if (fs::exists(p, ec)) return p.string(); + } + return std::string("/bin/sh"); + }(); + return shell; +} + int run_shell_deadline(std::string_view command, std::string_view cwd, std::chrono::milliseconds deadline, @@ -1239,7 +1260,7 @@ int run_shell_deadline(std::string_view command, // argv is what the POSIX branch launches; the Windows branch takes the // shaped command line instead. Both are built here so neither platform's // spelling can drift into a launcher that does not use it. - const std::vector argv{"/bin/sh", "-c", std::string(command)}; + const std::vector argv{posix_shell(), "-c", std::string(command)}; auto r = dispatch_bounded(argv, {}, cwd, deadline, /*capture=*/false, windows_shell_command_line(command)); // No fallback to the unbounded launcher here, unlike run_exec_deadline: a @@ -1262,7 +1283,7 @@ int run_streaming_bounded(std::string_view command, // The popen path's shaping, minus the outer cmd.exe pair: the Windows // launcher's command line supplies that pair itself. const auto sealed = seal_stdin(command); - const std::vector argv{"/bin/sh", "-c", sealed}; + const std::vector argv{posix_shell(), "-c", sealed}; std::function sink = on_line ? std::move(on_line) : [](std::string_view) {}; auto r = dispatch_bounded(argv, {}, {}, total, /*capture=*/true, @@ -1295,7 +1316,7 @@ BackgroundCommand start_shell_background(std::string_view command, out.process = r.process; } else { const std::string cmdStore(command); - const char* argv[] = {"/bin/sh", "-c", cmdStore.c_str()}; + const char* argv[] = {posix_shell().c_str(), "-c", cmdStore.c_str()}; auto r = mcpp::platform::unixproc::spawn_background( argv, 3, cwdArg, inheritStdio ? 1 : 0); out.ok = r.ok; diff --git a/src/build/schedule/detach_codegen.cppm b/src/build/schedule/detach_codegen.cppm index 5cf199ddb..4cff05f59 100644 --- a/src/build/schedule/detach_codegen.cppm +++ b/src/build/schedule/detach_codegen.cppm @@ -81,6 +81,7 @@ extern char** environ; export module mcpp.build.schedule.detach_codegen; import std; +import mcpp.platform.process; import mcpp.build.stage; export namespace mcpp::build::schedule::detach { @@ -447,7 +448,7 @@ bool spawn_detached(const std::vector& argv) { int run_to_completion(std::string_view command, const std::filesystem::path& logPath) { - const std::vector argv{"/bin/sh", "-c", std::string(command)}; + const std::vector argv{mcpp::platform::process::posix_shell(), "-c", std::string(command)}; posix_spawn_file_actions_t fa; ::posix_spawn_file_actions_init(&fa); ::posix_spawn_file_actions_addopen(&fa, 0, "/dev/null", O_RDONLY, 0); From d08bc01288524158e7781a3433b9925f82bc4ad8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:49:13 +0900 Subject: [PATCH 09/32] feat(platform): an Android host is recognised and reported, not given another toolchain (D21) --- modules/platform/src/linux/linux.cppm | 21 +++++++++++++++++++++ src/build/prepare/toolchain.cpp | 16 ++++++++-------- src/cli/cmd_self.cppm | 4 ++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/modules/platform/src/linux/linux.cppm b/modules/platform/src/linux/linux.cppm index 0de8cbc9e..c660be2d5 100644 --- a/modules/platform/src/linux/linux.cppm +++ b/modules/platform/src/linux/linux.cppm @@ -29,6 +29,13 @@ std::string build_clean_ld_library_path_prefix( std::vector runtime_lib_dirs(const std::filesystem::path& toolchain_root); +// Is this Linux an Android (D21)? `ANDROID_ROOT` and `ANDROID_DATA` are set in +// every Android process environment (Termux included), and +// `/system/bin/linker64` is bionic's dynamic loader. Answers which shell +// exists and how a failure is explained; it does not choose a toolchain: +// Android takes the same default as GNU/Linux on its architecture. +bool is_android_host(); + } // namespace mcpp::platform::linux_ // ─── Implementation ────────────────────────────────────────────────────── @@ -91,4 +98,18 @@ runtime_lib_dirs(const std::filesystem::path& toolchain_root) { return dirs; } +bool is_android_host() { +#if defined(__linux__) + static const bool android = [] { + if (std::getenv("ANDROID_ROOT") && std::getenv("ANDROID_DATA")) return true; + std::error_code ec; + return std::filesystem::exists("/system/bin/linker64", ec) + || std::filesystem::exists("/system/bin/linker", ec); + }(); + return android; +#else + return false; +#endif +} + } // namespace mcpp::platform::linux_ diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index afba8544a..479d42044 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1591,14 +1591,14 @@ step2_first_run_auto_install(PrepareState& state) { // breaks GUI/native packages out of the box. musl-static stays // opt-in via `mcpp build --target x86_64-linux-musl` for users // who explicitly want portable static binaries. - // Linux default is arch-aware: - // x86_64 → glibc gcc (native ABI; the glibc toolchain is published - // for x86_64). musl-static stays opt-in via --target. - // other arches (aarch64, ...) → musl-static gcc: it's what's - // published for them, is self-contained, and yields portable - // static binaries (ideal for aarch64 / Termux, no bionic dep). - // glibc-world linking (X11/GL) needs an explicit glibc - // toolchain, addable later for native-ABI aarch64 builds. + // Linux default is arch-aware, and the table is + // `pins::host_default_toolchain` (`mcpp self env` reports it): + // x86_64 → glibc gcc (native ABI). musl-static stays opt-in via + // --target. + // aarch64 → llvm with the managed glibc (2026.10.8.1), on GNU/Linux + // and on Android (Termux) alike: an Android host is + // recognised (`linux_::is_android_host`) but not given a + // different toolchain (design 2026-10-10 §13, D21). // `native_first_run_spec()` (declared above) is this exact selection // — on Windows it re-checks `msvc_usable_either_origin()`, which here // is redundant (the seed above already diverted the unusable case diff --git a/src/cli/cmd_self.cppm b/src/cli/cmd_self.cppm index 8ba9b3078..84b8ee20a 100644 --- a/src/cli/cmd_self.cppm +++ b/src/cli/cmd_self.cppm @@ -64,6 +64,10 @@ nlohmann::json env_data_readonly() { // read-only, as the rest of this path is. {"defaultToolchain", std::string(mcpp::toolchain::triple::pins::host_default_toolchain( mcpp::toolchain::msvc::msvc_available_here(registry / "data" / "xpkgs")))}, + // An Android host (Termux): the same default toolchain as GNU/Linux on + // this architecture; reported because the shell and the loader rules + // differ (D21, D28). + {"android", mcpp::platform::linux_::is_android_host()}, }; } From 1655f0c3f112f0824e4f7a1cf30bc1267af94c2b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:52:56 +0900 Subject: [PATCH 10/32] feat(install): the hint follows the failure's class (D22); extraction and hook progress reach the terminal (D23) --- src/fetcher/progress.cppm | 27 +++++++++++++++++++++++++++ src/pm/package_fetcher.cppm | 25 +++++++++++++++++++++++-- src/xlings/xlings.cppm | 6 +++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/fetcher/progress.cppm b/src/fetcher/progress.cppm index 02d75bb58..aa8e37907 100644 --- a/src/fetcher/progress.cppm +++ b/src/fetcher/progress.cppm @@ -282,6 +282,33 @@ export struct InstallProgressHandler : mcpp::fetcher::EventHandler { capturedDiagnostics_.push_back(std::move(blob)); } + // D23: WHAT XLINGS IS DOING, WHILE IT DOES IT. A toolchain install runs + // inside `Planning`, and an archive being unpacked or a hook running for + // half an hour on a phone showed nothing but the clock (design 2026-10-10 + // §13, R4). xlings reports both (`extract` per percent of the archive, + // `hook` after 10 s and then every 30 s; interface 1.7). A hook line is + // shown as it comes; an extraction once it has run 10 s, then every 10 s, + // so a quick one prints nothing. Every event is in the log. + struct Shown { + std::chrono::steady_clock::time_point first, last; + }; + std::map shown_; + + void on_progress(const mcpp::fetcher::ProgressEvent& e) override { + mcpp::log::verbose("xlings", std::format("[{}] {}", e.phase, e.message)); + if (e.phase != "extract" && e.phase != "hook") return; + const auto now = std::chrono::steady_clock::now(); + const auto subject = e.phase + ":" + e.message.substr(0, e.message.find(' ')); + auto it = shown_.try_emplace(subject, Shown{now, {}}).first; + if (e.phase == "extract") { + if (now - it->second.first < std::chrono::seconds(10)) return; + if (it->second.last != std::chrono::steady_clock::time_point{} + && now - it->second.last < std::chrono::seconds(10)) return; + } + it->second.last = now; + mcpp::ui::status("Installing", e.message); + } + // progress_'s own destructor finishes the active bar. ~InstallProgressHandler() override = default; }; diff --git a/src/pm/package_fetcher.cppm b/src/pm/package_fetcher.cppm index 9f6fe5e4f..7586c01be 100644 --- a/src/pm/package_fetcher.cppm +++ b/src/pm/package_fetcher.cppm @@ -1016,6 +1016,27 @@ ParsedTarget parse_target(std::string_view target, std::string_view defaultIndex return r; } +// D22: what to do about a failed install depends on why it failed, and xlings +// says why (`code`, interface 1.5+; extraction since 2026.10.10.2). Every +// failure used to end in "check network and retry": a phone whose app sandbox +// refused hard links was sent to check its network (design 2026-10-10 §13). +std::string install_failure_hint(const std::optional& error, + const std::filesystem::path& home) { + const std::string code = error ? error->code : std::string{}; + if (code == "E_NETWORK") + return "check network and retry, or `mcpp self init --force`"; + if (code == "E_DISK_FULL") + return std::format("free space on the disk that holds {}, then retry", home.string()); + if (code == "E_PERMISSION") + return std::format("a file under {} could not be written: check the owner and " + "permissions of its directory, then retry", home.string()); + if (code == "E_INVALID_INPUT") + return "the downloaded archive was damaged and has been dropped; a retry downloads it again"; + if (error && !error->hint.empty()) return error->hint; + return "the whole failure is in ~/.mcpp/log/mcpp.log (`mcpp self doctor` checks the " + "installation); retry, or `mcpp self init --force`"; +} + } // namespace std::expected @@ -1254,8 +1275,8 @@ Fetcher::resolve_xpkg_path(std::string_view target, if (inst->error) installError += ": " + inst->error->message; return std::unexpected(CallError{std::format( - "{}\n hint: check network and retry, or `mcpp self init --force`", - installError)}); + "{}\n hint: {}", installError, + install_failure_hint(inst->error, cfg_.xlingsHome()))}); } } // No autoInstall fallback: when the caller explicitly disables diff --git a/src/xlings/xlings.cppm b/src/xlings/xlings.cppm index fad4945eb..3f3571493 100644 --- a/src/xlings/xlings.cppm +++ b/src/xlings/xlings.cppm @@ -1553,7 +1553,11 @@ call(const Env& env, std::string_view capability, result.dataEvents.push_back(e); if (handler) handler->on_data(e); } else if constexpr (std::is_same_v) { - result.error = e; + // The FIRST error is the cause. A package whose + // dependency failed reports that after it (xlings + // 2026.10.10.2), and older clients followed a failure with + // its consequences ("registered none of its programs"). + if (!result.error) result.error = e; if (handler) handler->on_error(e); } else if constexpr (std::is_same_v) { result.exitCode = e.exitCode; From 7146c4e5328062c9a1fc56e3ea2b8ac6f3c19f73 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 06:58:11 +0900 Subject: [PATCH 11/32] fix(windows): the SDK is found where its installer recorded it; the MSVC probe names the missing half (D15-D18) --- modules/platform/src/windows/windows.cppm | 48 +++++++++- src/build/prepare/options.cpp | 41 ++++---- src/build/prepare/toolchain.cpp | 26 +++++- src/toolchain/msvc.cppm | 109 +++++++++++++++++++--- tests/unit/test_toolchain_msvc.cpp | 28 ++++++ 5 files changed, 215 insertions(+), 37 deletions(-) diff --git a/modules/platform/src/windows/windows.cppm b/modules/platform/src/windows/windows.cppm index 684e393b9..9a059bbd3 100644 --- a/modules/platform/src/windows/windows.cppm +++ b/modules/platform/src/windows/windows.cppm @@ -18,7 +18,10 @@ module; #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif -#include // GetACP +#include // GetACP, RegGetValueW +// RegGetValueW lives in advapi32, which a link line names only by default on +// MinGW; this asks MSVC-ABI links for it too. +#pragma comment(lib, "advapi32") #endif export module mcpp.platform.windows; @@ -37,6 +40,19 @@ void prepend_path(const std::filesystem::path& dir); // this code page (#693). unsigned active_code_page(); +// A string value under HKEY_LOCAL_MACHINE, read from both registry views (the +// 64-bit one first, then WOW6432Node's), as a path. An installer records its +// installation directory there wherever it put it -- the Windows SDK's +// `Installed Roots\KitsRoot10` -- so a search that knows only `C:\Program +// Files` misses an SDK on another drive (D15). nullopt when absent and on +// every other platform. +std::optional machine_registry_path(std::wstring_view subkey, + std::wstring_view value); + +// A directory a known-folder environment variable names (`ProgramFiles`, +// `ProgramFiles(x86)`), or nullopt. The system drive is not always `C:`. +std::optional env_directory(const char* name); + } // namespace mcpp::platform::windows // ─── Implementation ────────────────────────────────────────────────────── @@ -53,6 +69,36 @@ void prepend_path(const std::filesystem::path& dir) { #endif } +std::optional machine_registry_path(std::wstring_view subkey, + std::wstring_view value) { +#if defined(_WIN32) + const std::wstring key(subkey), name(value); + for (DWORD view : {static_cast(KEY_WOW64_64KEY), static_cast(KEY_WOW64_32KEY)}) { + DWORD bytes = 0; + if (RegGetValueW(HKEY_LOCAL_MACHINE, key.c_str(), name.c_str(), + RRF_RT_REG_SZ | view, nullptr, nullptr, &bytes) != ERROR_SUCCESS + || bytes < sizeof(wchar_t)) + continue; + std::wstring buf(bytes / sizeof(wchar_t), L'\0'); + if (RegGetValueW(HKEY_LOCAL_MACHINE, key.c_str(), name.c_str(), + RRF_RT_REG_SZ | view, nullptr, buf.data(), &bytes) != ERROR_SUCCESS) + continue; + while (!buf.empty() && (buf.back() == L'\0' || buf.back() == L'\\')) + buf.pop_back(); + if (!buf.empty()) return std::filesystem::path(buf); + } + return std::nullopt; +#else + (void)subkey; (void)value; + return std::nullopt; +#endif +} + +std::optional env_directory(const char* name) { + if (const char* v = std::getenv(name); v && *v) return std::filesystem::path(v); + return std::nullopt; +} + unsigned active_code_page() { #if defined(_WIN32) return static_cast(GetACP()); diff --git a/src/build/prepare/options.cpp b/src/build/prepare/options.cpp index ac63fa7dd..fce6b8c6b 100644 --- a/src/build/prepare/options.cpp +++ b/src/build/prepare/options.cpp @@ -37,29 +37,32 @@ import mcpp.platform; namespace mcpp::build { +// D18: a toolchain that was SPECIFIED (manifest, `--toolchain`, global default) +// and needs the MSVC ABI is never replaced by MinGW when that ABI is +// incomplete; the build is refused here, and the refusal says which half is +// missing and where it was looked for (D16) -- the sentence it replaced said +// "neither was found" on a machine where Visual Studio was. std::string msvc_unavailable_guidance(const mcpp::toolchain::Toolchain& tc) { namespace pins = mcpp::toolchain::triple::pins; - const bool haveVcTools = tc.compiler == mcpp::toolchain::CompilerId::MSVC; - if (haveVcTools && mcpp::toolchain::msvc::find_msvc_tools_dir()) { - return std::format( - "msvc {} was detected at {}, but no Windows SDK was found —\n" - " cl.exe cannot compile without the UCRT/SDK headers.\n" - " Install the 'Windows 11 SDK' component via the Visual Studio\n" - " Installer (it is part of the Desktop development with C++\n" - " workload), then retry.", - tc.version, tc.binaryPath.string()); - } + const auto probe = mcpp::toolchain::msvc::probe_msvc(); + const bool haveVcTools = tc.compiler == mcpp::toolchain::CompilerId::MSVC + && mcpp::toolchain::msvc::find_msvc_tools_dir(); + std::string what = haveVcTools + ? std::format("msvc {} was detected at {}, but no Windows SDK was found", + tc.version, tc.binaryPath.string()) + : mcpp::toolchain::msvc::describe_msvc_probe(probe); + if (what.empty()) what = "the MSVC STL or the Windows SDK is incomplete"; return std::format( - "this build targets the MSVC ABI, which needs Visual Studio /\n" - " Build Tools (MSVC STL + Windows SDK) — neither was found.\n" - "\n" - " No Visual Studio? Use the self-contained MinGW-w64 toolchain\n" - " (no Visual Studio required, `import std` works):\n" - " mcpp toolchain default {} --target {}\n" + "this build targets the MSVC ABI, which needs Visual Studio's C++ tools (the MSVC\n" + " STL) and a Windows SDK: {}.\n" "\n" - " Have Visual Studio? Install the 'Desktop development with C++'\n" - " workload — it provides the MSVC STL and the Windows SDK.", - pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget); + " The toolchain was specified, so mcpp does not replace it. Either:\n" + " - install the missing part (the 'Desktop development with C++' workload,\n" + " or its Windows SDK component), or set WindowsSdkDir to an SDK root;\n" + " - `mcpp toolchain install msvc`: a managed MSVC with its own SDK;\n" + " - or choose MinGW-w64 yourself: `--toolchain {}` (or [toolchain] in\n" + " mcpp.toml) with `--target {}`.", + what, pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget); } std::optional parse_cache_mode(std::string_view v) { diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 479d42044..23070c89e 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1546,11 +1546,12 @@ step2_offline_refusal(PrepareState& state) { && !state.msvc_usable_either_origin()) { refusal::record(refusal::Code::OfflineDownloadRequired); return std::unexpected(std::format( - "no toolchain configured (and no Visual Studio found).\n" + "no toolchain configured, and the MSVC ABI is not usable here: {}.\n" " run one of:\n" " mcpp toolchain install {} --target {}\n" " mcpp toolchain default {} --target {}\n" " {}", + mcpp::toolchain::msvc::describe_msvc_probe(mcpp::toolchain::msvc::probe_msvc()), pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget, pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget, release)); } @@ -1719,10 +1720,25 @@ step2_windows_gnu_first_run_persist(PrepareState& state) { // still the one this branch was written for. if (state.windowsGnuFirstRun && state.tcSpec.has_value() && tc_origin_may_persist(state.tcOrigin)) { - mcpp::ui::info("First run", - std::format("no toolchain configured and no Visual Studio found — " - "using {} for {} (MinGW-w64, self-contained)", - *state.tcSpec, state.overrides.target_triple)); + // D17: said once, and said exactly: which half of the MSVC ABI is + // missing and where it was looked for -- not "no Visual Studio found" + // on a machine whose Visual Studio was found and whose Windows SDK + // was on another drive (design 2026-10-10 §12.1, S1) -- and how to + // move to the MSVC ABI once it is complete. Nothing was specified, so + // MinGW is chosen and recorded; a toolchain that was specified is + // never replaced by it (D18, `msvc_unavailable_guidance`). + static bool told = false; + if (!std::exchange(told, true)) { + const auto why = mcpp::toolchain::msvc::describe_msvc_probe( + mcpp::toolchain::msvc::probe_msvc()); + mcpp::ui::info("First run", + std::format("no toolchain configured, and the MSVC ABI is not usable here: {}. " + "Using {} for {} (MinGW-w64, self-contained). Once Visual Studio " + "and the Windows SDK are both in place: `mcpp toolchain default {}`", + why.empty() ? std::string("its STL or SDK is incomplete") : why, + *state.tcSpec, state.overrides.target_triple, + mcpp::toolchain::triple::pins::kFirstRunWinMsvc)); + } if (auto cfgW = state.get_cfg(true); cfgW) { if (mcpp::config::write_default_toolchain(**cfgW, *state.tcSpec)) (*cfgW)->defaultToolchain = *state.tcSpec; diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 65ccf0a89..4b8f48b21 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -299,6 +299,31 @@ struct WindowsSdk { std::optional find_windows_sdk( std::span extraRoots = {}); +// 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)% +// and %ProgramFiles%, then the same two under `C:`. Deduplicated. +std::vector machine_sdk_roots(); + +// WHAT A MACHINE HAS OF THE MSVC ABI, AND WHERE IT LOOKED (D16). +// +// "MSVC is usable" is two halves -- Visual Studio's C++ tools with their STL, +// and a Windows SDK -- and a diagnostic that only had the conjunction said +// "no Visual Studio found" on a machine whose Visual Studio was found and +// whose SDK sat on another drive (design 2026-10-10 §12, run 37974353683). +struct MsvcProbe { + std::vector visualStudio; // instances with the C++ tools + bool stl = false; // the MSVC STL's std module source + std::optional sdk; + std::vector sdkSearched; // the roots step 3 tried + bool usable() const { return stl && sdk.has_value(); } +}; +MsvcProbe probe_msvc(); + +// One sentence: what was found, what is missing, and where it was looked +// for. Empty when both halves are present. +std::string describe_msvc_probe(const MsvcProbe& probe); + // Windows SDK payload roots that belong to the same xlings store as this // compiler. The compiler binary says which store it came from, so a managed // toolset finds its own SDK with nothing configured and no version hardcoded @@ -492,10 +517,14 @@ std::optional find_vs_via_comntools() { // Strategy 4: Scan well-known paths. std::optional find_vs_via_paths() { - static constexpr std::string_view bases[] = { - "C:\\Program Files\\Microsoft Visual Studio", - "C:\\Program Files (x86)\\Microsoft Visual Studio", - }; + // %ProgramFiles% and %ProgramFiles(x86)% first: the system drive is not + // `C:` on every machine (D15). + std::vector bases; + for (const char* var : {"ProgramFiles", "ProgramFiles(x86)"}) + if (auto b = mcpp::platform::windows::env_directory(var)) + bases.push_back(*b / "Microsoft Visual Studio"); + bases.emplace_back("C:\\Program Files\\Microsoft Visual Studio"); + bases.emplace_back("C:\\Program Files (x86)\\Microsoft Visual Studio"); // Newer VS installs use the major version as the directory ("18", seen // on windows-latest 2026-07: …\Microsoft Visual Studio\18\Enterprise), // older ones the year branding. @@ -505,10 +534,10 @@ std::optional find_vs_via_paths() { }; std::error_code ec; - for (auto base : bases) { + for (auto const& base : bases) { for (auto year : years) { for (auto edition : editions) { - auto p = std::filesystem::path(base) / std::string(year) / std::string(edition); + auto p = base / std::string(year) / std::string(edition); if (std::filesystem::exists(p / "VC" / "Tools" / "MSVC", ec)) return p; } @@ -1057,8 +1086,12 @@ std::vector enumerate_vs_instances() { if (same_root(e.root, inst.root)) return; out.push_back(std::move(inst)); }; + // Where the Visual Studio Installer keeps it: %ProgramFiles(x86)%, which + // is not `C:` on every machine (D15). const std::filesystem::path vswhere = - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"; + mcpp::platform::windows::env_directory("ProgramFiles(x86)") + .value_or(std::filesystem::path("C:\\Program Files (x86)")) + / "Microsoft Visual Studio" / "Installer" / "vswhere.exe"; bool listed = false; if (std::filesystem::exists(vswhere)) { // `-all -prerelease`: every instance, Insiders included, complete or @@ -1268,14 +1301,66 @@ std::optional find_windows_sdk( for (const auto& root : extraRoots) if (auto s = pick_sdk_in(root, want)) return s; - // 3. The conventional absolute install roots. - for (const char* base : {"C:\\Program Files (x86)\\Windows Kits\\10", - "C:\\Program Files\\Windows Kits\\10"}) { - if (auto s = pick_sdk_in(std::filesystem::path{base}, want)) return s; - } + // 3. Where the machine's SDK installer put it (D15). + for (const auto& root : machine_sdk_roots()) + if (auto s = pick_sdk_in(root, want)) return s; return std::nullopt; } +std::vector machine_sdk_roots() { + std::vector out; + auto add = [&](std::optional p) { + if (!p || p->empty()) return; + auto n = p->lexically_normal(); + for (auto const& e : out) + if (e.lexically_normal() == n) return; + out.push_back(std::move(n)); + }; + namespace win = mcpp::platform::windows; + add(win::machine_registry_path(L"SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots", + L"KitsRoot10")); + for (const char* var : {"ProgramFiles(x86)", "ProgramFiles"}) + if (auto base = win::env_directory(var)) add(*base / "Windows Kits" / "10"); + add(std::filesystem::path("C:\\Program Files (x86)\\Windows Kits\\10")); + add(std::filesystem::path("C:\\Program Files\\Windows Kits\\10")); + return out; +} + +MsvcProbe probe_msvc() { + MsvcProbe p; +#if defined(_WIN32) + for (auto const& inst : enumerate_vs_instances()) p.visualStudio.push_back(inst.root); + p.stl = find_std_module_source().has_value(); + p.sdk = find_windows_sdk(); + p.sdkSearched = machine_sdk_roots(); +#endif + return p; +} + +std::string describe_msvc_probe(const MsvcProbe& p) { + if (p.usable()) return {}; + std::string searched; + for (auto const& r : p.sdkSearched) + searched += std::format("{}{}", searched.empty() ? "" : ", ", r.string()); + const bool haveVs = !p.visualStudio.empty() && p.stl; + if (haveVs && !p.sdk) + return std::format("Visual Studio was found at {}, but no Windows SDK was " + "(WindowsSdkDir is not set; searched: {})", + p.visualStudio.front().string(), searched); + if (!haveVs && p.sdk) + return std::format("the Windows SDK {} was found at {}, but no Visual Studio with " + "the C++ tools (its MSVC STL) was", + p.sdk->version, p.sdk->root.string()); + if (!p.visualStudio.empty() && !p.stl) + return std::format("Visual Studio was found at {}, but without the MSVC STL's " + "std module (the C++ tools component), and {}", + p.visualStudio.front().string(), + p.sdk ? std::format("the Windows SDK {} was found", p.sdk->version) + : std::format("no Windows SDK was (searched: {})", searched)); + return std::format("no Visual Studio with the C++ tools and no Windows SDK were found " + "(searched for the SDK: {})", searched); +} + Origin origin_of(const std::filesystem::path& clPath) { return mcpp::xlings::paths::xpkgs_from_compiler(clPath) ? Origin::Managed : Origin::SystemMsvc; diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index 89249f2e3..c57026fc8 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -1276,3 +1276,31 @@ TEST(MsvcVswhereText, OneInstancePerInstanceId) { EXPECT_EQ(v[1].product, "Visual Studio Community 2026 Insiders"); EXPECT_TRUE(mcpp::toolchain::msvc::parse_vswhere_text("").empty()); } + +// D16: the refusal and the first-run notice say which half of the MSVC ABI is +// missing and where the SDK was looked for. The sentence they replaced said +// "no Visual Studio found" on a machine whose Visual Studio was found and +// whose SDK sat on another drive (design 2026-10-10 §12.1, run 37974353683). +TEST(MsvcProbe, TheDescriptionNamesTheMissingHalfAndWhereItLooked) { + namespace msvc = mcpp::toolchain::msvc; + msvc::MsvcProbe vsOnly; + vsOnly.visualStudio = {"D:\\VS\\2026\\Enterprise"}; + vsOnly.stl = true; + vsOnly.sdkSearched = {"D:\\Kits\\10", "C:\\Program Files (x86)\\Windows Kits\\10"}; + const auto a = msvc::describe_msvc_probe(vsOnly); + EXPECT_NE(a.find("Visual Studio was found at D:\\VS\\2026\\Enterprise"), std::string::npos) << a; + EXPECT_NE(a.find("no Windows SDK"), std::string::npos) << a; + EXPECT_NE(a.find("D:\\Kits\\10"), std::string::npos) << a; + EXPECT_EQ(a.find("no Visual Studio"), std::string::npos) << a; + + msvc::MsvcProbe sdkOnly; + sdkOnly.sdk = msvc::WindowsSdk{"D:\\Kits\\10", "10.0.26100.0"}; + const auto b = msvc::describe_msvc_probe(sdkOnly); + EXPECT_NE(b.find("10.0.26100.0"), std::string::npos) << b; + EXPECT_NE(b.find("no Visual Studio"), std::string::npos) << b; + + msvc::MsvcProbe both = vsOnly; + both.sdk = sdkOnly.sdk; + EXPECT_TRUE(both.usable()); + EXPECT_TRUE(msvc::describe_msvc_probe(both).empty()); +} From 6af0689378a40a0201555f92d5cee89f1d28862f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:01:52 +0900 Subject: [PATCH 12/32] fix(indices): the index router reads [workspace.indices] and the position reading through one function --- src/pm/index_route.cppm | 18 +++++++++++++++--- src/project.cppm | 37 ++++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/pm/index_route.cppm b/src/pm/index_route.cppm index 823d0db30..a13e22c42 100644 --- a/src/pm/index_route.cppm +++ b/src/pm/index_route.cppm @@ -251,12 +251,24 @@ cross_namespace_suggestions( IndexMap effective_indices(const std::filesystem::path& root) { auto m = mcpp::manifest::load(root / "mcpp.toml"); if (!m) return {}; - if (!m->indices.empty()) return m->indices; - // A member with no [indices] of its own inherits the workspace root's, - // whose relative `path` is anchored at the ROOT, not the member (#224). + // A member's own [indices], then its workspace's (`[workspace.indices]` + // by name; the root's own [indices] by position when it has none of the + // former), whose relative `path` is anchored at the ROOT (#224). auto wsRoot = mcpp::project::find_workspace_root(root); if (wsRoot.empty()) return m->indices; + // The workspace root itself: its own [indices], and what it gives every + // member, its root package included. + std::error_code ec; + if (std::filesystem::equivalent(wsRoot, root, ec)) { + if (m->layer) + for (auto const& [name, idx] : m->layer->indices) { + auto [it, inserted] = m->indices.try_emplace(name, idx); + if (inserted && it->second.is_local() && it->second.path.is_relative()) + it->second.path = (wsRoot / it->second.path).lexically_normal(); + } + return m->indices; + } auto ws = mcpp::manifest::load(wsRoot / "mcpp.toml"); if (!ws || !ws->workspace.present) return m->indices; mcpp::project::inherit_workspace_indices(*m, *ws, wsRoot); diff --git a/src/project.cppm b/src/project.cppm index 8f98ffbdb..9945b985c 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -114,21 +114,7 @@ export void merge_workspace_deps(mcpp::manifest::Manifest& member, // disagreeing about which packages are real. export void inherit_workspace_indices(mcpp::manifest::Manifest& member, const mcpp::manifest::Manifest& workspace, - const std::filesystem::path& wsRoot) { - if (!member.indices.empty() || workspace.indices.empty()) return; - member.indices = workspace.indices; - for (auto& [_, idx] : member.indices) { - if (idx.is_local() && idx.path.is_relative()) { - // This is an ownership/anchoring operation, not a request to - // resolve filesystem aliases. weakly_canonical can rewrite a - // Windows short/case-preserving workspace path into a different - // spelling before the inherited index is opened. Keep the path - // rooted exactly where the workspace manifest declared it; the - // normal reader remains responsible for existence/readability. - idx.path = (wsRoot / idx.path).lexically_normal(); - } - } -} + const std::filesystem::path& wsRoot); // Is `candidate` one of this workspace's declared members? // @@ -430,6 +416,27 @@ export void inherit_workspace_root_position(mcpp::manifest::Manifest& member, } } +// The effective `[indices]` of a member, for a reader that needs only them +// (`mcpp add`, the index router): `[workspace.indices]` by name (W3), and the +// root's own `[indices]` by position where the root has no +// `[workspace.indices]` (COMPAT(workspace-position)). A relative `path` was +// written at the workspace root and is anchored there (#224). +void inherit_workspace_indices(mcpp::manifest::Manifest& member, + const mcpp::manifest::Manifest& workspace, + const std::filesystem::path& wsRoot) { + if (auto const* layer = workspace.layer.get()) + for (auto const& [name, idx] : layer->indices) { + auto [it, inserted] = member.indices.try_emplace(name, idx); + if (inserted && it->second.is_local() && it->second.path.is_relative()) + it->second.path = (wsRoot / it->second.path).lexically_normal(); + } + const std::vector indices{"indices"}; + if (!detail::lists(mcpp::pm::compat::position_tables(workspace), "indices")) return; + const auto received = mcpp::pm::compat::inherit_root_position_by_position( + member, workspace, wsRoot, indices); + detail::warn_position(workspace, member, "indices", !received.empty()); +} + // THE ROOT PACKAGE OF A WORKSPACE IS ONE OF ITS MEMBERS. // // A root that carries `[package]` beside `[workspace]` is selected as member From 680808cfcfe513904e83f0b049ccd6ca02962e04 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:08:27 +0900 Subject: [PATCH 13/32] test(e2e): 893-901 -- the workspace layer and its compat reading, conditional rows, one writer per output, per-configuration out_dir, module roles, the toolchain override note, publish write-back, the tools' library path --- tests/e2e/120_ws_root_indices.sh | 15 ++ tests/e2e/12_add_command.sh | 2 +- ...10_local_index_addressing_on_every_host.sh | 2 +- tests/e2e/44_indices_e2e_integration.sh | 2 +- ...e_the_build_read_the_effective_manifest.sh | 4 + ...he_workspace_prefix_carries_the_meaning.sh | 150 ++++++++++++++++++ ...e_position_reading_is_kept_and_reported.sh | 78 +++++++++ ...nfiguration_rows_count_as_they_evaluate.sh | 62 ++++++++ .../896_an_action_output_has_one_writer.sh | 83 ++++++++++ ...figuration_has_its_own_generated_output.sh | 91 +++++++++++ tests/e2e/898_module_units_in_cpp_files.sh | 50 ++++++ ..._replacing_a_declared_toolchain_says_so.sh | 52 ++++++ ...tes_the_workspace_layer_into_the_member.sh | 86 ++++++++++ ...chain_library_path_stays_with_the_tools.sh | 59 +++++++ 14 files changed, 733 insertions(+), 3 deletions(-) create mode 100755 tests/e2e/893_the_workspace_prefix_carries_the_meaning.sh create mode 100755 tests/e2e/894_the_position_reading_is_kept_and_reported.sh create mode 100755 tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh create mode 100755 tests/e2e/896_an_action_output_has_one_writer.sh create mode 100755 tests/e2e/897_each_configuration_has_its_own_generated_output.sh create mode 100755 tests/e2e/898_module_units_in_cpp_files.sh create mode 100755 tests/e2e/899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh create mode 100755 tests/e2e/900_publish_writes_the_workspace_layer_into_the_member.sh create mode 100755 tests/e2e/901_the_toolchain_library_path_stays_with_the_tools.sh diff --git a/tests/e2e/120_ws_root_indices.sh b/tests/e2e/120_ws_root_indices.sh index d1d909ab2..e2aa5c236 100755 --- a/tests/e2e/120_ws_root_indices.sh +++ b/tests/e2e/120_ws_root_indices.sh @@ -16,6 +16,10 @@ # the member's path is anchored at the member (2026.9.29.1 resolved it # against the workspace root and found no index). The two members name # one tree, so they are one configuration. +# 4. COMPAT(workspace-position): the root's own `[indices]` reaches the +# members by POSITION, with a warning naming `[workspace.indices]`, until +# mcpp 1.0.0 (SPEC-004 §9.10). Kept in this spelling as that coverage; +# tests/e2e/893 covers `[workspace.indices]`. set -e TMP=$(mktemp -d) @@ -150,6 +154,17 @@ EOF exit 1 } +grep -q '\[indices\] on the workspace root reaches every member by its position' build.log || { + cat build.log + echo "FAIL: the position reading of [indices] says nothing (COMPAT(workspace-position))" + exit 1 +} +grep -q '\[workspace.indices\]' build.log || { + cat build.log + echo "FAIL: the warning does not name the canonical spelling" + exit 1 +} + "$MCPP" run -p member-a > run.log 2>&1 || { cat run.log echo "FAIL: run failed" diff --git a/tests/e2e/12_add_command.sh b/tests/e2e/12_add_command.sh index 791e4b351..231347718 100755 --- a/tests/e2e/12_add_command.sh +++ b/tests/e2e/12_add_command.sh @@ -226,7 +226,7 @@ cat > mcpp.toml <<'EOF' [workspace] members = ["m1"] -[indices] +[workspace.indices] acme = { path = "index" } EOF "$MCPP" new m1 > /dev/null diff --git a/tests/e2e/210_local_index_addressing_on_every_host.sh b/tests/e2e/210_local_index_addressing_on_every_host.sh index cb9732208..0aea5085d 100755 --- a/tests/e2e/210_local_index_addressing_on_every_host.sh +++ b/tests/e2e/210_local_index_addressing_on_every_host.sh @@ -83,7 +83,7 @@ cat > "$TMP/ws/mcpp.toml" <<'EOF' [workspace] members = ["m1"] -[indices] +[workspace.indices] acme = { path = "index" } EOF mkdir -p "$TMP/ws/m1" diff --git a/tests/e2e/44_indices_e2e_integration.sh b/tests/e2e/44_indices_e2e_integration.sh index 2c0beb1e1..107958788 100755 --- a/tests/e2e/44_indices_e2e_integration.sh +++ b/tests/e2e/44_indices_e2e_integration.sh @@ -76,7 +76,7 @@ cat > mcpp.toml <.build]` reaches every member: `dialect_cxxflags` +# (a configuration key, read from the build's root) and `cxxflags` / +# `defines` (package keys). In 2026.10.8.1 a `[target..build]` on +# the root reached none, without a word (#786). +# P2 The package keys reach a member at every position: built as a +# sibling's `path` dependency too (W5). +# P3 `[workspace.profile.]` merges key by key with a member's own +# table of that name; the workspace's list comes first (W3). +# P4 A member's own definition of a macro the workspace defines wins. +# P5 `[workspace]` refuses a table it does not know, and one table written +# both at the root position and as `[workspace.X]` on a root without +# `[package]`. +# P6 W7: `[build]` on a root without `[package]` acts on nothing and is +# reported; `--strict` refuses. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +WS="$TMP/ws" +mkdir -p "$WS/lib/src" "$WS/app/src" +cd "$WS" +cat > mcpp.toml <<'TOML' +[workspace] +members = ["lib", "app"] + +[workspace.package] +mcpp = ">=2026.10.10.1" + +[workspace.target.'cfg(any(os = "linux", os = "macos", os = "windows"))'.build] +dialect_cxxflags = ["-DWS_DIALECT=1"] +cxxflags = ["-DWS_PACKAGE=1"] +defines = ["WS_MACRO=1"] + +[workspace.profile.release] +cxxflags = ["-DWS_RELEASE=1"] +TOML +cat > lib/mcpp.toml <<'TOML' +[package] +name = "lib" +version = "0.1.0" + +[targets.lib] +kind = "lib" +TOML +cat > lib/src/lib.cppm <<'CPP' +module; +#if !defined(WS_DIALECT) || !defined(WS_PACKAGE) +#error "the workspace layer did not reach lib" +#endif +export module lib; +export int lib_value() { return WS_MACRO; } +CPP +cat > app/mcpp.toml <<'TOML' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +lib = { path = "../lib" } + +[target.'cfg(any(os = "linux", os = "macos", os = "windows"))'.build] +defines = ["WS_MACRO=2"] + +[profile.release] +cxxflags = ["-DAPP_RELEASE=1"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" +TOML +cat > app/src/main.cpp <<'CPP' +#if !defined(WS_DIALECT) || !defined(WS_PACKAGE) +#error "the workspace layer did not reach app" +#endif +#include +import lib; +int main() { std::printf("macro=%d lib=%d\n", WS_MACRO, lib_value()); } +CPP + +# ── P1, P2, P4 ──────────────────────────────────────────────────────────── +"$MCPP" build -p app > build.log 2>&1 || fail "the workspace layer build failed" build.log +out=$(find target -path '*/bin/*' -name 'app' -type f -o -path '*/bin/*' -name 'app.exe' -type f | head -1) +[ -n "$out" ] || fail "no app binary" build.log +got=$("$out" | tr -d '\r') +[ "$got" = "macro=2 lib=1" ] \ + || fail "expected app's own WS_MACRO=2 and lib's inherited WS_MACRO=1, got '$got'" build.log +if grep -q "workspace-position\|acts on no package" build.log; then + fail "the canonical spelling was reported as a deprecated or inert one" build.log +fi +echo "ok P1 P2 P4" + +# ── P3 ──────────────────────────────────────────────────────────────────── +"$MCPP" build -p app --release > release.log 2>&1 || fail "release build failed" release.log +entry=$(python3 - <<'PY' +import json, glob +for db in glob.glob("target/**/compile_commands.json", recursive=True): + for e in json.load(open(db)): + if e["file"].replace("\\", "/").endswith("app/src/main.cpp"): + args = e.get("arguments") or e["command"].split() + if any("APP_RELEASE" in a for a in args): + print(" ".join(args)); raise SystemExit +PY +) +case "$entry" in + *-DWS_RELEASE=1*-DAPP_RELEASE=1*) ;; + *) fail "[workspace.profile.release] and the member's [profile.release] did not merge, workspace first: $entry" release.log ;; +esac +echo "ok P3" + +# ── P5 ──────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/unknown/app" "$TMP/twice/app" +cp app/mcpp.toml "$TMP/unknown/app/"; cp app/mcpp.toml "$TMP/twice/app/" +printf '[workspace]\nmembers = ["app"]\n[workspace.tolchain]\ndefault = "gcc@16.1.0"\n' > "$TMP/unknown/mcpp.toml" +if ( cd "$TMP/unknown" && "$MCPP" build -p app > "$TMP/unknown.log" 2>&1 ); then + fail "an unknown [workspace] table was accepted" "$TMP/unknown.log" +fi +grep -q "\[workspace\] has no key or table 'tolchain'" "$TMP/unknown.log" \ + || fail "the refusal does not name the table" "$TMP/unknown.log" +printf '[workspace]\nmembers = ["app"]\n[toolchain]\ndefault = "gcc@16.1.0"\n[workspace.toolchain]\ndefault = "gcc@16.1.0"\n' > "$TMP/twice/mcpp.toml" +if ( cd "$TMP/twice" && "$MCPP" build -p app > "$TMP/twice.log" 2>&1 ); then + fail "one table written twice was accepted" "$TMP/twice.log" +fi +grep -q "write it once, as \[workspace.toolchain\]" "$TMP/twice.log" \ + || fail "the refusal does not say which spelling" "$TMP/twice.log" +echo "ok P5" + +# ── P6 ──────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/inert/app/src" +printf '[package]\nname = "app"\nversion = "0.1.0"\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/inert/app/mcpp.toml" +echo 'int main() { return 0; }' > "$TMP/inert/app/src/main.cpp" +printf '[workspace]\nmembers = ["app"]\n[build]\ncxxflags = ["-DNOWHERE=1"]\n' > "$TMP/inert/mcpp.toml" +( cd "$TMP/inert" && "$MCPP" build -p app > "$TMP/inert.log" 2>&1 ) \ + || fail "W7 is a warning, not a refusal" "$TMP/inert.log" +grep -q "\[build\] is written on a workspace root without \[package\]" "$TMP/inert.log" \ + || fail "W7 said nothing" "$TMP/inert.log" +if ( cd "$TMP/inert" && "$MCPP" build -p app --strict > "$TMP/inert-strict.log" 2>&1 ); then + fail "--strict accepted W7" "$TMP/inert-strict.log" +fi +echo "ok P6" + +echo "PASS: 893_the_workspace_prefix_carries_the_meaning" diff --git a/tests/e2e/894_the_position_reading_is_kept_and_reported.sh b/tests/e2e/894_the_position_reading_is_kept_and_reported.sh new file mode 100755 index 000000000..b19b2247a --- /dev/null +++ b/tests/e2e/894_the_position_reading_is_kept_and_reported.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# requires: gcc +# 894 -- COMPAT(workspace-position): a workspace root's own `[profile.*]`, +# `[toolchain]`, `[indices]`, the rows of `[target.]` and `[xlings]` still +# reach its members by POSITION, with the merge they had, until mcpp 1.0.0 +# (SPEC-004 §9.10, design 2026-10-10 §3.6). What changed is that it is said. +# +# C1 A root without `[package]`: the root's `[profile.release]` reaches the +# member as before, and every use is reported with the canonical +# spelling. +# C2 A member's own table of that name replaces the root's whole, as before. +# C3 A root with `[package]`: building only the root package says nothing -- +# the table is that package's own -- and building a member that receives +# it by position names the member. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +flags_of() { # $1 = path suffix of the source; every argument of its entry, one line + python3 - "$1" <<'PY' +import json, glob, sys +for db in glob.glob("target/**/compile_commands.json", recursive=True): + for e in json.load(open(db)): + if e["file"].replace("\\", "/").endswith(sys.argv[1]): + print(" ".join(e.get("arguments") or e["command"].split())) +PY +} + +member() { # $1 = dir, $2 = extra manifest text + mkdir -p "$1/src" + printf '[package]\nname = "%s"\nversion = "0.1.0"\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n%s\n' \ + "$(basename "$1")" "$(basename "$1")" "$2" > "$1/mcpp.toml" + echo 'int main() { return 0; }' > "$1/src/main.cpp" +} + +# ── C1, C2 ──────────────────────────────────────────────────────────────── +mkdir -p "$TMP/virtual" && cd "$TMP/virtual" +printf '[workspace]\nmembers = ["plain", "own"]\n\n[profile.release]\ncxxflags = ["-DROOT_RELEASE=1"]\n' > mcpp.toml +member plain "" +member own $'[profile.release]\ncxxflags = ["-DOWN_RELEASE=1"]' +"$MCPP" build -p plain --release > plain.log 2>&1 || fail "plain" plain.log +flags_of plain/src/main.cpp | grep -q -- "-DROOT_RELEASE=1" \ + || fail "the root's [profile.release] no longer reaches the member by position" plain.log +grep -q "\[profile.\] on the workspace root reaches every member by its position" plain.log \ + || fail "the position reading said nothing" plain.log +grep -q "\[workspace.profile.\]" plain.log || fail "the warning names no canonical spelling" plain.log +"$MCPP" build -p own --release > own.log 2>&1 || fail "own" own.log +got=$(flags_of own/src/main.cpp) +case "$got" in + *-DOWN_RELEASE=1*) ;; + *) fail "the member's own [profile.release] was lost: $got" own.log ;; +esac +case "$got" in + *-DROOT_RELEASE=1*) fail "the position reading merged a named table it used to replace whole: $got" own.log ;; +esac +echo "ok C1 C2" + +# ── C3 ──────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/rooted/src" && cd "$TMP/rooted" +printf '[package]\nname = "root"\nversion = "0.1.0"\n[targets.root]\nkind = "bin"\nmain = "src/main.cpp"\n\n[workspace]\nmembers = ["sub"]\n\n[profile.release]\ncxxflags = ["-DROOT_RELEASE=1"]\n' > mcpp.toml +echo 'int main() { return 0; }' > src/main.cpp +member sub "" +"$MCPP" build --release > root.log 2>&1 || fail "root package" root.log +if grep -q "workspace-position\|by its position" root.log; then + fail "the root package's own table was reported as a position reading" root.log +fi +"$MCPP" build -p sub --release > sub.log 2>&1 || fail "sub" sub.log +grep -q "reaches member 'sub' by its position" sub.log \ + || fail "a member that received the root's table by position was not told" sub.log +flags_of sub/src/main.cpp | grep -q -- "-DROOT_RELEASE=1" \ + || fail "the position reading changed for a rooted workspace" sub.log +echo "ok C3" + +echo "PASS: 894_the_position_reading_is_kept_and_reported" diff --git a/tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh b/tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh new file mode 100755 index 000000000..4b964e383 --- /dev/null +++ b/tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# requires: gcc +# 895 -- a member's conditional configuration rows count as they evaluate for +# the build (W4, design 2026-10-10 §3.5 item 5; #786). +# +# R1 A member's own `[target..build] dialect_cxxflags` reaches the +# build when the member is planned under the workspace's virtual root. +# 2026.10.8.1 dropped it: the plan's root carried none of the member's +# rows (`DIAL=0` with `-p app`, `DIAL=2` for the same package alone). +# R2 Two members that differ only in a row for another target are one +# configuration: `--workspace` plans one graph. +# R3 Two members whose rows evaluate differently for this target are two. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +HOST='cfg(any(os = "linux", os = "macos", os = "windows"))' +member() { # $1 = name, $2 = extra manifest text + mkdir -p "$1/src" + printf '[package]\nname = "%s"\nversion = "0.1.0"\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n%s\n' \ + "$1" "$1" "$2" > "$1/mcpp.toml" + printf '#include \n#ifndef DIAL\n#define DIAL 0\n#endif\nint main() { std::printf("DIAL=%%d\\n", DIAL); }\n' > "$1/src/main.cpp" +} +run_of() { local b; b=$(find target -path "*/bin/$1/$1" -type f -o -path "*/bin/$1" -type f -o -path "*/bin/$1.exe" -type f | head -1); "$b" | tr -d '\r'; } + +# ── R1 ──────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/r1" && cd "$TMP/r1" +printf '[workspace]\nmembers = ["app"]\n' > mcpp.toml +member app "[target.'$HOST'.build] +dialect_cxxflags = [\"-DDIAL=2\"]" +"$MCPP" build -p app > b.log 2>&1 || fail "build" b.log +got=$(run_of app) +[ "$got" = "DIAL=2" ] || fail "the member's own conditional dialect_cxxflags did not reach the build: $got" b.log +echo "ok R1" + +# ── R2, R3 ──────────────────────────────────────────────────────────────── +mkdir -p "$TMP/r2" && cd "$TMP/r2" +printf '[workspace]\nmembers = ["a", "b"]\n' > mcpp.toml +member a "[target.'cfg(os = \"none\")'.build] +dialect_cxxflags = [\"-DNEVER=1\"]" +member b "" +"$MCPP" build --workspace > ws.log 2>&1 || fail "--workspace" ws.log +n=$(find target -name build.ninja | wc -l | tr -d ' ') +[ "$n" = 1 ] || fail "a row for another target split the members into $n configurations" ws.log + +mkdir -p "$TMP/r3" && cd "$TMP/r3" +printf '[workspace]\nmembers = ["a", "b"]\n' > mcpp.toml +member a "[target.'$HOST'.build] +dialect_cxxflags = [\"-DDIAL=3\"]" +member b "" +"$MCPP" build --workspace > ws.log 2>&1 || fail "--workspace" ws.log +n=$(find target -name build.ninja | wc -l | tr -d ' ') +[ "$n" = 2 ] || fail "members whose rows evaluate differently share $n configuration(s)" ws.log +[ "$(run_of a)" = "DIAL=3" ] || fail "member a lost its dialect: $(run_of a)" ws.log +[ "$(run_of b)" = "DIAL=0" ] || fail "member b received a's dialect: $(run_of b)" ws.log +echo "ok R2 R3" + +echo "PASS: 895_conditional_configuration_rows_count_as_they_evaluate" diff --git a/tests/e2e/896_an_action_output_has_one_writer.sh b/tests/e2e/896_an_action_output_has_one_writer.sh new file mode 100755 index 000000000..747943671 --- /dev/null +++ b/tests/e2e/896_an_action_output_has_one_writer.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell +# 896 -- an action's output is written by the action and by nothing else +# (D6, design 2026-10-10 §4; #778). +# +# Prepare used to write an empty placeholder for every generated translation +# unit, so the scan had a file to read. The placeholder was newer than the +# action's inputs: whenever a ninja log already held the action, ninja took +# the placeholder for the generated file, compiled it empty, and the link +# failed with `undefined reference to resource()` (#778). +# +# O1 `--configure-only` leaves no file where the action will write. +# O2 A generated file removed after a build is generated again, not +# replaced by an empty one. +# O3 A generated file that exists (here a legitimately empty one written by +# the action) is not rewritten by prepare. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +mkdir -p "$TMP/ws/app" && cd "$TMP/ws" +printf '[workspace]\nmembers = ["app"]\n' > mcpp.toml +cat > app/mcpp.toml <<'TOML' +[package] +name = "app" +version = "0.0.1" +[build] +sources = [] +[targets.app] +kind = "bin" +main = "main.cpp" +TOML +printf 'extern const char* resource();\nint main() { return resource()[0] == %sR%s ? 0 : 1; }\n' "'" "'" > app/main.cpp +printf '#!/bin/sh\nprintf %sconst char* resource() { return "RESOURCE"; }\\n%s > "$1"\nprintf "" > "$2"\n' "'" "'" > app/gen.sh +cat > app/build.mcpp <<'CPP' +import std; +import mcpp; +int main() { + const auto root = std::filesystem::path(mcpp::manifest_dir()); + const auto out = std::filesystem::path(mcpp::out_dir()); + const auto gen = (root / "gen.sh").generic_string(); + const auto cpp = (out / "generated.cpp").generic_string(); + const auto empty = (out / "empty.cpp").generic_string(); + mcpp::action a; + a.id = "generate:resource"; + a.role = mcpp::roles::source; + a.arg("sh").arg(gen.c_str()).arg(cpp.c_str()).arg(empty.c_str()) + .input(gen.c_str()).output(cpp.c_str()).output(empty.c_str()).submit(); + return 0; +} +CPP + +# ── O1 ──────────────────────────────────────────────────────────────────── +"$MCPP" build -p app --configure-only > configure.log 2>&1 || fail "configure-only" configure.log +if find . -name generated.cpp | grep -q .; then + fail "prepare wrote the action's output: $(find . -name generated.cpp)" configure.log +fi +echo "ok O1" + +# ── O2 ──────────────────────────────────────────────────────────────────── +"$MCPP" build -p app > b1.log 2>&1 || fail "first build" b1.log +gen=$(find . -path '*/out/*' -name generated.cpp | head -1) +[ -s "$gen" ] || fail "the action did not write its output" b1.log +rm -f "$gen" +"$MCPP" build -p app > b2.log 2>&1 || fail "the build after the generated file was removed failed (#778)" b2.log +[ -s "$gen" ] || fail "the generated file was not generated again" b2.log +echo "ok O2" + +# ── O3 ──────────────────────────────────────────────────────────────────── +empty=$(find . -path '*/out/*' -name empty.cpp | head -1) +[ -f "$empty" ] && [ ! -s "$empty" ] || fail "the action's empty output is missing or not empty" b2.log +before=$(python3 -c 'import os,sys; print(os.stat(sys.argv[1]).st_mtime_ns)' "$empty") +sleep 1 +"$MCPP" build -p app > b3.log 2>&1 || fail "the third build" b3.log +after=$(python3 -c 'import os,sys; print(os.stat(sys.argv[1]).st_mtime_ns)' "$empty") +[ "$before" = "$after" ] || fail "an existing output was rewritten by prepare" b3.log +echo "ok O3" + +echo "PASS: 896_an_action_output_has_one_writer" diff --git a/tests/e2e/897_each_configuration_has_its_own_generated_output.sh b/tests/e2e/897_each_configuration_has_its_own_generated_output.sh new file mode 100755 index 000000000..64e6e759a --- /dev/null +++ b/tests/e2e/897_each_configuration_has_its_own_generated_output.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell +# 897 -- `mcpp::out_dir()` is per configuration (D7, D25; design 2026-10-10 +# §4.2), and `resolution.json` says where it is. +# +# One `target/.build-mcpp/out` used to serve every configuration. A generator +# whose output depends on `mcpp::profile()` wrote one `gen.cpp`; each +# configuration's ninja log believed it current, so a dev build after a +# release build ran with the release text (R2: `GEN=release`). +# +# G1 dev, release, dev: each binary prints its own profile. +# G2 the two configurations' `outDir` (resolution.json, graph.packages) +# differ, exist, and hold the generated file. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +mkdir -p "$TMP/gp/src" && cd "$TMP/gp" +cat > mcpp.toml <<'TOML' +[package] +name = "gp" +version = "0.1.0" +[build] +sources = ["src/main.cpp"] +[targets.gp] +kind = "bin" +main = "src/main.cpp" +TOML +cat > src/main.cpp <<'CPP' +#include +const char* gen_profile(); +int main() { std::printf("GEN=%s\n", gen_profile()); } +CPP +printf '#!/bin/sh\nprintf %sconst char* gen_profile() { return "%%s"; }\\n%s "$1" > "$2"\n' "'" "'" > gen.sh +cat > build.mcpp <<'CPP' +import std; +import mcpp; +int main() { + const auto root = std::filesystem::path(mcpp::manifest_dir()); + const auto out = (std::filesystem::path(mcpp::out_dir()) / "gen.cpp").generic_string(); + const auto gen = (root / "gen.sh").generic_string(); + mcpp::action a; + a.id = "gen"; + a.role = mcpp::roles::source; + a.arg("sh").arg(gen.c_str()).arg(mcpp::profile()).arg(out.c_str()) + .input(gen.c_str()).output(out.c_str()).submit(); + return 0; +} +CPP + +outdir() { # $1 = a configuration directory: the root package's outDir + python3 - "$1/resolution.json" <<'PY' +import json, sys +for p in json.load(open(sys.argv[1]))["graph"]["packages"]: + if p.get("root"): + print(p.get("outDir", "")); break +PY +} + +for p in dev release dev; do + if [ "$p" = release ]; then "$MCPP" build --release > "b-$p.log" 2>&1 || fail "$p" "b-$p.log" + else "$MCPP" build > "b-$p.log" 2>&1 || fail "$p" "b-$p.log"; fi +done + +# Each configuration directory: what its binary prints, and what its outDir's +# generated file says. They must agree, and the two must be dev and release. +seen="" +for r in $(find target -name resolution.json); do + d=$(dirname "$r") + out=$(outdir "$d") + [ -n "$out" ] || fail "resolution.json records no outDir: $r" + [ -s "$out/gen.cpp" ] || fail "outDir $out does not hold the generated file" + text=$(grep -o '"[a-z]*"' "$out/gen.cpp" | tr -d '"') + got=$("$d/bin/gp" | tr -d '\r') + [ "$got" = "GEN=$text" ] || fail "the binary in $d prints '$got' and its outDir was generated for '$text'" + seen="$seen $text:$out" +done +case "$seen" in + *dev:*release:* | *release:*dev:*) ;; + *) fail "expected one dev and one release configuration, saw:$seen" ;; +esac +dev=$(echo "$seen" | tr ' ' '\n' | grep '^dev:' | cut -d: -f2-) +rel=$(echo "$seen" | tr ' ' '\n' | grep '^release:' | cut -d: -f2-) +[ "$dev" != "$rel" ] || fail "dev and release share one out_dir: $dev" +echo "ok G1 G2" + +echo "PASS: 897_each_configuration_has_its_own_generated_output" diff --git a/tests/e2e/898_module_units_in_cpp_files.sh b/tests/e2e/898_module_units_in_cpp_files.sh new file mode 100755 index 000000000..82f77ea00 --- /dev/null +++ b/tests/e2e/898_module_units_in_cpp_files.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# requires: gcc llvm +# 898 -- the module declaration decides a unit's role, the extension its +# language (D8, design 2026-10-10 §5; #790). +# +# The rule used to be chosen by extension: a `.cpp` holding a module unit was +# compiled as a plain object. Under Clang that meant +# +# M1 `module m:part;` in a .cpp declared a BMI its edge never wrote, so +# every build compiled it again and relinked (R4); +# M2 `export module m;` in a .cpp wrote no BMI, and the importer failed +# with `module 'm' not found` (R5); +# M3 `import :part;` of such a partition failed the same way (R6). +# +# GCC reads the role from the content and built all three; it is the control. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +steps() { grep -cE '^\[[0-9]+/' "$1" || true; } + +for tc in llvm@23.1.3 gcc@16.1.0; do + # ── M1, M3 ──────────────────────────────────────────────────────────── + P="$TMP/$tc/parts"; mkdir -p "$P/src"; cd "$P" + printf '[package]\nname = "repro"\nversion = "0.1.0"\n[toolchain]\ndefault = "%s"\n[targets.repro]\nkind = "bin"\nmain = "src/main.cpp"\n' "$tc" > mcpp.toml + printf 'export module repro;\nexport import :api;\nimport :api_impl;\n' > src/repro.cppm + printf 'export module repro:api;\nexport auto answer() -> int;\n' > src/api.cppm + printf 'module repro:api_impl;\nimport :api;\nauto answer() -> int { return 42; }\n' > src/api_impl.cpp + printf 'import repro;\nimport std;\nauto main() -> int { std::println("{}", answer()); }\n' > src/main.cpp + "$MCPP" build > b1.log 2>&1 || fail "$tc: a .cpp implementation partition, imported" b1.log + "$MCPP" build --verbose > b2.log 2>&1 || fail "$tc: second build" b2.log + [ "$(steps b2.log)" = 0 ] || fail "$tc: the second build did $(steps b2.log) steps" b2.log + [ "$(find target -path '*/bin/repro*' -type f -exec {} \; | tr -d '\r')" = 42 ] || fail "$tc: wrong answer" b1.log + + # ── M2 ──────────────────────────────────────────────────────────────── + I="$TMP/$tc/iface"; mkdir -p "$I/src"; cd "$I" + printf '[package]\nname = "r5"\nversion = "0.1.0"\n[toolchain]\ndefault = "%s"\n[targets.r5]\nkind = "bin"\nmain = "src/main.cpp"\n' "$tc" > mcpp.toml + printf 'export module r5;\nexport auto answer() -> int { return 42; }\n' > src/r5.cpp + printf 'import r5;\nimport std;\nauto main() -> int { std::println("{}", answer()); }\n' > src/main.cpp + "$MCPP" build > b1.log 2>&1 || fail "$tc: an interface unit in a .cpp" b1.log + "$MCPP" build --verbose > b2.log 2>&1 || fail "$tc: second build" b2.log + [ "$(steps b2.log)" = 0 ] || fail "$tc: the second build did $(steps b2.log) steps" b2.log + echo "ok $tc" +done + +echo "PASS: 898_module_units_in_cpp_files" diff --git a/tests/e2e/899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh b/tests/e2e/899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh new file mode 100755 index 000000000..3e847b716 --- /dev/null +++ b/tests/e2e/899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# requires: gcc llvm +# 899 -- `--toolchain` replacing a toolchain the manifest declared is done as +# asked, and said (D11, design 2026-10-10 §8). +# +# T1 A different spec: one warning naming both specs, the key and the file. +# T2 The same spec, or a manifest that declares none: nothing. +# T3 A member's toolchain from `[workspace.toolchain]` names the workspace +# root's manifest. +# T4 It is a warning: `--strict` builds. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +pkg() { # $1 = dir, $2 = extra manifest text + mkdir -p "$1/src" + printf '[package]\nname = "%s"\nversion = "0.1.0"\n%s\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' \ + "$(basename "$1")" "$2" "$(basename "$1")" > "$1/mcpp.toml" + echo 'int main() { return 0; }' > "$1/src/main.cpp" +} + +# ── T1, T4 ──────────────────────────────────────────────────────────────── +pkg "$TMP/declared" $'[toolchain]\ndefault = "gcc@16.1.0"' +cd "$TMP/declared" +"$MCPP" build --toolchain llvm@23.1.3 --strict > t1.log 2>&1 || fail "--strict failed on the override warning" t1.log +grep -q "warning: --toolchain llvm@23.1.3 replaces gcc@16.1.0 declared at \[toolchain\].default (mcpp.toml)" t1.log \ + || fail "no warning for a replaced toolchain" t1.log +echo "ok T1 T4" + +# ── T2 ──────────────────────────────────────────────────────────────────── +"$MCPP" build --toolchain gcc@16.1.0 > same.log 2>&1 || fail "same" same.log +if grep -q "replaces" same.log; then fail "warned for the declared spec itself" same.log; fi +pkg "$TMP/undeclared" "" +cd "$TMP/undeclared" +"$MCPP" build --toolchain llvm@23.1.3 > none.log 2>&1 || fail "undeclared" none.log +if grep -q "replaces" none.log; then fail "warned where the manifest declares no toolchain" none.log; fi +echo "ok T2" + +# ── T3 ──────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/ws" && cd "$TMP/ws" +printf '[workspace]\nmembers = ["app"]\n\n[workspace.toolchain]\ndefault = "gcc@16.1.0"\n' > mcpp.toml +pkg app "" +"$MCPP" build -p app --toolchain llvm@23.1.3 > ws.log 2>&1 || fail "workspace member" ws.log +grep -q "replaces gcc@16.1.0 declared at \[toolchain\].default (mcpp.toml)" ws.log \ + || fail "the warning does not name the workspace root's manifest" ws.log +echo "ok T3" + +echo "PASS: 899_toolchain_flag_replacing_a_declared_toolchain_says_so" diff --git a/tests/e2e/900_publish_writes_the_workspace_layer_into_the_member.sh b/tests/e2e/900_publish_writes_the_workspace_layer_into_the_member.sh new file mode 100755 index 000000000..329310b2e --- /dev/null +++ b/tests/e2e/900_publish_writes_the_workspace_layer_into_the_member.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell +# 900 -- `mcpp publish` writes what the workspace layer gave a member into the +# member's own tables (W6, SPEC-004 §9.7 and §9.10; design 2026-10-10 §3.2). +# +# The archive holds the member's directory alone. Built as the root of a +# host-tool sub-build from an index, it must be configured as it was in its +# workspace, so `[workspace.toolchain]` is written as `[toolchain]` and +# `[workspace.target..build]` as `[target..build]`, merged as the +# build merged them. +# +# W1 the archived mcpp.toml carries `[toolchain]` and the conditional row; +# W2 a relative path the workspace wrote is rewritten against the package; +# W3 one that leaves the package is refused, naming the key. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +REPO="$TMP/repo" +mkdir -p "$REPO/util/src" "$REPO/util/inc" +cd "$REPO" +cat > mcpp.toml <<'TOML' +[workspace] +members = ["util"] + +[workspace.package] +version = "0.3.0" +license = "MIT" +repo = "https://github.com/example/probe" + +[workspace.toolchain] +default = "gcc@16.1.0" + +[workspace.target.'cfg(os = "linux")'.build] +cxxflags = ["-DWS_COND=1"] +include_dirs = ["util/inc"] +TOML +cat > util/mcpp.toml <<'TOML' +[package] +namespace = "probe" +name = "util" + +[target.'cfg(os = "linux")'.build] +cxxflags = ["-DOWN_COND=1"] + +[targets.probe_util] +kind = "lib" +TOML +cat > util/src/util.cppm <<'CPP' +export module probe.util; +export int util_value() { return 42; } +CPP +git init -q -b main . && git add -A \ + && git -c user.email=e2e@example.invalid -c user.name=e2e commit -qm init + +# ── W1, W2 ──────────────────────────────────────────────────────────────── +( cd util && "$MCPP" publish --dry-run --allow-dirty > "$TMP/pub.log" 2>&1 ) \ + || fail "publish of a member with a workspace layer failed" "$TMP/pub.log" +ARCHIVE="$REPO/util/target/dist/util-0.3.0.tar.gz" +[ -f "$ARCHIVE" ] || fail "no archive at $ARCHIVE" "$TMP/pub.log" +tar -xzf "$ARCHIVE" -O util-0.3.0/mcpp.toml > "$TMP/util.toml" +python3 - "$TMP/util.toml" <<'PY' || fail "the archived manifest is not what the member was built with" "$TMP/util.toml" +import sys, tomllib +m = tomllib.load(open(sys.argv[1], "rb")) +assert m["toolchain"]["default"] == "gcc@16.1.0", m.get("toolchain") +row = m["target"]['cfg(os = "linux")']["build"] +assert row["cxxflags"] == ["-DWS_COND=1", "-DOWN_COND=1"], row +assert row["include_dirs"] == ["inc"], row +PY +echo "ok W1 W2" + +# ── W3 ──────────────────────────────────────────────────────────────────── +sed -i.bak 's|include_dirs = \["util/inc"\]|include_dirs = ["shared/inc"]|' mcpp.toml && rm -f mcpp.toml.bak +mkdir -p shared/inc +if ( cd util && "$MCPP" publish --dry-run --allow-dirty > "$TMP/pub-out.log" 2>&1 ); then + fail "a workspace path outside the package was published" "$TMP/pub-out.log" +fi +grep -q "outside this package's directory" "$TMP/pub-out.log" \ + || fail "the refusal does not say why" "$TMP/pub-out.log" +echo "ok W3" + +echo "PASS: 900_publish_writes_the_workspace_layer_into_the_member" diff --git a/tests/e2e/901_the_toolchain_library_path_stays_with_the_tools.sh b/tests/e2e/901_the_toolchain_library_path_stays_with_the_tools.sh new file mode 100755 index 000000000..98139cb41 --- /dev/null +++ b/tests/e2e/901_the_toolchain_library_path_stays_with_the_tools.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# requires: llvm elf unix-shell +# 901 -- a managed LLVM's private library path reaches the toolchain's own +# programs and nothing else ninja starts (D29, design 2026-10-10 §13). +# +# It used to be in ninja's environment, so every `/bin/sh -c` ninja started +# inherited it. A shell linked against libc++ -- Android's bionic `sh` -- +# then loaded the LLVM's glibc-built libc++ and died before running anything +# (`CANNOT LINK EXECUTABLE "/bin/sh"`, termux-docker aarch64). On a glibc +# host the shell survives, which is why the assertion reads the variable an +# action sees rather than waiting for a shell to fail. +# +# L1 a check action run by ninja sees no LLVM directory in LD_LIBRARY_PATH; +# L2 the build with `import std` still links and runs (the tools have it). +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +mkdir -p "$TMP/p/src" && cd "$TMP/p" +cat > mcpp.toml <<'TOML' +[package] +name = "lw" +version = "0.1.0" +[toolchain] +default = "llvm@23.1.3" +[targets.lw] +kind = "bin" +main = "src/main.cpp" +TOML +printf 'import std;\nint main() { std::println("ran"); }\n' > src/main.cpp +cat > build.mcpp <<'CPP' +import std; +import mcpp; +int main() { + const auto out = (std::filesystem::path(mcpp::out_dir()) / "ldpath.txt").generic_string(); + mcpp::action a; + a.id = "ldpath"; + a.role = "check"; + a.arg("/bin/sh").arg("-c").arg(("echo \"LDP=[$LD_LIBRARY_PATH]\" > " + out).c_str()) + .output(out.c_str()).submit(); + return 0; +} +CPP + +"$MCPP" build > build.log 2>&1 || fail "the llvm build failed" build.log +seen=$(cat "$(find target -name ldpath.txt | head -1)" 2>/dev/null) +[ -n "$seen" ] || fail "the check action did not run" build.log +case "$seen" in + *xim-x-llvm*) fail "the toolchain's library path reached a shell ninja started: $seen" build.log ;; +esac +echo "ok L1 ($seen)" +[ "$(find target -path '*/bin/lw' -type f -exec {} \;)" = ran ] || fail "the program did not run" build.log +echo "ok L2" + +echo "PASS: 901_the_toolchain_library_path_stays_with_the_tools" From 0c47db0a577802ad0b821d31b8f0df01b440e59e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:12:00 +0900 Subject: [PATCH 14/32] docs(workspace): the prefix carries the meaning; ci: Termux fresh-install jobs; the design record lands --- ...026-10-10-one-source-one-meaning-design.md | 28 +++- .agents/docs/README.md | 4 +- .github/tools/termux_fresh_install.sh | 98 ++++++++++++ .github/workflows/ci-fresh-install.yml | 52 ++++++ build.log | 1 + docs/07-workspace.md | 150 ++++++++++++------ docs/zh/07-workspace.md | 120 +++++++++----- release.log | 1 + 8 files changed, 363 insertions(+), 91 deletions(-) create mode 100644 .github/tools/termux_fresh_install.sh create mode 100644 build.log create mode 100644 release.log diff --git a/.agents/docs/2026-10-10-one-source-one-meaning-design.md b/.agents/docs/2026-10-10-one-source-one-meaning-design.md index 4f7739228..55e0c1eb6 100644 --- a/.agents/docs/2026-10-10-one-source-one-meaning-design.md +++ b/.agents/docs/2026-10-10-one-source-one-meaning-design.md @@ -1,17 +1,41 @@ --- subject: design -status: proposed +status: landed --- # 一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790) -- 日期:2026-10-10。状态:修订 5,跨仓库方案(mcpp、xlings、xim-pkgindex/xlings-res、mcpp-plugins),待维护者 review;尚未实施。 +- 日期:2026-10-10。状态:修订 6,已实施(mcpp 2026.10.10.1、xlings 2026.10.10.2、xim-pkgindex glibc 2.44.3 r4、mcpp-plugins)。 - 基线:`main` `b1652184`(2026.10.8.1)。 - 证据:本地 Linux x86_64,已发布的 mcpp 2026.10.8.1,gcc 16.1.0 与 llvm 23.1.3。 复现脚本 [2026-10-10-one-source-one-meaning-repro.sh](2026-10-10-one-source-one-meaning-repro.sh) 只打印观察结果,不做断言;下文 R1–R6 引用其中的用例。 - 外部 PR #787(#786)与 #779(#778)各自只修复报告中的一个实例,本文不以它们为基础(§15)。 +**修订 6 的变化(实施时的结论):** +- review 结论:Q1 X1–X3 全做;Q2 做 P1;Q3 按依赖关系等 xlings 发布;Q4 D29 用 `env LD_LIBRARY_PATH=… `; + Q5 全部发布后由维护者在真机验收(O11)。 +- X3 的钩子心跳为 10 s 后首次、之后每 30 s(不是每 10 s);终端上的解包行同样在 10 s 后首次、之后每 30 s, + NDJSON 的 `extract` 事件每多读 1% 一次。接口协议升到 1.7。 +- P1 只重新打包 glibc(r4,两个架构,已发布到 GitHub 与 GitCode,逐字节核对)。gcc 16.1.0、musl-gcc 15.1.0 + 不重新发布:X1 已能处理,新 revision 会让每个已安装的用户重新下载。P2 只检查一次变更新增的载荷 URL。 +- 新增 X4(维护者报告):交互式加入已运行的 SubOS 会话时,命令得到调用者的终端,fish 退出 + (`tcgetpgrp failed`)。改为在沙箱内为它分配 PTY 作为控制终端,调用者终端置 raw 模式并转发字节与窗口尺寸。 +- xlings PR 651 另修一处竞态:proxy 会话结束时 supervisor 立即 SIGKILL bwrap,命令的退出码变成 137。 +- 兼容模块 `mcpp.pm.compat.workspace_position` 由 `mcpp.manifest` 伞模块导出,而不是 `mcpp.pm.compat` 门面: + 它读 `Manifest`,而 `mcpp.manifest.types` 导入该门面。 +- `KeyRegistry`(`mcpp.manifest.key_registry`)推导出:解析器的 `[build]` 与 `[target..build]` 已知键、 + `[workspace.target..build]` 接受的键、W7 的包表清单;单元测试核对它与 `kWorkspaceBuildKeys`、与分组键。 + `[target.]` 标量清单与文档中的键类别表仍由各自已有的测试守护。 +- D7:构建程序的缓存仍是每个包一条记录;切换配置会重新运行程序,不会重新编译它。 +- D28:以 argv 启动的 shell 由 `posix_shell()` 解析;`popen` 与 `std::system` 仍用 C 库的 `/bin/sh`, + 因此 Android 的基线是 10 及以上。 +- D23:mcpp 把 xlings 的 `extract`、`hook` 事件显示为 `Installing …` 行,并写入日志;不另加 30 s 心跳。 +- D26 以 `degraded`(`build-program/provides`)实现。 +- 工作空间层的解析:`[workspace.*]` 下的文档由同一个 `parse_document` 读取,保留原值的行号。 +- 测试:e2e 893–901(在 2026.10.8.1 上全部失败);Windows 探测由单元测试覆盖;D27 的两个 job 在 + `ci-fresh-install.yml`(发布后),发布前在探测 PR #791 上以候选包运行。 + **修订 5 的变化(review 结论与探测):** - Android 不换工具链:aarch64 仍用 llvm@23.1.3 + glibc,目标是真实跑通(D21 改为只识别、不改默认值)。 - §13 改写为跨仓库修复:xlings 的硬链接回退(X1)、失败分类与止损(X2)、阶段内进度(X3);载荷重新打包(P1、P2); diff --git a/.agents/docs/README.md b/.agents/docs/README.md index c0188c8fa..525851342 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -30,7 +30,7 @@ Records that declare one. Everything else is listed by date below. ### design -- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-design.md) — proposed +- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-design.md) — landed - [2026.10.5.3 发布方案:32 位 x86 的架构词汇、资源编译器的识别与增量(#776 后续)](2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md) — active - [下一个版本的发布方案:标准库模块、原生 MSVC LTO 与导出发现、共享库的链接配置、Windows 参数引号(#768 后续、#770、#771)](2026-10-05-std-module-pair-msvc-lto-and-export-discovery-design.md) — active - [`mcpp run` hands the terminal to the program, and the follow-ups of #761, #763 and #765 (#766)](2026-10-05-run-terminal-handoff-and-766-follow-ups-design.md) — landed @@ -124,7 +124,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-10 -- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-design.md) — proposed +- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-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 - [2026.10.5.3 发布方案:32 位 x86 的架构词汇、资源编译器的识别与增量(#776 后续)](2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md) — active diff --git a/.github/tools/termux_fresh_install.sh b/.github/tools/termux_fresh_install.sh new file mode 100644 index 000000000..a5e29397e --- /dev/null +++ b/.github/tools/termux_fresh_install.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# A fresh mcpp in Termux: `mcpp new` and `mcpp run` with the default toolchain +# (design 2026-10-10 §13, D27). Two places, because neither has both halves +# of a phone: +# +# docker termux/termux-docker (aarch64): Termux's bionic +# userland and paths, on a native arm64 runner. It +# has no `/bin`; Android 10 and later do +# (`/bin -> /system/bin`), which is mcpp's baseline, +# so the link is made first. No SELinux. +# emulator-download , then emulator-run +# An Android 14 emulator (x86_64, KVM) with the +# debuggable Termux APK; commands run through +# `run-as com.termux`, in the app's SELinux domain, +# where link(2) is refused -- the half that made +# every install of the managed glibc fail. +# +# is a release archive (`mcpp--linux-.tar.gz`) or a URL. +# Exits non-zero unless the program built and printed its line. +set -euo pipefail +MODE="${1:?mode}" +WORK="${RUNNER_TEMP:-/tmp}/termux-fresh" +mkdir -p "$WORK/logs" +S="${GITHUB_STEP_SUMMARY:-/dev/null}" +WANT="hello from termux" + +fetch() { # $1 = tarball path or URL, $2 = destination + case "$1" in + http*) curl -fsSL --retry 3 -o "$2" "$1" ;; + *) cp "$1" "$2" ;; + esac +} + +# The script that runs inside Termux. POSIX sh: under run-as there is only the +# system shell, and in termux-docker it is the same file. +cat > "$WORK/inside.sh" <<'SH' +#!/bin/sh +set -u +TARBALL="$1"; HOME="$2"; export HOME +export TMPDIR="$HOME/tmp"; mkdir -p "$TMPDIR" "$HOME/dist" +tar -xzf "$TARBALL" -C "$HOME/dist" +MCPP=$(find "$HOME/dist" -path '*/bin/mcpp' -type f | head -1) +"$MCPP" --version +"$MCPP" self env --format json | grep -o '"android":[a-z]*' +cd "$HOME" && rm -rf hello && "$MCPP" new hello >/dev/null +cd hello +printf 'import std;\nint main() { std::println("hello from termux"); }\n' > src/main.cpp +timeout 5400 "$MCPP" run +SH +chmod 755 "$WORK/inside.sh" + +case "$MODE" in +docker) + fetch "${2:?tarball}" "$WORK/mcpp.tar.gz" + chmod -R a+rX "$WORK"; chmod 777 "$WORK/logs" + docker pull -q termux/termux-docker:aarch64 + set +e + docker run --rm --privileged --entrypoint /system/bin/sh \ + -v "$WORK:/mnt/w" termux/termux-docker:aarch64 \ + -c 'ln -s /system/bin /bin && exec /entrypoint.sh sh /mnt/w/inside.sh /mnt/w/mcpp.tar.gz /data/data/com.termux/files/home' \ + 2>&1 | tee "$WORK/logs/docker.log" + set -e + ;; +emulator-download) + fetch "${2:?tarball}" "$WORK/mcpp.tar.gz" + curl -fsSL --retry 3 -o "$WORK/termux.apk" \ + "https://github.com/termux/termux-app/releases/download/v0.118.3/termux-app_v0.118.3+github-debug_x86_64.apk" + exit 0 + ;; +emulator-run) + A=/data/data/com.termux/files + adb wait-for-device + adb install -r "$WORK/termux.apk" >/dev/null + adb shell am start -n com.termux/com.termux.app.TermuxActivity >/dev/null + for _ in $(seq 1 60); do + adb shell run-as com.termux ls "$A/usr/bin/sh" >/dev/null 2>&1 && break + sleep 5 + done + for f in mcpp.tar.gz inside.sh; do + adb push "$WORK/$f" /data/local/tmp/ >/dev/null + adb shell chmod 644 "/data/local/tmp/$f" + adb shell run-as com.termux cp "/data/local/tmp/$f" "$A/$f" + done + echo "SELinux: $(adb shell getenforce | tr -d '\r'); context: $(adb shell run-as com.termux cat /proc/self/attr/current | tr -d '\r\0')" \ + | tee -a "$S" + set +e + adb shell run-as com.termux "$A/usr/bin/env" PREFIX="$A/usr" PATH="$A/usr/bin" \ + LANG=en_US.UTF-8 sh "$A/inside.sh" "$A/mcpp.tar.gz" "$A/home" 2>&1 \ + | tr -d '\r' | tee "$WORK/logs/emulator.log" + set -e + ;; +*) echo "unknown mode $MODE"; exit 2 ;; +esac + +log=$(ls "$WORK"/logs/*.log | head -1) +{ echo "### Termux ($MODE)"; echo '```'; tail -25 "$log"; echo '```'; } >> "$S" +grep -qx "$WANT" "$log" || { echo "FAIL: the program did not print '$WANT'"; exit 1; } +echo "PASS: mcpp new + run in Termux ($MODE)" diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 557cf26a6..41c532b33 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -358,6 +358,58 @@ jobs: # ────────────────────────────────────────────────────────────────── # macOS: llvm@23.1.3 # ────────────────────────────────────────────────────────────────── + # ────────────────────────────────────────────────────────────────── + # Termux (Android): the released tarball, `mcpp new` → `mcpp run` with the + # default toolchain (design 2026-10-10 §13, D27). Two halves, because no + # runner has a phone: termux-docker on a native arm64 runner is Termux's + # bionic userland and aarch64's default toolchain (llvm + the managed glibc); + # an Android 14 emulator (x86_64, KVM) is the app sandbox, where SELinux + # refuses link(2) and every install of a payload with hard links failed. + # The real aarch64 phone is a release check (O11), not a runner. + # ────────────────────────────────────────────────────────────────── + termux-docker: + needs: [wait-index] + name: Termux userland (termux-docker aarch64) + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-24.04-arm + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - name: "Default: mcpp new → run" + run: | + V="${{ needs.wait-index.outputs.version }}" + bash .github/tools/termux_fresh_install.sh docker \ + "https://github.com/mcpp-community/mcpp/releases/download/v$V/mcpp-$V-linux-aarch64.tar.gz" + + termux-emulator: + needs: [wait-index] + name: Termux app sandbox (Android 14 emulator, x86_64) + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Download Termux and mcpp + run: | + V="${{ needs.wait-index.outputs.version }}" + bash .github/tools/termux_fresh_install.sh emulator-download \ + "https://github.com/mcpp-community/mcpp/releases/download/v$V/mcpp-$V-linux-x86_64.tar.gz" + - name: "Default: mcpp new → run, in the app sandbox" + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + arch: x86_64 + target: google_apis + disk-size: 14G + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: bash .github/tools/termux_fresh_install.sh emulator-run + macos-fresh: needs: [wait-index] name: macOS fresh install (${{ matrix.image }}) diff --git a/build.log b/build.log new file mode 100644 index 000000000..b763ce83d --- /dev/null +++ b/build.log @@ -0,0 +1 @@ +/bin/bash: line 239: : command not found diff --git a/docs/07-workspace.md b/docs/07-workspace.md index 5146bbddb..e1f26925e 100644 --- a/docs/07-workspace.md +++ b/docs/07-workspace.md @@ -142,47 +142,107 @@ its own `[package]` resolves its own entries the same way. ## 4. Inheriting Toolchain and Build Configuration -The workspace root's `[toolchain]` and `[target.]` settings are automatically inherited by all members. A member can override them in its own project file. +A table under `workspace.` speaks to every member; a table outside it speaks +about the package of the manifest that holds it (mcpp 2026.10.10.1+). To share +a table with every member, write it with the `workspace.` prefix: -Configuration precedence (highest to lowest): - -1. Command-line arguments (`--target`, `--static`) -2. Declarations in the member `mcpp.toml` -3. Declarations in the workspace-root `mcpp.toml` -4. Global configuration (`~/.mcpp/config.toml`) -5. Built-in defaults +| A package's own table | Shared with every member | +|---|---| +| `[package]` (metadata, `standard`) | `[workspace.package]` | +| `[build]` | `[workspace.build]` | +| `[dependencies]` entries | `[workspace.dependencies]` + `x.workspace = true` (§3) | +| `[toolchain]` | `[workspace.toolchain]` | +| `[indices]` | `[workspace.indices]` | +| `[profile.]` | `[workspace.profile.]` | +| `[target.]` scalars, `.build`, `.abi`, `.runtime`, `.xlings.workspace` | `[workspace.target.]` and the same subtables | +| `[xlings.workspace]` | `[workspace.xlings.workspace]` | + +A `[workspace.X]` table has the keys, subtables and selectors of `X`. Keys +that describe one package are not shared and are refused there: `[targets]`, +`[features]`, `[resources]`, `[test]`, a selector's `.targets` and +`.dependencies`, `requires_abi`, `allow_host_libs`, and the `sources` and +per-glob `flags` of a `.build` table. An unknown table under `[workspace]` is +refused. ```toml # workspace root -[toolchain] +[workspace] +members = ["libs/core", "apps/server"] + +[workspace.package] +mcpp = ">=2026.10.10.1" + +[workspace.toolchain] default = "gcc@16.1.0" -[target.x86_64-linux-musl] +[workspace.target.x86_64-linux-musl] toolchain = "gcc@16.1.0" linkage = "static" + +[workspace.target.'cfg(os = "windows")'.build] +dialect_cxxflags = ["-DARCH_COMPAT=1"] ``` ```toml -# a member overrides the toolchain +# a member overrides the toolchain for itself [toolchain] default = "llvm@23.1.3" ``` -`[toolchain]`, `[target.]` and `[indices]` choose the compiler, the -target rows and the indices for a whole graph, so a member takes them from the -workspace root only where it is the root of a build: built from the workspace, -with `-p`, or as a host tool of another package (mcpp 2026.9.27.1+ for the -last). A member reached as a dependency takes them from that build's root. +Configuration precedence (highest to lowest): -A build without `--target` targets the host, and `[target.]` -applies to it as `--target ` would (mcpp 2026.9.27.1+). +1. Command-line arguments (`--target`, `--toolchain`, `--profile`, `--static`) +2. Declarations in the member `mcpp.toml` +3. The workspace's `[workspace.X]` tables +4. Global configuration (`~/.mcpp/config.toml`) +5. Built-in defaults -The root's `[xlings.workspace]` entries, including its -`[target..xlings.workspace]` rows, are inherited implicitly as well -(mcpp 2026.9.27.1+): a payload describes the environment a build runs in, like -`[toolchain]`, so no opt-in is needed. A member's own declaration of the same -package wins. `[feature-xlings.]` entries are not inherited, because a -feature belongs to the package that declares it. +`--toolchain` that differs from a toolchain the manifest declares prints a +warning naming both and the file and key that declared the replaced one; it +does not fail under `--strict`. + +`[workspace.toolchain]`, `[workspace.indices]`, `[workspace.profile.]` +and the scalar rows of `[workspace.target.]` choose the compiler, the +indices, the profile and the target rows for a whole graph, so a member takes +them where it is the root of a build: built from the workspace, with `-p`, or as +a host tool of another package. A member reached as a dependency takes them +from that build's root. The rows of `[workspace.target.]` -- +`.build`, `.abi`, `.runtime`, `.xlings.workspace` -- and `[workspace.xlings]` +reach a member in every position (§4.1). + +A conditional row counts as it evaluates for the target being built: a +`dialect_cxxflags` or `.abi` row for another target does not separate a member +from the others (§5.4), and one that holds is applied when the member is built +under the workspace's root. + +A build without `--target` targets the host, and `[target.]` +applies to it as `--target ` would. + +`[feature-xlings.]` entries are not shared, because a feature belongs to +the package that declares it. + +**A workspace that uses `[workspace.X]` declares the engine floor.** An mcpp +older than 2026.10.10.1 ignores these tables and builds the members without +them. `[workspace.package] mcpp = ">=2026.10.10.1"` makes an older mcpp refuse +instead; without it the build prints a note. + +**The root's own tables, read by position.** On a workspace root, a +`[toolchain]`, `[indices]`, `[profile.]`, a `[target.]` scalar row +or an `[xlings]` / `[target..xlings]` entry that has no `[workspace.X]` +spelling still reaches the members as it did before 2026.10.10.1: a member's own +`[toolchain]` replaces the root's whole, a member's row or profile of the same +name replaces the root's whole, and `[indices]` reaches only a member that +declares none. Each use prints a `manifest/workspace-position` warning with the +`[workspace.X]` spelling; on a root with `[package]` the tables are the root +package's own and the warning appears only for a member that received a value +from them. This reading is removed in mcpp 1.0.0. On a root without +`[package]`, writing a table both ways is refused. + +**A root without `[package]` holds no package.** `[build]`, `[targets]`, +`[dependencies]`, `[features]`, `[resources]`, `[test]`, `[hooks]`, `[runtime]` +and a selector's `.build`, `.abi`, `.runtime`, `.targets` and `.dependencies` +written there act on nothing; each prints a warning naming the shared spelling +where there is one, and `--strict` refuses. ### 4.1 `[workspace.package]` and `[workspace.build]` @@ -277,31 +337,22 @@ with the package whose artifact it is. Any other unknown key in a key that is silently dropped from a table whose whole purpose is propagation produces a workspace that looks configured and is not. -**There is no `[workspace.target.]`.** A plain `[target.]` block -in the workspace root is already inherited by every member, per triple, with the -member winning. A second spelling for the same capability would be surface with -no function. - -**Profiles are inherited by name (2026.10.5.2+).** A `[profile.]` in the -workspace root reaches every member that does not declare a profile of that -name; a member's own table replaces the workspace's whole, as -`[target.]` does. A profile is one value per graph, so members that -share it are planned and compiled together. Before 2026.10.5.2 a virtual root's -profiles reached no member and were ignored without a diagnostic, and a rooted -workspace's applied only when its own package was the first one selected. +**Named tables merge key by key.** A member's `[profile.release]` and +`[workspace.profile.release]` combine: a key the member wrote is the member's, a +list has the workspace's entries first. `[target.]` rows combine the +same way. A profile is one value per graph, so members that share it are +planned and compiled together. **The root package is a member.** In a workspace whose root carries -`[package]`, the root package receives `[workspace.package]` and -`[workspace.build]` once, as every other member does (2026.10.5.2+), so its -commands are the same in every selection. The root manifest holds three kinds -of keys: +`[package]`, the root package receives `[workspace.package]`, +`[workspace.build]` and every `[workspace.X]` table once, as every other member +does, so its commands are the same in every selection. The root manifest holds +two kinds of tables: -| Keys | Owner | Effect on the root package | Effect on other members | +| Tables | Owner | Effect on the root package | Effect on other members | |---|---|---|---| -| `[workspace]`, `[workspace.dependencies]` | the workspace | through `x.workspace = true` | through `x.workspace = true` | -| `[workspace.package]`, `[workspace.build]` | the workspace | inherited | inherited | -| `[toolchain]`, `[target.]`, `[indices]`, `[profile.]` | the root position | its own | inherited where the member is the root of a build; the member's own declaration wins | -| `[package]`, `[build]`, `[dependencies]`, `[targets]`, `[features]`, `[resources]`, `[test]` | the root package | its own | none | +| `[workspace]` and every `[workspace.X]` | the workspace | received, its own declarations first | received; `[workspace.dependencies]` through `x.workspace = true` | +| every other table (`[package]`, `[build]`, `[toolchain]`, `[target.]`, `[profile.]`, ...) | the root package | its own | none (the position reading above excepted, until 1.0.0) | The root package's `[build] ldflags` are its own as well: they reach its own images, and neither the members' images nor a dependency's shared library. @@ -504,9 +555,12 @@ member that several members use is compiled once. toolchain request, target, C++ standard, `dialect_cxxflags`, C++ runtime, `linkage`, profile, indices and the other `[build]` values that apply to a whole graph. Members that differ in one of them are built in separate - graphs, at the same time, sharing the command's jobs. A relative path a - member writes, such as its own `[indices]` path, is read from the member's - directory. + graphs, at the same time, sharing the command's jobs. A conditional row + (`[target..build] dialect_cxxflags`, `[target..abi]`, + a `[target.]` scalar) counts as it evaluates for the target + being built: `--target`, else the member's `[build] target`, else the host + (2026.10.10.1+). A relative path a member writes, such as its own + `[indices]` path, is read from the member's directory. - **Selection.** `--workspace`, and a virtual root without `-p`, select every member. `-p X`, and a command run in X's directory, plan X and what X reaches; `-p X -p Y` plans both together, as one selection (§5.3). The diff --git a/docs/zh/07-workspace.md b/docs/zh/07-workspace.md index 15c02998a..68253fbc9 100644 --- a/docs/zh/07-workspace.md +++ b/docs/zh/07-workspace.md @@ -143,45 +143,93 @@ mbedtls = "4.0.0" # override; does not use the workspace version ## 4. 工具链与构建配置的继承 -工作空间根的 `[toolchain]` 与 `[target.]` 配置由全体成员自动继承。成员 -可以在自己的工程文件中覆盖。 +`workspace.` 之下的表对全体成员说话;`workspace.` 之外的表只说它所在清单的那个包 +(mcpp 2026.10.10.1+)。要与全体成员共享一张表,就加上 `workspace.` 前缀: -配置优先级(从高到低): - -1. 命令行参数(`--target`、`--static`) -2. 成员 `mcpp.toml` 中的声明 -3. 工作空间根 `mcpp.toml` 中的声明 -4. 全局配置(`~/.mcpp/config.toml`) -5. 内置默认值 +| 包自己的表 | 与全体成员共享 | +|---|---| +| `[package]`(元数据、`standard`) | `[workspace.package]` | +| `[build]` | `[workspace.build]` | +| `[dependencies]` 的条目 | `[workspace.dependencies]` + `x.workspace = true`(§3) | +| `[toolchain]` | `[workspace.toolchain]` | +| `[indices]` | `[workspace.indices]` | +| `[profile.]` | `[workspace.profile.]` | +| `[target.]` 的标量行、`.build`、`.abi`、`.runtime`、`.xlings.workspace` | `[workspace.target.]` 及同名子表 | +| `[xlings.workspace]` | `[workspace.xlings.workspace]` | + +`[workspace.X]` 的键、子表与选择器与 `X` 相同。描述某一个包的键不共享,写在那里会被 +拒绝:`[targets]`、`[features]`、`[resources]`、`[test]`、选择器的 `.targets` 与 +`.dependencies`、`requires_abi`、`allow_host_libs`,以及 `.build` 表的 `sources` 与按 +glob 的 `flags`。`[workspace]` 下的未知表会被拒绝。 ```toml # workspace root -[toolchain] +[workspace] +members = ["libs/core", "apps/server"] + +[workspace.package] +mcpp = ">=2026.10.10.1" + +[workspace.toolchain] default = "gcc@16.1.0" -[target.x86_64-linux-musl] +[workspace.target.x86_64-linux-musl] toolchain = "gcc@16.1.0" linkage = "static" + +[workspace.target.'cfg(os = "windows")'.build] +dialect_cxxflags = ["-DARCH_COMPAT=1"] ``` ```toml -# a member overrides the toolchain +# a member overrides the toolchain for itself [toolchain] default = "llvm@23.1.3" ``` -`[toolchain]`、`[target.]` 与 `[indices]` 为整个依赖图选择编译器、目标行与索引, -因此成员只在作为一次构建的根时从工作空间根继承它们:从工作空间构建、以 `-p` 选中,或作为 -另一个包的宿主工具构建(最后一种自 mcpp 2026.9.27.1)。作为依赖到达的成员从该次构建的根 -取得它们。 +配置优先级(从高到低): + +1. 命令行参数(`--target`、`--toolchain`、`--profile`、`--static`) +2. 成员 `mcpp.toml` 中的声明 +3. 工作空间的 `[workspace.X]` 表 +4. 全局配置(`~/.mcpp/config.toml`) +5. 内置默认值 + +`--toolchain` 与清单声明的工具链不同时,打印一条警告,写出两者以及声明被替换者的文件与键; +`--strict` 不因此失败。 + +`[workspace.toolchain]`、`[workspace.indices]`、`[workspace.profile.]` 与 +`[workspace.target.]` 的标量行为整个依赖图选择编译器、索引、配置档与目标行,因此 +成员在作为一次构建的根时接收它们:从工作空间构建、以 `-p` 选中,或作为另一个包的宿主工具 +构建。作为依赖到达的成员从该次构建的根取得它们。`[workspace.target.]` 的各行 +(`.build`、`.abi`、`.runtime`、`.xlings.workspace`)与 `[workspace.xlings]` 在每种位置 +都到达成员(§4.1)。 + +条件行按所构建的目标求值后计入:其他目标上的 `dialect_cxxflags` 或 `.abi` 行不会把成员与 +其他成员分开(§5.4);对当前目标成立的行,在成员于工作空间的根下构建时生效。 不带 `--target` 的构建以宿主为目标,`[target.<宿主三元组>]` 对它生效,与 -`--target <宿主三元组>` 相同(mcpp 2026.9.27.1+)。 +`--target <宿主三元组>` 相同。 + +`[feature-xlings.]` 不共享,因为特性属于声明它的包。 -根的 `[xlings.workspace]` 条目,包括 `[target..xlings.workspace]` 行,同样隐式 -继承(mcpp 2026.9.27.1+):载荷描述的是构建运行的环境,与 `[toolchain]` 相同,不需要 -显式声明。成员自己声明的同一个包优先。`[feature-xlings.]` 不被继承,因为特性属于声明 -它的包。 +**使用 `[workspace.X]` 的工作空间应声明引擎下限。** 早于 2026.10.10.1 的 mcpp 忽略这些表, +不带它们构建成员。`[workspace.package] mcpp = ">=2026.10.10.1"` 让旧版 mcpp 拒绝构建; +不写时构建打印一条 note。 + +**根自己的表按位置读取。** 工作空间根上没有对应 `[workspace.X]` 写法的 `[toolchain]`、 +`[indices]`、`[profile.]`、`[target.]` 标量行,以及 `[xlings]` / +`[target..xlings]` 条目,仍按 2026.10.10.1 之前的方式到达成员:成员自己的 +`[toolchain]` 整体替换根的,成员同名的行或配置档整体替换根的,`[indices]` 只到达未声明 +任何索引的成员。每次使用打印一条 `manifest/workspace-position` 警告,并给出 +`[workspace.X]` 写法;在带 `[package]` 的根上,这些表首先属于根包,警告只对确实经由它们 +得到值的成员给出。这种读法在 mcpp 1.0.0 移除。在不带 `[package]` 的根上,同一张表两种写法 +并存会被拒绝。 + +**不带 `[package]` 的根上没有包。** 写在那里的 `[build]`、`[targets]`、`[dependencies]`、 +`[features]`、`[resources]`、`[test]`、`[hooks]`、`[runtime]`,以及选择器的 `.build`、 +`.abi`、`.runtime`、`.targets`、`.dependencies` 不作用于任何包;每张表打印一条警告,有共享 +写法时一并给出,`--strict` 下拒绝。 ### 4.1 `[workspace.package]` 与 `[workspace.build]` @@ -267,26 +315,18 @@ workspace 键。 为唯一目的的表,如果能静默丢弃某个键,产出的就是一个看起来配置好了、实际上没有 的工作空间。 -**没有 `[workspace.target.]`。** 工作空间根里一个普通的 `[target.]` -块本来就按 triple 逐项被全体成员继承(成员优先)。为同一能力再造一种拼法,只会 -增加接口面而不增加功能。 - -**profile 按名字继承(2026.10.5.2+)。** 工作空间根里的 `[profile.]` 到达每个 -没有声明同名 profile 的成员;成员自己的同名表整体替换工作空间的,与 -`[target.]` 相同。profile 是每张构建图一个的值,因此共享它的成员一起规划、 -一起编译。2026.10.5.2 之前,虚拟根的 profile 不到达任何成员,并且被无声忽略;带根包 -的工作空间的 profile 只在根包是第一个被选中的包时生效。 +**命名表逐键合并。** 成员的 `[profile.release]` 与 `[workspace.profile.release]` 合在 +一起:成员写下的键属于成员,列表以工作空间的条目在前。`[target.]` 行按同样的方式 +合并。配置档是每张构建图一个的值,因此共享它的成员一起规划、一起编译。 **根包是一个成员。** 根上带 `[package]` 的工作空间中,根包与其它成员一样恰好一次地 -继承 `[workspace.package]` 与 `[workspace.build]`(2026.10.5.2+),因此它的命令在 -每种选择下相同。根清单包含三类键: +接收 `[workspace.package]`、`[workspace.build]` 与每一张 `[workspace.X]` 表,因此它的命令 +在每种选择下相同。根清单包含两类表: -| 键 | 归属 | 对根包的作用 | 对其它成员的作用 | +| 表 | 归属 | 对根包的作用 | 对其它成员的作用 | |---|---|---|---| -| `[workspace]`、`[workspace.dependencies]` | 工作空间 | 通过 `x.workspace = true` | 通过 `x.workspace = true` | -| `[workspace.package]`、`[workspace.build]` | 工作空间 | 继承 | 继承 | -| `[toolchain]`、`[target.]`、`[indices]`、`[profile.]` | 根位置 | 本身的值 | 成员作为一次构建的根时继承;成员自己的声明优先 | -| `[package]`、`[build]`、`[dependencies]`、`[targets]`、`[features]`、`[resources]`、`[test]` | 根包 | 本身的值 | 无 | +| `[workspace]` 与每一张 `[workspace.X]` | 工作空间 | 接收,根包自己的声明优先 | 接收;`[workspace.dependencies]` 通过 `x.workspace = true` | +| 其余每一张表(`[package]`、`[build]`、`[toolchain]`、`[target.]`、`[profile.]`……) | 根包 | 本身的值 | 无(上文的按位置读取除外,直到 1.0.0) | 根包的 `[build] ldflags` 同样属于它自己:它们到达根包自己的镜像,不到达成员的镜像, 也不到达依赖的共享库。 @@ -457,8 +497,10 @@ mcpp test --workspace --workspace-timeout 1800 # whole fan-out (default 0 = no - **配置。** 工具链请求、目标、C++ 标准、`dialect_cxxflags`、C++ 运行时、`linkage`、配置档、 索引,以及其他作用于整张图的 `[build]` 值都相同的成员在同一张图中构建。其中任一项不同的成员 - 在各自的图中构建,这些图同时进行,共享命令的并行任务数。成员写下的相对路径(例如它自己的 - `[indices]` 路径)按该成员的目录解析。 + 在各自的图中构建,这些图同时进行,共享命令的并行任务数。条件行(`[target..build] + dialect_cxxflags`、`[target..abi]`、`[target.]` 标量)按所构建的目标求值后 + 计入:`--target`,否则成员的 `[build] target`,否则宿主(2026.10.10.1+)。成员写下的相对路径 + (例如它自己的 `[indices]` 路径)按该成员的目录解析。 - **选择。** `--workspace`,以及虚拟工作空间根下不带 `-p` 的命令,选中全体成员;`-p X` 与在 X 的目录中执行的命令规划 X 及其所依赖的一切;`-p X -p Y` 把两者放在一起规划,作为一个 选择(§5.3)。这些选择共用构建目录,同一个编译单元在包含它的每个选择中都由同一条命令编译: diff --git a/release.log b/release.log new file mode 100644 index 000000000..6e3855ba1 --- /dev/null +++ b/release.log @@ -0,0 +1 @@ +/bin/bash: line 253: : command not found From 0c212bce70d90ca62a834f2c032830a57ed3e505 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:14:50 +0900 Subject: [PATCH 15/32] docs: SPEC-004 1.14 and SPEC-007 0.8; 04, 20, 30 and their zh counterparts --- docs/04-mcpp-toml.md | 26 +++++++++++- docs/20-toolchains.md | 45 ++++++++++++++++++-- docs/30-build-mcpp.md | 34 ++++++++++------ docs/specs/README.md | 4 +- docs/specs/build-plugins.md | 25 +++++++++--- docs/specs/manifest-semantics.md | 70 +++++++++++++++++++++++++------- docs/zh/04-mcpp-toml.md | 23 ++++++++++- docs/zh/20-toolchains.md | 33 +++++++++++++-- docs/zh/30-build-mcpp.md | 23 +++++++---- 9 files changed, 231 insertions(+), 52 deletions(-) diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index b7c511921..a60abd1c5 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -168,7 +168,8 @@ dialect_cxxflags = ["-D_HAS_EXCEPTIONS=0"] Unlike an ordinary build input, `dialect_cxxflags` is graph-wide (SPEC-004 §9 item 10), so only the root of the build contributes it: the command's own package, or the member `-p` selects. Entries are appended in this order — `[workspace.build]`, the root's own `[build]`, then each -matching `[target..build]` in order of selector specificity (a triple after an OS, +matching `[workspace.target..build]` row, then each matching +`[target..build]` in order of selector specificity (a triple after an OS, an OS after a family; SPEC-004 §3.1.1) — and the resolved list is what reaches the std BMI prebuild, the module scan and every translation unit, on the root and on every dependency alike. A dependency's own `dialect_cxxflags`, conditional or not, reaches no command: @@ -1871,6 +1872,29 @@ that runs them. The test programs receive the application manifest and nothing else of `[resources]`. +### 2.18 `[workspace.*]` — Tables a Workspace Shares (2026.10.10.1+) + +On a workspace root, a table under `workspace.` is given to every member, and +has the keys, subtables and selectors of the table it mirrors: +`[workspace.package]`, `[workspace.build]`, `[workspace.dependencies]`, +`[workspace.toolchain]`, `[workspace.indices]`, `[workspace.profile.]`, +`[workspace.target.]` with `.build`, `.abi`, `.runtime` and +`.xlings.workspace`, and `[workspace.xlings]`. A table without the prefix +describes the root's own package. The merge rules, what is refused and the +reading of the root's own tables by position are in +[07 — Workspaces](07-workspace.md) §4. + +```toml +[workspace] +members = ["core", "app"] + +[workspace.package] +mcpp = ">=2026.10.10.1" + +[workspace.target.'cfg(os = "windows")'.build] +dialect_cxxflags = ["-DARCH_COMPAT=1"] +``` + ## 3. Worked Examples Four of these are runnable projects rather than snippets, and the project is diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index 86a268722..cde7604b6 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -30,7 +30,24 @@ host-aware: - macOS uses `llvm@23.1.3`. - Windows with a usable MSVC installation uses `llvm@23.1.3` for the MSVC ABI. Without usable MSVC, it uses `gcc@16.1.0` with target - `x86_64-windows-gnu` (MinGW-w64, static by default). + `x86_64-windows-gnu` (MinGW-w64, static by default), says which half of the + MSVC ABI is missing — Visual Studio's C++ tools or the Windows SDK — and where + it looked, and records the pair as the machine's default. Once both halves + are present, `mcpp toolchain default llvm@23.1.3` moves the machine to the + MSVC ABI. +- Android (Termux) uses the default of GNU/Linux on its architecture: on + aarch64, `llvm@23.1.3` with the managed glibc. Android 10 or later is + required (the shell is reached as `/bin/sh`), with xlings 2026.10.10.2 or + later, which unpacks payloads in the app sandbox, where hard links cannot be + created. + +A toolchain that was specified — in `mcpp.toml`, with `--toolchain`, or as the +machine's default — is never replaced by MinGW-w64. When it needs the MSVC ABI +and Visual Studio's C++ tools or the Windows SDK is missing, the build is +refused at planning with the missing half, the places searched and the +options: install the missing part or set `WindowsSdkDir`, +`mcpp toolchain install msvc` (a managed MSVC with its own SDK), or choose +MinGW-w64 with `--toolchain` or in `mcpp.toml`. Fully static musl output remains one flag away on a Linux host: `mcpp build --target x86_64-linux-musl`. @@ -510,6 +527,18 @@ mcpp takes the first complete toolset in this order (2026.9.24.1+): 5. without `vswhere.exe`, the standard `Program Files\Microsoft Visual Studio\\` paths. +`vswhere.exe` is looked for under `%ProgramFiles(x86)%`, and the standard +paths under `%ProgramFiles%` and `%ProgramFiles(x86)%`, then under `C:` +(2026.10.10.1+). + +The Windows SDK is taken from, in order: `WindowsSdkDir` (with +`WindowsSdkVersion`), the SDK installed beside a managed toolset, the root the +SDK installer recorded (`HKLM\SOFTWARE\Microsoft\Windows Kits\Installed +Roots`, `KitsRoot10`, in either registry view), and `Windows Kits\10` under +`%ProgramFiles(x86)%` and `%ProgramFiles%`, then under `C:`. An SDK installed +on another drive is therefore found without a developer command prompt +(2026.10.10.1+). + An instance's default toolset is the one its `VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt` names. A toolset is complete when it has `include\`, `lib\x64\` and, for cl.exe builds, `cl.exe`. @@ -1341,8 +1370,18 @@ The options, in the order the note gives them: The newer compiler treats coroutines on this ABI as unsupported; code that uses them on this target does so at its own risk. -Defining `__cpp_impl_coroutine` in a project is not a workaround. It switches -on a feature the compiler has declared unsupported for this ABI. +Defining `__cpp_impl_coroutine` in a project is not a workaround and is not +recommended. It switches on a feature the compiler has declared unsupported for +this ABI, and mcpp gives no guarantee about the result. A project that defines +it anyway writes it in `dialect_cxxflags`, which reaches the std module's +precompile, the scan and every C++ unit ([04 — mcpp.toml](04-mcpp-toml.md)): + +```toml +[target.i686-windows-msvc.build] +dialect_cxxflags = ["-D__cpp_impl_coroutine=201902L"] +``` + +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. diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index dd1caffeb..4dc39e121 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -663,10 +663,15 @@ package's compile edges to that package's action outputs. > that would record it does not exist until a compile has already succeeded. > Before this, an action whose outputs were all headers had a node in > `build.ninja` that nothing could reach — not `default`, not the goal set, no -> consuming edge — so it never ran, and what the compiler read was the empty -> placeholder mcpp writes for a declared output. The ordering is **per -> package**, because `include_dir` colours only the declaring package's own -> translation units. +> consuming edge — so it never ran. The ordering is **per package**, because +> `include_dir` colours only the declaring package's own translation units. + +mcpp writes nothing at an action's outputs: the action is their only writer +(mcpp 2026.10.10.1+). A generated translation unit enters the plan as declared +— its path and extension, and the modules its action says it provides and +imports — and is read only by the compile that consumes it. After +`--configure-only` a generated file does not exist yet, and +`compile_commands.json` names it ahead of its first build. **An action whose command discovers its own dependencies declares a depfile** (mcpp 2026.9.7.1+). `input()` fixes the edge's inputs when `build.mcpp` runs, @@ -761,10 +766,11 @@ For a generated **module interface**, declare its interface too: a.output(gen.c_str()).provides("my.generated").imports("std").submit(); ``` -mcpp seeds a placeholder carrying exactly that declaration so the prepare-time -scan agrees with what the generator will emit — the same assertion-plus- -verification trade `[modules].scan_overrides` makes, and the compiler's own -P1689 output checks it at build time. +The plan takes the declaration as the unit's module interface, and the +compiler's own P1689 output checks it at build time — the trade +`[modules].scan_overrides` makes. A generated module interface without +`.provides(...)` has no BMI in the build, so an import of it fails; the build +reports such an action as degraded (`build-program/provides`). #### Environment and working directory: `env` / `cwd` (protocol 13) @@ -1335,7 +1341,7 @@ The running program receives the build context as `MCPP_*` variables | `MCPP_PACK_STRIP` *(2026.9.16.1+)* | `mcpp::pack_strip()` | `1` when the `mcpp pack` pass this program is part of strips, `0` under `--no-strip`; empty for every ordinary build. A member that stages libraries of its own follows it, so one switch governs every file in the package | | `MCPP_PACK_DEBUG_SYMBOLS_DIR` *(2026.9.16.1+)* | `mcpp::pack_debug_symbols_dir()` | Where `--debug-symbols` sends the separated `*.debug` files, absolute; empty when they are discarded or the build is not packing | | `MCPP_DEVICE_SOURCES` *(2026.9.5.2+)* | `mcpp::device_sources()` | the device-kind sources (`.cu`, `.hip`, …) the package's effective `sources` match, package-root-relative, one per line; empty when there are none. The engine compiles none of them — the rule package this program imports turns each into an `mcpp::action`. Already narrowed: a `{ glob, accel }` entry the build does not cover contributes nothing, so `--no-accel` yields an empty list | -| `MCPP_OUT_DIR` | `mcpp::out_dir()` | a writable scratch/output dir owned by mcpp | +| `MCPP_OUT_DIR` | `mcpp::out_dir()` | a writable output directory owned by mcpp, one per configuration (target, profile, toolchain, accelerators, the package's features) and package, below the build's root: `target/.build-mcpp/out//` (2026.10.10.1+). A script outside the build reads it from `resolution.json`, `graph.packages[].outDir` | | `MCPP_MANIFEST_DIR` | `mcpp::manifest_dir()` | the package root (= CWD) | | `MCPP_FEATURE_` | `mcpp::has_feature("name")` | set to `1` per active feature (same `` sanitization as the `MCPP_FEATURE_` compile macro) | | `MCPP_FEATURES` | — | comma-separated active feature list | @@ -1535,10 +1541,12 @@ Cargo `build.rs` model — building a package means trusting its build program), after its features are resolved and before the source scan. Scope follows Cargo: `cxxflag`/`cflag`/`cfg` directives color **only that package's own TUs**; `link-lib`/`link-search` reach the final link. Its artifacts (binary, -cache, `MCPP_OUT_DIR`) live in the **consuming project's** -`target/.build-mcpp/deps/@/` — a registry package root is shared -across projects (and may be read-only), so it is never written to; relative -`generated=` paths resolve against `MCPP_OUT_DIR`, not the package root. +cache) live in the **consuming project's** +`target/.build-mcpp/deps/@/` and its `MCPP_OUT_DIR` in that project's +`target/.build-mcpp/out//` — a registry package root is +shared across projects (and may be read-only), so it is never written to; +relative `generated=` paths resolve against `MCPP_OUT_DIR`, not the package +root. ### A library that is also built standalone: emit an absolute path diff --git a/docs/specs/README.md b/docs/specs/README.md index 7213c8c1d..ca2a7af5e 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -33,10 +33,10 @@ | [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-003](exit-codes.md) | 退出码契约(分类、语义、稳定性承诺) | 评审中 v1.0 | 2026-09-01 | mcpp >= 2026.9.1.1 | -| [SPEC-004](manifest-semantics.md) | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | 草案 v1.13 | 2026-10-06 | 条件化形状 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 | +| [SPEC-004](manifest-semantics.md) | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | 草案 v1.14 | 2026-10-10 | 条件化形状 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 | | [SPEC-005](build-database.md) | 构建数据库:`mcpp emit build-database` 的内容、取值规则与不写工程目录的保证 | 评审中 v1.6 | 2026-09-29 | 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 | | [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-007](build-plugins.md) | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | 草案 v0.6 | 2026-09-28 | 逐条标注;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 | +| [SPEC-007](build-plugins.md) | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | 草案 v0.8 | 2026-10-10 | 逐条标注;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 | | [SPEC-008](library-interface.md) | 库的接口:公开模块、发布闭包与两种形态的一致 | 草案 v0.1 | 2026-09-28 | 第一阶段(只警告)mcpp >= 2026.9.28.3 | | [SPEC-009](toolchain-maintenance.md) | 工具链的支持与维护:版本线、默认值、来源、移动与退役 | 草案 v0.6 | 2026-10-09 | 逐条标注;本版只有规范,多数条款未实现;§6.4 mcpp >= 2026.10.8.1 | diff --git a/docs/specs/build-plugins.md b/docs/specs/build-plugins.md index 417e4421d..7be5911bc 100644 --- a/docs/specs/build-plugins.md +++ b/docs/specs/build-plugins.md @@ -4,12 +4,12 @@ |---|---| | 规范编号 | SPEC-007 | | 标题 | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | -| 状态 | 草案 v0.7 | -| 版本 | 0.7 | -| 最后修改 | 2026-10-01 | -| 对应实现 | 逐条标注。未注明版本的「已实现」条款对应 mcpp >= 2026.9.26.1;注明 mcpp#702 的条款对应 mcpp >= 2026.9.26.2;注明 mcpp#707、#708、#709、#711 的条款对应 mcpp >= 2026.9.27.1;注明 mcpp#723 的条款对应 mcpp >= 2026.9.28.1;注明 mcpp 2026.9.28.2 的条款对应该版本;§9 与注明 mcpp#734 的条款对应 mcpp >= 2026.9.28.3 | -| 相关设计文档 | `.agents/docs/2026-09-26-compile-database-and-issue-699-design.md`(§5)、`.agents/docs/2026-09-28-ecosystem-design-and-optimisation-plan.md`(WS1、WS3)、`.agents/docs/2026-09-28-build-cost-foreign-toolsets-and-library-surface-design.md`(§3、§4) | -| 相关 issue | mcpp#699、mcpp#701、mcpp#702、mcpp#703、mcpp#707、mcpp#708、mcpp#709、mcpp#711、mcpp#734、mcpp#755 | +| 状态 | 草案 v0.8 | +| 版本 | 0.8 | +| 最后修改 | 2026-10-10 | +| 对应实现 | 逐条标注。未注明版本的「已实现」条款对应 mcpp >= 2026.9.26.1;注明 mcpp#702 的条款对应 mcpp >= 2026.9.26.2;注明 mcpp#707、#708、#709、#711 的条款对应 mcpp >= 2026.9.27.1;注明 mcpp#723 的条款对应 mcpp >= 2026.9.28.1;注明 mcpp 2026.9.28.2 的条款对应该版本;§9 与注明 mcpp#734 的条款对应 mcpp >= 2026.9.28.3;注明 mcpp#778 的条款对应 mcpp >= 2026.10.10.1 | +| 相关设计文档 | `.agents/docs/2026-09-26-compile-database-and-issue-699-design.md`(§5)、`.agents/docs/2026-09-28-ecosystem-design-and-optimisation-plan.md`(WS1、WS3)、`.agents/docs/2026-09-28-build-cost-foreign-toolsets-and-library-surface-design.md`(§3、§4)、`.agents/docs/2026-10-10-one-source-one-meaning-design.md`(§4) | +| 相关 issue | mcpp#699、mcpp#701、mcpp#702、mcpp#703、mcpp#707、mcpp#708、mcpp#709、mcpp#711、mcpp#734、mcpp#755、mcpp#778 | | 使用文档 | [docs/30 - build.mcpp](../30-build-mcpp.md)、[docs/31 - 编写规则包](../31-authoring-a-rule-package.md) | 本规范规定构建插件对引擎和对消费方承担的义务,以及引擎为此提供的机制。docs/31 说明怎样编写 @@ -133,6 +133,18 @@ 相对路径相对于 `cwd`。变量的值属于这条边的命令行,值改变时该 action 重新运行。两者都未 声明的 action,其命令行与协议 12 逐字节相同。(**已实现**,mcpp#708) +- **R3.9** action 的输出只有一个写入者,即 action 本身:引擎**禁止**在构建前或规划时写入 + action 的任何输出(包括为扫描而写的占位文件)。`source` action 生成的翻译单元以**声明单元** + 进入规划:它的路径与语言取自输出路径与扩展名,它提供与导入的模块取自 action 的 + `.provides()` 与 `.imports()`,引擎不读文件;声明了 `provides` 的单元在构建时由编译器的 + P1689 扫描核对。生成模块接口单元的 action **必须**声明 `.provides()`;未声明时引擎给出 + `degraded`(`build-program/provides`)。(**已实现**,mcpp#778) +- **R3.10** 构建程序的输出目录(`mcpp::out_dir()`、`MCPP_OUT_DIR`)按配置分开:它位于计划根 + 目录下,每个配置一个目录(目标三元组、profile、工具链、加速器与该包启用的 features 的摘要), + 每个包一个子目录。同一张图之外的读者(测试脚本)**必须**从 `resolution.json` 的 + `graph.packages[].outDir` 取得它,**禁止**拼写其路径。构建程序的二进制与缓存不按配置分开。 + `${mcpp.out_dir}`(构建目录)不变。(**已实现**,mcpp#778) + ## 4. 运行时:程序依赖的共享库的查找 - **R4.1** 一个目录中的共享库由施工产生(`prepare`)或文件名不定时,插件**必须**用 @@ -286,4 +298,5 @@ | 0.5 | 2026-09-28 | 随 mcpp 2026.9.28.2:R4.3 的部署清单是运行时放置解析器的答案(SPEC-006 §3.7.1),MSVC C++ 运行时按集合规则决定,`prepare` 填充的目录中的运行时名字由同一解析器决定;新增 R4.5,构建边在成功时的通告,构建后报告一次(2026-09-28 设计 WS1、WS3)。 | | 0.7 | 2026-10-01 | 随 mcpp 2026.10.1.3(mcpp#755):R6.2 改为禁止**未经指定**的宿主探测并要求报告回落;新增 R6.5(工具来源的顺序与记录)、R6.6(`provision = "on-request"` 与 `xpkg_request`)、R9.9(构建程序在工具链阶段陈述构建工具链);R9.2 的协议表新增第 15 行。 | | 0.6 | 2026-09-28 | 随 mcpp 2026.9.28.3:新增 §9(mcpp#734),即 `mcpp.core` 与 `mcpp` 的永久等价、接口的稳定性与协议表、构建信息、结构化诊断、批量放置、插件模块的名字、缺失模块指出 feature、包的版本下限;R7.1 的版本写在清单中;变更记录移为 §10。 | +| 0.8 | 2026-10-10 | 随 mcpp 2026.10.10.1(mcpp#778):新增 R3.9(action 的输出只有一个写入者,生成的翻译单元以声明单元进入规划,生成的模块接口须声明 `.provides()`)与 R3.10(构建程序的输出目录按配置分开,`resolution.json` 记录 `outDir`)。 | | 0.2 | 2026-09-26 | 随 mcpp 2026.9.26.2 落地:R1.3 的警告、R2.1 的 `runtime_search_dir`、R2.4、R3.3 的 `prepare`(目录须含文件;链接边等待所有 `prepare`)、R3.5、R3.6、R4.1、R4.3、R5.2、R5.3 标为已实现。 | diff --git a/docs/specs/manifest-semantics.md b/docs/specs/manifest-semantics.md index 5aa3832d3..4c85042df 100644 --- a/docs/specs/manifest-semantics.md +++ b/docs/specs/manifest-semantics.md @@ -5,11 +5,11 @@ | **规范编号** | SPEC-004 | | **标题** | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | | **状态** | **草案(Draft)** | -| **版本** | 1.13 | -| **最后修改** | 2026-10-06 | +| **版本** | 1.14 | +| **最后修改** | 2026-10-10 | | **最低实现版本** | 条件化形状: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-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` | | **相关使用文档** | [docs/04 —— mcpp.toml 字段参考](../04-mcpp-toml.md) | ## 规范用语 @@ -57,6 +57,13 @@ **状态:已实现。** +**每个键有两个互相独立的属性。** 作用域是键自身的属性:包私有(P,作用于本包的单元)、 +使用需求(U,从依赖流向消费者)、配置(C,一次构建中全图共有的值,只从构建的根读取)、 +元数据(M,不进入命令)。来源是表的属性:命令行、本包的表、工作空间的共享表 +(`[workspace.X]`,§9 第 12 条)、引擎默认值。条件(`[target.]`)是作用在值上的 +谓词,既不改变作用域,也不改变来源。键的类别只陈述一次(`mcpp.manifest.key_registry`), +解析器的已知键检查从中取得。 + **库依赖与工具是两个平面,不是一个。** 库有模块与 ABI,参与解析与链接;载荷是可执行 的工具或被编译对着的输入,不参与模块图。二者的解析规则不同(§4),因此**禁止**把工具 写进 `[dependencies]`,也**禁止**把 mcpp 包写进 `[xlings]`。 @@ -536,21 +543,28 @@ mcpp 2026.9.26.2,#703)。** 另一个包的编译单元。使用需求只从依赖流向它的消费者,**禁止**从消费者流入依赖。 一个依赖的编译命令**必须**与构建它的工程无关;依赖缓存键**必须**包含到达该命令的 全部输入。 -7. 工作空间成员的发布形态**必须**自包含:发布的清单写出继承来的值,兄弟成员之间的 - `path` 边以版本边发布,无法以版本表达的 `path` 边**必须**被拒绝发布。 -8. 成员**必须**继承工作空间根的 `[xlings.workspace]` 条目,包括 - `[target..xlings.workspace]` 的条件行(按行继承,合并时由选择器决定)。继承是 - 隐式的,与 `[toolchain]` 相同,因为载荷描述的是构建运行的环境,不是依赖图的边。成员自己 - 声明的同一个包(身份为 `(namespace, name)`)优先。`[feature-xlings.]` 不被继承:特性 - 属于声明它的包。 +7. 工作空间成员的发布形态**必须**自包含:发布的清单写出继承来的值,包括工作空间层 + (第 12 条)按第 12 条的合并规则写进成员自己的表(`[workspace.toolchain]` 写成 + `[toolchain]`,`[workspace.target..build]` 写成 `[target..build]`), + 工作空间写下的相对路径改写为相对于成员目录,离开成员目录的**必须**被拒绝发布;兄弟成员 + 之间的 `path` 边以版本边发布,无法以版本表达的 `path` 边**必须**被拒绝发布。 +8. 成员**必须**接收工作空间的 `[workspace.xlings.workspace]` 条目与 + `[workspace.target..xlings.workspace]` 的条件行(按行继承,合并时由选择器决定), + 在第 1 条所列的每个位置。接收是隐式的,因为载荷描述的是构建运行的环境,不是依赖图的边。 + 成员自己声明的同一个包(身份为 `(namespace, name)`)优先。`[feature-xlings.]` 不共享: + 特性属于声明它的包。 9. 一条 `x.workspace = true` 条目在继承之后仍未解析时,实现**必须**在它进入构建的每个位置 (根包、`-p` 选中的成员、`path` 与 `git` 依赖、索引依赖)拒绝它,并点名条目所在的表与 名称。带 `[package]` 的工作空间根按它自己的 `[workspace.dependencies]` 解析自己的 `workspace = true` 条目。 -10. `[toolchain]`、`[target.]`、`[indices]` 与 `[profile.]` 是根位置的键:它们 - 为整个依赖图选择编译器、目标行、索引与构建 profile,因此只在成员作为一次构建的根时继承。 - profile 按名字继承,成员自己声明的同名表整体替换工作空间的。作为宿主工具构建的成员是其 - 子构建的根,同样继承这三项(§10.1)。`[build] dialect_cxxflags`(及其条件形式 +10. 构建的配置(作用域 C 的键:`[toolchain]`、`[target.]` 的标量行、`[indices]`、 + `[profile.]`、`[build] dialect_cxxflags` 与 `[target..abi]` 等)按以下顺序 + 决定,先出现者优先:命令行;被选中成员按第 12 条合并后的配置键,条件行按该构建的目标 + (`--target`,否则 `[build] target`,否则宿主)求值。因此成员只在作为一次构建的根时 + 接收工作空间层中的这些键;作为宿主工具构建的成员是其子构建的根,同样接收(§10.1)。 + 同一张构建图中的成员,求值后的配置**必须**相等;不相等的**必须**分到不同的图;只在其他 + 目标上不同的成员**必须**在同一张图中。规划成员的根**必须**携带成员的条件配置行,以与成员 + 相同的上下文求值。`[build] dialect_cxxflags`(及其条件形式 `[target..build] dialect_cxxflags`)同样是根位置的键:它是 §3.1 所述的图级联方言开关,只在包作为一次构建的根时被渲染并到达命令。与前三项相同, 一个包声明它不被诊断——一个依赖包为自己将来作为根的构建合法地声明这些键,这一条 @@ -562,9 +576,34 @@ mcpp 2026.9.26.2,#703)。** `ldflags` 链接,**禁止**接收根包或无关包的私有 `ldflags`。图级链接 flag 是 profile 的 `ldflags` 与 `[target..abi]` 为链接渲染的词;它们到达每一个镜像。 +12. **前缀即语义(W1–W7)。** 一份清单中,`workspace.` 之外的每张表只说本包:P、U、M 键作用于 + 本包,C 键是本包作为构建的根时的配置;`workspace.` 之内的每张表只说成员。可共享的表 `X` + 有镜像 `[workspace.X]`:`[workspace.package]`、`[workspace.build]`、 + `[workspace.dependencies]`、`[workspace.toolchain]`、`[workspace.indices]`、 + `[workspace.profile.]`、`[workspace.target.]`(及其 `.build`、`.abi`、 + `.runtime`、`.xlings.workspace`)、`[workspace.xlings]`。镜像的键、子表与选择器与 `X` + 相同,由读取 `X` 的同一读取器读取;描述一个包的键(`targets`、`dependencies`、 + `requires_abi`、`allow_host_libs`、`.build` 的 `sources` 与 `flags`、`[workspace.xlings]` + 的 `subos`)**必须**被拒绝;`[workspace]` 下的未知表**必须**被拒绝。合并规则只由键决定: + 标量在成员**声明**了该键时取成员的值;向量追加,工作空间在前;`defines` 按 §8 的集合语义; + 命名表(`profile.`、`target.`)逐键合并;条件行逐行继承,工作空间的行在 + 成员的行之前;成员自己的 `[toolchain] default` 覆盖工作空间的全部平台条目;相对路径以 + 写下它的清单所在目录为锚点。不带 `[package]` 的工作空间根上的包表(`[build]`、 + `[targets]`、`[dependencies]`、`[features]`、`[resources]`、`[test]`、`[hooks]`、 + `[runtime]`,选择器的 `.build`、`.abi`、`.runtime`、`.targets`、`.dependencies`)不作用于 + 任何包,实现**必须**给出警告,`--strict` 下**必须**拒绝;同一张表在这样的根上同时写在根位置 + 与 `[workspace.X]` **必须**被拒绝。 +13. **按位置读取(兼容,至 1.0.0)。** 工作空间根上没有 `[workspace.X]` 写法的 `[toolchain]`、 + `[indices]`、`[profile.]`、`[target.]` 标量行与 `[xlings]`、 + `[target..xlings]` 条目,仍按 mcpp 2026.10.8.1 的规则到达成员(成员的 + `[toolchain]` 整体替换根的,同名的行与 profile 整体替换,`[indices]` 只到达未声明任何索引 + 的成员)。每次使用**必须**给出警告 `manifest/workspace-position` 并写出 `[workspace.X]` + 写法;在带 `[package]` 的根上,只在成员确实经由这一读法得到值时给出。 + **状态:已实现(第 1 至 7 条 mcpp 2026.9.25.1;第 8 至 10 条 mcpp 2026.9.27.1,mcpp#713、 #714、#710;第 10 条的 `dialect_cxxflags` 为 mcpp 2026.9.28.1,#717;第 1 条的根包、第 10 条的 -`[profile.]` 与第 11 条为 mcpp 2026.10.5.2,#771)。** +`[profile.]` 与第 11 条为 mcpp 2026.10.5.2,#771;第 7、8、10 条的工作空间层与第 12、13 条为 +mcpp 2026.10.10.1,#785、#786)。** ## 10. 依赖的程序 @@ -639,3 +678,4 @@ mcpp 2026.9.26.2,#703)。** | 1.11 | 2026-10-05 | 新增 §5.3:只在一个平台生效的键带平台前缀(mcpp 2026.10.5.1,#766;`auto_export` 在发布前更名为 `windows_auto_export`)。§1 与 §6 引用的字段准入条件改指 docs/90,字段参考改指 docs/04。 | | 1.12 | 2026-10-05 | mcpp 2026.10.5.2:§9 第 1 条补上带 `[package]` 的工作空间根自己的包;第 10 条把 `[profile.]` 列为根位置的键;新增第 11 条链接 flag 的作用域(#771);新增 §11 链接期优化与导出发现(#770)与 `[test] windows_code_page`;§7 补第 21 至 23 条判据。 | | 1.13 | 2026-10-06 | mcpp 2026.10.5.3:§4.6 陈述架构段的等同拼写,`x86` 即 `i686`;`i386` 至 `i586` 不与 `i686` 相互替代。 | +| 1.14 | 2026-10-10 | mcpp 2026.10.10.1(#785、#786):§2 陈述键的作用域与来源;§9 新增第 12 条前缀即语义(工作空间层 `[workspace.X]`、按键合并、W7)与第 13 条按位置读取的兼容期;第 7 条发布写出工作空间层;第 8 条改为 `[workspace.xlings]`;第 10 条按求值后的配置分组。 | diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index 13a60d0e9..dec128e19 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -175,7 +175,8 @@ dialect_cxxflags = ["-D_HAS_EXCEPTIONS=0"] 与普通的构建输入不同,`dialect_cxxflags` 是图级联的(SPEC-004 §9 第 10 条), 所以只有这次构建的根贡献它:命令直接构建的那个包,或 `-p` 选中的成员。条目 -按这个顺序追加——`[workspace.build]`、根自己的 `[build]`、再到每个命中的 +按这个顺序追加——`[workspace.build]`、根自己的 `[build]`、每个命中的 +`[workspace.target..build]` 行、再到每个命中的 `[target..build]`(按选择器的具体程度:三元组在操作系统之后,操作系统在族之后; SPEC-004 §3.1.1)——解出的列表到达 std BMI 的预构建、 模块扫描与每一个翻译单元,对根和对每个依赖一视同仁。依赖包自己声明的 @@ -1751,6 +1752,26 @@ discover = ["tests/**/*.cpp"] # the default = "utf-8"` 使测试在同一个代码页中运行,于是测试结果不取决于运行它的机器的区域设置。 测试程序只接收应用程序清单,不接收 `[resources]` 的其它内容。 +### 2.18 `[workspace.*]` —— 工作空间共享的表(2026.10.10.1+) + +在工作空间根上,`workspace.` 之下的表交给每个成员,其键、子表与选择器与它所镜像的表 +相同:`[workspace.package]`、`[workspace.build]`、`[workspace.dependencies]`、 +`[workspace.toolchain]`、`[workspace.indices]`、`[workspace.profile.]`、 +`[workspace.target.]`(及其 `.build`、`.abi`、`.runtime`、`.xlings.workspace`) +与 `[workspace.xlings]`。不带前缀的表描述根自己的包。合并规则、被拒绝的键以及根自己的表 +按位置读取的方式见 [07 —— 工作空间](07-workspace.md) §4。 + +```toml +[workspace] +members = ["core", "app"] + +[workspace.package] +mcpp = ">=2026.10.10.1" + +[workspace.target.'cfg(os = "windows")'.build] +dialect_cxxflags = ["-DARCH_COMPAT=1"] +``` + ## 3. 实战示例 其中四个是可运行的工程,而不是片段,工程是更好的答案:它能构建,并且 diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index 89bc8b881..f6bde7480 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -31,7 +31,18 @@ mcpp 把所有工具链装进同一个沙盒目录(`~/.mcpp/registry/data/xpkg - macOS 使用 `llvm@23.1.3`。 - Windows 上存在可用 MSVC 时使用面向 MSVC ABI 的 `llvm@23.1.3`;没有可用 MSVC 时使用 `gcc@16.1.0`,target 为 `x86_64-windows-gnu`(MinGW-w64,默认 - 静态链接)。 + 静态链接),说明 MSVC ABI 缺的是哪一半(Visual Studio 的 C++ 工具或 Windows SDK) + 以及查找过的位置,并把这一对记为本机默认值。两半都齐备之后, + `mcpp toolchain default llvm@23.1.3` 把本机切换到 MSVC ABI。 +- Android(Termux)使用 GNU/Linux 在同一架构上的默认值:aarch64 上为 + `llvm@23.1.3` 加受管 glibc。需要 Android 10 或更高版本(shell 以 `/bin/sh` 取得), + 以及 xlings 2026.10.10.2 或更高版本,它在不能创建硬链接的应用沙箱中解包载荷。 + +被指定过的工具链——写在 `mcpp.toml` 中、由 `--toolchain` 给出,或是本机默认值—— +永不被换成 MinGW-w64。它需要 MSVC ABI 而 Visual Studio 的 C++ 工具或 Windows SDK +缺失时,构建在规划阶段被拒绝,错误写出缺的那一半、查找过的位置与可选做法:安装缺失的 +部分或设置 `WindowsSdkDir`;`mcpp toolchain install msvc`(受管 MSVC,自带 SDK);或用 +`--toolchain`、在 `mcpp.toml` 中自行选择 MinGW-w64。 在 Linux 宿主上,全静态的 musl 产物始终只差一个参数: `mcpp build --target x86_64-linux-musl`。 @@ -471,6 +482,14 @@ mcpp 按下面的顺序取第一个完整的 toolset(2026.9.24.1+): 5. 没有 `vswhere.exe` 时,标准的 `Program Files\Microsoft Visual Studio\\` 路径。 +`vswhere.exe` 在 `%ProgramFiles(x86)%` 下查找,标准路径依次在 `%ProgramFiles%`、 +`%ProgramFiles(x86)%` 与 `C:` 下查找(2026.10.10.1+)。 + +Windows SDK 依次取自:`WindowsSdkDir`(及 `WindowsSdkVersion`)、受管 toolset 旁的 SDK、 +SDK 安装器记录的根(`HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots` 的 +`KitsRoot10`,两种注册表视图),以及 `%ProgramFiles(x86)%`、`%ProgramFiles%`、`C:` 下的 +`Windows Kits\10`。因此装在其他盘的 SDK 不需要开发者命令行也能找到(2026.10.10.1+)。 + 实例的默认 toolset 是它的 `VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt` 所指的那一个。 一个 toolset 具备 `include\`、`lib\x64\`,对 cl.exe 构建还要具备 `cl.exe`, @@ -1232,8 +1251,16 @@ mcpp 遵循编译器的决定:不定义该宏,也不改动 std 模块,而 新版编译器视该 ABI 上的协程为不支持;在此目标上使用协程的代码风险自负。 -在项目中定义 `__cpp_impl_coroutine` 不是解决办法:它打开了编译器已声明在该 ABI -上不支持的功能。 +在项目中定义 `__cpp_impl_coroutine` 不是解决办法,也不推荐:它打开了编译器已声明在 +该 ABI 上不支持的功能,mcpp 不对结果做保证。坚持定义时,写在 `dialect_cxxflags` 中, +它到达 std 模块的预编译、扫描与每个 C++ 单元([04 —— mcpp.toml](04-mcpp-toml.md)): + +```toml +[target.i686-windows-msvc.build] +dialect_cxxflags = ["-D__cpp_impl_coroutine=201902L"] +``` + +写在 `cxxflags` 中时,std 模块的预编译看不到它。 `i686-windows-msvc` 不是目标表中的一行;为它构建的项目需要声明 `[target.i686-windows-msvc]`,与其他自定义三元组相同。 diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index 281a3b385..39ec36341 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -566,8 +566,13 @@ int main() { > 那条边的输入,所以顺序是白得的。生成的**头文件**永远不会:它是通过 `-I` 找到的, > 而能记录它的 depfile 要等到某次编译成功之后才存在。在此之前,一个产物全是头文件的 > action 在 `build.ninja` 里有节点却无人可达 —— 不在 `default`、不在 goal 集、没有 -> 任何边消费它 —— 于是它从不执行,而编译器读到的是 mcpp 为已声明产物写下的那个空占位 -> 文件。这条边**按包**划分,因为 `include_dir` 只染色声明它的那个包自己的 TU。 +> 任何边消费它 —— 于是它从不执行。这条边**按包**划分,因为 `include_dir` 只染色声明它 +> 的那个包自己的 TU。 + +mcpp 不在 action 的产物位置写任何东西:action 是其产物唯一的写入者(mcpp 2026.10.10.1+)。 +生成的翻译单元以声明的形式进入规划——它的路径与扩展名,以及 action 声明它提供与导入的 +模块——只由消费它的那次编译读取。`--configure-only` 之后,生成的文件尚不存在, +`compile_commands.json` 在它首次构建之前就已写出它的条目。 **命令自己发现依赖的 action 要声明 depfile**(mcpp 2026.9.7.1+)。`input()` 在 `build.mcpp` 运行时就把边的输入定死了,而那时命令还没执行,所以一个靠解析源码才知道自己 @@ -641,9 +646,10 @@ JSON),放不下的声明会被拒绝,于是一个消费者的 checkout 深 a.output(gen.c_str()).provides("my.generated").imports("std").submit(); ``` -mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与生成器将要产出的 -内容一致 —— 与 `[modules].scan_overrides` 同一条「声明 + 验证」的取舍,build 期由 -编译器自己的 P1689 输出复核。 +规划以该声明作为这个单元的模块接口,build 期由编译器自己的 P1689 输出复核——与 +`[modules].scan_overrides` 同一条「声明 + 验证」的取舍。生成的模块接口不写 +`.provides(...)` 时,构建中没有它的 BMI,导入它会失败;构建把这样的 action 报为 +degraded(`build-program/provides`)。 #### 环境变量与工作目录:`env` / `cwd`(protocol 13) @@ -1139,7 +1145,7 @@ mcpp 会把它自己构建时用的**同一份** std 模块暂存过来,缓存 | `MCPP_PACK_STRIP` *(2026.9.16.1+)* | `mcpp::pack_strip()` | 本程序所处的这次 `mcpp pack` 剥离时为 `1`,`--no-strip` 下为 `0`;任何普通构建下都为空。自己暂存库的成员跟随它,于是一个开关管住包里的每个文件 | | `MCPP_PACK_DEBUG_SYMBOLS_DIR` *(2026.9.16.1+)* | `mcpp::pack_debug_symbols_dir()` | `--debug-symbols` 放置分离出的 `*.debug` 文件的位置,绝对路径;丢弃调试信息或本次构建不在打包时为空 | | `MCPP_DEVICE_SOURCES` *(2026.9.5.2+)* | `mcpp::device_sources()` | 本包有效 `sources` 匹配到的设备类源文件(`.cu`、`.hip`…),相对包根,一行一个;没有时为空串。引擎一个都不编译 —— 由本程序引入的规则包把每一个变成一条 `mcpp::action`。已经过收窄:构建未覆盖的 `{ glob, accel }` 条目贡献为空,因此 `--no-accel` 得到空列表 | -| `MCPP_OUT_DIR` | `mcpp::out_dir()` | mcpp 提供的可写输出/暂存目录 | +| `MCPP_OUT_DIR` | `mcpp::out_dir()` | mcpp 提供的可写输出目录,按配置(目标、profile、工具链、加速器、该包的 features)与包各一个,位于构建的根之下:`target/.build-mcpp/out/<配置>/<包>`(2026.10.10.1+)。构建之外的脚本从 `resolution.json` 的 `graph.packages[].outDir` 读取它 | | `MCPP_MANIFEST_DIR` | `mcpp::manifest_dir()` | 包根(= CWD) | | `MCPP_FEATURE_` | `mcpp::has_feature("name")` | 每个活跃 feature 置 `1`(`` 消毒规则与 `MCPP_FEATURE_` 编译宏一致) | | `MCPP_FEATURES` | — | 活跃 feature 逗号列表 | @@ -1292,8 +1298,9 @@ action,由引擎调度。工具说明的是构建程序需要、而没有任 带 `build.mcpp` 的依赖包也会被编译并运行(Cargo `build.rs` 模型——构建一个包 即信任其构建程序),时机在其 feature 解析之后、源扫描之前。作用域照 Cargo: `cxxflag`/`cflag`/`cfg` 指令只染色**该包自身的 TU**;`link-lib`/`link-search` -到达终链。其产物(二进制、缓存、`MCPP_OUT_DIR`)放在**消费方工程**的 -`target/.build-mcpp/deps/@/` 下——registry 包根跨工程共享(且可能只读), +到达终链。其产物(二进制、缓存)放在**消费方工程**的 +`target/.build-mcpp/deps/@/` 下,`MCPP_OUT_DIR` 在该工程的 +`target/.build-mcpp/out/<配置>/`——registry 包根跨工程共享(且可能只读), 绝不写入;相对 `generated=` 路径按 `MCPP_OUT_DIR` 解析,而非包根。 ### 既独立构建又被当依赖的库:发绝对路径 From 910d551cadd1dd17941889f542aa294751cf782e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:15:12 +0900 Subject: [PATCH 16/32] docs(changelog): 2026.10.10.1 --- CHANGELOG.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da1616b05..6bd68ce54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,85 @@ > 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.10.1] - 2026-10-10 + +### Changed + +- Give a table under `workspace.` to every member and keep every other table + of a manifest for its own package (#785, #786). `[workspace.toolchain]`, + `[workspace.indices]`, `[workspace.profile.]`, + `[workspace.target.]` with its `.build`, `.abi`, `.runtime` and + `.xlings.workspace` subtables, and `[workspace.xlings]` mirror the tables of + the same name and are read by the same reader. The merge rule belongs to the + key: a scalar the member declared is the member's, a list puts the + workspace's entries first, `profile.` and `target.` merge key + by key, and conditional rows are inherited ahead of the member's. A key that + describes one package is refused under `workspace.`, and so is an unknown + table under `[workspace]`. +- Keep reading a workspace root's own `[toolchain]`, `[indices]`, + `[profile.]`, `[target.]` scalars and `[xlings]` entries by + position, with the merge they had, and report each use as + `manifest/workspace-position` with the `[workspace.X]` spelling. This + reading is removed in mcpp 1.0.0. On a root without `[package]`, a table + written both ways is refused, and package tables (`[build]`, `[targets]`, + `[dependencies]`, a selector's `.build`, ...) are reported as acting on no + package; `--strict` refuses them. A root that uses `[workspace.X]` without + `[workspace.package] mcpp = ">=2026.10.10.1"` gets a note, because an older + engine ignores these tables. +- Group workspace members by their configuration as it evaluates for the + target being built, and carry a member's conditional `dialect_cxxflags` and + `.abi` rows to the root it is planned under. A row for another target no + longer splits two members into two graphs. +- Write the workspace layer into a published member's own tables + (`[workspace.toolchain]` as `[toolchain]`, `[workspace.target..build]` + as `[target..build]`), rewriting relative paths against the package and + refusing one that leaves it. +- Give each configuration and package its own build-program output directory, + `target/.build-mcpp/out//` below the build's root, + and record it as `outDir` in `resolution.json`. `mcpp::out_dir()` and + `MCPP_OUT_DIR` change; `${mcpp.out_dir}` does not. +- Write nothing at an action's outputs before the action runs. A generated + translation unit enters the plan from its action's declaration; a generated + module interface without `.provides(...)` is reported as degraded. +- Pass the managed toolchain's library path on Linux to the tools that need + it (`$cxx`, `$cc`, `$ar`, `clang-scan-deps`) instead of ninja's environment, + so no shell ninja starts loads the toolchain's libc++. +- Pin xlings 2026.10.10.2 and bootstrap from mcpp 2026.10.8.1. + +### Added + +- Warn when `--toolchain` replaces a toolchain the manifest declares, naming + both, the key and the file. The warning does not fail `--strict`. +- Report `android` in `mcpp self env --format json`. An Android host takes the + default toolchain of GNU/Linux on its architecture; Android 10 or later is + required. +- Show an xlings extraction that runs longer than 10 s and a long install hook + as `Installing` lines while a toolchain is installed, and give the install + hint by the failure's class (network, disk full, permission, damaged + archive). +- Run Termux fresh-install jobs on the released archive: termux-docker on an + arm64 runner and the Termux app sandbox on an Android 14 emulator. + +### Fixed + +- Take a module unit's role from its module declaration and its language from + its extension (#790). An implementation partition in a `.cpp` writes its BMI + and is not rebuilt by every build; an interface unit in a `.cpp` is found by + its importers under Clang. cl.exe compiles an implementation partition with + `/internalPartition`. +- Regenerate an action's output that was removed while the build's log + recorded the action (#778), and keep a profile's generated output from + serving another profile. +- Find the Windows SDK where its installer recorded it (`KitsRoot10`) and under + `%ProgramFiles(x86)%`/`%ProgramFiles%`, and `vswhere.exe` under + `%ProgramFiles(x86)%`. Say which half of the MSVC ABI is missing, and where it + was looked for, both on a first run that falls back to MinGW-w64 and when a + specified MSVC-ABI toolchain is refused; a specified toolchain is never + replaced. +- Resolve the shell an argv-launched command string runs in (`/bin/sh`, else + `/system/bin/sh`, else `$PREFIX/bin/sh`). +- Report the first error of an xlings install, which is its cause. + ## [2026.10.8.1] - 2026-10-09 ### Changed From eeea6aac149579a62e6b24d8c9e24ed2ad7bf74f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:16:48 +0900 Subject: [PATCH 17/32] chore: pin xlings 2026.10.10.2 --- .github/actions/bootstrap-mcpp/action.yml | 2 +- .github/actions/setup-macos-llvm/action.yml | 2 +- .github/workflows/bootstrap-macos.yml | 2 +- .github/workflows/ci-fresh-install.yml | 6 +++--- .github/workflows/ci-linux-e2e.yml | 2 +- .github/workflows/cross-build-test.yml | 4 ++-- .github/workflows/release.yml | 14 +++++++------- src/xlings/xlings.cppm | 10 +++++++++- 8 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 691a3e5c6..b3b4a70ab 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -33,7 +33,7 @@ inputs: # `package.name`, so one of the two was simply unreachable — and which one # depended on the machine, which is why CI failed on `compat:lua` on # Windows and `mcpplibs.capi:lua` on Linux. Never pin below that. - default: '2026.10.8.1' + default: '2026.10.10.2' outputs: sandbox-key: diff --git a/.github/actions/setup-macos-llvm/action.yml b/.github/actions/setup-macos-llvm/action.yml index 9888e90d0..1ff84674f 100644 --- a/.github/actions/setup-macos-llvm/action.yml +++ b/.github/actions/setup-macos-llvm/action.yml @@ -15,7 +15,7 @@ inputs: # Floor imposed by the index, not a routine bump — see # .github/actions/bootstrap-mcpp/action.yml for why 0.4.69 is required # (two packages named `lua` in one repo need openxlings/xlings#381). - default: '2026.10.8.1' + default: '2026.10.10.2' image: description: > The runner label the job runs on (macos-15, xcode-27). It is part of the diff --git a/.github/workflows/bootstrap-macos.yml b/.github/workflows/bootstrap-macos.yml index fb357b39e..18cc6b4ec 100644 --- a/.github/workflows/bootstrap-macos.yml +++ b/.github/workflows/bootstrap-macos.yml @@ -17,7 +17,7 @@ jobs: # Dormant (workflow_dispatch only), but kept in step with the rest — # check_version_pins.sh holds it there. Floor: 0.4.69, below which the # index cannot resolve two packages that share a short name. - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 41c532b33..591526ad6 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -152,7 +152,7 @@ jobs: env: XLINGS_NON_INTERACTIVE: '1' run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.8.1 + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.10.2 echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - name: Install mcpp and config mirror @@ -315,7 +315,7 @@ jobs: - name: Install xlings + mcpp run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.8.1 + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.10.2 # Deliberately NOT writing to $GITHUB_PATH here. On container # images that declare no PATH in their config (opensuse/ # tumbleweed), appending a single dir to GITHUB_PATH makes the @@ -464,7 +464,7 @@ jobs: # (older ones carry minos=15 and refuse to start). # v0.4.51+: in-process sha256 — this image has no sha256sum # binary, so pinned fetches failed before it. - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.8.1 + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.10.2 echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - name: Install mcpp and config mirror diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index 1fa19b1d7..e01246a16 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -359,7 +359,7 @@ jobs: - name: Bootstrap xlings + released mcpp run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.8.1 + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.10.2 export PATH="$HOME/.xlings/subos/current/bin:$PATH" xlings update xlings install mcpp -y -g diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml index 55ae586b9..ff412499e 100644 --- a/.github/workflows/cross-build-test.yml +++ b/.github/workflows/cross-build-test.yml @@ -131,7 +131,7 @@ jobs: # release assets were uploaded in a broken state (records present, # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX # half is handled by the marker-clear below. - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ @@ -300,7 +300,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d8fe630d..f471abc69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,7 +104,7 @@ jobs: # Pin xlings to a known-good version. The upstream install # script always grabs `latest` (no version override), so we # download + self-install manually to avoid broken releases. - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' run: | if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" @@ -322,7 +322,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ @@ -393,7 +393,7 @@ jobs: # below are pinned to the same version as XLINGS_VERSION; they are # NOT interpolated from it, so check_version_pins.sh scans for them # explicitly (they were absent from the old lock-step comment). - XLA="xlings-2026.10.8.1-linux-aarch64.tar.gz" + XLA="xlings-2026.10.10.2-linux-aarch64.tar.gz" # NOT fetch_release.sh: this asset is OPTIONAL and the `if` is the # point — an arch with no prebuilt xlings must fall through quietly, # while the helper retries a 404 five times before giving up. The one @@ -402,9 +402,9 @@ jobs: # cover it. if curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \ --connect-timeout 20 --max-time 600 -o "/tmp/$XLA" \ - "https://github.com/openxlings/xlings/releases/download/v2026.10.8.1/$XLA"; then + "https://github.com/openxlings/xlings/releases/download/v2026.10.10.2/$XLA"; then tar -xzf "/tmp/$XLA" -C /tmp - XLBIN=$(find /tmp/xlings-2026.10.8.1-linux-aarch64 -path '*/bin/xlings' -type f | head -1) + XLBIN=$(find /tmp/xlings-2026.10.10.2-linux-aarch64 -path '*/bin/xlings' -type f | head -1) if [ -n "$XLBIN" ]; then mkdir -p "$STAGING/$WRAPPER/registry/bin" cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings" @@ -482,7 +482,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' run: | if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then WORK=$(mktemp -d) @@ -665,7 +665,7 @@ jobs: shell: bash env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.10.8.1' + XLINGS_VERSION: '2026.10.10.2' run: | # Captured before the `cd` below, in POSIX form: this step never # returns to the workspace, and GITHUB_WORKSPACE is a backslash diff --git a/src/xlings/xlings.cppm b/src/xlings/xlings.cppm index 3f3571493..c64d7e5e2 100644 --- a/src/xlings/xlings.cppm +++ b/src/xlings/xlings.cppm @@ -114,7 +114,15 @@ namespace pinned { // page, as mcpp.exe does. // Metadata and install share the process ABI context from 2026.10.8.1; // ARM64 runtime exports must be correct before dependency resolution. - inline constexpr std::string_view kXlingsVersion = "2026.10.8.1"; + // + // Seventh, at 2026.10.10.2 (openxlings/xlings#651). Below it, a payload's + // hard-link entry failed the whole extraction where link(2) is refused -- + // Android's app sandbox, so every Termux install of the managed glibc -- + // every local write failure was reported as E_DISK_FULL, the packages that + // depended on a failed one ran their hooks anyway, and an extraction said + // nothing until it ended. mcpp reads the failure's code for its hint and + // the `extract` and `hook` progress events for its `Installing` lines. + inline constexpr std::string_view kXlingsVersion = "2026.10.10.2"; inline constexpr std::string_view kNasmVersion = "3.02"; } From c28e2ee4a9947f1192f0936383dd3efb7aa71c42 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:20:34 +0900 Subject: [PATCH 18/32] fix(build): the resource compiler takes the toolchain's library path too --- src/build/ninja_backend.cppm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index bd05e457c..aa60a8204 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1539,7 +1539,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, } const bool need_rc_rule = !plan.resourceUnits.empty(); if (need_rc_rule) { - append(std::format("rc = {}\n", escape_ninja_path(plan.rcPath))); + append(std::format("rc = {}{}\n", toolEnv, escape_ninja_path(plan.rcPath))); std::string rcf; for (auto const& f : plan.rcFlags) { rcf += ' '; rcf += shell_quote_arg(f); } append(std::format("rcflags ={}\n", rcf)); From 0856e8a1032987cbd635c3b08ec619b105284d74 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:34:56 +0900 Subject: [PATCH 19/32] fix(build): the managed tools run on their own RPATH; the library path is given only to a compiler without one --- ...026-10-10-one-source-one-meaning-design.md | 4 +++ CHANGELOG.md | 7 +++-- src/build/ninja_backend.cppm | 29 +++++++++++-------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.agents/docs/2026-10-10-one-source-one-meaning-design.md b/.agents/docs/2026-10-10-one-source-one-meaning-design.md index 55e0c1eb6..37a64af52 100644 --- a/.agents/docs/2026-10-10-one-source-one-meaning-design.md +++ b/.agents/docs/2026-10-10-one-source-one-meaning-design.md @@ -31,6 +31,10 @@ status: landed - D28:以 argv 启动的 shell 由 `posix_shell()` 解析;`popen` 与 `std::system` 仍用 C 库的 `/bin/sh`, 因此 Android 的基线是 10 及以上。 - D23:mcpp 把 xlings 的 `extract`、`hook` 事件显示为 `Installing …` 行,并写入日志;不另加 30 s 心跳。 +- D29:受管载荷的程序带有 xlings 安装时写入的 RPATH,不依赖 `LD_LIBRARY_PATH`(实测 clang、clang-scan-deps、 + llvm-ar、lld 去掉该变量都能运行)。因此在 Linux 上该变量不再进入 ninja 的环境,也不加在工具上;只有编译器 + 本身没有 RPATH/RUNPATH 时(以路径指定的工具链),才以 `env LD_LIBRARY_PATH=… ` 前缀加在工具上。 + 这样 `build.ninja` 中 `cxx =` 的写法对受管工具链不变(tests/e2e/188 读取它)。 - D26 以 `degraded`(`build-program/provides`)实现。 - 工作空间层的解析:`[workspace.*]` 下的文档由同一个 `parse_document` 读取,保留原值的行号。 - 测试:e2e 893–901(在 2026.10.8.1 上全部失败);Windows 探测由单元测试覆盖;D27 的两个 job 在 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd68ce54..d4f839243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,9 +44,10 @@ - Write nothing at an action's outputs before the action runs. A generated translation unit enters the plan from its action's declaration; a generated module interface without `.provides(...)` is reported as degraded. -- Pass the managed toolchain's library path on Linux to the tools that need - it (`$cxx`, `$cc`, `$ar`, `clang-scan-deps`) instead of ninja's environment, - so no shell ninja starts loads the toolchain's libc++. +- Keep the managed toolchain's library path out of ninja's environment on + Linux, so no shell ninja starts loads the toolchain's libc++. The managed + tools find their libraries through their own RPATH; a compiler without one + gets the path on the tools alone. - Pin xlings 2026.10.10.2 and bootstrap from mcpp 2026.10.8.1. ### Added diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index aa60a8204..bafbe1325 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1453,8 +1453,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, plan.toolchain.binaryPath, plan.toolchain.targetTriple); if (!fsObjcopy.empty()) { - append("objcopy = " + mcpp::platform::linux_::build_clean_ld_library_path_prefix( - plan.toolchain.compilerRuntimeDirs) + escape_ninja_path(fsObjcopy) + "\n\n"); + append("objcopy = " + escape_ninja_path(fsObjcopy) + "\n\n"); // ── The raw image a flasher takes ────────────────────────────────── // // A SEPARATE EDGE with the ELF as its input, not a second output of @@ -1484,20 +1483,26 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, const std::string launch = flags.launcher.empty() ? std::string{} : escape_ninja_path(std::filesystem::path(flags.launcher)) + " "; // D29: THE TOOLCHAIN'S PRIVATE LIBRARIES ARE THE TOOLCHAIN'S. A managed - // LLVM's `clang`, `llvm-ar` and `clang-scan-deps` find their own libc++ and - // libLLVM through LD_LIBRARY_PATH, and that path used to be in ninja's - // environment -- so every `/bin/sh -c` ninja starts had it too. A shell - // that links libc++ (Android's bionic `sh`) then loaded the LLVM's, + // LLVM's library directories used to be put in ninja's environment as + // LD_LIBRARY_PATH, so every `/bin/sh -c` ninja starts inherited them. A + // shell that links libc++ (Android's bionic `sh`) then loaded the LLVM's, // built for glibc, and died before running anything: // CANNOT LINK EXECUTABLE "/bin/sh": .../xim-x-llvm/23.1.3/lib/ // aarch64-unknown-linux-gnu/libc++.so is too small - // (termux-docker, design 2026-10-10 §13). The path now prefixes the tools - // that need it and reaches nothing else; macOS keeps no such path at all + // (termux-docker, design 2026-10-10 §13). A managed payload's programs + // carry their own search path (the RPATH xlings writes at install), so on + // Linux the variable reaches no process ninja starts. A compiler without + // one -- a toolchain named by path -- gets it on the tools alone, as an + // `env` prefix. macOS keeps no such path at all // (`runtime_library_path_key`), Windows keeps its PATH. - const std::string toolEnv = mcpp::platform::is_linux - ? mcpp::platform::linux_::build_clean_ld_library_path_prefix( - plan.toolchain.compilerRuntimeDirs) - : std::string{}; + const std::string toolEnv = [&]() -> std::string { + if (!mcpp::platform::is_linux || plan.toolchain.compilerRuntimeDirs.empty()) return {}; + if (auto facts = mcpp::platform::elf::inspect_elf_runtime(flags.cxxBinary); + facts && facts->searchPathTag != mcpp::platform::elf::SearchPathTag::None) + return {}; + return mcpp::platform::linux_::build_clean_ld_library_path_prefix( + plan.toolchain.compilerRuntimeDirs); + }(); append(std::format("cxx = {}{}{}\n", toolEnv, launch, escape_ninja_path(flags.cxxBinary))); // The driver alone, for the dependency scan: `clang-scan-deps` reads its // argv[0] as the compiler (and the resource directory beside it), and a From 576e8f9e286f53a1bf271bb43b6cebafde978acc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:39:35 +0900 Subject: [PATCH 20/32] fix(build-program): the program's directory is made beside its per-configuration output; e2e 111 reads the new place --- src/build/build_program.cppm | 5 ++++- tests/e2e/111_dep_build_mcpp.sh | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index 41485ccfb..14cdd8c84 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -1497,7 +1497,10 @@ std::expected run_build_program_impl( // the program's turn comes. if (mode == ProgramMode::Compile) programReport.reported = true; - fs::create_directories(outDir, ec); // creates bdir too + // Both: the output directory is per configuration and need not lie + // below `bdir`, where the program binary and its cache are (D7). + fs::create_directories(bdir, ec); + fs::create_directories(outDir, ec); // #230: on Windows the capture_exec shell is cmd.exe, which can only launch // a PE by an executable extension — a bare `.bin` is not in PATHEXT and // fails to run. Name the compiled program `.exe` there; other platforms keep diff --git a/tests/e2e/111_dep_build_mcpp.sh b/tests/e2e/111_dep_build_mcpp.sh index 71c7edf81..01f247432 100755 --- a/tests/e2e/111_dep_build_mcpp.sh +++ b/tests/e2e/111_dep_build_mcpp.sh @@ -80,8 +80,9 @@ after="$(find "$TMP/deplib" | sort)" diff <(echo "$before") <(echo "$after") || true echo "dependency root was written to"; exit 1; } -# Artifacts live in the consuming project. -find target/.build-mcpp/deps -name 'gen_dep.cpp' | grep -q . || { +# Artifacts live in the consuming project: the program in .build-mcpp/deps, +# its output in this configuration's directory (mcpp 2026.10.10.1+). +find target/.build-mcpp/out -path '*deplib/gen_dep.cpp' | grep -q . || { echo "dep OUT_DIR generated file not in project tree"; exit 1; } echo "OK" From 82c1d8759a54f6db5c119c51ab181968a6d2f14c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 07:43:08 +0900 Subject: [PATCH 21/32] ci(docs): the key registry and the field reference agree; docs/04 names the five [build] keys it missed --- ...026-10-10-one-source-one-meaning-design.md | 3 +- .github/tools/check_manifest_key_classes.py | 54 +++++++++++++++++++ .github/workflows/ci.yml | 3 ++ docs/04-mcpp-toml.md | 9 ++++ docs/zh/04-mcpp-toml.md | 9 ++++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 .github/tools/check_manifest_key_classes.py diff --git a/.agents/docs/2026-10-10-one-source-one-meaning-design.md b/.agents/docs/2026-10-10-one-source-one-meaning-design.md index 37a64af52..47de90515 100644 --- a/.agents/docs/2026-10-10-one-source-one-meaning-design.md +++ b/.agents/docs/2026-10-10-one-source-one-meaning-design.md @@ -26,7 +26,8 @@ status: landed 它读 `Manifest`,而 `mcpp.manifest.types` 导入该门面。 - `KeyRegistry`(`mcpp.manifest.key_registry`)推导出:解析器的 `[build]` 与 `[target..build]` 已知键、 `[workspace.target..build]` 接受的键、W7 的包表清单;单元测试核对它与 `kWorkspaceBuildKeys`、与分组键。 - `[target.]` 标量清单与文档中的键类别表仍由各自已有的测试守护。 + `[target.]` 标量清单仍由已有的测试守护。`.github/tools/check_manifest_key_classes.py` 核对登记表与 + docs/04(每个 `[build]` 键)、docs/07(每张共享表)的中英文版本;它发现 docs/04 漏了五个 `[build]` 键,已补。 - D7:构建程序的缓存仍是每个包一条记录;切换配置会重新运行程序,不会重新编译它。 - D28:以 argv 启动的 shell 由 `posix_shell()` 解析;`popen` 与 `std::system` 仍用 C 库的 `/bin/sh`, 因此 Android 的基线是 10 及以上。 diff --git a/.github/tools/check_manifest_key_classes.py b/.github/tools/check_manifest_key_classes.py new file mode 100644 index 000000000..9f3eba906 --- /dev/null +++ b/.github/tools/check_manifest_key_classes.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""The key registry and the documents that list its keys agree (SPEC-004 §2). + +`modules/manifest/src/key_registry.cppm` states the class of every key once, +and the parser reads its `[build]` keys from there. A key the registry adds +and the field reference does not mention, or a shared table the workspace +chapter does not list, is the drift this check stops: + + - every `[build]` key of the registry is named in docs/04 and docs/zh/04; + - every table the registry marks shared has a `[workspace.
]` row in + docs/07 and docs/zh/07 (`profile.` as `[workspace.profile.]`, + the `target.` rows as `[workspace.target.]`). +""" +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[2] +ROW = re.compile(r'\{"([^"]+)",\s*"([^"]*)",\s*KeyScope::(\w+),\s*KeyMerge::(\w+),\s*(true|false),\s*(true|false)\s*\}') + + +def main() -> int: + rows = ROW.findall((ROOT / "modules/manifest/src/key_registry.cppm").read_text()) + if not rows: + print("FAIL: no rows read from key_registry.cppm") + return 1 + problems = [] + for lang in ("", "zh/"): + ref = (ROOT / f"docs/{lang}04-mcpp-toml.md").read_text() + ws = (ROOT / f"docs/{lang}07-workspace.md").read_text() + for table, key, _scope, _merge, shared, _cond in rows: + if table == "build" and key and f"`{key}`" not in ref and f"{key} " not in ref: + problems.append(f"docs/{lang}04-mcpp-toml.md does not name [build] {key}") + if shared == "true" and not key: + mirror = { + "profile.": "[workspace.profile.]", + "package": "[workspace.package]", + }.get(table) + if table.startswith("target."): + mirror = "[workspace.target.]" + if mirror is None: + mirror = f"[workspace.{table}" + if mirror not in ws: + problems.append(f"docs/{lang}07-workspace.md does not list {mirror}") + for p in problems: + print(f"FAIL: {p}") + if problems: + return 1 + print(f"OK: {len(rows)} registry rows agree with docs/04 and docs/07 (en, zh)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d10643dc..78831618b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,9 @@ jobs: - name: Check documentation structure run: bash .github/tools/check_docs_structure.sh + - name: The key registry and the field reference agree + run: python3 .github/tools/check_manifest_key_classes.py + - name: Documented target tiers agree with the table run: python3 .github/tools/check_target_tiers.py diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index a60abd1c5..ed0edb8c8 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -886,6 +886,15 @@ machine running mcpp: `mcpp build --target aarch64-macos` honors this field (and the environment variable) the same way on Linux or Windows as it does on a Mac, and a non-macOS target never sees it. +### `[build]` keys described in other chapters + +| Key | Chapter | +|---|---| +| `allow_host_libs` | [20 — Toolchains](20-toolchains.md), [22 — Target Side](22-target-side.md) | +| `ios_deployment_target` | [20 — Toolchains](20-toolchains.md) | +| `platform-dependencies` | [22 — Target Side](22-target-side.md) | +| `std-module`, `std-compat-module` | [22 — Target Side](22-target-side.md) | + ### Build concurrency (`jobs`) and module scheduling (`bmi_schedule`) ```toml diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index dec128e19..659917b34 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -829,6 +829,15 @@ minos`)里记录的最低系统版本,也就是这个二进制能运行的 aarch64-macos` 在 Linux 或 Windows 上和在 Mac 上一样遵从这个字段 (以及环境变量),非 macOS 目标则永远不会看到它。 +### 在其他章节说明的 `[build]` 键 + +| 键 | 章节 | +|---|---| +| `allow_host_libs` | [20 —— 工具链](20-toolchains.md)、[22 —— 目标侧](22-target-side.md) | +| `ios_deployment_target` | [20 —— 工具链](20-toolchains.md) | +| `platform-dependencies` | [22 —— 目标侧](22-target-side.md) | +| `std-module`、`std-compat-module` | [22 —— 目标侧](22-target-side.md) | + ### 构建并发(`jobs`)与模块调度(`bmi_schedule`) ```toml From 58a3d0263c31592655ea0527d4d284ada247de9c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 08:28:26 +0900 Subject: [PATCH 22/32] fix(toolchain): the override warning names a path outside the project by its absolute form on Windows too A relative path that leaves the current directory is tested by its first component, not by the native string's prefix. --- src/build/prepare/toolchain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 23070c89e..ec2708355 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -67,7 +67,7 @@ static void warn_toolchain_override(const std::string& given, const std::string& if (auto it = tc.fileByPlatform.find(key); it != tc.fileByPlatform.end()) file = it->second; std::error_code ec; auto shown = std::filesystem::relative(file, std::filesystem::current_path(), ec); - if (ec || shown.empty() || shown.native().starts_with("..")) shown = file; + if (ec || shown.empty() || *shown.begin() == "..") shown = file; mcpp::diag::warning("toolchain/override", std::format( "--toolchain {} replaces {} declared at [toolchain].{} ({})", given, declared, key, shown.generic_string())); From 01e3e2aa72fa1f83b7211761ab60eff9bea1bd4c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 08:28:26 +0900 Subject: [PATCH 23/32] fix(build): a program's out dir is beside its cache; emit build-database reuses a member's run A workspace member's build program keeps its cache in the member's own target/.build-mcpp, also under a plan's work directory. Its out dir now has the same base, so MCPP_OUT_DIR, and the re-run key, match between a build and emit build-database (e2e 839 B4). e2e 652b, 810, 867, 868, 870 read the per-configuration out dir; resolution.json's outDir is not part of a program's graph document. --- src/build/prepare/features.cpp | 2 +- src/build/prepare/graph.cpp | 5 ++-- src/build/prepare/state.cppm | 14 ++++++---- src/build/prepare/target_side.cpp | 20 +++++++++---- ...lication_on_android_is_a_shared_library.sh | 14 ++++++---- ...kages_deploy_the_same_bytes_to_one_name.sh | 17 +++++------ ...ers_plans_builds_and_runs_programs_once.sh | 28 +++++++++++-------- ..._is_refused_before_anything_is_compiled.sh | 2 +- ...l_members_resolves_and_fails_per_member.sh | 4 ++- 9 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 1c38fa2ea..d899371e8 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -1941,7 +1941,7 @@ static std::expected step6_dependency_build_programs(PrepareS bpEnv.features = feature_closure(pkg.manifest, req, depDefaultFeatures); bpEnv.artifactsDir = state.workRoot / "target" / ".build-mcpp" / "deps" / (dirSafe(pkg.manifest.package.name) + "@" + pkg.manifest.package.version); - bpEnv.outDir = state.programOutDir(pkg.manifest, bpEnv.features); + bpEnv.outDir = state.programOutDir(state.workRoot, pkg.manifest, bpEnv.features); bpEnv.genBase = bpEnv.outDir; // What the program imports is kept once for the workspace, and in the // global cache where it comes from the engine or the index (#748). diff --git a/src/build/prepare/graph.cpp b/src/build/prepare/graph.cpp index 35e9c9066..5859cf49f 100644 --- a/src/build/prepare/graph.cpp +++ b/src/build/prepare/graph.cpp @@ -2159,7 +2159,8 @@ step4b_define_provisioning_closures(PrepareState& state) { // (the scanner walks the legacy modules.sources mirror). ninja overwrites // the placeholder before the compile edge runs, because that compile // depends on the action's output. - state.programOutDir = [&state](const mcpp::manifest::Manifest& pm, + state.programOutDir = [&state](const std::filesystem::path& base, + const mcpp::manifest::Manifest& pm, const std::vector& features) { // What changes the program's environment and is known before it runs. std::string id = std::format("target={}\nprofile={}\naccel={}\n", @@ -2174,7 +2175,7 @@ step4b_define_provisioning_closures(PrepareState& state) { ? pm.package.name : pm.package.namespace_ + "." + pm.package.name; for (auto& c : name) if (!std::isalnum(static_cast(c)) && c != '.' && c != '-' && c != '_') c = '_'; - return state.workRoot / "target" / ".build-mcpp" / "out" + return base / "target" / ".build-mcpp" / "out" / mcpp::toolchain::hash_string(id) / (name.empty() ? std::string("root") : name); }; diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 359bd28ce..ceef60222 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -622,12 +622,16 @@ struct PrepareState { std::function adoptActionOutputs; // A package's build-program output directory (`mcpp::out_dir()`, - // MCPP_OUT_DIR) for this build's configuration (D7, D25): below the plan - // root, one directory per configuration -- the target, the profile, the + // MCPP_OUT_DIR) for this build's configuration (D7, D25): below `base`, + // one directory per configuration -- the target, the profile, the // toolchain, the accelerators and the features the package is built - // with -- and per package. The program binary and its cache stay in the - // configuration-independent `.build-mcpp`. - std::function&)> programOutDir; std::function fillXpkgDirs; diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index 0873f214f..d7de9958d 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -1521,13 +1521,19 @@ static void step9_define_graph_package_entry_closure(PrepareState& state) { // D7: where this package's build program writes in this // configuration (`mcpp::out_dir()`), for a reader outside the build // -- a test script -- that needs a generated file and must not guess. - { + // resolution.json only: the graph document a build program reads is + // part of its re-run key, and must not change with when it is asked. + if (!forBuildProgram) { static const std::vector none; - entry["outDir"] = state.programOutDir(pm, i < state.activeFeaturesByPackage.size() - ? state.activeFeaturesByPackage[i] : none) + // A member's program keeps its cache in the project (step 9). + const bool member = state.m->package.virtualRoot && i != 0 + && state.isWorkspaceMemberPackage(i); + entry["outDir"] = state.programOutDir(member ? *state.root : state.workRoot, pm, + i < state.activeFeaturesByPackage.size() + ? state.activeFeaturesByPackage[i] : none) .generic_string(); + return entry; } - if (!forBuildProgram) return entry; std::error_code ec; auto dir = std::filesystem::absolute(state.packages[i].root, ec).lexically_normal(); @@ -1623,7 +1629,7 @@ static std::expected step9_root_build_program(PrepareState& s // contract hash — and therefore the build.mcpp cache — is unchanged // across the move for feature-identical builds. bpEnv.features = feature_closure(*state.m, parse_feature_request(state.overrides.features)); - bpEnv.outDir = state.programOutDir(*state.m, bpEnv.features); + bpEnv.outDir = state.programOutDir(state.workRoot, *state.m, bpEnv.features); // mcpp#241 (root): consumer index 0, same owner as the dep loop. // // AND THE LINK FORM OF EACH DEPENDENCY (#642 E2), to this program only. @@ -2024,7 +2030,9 @@ static std::expected step9_member_build_programs(PrepareState bpEnv.artifactsDir = pkg.root / "target" / ".build-mcpp"; if (i < state.activeFeaturesByPackage.size()) bpEnv.features = state.activeFeaturesByPackage[i]; - bpEnv.outDir = state.programOutDir(pkg.manifest, bpEnv.features); + // Beside the cache above, which is in the project under a plan's + // work directory too: `emit build-database` reuses the build's run. + bpEnv.outDir = state.programOutDir(*state.root, pkg.manifest, bpEnv.features); { std::map linkForms; for (auto const& [idx, form] : state.dependencyLinkForms) diff --git a/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh b/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh index 511bdca53..46c6b7f51 100755 --- a/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh +++ b/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh @@ -157,11 +157,11 @@ staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) echo "mcpp pack --format blob on Android stages lib/libmyapp.so OK" # ── 5. MCPP_TARGET_MIN_PLATFORM_VERSION: numeric on Android, empty on host ─ -# `mcpp::out_dir()` for a build PROGRAM (as opposed to `${mcpp.out_dir}` inside -# an action, which is per-triple) is one path per package, -# `target/.build-mcpp/out/` -- read the value RIGHT AFTER the Android pack -# above, before the host build below writes the SAME file with its own answer. -minplatFile="target/.build-mcpp/out/minplat.txt" +# `mcpp::out_dir()` for a build PROGRAM is one directory per configuration and +# package (mcpp 2026.10.10.1+), so the Android pack and the host build below +# write different files: the newest one is the build that just ran. +newest_minplat() { ls -t target/.build-mcpp/out/*/*/minplat.txt 2>/dev/null | head -1; } +minplatFile=$(newest_minplat) [ -f "$minplatFile" ] || fail "build.mcpp did not write $minplatFile" pack.log androidMinplat=$(cat "$minplatFile") case "$androidMinplat" in @@ -170,7 +170,9 @@ esac echo "MCPP_TARGET_MIN_PLATFORM_VERSION is numeric on $TARGET ($androidMinplat) OK" "$MCPP" build > buildhost.log 2>&1 || fail "the host build failed" buildhost.log -hostMinplat=$(cat "$minplatFile" 2>/dev/null || true) +hostFile=$(newest_minplat) +[ "$hostFile" != "$minplatFile" ] || fail "the host build wrote into the Android configuration's out_dir" buildhost.log +hostMinplat=$(cat "$hostFile" 2>/dev/null || true) [ -z "$hostMinplat" ] \ || fail "MCPP_TARGET_MIN_PLATFORM_VERSION was '$hostMinplat', not empty, on the host" buildhost.log echo "MCPP_TARGET_MIN_PLATFORM_VERSION is empty on the host OK" diff --git a/tests/e2e/810_two_packages_deploy_the_same_bytes_to_one_name.sh b/tests/e2e/810_two_packages_deploy_the_same_bytes_to_one_name.sh index d9b5ac0ad..da4c1c696 100755 --- a/tests/e2e/810_two_packages_deploy_the_same_bytes_to_one_name.sh +++ b/tests/e2e/810_two_packages_deploy_the_same_bytes_to_one_name.sh @@ -137,16 +137,17 @@ STAGE_LINES=$(grep -c "^build .*shared/shared\.bin : stage_file" "$G" || true) [ "$STAGE_LINES" -eq 1 ] \ || fail "expected exactly one stage_file edge for shared/shared.bin, found $STAGE_LINES" "$G" STAGE_LINE=$(grep "^build .*shared/shared\.bin : stage_file" "$G") -# The dependency's own build.mcpp runs in the CONSUMING project's tree -# (`target/.build-mcpp/deps/dep@/...`, 111_dep_build_mcpp.sh), so its -# source is distinguished by that path segment; the app's own source is the -# sibling under `target/.build-mcpp/out/...` with no `deps/` segment. +# Each package's build program writes in the CONSUMING project's tree, in +# its own directory of this configuration's output +# (`target/.build-mcpp/out///...`, mcpp 2026.10.10.1+, +# 111_dep_build_mcpp.sh), so the two sources are told apart by the package +# segment. INPUTS=$(echo "$STAGE_LINE" | sed 's/^.*: stage_file //') [ "$(echo "$INPUTS" | wc -w)" -eq 2 ] \ || fail "expected exactly two inputs on the stage_file edge, got: $INPUTS" "$G" -echo "$INPUTS" | tr ' ' '\n' | grep -qF "deps/dep@" \ +echo "$INPUTS" | tr ' ' '\n' | grep -q "\.build-mcpp/out/[0-9a-f]*/[a-z.]*dep/gen/shared\.bin$" \ || fail "the edge does not list the dependency's source" "$G" -echo "$INPUTS" | tr ' ' '\n' | grep -v "deps/dep@" | grep -q "\.build-mcpp/out/gen/shared\.bin$" \ +echo "$INPUTS" | tr ' ' '\n' | grep -q "\.build-mcpp/out/[0-9a-f]*/app/gen/shared\.bin$" \ || fail "the edge does not also list the app's own source" "$G" echo "PASS: one stage_file edge lists both sources" @@ -163,9 +164,9 @@ set -e [ "$rc" -ne 0 ] || fail "different bytes for one destination were accepted" collision.log grep -q "disagree" collision.log \ || fail "the refusal does not say the sources disagree" collision.log -grep -qF "deps/dep@" collision.log \ +grep -q '\.build-mcpp/out/[0-9a-f]*/[a-z.]*dep/gen/shared\.bin' collision.log \ || fail "the refusal does not name the dependency's source" collision.log -grep -q '\.build-mcpp/out/gen/shared\.bin' collision.log \ +grep -q '\.build-mcpp/out/[0-9a-f]*/app/gen/shared\.bin' collision.log \ || fail "the refusal does not name the app's own source" collision.log grep -q "shared.bin" collision.log || fail "the refusal does not name the destination" collision.log echo "PASS: different bytes are refused at build time, naming both sources" diff --git a/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh b/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh index af4f7b7a3..0df6865a2 100644 --- a/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh +++ b/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh @@ -132,8 +132,12 @@ int main() { EOF done +# A member's build-program output directory: one per configuration and +# package, below the workspace root (mcpp 2026.10.10.1+). Every command here +# builds the one configuration the pack asks for. +outdir() { ls -d target/.build-mcpp/out/*/"$1" 2>/dev/null | head -1; } # The lines a member's program appended to its log, all of them. -runs() { cat "$1/target/.build-mcpp/out/runs.log" 2>/dev/null; } +runs() { cat "$(outdir "$1")/runs.log" 2>/dev/null; } count() { runs "$1" | grep -c "$2" || true; } stage_of() { ls -d "$1"/target/dist/"$1"-0.1.0-* | grep -v -e '\.stage-manifest$' -e '\.tar\.gz$' | head -1; } @@ -144,11 +148,11 @@ objects() { awk -F'\t' '$4 ~ /\.o$/' "$1/.ninja_log" | wc -l | tr -d ' '; } # `core` is not a program, so it is not packed, and its program ran once in all: # in the pass that asks for no format, and never again for a member whose answer # does not depend on the request. -[ "$(count core '')" = 1 ] || fail "A: core's program ran $(count core '') times, not once" core/target/.build-mcpp/out/runs.log +[ "$(count core '')" = 1 ] || fail "A: core's program ran $(count core '') times, not once" $(outdir core)/runs.log for m in cli gui; do - [ "$(count $m '')" = 2 ] || fail "A: $m's program ran $(count $m '') times, not once for each pass" $m/target/.build-mcpp/out/runs.log - [ "$(count $m '^format= stage=$')" = 1 ] || fail "A: $m's first pass was told a request" $m/target/.build-mcpp/out/runs.log - [ "$(count $m '^format=zap stage=/')" = 1 ] || fail "A: $m's second pass was not told its format and stage" $m/target/.build-mcpp/out/runs.log + [ "$(count $m '')" = 2 ] || fail "A: $m's program ran $(count $m '') times, not once for each pass" $(outdir $m)/runs.log + [ "$(count $m '^format= stage=$')" = 1 ] || fail "A: $m's first pass was told a request" $(outdir $m)/runs.log + [ "$(count $m '^format=zap stage=/')" = 1 ] || fail "A: $m's second pass was not told its format and stage" $(outdir $m)/runs.log done grep -q "Packed .*cli.zap" a.log || fail "A: cli's distributable was not reported" a.log grep -q "Packed .*gui.zap" a.log || fail "A: gui's distributable was not reported" a.log @@ -159,15 +163,15 @@ for m in cli gui; do # The two spellings of one directory are compared as directories: a # temporary directory may be reached through a symbolic link. want="$(cd "$(stage_of $m)" && pwd -P)" - saw="$(cat $m/target/.build-mcpp/out/seen-stage.txt)" + saw="$(cat $(outdir $m)/seen-stage.txt)" [ -d "$saw" ] && [ "$(cd "$saw" && pwd -P)" = "$want" ] \ || fail "D: $m's program read '$saw', and its tree is '$want'" a.log - grep -qx "member=$m" $m/target/.build-mcpp/out/$m.zap || fail "D: $m's distributable was made for another member" $m/target/.build-mcpp/out/$m.zap - grep -qx "./bin/$m" $m/target/.build-mcpp/out/$m.zap || fail "D: $m's tree did not hold $m's program" $m/target/.build-mcpp/out/$m.zap + grep -qx "member=$m" $(outdir $m)/$m.zap || fail "D: $m's distributable was made for another member" $(outdir $m)/$m.zap + grep -qx "./bin/$m" $(outdir $m)/$m.zap || fail "D: $m's tree did not hold $m's program" $(outdir $m)/$m.zap done -[ "$(cat cli/target/.build-mcpp/out/seen-stage.txt)" != "$(cat gui/target/.build-mcpp/out/seen-stage.txt)" ] \ +[ "$(cat $(outdir cli)/seen-stage.txt)" != "$(cat $(outdir gui)/seen-stage.txt)" ] \ || fail "D: two members were told one staged tree" -grep -q "gui" cli/target/.build-mcpp/out/cli.zap && fail "D: cli's tree holds gui's program" cli/target/.build-mcpp/out/cli.zap +grep -q "gui" $(outdir cli)/cli.zap && fail "D: cli's tree holds gui's program" $(outdir cli)/cli.zap echo "ok: D, each member's program reads its own staged tree" # ── B ──────────────────────────────────────────────────────────────────────── @@ -176,11 +180,11 @@ for m in cli gui; do mkdir multi/$m single/$m cp -R "$(stage_of $m)" multi/$m/tree cp "$(stage_of $m).stage-manifest" multi/$m/stage-manifest - cp $m/target/.build-mcpp/out/$m.zap multi/$m/dist + cp $(outdir $m)/$m.zap multi/$m/dist "$MCPP" pack -p $m --format zap > b-$m.log 2>&1 || fail "B: mcpp pack -p $m failed" b-$m.log cp -R "$(stage_of $m)" single/$m/tree cp "$(stage_of $m).stage-manifest" single/$m/stage-manifest - cp $m/target/.build-mcpp/out/$m.zap single/$m/dist + cp $(outdir $m)/$m.zap single/$m/dist diff -r multi/$m single/$m > b-$m.diff || fail "B: $m's packed tree differs from the one mcpp pack -p $m makes" b-$m.diff done echo "ok: B, each member's tree and distributable are those of mcpp pack -p " diff --git a/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh b/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh index 153aa40c6..25279811b 100644 --- a/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh +++ b/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh @@ -287,7 +287,7 @@ set -e [ "$rc" -ne 0 ] || fail "K: a member that claimed nothing was reported as packed" k.log grep -q "member 'mute': no action claimed --format 'zap'" k.log \ || fail "K: the failure does not name the member" k.log -[ -n "$(find good -name good.zap | head -1)" ] || fail "K: the member that claimed the format was not packed" k.log +[ -n "$(find target/.build-mcpp/out -path '*/good/good.zap' | head -1)" ] || fail "K: the member that claimed the format was not packed" k.log grep -q "Packed .*good.zap" k.log || fail "K: the member that was packed was not reported" k.log echo "ok: K, a member that claimed nothing fails alone, by name, and the others are packed" diff --git a/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh b/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh index a797dbd54..8ca4e9258 100644 --- a/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh +++ b/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh @@ -94,7 +94,9 @@ member a tool member b tool member c ctool c++20 -zap() { echo "$1/target/.build-mcpp/out/$1.zap"; } +# A member's distributable, in its build-program output directory: one per +# configuration and package, below the workspace root (mcpp 2026.10.10.1+). +zap() { ls -d target/.build-mcpp/out/*/"$1"/"$1".zap 2>/dev/null | head -1; } # ── A ──────────────────────────────────────────────────────────────────────── "$MCPP" pack --workspace --format zap > a.log 2>&1 || fail "A: the pack failed" a.log From 0f9c782633cb5506d7bed2fbab35b3ab69aca7d3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 08:28:26 +0900 Subject: [PATCH 24/32] fix(index): index list and the router read the same effective indices on a workspace root `mcpp index list` lists what resolution searches: a member's own [indices], [workspace.indices], and a root's own table by position. effective_indices recognises a workspace root by its own [workspace] table (e2e 44). --- src/pm/index_management.cppm | 15 ++++++++------- src/pm/index_route.cppm | 5 +++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pm/index_management.cppm b/src/pm/index_management.cppm index 48889906c..d06d4eb96 100644 --- a/src/pm/index_management.cppm +++ b/src/pm/index_management.cppm @@ -16,6 +16,7 @@ import mcpp.lockfile; import mcpp.manifest; import mcpp.platform; import mcpp.project; +import mcpp.pm.index_route; import mcpp.ui; import mcpp.xlings; @@ -95,15 +96,15 @@ export int index_list() { // Show project-level custom indices from mcpp.toml [indices]. auto root = mcpp::project::find_manifest_root(std::filesystem::current_path()); if (root) { - // The effective manifest: a member inherits the workspace's - // `[indices]` (#690, W4). - std::optional m; - if (auto effective = mcpp::project::load_effective_manifest(*root)) - m = std::move(effective->manifest); - if (m && !m->indices.empty()) { + // The effective `[indices]`: a member's own, `[workspace.indices]`, and + // a workspace root's own read by position (#690; SPEC-004 §9.12, §9.13). + // One function with the index router, so the list names what + // resolution searches. + const auto indices = mcpp::pm::effective_indices(*root); + if (!indices.empty()) { std::println(""); std::println("Project indices (mcpp.toml):"); - for (auto& [name, spec] : m->indices) { + for (auto& [name, spec] : indices) { if (spec.is_local()) { std::println(" {:<15} {} (local path)", name, spec.path.string()); } else { diff --git a/src/pm/index_route.cppm b/src/pm/index_route.cppm index a13e22c42..4f73eb8af 100644 --- a/src/pm/index_route.cppm +++ b/src/pm/index_route.cppm @@ -256,11 +256,11 @@ IndexMap effective_indices(const std::filesystem::path& root) { // by name; the root's own [indices] by position when it has none of the // former), whose relative `path` is anchored at the ROOT (#224). auto wsRoot = mcpp::project::find_workspace_root(root); - if (wsRoot.empty()) return m->indices; // The workspace root itself: its own [indices], and what it gives every // member, its root package included. std::error_code ec; - if (std::filesystem::equivalent(wsRoot, root, ec)) { + if (m->workspace.present || (!wsRoot.empty() && std::filesystem::equivalent(wsRoot, root, ec))) { + if (m->workspace.present) wsRoot = root; if (m->layer) for (auto const& [name, idx] : m->layer->indices) { auto [it, inserted] = m->indices.try_emplace(name, idx); @@ -269,6 +269,7 @@ IndexMap effective_indices(const std::filesystem::path& root) { } return m->indices; } + if (wsRoot.empty()) return m->indices; auto ws = mcpp::manifest::load(wsRoot / "mcpp.toml"); if (!ws || !ws->workspace.present) return m->indices; mcpp::project::inherit_workspace_indices(*m, *ws, wsRoot); From 42e9ca538f3dbdd2c276d0dbf408dc74938ba169 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 08:30:38 +0900 Subject: [PATCH 25/32] ci: the offscreen example's shaders are looked for in the outDir resolution.json records .github/tools/out_dir.sh prints the root package's outDir of the newest build; the Linux, macOS and Windows graphics steps read the spirv headers there instead of naming target/.build-mcpp/out. --- .github/tools/out_dir.sh | 15 +++++++++++++++ .github/workflows/ci-linux.yml | 2 +- .github/workflows/ci-macos.yml | 2 +- .github/workflows/ci-windows.yml | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100755 .github/tools/out_dir.sh diff --git a/.github/tools/out_dir.sh b/.github/tools/out_dir.sh new file mode 100755 index 000000000..2ac9998f0 --- /dev/null +++ b/.github/tools/out_dir.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Prints where `mcpp::out_dir()` pointed for the root package in the newest +# build of the package in the current directory: its `outDir` in +# resolution.json. Each configuration has its own output directory, so a check +# reads it instead of naming target/.build-mcpp/out. +r=$(ls -t target/*/*/resolution.json 2>/dev/null | head -1) +[ -n "$r" ] || { echo "out_dir.sh: no resolution.json under target/" >&2; exit 1; } +py=$(command -v python3 || command -v python) +d=$("$py" -c 'import json, sys +for p in json.load(open(sys.argv[1])).get("graph", {}).get("packages", []): + if p.get("root"): + print(p.get("outDir", "")) + break' "$r") +[ -n "$d" ] || { echo "out_dir.sh: $r records no outDir for the root package" >&2; exit 1; } +echo "$d" diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 797969e07..ac180b1c3 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -349,8 +349,8 @@ jobs: # THE SHADERS ARE THE FIRST ASSERTION, and both of them: a rule that # compiled only the first source would leave the second header absent # and everything after this would still pass. + d="$(sh "$GITHUB_WORKSPACE/.github/tools/out_dir.sh")/spirv" for f in triangle_vert triangle_frag; do - d="target/.build-mcpp/out/spirv" test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; } # Either file: which of the two carries the words is a property of # the shader compiler the rule chose, not of the shader. See the diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index e29389345..668c0d47c 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -374,8 +374,8 @@ jobs: # a neighbouring step happened to select makes this step's subject # depend on step order, which is not a property anybody reads. "/tmp/mcpp-fresh" build --toolchain "llvm@${MCPP_LLVM_VER}" + d="$(sh "$GITHUB_WORKSPACE/.github/tools/out_dir.sh")/spirv" for f in triangle_vert triangle_frag; do - d="target/.build-mcpp/out/spirv" test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; } # THE MAGIC IS NOT ALWAYS IN THE HEADER, AND THAT IS THE POINT OF # THIS JOB. The rule chooses the shader compiler this platform diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index f1230ebfc..e4f7d0457 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -432,8 +432,8 @@ jobs: # a neighbouring step happened to select makes this step's subject # depend on step order, which is not a property anybody reads. "$MCPP_SELF" build --toolchain "llvm@23.1.3" + d="$(sh "$GITHUB_WORKSPACE/.github/tools/out_dir.sh")/spirv" for f in triangle_vert triangle_frag; do - d="target/.build-mcpp/out/spirv" test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; } # THE MAGIC IS NOT ALWAYS IN THE HEADER, AND THAT IS THE POINT OF # THIS JOB. The rule chooses the shader compiler this platform From 879a3af5444648cdd539cd45831d4b31fa9e4997 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 08:37:36 +0900 Subject: [PATCH 26/32] fix(fetch): a failed install names the first address's failure, not only the compat retry's A bare or default-namespace dependency is retried as compat: and compat.; those fail as "not found" whatever the cause was. The diagnostic now carries the first address's error ahead of the last one. --- src/build/prepare/graph_load.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/build/prepare/graph_load.cpp b/src/build/prepare/graph_load.cpp index 57bb276bc..544faaacf 100644 --- a/src/build/prepare/graph_load.cpp +++ b/src/build/prepare/graph_load.cpp @@ -1025,6 +1025,15 @@ step4a_load_version_dep_fetch(PrepareState& state, LoadVersionDepCtx& ctx) { mcpp::platform::env::ScopedEnv hookVar4(hookVars.at(4).first, hookVars.at(4).second); mcpp::platform::env::ScopedEnv hookVar5(hookVars.at(5).first, hookVars.at(5).second); auto r = install_one(target); + // The first address's failure is the one about the package; a + // compat spelling tried after it fails as "not found" whatever + // the cause was. + std::string firstError; + if (r && r->exitCode != 0) { + firstError = capturedChildError; + if (r->error) + firstError += (firstError.empty() ? "" : "; ") + r->error->message; + } if (r && r->exitCode != 0 && (ns.empty() || ns == mcpp::pm::kDefaultNamespace)) { // Compat retry for a bare/default-namespace request whose @@ -1061,6 +1070,10 @@ step4a_load_version_dep_fetch(PrepareState& state, LoadVersionDepCtx& ctx) { / ".xlings.json"; auto indexRepos = mcpp::pm::read_seeded_index_repos(xlingsJson); std::string childErr = capturedChildError; + if (attempted.size() > 1 && !firstError.empty()) { + if (!childErr.empty()) childErr += "; "; + childErr += std::format("{}: {}", attempted.front(), firstError); + } if (r->error) { if (!childErr.empty()) childErr += "; "; childErr += r->error->message; From 26943c7ad2f528e8cd92d7ec3936f84caf93f15e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 09:37:51 +0900 Subject: [PATCH 27/32] test(e2e): an inherited subos comes with the ledger that registers its assets _inherit_toolchain.sh linked the user's subos into the test home but not the version ledger that registers the assets its workspace projects. xlings checks those before it changes the sysroot, so every install that touched it was refused ("Scrt1.o@glibc-2.44.3: installed asset registration missing"): e2e 32, 33 and 40 on a runner whose ~/.mcpp held no mcpplibs.cmdline. --- tests/e2e/_inherit_toolchain.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/e2e/_inherit_toolchain.sh b/tests/e2e/_inherit_toolchain.sh index ba3eb9bd2..09329b975 100644 --- a/tests/e2e/_inherit_toolchain.sh +++ b/tests/e2e/_inherit_toolchain.sh @@ -83,6 +83,13 @@ if [[ "${MCPP_INHERIT_SUBOS:-1}" != "0" && -d "$USER_MCPP/registry/subos" ]]; th [[ -e "$MCPP_HOME/registry/subos" ]] \ || ln -sf "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" 2>/dev/null \ || cp -r "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" + # The subos's workspace names the assets it projects (glibc's Scrt1.o, + # ...), and xlings checks each one against the home's version ledger + # before it changes the sysroot. A ledger without them refuses every + # install that touches the sysroot ("installed asset registration + # missing"), so the inherited subos comes with a copy of the ledger. + [[ -e "$MCPP_HOME/registry/.xlings.json" || ! -f "$USER_MCPP/registry/.xlings.json" ]] \ + || cp -f "$USER_MCPP/registry/.xlings.json" "$MCPP_HOME/registry/.xlings.json" fi if [[ "${MCPP_INHERIT_CONFIG:-1}" != "0" && -f "$USER_MCPP/config.toml" ]]; then cp -f "$USER_MCPP/config.toml" "$MCPP_HOME/config.toml" 2>/dev/null || true From ab9dc24ddf8a752ef0bcfb4479e7608092c96a80 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 09:37:51 +0900 Subject: [PATCH 28/32] test(e2e): a tool read from build.ninja is run as a command line For a compiler without its own library search path the binding starts with `env LD_LIBRARY_PATH=...` (D29); the resource round-trip (mingw-cross) and e2e 188 ran the binding as one program name. --- tests/e2e/188_build_actions.sh | 4 +++- tests/e2e/_windows_resources_body.sh | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/e2e/188_build_actions.sh b/tests/e2e/188_build_actions.sh index ecf1f8a38..4cb79a844 100755 --- a/tests/e2e/188_build_actions.sh +++ b/tests/e2e/188_build_actions.sh @@ -298,6 +298,8 @@ EOF printf 'int main() { return 0; }\n' > src/main.cpp "$MCPP" build > o0.log 2>&1 || { cat o0.log; echo "FAIL: probe build failed"; exit 1; } OBJ_NINJA=$(find target -name build.ninja | head -1) +# A command line, written into mkobj.sh as one: it may start with +# `env LD_LIBRARY_PATH=...` for a compiler without its own search path. OBJ_CXX=$(sed -n 's/^cxx *= *//p' "$OBJ_NINJA" | head -1) [ -n "$OBJ_CXX" ] || { cat "$OBJ_NINJA"; echo "FAIL: could not read the compiler out of build.ninja"; exit 1; } @@ -311,7 +313,7 @@ printf 'extern "C" int blob_value() { return 7; }\n' > blob.cpp cat > mkobj.sh < build.mcpp <<'EOF' diff --git a/tests/e2e/_windows_resources_body.sh b/tests/e2e/_windows_resources_body.sh index 41c6de26e..b7e3b2a36 100644 --- a/tests/e2e/_windows_resources_body.sh +++ b/tests/e2e/_windows_resources_body.sh @@ -119,7 +119,9 @@ esac # The rc tool comes out of build.ninja rather than off PATH: mcpp resolved it # payload-relative on purpose (a bare `windres` on PATH is an xlings shim), so # asking PATH here would inspect the artifact with a different tool than the one -# that built it. +# that built it. The binding is a command line: for a compiler without its own +# library search path it starts with `env LD_LIBRARY_PATH=...`, so it is run by +# a shell. RC_TOOL=$(sed -n 's/^rc *= *//p' "$BUILD_DIR/build.ninja" | head -1) [ -n "$RC_TOOL" ] || fail "no 'rc =' binding in build.ninja" "$BUILD_DIR/build.ninja" @@ -138,7 +140,7 @@ version_name_is() { # Route 1 — windres back to rc SOURCE. Readable, and no PE parser needed. if [ -n "$RC_TOOL" ] \ - && "$RC_TOOL" -J coff -O rc -i "$1" -o "rt.$2.rc" 2>"rt.$2.err" \ + && sh -c "$RC_TOOL -J coff -O rc -i \"\$1\" -o \"\$2\"" rc "$1" "rt.$2.rc" 2>"rt.$2.err" \ && grep -qiE 'VERSIONINFO' "rt.$2.rc"; then grep -qE "$_want" "rt.$2.rc" \ || fail "expected the version resource to be named by $3 in $1" "rt.$2.rc" From 21936a0a07a8b4f17c3287d9b30649cc7225e6d1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 09:48:38 +0900 Subject: [PATCH 29/32] chore: drop two build logs committed by mistake --- build.log | 1 - release.log | 1 - 2 files changed, 2 deletions(-) delete mode 100644 build.log delete mode 100644 release.log diff --git a/build.log b/build.log deleted file mode 100644 index b763ce83d..000000000 --- a/build.log +++ /dev/null @@ -1 +0,0 @@ -/bin/bash: line 239: : command not found diff --git a/release.log b/release.log deleted file mode 100644 index 6e3855ba1..000000000 --- a/release.log +++ /dev/null @@ -1 +0,0 @@ -/bin/bash: line 253: : command not found From cdb8c9a6e5288b1ed8f9a56d02114dfe7688c3d9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 09:48:48 +0900 Subject: [PATCH 30/32] docs(changelog): the install diagnostic, index list and the emit reuse --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f839243..8531adc8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,7 +82,13 @@ replaced. - Resolve the shell an argv-launched command string runs in (`/bin/sh`, else `/system/bin/sh`, else `$PREFIX/bin/sh`). -- Report the first error of an xlings install, which is its cause. +- Report the first error of an xlings install, which is its cause, and the + failure of a dependency's own address ahead of the `compat` spellings tried + after it. +- List in `mcpp index list` the indices resolution searches on a workspace + root and its members: `[workspace.indices]` and the root's own `[indices]`. +- Run a build program of a workspace member once for a build and + `mcpp emit build-database`: its output directory is beside its cache. ## [2026.10.8.1] - 2026-10-09 From 3a7748451a0e817dc70327a038724a77585ea936 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 10:10:03 +0900 Subject: [PATCH 31/32] Revert "test(e2e): an inherited subos comes with the ledger that registers its assets" This reverts commit 26943c7ad2f528e8cd92d7ec3936f84caf93f15e. --- tests/e2e/_inherit_toolchain.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/e2e/_inherit_toolchain.sh b/tests/e2e/_inherit_toolchain.sh index 09329b975..ba3eb9bd2 100644 --- a/tests/e2e/_inherit_toolchain.sh +++ b/tests/e2e/_inherit_toolchain.sh @@ -83,13 +83,6 @@ if [[ "${MCPP_INHERIT_SUBOS:-1}" != "0" && -d "$USER_MCPP/registry/subos" ]]; th [[ -e "$MCPP_HOME/registry/subos" ]] \ || ln -sf "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" 2>/dev/null \ || cp -r "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" - # The subos's workspace names the assets it projects (glibc's Scrt1.o, - # ...), and xlings checks each one against the home's version ledger - # before it changes the sysroot. A ledger without them refuses every - # install that touches the sysroot ("installed asset registration - # missing"), so the inherited subos comes with a copy of the ledger. - [[ -e "$MCPP_HOME/registry/.xlings.json" || ! -f "$USER_MCPP/registry/.xlings.json" ]] \ - || cp -f "$USER_MCPP/registry/.xlings.json" "$MCPP_HOME/registry/.xlings.json" fi if [[ "${MCPP_INHERIT_CONFIG:-1}" != "0" && -f "$USER_MCPP/config.toml" ]]; then cp -f "$USER_MCPP/config.toml" "$MCPP_HOME/config.toml" 2>/dev/null || true From 511ba6efa1a64d69d13fdca86e789020bae25b3c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 10 Oct 2026 10:10:57 +0900 Subject: [PATCH 32/32] ci(e2e): the index payloads e2e 32, 33 and 40 link are installed with the suite's toolchains The tests link mcpplibs.cmdline from ~/.mcpp; installed in their own home instead, the install is refused because the inherited subos projects assets that home's ledger does not register. A warm sandbox held them by accident; the Linux and macOS e2e jobs now install them, as they do the toolchains. --- .github/workflows/ci-linux-e2e.yml | 6 ++++++ .github/workflows/ci-macos-e2e.yml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index e01246a16..f038ef47c 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -70,6 +70,12 @@ jobs: "$MCPP" toolchain install llvm 23.1.3 "$MCPP" toolchain install mingw-cross 16.1.0 XLINGS_HOME="$MCPP_HOME/registry" "$MCPP_VENDORED_XLINGS" install xim:nasm -y + # The index payloads e2e 32, 33 and 40 link from this home into their + # own (_inherit_toolchain.sh). Installed in a test home instead, they + # are refused: the inherited subos projects assets that home's ledger + # does not register. A warm sandbox used to hold them by accident. + XLINGS_HOME="$MCPP_HOME/registry" "$MCPP_VENDORED_XLINGS" install \ + mcpplibs:cmdline@0.0.1 mcpplibs:cmdline@0.0.2 -y - name: E2E suite # About twice the shard's budget of ten minutes (R4): reached only by a diff --git a/.github/workflows/ci-macos-e2e.yml b/.github/workflows/ci-macos-e2e.yml index 70f7ba067..5428ed881 100644 --- a/.github/workflows/ci-macos-e2e.yml +++ b/.github/workflows/ci-macos-e2e.yml @@ -79,6 +79,10 @@ jobs: "$MCPP" self config # macOS default toolchain is LLVM "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" + # The index payloads e2e 32, 33 and 40 link from this home into their + # own (_inherit_toolchain.sh); see ci-linux-e2e.yml. + XLINGS_HOME="$HOME/.mcpp/registry" "$MCPP_VENDORED_XLINGS" install \ + mcpplibs:cmdline@0.0.1 mcpplibs:cmdline@0.0.2 -y echo "MCPP=$MCPP" >> "$GITHUB_ENV" set -o pipefail bash tests/e2e/run_all.sh 2>&1 | tee "$RUNNER_TEMP/e2e-suite.log"