From 2be9366ae35b793e6588619b1fa44f001eba1e79 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 6 Oct 2026 09:32:16 +0900 Subject: [PATCH 1/3] 2026.10.5.3: MSVC's x86 spelling, one answer to 32-bit x86, windres recognition and the manifest resource's command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - triple::parse writes MSVC's `x86` as `i686`, as `amd64` and `arm64` are written; `[target.x86-windows-msvc]` and `--target x86-windows-msvc` are the i686 row and reach clang and llvm-windres as `i686-pc-windows-msvc` (SPEC-004 1.13 §4.6). - Triple::is_x86_32 and Triple::msvc_arch answer the 32-bit x86 question for the NASM format, the windres COFF target, the MSVC toolset and redist directories, the ABI tool environment and PE export discovery; i386-i586 no longer select the x64 MSVC directory. The 32-bit host_arch is spelled `i686`. - RcTool::llvm is decided by find_rc_tool (is_llvm_windres: the name, or the file a symlink resolves to), so llvm-mingw's `-windres` receives a triple. - The build program's UTF-8 manifest resource keeps its command beside the object and is compiled again when it changes. - E2E 889 builds the `x86` spelling; unit tests for the vocabulary, the MSVC arch mapping, windres recognition and the manifest command. - Docs 04/21 in both languages, SPEC-004 1.13, CHANGELOG (including #776). Design: .agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md --- ...rch-vocabulary-and-rc-follow-ups-design.md | 196 ++++++++++++++++++ .agents/docs/README.md | 4 +- CHANGELOG.md | 38 ++++ docs/04-mcpp-toml.md | 9 + docs/21-the-target-triple.md | 7 + docs/specs/README.md | 2 +- docs/specs/manifest-semantics.md | 13 +- docs/zh/04-mcpp-toml.md | 7 + docs/zh/21-the-target-triple.md | 6 + mcpp.toml | 2 +- modules/platform/src/common.cppm | 2 +- modules/toolchain-model/src/triple.cppm | 31 ++- .../tests/test_triple_vocabulary.cpp | 34 +++ modules/versioning/src/version.cppm | 2 +- src/build/pe_exports.cppm | 5 +- src/build/prepare/toolchain_env.cpp | 6 +- src/build/resources.cppm | 54 +++-- src/toolchain/msvc.cppm | 21 +- ...indows_driver_target_and_coff_resources.sh | 9 +- tests/unit/test_build_resources.cpp | 75 ++++++- tests/unit/test_toolchain_msvc.cpp | 6 + 21 files changed, 491 insertions(+), 38 deletions(-) create mode 100644 .agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md diff --git a/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md b/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md new file mode 100644 index 000000000..ccacf8199 --- /dev/null +++ b/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md @@ -0,0 +1,196 @@ +--- +subject: design +status: active +--- + +# 2026.10.5.3 发布方案:32 位 x86 的架构词汇、资源编译器的识别与增量(#776 后续) + +- 日期:2026-10-06。状态:修订 2,review 通过(O1–O3 按建议),实现于 mcpp 2026.10.5.3。本文是 **2026.10.5.3 的统一发布方案**,所有项目放在一个发布 PR 中完成(§7)。 +- 基线:`main` @ `a0c40ec3`(已合入 #776)。 +- 来源:`.agents/reviews/2026-10-06-review-776.md` 的问题 1–5。 +- 证据: + - Linux 本地实测,工具为 LLVM 22.1.8 的 clang,以及 NDK 30.0.16248370 自带的 llvm-windres(§2.1)。 + - #776 的 CI run `37358775307`:Windows e2e 2/3 中 E2E 889 通过,用时 14.52s。 +- 不需要探测 PR。本文所有待测结论都可以在 Linux 本地测出,或者由发布 PR 自身的 Windows CI(E2E 889 的扩展)直接验证。 + +**修订 2:实施记录。** 实现与本文的差异: + +| 项 | 本文 | 实现 | 理由 | +| --- | --- | --- | --- | +| D2 | `is_x86_32()` 含 `x86` | 只含 `i386`–`i686` | `parse` 已把 `x86` 写成 `i686`,集合里再列它就是第二个回答 | +| D2 | `msvc.cppm` 两个函数改收 `Triple` | 签名不变,内部经 `msvc_arch_dir` 询问 `Triple::msvc_arch` | 调用处与单测都传 GNU 架构名;改签名不改变回答 | +| D2 | 6 处 | 另加 `mcpp.platform` 的 `host_arch`:32 位宿主由 `"x86"` 改为 `"i686"` | 它绕过 `parse` 直接成为 `host_triple()` 的架构段;mcpp 不发布 32 位宿主,无可见变化 | +| D3 | `enum class Flavour` | `RcTool::llvm`(bool)与导出的 `is_llvm_windres(path)` | `style` 已经区分 msvc 与 gnu,第三个取值只在 gnu 内部有意义;bool 不重复 `style` | +| D5 | 不改 specs | SPEC-004 升到 1.13:§4.6 陈述架构段的等同拼写 | §4.6 已规定 `[target.X]` 的查找与拼写无关,`x86` 改变了这条规则的内容 | + +--- + +## 0. 决策一览(待 review) + +| # | 问题 | 提议 | 分级 | +| --- | --- | --- | --- | +| D1 | `x86-*` 写法在 clang 和 llvm-windres 上都不可用 | `triple::parse` 把 `x86` 归一为 `i686`,与 `amd64 → x86_64`、`arm64 → aarch64` 同一机制(§2) | 无诊断 | +| D2 | 32 位 x86 的判断散落 6 处,范围互不一致 | `Triple` 上新增 `is_x86_32()` 和 `msvc_arch()`,6 处全部改用它们(§3) | 无诊断 | +| D3 | 按文件名判断是不是 llvm-windres | `find_rc_tool` 在发现工具时记下 `flavour`,判断时解析软链接;使用处不再猜(§4) | 无诊断 | +| D4 | `compile_utf8_manifest` 的增量判断不包含命令行 | 命令行写入同目录的 stamp,命令变化就重新生成(§5) | 无诊断 | +| D5 | #776 的行为没有写进文档,也没有 CHANGELOG | `docs/04` 的 `[resources]` 一节和 `docs/21` 的 arch 段补写,中英双语;CHANGELOG 在 5.3 中补记 #776(§6) | — | +| — | review 问题 5(E2E 脚本权限是 644) | **不处理**:`main` 上 584 个 e2e 文件里有 73 个是 644,`run_all.sh` 通过 bash 调用,仓库并没有“必须可执行”的约定 | — | + +待定问题:O1–O3(§8)。 + +--- + +## 1. 诊断分级 + +本次四项都是**纠正 mcpp 内部给出的错误回答**。用户写下的输入在修复前后的含义不变,只是从“得到错误产物”或“在后续阶段失败”变成正确产物,因此都不新增诊断。 + +D1 例外的地方在于它改变了一个**写法的规范形式**:`x86-windows-msvc` 的规范形式变成 `i686-windows-msvc`。先例是 `amd64`、`arm64`,它们一直被静默归一(`modules/toolchain-model/src/triple.cppm:1267`),没有 note。本方案沿用这一先例(见 O1)。 + +## 2. D1:`x86` 归一为 `i686` + +### 2.1 现象(实测) + +``` +$ clang --target=x86-pc-windows-msvc -c t.c +error: unknown target triple 'x86-pc-windows-msvc19.33.0' +$ llvm-windres --target=x86-pc-windows-msvc -O coff -o t.o t.rc +error: unknown target triple 'x86-pc-windows-msvc19.33.0' +llvm-rc: Preprocessing failed. +``` + +同一份输入写成 `i686-pc-windows-msvc` 或 `i386-pc-windows-msvc`,两个工具都接受,生成的 machine 是 `0x014c`。 + +### 2.2 原因 + +- `normalize_arch` 只归一了 `arm64` 和 `amd64`,`x86` 原样保留。 +- `Triple::llvm_triple()` 直接拼成 `x86-pc-windows-msvc`。LLVM 的 `Triple` 不认识 `x86` 这个架构名,它是 MSVC 的写法,不是 GNU 的写法。 +- 结果是 `[target.x86-windows-msvc]` 这一行在编译阶段就失败。#776 让 GNU windres 能把 `x86` 映射到 `pe-i386`,但这条路径实际上走不到。 + +### 2.3 方案 + +在 `normalize_arch` 中加一行:`if (a == "x86") return "i686";`。 + +- `[target.X]` 的查找本来就与写法无关(`src/build/prepare/toolchain.cpp:494-503`,先 parse 再比较 `str()`),所以写 `[target.x86-windows-msvc]` 的项目会自动匹配到 `--target i686-windows-msvc`,反过来也一样。 +- `i386`、`i486`、`i586` **保持原样**。它们对 clang 来说是不同的基线 CPU,不是同义词。`x86` 只是 MSVC 对“32 位 x86”的总称,Windows 上它的实际基线就是 i686。`msvc.cppm` 的 `triple_for_arch("x86")` 也已经把它映射成 `i686-pc-windows-msvc`,本方案与它一致。 +- 归一之后,下游代码里 `starts_with("x86-")` 这样的分支都不会再命中。这些分支在 D2 中一并移除。 + +### 2.4 测试 + +- 单测(`modules/toolchain-model/tests/test_triple_vocabulary.cpp`): + - `parse("x86-windows-msvc")->str() == "i686-windows-msvc"` + - `llvm_triple() == "i686-pc-windows-msvc"` + - `i386-windows-msvc` 不被归一 +- 单测(`tests/unit/test_build_resources.cpp`):`coff_target_flag(llvm-windres, "x86-windows-msvc") == "--target=i686-pc-windows-msvc"` +- E2E 889 扩展:增加一组 `[target.x86-windows-msvc]` + `--target x86-windows-msvc`,构建 C++ exe 加资源,检查 PE machine 为 332 并运行。在 2026.10.5.2 上,这一组会在第一个编译命令处失败(`unknown target triple`),满足“新 E2E 必须在上一版本上失败”。 + +## 3. D2:32 位 x86 判断收敛到 `Triple` + +### 3.1 现状:6 处判断,4 种范围 + +| 位置 | 判断方式 | 覆盖 `x86` | `i386` | `i486`/`i586` | `i686` | +| --- | --- | --- | --- | --- | --- | +| `triple.cppm:385` `nasm_format` | `arch ==` | ✅ | ✅ | ✅ | ✅ | +| `resources.cppm:171` `coff_target_flag` | `arch ==` | ✅ | ✅ | ✅ | ✅ | +| `toolchain_env.cpp:315` `msvc_arch_of` | 字符串前缀 | ✅ | ✅ | ❌ → `x64` | ✅ | +| `pe_exports.cppm:255` | 字符串前缀 | ✅ | ✅ | ❌ | ✅ | +| `msvc.cppm:1455` cl 版本探测 | `archGnu ==` | ✅ | ❌ → `x64` | ❌ → `x64` | ✅ | +| `msvc.cppm:1616` redist 目录 | `archGnu ==` | ✅ | ❌ → `x64` | ❌ → `x64` | ✅ | + +后四处都是潜在缺陷。例如 `pe_exports` 把 i586 当成非 i386,导出名的前导下划线就会处理错;`msvc.cppm` 会给 `i386-windows-msvc` 选中 x64 的 cl 和 redist 目录。这些 target 很少见,所以一直没有暴露,但它们和 #775 是同一类问题:**目标架构的同一个问题,在不同的地方得到了不同的回答。** + +### 3.2 方案 + +在 `Triple` 上新增两个成员,作为唯一的回答来源: + +```cpp +bool is_x86_32() const; // i386 / i486 / i586 / i686(x86 已由 parse 归一) +std::string_view msvc_arch() const; // "x86" | "x64" | "arm64" | ""(非 Windows 或未知时为空) +``` + +6 处全部改用这两个成员。其中 `msvc_arch_of` 和 `pe_exports` 拿到的是三元组字符串,先 `parse` 再提问,不再比较前缀。`msvc.cppm` 的两个函数接收的是 `archGnu` 字符串,改为接收 `const Triple&`,或者在调用处 parse。具体选哪种,以改动面最小为准,实施时记录在修订中。 + +### 3.3 测试 + +在 `test_triple_vocabulary.cpp` 中用一张表覆盖 `is_x86_32` 和 `msvc_arch`:x86 的 5 种写法、`x86_64`/`amd64`、`aarch64`/`arm64`、一个非 Windows 三元组。 + +## 4. D3:资源编译器的 flavour 在发现时确定 + +### 4.1 现状 + +`coff_target_flag` 用 `tool.name().find("llvm-windres")` 判断该传 triple 还是 BFD 名。两种工具的接受情况: + +| 工具 | triple | BFD 名(`pe-i386` 等) | +| --- | --- | --- | +| llvm-windres | ✅(并决定预处理用哪个 triple) | ✅(预处理退回 mingw triple) | +| GNU windres | ❌ | ✅ | + +所以把 llvm-windres 误判成 GNU windres,功能上不会出错。唯一的差别在 msvc-env target 上:预处理宏会按 mingw 来定义,而不是按 msvc。llvm-mingw 在 Linux/macOS 上把 `-windres` 做成指向 `llvm-windres` 的软链接,这种情况会被误判。 + +### 4.2 方案 + +- `RcTool` 增加 `enum class Flavour { Msvc, Binutils, Llvm }`,在 `find_rc_tool` 中一次确定。 + - msvc 风格(`rc` / `llvm-rc`)记为 `Msvc`。 + - gnu 风格中,文件名含 `llvm-windres` 的,记为 `Llvm`。 + - 否则用 `weakly_canonical` 解析软链接,**解析后**的文件名含 `llvm-windres` 或 `llvm-rc` 的,也记为 `Llvm`。 + - 其余记为 `Binutils`。 +- 现有的 `style` 字段(`"gnu"`/`"msvc"`)保留不动。它决定的是 ninja 规则的写法,读取它的地方有好几处,本方案不改这些读取点。 +- `coff_target_flag` 只读 `flavour`。 + +不起进程做 `--version` 探测,理由见 O2。 + +### 4.3 测试 + +单测(Linux/macOS 上运行,Windows 上跳过,因为没有软链接权限的保证): +- 在临时目录里放一个 `llvm-windres` 文件,再建一个 `i686-w64-mingw32-windres` 软链接指向它。 +- 断言 `find_rc_tool` 返回 `Flavour::Llvm`,`coff_target_flag` 返回 triple。 +- 断言一个普通文件 `windres` 返回 `Binutils`。 + +## 5. D4:UTF-8 manifest 的增量判断包含命令行 + +### 5.1 现状 + +`resources.cppm:337`:`if (!changed && is_regular_file(out)) return out;`。这里只比较 manifest 和 `.rc` 的文本,命令行变了(换了工具路径、加了 `--target`、升级了 mcpp)不会重新生成。build program 是 host 架构,旧产物恰好仍然正确,所以目前没有实际后果。但这违反了构建图其他部分都遵守的约定:命令是边的一部分(ninja 对 `rc_object` 边就是这样处理的)。 + +### 5.2 方案 + +先拼好 argv,再把它逐行写入 `.cmd`,沿用同一个 `write_if_changed`。三个文件中任何一个变化都会触发重新生成。只多写一个小文件,不增加进程。 + +### 5.3 测试 + +单测(非 Windows): +- 在临时目录里放一个假的 `windres` shell 脚本,它把收到的参数写进输出文件。toolchain 的 `binaryPath` 指向同一个目录。 +- 连续两次调用,第二次输出不变。 +- 把 `targetTriple` 从 `x86_64-windows-gnu` 改成 `i686-windows-gnu` 后再调用,输出内容变为带 `pe-i386` 的版本。 + +## 6. D5:文档与 CHANGELOG + +- `docs/04-mcpp-toml.md` 与 `docs/zh/04-mcpp-toml.md` 的 `[resources]` 一节,补写:gnu 风格的资源编译器按目标架构生成 COFF 对象(llvm-windres 收到三元组,GNU windres 收到 BFD 名);rc.exe / llvm-rc 生成的 `.res` 与架构无关。 +- `docs/21-the-target-triple.md` 与 `docs/zh/21-…` 的 Segments 表下,补写 arch 的接受写法:`arm64 → aarch64`、`amd64 → x86_64`、`x86 → i686`。`i386`/`i486`/`i586` 保持各自的含义。 +- `CHANGELOG.md` 新增 `## [2026.10.5.3]`: + - Fixed:#775/#776 的 driver link 与 COFF 资源;`x86-*` 写法(D1);i386–i586 在 MSVC 目录和导出名上的判断(D2)。 + - Changed:资源编译器识别(D3);manifest 增量(D4)。 +- 本次不改 specs:目标三元组的规范不在 `docs/specs/` 中,`[resources]` 的语义也没有变化。 + +## 7. 发布 PR 与任务依赖 + +``` +mcpp(一个发布 PR,release/2026.10.5.3,基于 main a0c40ec3) + D1 归一 ──→ D2 Triple 成员(依赖 D1:x86 不再出现) + D3 flavour ┐ + D4 stamp ┼─→ docs 04/21(中英)+ CHANGELOG ─→ 版本号 bump ─→ 完整 CI ─→ 自审 ─→ squash 合入 main + E2E 889 扩展┘ + ─→ release.yml ─→ xlings-res 镜像(GitHub + GitCode,必要时 gtc 补齐) + ─→ xim-pkgindex bump PR ─→ 合入 ─→ 干净 XLINGS_HOME 安装验证 +``` + +- 版本号:`mcpp.toml` 和 `modules/versioning/src/version.cppm` 改为 `2026.10.5.3`。 +- 合入前检查:`check_docs_style.sh`、`check_docs_structure.sh`、`check_modules_wiring.sh`、`check_narrow_conversions.sh`、`check_version_pins.sh`、`check_file_lengths.sh`、`check_workflow_assertions.py`、`git diff --check`、`gen_agents_index.py`。 +- E2E 889 的 x86 写法组在 2026.10.5.2 上必须失败:由 Windows CI 跑一次 `MCPP=<2026.10.5.2>` 对照。如果 CI 上不方便,就按 #776 作者的做法在 PR 描述中给出失败输出。 + +## 8. 待定问题 + +| # | 问题 | 建议 | +| --- | --- | --- | +| O1 | D1 是静默归一,还是在 parse 时拒绝 `x86` 并提示改用 `i686`? | **静默归一**。先例是 `amd64`/`arm64`;`x86` 是 MSVC 用户最自然的写法,而且含义唯一。 | +| O2 | D3 要不要对名字不明确的 windres 起进程跑 `--version` 来判断 flavour?(Windows 上的 llvm-mingw 是复制出来的 exe,不是软链接) | **不探测**。误判的后果只是预处理 triple 退回 mingw,而且只在“msvc-env target 恰好找到一个改了名的 llvm-windres”时出现;每次 prepare 多一个进程不划算。 | +| O3 | 既然 E2E 889 已经在 CI 上跑通 `i686-windows-msvc`,要不要把它登记进 known-target 表(tier `preview`)? | **不纳入 5.3**。登记会带来 target-matrix 的扫描行、覆盖门禁、README 和 docs/21 的表格,范围超出本次修复;另开 issue。目前仍可以通过 `[target.i686-windows-msvc]` 段使用。 | diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 8354c2f17..254163a0e 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 --- ``` -326 records. +327 records. ## By subject @@ -30,6 +30,7 @@ Records that declare one. Everything else is listed by date below. ### design +- [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 - [PR CI acceleration and the toolchain specification (#756, #757, #669)](2026-10-02-pr-ci-acceleration-and-the-toolchain-specification-design.md) — active @@ -117,6 +118,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-10 +- [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 - [PR CI acceleration and the toolchain specification (#756, #757, #669)](2026-10-02-pr-ci-acceleration-and-the-toolchain-specification-design.md) — active diff --git a/CHANGELOG.md b/CHANGELOG.md index 79228969b..80041230b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ > 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.5.3] - 2026-10-06 + +This release carries the fix of mcpp#775 (#776), in which an x86 Windows build +linked against the host's runtime and received an x64 resource object, and the +follow-ups of its review: MSVC's `x86` spelling, one answer to the question +"is this 32-bit x86", how LLVM's windres is recognised, and when the build +program's manifest resource is compiled again. No default toolchain changes. +The design record is +`.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md`. + +### Fixed + +- **An x86 Windows build links against x86 libraries and receives an x86 + resource object (#775, #776).** The clang driver link on a Windows host + carried no `--target`, so the driver searched the x64 MSVC libraries; windres + received no target and produced an x64 COFF object. Both links of C and C++ + carry the target now; LLVM's windres receives the target triple and GNU + windres the BFD format (`pe-i386`, `pe-x86-64`). E2E 889. +- **`x86` names the i686 target.** `[target.x86-windows-msvc]` and + `--target x86-windows-msvc` reached clang as `x86-pc-windows-msvc`, which no + LLVM tool accepts. `x86` is written `i686` when a triple is read, as `amd64` + and `arm64` are written `x86_64` and `aarch64`; a section and a `--target` + match whichever spelling either uses (SPEC-004 §4.6). E2E 889. +- **`i386`, `i486` and `i586` on the MSVC ABI select the x86 toolset.** The + MSVC directory of the target's compiler and redistributable runtime was + `x64` for them, and export discovery read the LLVM bitcode of their DLLs + without removing the leading underscore that 32-bit x86 adds to a C name. Every such question is answered by the triple + module now (`Triple::is_x86_32`, `Triple::msvc_arch`). + +### Changed + +- **LLVM's windres is recognised where it is found**, by its name or by the + file its symlink resolves to, so llvm-mingw's `-windres` receives a + target triple rather than a BFD format name. +- **The build program's UTF-8 manifest resource is compiled again when its + command changes**, as every edge of the build graph is; before, only the + manifest and the script were compared. + ## [2026.10.5.2] - 2026-10-05 This release closes mcpp#771 and mcpp#770 and the follow-ups of mcpp#768: the diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index cc61a05a1..b7c511921 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -1751,6 +1751,15 @@ generates the resource script automatically. **not** need (and cannot use) a `cfg(windows)` predicate — write it once, unconditionally. +**The resource object is the target's.** rc.exe and llvm-rc produce a `.res`, +which carries no machine and is linked as it is. windres produces a COFF object, +which does: mcpp gives LLVM's windres the target triple and GNU windres the BFD +format (`pe-i386`, `pe-x86-64`), so an `i686` image receives an i386 object on an +x86_64 machine (2026.10.5.3+; LLVM's windres is recognised by its name or by the +file its symlink resolves to). The resource that gives a build program the +UTF-8 code page is compiled the same way for the host, and is compiled again +when its command changes. + **A declared file that does not exist fails the build — on every target.** A resource is a build input like a source file; mcpp will not quietly ship a binary without it. Validation is deliberately *not* PE-gated: whether a path diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 5dac8f4c2..b8cada43b 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -42,6 +42,13 @@ and the build reports what it resolved. | `os` | operating system, or `none` | `linux`, `windows`, `macos`, `ios`, `emscripten`, `none` | | `env` | see below — it is a different axis per platform | `gnu`, `musl`, `msvc`, `android`, `elf` | +The architecture is spelled the GNU way. Three other spellings are accepted and +written in it when the triple is read, so a `[target.]` section and a +`--target` match whichever was used: `amd64` is `x86_64`, `arm64` is `aarch64`, +and MSVC's `x86` is `i686` (2026.10.5.3+; no LLVM tool accepts `x86` as an +architecture). `i386`, `i486` and `i586` keep their own meaning, because each +selects a different baseline CPU. + The third segment is the one that repays attention, because it does not name the same kind of thing everywhere: diff --git a/docs/specs/README.md b/docs/specs/README.md index 80f388630..fefc9517c 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -33,7 +33,7 @@ | [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.12 | 2026-10-05 | 条件化形状 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 | +| [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-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.6 | 2026-10-02 | 逐条标注;已实现条款 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 | | [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 | diff --git a/docs/specs/manifest-semantics.md b/docs/specs/manifest-semantics.md index e8fc3953d..5aa3832d3 100644 --- a/docs/specs/manifest-semantics.md +++ b/docs/specs/manifest-semantics.md @@ -5,11 +5,11 @@ | **规范编号** | SPEC-004 | | **标题** | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | | **状态** | **草案(Draft)** | -| **版本** | 1.12 | -| **最后修改** | 2026-10-05 | +| **版本** | 1.13 | +| **最后修改** | 2026-10-06 | | **最低实现版本** | 条件化形状: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-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` | | **相关使用文档** | [docs/04 —— mcpp.toml 字段参考](../04-mcpp-toml.md) | ## 规范用语 @@ -299,7 +299,11 @@ feature-deps feature-xlings ← 限定词是门 `x86_64-unknown-linux-gnu` 找到 `[target.x86_64-linux-gnu]`。命令行的 `--toolchain` (`MCPP_TOOLCHAIN`)仍优先于行的 `toolchain`。 -**状态:已实现**(mcpp 2026.9.27.1,mcpp#704)。 +该比较中,架构段的 `amd64`、`arm64` 与 `x86` 依次与 `x86_64`、`aarch64` 与 `i686` 相同: +`[target.x86-windows-msvc]` 即 `i686-windows-msvc` 的行,交给工具的也是后者的拼写。 +`i386`、`i486` 与 `i586` 是不同的目标,**禁止**与 `i686` 相互替代。 + +**状态:已实现**(mcpp 2026.9.27.1,mcpp#704;`x86` 自 mcpp 2026.10.5.3)。 ### 4.7 载荷的来源与供给时机 @@ -634,3 +638,4 @@ mcpp 2026.9.26.2,#703)。** | 1.10 | 2026-09-28 | 多个命中的条件表按选择器的具体程度生效,三元组高于操作系统高于族,字典序只打破平局(mcpp 2026.9.28.2,mcpp#728,2026-09-28 设计 D7):§3.1.1 陈述规则与具体程度,§3.1 与 §9 第 2 条的「按清单顺序」随之更正;§3.1.1 转为已实现。 | | 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` 相互替代。 | diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index ae1451ef1..13a60d0e9 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -1642,6 +1642,13 @@ icon = "assets/app.ico" 没有资源单元、没有诊断、构建逐字节相同。**不需要**(也不能用)一个 `cfg(windows)` 谓词——写一次,无条件生效即可。 +**资源对象属于目标。** rc.exe 与 llvm-rc 产出 `.res`,它不带机器类型, +按原样参与链接。windres 产出 COFF 对象,它带机器类型:mcpp 交给 LLVM 的 +windres 目标三元组,交给 GNU windres BFD 格式名(`pe-i386`、`pe-x86-64`), +因此在 x86_64 机器上构建的 `i686` 镜像得到的是 i386 对象(2026.10.5.3+; +LLVM 的 windres 由文件名或其软链接最终指向的文件名识别)。给构建程序提供 +UTF-8 代码页的那份资源以同样方式为主机编译,命令变化时重新编译。 + **一个声明了却不存在的文件会让构建失败——在每一个目标上都一样。** 一份资源和一个源文件一样是构建输入;mcpp 不会悄悄发布一个缺了它的 二进制。校验刻意**不**只针对 PE:一个路径是否存在,是工作树的一个 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 3868d4a72..0f9b92564 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -38,6 +38,12 @@ C 库。选中 `x86_64-linux-musl` 就是选中 musl-gcc 的 payload,选中 | `os` | 操作系统,或 `none` | `linux`、`windows`、`macos`、`ios`、`emscripten`、`none` | | `env` | 见下文 —— 它在每个平台上是不同的一根轴 | `gnu`、`musl`、`msvc`、`android`、`elf` | +架构段采用 GNU 写法。另有三种写法在读取三元组时被改写为 GNU 写法,因此 +`[target.]` 段与 `--target` 无论用哪一种都能互相匹配:`amd64` 即 +`x86_64`,`arm64` 即 `aarch64`,MSVC 的 `x86` 即 `i686`(2026.10.5.3+;没有 +任何 LLVM 工具接受 `x86` 作为架构名)。`i386`、`i486` 与 `i586` 保持各自的 +含义,因为它们各自选择不同的基线 CPU。 + 第三段值得留意,因为它在不同平台上命名的并不是同一类东西: | 平台 | `env` 命名的对象 | 取值 | diff --git a/mcpp.toml b/mcpp.toml index fafd10421..9fd5a2d33 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.5.2" +version = "2026.10.5.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/platform/src/common.cppm b/modules/platform/src/common.cppm index 50955baf1..2c6f241bf 100644 --- a/modules/platform/src/common.cppm +++ b/modules/platform/src/common.cppm @@ -84,7 +84,7 @@ constexpr std::string_view host_arch = #elif defined(__x86_64__) || defined(_M_X64) "x86_64"; #elif defined(__i386__) || defined(_M_IX86) - "x86"; + "i686"; // the GNU spelling; `x86` is MSVC's, and `triple::parse` writes it `i686` #elif defined(__riscv) && (__riscv_xlen == 64) "riscv64"; #else diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 5a7a52d8f..c77ccccc0 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -377,13 +377,35 @@ struct Triple { return {}; } + // THE 32-BIT x86 ISA, ASKED IN ONE PLACE. + // + // Four spellings name it, and to clang they are not synonyms -- each sets + // a different baseline CPU. They are one answer to the question every + // caller here asks: the NASM format, the COFF machine of a resource, the + // MSVC directory, the decoration of a C symbol. Six sites used to answer it + // for themselves, with four different sets; `i386-windows-msvc` reached the + // x64 toolset directory through two of them. `x86` is not among the + // spellings because `parse` writes it `i686`. + bool is_x86_32() const { + return arch == "i386" || arch == "i486" || arch == "i586" || arch == "i686"; + } + + // MSVC's name for this architecture: the directory under a toolset's + // `bin/Host/`, the Windows SDK's `bin//`, and the redistributable + // CRT. Empty for an architecture MSVC ships no directory for. + std::string_view msvc_arch() const { + if (arch == "x86_64") return "x64"; + if (arch == "aarch64") return "arm64"; + if (is_x86_32()) return "x86"; + return {}; + } + // NASM `-f` output format for this target. NASM is x86-family only: // nullopt off x86, and the caller must hard-error (suggesting cfg-gated // sources) rather than pick a format. std::optional nasm_format() const { bool x64 = arch == "x86_64"; - bool x32 = arch == "x86" || arch == "i386" || arch == "i486" - || arch == "i586" || arch == "i686"; + bool x32 = is_x86_32(); if (!x64 && !x32) return std::nullopt; if (os == "windows") return x64 ? "win64" : "win32"; if (os == "macos") return x64 ? "macho64" : "macho32"; @@ -1267,6 +1289,11 @@ bool starts_with(std::string_view s, std::string_view p) { std::string normalize_arch(std::string_view a) { if (a == "arm64") return "aarch64"; // Apple/xlings spelling → GNU if (a == "amd64") return "x86_64"; + // MSVC's spelling. No LLVM tool accepts it as an architecture -- measured: + // clang and llvm-windres both answer `unknown target triple + // 'x86-pc-windows-msvc'` -- and Windows' 32-bit baseline is i686, the + // triple MSVC's own `x86` toolset reports. + if (a == "x86") return "i686"; return std::string(a); } diff --git a/modules/toolchain-model/tests/test_triple_vocabulary.cpp b/modules/toolchain-model/tests/test_triple_vocabulary.cpp index ab3448ebf..e750f863c 100644 --- a/modules/toolchain-model/tests/test_triple_vocabulary.cpp +++ b/modules/toolchain-model/tests/test_triple_vocabulary.cpp @@ -39,3 +39,37 @@ TEST(TripleVocabulary, AnUnknownTripleIsNotSilentlyKnown) { auto t = tr::parse("nosucharch-unknown-nosuchos-nosuchenv"); if (t.has_value()) EXPECT_FALSE(tr::is_known_target(*t)); } + +TEST(TripleVocabulary, MsvcsX86IsWrittenI686AndTheOtherSpellingsKeepTheirMeaning) { + // `x86` is MSVC's name for the ISA and no LLVM tool takes it as an + // architecture, so it is written the way `amd64` and `arm64` are: once, at + // the parse. i386-i586 are different baseline CPUs to clang and stay. + auto x86 = tr::parse("x86-windows-msvc"); + ASSERT_TRUE(x86.has_value()); + EXPECT_EQ(x86->str(), "i686-windows-msvc"); + EXPECT_EQ(x86->llvm_triple(), "i686-pc-windows-msvc"); + EXPECT_EQ(*tr::parse("x86-pc-windows-msvc"), *tr::parse("i686-windows-msvc")); + EXPECT_EQ(tr::parse("i386-windows-msvc")->llvm_triple(), "i386-pc-windows-msvc"); +} + +TEST(TripleVocabulary, The32BitX86QuestionHasOneAnswer) { + struct Row { std::string_view triple; bool x86_32; std::string_view msvc; }; + for (auto const& r : { + Row{"x86-windows-msvc", true, "x86"}, + Row{"i386-windows-msvc", true, "x86"}, + Row{"i486-windows-gnu", true, "x86"}, + Row{"i586-linux-gnu", true, "x86"}, + Row{"i686-windows-msvc", true, "x86"}, + Row{"x86_64-windows-msvc", false, "x64"}, + Row{"amd64-windows-msvc", false, "x64"}, + Row{"aarch64-windows-msvc", false, "arm64"}, + Row{"arm64-apple-macos", false, "arm64"}, + Row{"riscv64-linux-musl", false, ""}}) { + auto t = tr::parse(r.triple); + ASSERT_TRUE(t.has_value()) << r.triple; + EXPECT_EQ(t->is_x86_32(), r.x86_32) << r.triple; + EXPECT_EQ(t->msvc_arch(), r.msvc) << r.triple; + } + EXPECT_EQ(tr::parse("i586-linux-gnu")->nasm_format(), "elf32"); + EXPECT_EQ(tr::parse("x86-windows-msvc")->nasm_format(), "win32"); +} diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index bf2f23282..d0af095d0 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.5.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.5.3"; } // namespace mcpp diff --git a/src/build/pe_exports.cppm b/src/build/pe_exports.cppm index ee8bc11c9..ad10be6dd 100644 --- a/src/build/pe_exports.cppm +++ b/src/build/pe_exports.cppm @@ -7,6 +7,7 @@ import mcpp.build.coff_exports; import mcpp.modgraph.glob; import mcpp.platform.fs; import mcpp.platform.process; +import mcpp.toolchain.triple; export namespace mcpp::build::pe { @@ -252,8 +253,8 @@ discover_exports(std::span objects, const LLVMTools if (!arg) return error(obj, "object has no UTF-8 spelling"); arguments.push_back(std::move(*arg)); } - const bool i386 = tools.target.starts_with("i386-") || tools.target.starts_with("i686-") - || tools.target.starts_with("x86-"); + const auto targetTriple = mcpp::toolchain::triple::parse(tools.target); + const bool i386 = targetTriple && targetTriple->is_x86_32(); std::vector lto; for (std::size_t i = 0; i < objects.size(); ++i) if (bitcode[i]) lto.push_back(i); diff --git a/src/build/prepare/toolchain_env.cpp b/src/build/prepare/toolchain_env.cpp index f0fe54fff..78331c0ca 100644 --- a/src/build/prepare/toolchain_env.cpp +++ b/src/build/prepare/toolchain_env.cpp @@ -313,9 +313,9 @@ namespace { // #734 E2: the MSVC architecture directory name of a target triple. std::string msvc_arch_of(std::string_view triple) { - if (triple.starts_with("aarch64") || triple.starts_with("arm64")) return "arm64"; - if (triple.starts_with("i686") || triple.starts_with("i386") || triple.starts_with("x86-")) return "x86"; - return "x64"; + const auto tt = mcpp::toolchain::triple::parse(triple); + const auto arch = tt ? tt->msvc_arch() : std::string_view{}; + return arch.empty() ? "x64" : std::string(arch); } // The `bin/Host/` directory of an MSVC toolset that holds cl.exe, diff --git a/src/build/resources.cppm b/src/build/resources.cppm index cc2dde154..d744f7592 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -64,9 +64,22 @@ struct RcTool { // take directly. llvm-rc preprocesses by default and accepts // /I and /D (measured; there is no depfile option). std::string style; + // A "gnu"-style tool that is LLVM's windres, decided where the tool is + // found (`is_llvm_windres`) rather than guessed where it is used. It takes + // a target triple and preprocesses for it; GNU windres takes only a BFD + // format name. + bool llvm = false; std::string name() const { return path.filename().string(); } }; +// Is the windres at `p` LLVM's? By its name, or by the name of the file it +// resolves to: llvm-mingw installs `-windres` as a symlink to +// `llvm-windres`, and an LLVM distribution installs `llvm-windres` as one to +// `llvm-rc`. No process is run. A renamed COPY reads as GNU windres, which is +// harmless -- llvm-windres accepts the BFD names too, and only its +// preprocessing triple falls back to MinGW's. +bool is_llvm_windres(const std::filesystem::path& p); + // Find the resource compiler for `tc`, searching PAYLOAD-RELATIVE locations // only — never the host PATH. // @@ -168,17 +181,26 @@ compile_utf8_manifest(const mcpp::toolchain::Toolchain& tc, namespace mcpp::build::resources { +bool is_llvm_windres(const std::filesystem::path& p) { + const auto named = [](const std::filesystem::path& q) { + const auto n = q.filename().string(); + return n.find("llvm-windres") != std::string::npos + || n.find("llvm-rc") != std::string::npos; + }; + if (named(p)) return true; + std::error_code ec; + const auto real = std::filesystem::canonical(p, ec); + return !ec && named(real); +} + std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple) { if (tool.style != "gnu") return {}; auto trip = mcpp::toolchain::triple::parse(targetTriple); if (!trip) return {}; // LLVM accepts a full triple; GNU windres takes a BFD format name. // BFD's pe-i386 denotes 32-bit x86 COFF, including i686 targets. - if (tool.name().find("llvm-windres") != std::string::npos) - return "--target=" + trip->llvm_triple(); - if (trip->arch == "x86" || trip->arch == "i386" || trip->arch == "i486" - || trip->arch == "i586" || trip->arch == "i686") - return "--target=pe-i386"; + if (tool.llvm) return "--target=" + trip->llvm_triple(); + if (trip->is_x86_32()) return "--target=pe-i386"; if (trip->arch == "x86_64") return "--target=pe-x86-64"; return {}; } @@ -283,12 +305,12 @@ std::optional find_rc_tool(const mcpp::toolchain::Toolchain& tc, } const auto names = gnu_candidates(tc.targetTriple); - if (auto p = probe_dir(compilerDir, names)) return RcTool{*p, "gnu"}; + if (auto p = probe_dir(compilerDir, names)) return RcTool{*p, "gnu", is_llvm_windres(*p)}; // Cross payloads keep binutils in a sibling /bin. if (!tc.targetTriple.empty()) { auto root = compilerDir.parent_path(); if (auto p = probe_dir(root / tc.targetTriple / "bin", names)) - return RcTool{*p, "gnu"}; + return RcTool{*p, "gnu", is_llvm_windres(*p)}; } return std::nullopt; } @@ -331,11 +353,6 @@ compile_utf8_manifest(const mcpp::toolchain::Toolchain& tc, os << text; return true; }; - bool changed = write_if_changed(manifest, utf8_code_page_manifest()); - changed = write_if_changed(script, std::format( - "1 24 \"{}\"\n", escape_rc_string(manifest.generic_string()))) || changed; - if (!changed && std::filesystem::is_regular_file(out, ec)) return out; - // The same spelling as the `rc_object` rule of the ninja backend. std::vector argv = {tool->path.string()}; if (auto target = coff_target_flag(*tool, tc.targetTriple); !target.empty()) @@ -346,6 +363,19 @@ compile_utf8_manifest(const mcpp::toolchain::Toolchain& tc, for (auto a : {"-O", "coff", "--codepage=65001", "-o"}) argv.emplace_back(a); argv.push_back(out.string()); argv.push_back(script.string()); + + // THE COMMAND IS PART OF WHAT THE OBJECT IS, as it is on every edge of the + // ninja graph: a different tool, or a different target for it, makes a + // different object from the same script. Its words are kept beside the + // output and compared like the two inputs. + std::string command; + for (auto const& a : argv) command += a + '\n'; + bool changed = write_if_changed(manifest, utf8_code_page_manifest()); + changed = write_if_changed(script, std::format( + "1 24 \"{}\"\n", escape_rc_string(manifest.generic_string()))) || changed; + changed = write_if_changed(dir / (std::string(stem) + ".cmd"), command) || changed; + if (!changed && std::filesystem::is_regular_file(out, ec)) return out; + // rc.exe resolves through the SDK PATH the toolchain states for itself. std::vector> env; for (auto const& ev : tc.envOverrides) env.emplace_back(ev.key, ev.value); diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 932dd9c14..65ccf0a89 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -32,6 +32,7 @@ import std; import mcpp.platform; import mcpp.toolchain.model; import mcpp.toolchain.probe; +import mcpp.toolchain.triple; import mcpp.xlings; export namespace mcpp::toolchain::msvc { @@ -445,6 +446,17 @@ namespace mcpp::toolchain::msvc { namespace { +// MSVC's directory name for a GNU-spelled architecture, `x64` when MSVC has +// none: the toolset's `bin/Host/` and the redistributable CRT's. +// The question is the triple module's (`Triple::msvc_arch`), asked here +// rather than answered again. +std::string_view msvc_arch_dir(std::string_view archGnu) { + mcpp::toolchain::triple::Triple t; + t.arch = std::string(archGnu); + const auto arch = t.msvc_arch(); + return arch.empty() ? std::string_view{"x64"} : arch; +} + #if defined(_WIN32) // Strategy 1: VSINSTALLDIR — someone SAID which install to use. @@ -1450,9 +1462,7 @@ std::string compiler_version_in_tools_dir(const std::filesystem::path& toolsDir, // have chosen, or the flag would change what a build compiles for. const std::string_view host = mcpp::platform::host_arch == std::string_view("x86_64") ? "Hostx64" : "Hostx86"; - std::string_view target = "x64"; - if (archGnu == "aarch64") target = "arm64"; - else if (archGnu == "i686" || archGnu == "x86") target = "x86"; + const auto target = msvc_arch_dir(archGnu); const auto cl = toolsDir / "bin" / host / target / "cl.exe"; std::error_code ec; if (!std::filesystem::is_regular_file(cl, ec)) return {}; @@ -1611,10 +1621,7 @@ std::filesystem::path vc_redist_dir_for_tools_dir( auto vc = toolsDir.parent_path() // Tools/MSVC .parent_path() // Tools .parent_path(); // - std::string_view arch = "x64"; - if (archGnu == "aarch64") arch = "arm64"; - else if (archGnu == "i686" || archGnu == "x86") arch = "x86"; - return vc_redist_dir_under(vc, arch); + return vc_redist_dir_under(vc, msvc_arch_dir(archGnu)); } std::expected enrich_toolchain_from_cl(Toolchain& tc) { diff --git a/tests/e2e/889_windows_driver_target_and_coff_resources.sh b/tests/e2e/889_windows_driver_target_and_coff_resources.sh index 11330dd80..9d1b7ea7b 100644 --- a/tests/e2e/889_windows_driver_target_and_coff_resources.sh +++ b/tests/e2e/889_windows_driver_target_and_coff_resources.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # requires: msvc # mcpp#775: the driver link and COFF resource must target the selected machine. +# 2026.10.5.3: `x86-windows-msvc` is the i686 row. set -e source "$(dirname "$0")/_host_path.sh" TMP=$(mktemp -d) @@ -18,10 +19,14 @@ pe_machine() { offset=$(od -An -tu4 -j60 -N4 "$1" | tr -d ' \n') od -An -tu2 -j"$((offset + 4))" -N2 "$1" | tr -d ' \n' } -for arch in i686 x86_64; do +# `x86` is MSVC's spelling of i686 (2026.10.5.3): the same row, reached by the +# name an MSVC user writes. Before, it reached clang as `x86-pc-windows-msvc`. +for arch in i686 x86_64 x86; do machine=332 [ "$arch" != x86_64 ] || machine=34404 - for case_name in c cxx dll; do + cases="c cxx dll" + [ "$arch" != x86 ] || cases=cxx + for case_name in $cases; do mkdir -p "$arch/$case_name/src" cd "$arch/$case_name" kind=bin diff --git a/tests/unit/test_build_resources.cpp b/tests/unit/test_build_resources.cpp index 512b668c1..2fe1ff570 100644 --- a/tests/unit/test_build_resources.cpp +++ b/tests/unit/test_build_resources.cpp @@ -3,12 +3,14 @@ import std; import mcpp.manifest; import mcpp.build.resources; +import mcpp.platform; +import mcpp.toolchain.detect; namespace res = mcpp::build::resources; namespace fs = std::filesystem; TEST(BuildResources, CoffTargetFollowsTheTargetInsteadOfTheHost) { - const res::RcTool llvm{"/selected/bin/llvm-windres.exe", "gnu"}; + const res::RcTool llvm{"/selected/bin/llvm-windres.exe", "gnu", true}; EXPECT_EQ(res::coff_target_flag(llvm, "i686-windows-msvc"), "--target=i686-pc-windows-msvc"); EXPECT_EQ(res::coff_target_flag(llvm, "x86_64-windows-msvc"), @@ -22,6 +24,9 @@ TEST(BuildResources, CoffTargetFollowsTheTargetInsteadOfTheHost) { } EXPECT_EQ(res::coff_target_flag(llvm, "i386-windows-msvc"), "--target=i386-pc-windows-msvc"); + // MSVC's spelling reaches LLVM as the triple LLVM accepts (#776 review). + EXPECT_EQ(res::coff_target_flag(llvm, "x86-windows-msvc"), + "--target=i686-pc-windows-msvc"); EXPECT_EQ(res::coff_target_flag(gnu, "x86_64-windows-gnu"), "--target=pe-x86-64"); EXPECT_TRUE(res::coff_target_flag({"rc.exe", "msvc"}, "i686-windows-msvc").empty()); EXPECT_TRUE(res::coff_target_flag({"llvm-rc.exe", "msvc"}, "x86_64-windows-msvc").empty()); @@ -59,6 +64,74 @@ struct TempDir { } // namespace +// ─── Which windres: decided where the tool is found ─────────────────────── + +TEST(BuildResources, LlvmWindresIsRecognisedThroughTheSymlinkThatNamesIt) { + if constexpr (mcpp::platform::is_windows) + GTEST_SKIP() << "creating a symlink needs a privilege a Windows runner may lack"; + TempDir d; + const auto real = d.write("llvm-rc", ""); + std::error_code ec; + fs::create_symlink(real, d.path / "llvm-windres", ec); + ASSERT_FALSE(ec) << ec.message(); + fs::create_symlink(d.path / "llvm-windres", d.path / "i686-w64-mingw32-windres", ec); + ASSERT_FALSE(ec) << ec.message(); + const auto gnu = d.write("windres", ""); + EXPECT_TRUE(res::is_llvm_windres(d.path / "llvm-windres")); + EXPECT_TRUE(res::is_llvm_windres(d.path / "i686-w64-mingw32-windres")); + EXPECT_FALSE(res::is_llvm_windres(gnu)); + EXPECT_FALSE(res::is_llvm_windres(d.path / "absent-windres")); + + // And `find_rc_tool` records it: the llvm-mingw spelling gets the triple. + mcpp::toolchain::Toolchain tc; + tc.binaryPath = d.path / "clang"; + tc.targetTriple = "i686-windows-gnu"; + auto tool = res::find_rc_tool(tc, "gnu"); + ASSERT_TRUE(tool.has_value()); + EXPECT_EQ(tool->name(), "i686-w64-mingw32-windres"); + EXPECT_TRUE(tool->llvm); + EXPECT_EQ(res::coff_target_flag(*tool, tc.targetTriple), "--target=i686-w64-windows-gnu"); +} + +// ─── The build program's manifest object: the command is part of it ─────── + +TEST(BuildResources, TheManifestObjectIsRemadeWhenItsCommandChanges) { + if constexpr (mcpp::platform::is_windows) + GTEST_SKIP() << "the stand-in resource compiler is a POSIX shell script"; + TempDir d; + // A windres that writes the words it was given into its output. + const auto tool = d.write("windres", + "#!/bin/sh\nall=\"$*\"\n" + "while [ $# -gt 0 ]; do [ \"$1\" = -o ] && out=\"$2\"; shift; done\n" + "printf '%s\\n' \"$all\" > \"$out\"\n"); + fs::permissions(tool, fs::perms::owner_all); + mcpp::toolchain::Toolchain tc; + tc.binaryPath = d.path / "gcc"; + tc.targetTriple = "x86_64-windows-gnu"; + const auto read = [](const fs::path& p) { + std::ifstream in(p, std::ios::binary); + return std::string(std::istreambuf_iterator(in), {}); + }; + + auto first = res::compile_utf8_manifest(tc, "gnu", d.path / "out", "m"); + ASSERT_TRUE(first.has_value()) << first.error(); + EXPECT_NE(read(*first).find("--target=pe-x86-64"), std::string::npos); + + // The same command reuses the object: a marker written into it survives. + { std::ofstream(*first, std::ios::binary) << "reused"; } + auto again = res::compile_utf8_manifest(tc, "gnu", d.path / "out", "m"); + ASSERT_TRUE(again.has_value()) << again.error(); + EXPECT_EQ(read(*again), "reused"); + + // Another target is another command, so the object is made again. Before + // 2026.10.5.3 only the manifest and the script were compared, and this + // returned the x86-64 object. + tc.targetTriple = "i686-windows-gnu"; + auto other = res::compile_utf8_manifest(tc, "gnu", d.path / "out", "m"); + ASSERT_TRUE(other.has_value()) << other.error(); + EXPECT_NE(read(*other).find("--target=pe-i386"), std::string::npos); +} + // ─── Synthesis: the mcpp#365 headline ───────────────────────────────────── // // The reported symptom was a VERSIONINFO that llvm-readobj shows and Windows diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index 30b91f7fb..89249f2e3 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -114,6 +114,10 @@ TEST(MsvcCompilerVersion, IsReadFromTheCompilerClangReadsForTheTarget) { EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "x86_64"), "19.51.36260"); EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "aarch64"), "19.51.36261"); EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "i686"), "19.51.36262"); + // Every 32-bit x86 spelling is the x86 directory (2026.10.5.3; i386-i586 + // used to read x64's compiler). + EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "i386"), "19.51.36262"); + EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "i586"), "19.51.36262"); // No compiler where clang looks: no version, and clang falls back itself. EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools / "absent", "x86_64"), ""); std::error_code ec; @@ -564,6 +568,8 @@ TEST(MsvcRedist, ToolsDirArchMapping) { auto toolsDir = t.clPath.parent_path().parent_path().parent_path().parent_path(); EXPECT_FALSE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "aarch64").empty()); EXPECT_FALSE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "i686").empty()); + EXPECT_EQ(msvc::vc_redist_dir_for_tools_dir(toolsDir, "i386"), + msvc::vc_redist_dir_for_tools_dir(toolsDir, "i686")); EXPECT_FALSE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "x86_64").empty()); // An architecture this toolset was never given a redist for finds none — // it does not fall back to a different one. From e5f27893716aee270a9dcf5b208b953c913ad439 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 6 Oct 2026 09:41:35 +0900 Subject: [PATCH 2/3] prepare: every reader of a [target.] row finds it by any spelling find_target_entry moves to mcpp.build.prepare_inputs and is exported; the runner readers (the prepare conflict check, choose_device_action) and the min_api_level readers use it instead of an exact key, so a section written [target.x86-windows-msvc] supplies its runner as it supplies its toolchain. Unit test TargetRowSpelling. --- ...rch-vocabulary-and-rc-follow-ups-design.md | 1 + CHANGELOG.md | 4 ++ src/build/execute.cppm | 38 ++++++++-------- src/build/prepare/features.cpp | 4 +- src/build/prepare/state.cppm | 3 -- src/build/prepare/target_side.cpp | 20 +++++---- src/build/prepare/toolchain_env.cpp | 21 +-------- src/build/prepare_inputs.cppm | 18 ++++++++ tests/unit/test_target_row_spelling.cpp | 45 +++++++++++++++++++ 9 files changed, 104 insertions(+), 50 deletions(-) create mode 100644 tests/unit/test_target_row_spelling.cpp diff --git a/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md b/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md index ccacf8199..08dccda04 100644 --- a/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md +++ b/.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md @@ -22,6 +22,7 @@ status: active | D2 | 6 处 | 另加 `mcpp.platform` 的 `host_arch`:32 位宿主由 `"x86"` 改为 `"i686"` | 它绕过 `parse` 直接成为 `host_triple()` 的架构段;mcpp 不发布 32 位宿主,无可见变化 | | D3 | `enum class Flavour` | `RcTool::llvm`(bool)与导出的 `is_llvm_windres(path)` | `style` 已经区分 msvc 与 gnu,第三个取值只在 gnu 内部有意义;bool 不重复 `style` | | D5 | 不改 specs | SPEC-004 升到 1.13:§4.6 陈述架构段的等同拼写 | §4.6 已规定 `[target.X]` 的查找与拼写无关,`x86` 改变了这条规则的内容 | +| D6(新增) | — | `find_target_entry` 移入 `mcpp.build.prepare_inputs` 并导出;`runner` 的两处读取(prepare 的冲突检查、`execute` 的 `choose_device_action`)与 `min_api_level` 的两处读取改用它 | 自审发现:这四处按键精确比较,`[target.x86-windows-msvc]` 在 D1 之后对 `toolchain`/`sysroot` 生效而对 `runner` 不生效。`toolchain` 与 `sysroot` 一直经由与拼写无关的查找,规则本身不变,只是让每个读取者使用它。单测 `TargetRowSpelling` | --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 80041230b..251bf0e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ The design record is - **LLVM's windres is recognised where it is found**, by its name or by the file its symlink resolves to, so llvm-mingw's `-windres` receives a target triple rather than a BFD format name. +- **Every reader of a `[target.]` row finds it by any spelling.** + `toolchain` and `sysroot` were found whichever spelling the section used; + `runner`, the named runners and `min_api_level` were found only under the + canonical one. - **The build program's UTF-8 manifest resource is compiled again when its command changes**, as every edge of the build graph is; before, only the manifest and the script were compared. diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 6772cd0e2..c68592d6c 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -959,26 +959,28 @@ RunnerChoice choose_device_action(const BuildContext& ctx, c.tmpl = it->second.argv; c.longLived = it->second.longLived; } - // The manifest key is the CANONICAL spelling — `aarch64-macos`, the name - // of the output directory and the key every other `[target.]` - // reader uses (prepare.cppm resolves overrides by `t.str()`). The - // toolchain's own `targetTriple` is what the driver reported, which on a - // Linux host happens to be the canonical spelling and on macOS is - // `arm64-apple-darwin24.6.0`. Looking up the raw spelling alone matched on - // Linux and never on macOS (measured on CI, 2026-09-02); the raw form is - // kept as a fallback for a triple the parser does not know. - auto lookup = [&](std::string_view key) { - auto it = ctx.manifest.targetOverrides.find(std::string(key)); + // The row is found as every other `[target.]` reader finds it + // (`find_target_entry`), whichever spelling the section and the toolchain + // use. The toolchain's own `targetTriple` is what the driver reported, + // which on macOS is `arm64-apple-darwin24.6.0`; an exact lookup of it + // matched on Linux and never on macOS (measured on CI, 2026-09-02), and an + // exact lookup of the canonical spelling missed a section written + // `[target.x86-windows-msvc]`. The raw form is kept as a fallback for a + // triple the parser does not know. + auto with_slot = [&](const mcpp::manifest::TargetEntry* e) { const mcpp::manifest::TargetEntry* none = nullptr; - if (it == ctx.manifest.targetOverrides.end()) return none; - if (isDefault) return it->second.runner.empty() ? none : &it->second; - auto nr = it->second.namedRunners.find(std::string(which)); - return (nr != it->second.namedRunners.end() && !nr->second.empty()) - ? &it->second : none; + if (!e) return none; + if (isDefault) return e->runner.empty() ? none : e; + auto nr = e->namedRunners.find(std::string(which)); + return (nr != e->namedRunners.end() && !nr->second.empty()) ? e : none; }; - const mcpp::manifest::TargetEntry* entry = lookup(c.tripleKey); - if (!entry && c.tripleKey != ctx.tc.targetTriple) - entry = lookup(ctx.tc.targetTriple); + const mcpp::manifest::TargetEntry* entry = nullptr; + if (ft) { + entry = with_slot(mcpp::build::find_target_entry(ctx.manifest, *ft)); + } else if (auto it = ctx.manifest.targetOverrides.find(ctx.tc.targetTriple); + it != ctx.manifest.targetOverrides.end()) { + entry = with_slot(&it->second); + } if (entry) { if (isDefault) { c.fromManifest = !ctx.manifest.buildConfig.runner.empty(); diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index f403b3bac..8d2f294ae 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -134,9 +134,9 @@ static void step6_check_version_floors_closure(PrepareState& state) { const auto value = min_platform_version(*state.m, *t, state.tc->binaryPath); std::string name, origin; if (t->is_android()) { - auto row = state.m->targetOverrides.find(t->str()); + auto* row = find_target_entry(*state.m, *t); name = "android.api-level"; - origin = row != state.m->targetOverrides.end() && row->second.minApiLevel > 0 + origin = row && row->minApiLevel > 0 ? std::format("[target.{}] min_api_level", t->str()) : std::format("the toolchain's lowest supported level, because " "[target.{}] min_api_level is not set", t->str()); diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 9092a0771..7e71be3bb 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -833,9 +833,6 @@ bool graph_or_targets_import(const mcpp::modgraph::Graph& graph, const std::function& wanted); // toolchain_env.cpp: target rows, sysroots, the MSVC binding, build-program environments -const mcpp::manifest::TargetEntry* -find_target_entry(const mcpp::manifest::Manifest& m, - const mcpp::toolchain::triple::Triple& t); const std::string* sysroot_override(const mcpp::manifest::Manifest& m, const mcpp::toolchain::triple::Triple& t); diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index cc6f9bcaa..178cd2605 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -1755,16 +1755,20 @@ static std::expected step9_root_build_program(PrepareState& s // project writes outranks every supplied one where the runner is // looked up, so a name the manifest declares is not refused here. { - const auto rowKey = [&]() -> std::string { - if (!state.tc) return {}; - auto t = mcpp::toolchain::triple::parse(state.tc->targetTriple); - return t ? t->str() : state.tc->targetTriple; + const auto parsedTarget = state.tc + ? mcpp::toolchain::triple::parse(state.tc->targetTriple) + : std::nullopt; + const auto rowKey = parsedTarget ? parsedTarget->str() + : state.tc ? state.tc->targetTriple : std::string{}; + const auto* row = [&]() -> const mcpp::manifest::TargetEntry* { + if (parsedTarget) return find_target_entry(*state.m, *parsedTarget); + auto it = state.m->targetOverrides.find(rowKey); + return it == state.m->targetOverrides.end() ? nullptr : &it->second; }(); - const auto row = state.m->targetOverrides.find(rowKey); const auto manifestNames = [&](std::string_view name) { - if (row == state.m->targetOverrides.end()) return false; - if (name.empty()) return !row->second.runner.empty(); - return row->second.namedRunners.contains(std::string(name)); + if (!row) return false; + if (name.empty()) return !row->runner.empty(); + return row->namedRunners.contains(std::string(name)); }; if (!state.runnerProvider.empty() && !runnerBeforeRoot.empty() && bcRoot.runner.size() > runnerBeforeRoot.size() diff --git a/src/build/prepare/toolchain_env.cpp b/src/build/prepare/toolchain_env.cpp index 78331c0ca..5fa8b3423 100644 --- a/src/build/prepare/toolchain_env.cpp +++ b/src/build/prepare/toolchain_env.cpp @@ -67,22 +67,6 @@ namespace mcpp::build { // // A section keyed `x86_64-w64-mingw32` matches a resolved `x86_64-windows-gnu`, // and unparseable keys compare exactly (the escape hatch for custom triples). -// Factored out of the toolchain-override path because the sysroot override must -// use the SAME matching: two lookups that disagreed about spelling would give a -// section that applies to `toolchain` and not to `sysroot`, which is a defect -// nobody would think to look for. -const mcpp::manifest::TargetEntry* -find_target_entry(const mcpp::manifest::Manifest& m, - const mcpp::toolchain::triple::Triple& t) { - if (auto it = m.targetOverrides.find(t.str()); it != m.targetOverrides.end()) - return &it->second; - for (auto const& [key, entry] : m.targetOverrides) { - if (auto k = mcpp::toolchain::triple::parse(key); k && k->str() == t.str()) - return &entry; - } - return nullptr; -} - // The project's `[target.].sysroot`, or nullptr when it declared none. const std::string* sysroot_override(const mcpp::manifest::Manifest& m, @@ -520,9 +504,8 @@ std::string min_platform_version(const mcpp::manifest::Manifest& m, const mcpp::toolchain::triple::Triple& t, const std::filesystem::path& compilerPath) { if (t.is_android()) { - if (auto it = m.targetOverrides.find(t.str()); it != m.targetOverrides.end()) - if (it->second.minApiLevel > 0) - return std::to_string(it->second.minApiLevel); + if (auto* row = find_target_entry(m, t); row && row->minApiLevel > 0) + return std::to_string(row->minApiLevel); // AND THERE IS NO SUCH THING AS LEAVING IT OUT. This returned an empty // string with the comment "the NDK's own default, which clang // supplies", which was never verified and is false. Measured: diff --git a/src/build/prepare_inputs.cppm b/src/build/prepare_inputs.cppm index dfc1f8150..ccda13245 100644 --- a/src/build/prepare_inputs.cppm +++ b/src/build/prepare_inputs.cppm @@ -494,6 +494,24 @@ inline std::string vocabulary_sentence() { } // namespace cfgpred +// The `[target.]` section of `t`, whichever spelling either uses: a +// section keyed `x86_64-w64-mingw32` is the `x86_64-windows-gnu` row, and one +// keyed `x86-windows-msvc` the `i686-windows-msvc` row. Every reader of a row +// asks here. Readers that compared the key exactly gave a section that applied +// to `toolchain` and not to `runner`, which is a defect nobody would think to +// look for -- measured for the `x86` spelling (2026.10.5.3). +inline const mcpp::manifest::TargetEntry* +find_target_entry(const mcpp::manifest::Manifest& m, + const mcpp::toolchain::triple::Triple& t) { + if (auto it = m.targetOverrides.find(t.str()); it != m.targetOverrides.end()) + return &it->second; + for (auto const& [key, entry] : m.targetOverrides) { + if (auto k = mcpp::toolchain::triple::parse(key); k && k->str() == t.str()) + return &entry; + } + return nullptr; +} + std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc, const mcpp::toolchain::Fingerprint& fp, const std::filesystem::path& root) diff --git a/tests/unit/test_target_row_spelling.cpp b/tests/unit/test_target_row_spelling.cpp new file mode 100644 index 000000000..1df8135a3 --- /dev/null +++ b/tests/unit/test_target_row_spelling.cpp @@ -0,0 +1,45 @@ +#include + +import std; +import mcpp.manifest; +import mcpp.toolchain.triple; +import mcpp.build.prepare_inputs; + +// A `[target.]` row is one row whichever spelling names it. Every +// reader of a row asks `find_target_entry`; before 2026.10.5.3 the readers of +// `runner` and `min_api_level` compared the key exactly, so a section written +// `[target.x86-windows-msvc]` applied to `toolchain` and `sysroot` and not to +// `runner`. + +namespace tr = mcpp::toolchain::triple; + +TEST(TargetRowSpelling, EverySpellingOfATripleFindsItsRow) { + auto m = mcpp::manifest::parse_string(R"( +[package] +name = "p" +version = "0.1.0" + +[target.x86-windows-msvc] +runner = ["wine"] + +[target.x86_64-w64-mingw32] +runner = ["wine64"] + +[target.i386-windows-msvc] +runner = ["i386"] +)"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + auto runner_of = [&](std::string_view triple) -> std::string { + auto t = tr::parse(triple); + if (!t) return ""; + auto* row = mcpp::build::find_target_entry(*m, *t); + return row && !row->runner.empty() ? row->runner.front() : ""; + }; + EXPECT_EQ(runner_of("i686-windows-msvc"), "wine"); + EXPECT_EQ(runner_of("i686-pc-windows-msvc"), "wine"); + EXPECT_EQ(runner_of("x86-windows-msvc"), "wine"); + EXPECT_EQ(runner_of("x86_64-windows-gnu"), "wine64"); + // A different baseline CPU is a different row. + EXPECT_EQ(runner_of("i386-windows-msvc"), "i386"); + EXPECT_EQ(runner_of("i586-windows-msvc"), ""); +} From 67ddf590b73e12db313362d6832c378f233e8589 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 6 Oct 2026 10:20:34 +0900 Subject: [PATCH 3/3] prepare: one comment for the spelling-independent row lookup, where it lives --- src/build/prepare/toolchain_env.cpp | 5 +---- src/build/prepare_inputs.cppm | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/build/prepare/toolchain_env.cpp b/src/build/prepare/toolchain_env.cpp index 5fa8b3423..67d7ca4f8 100644 --- a/src/build/prepare/toolchain_env.cpp +++ b/src/build/prepare/toolchain_env.cpp @@ -63,11 +63,8 @@ namespace mcpp::build { // index is first opened, which can be hundreds of lines earlier; the message // that STOPS the build has to carry the cause, because that is the one a user // reads. See mcpp::pm::unusable_index_hint. -// Spelling-independent `[target.]` lookup. -// -// A section keyed `x86_64-w64-mingw32` matches a resolved `x86_64-windows-gnu`, -// and unparseable keys compare exactly (the escape hatch for custom triples). // The project's `[target.].sysroot`, or nullptr when it declared none. +// The row is found by `find_target_entry` (prepare_inputs), as every row is. const std::string* sysroot_override(const mcpp::manifest::Manifest& m, const mcpp::toolchain::triple::Triple& t) { diff --git a/src/build/prepare_inputs.cppm b/src/build/prepare_inputs.cppm index ccda13245..aaa7abcd7 100644 --- a/src/build/prepare_inputs.cppm +++ b/src/build/prepare_inputs.cppm @@ -499,7 +499,8 @@ inline std::string vocabulary_sentence() { // keyed `x86-windows-msvc` the `i686-windows-msvc` row. Every reader of a row // asks here. Readers that compared the key exactly gave a section that applied // to `toolchain` and not to `runner`, which is a defect nobody would think to -// look for -- measured for the `x86` spelling (2026.10.5.3). +// look for -- measured for the `x86` spelling (2026.10.5.3). A key the parser +// does not know compares exactly: the escape hatch for a custom triple. inline const mcpp::manifest::TargetEntry* find_target_entry(const mcpp::manifest::Manifest& m, const mcpp::toolchain::triple::Triple& t) {