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..47de90515 --- /dev/null +++ b/.agents/docs/2026-10-10-one-source-one-meaning-design.md @@ -0,0 +1,800 @@ +--- +subject: design +status: landed +--- + +# 一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790) + +- 日期: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.]` 标量清单仍由已有的测试守护。`.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 及以上。 +- 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 在 + `ci-fresh-install.yml`(发布后),发布前在探测 PR #791 上以候选包运行。 + +**修订 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..525851342 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) — 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 @@ -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) — 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/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/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/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/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/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 557cf26a6..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 @@ -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 }}) @@ -412,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..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 @@ -359,7 +365,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/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-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" 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 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/.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/.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/CHANGELOG.md b/CHANGELOG.md index da1616b05..8531adc8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,92 @@ > 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. +- 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 + +- 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, 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 ### Changed diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index b7c511921..ed0edb8c8 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: @@ -885,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 @@ -1871,6 +1881,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/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/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..659917b34 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 的预构建、 模块扫描与每一个翻译单元,对根和对每个依赖一视同仁。依赖包自己声明的 @@ -828,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 @@ -1751,6 +1761,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/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/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` 解析,而非包根。 ### 既独立构建又被当依赖的库:发绝对路径 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/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/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/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 a213459cd..a6ba5605b 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,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 626ebb2e1..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; @@ -198,6 +199,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 +882,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 +895,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 +950,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 +964,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 +1117,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()) @@ -2921,24 +2954,10 @@ std::expected parse_string(std::string_view content, // 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; @@ -4023,11 +4042,7 @@ std::expected parse_string(std::string_view content, // 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) @@ -4436,6 +4451,170 @@ 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 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()) { + const auto shared = shared_conditional_build_keys(); + if (std::ranges::find(shared, bk) != shared.end()) continue; + std::string supported; + 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), + 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.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) { + // 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], " + "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 +4704,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..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 @@ -324,6 +329,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 +1813,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. @@ -1852,8 +1858,12 @@ 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.X]` tables the root writes (`toolchain`, `indices`, + // `profile`, `target`, `xlings`); what they say is `Manifest::layer`. + std::vector layerTables; }; // `[hooks]` — project build lifecycle commands (#496). @@ -1944,6 +1954,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 +2056,24 @@ 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`: + // 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/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/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/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/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/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 diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index b8286f442..14cdd8c84 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 @@ -1493,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/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 1267d85da..bafbe1325 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1482,7 +1482,28 @@ 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 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). 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 = [&]() -> 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 // launcher there is not one. @@ -1509,7 +1530,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)); } @@ -1523,7 +1544,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)); @@ -1537,7 +1558,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 +1574,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"); @@ -2518,11 +2539,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 +2558,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 +2866,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 +2912,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 +2977,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()) @@ -4449,11 +4478,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 { @@ -4508,8 +4539,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); 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 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/features.cpp b/src/build/prepare/features.cpp index 8d2f294ae..d899371e8 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(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). 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..5859cf49f 100644 --- a/src/build/prepare/graph.cpp +++ b/src/build/prepare/graph.cpp @@ -2159,6 +2159,26 @@ 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 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", + 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 base / "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 +2189,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 +2207,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/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; diff --git a/src/build/prepare/manifest.cpp b/src/build/prepare/manifest.cpp index 105b0bb9a..72de01f36 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.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/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/state.cppm b/src/build/prepare/state.cppm index 7e71be3bb..ceef60222 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -621,6 +621,18 @@ 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 `base`, + // one directory per configuration -- the target, the profile, the + // toolchain, the accelerators and the features the package is built + // with -- and per package. `base` is the root that holds the program's + // cache, so a plan under `BuildOverrides::work_dir` gives a program whose + // cache is in the project the same directory a build gives it. 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}, }; - if (!forBuildProgram) return entry; + // 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; + // 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; + } std::error_code ec; auto dir = std::filesystem::absolute(state.packages[i].root, ec).lexically_normal(); @@ -1614,6 +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.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. @@ -1819,6 +1835,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 +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]; + // 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/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 8f0a817b5..ec2708355 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.begin() == "..") 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; @@ -1506,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)); } @@ -1551,14 +1592,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 @@ -1679,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/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); 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/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()}, }; } 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/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/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); 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 823d0db30..4f73eb8af 100644 --- a/src/pm/index_route.cppm +++ b/src/pm/index_route.cppm @@ -251,11 +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); + // The workspace root itself: its own [indices], and what it gives every + // member, its root package included. + std::error_code 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); + if (inserted && it->second.is_local() && it->second.path.is_relative()) + it->second.path = (wsRoot / it->second.path).lexically_normal(); + } + 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; 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/project.cppm b/src/project.cppm index 0906b2d12..9945b985c 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 { @@ -113,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? // @@ -183,44 +170,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 +229,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 +245,206 @@ 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.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.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.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 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 -// `"."`, 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 +453,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.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 +497,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 +908,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'; @@ -753,6 +931,13 @@ export std::string root_position_key(const mcpp::manifest::Manifest& m) { 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); @@ -764,7 +949,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 +971,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 +1055,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; } 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/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/src/xlings/xlings.cppm b/src/xlings/xlings.cppm index fad4945eb..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"; } @@ -1553,7 +1561,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; 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" 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/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/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 </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/773_commands_outside_the_build_read_the_effective_manifest.sh b/tests/e2e/773_commands_outside_the_build_read_the_effective_manifest.sh index 312318577..cf25c1da4 100755 --- a/tests/e2e/773_commands_outside_the_build_read_the_effective_manifest.sh +++ b/tests/e2e/773_commands_outside_the_build_read_the_effective_manifest.sh @@ -15,6 +15,10 @@ # Each check has a control at the workspace root or in a member that declares # the value itself, so a reading that never depended on inheritance cannot # pass as one that does. +# +# The root's `[toolchain]` is read by POSITION here on purpose: with +# tests/e2e/120 this is the coverage of COMPAT(workspace-position) until +# mcpp 1.0.0 (SPEC-004 §9.10); tests/e2e/893 covers `[workspace.toolchain]`. set -e TMP=$(mktemp -d) 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 diff --git a/tests/e2e/893_the_workspace_prefix_carries_the_meaning.sh b/tests/e2e/893_the_workspace_prefix_carries_the_meaning.sh new file mode 100755 index 000000000..d53e22e11 --- /dev/null +++ b/tests/e2e/893_the_workspace_prefix_carries_the_meaning.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# requires: gcc +# 893 -- the prefix carries the meaning (SPEC-004 §9.10, design 2026-10-10 §3; +# #785, #786). +# +# P1 `[workspace.target..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" 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" 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)); +} 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()); +}