diff --git a/.agents/docs/2026-07-15-toolchain-target-naming-unification-design.md b/.agents/docs/2026-07-15-toolchain-target-naming-unification-design.md new file mode 100644 index 0000000..163f525 --- /dev/null +++ b/.agents/docs/2026-07-15-toolchain-target-naming-unification-design.md @@ -0,0 +1,456 @@ +# 工具链 × 目标 命名统一 — 设计方案(实现 + 显示 + 使用) + +> 2026-07-15 · 基于 0.0.92(mingw-cross 已发布)代码审计 + 跨生态调研(rustup/cargo、Zig、Go、Clang、GCC 世界、xmake、CMake、Swift SDK、Bazel、Nix) +> 姊妹文档:`2026-07-15-mingw-linux-cross-windows-design.md`(本方案的直接诱因)、 +> `2026-07-13-toolchain-backend-abstraction-msvc-mingw-design.md`(CommandDialect 抽象层)、 +> `2026-06-30-target-bare-alias-sugar-design.md`(`[target.linux]` 糖,本方案需兼容) +> 记忆:[[mingw-linux-cross-windows]]、[[toolchain-dialect-and-mingw]]、[[msvc-system-toolchain]] + +## 0. 一句话 + +把「**toolchain = family@version** × **target = triple(arch-os[-env])**」立为唯一的二轴身份模型: +musl / mingw / cross 都**不再是工具链名字**——变体(gnu/musl/msvc)进 triple 的 env 段,"cross" 只允许 +出现在 xim 分发包名层(那里它是合法的,业界先例一致);实现上把四个平行的 triple 解析器收敛为单一 +`triple.cppm`,全部旧拼写的识别/归一收进单一 **`compat.cppm`**(核心代码只见 canonical 形), +`--target` 从未校验自由文本变为封闭词汇表 + 逃生舱;显示与 CLI 按 rustup 的 toolchain/target +两轴模型重排。**单 PR 一次落地(0.0.93)**,沿用 0.0.90/0.0.91 的 single-PR 惯例, +PR 内按 §7 的八步各自编译绿、e2e 门禁收口。 + +> **决策已定**(2026-07-15 review,含二次 review):D1 = `x86_64-windows-gnu` canonical; +> D2 = mingw/mingw-cross 降级为别名(family 只剩 gcc/llvm/msvc);D4 = **单 `toolchain` 名词 + +> `--target` 选项,不设 `mcpp target` 子命令**(二次 review 改定,理由见 §6.1);D8 = 不分期, +> 整套单 PR;D9 = 兼容层独立 `src/toolchain/compat.cppm`。其余(D3/D5/D6/D7)按推荐执行。 + +--- + +## 1. 问题定性 — 为什么 `mingw-cross` 让人疑惑(case study) + +从 mcpp 用户视角,`mcpp toolchain install mingw-cross 16.1.0` 有三重错位: + +**① 同一概念、两个名字,按 host 劈开。** 「GCC 家族、产出 Windows PE(GNU CRT)」这一个概念, +在 Windows host 上叫 `mingw`,在 Linux host 上叫 `mingw-cross`——名字随用户站在哪台机器而变。 +但用户的意图始终是 target 语义(「我要产出给 Windows」),却被迫在 toolchain 名里表达 host 关系。 +`available_toolchain_indexes()`(registry.cppm:281-294)按 host `if constexpr` 分流两个名字, +就是这个错位的代码形态。 + +**② "cross" 是 host 相对属性,不是身份。** 行业调研结论(§3):Rust 官方生态**零** "cross" 命名 +——装交叉目标是 `rustup target add `(target 操作,rustc 本体不动);Zig/Go 里 native 与 +cross 是同一条命令改参数;GNU 传统里 "cross compiler" 是描述词(host≠target 的那条链)而非名字。 +"cross"/"native" 后缀合法出现的**唯一**场合是分发包命名层:musl.cc 的 +`aarch64-linux-musl-cross.tgz` vs `-native.tgz`、Debian 的 `g++-mingw-w64-x86-64`——因为分发包 +确实要区分「这个包的宿主是谁」。**mcpp 的 xim 包 `mingw-cross-gcc` 恰好就在分发层,命名是对的; +错误在于让它漏进了用户 spec 层**(`mingw-cross@16.1.0` 成了用户要打的字、`toolchain list` 里的一行)。 + +**③ 同一语义轴、四种编码。** 「target 环境变体」这一个轴,现状有四种写法: + +| 变体 | 编码位置 | 例 | 证据 | +|---|---|---|---| +| musl | 编译器名前缀 | `musl-gcc@15.1.0` | registry.cppm:155 | +| musl | 版本号后缀 | `gcc@15.1.0-musl` | registry.cppm:156 | +| musl | triple 命名编译器 | `aarch64-linux-musl-gcc@16.1.0` | registry.cppm:157,171-178 | +| mingw(msvcrt) | 编译器名 | `mingw@16.1.0` / `mingw-cross@16.1.0` | registry.cppm:186,191 | + +业界的正典位置只有一个:**triple 的 env/abi 段**(Rust `-gnu/-musl/-msvc`、Zig 第三段、Clang env 段,§3 收敛点 3)。 + +--- + +## 2. 现状审计(file:line) + +### 2.1 三层命名 + 有损反向映射 + +| 层 | 例 | 定义处 | +|---|---|---| +| 用户 spec | `gcc@16.1.0`、`gcc 15.1.0-musl`、`mingw-cross@16.1.0`、`msvc@system` | `parse_toolchain_spec` registry.cppm:135-159 | +| xim 包名(`xim-x-`) | `gcc`、`musl-gcc`、`mingw-cross-gcc`、`llvm` | `to_xim_package` registry.cppm:161-207;目录形成 xlings.cppm:553-559 | +| 显示 label | `gcc 15.1.0-musl`、`mingw-cross 16.1.0` | `display_label` registry.cppm:240-248 | + +`display_label` 是从 xim 名**反推**用户名的有损映射;`matches_default_toolchain` +(registry.cppm:250-274)已积累四条特例(musl/msvc/mingw/mingw-cross)。每加一个变体 = +`to_xim_package` + `display_label` + `matches_default_toolchain` + `available_toolchain_indexes` + +`frontend_candidates_for` 五处 if——**线性增长的特例链,是命名模型错位的实现代价**。 + +### 2.2 四个平行 triple 解析器,词汇表已分叉 + +| 解析器 | 位置 | os 词汇 | 备注 | +|---|---|---|---| +| `cfgpred::context_for` | prepare.cppm:62-87 | **`macos`** | substring 匹配;`env=gnu\|musl\|msvc` | +| `abi_profile` | abi.cppm:67-96 | **`darwin`** | 同一概念、不同拼写;prepare.cppm:59-61 注释声称「vocabulary is consistent」——os 维恰恰不一致 | +| `model.cppm` 谓词 | model.cppm:99-110 | — | `is_musl_target`/`is_msvc_target`/`is_mingw_target` 独立 substring | +| registry musl 三信号 | registry.cppm:155-157 | — | `toolchain_frontend(binDir, compiler)` 重载(:219)只判其一,漏两信号、不传 triple——同一规则两处实现已经分叉 | + +目前潜伏(无代码互比两个 os 值),但谁把 `cfg(os="macos")` 接到 abi 约束上就会静默匹配失败。 + +### 2.3 `--target` 是未校验自由文本 + +- 无合法 triple 表、无校验;只有两条硬编码约定:`*-musl` 后缀(prepare.cppm:651-662,host-aware + 分 native/cross)与精确 `x86_64-w64-mingw32`(prepare.cppm:673-679)。 +- **打错字 `--target x86_64-linux-mus` 不报错,静默 fall through 到宿主默认工具链**——用户以为在交叉, + 实际编了本地。这是最坏的失败模式。 +- 对照:`[package] platforms` **有**固定词汇表校验(prepare.cppm:607-617,`--strict` 硬错)。 + 同一个 target 系统,一头严一头全放。 +- 命名冲突:`mcpp run ` 的位置参数是**二进制名**(cli.cppm:244),与 triple 无关。 + +### 2.4 显示层缺陷 + +- **排序 bug**:lifecycle.cppm:322-329 版本字典序降序,注释自认「single-digit segments」前提—— + GCC 11/13/15/16 全是两位主版本,实际输出 `9.4.0` 排在 `15.1.0` 之前。 +- **分组割裂**:排序主键是内部编译器名(`gcc` < `llvm` < `musl-gcc`),显示却是 label, + `gcc X-musl` 行被 `llvm` 劈开。 +- **可发现性缺口**:`aarch64-linux-musl-gcc` 装得上(parse/to_xim_package 全支持)但 + `available_toolchain_indexes` 不列它——用户只能读 CI yml 才知道存在。 + +### 2.5 版本 pin 散落 + +`16.1.0` 在 prepare.cppm:659,661,676;默认三元组在 :813-818;help/错误串里还有 +cli.cppm:346-347、lifecycle.cppm:472、config.cppm:652、doctor.cppm:149。升默认版本要改七八处, +文档漂移(docs/03 说 Linux 默认 `gcc@15.1.0-musl`,代码是 x86_64→`gcc@16.1.0` glibc)是必然结果。 +README 平台表(README.md:234-241)MSVC 仍标 planned、mingw 两列整体缺失——表的维度(OS×编译器) +装不下 host≠target。 + +### 2.6 已经对的部分(方案必须保住) + +| 资产 | 位置 | +|---|---| +| 输出目录按 triple 键控 `target//` | prepare.cppm:161-167 | +| cfg() 按 **resolved target** 求值(非 host) | prepare.cppm:51-54,687-705 | +| PE 链接模型按 target(`is_mingw_target`) | linkmodel.cppm:231-236、model.cppm:107-110 | +| `[target.linux]` bare-alias 糖(alias 无 dash,与 triple 无歧义) | prepare.cppm:142-157 | +| `msvc@system` detection-first、pin-verify(`msvc@19.44`) | lifecycle.cppm:476-495 | +| 平台能力常量(`supports_full_static` 等)替代 #ifdef | platform/common.cppm:110-112 | +| arch 扩展是数据不是代码(loader triple 表 + glob 兜底) | linkmodel.cppm:139-186 | +| `aarch64` 为唯一 arch 拼写(GNU triple 系,可与 triple 拼接;`arm64` 属 xlings 资产命名空间) | common.cppm:76-92 | +| static 不进 triple:`*-musl`/mingw32 target **默认** `linkage=static`,`[build]` 可翻转 | prepare.cppm:663-666,677-678 | + +最后一条与业界完全一致(Rust `crt-static` 是 target 默认属性 + flag 翻转,static 普遍不进 triple) +——本方案不动它。 + +--- + +## 3. 行业模型对照(调研摘要,详见调研记录) + +| 生态 | toolchain 身份 | target 身份 | cross 表达 | 变体位置 | +|---|---|---|---|---| +| Rust (rustup+cargo) | `channel-`(名字里的 triple 是 **host**) | 4 段 LLVM triple,tier 分级 | `rustup target add` + `cargo build --target`;**全生态零 "cross" 命名** | triple 第 4 段(`-gnu/-musl/-msvc`);static=target 默认属性+flag | +| Zig | 无(编译器即全部,零安装 target) | **3 段 `arch-os-abi`(砍掉 vendor)**;glibc 版本点号后缀 `-gnu.2.17` | `-target` 一个参数,native/cross 零差别 | abi 段 | +| Go | 无 | `GOOS`/`GOARCH` 二元组 | 改环境变量;cgo 一介入即退化回 GCC 世界 | 无 libc 维(被消灭);arch 子变体=另一个环境变量 | +| Clang | 一个多 target 编译器;`-clang` 调用名→隐式 `--target` | LLVM Triple(所有 triple 的事实源头) | `--target=` 只切 codegen;sysroot 靠 `.cfg` 事后捆绑 | triple env 段 + cfg 两层(公认模糊地带) | +| GCC 世界 | **一条链=一个 target**,链名=triple(`x86_64-w64-mingw32-g++`) | GNU triple | 用户必须感知(换整套前缀命令);"cross compiler"=host≠target 描述词 | triple + **溢出到打包层**(MSYS2 `mingw-w64-ucrt-*`) | +| xmake | 具名 toolchain 注册表(含一个叫 `cross` 的通用链) | `--plat` × `--arch` 两参数;**mingw 被提升为独立 plat** | 切 plat | plat 之别(windows vs mingw)+ SDK 选择,分裂 | +| CMake | toolchain file(自由脚本) | **无身份**——不可枚举/比较/分发,公认痛点之首 | file 内隐式(`CMAKE_SYSTEM_NAME` 即 cross) | file 内任意变量 | +| Swift SDK | 编译器不变,per-target 装 artifact bundle | triple(`x86_64-swift-linux-musl`) | `swift sdk install` + `--swift-sdk ` | musl 进 triple;static 进 SDK 产品名 | +| Bazel | toolchain 声明 exec/target 兼容性 | platform=constraint 集合(可扩维,无全球命名) | `--platforms=` 触发自动 resolution | 自定义 constraint | +| Nix | pkgsCross 属性集 | **规范身份=triple,人机接口=短别名**(`pkgsCross.mingwW64` → `config="x86_64-w64-mingw32"`) | buildPlatform/hostPlatform/targetPlatform 三平台显式建模 | triple + 别名层 | + +**收敛点(设计公理的来源)**: +1. target 规范身份 = triple(不用的只有 Go 二元组——cgo 一来就装不下 libc 维;和 CMake——反面教材)。 +2. **"cross" 不是 target/toolchain 命名成分,是 host≠target 的相对状态**;例外全在分发/打包层 + (musl.cc `-cross` 后缀、Debian 包名、第三方 cross-rs)。 +3. 变体正典位置 = triple env/abi 段;**static 普遍不进 triple**(target 默认属性 + flag 翻转)。 +4. toolchain 与 target 正交;multi-target 编译器使「装 target」退化为「装 std/sysroot」 + (rustup target add / swift sdk install);单 target 编译器(GCC)才被迫把 target 烙进链名—— + **mcpp 用 GCC 系工具链但不必继承 GCC 的命名宿命:管理层完全可以呈现 rustup 模型, + 「per-target 装的是整条 GCC 链」只是 acquisition 的实现细节**(重量级版的 rust-std 下载)。 +5. CLI 收敛为单参数选 target、默认=host,用户不感知 host。 +6. 分歧点里对 mcpp 有立场的:vendor 段——新设计趋势是砍(Zig 3 段);mingw 转正为一等平台 + (xmake)vs 普通 target(Rust)——**取 Rust 侧**,转正污染正交性。 + +--- + +## 4. 统一模型 + +### 4.1 公理 + +- **A1** target 身份 = mcpp 自有 triple 语言 **`arch-os[-env]`**(Zig 式三段,砍 vendor)。 + mcpp 已在用 `x86_64-linux-musl`(三段、无 vendor)——这是**扩展既有约定,不是换语言**。 + prepare.cppm:607-608 已声明「mcpp owns the target/triple system」,本方案兑现它。 +- **A2** toolchain 身份 = **family@version**,family ∈ {gcc, llvm, msvc}, + version ∈ semver | `system`。source(managed-xim / system-detected)由 (family,version) 派生, + 不进名字(`msvc@system` 既有形态保持)。 +- **A3** **cross 是关系不是名字**:host≠target 由 (host, target) 二元组派生,仅允许出现在 + ① xim 分发包名(`mingw-cross-gcc`——保持不动)② 显示层的状态注记(如 `(cross)`)。 + 用户 spec、manifest、CLI 参数中不得出现。 +- **A4** 变体 = triple env 段:`gnu | musl | msvc`。static **不是**变体,是 target 的默认链接属性 + (现状已对,见 §2.6)。 +- **A5** 人机别名层允许存在(Nix 先例:规范身份=triple、接口=短别名),但一律 normalize 到 + canonical 并单行提示,别名永久可解析(不搞 breaking 废弃)。 + +### 4.2 target 封闭词汇表(数据,不是代码) + +| canonical triple | env | 状态(tier) | 别名(永久接受) | payload 来源(§4.3) | +|---|---|---|---|---| +| `x86_64-linux-gnu` | gnu | **verified**(ci-linux) | `x86_64-linux`(裸 os 省略 env→host 默认) | xim:gcc / xim:llvm | +| `x86_64-linux-musl` | musl | **verified**(ci-linux、release) | — | xim:musl-gcc | +| `aarch64-linux-musl` | musl | **verified**(cross-build-test qemu、ci-aarch64) | — | xim:musl-gcc(host=aarch64)/ xim:aarch64-linux-musl-gcc(cross) | +| `x86_64-windows-gnu` | gnu(MSVCRT) | **verified**(102_e2e wine、cross-build-test) | **`x86_64-w64-mingw32`**(GNU 正典拼写,0.0.92 已发布,永久别名) | xim:mingw-gcc(host=win)/ xim:mingw-cross-gcc(host=linux) | +| `x86_64-windows-msvc` | msvc | **verified**(ci-windows msvc 原生) | — | system(vswhere 探测) | +| `aarch64-macos` | — | **verified**(ci-macos) | `arm64-apple-darwin` 系拼写 | xim:llvm | +| `riscv64-linux-musl` | musl | planned(等 xim 包,loader 表已备 linkmodel.cppm:145) | — | xim:riscv64-linux-musl-gcc | +| `aarch64-linux-gnu` | gnu | planned(glibc gcc 未发布 aarch64) | — | — | +| `x86_64-macos` | — | planned | — | — | + +tier 语义借 Rust:**verified** = CI 端到端真跑(编译+执行,qemu/wine 也算); +**published** = 包已发未进 CI;**planned** = 登记未接。词汇表是 `triple.cppm` 里的一张 +constexpr 表 + 每行的 e2e 引用注释——README 平台表从这张表**生成或人工对照**,根治 §2.5 的漂移。 + +**windows-gnu 的 canonical 拼写决策(D1)**:取 mcpp 自有 `x86_64-windows-gnu`, +`x86_64-w64-mingw32` 降为永久别名。理由:①与既有 `x86_64-linux-musl` 三段式一致(GNU 拼写 +`w64` vendor 段 + os 段叫 `mingw32` 是历史残留——64 位也叫 mingw32);②cfg 谓词 +`os="windows", env="gnu"` 与 triple 字面直接对应,substring 匹配的两个特判(`mingw`→windows, +prepare.cppm:69;`mingw32` 判 PE,model.cppm:107-110)变成结构化字段读取;③Zig 同拼写先例 +(`x86_64-windows-gnu`)。**代价**:`target/` 输出目录名随 canonical 变(S3 迁移,别名输入 +normalize 后仍进同一目录);`Toolchain::targetTriple`(dumpmachine 报告的 +`x86_64-w64-mingw32`)保持为**独立内部字段**——用户 Target 与编译器自报 triple 本就是两个概念 +(Zig 的 -target vs LLVM triple 同构)。 + +### 4.3 ToolchainSpec v2 + payload 映射数据表 + +```cpp +struct ToolchainSpec { // registry.cppm 重构 + Family family; // gcc | llvm | msvc(枚举,不再是自由串) + std::string version; // "16.1.0" | "system";partial "16" 保持现有 resolve + Triple target; // 空 = host;唯一的变体载体(A4) +}; +// 删除:isMusl(bool 硬编一个变体)、compiler 自由串 +``` + +**payload 映射 = 一张数据表**(family, target.env, target.arch, host)→ xim 包名 + 前端候选: + +| family | target | host | xim 包(**分发层,"cross" 在此合法,零改名**) | 前端 | +|---|---|---|---|---| +| gcc | x86_64-linux-gnu | linux-x86_64 | `gcc` | `g++` | +| gcc | \*-linux-musl(arch==host) | linux | `musl-gcc` | `-g++` | +| gcc | \*-linux-musl(arch≠host) | linux | `-gcc`(如 aarch64-linux-musl-gcc) | `-g++` | +| gcc | x86_64-windows-gnu | **windows** | `mingw-gcc`(winlibs UCRT) | `g++.exe` | +| gcc | x86_64-windows-gnu | **linux** | `mingw-cross-gcc`(MSVCRT 交叉) | `x86_64-w64-mingw32-g++` | +| llvm | host | any | `llvm` | `clang++` | +| msvc | x86_64-windows-msvc | windows | —(system 探测,vswhere 链) | `cl.exe` | + +这张表替换掉五处 if 链(§2.1):`to_xim_package`/`frontend_candidates_for` 查表; +`display_label`/`matches_default_toolchain` **整体删除**——显示从 Spec 结构渲染 +(`gcc 16.1.0 → x86_64-windows-gnu`),default 匹配变成 Spec 相等比较。 +`available_toolchain_indexes` 的 host `if constexpr` 分流(registry.cppm:287-292)也由表的 +host 列取代——同一个 target 行,host 不同只是取不同包,**用户看到的名字不变**。 + +注意表里 winlibs=UCRT、交叉=MSVCRT:两条 CRT 目前都折叠在 `x86_64-windows-gnu`。 +abi.cppm:76 现状 mingw→`msvcrt` 单值,与 0.0.92 选型一致;UCRT 细分若未来需要, +按 Rust 先例另立 env(`gnullvm` 式)或 abi 维度细化,登记为 follow-up,不在本期。 + +### 4.4 单一 `triple.cppm`(src/toolchain/ 或 src/target/) + +```cpp +struct Triple { + std::string arch; // x86_64 | aarch64 | riscv64 | ...(GNU 拼写,永不 arm64) + std::string os; // linux | macos | windows(darwin 在 parse 时归一为 macos) + std::string env; // gnu | musl | msvc | ""(macos) + // 派生只读:family() → unix|windows;is_pe();is_musl();is_msvc_env();is_host(); +}; +Triple parse(std::string_view); // 接受别名(w64-mingw32、darwin、apple)→ canonical +std::optional validate(...); // 对 §4.2 封闭表;附 did-you-mean +``` + +四个现有解析器全部改为消费者: + +| 消费者 | 现状 | 改后 | +|---|---|---| +| `cfgpred::context_for`(prepare.cppm:62-87) | substring 自解析 | 读 Triple 字段;cfg 词汇 `{os,arch,family,env}` 值域=Triple 值域,文档化 | +| `abi_profile`(abi.cppm:81-83) | os 吐 `darwin` | 吐 `macos`——**词汇分叉在源头消灭**;abi 五维结构保留 | +| `model.cppm:99-110` 谓词 | 独立 substring | `Triple` 派生方法的薄别名(或直接删,call site 改) | +| registry musl 三信号(:155-157)+ `toolchain_frontend`(:219)漏判 | 两处实现已分叉 | Spec v2 后自然消失(env 是唯一信号源) | + +### 4.5 `--target` 校验 + +- 输入先 `triple::parse`(别名归一)再 `validate` 对封闭表。 +- **逃生舱**:manifest 存在显式 `[target.]` 节的未知 triple 放行(自带 toolchain override + 的高级用法不被词汇表卡死)——否则**硬错** + did-you-mean(编辑距离): + `unknown target 'x86_64-linux-mus' — did you mean 'x86_64-linux-musl'?`。 +- `[target.linux]` bare-alias 糖不受影响(alias 无 dash,原有歧义消解规则保持,prepare.cppm:148-157)。 +- 两条硬编码约定(`*-musl`、mingw32,prepare.cppm:651-679)改为查 §4.2 表的 + 「target → 默认 toolchain pin」列,顺带完成 §2.5 的 pin 集中(见 §4.6)。 + +### 4.6 版本 pin 集中 + +新增单一常量表(如 `src/toolchain/pins.cppm` 或并入 triple 表): +first-run 默认三元组(prepare.cppm:813-818)、musl/mingw 约定 pin(16.1.0)、llvm 默认(20.1.7)。 +help/错误串(cli.cppm:346-347 等)一律 `std::format` 引用。docs 侧在表旁注释「改此处须同步 +docs/03、README 表」。 + +### 4.7 `compat.cppm` — 兼容层独立成模块(决策新增) + +所有旧拼写的**识别与归一**收进单一 `src/toolchain/compat.cppm`,核心代码只见 canonical 形: + +```cpp +export module mcpp.toolchain.compat; +// 职责(唯一知道旧拼写的文件;整体删除 = 只断旧输入,不断任何 canonical 路径): +// 1. spec 别名:musl-gcc@V / gcc@V-musl / -gcc@V / mingw@V / mingw-cross@V / clang@V +// → 归一为 ToolchainSpec v2{family, version, target}(§6.2 表的实现载体) +// 2. triple 别名:x86_64-w64-mingw32 / darwin / apple 系拼写 → canonical Triple +// 3. 持久化状态迁移:config 里存量 default 串("gcc@15.1.0-musl"、"mingw-cross@16.1.0"、 +// manifest [toolchain] 旧值)在**读取路径**归一(可选:写回时重写为 canonical) +// 4. 归一提示:一次性单行 note(§5.2),提示文案只在此处 +export std::optional normalize_spec(std::string_view); // 非别名→nullopt(走 canonical 解析) +export std::optional normalize_triple(std::string_view); +``` + +**依赖方向**:compat → 核心类型(Triple/ToolchainSpec);核心只在两个公共解析入口 +(`parse_toolchain_spec`、`triple::parse`)的第一行调 `compat::normalize_*`,其余核心代码 +零 compat 依赖。**xim 包名映射不在 compat**——`mingw-cross-gcc` 等是分发层现役身份(§4.3 数据表), +不是遗留拼写。命名说明:模块职责单一(legacy-spelling compatibility), +不是口袋模块([[naming-no-grab-bag-modules]] 约束仍满足)。 + +--- + +## 5. 显示/输出规范 + +### 5.1 `mcpp toolchain list` 重排(rustup 两轴模型) + +``` +Toolchains: + * gcc 16.1.0 (default) + gcc 15.1.0 + llvm 22.1.8 +System: + msvc 19.44 (VS 2022 BuildTools) [Windows host 才有此块,现状保持] + +Targets: TOOLCHAIN STATUS + * x86_64-linux-gnu (host) gcc 16.1.0 installed + x86_64-linux-musl static gcc 16.1.0 installed + x86_64-windows-gnu PE, cross gcc 16.1.0 installed + aarch64-linux-musl static, cross gcc 16.1.0 available + riscv64-linux-musl static, cross — planned + +Available toolchains (run `mcpp toolchain install `): + gcc 15.1.0 / 13.3.0 / 11.5.0 / 9.4.0 + llvm 20.1.7 +``` + +要点: +- **toolchain 块只剩 family@version**——`gcc 15.1.0-musl`、`mingw-cross 16.1.0` 这类行消失, + 它们变成 Targets 块的行(「变体是 target」的显示兑现)。 +- `cross` 只出现在 STATUS 注记(A3 允许的第二处)——描述当前 host 关系,不是身份。 +- **排序修复**:版本按数值 semver 降序(修 lifecycle.cppm:326 字典序 bug); + family 分组(修 `gcc-musl` 被 `llvm` 劈开)。此两条**独立于大方案,S1 先行**。 +- planned 行的展示给 target 词汇表一个用户可见出口(修 §2.4 可发现性缺口—— + `aarch64-linux-musl` 不再只活在 CI yml 里)。 +- 路径缩写 `@mcpp/` 保持。 + +### 5.2 消息拼写规则 + +- 一切输出中 toolchain 用 **`family@version`**(`Resolved gcc@16.1.0 → x86_64-windows-gnu`); + 空格形式(`gcc 16`)只作输入接受。 +- 别名 normalize 时单行提示一次: + `note: 'mingw-cross@16.1.0' is now 'gcc@16.1.0' targeting 'x86_64-windows-gnu'`—— + 提示不是 warning,不入 stderr 告警级。 + +--- + +## 6. CLI / 使用规范 + +### 6.1 单名词 CLI:`mcpp toolchain` + `--target` 选项(D4 二次 review 改定) + +**不引入 `mcpp target` 子命令**。rustup 的 target/toolchain 双名词有个 mcpp 不具备的前提: +rustc 是多 target 编译器,`rustup target add` 只下载 rust-std、编译器本体不动——两个名词对应 +两种物理实体。mcpp 是 GCC 世界,**每个 (family, target) 组合 = 装一整条链**,「加 target」和 +「装 toolchain」底下是同一个动作;两个名词、一种实体才是概念冗余。且 mcpp 已有 build 时 +autoInstall——交叉的主 UX 本来就是 Zig 式零仪式: + +```bash +mcpp build --target x86_64-windows-gnu # 主路径:缺链自动装,一条命令 +``` + +显式命令面(低频:CI 预热/离线准备/切默认): + +```bash +mcpp toolchain install gcc 16 # host target(不变) +mcpp toolchain install gcc 16 --target x86_64-windows-gnu # 装该 target 的链 +mcpp toolchain install --target aarch64-linux-musl # 省略 family → 词汇表 pin(gcc) +mcpp toolchain default gcc@16 --target x86_64-linux-musl # 默认=(family@ver, target) 一对; + # --target 省略 = host +mcpp toolchain remove gcc@16 --target x86_64-windows-gnu +mcpp toolchain list # §5.1 两块式,Targets 块即词汇表出口 +``` + +**顺带收益**:`mcpp run `(二进制名,cli.cppm:244)与顶级 target 名词的冲突自然消失, +help 文案无需改动。词汇表的发现职责由 `toolchain list` 的 Targets 块(含 planned 行)+ +README 生成表承担,无损失。 + +### 6.2 兼容别名(永久,查表解析;实现载体 = §4.7 `compat.cppm`) + +| 旧拼写 | normalize 到 | +|---|---| +| `musl-gcc@V` / `gcc@V-musl` | `gcc@V` + target `-linux-musl` | +| `-gcc@V`(如 aarch64-linux-musl-gcc) | `gcc@V` + target 对应 triple | +| `mingw@V`(Windows host) | `gcc@V` + target `x86_64-windows-gnu` | +| `mingw-cross@V`(Linux host) | 同上(host 只影响 payload 选取,§4.3 表) | +| `--target x86_64-w64-mingw32` | `--target x86_64-windows-gnu` | +| `clang@V` | `llvm@V`(现状已有,保持) | + +别名解析**不设废弃时限**(解析成本一张表,breaking 无收益);§5.2 的单行提示随本 PR 上线(compat 内一处文案)。 + +### 6.3 manifest 面 + +```toml +[toolchain] # 值域收紧为 family@version(+ msvc@system / msvc@ pin-verify) +linux = "gcc@16" +windows = "gcc@16" # 旧值 "mingw@16.1.0"/"msvc@system" 永久兼容(6.2 表) + +[build] +target = "x86_64-linux-musl" # 新增:默认构建 target(≙ cargo build.target)。 + # 「默认就要 musl 静态」从 toolchain 名字挪到这里——语义归位: + # 全静态是产物属性,不是编译器家族属性 +``` + +`[target.]` override、`[target.'cfg(...)'.build]`、bare-alias 糖全部不变。 + +--- + +## 7. 单 PR 实施计划(0.0.93;沿用 `single-pr-090/091-implementation-plan` 惯例) + +**决策(D8 定案)**:不分期,整套一个 PR。理由:①分期的中间态(S2「校验用新表、拼写还是旧 canonical」) +本身是额外的一致性负担;②改动集中在 registry/prepare/lifecycle 三文件 + 两个新模块,分期会让 +同一批行被改两遍;③repo 已有 0.0.90(MSVC 原生)、0.0.91(c++fly)单 PR 落大特性的先例与节奏。 +**风险对冲**:PR 内按下列八步组织 commit,每步独立编译绿 + 单测绿;e2e 全量门禁收口; +第 1、2 步是纯新增(先立地基),破坏性重写集中在第 3-5 步。 + +| # | 步骤 | 内容 | 验证 | +|---|---|---|---| +| 1 | `triple.cppm`(纯新增) | Triple 类型 + parse/validate + §4.2 封闭词汇表 + §4.6 pin 集中表 | 单测:parse/canonical/别名矩阵、did-you-mean | +| 2 | `compat.cppm`(纯新增) | §4.7 全部旧拼写归一 + 提示文案 | 单测:§6.2 别名表全行 round-trip;存量 config 串归一 | +| 3 | ToolchainSpec v2 + payload 数据表 | registry.cppm 重写:删 `isMusl`/自由串 compiler、删 `display_label`/`matches_default_toolchain` if 链、`available_toolchain_indexes` host-constexpr 分流并入表;两个解析入口首行接 compat | 单测:spec 相等比较替代 default 匹配;既有 33 单测迁移 | +| 4 | 四解析器改 Triple 消费者 | cfgpred(prepare.cppm:62-87)、abi `darwin`→`macos`(abi.cppm:81-83)、model.cppm 谓词薄化、registry 三信号删除(随步 3);canonical 即切 `x86_64-windows-gnu`(target/ 目录名随之;compat 归一保证 `--target x86_64-w64-mingw32` 进同一目录) | e2e 85/91(cfg/bare-alias)全绿;abi e2e 全绿 | +| 5 | `--target` 校验 + 约定表化 | validate + did-you-mean + `[target.X]` 逃生舱;prepare.cppm:651-679 两条硬编码约定改查 §4.2 表 | 新增 e2e:typo 硬错 + did-you-mean;28(musl)、102(wine,**双拼写各跑一遍**)全绿 | +| 6 | CLI/显示 | `toolchain install/default/remove` 接 `--target` 选项(§6.1,单名词面);list 两轴重排 + 数值 semver 排序(§5.1);`[build] target` | 新增 e2e:`install --target`→build 闭环、`build --target` autoInstall 闭环、list 输出断言(排序/分组/planned 行) | +| 7 | 文档 | README 平台表从 §4.2 表重画(补 MSVC=✅、windows-gnu 行、host≠target 维);docs/03 三处漂移(Linux 默认、mingw-cross→target 章节改写)+ docs/zh 同步;CHANGELOG | 文档 review;prepare.cppm:643 注释顺改 | +| 8 | 全量收口 | e2e run_all(基线 102+新增);ci-linux/macos/windows/aarch64/cross-build-test 五工作流全绿;`95-97/99`(msvc/mingw)兼容别名路径断言 | PR 门禁 | + +**PR 外的 follow-up(不阻塞)**:UCRT env 细分、riscv64 行转 verified、clang cross +(`-target`+sysroot 注入)——三者都只是往 §4.2/§4.3 的表里**加行**,这是本方案的可扩展性验收标准。 +别名单行提示(§5.2 note)随本 PR 直接上线(compat 内一处文案)。 + +--- + +## 8. 决策清单(2026-07-15 review 已定案) + +| # | 决策 | 定案 | 当时的备选及代价(存档) | +|---|---|---|---| +| D1 | windows-gnu canonical 拼写 | ✅ **`x86_64-windows-gnu`**(Zig 式,与 linux-musl 三段一致);`x86_64-w64-mingw32` 永久别名 | 保持 GNU 拼写为 canonical:少一次 target/ 目录迁移,但 cfg 词汇与 triple 字面永久错位、vendor 段特例永存 | +| D2 | `mingw`/`mingw-cross` 降级为别名,family 只剩 gcc/llvm/msvc | ✅ **是**(§1 三重错位的根治) | 保留 mingw 为 family(xmake 路线):高频路径少打一个 --target,但正交性破坏、§2.1 特例链保留 | +| D3 | musl 出 toolchain 名、入 target;「默认 musl」= `[build] target` | ✅ 按推荐 | 保留 `-musl` 版本后缀为 canonical:改动小,但同轴四编码之一转正、其余三个仍需特判 | +| D4 | CLI 名词面 | ✅ **单 `toolchain` + `--target` 选项**(2026-07-15 二次 review 改定,§6.1):mcpp 里「加 target=装整条链」,双名词是概念冗余;主 UX 是 `build --target` autoInstall(Zig 式零仪式);`run ` 冲突顺带消失 | 原推荐 rustup 式 `mcpp target add/list/default`:业界心智模型贴合,但其前提(target=只装 std、编译器不动)mcpp 不具备 | +| D5 | 词汇表:os 统一 `macos`(abi 的 darwin 消灭)、arch 保持 `aarch64` | ✅ 按推荐 | — | +| D6 | `--target` 硬校验 + `[target.X]` 逃生舱 | ✅ 按推荐(静默编错是最坏失败模式) | 仅 warning:不破坏任何脚本,但错误继续静默 | +| D7 | tier 标注(verified/published/planned)进 `toolchain list` Targets 块与 README | ✅ 按推荐(Rust tier 先例;README 从表生成对照,根治漂移) | 不标:表更短,但 planned target 继续只活在 CI 注释里 | +| D8 | 落地节奏 | ✅ **不分期,整套单 PR**(§7;canonical 随 PR 一次切换) | 原推荐两期(S2 别名+校验、S3 切拼写):分摊风险,但中间态本身是一致性负担、同批代码改两遍 | +| D9 | 兼容层位置 | ✅ **独立 `src/toolchain/compat.cppm`**(§4.7;核心只见 canonical) | 散在 registry/triple 各解析点:少一个模块,但旧拼写知识扩散、无法整体衡量/删除 | + +## 9. 非目标 + +- **不改 xim 分发包名**(`mingw-cross-gcc`/`musl-gcc`/`aarch64-linux-musl-gcc` 保持)—— + 分发层的 "cross"/triple 命名与业界先例一致(musl.cc、Debian),且改名牵动 xlings-res/xim-pkgindex + 发布链,零收益。 +- 不引入 vendor 段(Zig 论据:实践证明无信息量)。 +- 不做 clang cross(sysroot 注入)——cross-build-test.yml 已登记 planned,与本方案正交, + 落地时只是 §4.3 表加行。 +- 不动 static 语义(target 默认属性 + `[build]` 翻转,现状即业界正解)。 +- 不做 UCRT/MSVCRT env 细分(登记 follow-up,§4.3)。 +- `msvc@system` 模型不动(detection-first 是对的;它已经天然符合 A2——family=msvc、 + version=system、target=x86_64-windows-msvc)。 diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index fc3a364..e0fdd02 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -138,7 +138,7 @@ jobs: - name: "Toolchain: musl-gcc — build mcpp (--target)" run: | "$MCPP" clean - "$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0-musl" build.log + "$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0 → x86_64-linux-musl" build.log - name: "Toolchain: LLVM — build mcpp" run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 06415e3..2f0e285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,53 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.93] — 2026-07-15 + +### 变更(命名统一,全部旧拼写永久兼容) + +- **工具链 × 目标 命名统一 —— 二轴身份模型**。toolchain = `family@version` + (family 只剩 **gcc | llvm | msvc**),target = mcpp 自有三段 triple + `arch-os[-env]`(Zig 式砍 vendor)。变体(gnu/musl/msvc)进 triple env 段, + **"cross"/"musl"/"mingw" 不再是工具链名字**——`mingw-cross 16.1.0` 的本体是 + `gcc@16.1.0 → x86_64-windows-gnu`,交叉只是 host≠target 的关系。业界对照 + (rustup 零 "cross" 命名/Zig 三段 triple/musl.cc 分发层先例)与决策记录见 + `.agents/docs/2026-07-15-toolchain-target-naming-unification-design.md`。 + - **canonical triple**:`x86_64-windows-gnu` 为正典(D1);GNU 拼写 + `x86_64-w64-mingw32` 及 4 段 Rust 拼写为**永久别名**,归一后进同一 + `target//` 目录(同一构建缓存)。macOS 产物目录随 canonical 变为 + `aarch64-macos`。 + - **`--target` 封闭词汇表校验**:打错字**硬错 + did-you-mean** + (`did you mean 'x86_64-linux-musl'?`),不再静默 fall through 编成宿主产物 + (最坏失败模式根治);自定义 triple 走显式 `[target.X]` 节逃生舱;planned + 档位(riscv64 等)报「registered but not yet supported」。两条硬编码约定 + (`*-musl`、`x86_64-w64-mingw32`)改为词汇表数据行(pin + 默认 static)。 + - **单一 triple 解析器 `triple.cppm`**:cfgpred/abi/model 谓词/registry 四处 + 平行解析收敛;abi 的 os 维 `darwin`→`macos`(与 cfg 词汇分叉消灭, + `darwin`/`arm64` 作为约束别名接受)。 + - **`compat.cppm` 兼容层**:唯一知道旧拼写的文件(musl-gcc/gcc@V-musl/ + -gcc/mingw/mingw-cross/clang),归一 + 单行 `note:` 提示;xim 分发包名 + (`mingw-cross-gcc` 等)不动——"cross" 在分发层合法(musl.cc/Debian 先例)。 + - **CLI 单名词 + `--target` 选项**(D4,不设 `mcpp target` 子命令): + `toolchain install [gcc 16] --target `(family 可省→约定 pin)、 + `toolchain default gcc@16 --target `(默认变 **pair**,持久化 + `default` + `default_target` 两键)、`toolchain remove … --target `; + 主路径仍是 `mcpp build --target ` 自动装链(零仪式)。 + - **`[build] target = ""`** 新 manifest 键(≙ cargo `build.target`): + 「默认全静态 musl」的正确归宿(产物属性,非编译器家族属性);优先级 + `--target` flag > `[build] target` > 全局 `default_target` > host。 + - **`toolchain list` 两轴重排**:Toolchains 块(family@version)+ Targets 块 + (target × 状态 installed/available/**planned**,planned 行使词汇表用户可见); + 修版本字典序排序 bug(9.4.0 不再排在 15.1.0 前);`gcc X-musl` 行不再被 + `llvm` 劈开。README 平台表从词汇表重画(target × tier 维度,补 MSVC=✅ 与 + windows-gnu 行——旧表 MSVC 仍标 planned 是错的)。 + - 修 Windows host 上 `mingw` 的门:Linux 上 `toolchain install mingw` 现在 + 合法(= 装交叉 payload,同一身份 host 分流);`mcpp run` 位置参数 help 改为 + 「Binary name」消除与 `--target` 的语义撞名。 + - 验证:单测 35(新增 triple/compat 套件);e2e 新增 103(typo/planned/逃生舱/ + `[build] target`/别名同目录)、102 双拼写断言;本机实测双拼写同 Resolved + 行+同缓存(alias 二跑 0.07s 全命中)、PE wine 真跑、musl 静态链、typo + did-you-mean。 + ## [0.0.92] — 2026-07-15 ### 新增 diff --git a/README.md b/README.md index 1c466be..27c570d 100644 --- a/README.md +++ b/README.md @@ -231,22 +231,39 @@ import mcpplibs.cmdline; ## Platform Support -| OS / arch | GCC (glibc) | GCC (musl) | Clang / LLVM | MSVC | -|------------------|:-----------:|:----------:|:------------:|:----:| -| Linux x86_64 | ✅ | ✅ *default* | ✅ | — | -| Linux aarch64 | 🔄 | ✅ *default* | 🔄 | — | -| macOS arm64 | — | — | ✅ *default* | — | -| macOS x86_64 | — | — | 🔄 | — | -| Windows x86_64 | — | — | ✅ ¹ *default* | 🔄 | - -✅ supported | 🔄 planned - -> *default*: the default toolchain on Linux is musl-gcc; release binaries are fully static musl builds. -> The default toolchain on macOS ARM64 / Windows x86_64 is LLVM/Clang. +mcpp's identity model has two orthogonal axes: a **toolchain** is +`family@version` (family ∈ gcc | llvm | msvc), a **target** is a triple +`arch-os[-env]`. Cross-compiling is just `mcpp build --target ` — +the right toolchain payload is resolved and installed automatically. +`mcpp toolchain list` shows live status on your machine. + +**Hosts** (where mcpp itself runs): Linux x86_64 / aarch64, macOS arm64, Windows x86_64. + +**Targets** (what `--target` accepts; this table mirrors the in-code vocabulary): + +| Target | Convention toolchain | Status | +|---|---|:---:| +| `x86_64-linux-gnu` | gcc *(Linux default)* or llvm | ✅ | +| `x86_64-linux-musl` | gcc 16, fully static | ✅ | +| `aarch64-linux-musl` | gcc 16, fully static — cross from x86_64 (qemu-verified) or native | ✅ | +| `x86_64-windows-gnu` | gcc 16 MinGW-w64 — native on Windows, cross from Linux (wine-verified) | ✅ | +| `x86_64-windows-msvc` | `msvc@system` (detected VS/BuildTools) or llvm ¹ *(Windows default)* | ✅ | +| `aarch64-macos` | llvm *(macOS default)* | ✅ | +| `riscv64-linux-musl` | — | 🔄 | +| `aarch64-linux-gnu` | — | 🔄 | +| `x86_64-macos` | — | 🔄 | + +✅ verified — CI builds **and executes** the artifact end-to-end (qemu/wine included) | 🔄 planned + +> Release binaries for Linux are fully static musl builds (`x86_64-linux-musl`). +> Legacy spellings — `x86_64-w64-mingw32`, `gcc@16.1.0-musl`, `mingw-cross@…`, +> `musl-gcc@…` — stay permanently accepted as aliases and normalize to the +> canonical forms above. > -> ¹ On Windows, Clang/LLVM currently requires an existing **MSVC BuildTools or Visual Studio** installation -> (providing the UCRT, Windows SDK, and MSVC STL). A zero-MSVC `llvm-mingw` route is planned -> ([discussion](https://github.com/mcpp-community/mcpp/issues)). +> ¹ On Windows, llvm requires an existing **MSVC BuildTools or Visual Studio** +> (UCRT, Windows SDK, MSVC STL). The MinGW route (`--target x86_64-windows-gnu`, +> or `mcpp toolchain default gcc@16 --target x86_64-windows-gnu`) needs no +> Visual Studio at all. ## Documentation diff --git a/README.zh-CN.md b/README.zh-CN.md index 020eec7..95d36a4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -231,22 +231,35 @@ import mcpplibs.cmdline; ## 平台支持 -| OS / arch | GCC (glibc) | GCC (musl) | Clang / LLVM | MSVC | -|------------------|:-----------:|:----------:|:------------:|:----:| -| Linux x86_64 | ✅ | ✅ *默认* | ✅ | — | -| Linux aarch64 | 🔄 | ✅ *默认* | 🔄 | — | -| macOS arm64 | — | — | ✅ *默认* | — | -| macOS x86_64 | — | — | 🔄 | — | -| Windows x86_64 | — | — | ✅ ¹ *默认* | 🔄 | - -✅ 已支持 | 🔄 计划中 - -> *默认*:Linux 默认工具链为 musl-gcc,release 二进制走 musl 全静态; -> macOS ARM64 / Windows x86_64 默认工具链均为 LLVM/Clang。 +mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family ∈ gcc | llvm | msvc), +**目标** = 三段 triple `arch-os[-env]`。交叉编译只需 `mcpp build --target `—— +对应的工具链包会自动解析并安装。`mcpp toolchain list` 查看本机实时状态。 + +**宿主**(mcpp 本身运行在哪):Linux x86_64 / aarch64、macOS arm64、Windows x86_64。 + +**目标**(`--target` 接受什么;本表与代码内词汇表同源): + +| Target | 约定工具链 | 状态 | +|---|---|:---:| +| `x86_64-linux-gnu` | gcc(*Linux 默认*)或 llvm | ✅ | +| `x86_64-linux-musl` | gcc 16,全静态 | ✅ | +| `aarch64-linux-musl` | gcc 16,全静态——x86_64 交叉(qemu 实测)或原生 | ✅ | +| `x86_64-windows-gnu` | gcc 16 MinGW-w64——Windows 原生,Linux 交叉(wine 实测) | ✅ | +| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*Windows 默认*) | ✅ | +| `aarch64-macos` | llvm(*macOS 默认*) | ✅ | +| `riscv64-linux-musl` | — | 🔄 | +| `aarch64-linux-gnu` | — | 🔄 | +| `x86_64-macos` | — | 🔄 | + +✅ 已验证——CI 端到端构建**并真实执行**产物(含 qemu/wine)| 🔄 计划中 + +> Linux release 二进制为 musl 全静态构建(`x86_64-linux-musl`)。 +> 旧拼写——`x86_64-w64-mingw32`、`gcc@16.1.0-musl`、`mingw-cross@…`、`musl-gcc@…`—— +> 作为别名**永久接受**,归一到上表的 canonical 形式。 > -> ¹ Windows 上 Clang/LLVM 当前依赖系统已安装 **MSVC BuildTools 或 Visual Studio** -> (提供 UCRT、Windows SDK、MSVC STL)。零-MSVC 依赖的 `llvm-mingw` 路线在规划中 -> ([讨论](https://github.com/mcpp-community/mcpp/issues))。 +> ¹ Windows 上 llvm 依赖已安装的 **MSVC BuildTools 或 Visual Studio**(UCRT、Windows +> SDK、MSVC STL)。MinGW 路线(`--target x86_64-windows-gnu`,或 +> `mcpp toolchain default gcc@16 --target x86_64-windows-gnu`)完全不需要 Visual Studio。 ## 文档 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 560d7a4..4fa68a5 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -16,22 +16,45 @@ Downloading xim:musl-gcc@15.1.0 [====> ] 312 MB / 808 MB 3.7 MB/s Default set to gcc@15.1.0-musl ``` -Linux defaults to `gcc@15.1.0-musl`; macOS defaults to `llvm@20.1.7`. +The first-run default is host-aware: Linux x86_64 → `gcc@16.1.0` (glibc — the +native ABI, so X11/GL/system libraries link out of the box); other Linux +arches (aarch64, …) → `gcc@15.1.0-musl` (self-contained, fully static); +macOS and Windows → `llvm@20.1.7`. Fully-static musl output stays one flag +away on any Linux host: `mcpp build --target x86_64-linux-musl`. Subsequent builds do not trigger this process again. > [!TIP] > In CI or offline environments, you can disable automatic installation by setting `MCPP_NO_AUTO_INSTALL=1`. With this set, if no toolchain is installed, `mcpp build` fails immediately instead of making any network requests. +## The Identity Model: Toolchain × Target + +Two orthogonal axes name everything: + +- **toolchain** = `family@version`, family ∈ `gcc | llvm | msvc` — *who compiles* +- **target** = a triple `arch-os[-env]` (e.g. `x86_64-linux-musl`, + `x86_64-windows-gnu`, `aarch64-macos`) — *what it produces for* + +Variants live in the target's `env` segment (`gnu | musl | msvc`), never in +the toolchain name. "Cross" is not a name either — it's just the relation +`host ≠ target`, and the same command works for both. Legacy spellings +(`musl-gcc`, `gcc@15.1.0-musl`, `mingw`, `mingw-cross`, `clang`, +`x86_64-w64-mingw32`) are **permanently accepted aliases** that normalize to +this model with a one-line `note:` hint. + ## Manual Installation ```bash -mcpp toolchain install gcc 16.1.0 # GNU libc, for the default dynamic-linking case -mcpp toolchain install gcc 15.1.0-musl # musl libc, for fully static builds -mcpp toolchain install musl-gcc 15.1.0 # equivalent to the line above -mcpp toolchain install llvm 20.1.7 # LLVM/Clang, the default toolchain on macOS +mcpp toolchain install gcc 16.1.0 # host target (GNU libc on Linux) +mcpp toolchain install llvm 20.1.7 # LLVM/Clang, the default on macOS/Windows +mcpp toolchain install gcc 16 --target x86_64-linux-musl # musl target payload +mcpp toolchain install --target x86_64-windows-gnu # family omitted → the + # target's convention pin (gcc@16.1.0) ``` +Explicit installation is mostly for CI cache warm-up and offline prep — +`mcpp build --target ` auto-installs whatever the target needs. + Version numbers support partial matching: ```bash @@ -41,56 +64,76 @@ mcpp toolchain install gcc@16 # the @ form works too ## Switching the Default Toolchain +The default is a *pair* — toolchain axis + target axis (target omitted = host): + ```bash mcpp toolchain default gcc@16.1.0 -mcpp toolchain default gcc 15 # with a partial version, picks the highest installed match +mcpp toolchain default gcc 15 # partial version → highest installed match +mcpp toolchain default gcc@16 --target x86_64-linux-musl # "default to fully-static musl" ``` +The pair persists as `[toolchain] default = "gcc@16.1.0"` + +`default_target = "x86_64-linux-musl"` in `~/.mcpp/config.toml`. (Older +configs with combined spellings like `default = "gcc@15.1.0-musl"` keep +working unchanged.) + ## Inspecting Toolchain Status ```bash mcpp toolchain list ``` -The output looks like this: +The output has two blocks — one per axis: ``` -Installed: - TOOLCHAIN BINARY - gcc 15.1.0-musl @mcpp/registry/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-linux-musl-g++ - * gcc 16.1.0 @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++ - -Available (run `mcpp toolchain install `): - TOOLCHAIN - gcc 13.3.0 - gcc 11.5.0 - ... +Toolchains: + * gcc 16.1.0 (default) + gcc 15.1.0 + llvm 22.1.8 + +Targets: + TARGET NOTE TOOLCHAIN STATUS + x86_64-linux-gnu host gcc 16.1.0 installed + * x86_64-linux-musl static gcc 16.1.0 installed + x86_64-windows-gnu PE, static, cross gcc 16.1.0 installed + aarch64-linux-musl static, cross gcc 16.1.0 available + riscv64-linux-musl static, cross — planned + +Available toolchains (run `mcpp toolchain install `): + gcc 15.1.0 / 13.3.0 / 11.5.0 / 9.4.0 + llvm 20.1.7 ``` -The entry marked with `*` is the current default toolchain. `@mcpp/...` is shorthand for `~/.mcpp/...`, used to keep the output narrower. +`*` marks the default pair. The Targets block is the live view of the target +vocabulary: `installed` payloads, `available` targets this host can install, +and `planned` targets that are registered but not yet shipped. -## MinGW (Windows-native GCC, no Visual Studio required) +## Windows PE via MinGW-w64 (`x86_64-windows-gnu`, no Visual Studio required) -On Windows, `mingw` installs a self-contained MinGW-w64 GCC (winlibs -standalone build, UCRT runtime) into mcpp's sandbox — the same managed-xpkg -model as `gcc`/`llvm`, no Visual Studio needed: +"MinGW" in mcpp is a **target**, not a toolchain name: `x86_64-windows-gnu` +— GCC producing Windows PE with the GNU CRT. The same identity works from +both hosts; which self-contained payload serves it is resolved automatically +(Windows host → winlibs UCRT build; Linux host → the from-source MSVCRT +cross toolchain, wine-verified in CI): ```bash -mcpp toolchain install mingw 16.1.0 -mcpp toolchain default mingw@16.1.0 +mcpp build --target x86_64-windows-gnu # from Windows OR Linux +mcpp toolchain default gcc@16 --target x86_64-windows-gnu +# legacy spellings still accepted: mingw@16.1.0, mingw-cross@16.1.0, +# --target x86_64-w64-mingw32 ``` It uses the regular GCC module pipeline (`gcm.cache`, `import std` via -libstdc++'s `bits/std.cc`). Produced binaries statically link libstdc++ and -libgcc by default, so they run standalone — no `libstdc++-6.dll` needs to -travel next to your exe (opt out with `[build] static_stdlib = false`). -`[build] linkage = "static"` upgrades that to a fully static link. +libstdc++'s `bits/std.cc`). The target's default linkage is **static** — +the produced `.exe` is fully self-contained (no `libstdc++-6.dll` to ship, +runs directly under wine); `[build] linkage = "dynamic"` opts out. In a manifest: ```toml [toolchain] -windows = "mingw@16.1.0" +windows = "gcc@16" # gcc family on Windows = MinGW-w64 +# legacy value "mingw@16.1.0" keeps working ``` ## MSVC (System Toolchain, Windows) @@ -157,13 +200,42 @@ macos = "llvm@20" A project-level declaration takes precedence over the global default configuration. -## Cross-Toolchain Builds +## Targets & Cross Builds ```bash -mcpp build --target x86_64-linux-musl +mcpp build --target x86_64-linux-musl # fully static ELF +mcpp build --target aarch64-linux-musl # cross-arch (aarch64 on x86_64) +mcpp build --target x86_64-windows-gnu # Windows PE from Linux +``` + +`--target` is validated against the known-target vocabulary (see the README +platform table, which mirrors it): a typo is a **hard error with a +suggestion** (`did you mean 'x86_64-linux-musl'?`) — never a silent host +build. Custom triples outside the vocabulary are allowed when an explicit +`[target.]` section declares them in `mcpp.toml`. + +Each known target carries a convention: its pinned toolchain (installed on +demand) and its default linkage (`*-linux-musl` and `x86_64-windows-gnu` +default to static). An explicit `[target.]` section overrides both: + +```toml +[target.x86_64-linux-musl] +toolchain = "gcc@16.1.0" +linkage = "static" +``` + +A project can set its *default* build target — this is where "this project +ships fully-static" belongs (static output is a product property, not a +compiler-family property): + +```toml +[build] +target = "x86_64-linux-musl" # ≙ cargo's build.target ``` -mcpp reads the `[target.x86_64-linux-musl]` section in `mcpp.toml`, overriding the default toolchain and linkage settings. Combined with `mcpp pack --mode static`, this lets you produce a fully static release package; for a complete example, see [`examples/03-pack-static`](../examples/03-pack-static/). +Combined with `mcpp pack --mode static` this produces a fully static release +package; for a complete example, see +[`examples/03-pack-static`](../examples/03-pack-static/). ## Uninstalling diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 892c583..25e51df 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -141,6 +141,8 @@ cflags = ["-DFOO=1"] # Extra C compile flags cxxflags = ["-DBAR=2"] # Extra C++ compile flags (do not put -std=... here) ldflags = ["-lfoo"] # Extra link flags static_stdlib = true # Statically link libstdc++ (default true) +target = "x86_64-linux-musl" # Default build target when no --target is passed + # (≙ cargo build.target; e.g. "ship fully-static") macos_deployment_target = "14.0" # Minimum supported OS version for macOS artifacts (macOS only) ``` diff --git a/docs/08-toolchain-internals.md b/docs/08-toolchain-internals.md index 1047039..cfb7e57 100644 --- a/docs/08-toolchain-internals.md +++ b/docs/08-toolchain-internals.md @@ -41,12 +41,19 @@ Two principles run through everything: ## 2. Toolchain resolution -A toolchain spec (`gcc@16.1.0`, `llvm@22.1.8`, `gcc@15.1.0-musl`) maps to an -xim package (`src/toolchain/registry.cppm`: `parse_toolchain_spec` → -`to_xim_package`, producing an `XimToolchainPackage` with the xim name, -version, and frontend candidates). The payload is resolved/auto-installed via -the xlings backend into the sandbox -(`$MCPP_HOME/registry/data/xpkgs/xim-x-//`). +Since 0.0.93 identity is two orthogonal axes: a `ToolchainSpec` is +`(family ∈ gcc|llvm|msvc, version, target Triple)`. `triple.cppm` is the +single triple parser + the closed known-target vocabulary; `compat.cppm` is +the only file that knows legacy spellings (`gcc@15.1.0-musl`, `musl-gcc`, +`mingw`, `mingw-cross`, `clang`, `-gcc` — normalized on parse, +permanently accepted). `to_xim_package` is a *(family, target, host)* payload +mapping producing an `XimToolchainPackage` with the xim name, version, and +frontend candidates — this is where host-split distribution names like +`mingw-cross-gcc` (Linux host) vs `mingw-gcc` (Windows host) live; they are +current distribution-layer identity, not user-facing spellings. The payload +is resolved/auto-installed via the xlings backend into the sandbox +(`$MCPP_HOME/registry/data/xpkgs/xim-x-//`). See +`.agents/docs/2026-07-15-toolchain-target-naming-unification-design.md`. `detect`/`probe` (`src/toolchain/detect.cppm`, `probe.cppm`) then derive: @@ -200,9 +207,11 @@ machine. `xim:linux-headers`). Follow the llvm/gcc packaging SOP including the admission gate (`verify-toolchain.sh`): completeness + hermetic CRT resolution + a real compile/link/run before an asset ships. -2. **Registry** (`src/toolchain/registry.cppm`): teach - `parse_toolchain_spec`/`to_xim_package` the spec spelling, xim package - name, and `frontendCandidates` (which binary is the C++ driver). +2. **Vocabulary + registry**: add the target row to `triple.cppm`'s + `kKnownTargets` (tier/pin/defaultStatic), then teach + `to_xim_package` (`src/toolchain/registry.cppm`) the *(family, target, + host)* → xim package row and its `frontendCandidates` (which binary is + the C++ driver). Legacy spellings, if any, go in `compat.cppm` only. 3. **Capabilities** (`src/toolchain/provider.cppm`): stdlib identity, BMI traits, and feature switches consumed by `flags.cppm`. 4. **Fixup kind** (`post_install.cppm`): decide what post-install alignment diff --git a/docs/zh/03-toolchains.md b/docs/zh/03-toolchains.md index 499b5a7..45c75b1 100644 --- a/docs/zh/03-toolchains.md +++ b/docs/zh/03-toolchains.md @@ -21,7 +21,11 @@ Downloading xim:musl-gcc@15.1.0 [====> ] 312 MB / 808 MB 3.7 MB/s Default set to gcc@15.1.0-musl ``` -Linux 默认使用 `gcc@15.1.0-musl`; macOS 默认使用 `llvm@20.1.7`。 +首跑默认是 host-aware 的:Linux x86_64 → `gcc@16.1.0`(glibc——平台原生 +ABI,X11/GL/系统库开箱即链);其他 Linux arch(aarch64 等)→ +`gcc@15.1.0-musl`(自包含,全静态);macOS 与 Windows → `llvm@20.1.7`。 +任何 Linux 宿主上,全静态 musl 产物始终只差一个参数: +`mcpp build --target x86_64-linux-musl`。 后续构建不再触发该流程。 @@ -30,15 +34,33 @@ Linux 默认使用 `gcc@15.1.0-musl`; macOS 默认使用 `llvm@20.1.7`。 > 安装行为。此时若未安装工具链,`mcpp build` 将直接报错而不会发起 > 网络请求。 +## 身份模型:Toolchain × Target + +一切命名由两条正交轴构成: + +- **toolchain** = `family@version`,family ∈ `gcc | llvm | msvc` ——*用谁编* +- **target** = 三段 triple `arch-os[-env]`(如 `x86_64-linux-musl`、 + `x86_64-windows-gnu`、`aarch64-macos`)——*产出给谁* + +变体(`gnu | musl | msvc`)在 target 的 `env` 段里,永远不进工具链名字; +"cross" 也不是名字——它只是 `host ≠ target` 这一关系,原生与交叉是同一条 +命令。旧拼写(`musl-gcc`、`gcc@15.1.0-musl`、`mingw`、`mingw-cross`、 +`clang`、`x86_64-w64-mingw32`)作为别名**永久接受**,归一到该模型并打印 +一行 `note:` 提示。 + ## 手动安装 ```bash -mcpp toolchain install gcc 16.1.0 # GNU libc,适用于动态链接默认场景 -mcpp toolchain install gcc 15.1.0-musl # musl libc,适用于全静态构建 -mcpp toolchain install musl-gcc 15.1.0 # 等价于上一条 -mcpp toolchain install llvm 20.1.7 # LLVM/Clang,macOS 默认工具链 +mcpp toolchain install gcc 16.1.0 # host target(Linux 上为 GNU libc) +mcpp toolchain install llvm 20.1.7 # LLVM/Clang,macOS/Windows 默认工具链 +mcpp toolchain install gcc 16 --target x86_64-linux-musl # musl target 的链 +mcpp toolchain install --target x86_64-windows-gnu # 省略 family → + # 取该 target 的约定 pin(gcc@16.1.0) ``` +显式安装主要用于 CI 缓存预热与离线准备——`mcpp build --target ` +会自动安装该 target 所需的一切。 + 版本号支持部分匹配: ```bash @@ -48,34 +70,75 @@ mcpp toolchain install gcc@16 # 同样支持 @ 形式 ## 切换默认工具链 +默认值是一*对*——toolchain 轴 + target 轴(省略 target = host): + ```bash mcpp toolchain default gcc@16.1.0 mcpp toolchain default gcc 15 # 部分版本时,从已安装的版本中选择最高 +mcpp toolchain default gcc@16 --target x86_64-linux-musl # "默认就要全静态 musl" ``` +这对默认值持久化为 `~/.mcpp/config.toml` 中的 +`[toolchain] default = "gcc@16.1.0"` + `default_target = "x86_64-linux-musl"`。 +(存量 config 里 `default = "gcc@15.1.0-musl"` 这类合并拼写原样可用。) + ## 查看工具链状态 ```bash mcpp toolchain list ``` -输出形式如下: +输出分两块——每条轴一块: ``` -Installed: - TOOLCHAIN BINARY - gcc 15.1.0-musl @mcpp/registry/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-linux-musl-g++ - * gcc 16.1.0 @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++ +Toolchains: + * gcc 16.1.0 (default) + gcc 15.1.0 + llvm 22.1.8 + +Targets: + TARGET NOTE TOOLCHAIN STATUS + x86_64-linux-gnu host gcc 16.1.0 installed + * x86_64-linux-musl static gcc 16.1.0 installed + x86_64-windows-gnu PE, static, cross gcc 16.1.0 installed + aarch64-linux-musl static, cross gcc 16.1.0 available + riscv64-linux-musl static, cross — planned + +Available toolchains (run `mcpp toolchain install `): + gcc 15.1.0 / 13.3.0 / 11.5.0 / 9.4.0 + llvm 20.1.7 +``` + +`*` 标记当前的默认对。Targets 块是 target 词汇表的实时视图:`installed` +为已装的链,`available` 为本宿主可安装的 target,`planned` 为已登记但尚未 +发布的 target。 + +## Windows PE 之 MinGW-w64(`x86_64-windows-gnu`,无需 Visual Studio) -Available (run `mcpp toolchain install `): - TOOLCHAIN - gcc 13.3.0 - gcc 11.5.0 - ... +mcpp 里 "MinGW" 是一个 **target**,不是工具链名:`x86_64-windows-gnu` +——GCC 产出 Windows PE(GNU CRT)。两种宿主用同一个身份、同一条命令; +由哪个自包含 payload 来承接是自动分流的(Windows 宿主 → winlibs UCRT +构建;Linux 宿主 → 从源码构建的 MSVCRT 交叉链,CI 中经 wine 实测): + +```bash +mcpp build --target x86_64-windows-gnu # Windows 或 Linux 上皆可 +mcpp toolchain default gcc@16 --target x86_64-windows-gnu +# 旧拼写仍然接受:mingw@16.1.0、mingw-cross@16.1.0、 +# --target x86_64-w64-mingw32 ``` -带有 `*` 标记的条目为当前默认工具链。`@mcpp/...` 是 `~/.mcpp/...` -的简写形式,用于减少输出宽度。 +它走常规的 GCC 模块管线(`gcm.cache`、经 libstdc++ `bits/std.cc` 的 +`import std`)。该 target 默认 linkage 为 **static**——产出的 `.exe` +完全自包含(无需随包分发 `libstdc++-6.dll`,可直接在 wine 下运行); +`[build] linkage = "dynamic"` 可退出。 + +manifest 中: + +```toml +[toolchain] +windows = "gcc@16" # Windows 上的 gcc family = MinGW-w64 +# 旧值 "mingw@16.1.0" 原样可用 +``` ## 项目级版本锁定 @@ -93,15 +156,38 @@ macos = "llvm@20" 项目级声明优先于全局默认配置。 -## 跨工具链构建 +## Target 与交叉构建 ```bash -mcpp build --target x86_64-linux-musl +mcpp build --target x86_64-linux-musl # 全静态 ELF +mcpp build --target aarch64-linux-musl # 跨 arch(x86_64 上出 aarch64) +mcpp build --target x86_64-windows-gnu # Linux 上出 Windows PE +``` + +`--target` 会对已知 target 词汇表做校验(README 平台表与之同源):打错 +字是**硬错误并附建议**(`did you mean 'x86_64-linux-musl'?`)——绝不会 +静默退回宿主构建。词汇表之外的自定义 triple,在 `mcpp.toml` 中显式声明 +`[target.]` 节即可放行(逃生舱)。 + +每个已知 target 自带约定:pin 的工具链(按需自动安装)与默认 linkage +(`*-linux-musl` 与 `x86_64-windows-gnu` 默认 static)。显式的 +`[target.]` 节可同时覆盖两者: + +```toml +[target.x86_64-linux-musl] +toolchain = "gcc@16.1.0" +linkage = "static" +``` + +项目还可以声明自己的*默认*构建 target——"本项目发布全静态"这类语义 +就该放在这里(全静态是产物属性,不是编译器家族属性): + +```toml +[build] +target = "x86_64-linux-musl" # ≙ cargo 的 build.target ``` -mcpp 将读取 `mcpp.toml` 中 `[target.x86_64-linux-musl]` 节,覆盖默认的 -工具链与 linkage 设置。该机制配合 `mcpp pack --mode static` 可生成 -全静态发布包,完整示例参见 +配合 `mcpp pack --mode static` 即可产出全静态发布包,完整示例参见 [`examples/03-pack-static`](../../examples/03-pack-static/)。 ## 卸载 diff --git a/mcpp.toml b/mcpp.toml index bf58e45..52a51a5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.92" +version = "0.0.93" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index fa49709..5e21c1b 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -25,6 +25,7 @@ import mcpp.toolchain.registry; import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; +import mcpp.toolchain.triple; import mcpp.build.plan; import mcpp.build.build_program; import mcpp.lockfile; @@ -56,33 +57,29 @@ namespace cfgpred { struct Ctx { std::string os, arch, family, env; }; -// Derive the cfg context from the resolved --target triple, falling back to the -// host for a native build. OS/arch/env detection mirrors abi_profile's -// substring approach (toolchain/abi.cppm) so the vocabulary is consistent. +// Derive the cfg context from the resolved --target triple, falling back to +// the host for a native build. Parsing goes through triple.cppm — the single +// triple parser — so the cfg vocabulary IS the canonical triple vocabulary +// (os: linux|macos|windows, arch: GNU spellings, env: gnu|musl|msvc), and +// alias spellings ("x86_64-w64-mingw32") evaluate identically to canonical. inline Ctx context_for(std::string_view targetTriple) { + namespace triple = mcpp::toolchain::triple; Ctx c; - if (targetTriple.empty()) { - c.os = std::string(mcpp::platform::name); // host: linux/macos/windows - c.arch = std::string(mcpp::platform::host_arch); // host: x86_64/aarch64/... + auto t = targetTriple.empty() + ? std::optional(triple::host_triple()) + : triple::parse(targetTriple); + if (t) { + c.os = t->os; + c.arch = t->arch; + c.env = t->env; + c.family = t->family(); } else { - auto has = [&](std::string_view n){ return targetTriple.find(n) != std::string_view::npos; }; - c.os = has("windows") || has("mingw") ? "windows" - : has("darwin") || has("apple") || has("macos") ? "macos" - : has("linux") ? "linux" : ""; + // Escape-hatch triple outside the language: only the leading arch + // segment is derivable; other dimensions stay empty (never match). auto dash = targetTriple.find('-'); c.arch = std::string(dash == std::string_view::npos ? targetTriple : targetTriple.substr(0, dash)); } - c.family = (c.os == "linux" || c.os == "macos") ? "unix" - : (c.os == "windows") ? "windows" : ""; - // env (libc/abi): musl/gnu on linux, msvc on windows; substring or host default. - if (!targetTriple.empty()) { - auto has = [&](std::string_view n){ return targetTriple.find(n) != std::string_view::npos; }; - c.env = has("musl") ? "musl" : has("msvc") ? "msvc" - : (has("gnu") || c.os == "linux") ? "gnu" : ""; - } else { - c.env = c.os == "linux" ? "gnu" : c.os == "windows" ? "msvc" : ""; - } return c; } @@ -153,7 +150,16 @@ inline bool matches(const std::string& predicate, const Ctx& c, std::string_view Parser p{ predicate, 0, c }; return p.expr(); } - return !triple.empty() && predicate == triple; // bare-triple exact match + // Bare-triple match, spelling-independent: a `[target.x86_64-w64-mingw32]` + // key matches a resolved `x86_64-windows-gnu` build (and vice versa) — + // both normalize through triple::parse. Unparseable keys (the explicit- + // section escape hatch) fall back to exact string comparison. + if (triple.empty()) return false; + if (auto p = mcpp::toolchain::triple::parse(predicate)) { + if (auto rt = mcpp::toolchain::triple::parse(triple)) + return p->str() == rt->str(); + } + return predicate == triple; } } // namespace cfgpred @@ -162,7 +168,12 @@ export std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc, const mcpp::toolchain::Fingerprint& fp, const std::filesystem::path& root) { + // Canonical triple names the output directory (D1: `target/ + // x86_64-windows-gnu/`, not the GNU spelling the compiler reports via + // -dumpmachine) — alias inputs land in the same directory. Triples + // outside the language keep their raw spelling. auto triple = tc.targetTriple.empty() ? std::string{"unknown"} : tc.targetTriple; + if (auto t = mcpp::toolchain::triple::parse(triple)) triple = t->str(); return root / "target" / triple / fp.hex; } @@ -625,58 +636,78 @@ prepare_build(bool print_fingerprint, } // ─── --target / --static overrides ────────────────────────────────── - // Look up [target.] from manifest; fall back to convention - // (anything ending with "-musl" → gcc@-musl + static). - auto endswith = [](std::string_view s, std::string_view suf) { - return s.size() >= suf.size() - && s.compare(s.size() - suf.size(), suf.size(), suf) == 0; - }; + // Target-axis default resolution when no --target flag was passed: + // [build] target (project default, ≙ cargo build.target) > + // [toolchain] default_target (global config) > host. + if (overrides.target_triple.empty() && !m->buildConfig.target.empty()) + overrides.target_triple = m->buildConfig.target; + if (overrides.target_triple.empty()) { + if (auto cfg = get_cfg(); cfg && !(*cfg)->defaultTarget.empty()) + overrides.target_triple = (*cfg)->defaultTarget; + } + // Normalize the triple (alias spellings → canonical), validate against + // the known-target vocabulary, then apply the manifest [target.] + // override and the vocabulary-table convention (pin + default linkage). if (!overrides.target_triple.empty()) { + namespace triple = mcpp::toolchain::triple; + auto parsed = triple::parse(overrides.target_triple); + + // [target.X] lookup is spelling-independent: a section keyed + // `x86_64-w64-mingw32` matches `--target x86_64-windows-gnu` and + // vice versa. Unparseable keys/inputs compare exactly (escape hatch). auto it = m->targetOverrides.find(overrides.target_triple); - if (it != m->targetOverrides.end()) { + if (it == m->targetOverrides.end() && parsed) { + for (auto o = m->targetOverrides.begin(); + o != m->targetOverrides.end(); ++o) { + if (auto k = triple::parse(o->first); + k && k->str() == parsed->str()) { it = o; break; } + } + } + bool hasExplicitSection = it != m->targetOverrides.end(); + bool hasToolchainOverride = hasExplicitSection + && !it->second.toolchain.empty(); + const triple::TargetInfo* known = + parsed ? triple::find_known_target(*parsed) : nullptr; + + // Validation: a typo must never silently fall through to the host + // toolchain (the worst failure mode — you think you cross-compiled). + // An explicit [target.X] section is the escape hatch for custom + // triples outside the vocabulary. + if (!known && !hasExplicitSection) { + auto sug = triple::did_you_mean(overrides.target_triple); + return std::unexpected(std::format( + "unknown target '{}'{}\n" + " known targets: `mcpp toolchain list`; a custom triple needs an\n" + " explicit [target.{}] section in mcpp.toml", + overrides.target_triple, + sug ? std::format(" — did you mean '{}'?", *sug) : "", + overrides.target_triple)); + } + if (known && known->tier == "planned" && !hasToolchainOverride) { + return std::unexpected(std::format( + "target '{}' is registered but not yet supported (planned) — " + "no toolchain is published for it yet.\n" + " An explicit [target.{}] toolchain override can opt in early.", + parsed->str(), parsed->str())); + } + // Canonical from here on: cfg evaluation, spec attachment and the + // target/ output directory all see one spelling. + if (parsed) overrides.target_triple = parsed->str(); + + if (hasExplicitSection) { if (!it->second.toolchain.empty()) tcSpec = it->second.toolchain; if (!it->second.linkage.empty()) m->buildConfig.linkage = it->second.linkage; } - // Convention: "*-musl" target without an explicit `[target.X]` - // override gets a canonical musl toolchain spec. The choice is - // host-aware: - // - target arch == host arch → NATIVE build, use `gcc@15.1.0-musl` - // (→ xim:musl-gcc; XLINGS_RES picks the host-matching asset). - // - target arch != host arch → CROSS build, use the target-named - // cross toolchain `-gcc@15.1.0` (→ xim:-gcc), - // e.g. building aarch64 on an x86_64 host. - // Both native and cross musl default to gcc 16.1.0 — GCC 15 drops - // module template instantiations at link (remediation doc A2; - // packages shipped 2026-07-08/09, stripped, GitHub+GitCode). - if (endswith(overrides.target_triple, "-musl") - && (it == m->targetOverrides.end() || it->second.toolchain.empty())) - { - auto dash = overrides.target_triple.find('-'); - std::string targetArch = dash == std::string::npos - ? overrides.target_triple - : overrides.target_triple.substr(0, dash); - if (targetArch.empty() || targetArch == mcpp::platform::host_arch) - tcSpec = "gcc@16.1.0-musl"; // native - else - tcSpec = overrides.target_triple + "-gcc@16.1.0"; // cross - } - if (endswith(overrides.target_triple, "-musl") - && m->buildConfig.linkage.empty()) { + // Convention from the vocabulary table (triple.cppm): the target's + // pinned toolchain (host-awareness — native musl-gcc vs triple-named + // cross, winlibs mingw vs Linux-hosted cross — lives in the payload + // mapping, not here) and its default linkage. GCC 16 pin rationale: + // GCC 15 drops module template instantiations at link (remediation + // doc A2; packages shipped 2026-07-08/09, GitHub+GitCode). + if (known && !hasToolchainOverride && !known->pin.empty()) + tcSpec = std::string(known->pin); + if (known && known->defaultStatic && m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static"; - } - // Convention: the Windows PE cross target `x86_64-w64-mingw32` without - // an explicit [target.X] override resolves to the from-source GCC-16 - // MSVCRT cross toolchain. host≠target — an ELF frontend producing PE. - // Default static linkage: MinGW standalone-exe convention and the clean - // path for running the artifact under wine (no DLL deployment needed). - // See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md. - if (overrides.target_triple == "x86_64-w64-mingw32" - && (it == m->targetOverrides.end() || it->second.toolchain.empty())) - { - tcSpec = "mingw-cross@16.1.0"; - if (m->buildConfig.linkage.empty()) - m->buildConfig.linkage = "static"; - } } if (overrides.force_static) m->buildConfig.linkage = "static"; @@ -732,11 +763,15 @@ prepare_build(bool print_fingerprint, "[toolchain].{} = '{}' is invalid; expected '@'", kCurrentPlatform, *tcSpec)); } - // For a cross `--target ` build, carry the triple into the spec - // so a musl toolchain resolves its `-g++` cross frontend - // (e.g. aarch64-linux-musl-g++) instead of the host x86_64 one. - if (spec->isMusl && !overrides.target_triple.empty()) - spec->targetTriple = overrides.target_triple; + // A `--target ` build carries the (already canonical) triple + // into the spec's target axis: the payload mapping then resolves the + // right package/frontend (e.g. aarch64-linux-musl-g++ for a cross + // musl build, never the host g++). Escape-hatch triples outside the + // language don't parse and leave the spec on the host target. + if (!overrides.target_triple.empty()) { + if (auto t = mcpp::toolchain::triple::parse(overrides.target_triple)) + spec->target = *t; + } auto pkg = mcpp::toolchain::to_xim_package(*spec); auto cfg = get_cfg(); @@ -761,8 +796,10 @@ prepare_build(bool print_fingerprint, // [toolchain] path previously ran none, so a freshly auto-installed // payload kept its stale install-time cfg / unpatched runtime libs. mcpp::toolchain::ensure_post_install_fixup(**cfg, payload->root, pkg); + // Canonical rendering, whatever spelling the manifest/config used: + // "Resolved gcc@16.1.0 → x86_64-linux-musl → ". mcpp::ui::info("Resolved", - std::format("{} → {}", *tcSpec, + std::format("{} → {}", spec->display(), mcpp::ui::shorten_path(explicit_compiler, mcpp::fetcher::make_path_ctx(&**get_cfg(), *root)))); } else if (tcSpec.has_value() && *tcSpec == "system") { @@ -771,20 +808,23 @@ prepare_build(bool print_fingerprint, // CI / offline / test opt-out: hard-error instead of silently // pulling ~800 MB of toolchain. Preserves the original M5.5 // contract for environments that need it. + namespace pins = mcpp::toolchain::triple::pins; if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) { - return std::unexpected( + return std::unexpected(std::format( "no toolchain configured.\n" " run one of:\n" - " mcpp toolchain install llvm 20.1.7\n" - " mcpp toolchain default llvm@20.1.7\n" - " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install."); + " mcpp toolchain install {}\n" + " mcpp toolchain default {}\n" + " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install.", + pins::kSuggestLlvm, pins::kFirstRunMacWin)); } else { - return std::unexpected( + return std::unexpected(std::format( "no toolchain configured.\n" " run one of:\n" - " mcpp toolchain install gcc 15.1.0-musl\n" - " mcpp toolchain default gcc@15.1.0-musl\n" - " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install."); + " mcpp toolchain install {}\n" + " mcpp toolchain default {}\n" + " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install.", + pins::kSuggestGccMusl, pins::kFirstRunLinuxOther)); } } else { // First-run UX: no project-level [toolchain], no global default, @@ -809,21 +849,20 @@ prepare_build(bool print_fingerprint, // 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. + namespace pins = mcpp::toolchain::triple::pins; std::string defaultSpec; if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) { - defaultSpec = "llvm@20.1.7"; + defaultSpec = std::string(pins::kFirstRunMacWin); } else if (mcpp::platform::host_arch == std::string_view("x86_64")) { - defaultSpec = "gcc@16.1.0"; + defaultSpec = std::string(pins::kFirstRunLinuxX86_64); } else { - defaultSpec = "gcc@15.1.0-musl"; + defaultSpec = std::string(pins::kFirstRunLinuxOther); } - bool muslDefault = defaultSpec.find("-musl") != std::string::npos; auto defaultParsed = mcpp::toolchain::parse_toolchain_spec(defaultSpec); - // Host-native musl default has no --target, so seed the triple so the - // resolver finds the `-linux-musl-g++` frontend. - if (muslDefault) - defaultParsed->targetTriple = - std::string(mcpp::platform::host_arch) + "-linux-musl"; + // The legacy "-musl" spelling normalizes to (gcc, -linux-musl), + // so the resolver finds the `-linux-musl-g++` frontend + // without any manual triple seeding. + bool muslDefault = defaultParsed->target.is_musl(); auto defaultPkg = mcpp::toolchain::to_xim_package(*defaultParsed); if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) { @@ -854,9 +893,8 @@ prepare_build(bool print_fingerprint, return std::unexpected(std::format( "auto-installing default toolchain {} failed: {}\n" " you can install it manually with:\n" - " mcpp toolchain install {} {}", - defaultSpec, payload.error().message, - defaultParsed->compiler, defaultParsed->version)); + " mcpp toolchain install {}", + defaultSpec, payload.error().message, defaultSpec)); } explicit_compiler = mcpp::toolchain::toolchain_frontend(payload->binDir, defaultPkg); if (!std::filesystem::exists(explicit_compiler)) { @@ -901,7 +939,7 @@ prepare_build(bool print_fingerprint, // For musl-gcc the toolchain is fully self-contained // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. - bool isMuslTc = tc->targetTriple.find("-musl") != std::string::npos; + bool isMuslTc = mcpp::toolchain::is_musl_target(*tc); // A musl toolchain only really makes sense with static linkage — // dynamic-musl binaries depend on a system /lib/ld-musl-x86_64.so.1 diff --git a/src/cli.cppm b/src/cli.cppm index a7375e6..71e2db2 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -241,7 +241,9 @@ int run(int argc, char** argv) { .action(wrap_rc(cmd_build))) .subcommand(cl::App("run") .description("Build + run a binary target (after `--`, args are passed to it)") - .arg(cl::Arg("target").help("Binary target name (optional)")) + // NB: this positional is a BINARY NAME from [[bin]]/src layout — + // unrelated to `--target ` (the cross-target axis). + .arg(cl::Arg("target").help("Binary name (optional)")) .action(wrap_rc([&passthrough](const cl::ParsedArgs& p) { return cmd_run(p, std::span(passthrough)); }))) @@ -343,17 +345,26 @@ int run(int argc, char** argv) { // Both `mcpp toolchain install gcc 16.1.0` and `mcpp toolchain // install gcc@16.1.0` are accepted, and the version may be // partial (`15`, `15.1`) — mcpp resolves to the highest match. - .arg(cl::Arg("compiler").help("e.g. gcc, gcc@16.1.0, gcc@15-musl").required()) - .arg(cl::Arg("version").help("e.g. 16.1.0, 15, 15.1, 15.1.0-musl"))) + // With --target the family may be omitted entirely (taken from + // the target's convention pin): + // mcpp toolchain install --target x86_64-windows-gnu + .arg(cl::Arg("compiler").help("gcc | llvm | msvc (or gcc@16.1.0; legacy aliases accepted)")) + .arg(cl::Arg("version").help("e.g. 16.1.0, 15, 15.1")) + .option(cl::Option("target").takes_value().help( + "Install the toolchain payload for (e.g. x86_64-windows-gnu)"))) .subcommand(cl::App("default") - .description("Set the default toolchain") + .description("Set the default toolchain (and optionally the default target)") // Same dual-form as `install`: `gcc@16.1.0` or `gcc 16.1.0`, // partial versions allowed. - .arg(cl::Arg("spec").help("[@] (version may be partial)").required()) - .arg(cl::Arg("version").help("(optional, alternative to @-form)"))) + .arg(cl::Arg("spec").help("[@] (version may be partial)").required()) + .arg(cl::Arg("version").help("(optional, alternative to @-form)")) + .option(cl::Option("target").takes_value().help( + "Default build target (omit = host)"))) .subcommand(cl::App("remove") .description("Uninstall a toolchain") - .arg(cl::Arg("spec").help("@").required())) + .arg(cl::Arg("spec").help("@").required()) + .option(cl::Option("target").takes_value().help( + "Remove the payload for instead of the host one"))) .action(wrap_rc(cmd_toolchain))) .subcommand(cl::App("cache") .description("Inspect and manage the global BMI cache") diff --git a/src/cli/cmd_toolchain.cppm b/src/cli/cmd_toolchain.cppm index 3992034..a6b42df 100644 --- a/src/cli/cmd_toolchain.cppm +++ b/src/cli/cmd_toolchain.cppm @@ -31,16 +31,21 @@ export int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) { mcpp::fetcher::make_bootstrap_progress_callback()); if (!cfg) { mcpp::ui::error(cfg.error().message); return 4; } + // The optional --target axis (install/default/remove): which target's + // payload the command operates on. Omitted = host. + std::string targetArg; + if (auto t = sub_parsed.value("target")) targetArg = *t; + if (subname == "list") return mcpp::toolchain::toolchain_list(*cfg); if (subname == "install") return mcpp::toolchain::toolchain_install( - *cfg, sub_parsed.positional(0), sub_parsed.positional(1)); + *cfg, sub_parsed.positional(0), sub_parsed.positional(1), targetArg); if (subname == "default") return mcpp::toolchain::toolchain_set_default( - *cfg, sub_parsed.positional(0), sub_parsed.positional(1)); + *cfg, sub_parsed.positional(0), sub_parsed.positional(1), targetArg); if (subname == "remove") - return mcpp::toolchain::toolchain_remove(*cfg, sub_parsed.positional(0)); + return mcpp::toolchain::toolchain_remove(*cfg, sub_parsed.positional(0), targetArg); mcpp::ui::error(std::format("unknown toolchain subcommand '{}'", subname)); return 2; diff --git a/src/config.cppm b/src/config.cppm index ee7be2a..dda011c 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -73,6 +73,10 @@ struct GlobalConfig { // Empty means no global default; mcpp will hard-error unless the project // mcpp.toml declares its own [toolchain]. std::string defaultToolchain; + // default_target = "" e.g. "x86_64-linux-musl" + // The target axis of the default pair (toolchain = family@version and + // target = triple are orthogonal axes). Empty means host. + std::string defaultTarget; // Resolved xlings home (registryDir unless overridden) std::filesystem::path xlingsHome() const { @@ -251,6 +255,10 @@ void print_env(const GlobalConfig& cfg); std::expected write_default_toolchain(const GlobalConfig& cfg, std::string_view spec); +// Persist [toolchain].default_target ("" = host / clear). +std::expected +write_default_target(const GlobalConfig& cfg, std::string_view targetTriple); + } // namespace mcpp::config namespace mcpp::config { @@ -505,6 +513,7 @@ std::expected load_or_init( cfg.defaultJobs = doc->get_int("build.default_jobs").value_or(0); cfg.defaultBackend = doc->get_string("build.default_backend").value_or("ninja"); cfg.defaultToolchain = doc->get_string("toolchain.default").value_or(""); + cfg.defaultTarget = doc->get_string("toolchain.default_target").value_or(""); // [log] section — re-initialize logger with config values { @@ -751,14 +760,15 @@ bool ensure_project_index_dir( // M5.5: persist [toolchain].default into config.toml without disturbing // other fields. Naive: read text, replace/insert one line. std::expected -write_default_toolchain(const GlobalConfig& cfg, std::string_view spec) { +write_toolchain_key(const GlobalConfig& cfg, std::string_view key, + std::string_view value) { std::ifstream is(cfg.configFile); if (!is) return std::unexpected(ConfigError{ std::format("cannot open '{}'", cfg.configFile.string())}); std::stringstream ss; ss << is.rdbuf(); std::string text = ss.str(); - std::string line = std::format("default = \"{}\"\n", spec); + std::string line = std::format("{} = \"{}\"\n", key, value); auto sectionPos = text.find("[toolchain]"); if (sectionPos == std::string::npos) { @@ -766,18 +776,34 @@ write_default_toolchain(const GlobalConfig& cfg, std::string_view spec) { if (!text.empty() && text.back() != '\n') text += '\n'; text += std::format("\n[toolchain]\n{}", line); } else { - // Locate existing `default = ...` within [toolchain]. If absent, - // insert just after the section header. + // Locate an existing ` = ...` line within [toolchain]. Match at + // line starts with a word boundary after the key, so `default` never + // matches the `default_target` line (and vice versa). auto eol = text.find('\n', sectionPos); if (eol == std::string::npos) eol = text.size(); auto bodyStart = (eol == text.size()) ? text.size() : eol + 1; auto nextSec = text.find("\n[", bodyStart); auto bodyEnd = (nextSec == std::string::npos) ? text.size() : nextSec; auto body = std::string_view(text).substr(bodyStart, bodyEnd - bodyStart); - auto k = body.find("default"); - if (k != std::string_view::npos) { - // replace that whole line - auto kAbs = bodyStart + k; + + std::size_t kRel = std::string_view::npos; + for (std::size_t p = 0; p < body.size();) { + auto lineEnd = body.find('\n', p); + if (lineEnd == std::string_view::npos) lineEnd = body.size(); + auto l = body.substr(p, lineEnd - p); + auto ws = l.find_first_not_of(" \t"); + if (ws != std::string_view::npos && l.substr(ws).starts_with(key)) { + auto after = l.substr(ws + key.size()); + if (after.empty() || after[0] == ' ' || after[0] == '\t' + || after[0] == '=') { + kRel = p + ws; + break; + } + } + p = lineEnd + 1; + } + if (kRel != std::string_view::npos) { + auto kAbs = bodyStart + kRel; auto lineEnd = text.find('\n', kAbs); if (lineEnd == std::string::npos) lineEnd = text.size(); text.replace(kAbs, lineEnd - kAbs + 1, line); @@ -793,4 +819,14 @@ write_default_toolchain(const GlobalConfig& cfg, std::string_view spec) { return {}; } +std::expected +write_default_toolchain(const GlobalConfig& cfg, std::string_view spec) { + return write_toolchain_key(cfg, "default", spec); +} + +std::expected +write_default_target(const GlobalConfig& cfg, std::string_view targetTriple) { + return write_toolchain_key(cfg, "default_target", targetTriple); +} + } // namespace mcpp::config diff --git a/src/doctor.cppm b/src/doctor.cppm index a077b47..21ed50f 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -260,21 +260,26 @@ export int doctor_report() { bool anyMissing = false; if (std::filesystem::exists(pkgsDir, ec)) { - // Mirror `mcpp toolchain list`: each xim-x-//bin + // Mirror `mcpp toolchain list`: each xim-x-//bin // holds one installed toolchain frontend (clang++/g++/musl-gcc-…). for (auto& entry : std::filesystem::directory_iterator(pkgsDir, ec)) { auto name = entry.path().filename().string(); if (name.rfind("xim-x-", 0) != 0) continue; // toolchains only - std::string compiler = name.substr(std::string("xim-x-").size()); + auto id = mcpp::toolchain::identify_xim_payload( + name.substr(std::string("xim-x-").size())); + if (!id) continue; // not a compiler pkg for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) { + mcpp::toolchain::ToolchainSpec s; + s.family = id->family; + s.version = vEntry.path().filename().string(); + s.target = id->target; auto bin = mcpp::toolchain::toolchain_frontend( - vEntry.path() / "bin", compiler); - if (bin.empty()) continue; // not a compiler pkg + vEntry.path() / "bin", mcpp::toolchain::to_xim_package(s)); + if (bin.empty()) continue; sawAny = true; - auto label = mcpp::toolchain::display_label( - compiler, vEntry.path().filename().string()); + auto label = s.display(); // readelf is part of binutils, always present in our sandbox. auto cmd = std::format("readelf -d \"{}\"", bin.string()); diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index be18e6e..a50df68 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -649,6 +649,7 @@ std::expected parse_string(std::string_view content, m.buildConfig.dialectCxxflags = *v; if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v; if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v; + if (auto v = doc->get_string("build.target")) m.buildConfig.target = *v; if (auto v = doc->get_string("build.default-profile")) m.buildConfig.defaultProfile = *v; else if (auto v = doc->get_string("build.profile")) m.buildConfig.defaultProfile = *v; // accepted alias diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index c79a553..32ce31c 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -145,6 +145,11 @@ struct BuildConfig { // time from --static / --target / [target.].linkage. Wired // through to ninja backend as the `-static` link flag. std::string linkage; + // [build] target = "" — the project's default build target + // (≙ cargo's build.target). Used when no --target flag is passed; + // "default to fully-static musl" belongs here, not in a toolchain name + // (static output is a product property, not a compiler-family property). + std::string target; // M5.x C-language support. `cflags` / `cxxflags` are appended verbatim // to the per-rule baseline (see `ninja_backend` cflags / cxxflags). // `cStandard` controls -std= for the C compile rule (.c files). diff --git a/src/toolchain/abi.cppm b/src/toolchain/abi.cppm index 34343fa..8725c4e 100644 --- a/src/toolchain/abi.cppm +++ b/src/toolchain/abi.cppm @@ -24,6 +24,7 @@ export module mcpp.toolchain.abi; import std; import mcpp.toolchain.model; +import mcpp.toolchain.triple; export namespace mcpp::toolchain { @@ -61,37 +62,46 @@ struct AbiProfile { } }; -// The single derivation site. libc/arch/os come from the target triple; -// cxxStdlib/cxxAbi come from the compiler. NO cross-dimension inference -// (that conflation was the bug). +// The single derivation site. libc/arch/os come from the target triple +// (parsed by triple.cppm — the one triple parser); cxxStdlib/cxxAbi come +// from the compiler. NO cross-dimension inference (that conflation was the +// bug). os vocabulary is the canonical one: linux | macos | windows — +// this used to say "darwin" while cfg() said "macos" for the same concept; +// converged on "macos" (parse_abi_capability accepts "darwin" as an alias). inline AbiProfile abi_profile(const Toolchain& tc) { AbiProfile p; + + if (auto t = mcpp::toolchain::triple::parse(tc.targetTriple)) { + p.arch = t->arch; + p.os = t->os; + // libc — the C runtime ABI a C library links against. Windows is + // msvcrt for BOTH env=msvc and env=gnu (MinGW links the Windows CRT). + if (t->is_musl()) p.libc = "musl"; + else if (t->os == "macos") p.libc = "macos"; + else if (t->os == "windows") p.libc = "msvcrt"; + else if (t->os == "linux") p.libc = "glibc"; // *-linux-gnu (gcc AND clang+libc++) + p.cxxStdlib = tc.stdlibId; + p.cxxAbi = (tc.compiler == CompilerId::MSVC || t->is_msvc_env()) + ? "msvc" : "itanium"; + return p; + } + + // Fallback for triples outside the language (leave dims empty = + // don't-care where nothing is recognizable). const std::string& t = tc.targetTriple; auto has = [&](std::string_view s) { return t.find(s) != std::string::npos; }; - - // libc — the C runtime ABI a C library links against. if (has("musl")) p.libc = "musl"; else if (has("darwin") || has("apple")) p.libc = "macos"; else if (has("msvc")) p.libc = "msvcrt"; - else if (has("windows")) p.libc = "msvcrt"; // mingw → windows CRT - else if (has("linux") || has("gnu")) p.libc = "glibc"; // *-linux-gnu (gcc AND clang+libc++) - // else: leave empty (unknown) → don't-care - - // os + else if (has("windows")) p.libc = "msvcrt"; + else if (has("linux") || has("gnu")) p.libc = "glibc"; if (has("linux")) p.os = "linux"; - else if (has("darwin") || has("apple")) p.os = "darwin"; + else if (has("darwin") || has("apple")) p.os = "macos"; else if (has("windows")) p.os = "windows"; - - // arch — leading triple segment ("x86_64-linux-gnu" → "x86_64"). if (auto dash = t.find('-'); dash != std::string::npos) p.arch = t.substr(0, dash); else p.arch = t; - - // cxxStdlib — already canonical on the toolchain. p.cxxStdlib = tc.stdlibId; - - // cxxAbi p.cxxAbi = (tc.compiler == CompilerId::MSVC || has("msvc")) ? "msvc" : "itanium"; - return p; } @@ -127,7 +137,11 @@ parse_abi_capability(std::string_view cap, std::string_view source = {}) { else if (dimStr == "os") dim = AbiDim::Os; else if (dimStr == "cxxabi") dim = AbiDim::CxxAbi; else return std::nullopt; - return AbiConstraint{ dim, std::string(val), std::string(source) }; + // Vocabulary aliases → canonical (profiles emit macos / aarch64). + std::string v(val); + if (dim == AbiDim::Os && v == "darwin") v = "macos"; + if (dim == AbiDim::Arch && v == "arm64") v = "aarch64"; + return AbiConstraint{ dim, std::move(v), std::string(source) }; } struct AbiMismatch { diff --git a/src/toolchain/compat.cppm b/src/toolchain/compat.cppm new file mode 100644 index 0000000..b044be5 --- /dev/null +++ b/src/toolchain/compat.cppm @@ -0,0 +1,162 @@ +// mcpp.toolchain.compat — legacy-spelling compatibility layer. +// +// THE ONLY FILE that knows pre-0.0.93 toolchain/triple spellings. Core code +// (registry, prepare, lifecycle) sees canonical forms exclusively; the two +// public parse entry points call normalize_* first. Deleting this module +// would break exactly one thing: old inputs — never a canonical path. +// +// Owns four responsibilities (design §4.7): +// 1. spec aliases musl-gcc@V / gcc@V-musl / -gcc@V / mingw@V / +// mingw-cross@V / clang@V → (family, version, target) +// 2. triple aliases handled by triple::parse itself (GNU/LLVM/Apple +// spellings are grammar, not legacy) — compat only +// decides WHEN a compiler token is really a triple +// 3. persisted-state migration: old config/manifest spec strings normalize +// on the read path via the same normalize_spec +// 4. the one-line canonical hint text (printed at most once per process) +// +// NOT compat: xim package names (mingw-cross-gcc, musl-gcc, …). Those are +// the distribution layer's CURRENT identity — "cross" is legitimate there +// (musl.cc's -cross tarballs, Debian's g++-mingw-w64 precedent) — and they +// are produced by registry.cppm's payload mapping, not parsed from users. +// +// See .agents/docs/2026-07-15-toolchain-target-naming-unification-design.md. + +module; +#include + +export module mcpp.toolchain.compat; + +import std; +import mcpp.platform; +import mcpp.toolchain.triple; + +export namespace mcpp::toolchain::compat { + +// A user/config spec token pair, normalized to the two-axis identity model. +struct NormalizedSpec { + std::string family; // "gcc" | "llvm" | "msvc" + std::string version; // numeric (possibly partial), or "system"; never "-musl"-suffixed + triple::Triple target; // empty = host + // Set when a legacy spelling was rewritten; `hint` is the one-line note. + bool changed = false; + std::string hint; +}; + +// Normalize a (compiler, version) token pair. The caller has already split +// a combined "name@ver" form. Returns nullopt for a compiler token outside +// the family set and its aliases — the caller owns the error message. +std::optional normalize_spec(std::string_view compiler, + std::string_view version); + +// Print a normalization hint at most once per process (quiet by design: +// note-level, aliases are permanently supported — this is a pointer to the +// canonical spelling, not a deprecation warning). +void print_hint_once(std::string_view hint); + +} // namespace mcpp::toolchain::compat + +namespace mcpp::toolchain::compat { + +namespace { + +bool ends_with(std::string_view s, std::string_view suf) { + return s.size() >= suf.size() + && s.compare(s.size() - suf.size(), suf.size(), suf) == 0; +} + +std::string_view strip_namespace(std::string_view compiler) { + if (auto colon = compiler.find(':'); colon != std::string_view::npos) + return compiler.substr(colon + 1); + return compiler; +} + +triple::Triple host_musl_triple() { + triple::Triple t; + t.arch = std::string(mcpp::platform::host_arch); + t.os = "linux"; + t.env = "musl"; + return t; +} + +triple::Triple windows_gnu_triple() { + return { "x86_64", "windows", "gnu" }; +} + +NormalizedSpec with_hint(NormalizedSpec s, std::string_view oldSpelling) { + s.changed = true; + std::string canonical = std::format("{}@{}", s.family, + s.version.empty() ? std::string("") : s.version); + if (!s.target.empty()) + canonical += std::format(" targeting '{}'", s.target.str()); + s.hint = std::format("'{}' is now {}", oldSpelling, canonical); + return s; +} + +} // namespace + +std::optional normalize_spec(std::string_view compilerIn, + std::string_view versionIn) { + std::string_view compiler = strip_namespace(compilerIn); + std::string version(versionIn); + + // Legacy: musl flavor as a version suffix ("gcc@15.1.0-musl", "15-musl"). + bool muslVersionSuffix = ends_with(version, "-musl"); + if (muslVersionSuffix) version.resize(version.size() - 5); + + NormalizedSpec out; + out.version = version; + + // ── canonical families pass through ───────────────────────────────────── + if (compiler == "gcc" || compiler == "llvm" || compiler == "msvc") { + out.family = std::string(compiler); + if (muslVersionSuffix && compiler == "gcc") { + out.target = host_musl_triple(); + return with_hint(std::move(out), + std::format("{}@{}-musl", compiler, version)); + } + if (muslVersionSuffix) return std::nullopt; // llvm/msvc have no musl flavor + return out; + } + + // ── legacy spellings ───────────────────────────────────────────────────── + if (compiler == "clang") { // alias family → llvm + out.family = "llvm"; + return with_hint(std::move(out), std::format("clang@{}", version)); + } + if (compiler == "musl-gcc") { // musl as compiler-name prefix + out.family = "gcc"; + out.target = host_musl_triple(); + return with_hint(std::move(out), std::format("musl-gcc@{}", version)); + } + if (compiler == "mingw" || compiler == "mingw-cross" + || compiler == "mingw-gcc" || compiler == "mingw-cross-gcc") { + // One concept, two host-split legacy names: GCC targeting Windows PE + // (GNU CRT). Which payload serves it (native winlibs vs Linux-hosted + // cross) is decided by registry's payload mapping from the HOST — + // never by the name the user typed. + out.family = "gcc"; + out.target = windows_gnu_triple(); + return with_hint(std::move(out), std::format("{}@{}", compiler, version)); + } + if (ends_with(compiler, "-gcc")) { // triple-named: aarch64-linux-musl-gcc + auto tripleStr = compiler.substr(0, compiler.size() - 4); + if (auto t = triple::parse(tripleStr)) { + out.family = "gcc"; + out.target = *t; + return with_hint(std::move(out), std::format("{}@{}", compiler, version)); + } + } + + return std::nullopt; +} + +void print_hint_once(std::string_view hint) { + if (hint.empty()) return; + static bool printed = false; + if (printed) return; + printed = true; + std::println(stderr, "note: {}", hint); +} + +} // namespace mcpp::toolchain::compat diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index b9b24ef..9c947fd 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.92"; +inline constexpr std::string_view MCPP_VERSION = "0.0.93"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 5dd278d..6ea5ae1 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -17,6 +17,7 @@ import mcpp.platform; import mcpp.toolchain.detect; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; +import mcpp.toolchain.triple; import mcpp.toolchain.post_install; import mcpp.ui; import mcpp.log; @@ -183,18 +184,6 @@ int msvc_wrong_host() { return 1; } -// MinGW is a Windows-native toolchain package; on other hosts fail with a -// clear message instead of the confusing empty-version xim error -// (`invalid xpkg target 'xim:mingw-gcc@'`). -bool mingw_wrong_host(const mcpp::toolchain::XimToolchainPackage& pkg) { - if (pkg.ximName == "mingw-gcc" && !mcpp::platform::is_windows) { - mcpp::ui::error("mingw is a Windows-only toolchain (MinGW-w64 GCC); " - "on this host use gcc/llvm instead"); - return true; - } - return false; -} - void msvc_print_detected(const mcpp::toolchain::msvc::MsvcInstallation& inst) { mcpp::ui::status("Detected", std::format( "msvc {}{} (VC tools {})", @@ -219,141 +208,297 @@ EffectiveDefault effective_default_toolchain(const mcpp::config::GlobalConfig& c return { cfg.defaultToolchain, false }; } +// Numeric semver comparison for display ordering (newest first). The old +// lexicographic sort put "9.4.0" above "15.1.0" — two-digit majors are the +// norm for GCC, so compare component-wise. +bool version_greater(const std::string& a, const std::string& b) { + auto ca = parse_version_components(a); + auto cb = parse_version_components(b); + if (ca != cb) return cb < ca; + return b < a; // stable tie-break for non-numeric tails +} + export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { auto pkgsDir = cfg.xlingsHome() / "data" / "xpkgs"; auto effective = effective_default_toolchain(cfg); - auto pathCtx = mcpp::fetcher::make_path_ctx(&cfg); - struct Row { - std::string compiler; - std::string version; - std::filesystem::path bin; - bool isDefault = false; - }; - std::vector installed; - std::error_code ec; - if (std::filesystem::exists(pkgsDir, ec)) { - for (auto& entry : std::filesystem::directory_iterator(pkgsDir, ec)) { - auto name = entry.path().filename().string(); - std::string compiler = name; - if (auto sep = name.find("-x-"); sep != std::string::npos) - compiler = name.substr(sep + 3); - for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) { - auto bin = mcpp::toolchain::toolchain_frontend( - vEntry.path() / "bin", compiler); - if (bin.empty()) continue; - Row r; - r.compiler = compiler; - r.version = vEntry.path().filename().string(); - r.bin = bin; - r.isDefault = mcpp::toolchain::matches_default_toolchain( - effective.spec, r.compiler, r.version); - installed.push_back(std::move(r)); - } + // The default is a PAIR: toolchain axis (family@version) + target axis. + // Legacy default strings ("gcc@15.1.0-musl", "mingw-cross@16.1.0") carry + // their target inside the spelling — compat normalization surfaces it. + std::optional defSpec; + if (auto s = mcpp::toolchain::parse_toolchain_spec(effective.spec); s) + defSpec = *s; + mcpp::toolchain::triple::Triple defTarget; + if (defSpec) defTarget = defSpec->target; + if (!cfg.defaultTarget.empty()) { + if (auto t = mcpp::toolchain::triple::parse(cfg.defaultTarget)) + defTarget = *t; + } + auto hostT = mcpp::toolchain::triple::host_triple(); + + // ── enumerate installed payloads → (identity, version, frontend) ──── + struct Payload { + mcpp::toolchain::PayloadIdentity id; + std::string version; + std::filesystem::path bin; + }; + std::vector payloads; + std::error_code ec; + if (std::filesystem::exists(pkgsDir, ec)) { + for (auto& entry : std::filesystem::directory_iterator(pkgsDir, ec)) { + auto name = entry.path().filename().string(); + if (auto sep = name.find("-x-"); sep != std::string::npos) + name = name.substr(sep + 3); + auto id = mcpp::toolchain::identify_xim_payload(name); + if (!id) continue; // ninja/glibc/… — not a toolchain + for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) { + if (!vEntry.is_directory(ec)) continue; + mcpp::toolchain::ToolchainSpec s; + s.family = id->family; + s.version = vEntry.path().filename().string(); + s.target = id->target; + auto pkg = mcpp::toolchain::to_xim_package(s); + auto bin = mcpp::toolchain::toolchain_frontend(vEntry.path() / "bin", pkg); + if (bin.empty()) continue; + payloads.push_back({ *id, s.version, bin }); } } + } - if (installed.empty()) { - std::println("(no toolchains installed — run `mcpp toolchain install gcc 16.1.0` " - "or just `mcpp build` to auto-install the default)"); + // ── Toolchains block: the family@version axis ──────────────────────── + struct FamVer { mcpp::toolchain::Family family; std::string version; }; + std::vector famvers; + for (auto& p : payloads) { + bool seen = std::any_of(famvers.begin(), famvers.end(), [&](const FamVer& f){ + return f.family == p.id.family && f.version == p.version; }); + if (!seen) famvers.push_back({ p.id.family, p.version }); + } + std::sort(famvers.begin(), famvers.end(), [](const FamVer& a, const FamVer& b){ + if (a.family != b.family) + return mcpp::toolchain::family_name(a.family) < mcpp::toolchain::family_name(b.family); + return version_greater(a.version, b.version); + }); + + if (payloads.empty()) { + std::println("(no toolchains installed — run `mcpp toolchain install gcc 16.1.0` " + "or just `mcpp build` to auto-install the default)"); + } else { + std::println("Toolchains:"); + for (auto& f : famvers) { + bool isDefault = defSpec + && mcpp::toolchain::spec_matches_payload(*defSpec, { f.family, {} }, f.version); + std::println(" {:<3}{:<22}{}", + isDefault ? "*" : "", + std::format("{} {}", mcpp::toolchain::family_name(f.family), f.version), + isDefault ? " (default)" : ""); + } + if (effective.fromProject) { + std::println(" (* = effective toolchain from project mcpp.toml " + "[toolchain]; global default is '{}')", + cfg.defaultToolchain.empty() ? "" + : cfg.defaultToolchain); + } + } + + // ─── System section (Windows: detected MSVC) ──────────────────────── + // MSVC is never in xpkgs — it's located on the machine. Show it so + // `toolchain list` reflects everything `toolchain default` accepts. + if (mcpp::platform::is_windows) { + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + bool isDefault = defSpec + && defSpec->family == mcpp::toolchain::Family::Msvc; + std::println(""); + std::println("System:"); + std::println(" {:<3}{:<22} {}", + isDefault ? "*" : "", + std::format("msvc {}", inst->display_version()), + inst->clPath.string()); } else { - std::println("Installed:"); - std::println(" {:<3}{:<22} {}", "", "TOOLCHAIN", "BINARY"); - for (auto& r : installed) { - std::println(" {:<3}{:<22} {}", - r.isDefault ? "*" : "", - mcpp::toolchain::display_label(r.compiler, r.version), - mcpp::ui::shorten_path(r.bin, pathCtx)); - } - // Explain the `*` when it reflects a project override rather than - // the global default, so it never silently disagrees with what a - // build in this directory resolves. - if (effective.fromProject) { - std::println(" (* = effective toolchain from project mcpp.toml " - "[toolchain]; global default is '{}')", - cfg.defaultToolchain.empty() ? "" - : cfg.defaultToolchain); - } + std::println(""); + std::println(" (msvc: not detected — run `mcpp toolchain default msvc` " + "for setup guidance)"); } + } - // ─── System section (Windows: detected MSVC) ──────────────────── - // MSVC is never in xpkgs — it's located on the machine. Show it so - // `toolchain list` reflects everything `toolchain default` accepts. - if (mcpp::platform::is_windows) { - if (auto inst = mcpp::toolchain::msvc::detect_installation()) { - bool isDefault = mcpp::toolchain::matches_default_toolchain( - effective.spec, "msvc", inst->display_version()); - std::println(""); - std::println("System:"); - std::println(" {:<3}{:<22} {}", - isDefault ? "*" : "", - mcpp::toolchain::display_label("msvc", inst->display_version()), - inst->clPath.string()); - } else { - std::println(""); - std::println(" (msvc: not detected — run `mcpp toolchain default msvc` " - "for setup guidance)"); - } + // ── Targets block: the target axis (installed payload rows + the known- + // target vocabulary so available/planned targets are discoverable + // without reading CI configs) ─────────────────────────────────────── + struct TargetRow { + std::string target; // canonical triple + std::string note; // "host" / "static" / "PE" / "cross" tags + std::string toolchain; // "gcc 16.1.0" or "—" + std::string status; // installed | available | planned + bool isDefault = false; + int rank = 0; // display order: installed < available < planned + }; + std::vector targetRows; + + auto note_for = [&](const mcpp::toolchain::triple::Triple& t) { + std::vector tags; + if (t == hostT) tags.push_back("host"); + if (auto* info = mcpp::toolchain::triple::find_known_target(t)) { + if (!info->note.empty()) tags.emplace_back(info->note); + if (info->defaultStatic) tags.push_back("static"); } + if (t != hostT && (t.os != hostT.os || t.arch != hostT.arch)) + tags.push_back("cross"); + std::string out; + for (auto& tag : tags) { if (!out.empty()) out += ", "; out += tag; } + return out; + }; + + for (auto& p : payloads) { + auto t = p.id.target.empty() ? hostT : p.id.target; + std::string tcLabel = std::format("{} {}", + mcpp::toolchain::family_name(p.id.family), p.version); + bool dup = std::any_of(targetRows.begin(), targetRows.end(), + [&](const TargetRow& r){ return r.target == t.str() && r.toolchain == tcLabel; }); + if (dup) continue; + TargetRow r; + r.target = t.str(); + r.note = note_for(t); + r.toolchain = tcLabel; + r.status = "installed"; + r.rank = 0; + r.isDefault = defSpec + && mcpp::toolchain::spec_matches_payload(*defSpec, p.id, p.version) + && t == (defTarget.empty() ? hostT : defTarget); + targetRows.push_back(std::move(r)); + } - // ─── Available section ────────────────────────────────────────── - // List xim:gcc + xim:musl-gcc versions known to the local index - // that aren't already installed. Helpful to discover what users - // can install without going to a website. - struct AvailRow { std::string compiler; std::string version; }; - std::vector avail; - auto add_avail = [&](std::string_view ximName, std::string_view compiler) { - auto versions = list_available_xpkg_versions(cfg, ximName); + // Vocabulary rows not covered by an installed payload. Only list a + // verified target as "available" when this host can actually install it. + auto installable_here = [&](const mcpp::toolchain::triple::Triple& t) { + if (t.os == "linux") + return mcpp::platform::is_linux + && (t.is_musl() || t.arch == hostT.arch); + if (t.is_windows_gnu()) + return mcpp::platform::is_linux || mcpp::platform::is_windows; + if (t.os == "windows") return bool(mcpp::platform::is_windows); + if (t.os == "macos") return bool(mcpp::platform::is_macos); + return false; + }; + for (auto& info : mcpp::toolchain::triple::known_targets()) { + bool covered = std::any_of(targetRows.begin(), targetRows.end(), + [&](const TargetRow& r){ return r.target == info.canonical; }); + if (covered) continue; + auto t = mcpp::toolchain::triple::parse(info.canonical); + if (!t) continue; + bool planned = info.tier == "planned"; + if (!planned && !installable_here(*t)) continue; + TargetRow r; + r.target = std::string(info.canonical); + r.note = note_for(*t); + // Render the pin in the same "family version" shape as installed rows. + std::string pin(info.pin); + if (auto at = pin.find('@'); at != std::string::npos) pin[at] = ' '; + r.toolchain = pin.empty() ? "—" : pin; + r.status = planned ? "planned" : "available"; + r.rank = planned ? 2 : 1; + targetRows.push_back(std::move(r)); + } + std::sort(targetRows.begin(), targetRows.end(), + [](const TargetRow& a, const TargetRow& b){ + if (a.rank != b.rank) return a.rank < b.rank; + return a.target < b.target; + }); + + if (!targetRows.empty()) { + std::println(""); + std::println("Targets:"); + std::println(" {:<3}{:<24}{:<22}{:<18}{}", + "", "TARGET", "NOTE", "TOOLCHAIN", "STATUS"); + for (auto& r : targetRows) { + std::println(" {:<3}{:<24}{:<22}{:<18}{}", + r.isDefault ? "*" : "", + r.target, r.note, r.toolchain, r.status); + } + } + + // ── Available toolchains: versions known to the local index, per family, + // excluding family@version pairs already installed ────────────────── + std::map> avail; + for (auto& idx : mcpp::toolchain::available_toolchain_indexes()) { + for (auto& v : list_available_xpkg_versions(cfg, idx.ximName)) { + // Skip the `["latest"] = { ref = … }` alias entries. + if (v.empty() || !std::isdigit(static_cast(v[0]))) + continue; + bool installed = std::any_of(famvers.begin(), famvers.end(), + [&](const FamVer& f){ return f.family == idx.family && f.version == v; }); + if (installed) continue; + auto& vec = avail[idx.family]; + if (std::find(vec.begin(), vec.end(), v) == vec.end()) + vec.push_back(v); + } + } + if (!avail.empty()) { + std::println(""); + std::println("Available toolchains (run `mcpp toolchain install `):"); + for (auto& [family, versions] : avail) { + std::sort(versions.begin(), versions.end(), version_greater); + std::string joined; for (auto& v : versions) { - // xim packages declare a `["latest"] = { ref = "X.Y.Z" }` - // alias entry; it shows up here but isn't a version users - // can `mcpp toolchain install` directly. Skip non-numeric - // entries. - if (v.empty() || !std::isdigit(static_cast(v[0]))) - continue; - bool already = std::any_of(installed.begin(), installed.end(), - [&](const Row& r){ return r.compiler == compiler && r.version == v; }); - if (!already) avail.push_back({std::string(compiler), v}); - } - }; - for (auto& [ximName, compiler] : mcpp::toolchain::available_toolchain_indexes()) - add_avail(ximName, compiler); - - if (!avail.empty()) { - // Newest first. Lexicographic-on-strings is good enough for - // semver-shaped versions — any "X.Y.Z" with single-digit - // segments compares the way users expect. - std::sort(avail.begin(), avail.end(), - [](const AvailRow& a, const AvailRow& b) { - if (a.compiler != b.compiler) return a.compiler < b.compiler; - return b.version < a.version; - }); - if (!installed.empty()) std::println(""); - std::println("Available (run `mcpp toolchain install `):"); - std::println(" {:<3}{}", "", "TOOLCHAIN"); - for (auto& a : avail) { - std::println(" {:<3}{}", "", - mcpp::toolchain::display_label(a.compiler, a.version)); + if (!joined.empty()) joined += " / "; + joined += v; } + std::println(" {:<3}{} {}", "", + mcpp::toolchain::family_name(family), joined); } - return 0; } + return 0; +} + +// Parse-and-attach the optional `--target ` axis onto a spec. +// Validates against the known-target vocabulary (with did-you-mean). +// Returns non-zero exit code on error, 0 on success. +int attach_target_arg(mcpp::toolchain::ToolchainSpec& spec, + const std::string& targetArg) { + if (targetArg.empty()) return 0; + auto t = mcpp::toolchain::triple::parse(targetArg); + if (!t || !mcpp::toolchain::triple::is_known_target(*t)) { + auto suggestion = mcpp::toolchain::triple::did_you_mean(targetArg); + mcpp::ui::error(std::format( + "unknown target '{}'{}\n known targets: run `mcpp toolchain list`", + targetArg, + suggestion ? std::format(" — did you mean '{}'?", *suggestion) : "")); + return 2; + } + spec.target = *t; + return 0; +} -// `mcpp toolchain install ` — install + post-install fixups. +// `mcpp toolchain install [--target ]` — install + fixups. export int toolchain_install(const mcpp::config::GlobalConfig& cfg, - const std::string& pos0, const std::string& pos1) { - // Accept three input shapes — they all collapse to (compiler, version): + const std::string& pos0, const std::string& pos1, + const std::string& targetArg = {}) { + // Accept three input shapes — they all collapse to (family, version): // mcpp toolchain install gcc 16.1.0 → ("gcc", "16.1.0") // mcpp toolchain install gcc@16.1.0 → ("gcc", "16.1.0") // mcpp toolchain install gcc 15 → ("gcc", "15") partial + // plus the target axis: + // mcpp toolchain install gcc 16 --target x86_64-windows-gnu + // mcpp toolchain install --target x86_64-linux-musl (family from pin) // (parsed before the bootstrap check: system toolchains need no // bootstrap, and an invalid spec should not report a bootstrap error) + std::string pos0Eff = pos0; + if (pos0Eff.empty() && !targetArg.empty()) { + // Family omitted: take the target's convention pin (gcc@16.1.0 + // for musl / windows-gnu targets). + if (auto t = mcpp::toolchain::triple::parse(targetArg)) { + if (auto* info = mcpp::toolchain::triple::find_known_target(*t); + info && !info->pin.empty()) + pos0Eff = std::string(info->pin); + } + } auto spec = mcpp::toolchain::parse_toolchain_spec( - pos0, pos1); - if (!spec || spec->compiler.empty()) { - mcpp::ui::error("missing compiler name; e.g. `mcpp toolchain install gcc 16.1.0`"); + pos0Eff, pos1); + if (!spec) { + mcpp::ui::error(std::format( + "{}; e.g. `mcpp toolchain install gcc 16.1.0`", spec.error())); return 2; } + mcpp::toolchain::print_compat_hint(*spec); + if (int rc = attach_target_arg(*spec, targetArg); rc != 0) return rc; // msvc@system: mcpp never installs MSVC — report what's there, or // print installation guidance. @@ -382,7 +527,6 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, } auto pkg = mcpp::toolchain::to_xim_package(*spec); - if (mingw_wrong_host(pkg)) return 1; // Partial-version resolution: `gcc 15` → highest available 15.x.y in // the synced index. Empty version → latest of any major. @@ -390,15 +534,15 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, pkg.ximVersion, list_available_xpkg_versions(cfg, pkg.ximName))) { if (*picked != pkg.ximVersion) { mcpp::ui::info("Resolved", - std::format("{}@{} → {}@{}", spec->compiler, spec->version, - spec->compiler, *picked)); + std::format("{} → {}@{}", spec->spec_str(), + mcpp::toolchain::family_name(spec->family), *picked)); } spec = mcpp::toolchain::with_resolved_xim_version(*spec, *picked); pkg = mcpp::toolchain::to_xim_package(*spec); } mcpp::ui::info("Installing", - std::format("{} {} via mcpp's xlings", spec->compiler, spec->version)); + std::format("{} via mcpp's xlings", spec->display())); mcpp::log::verbose("toolchain", std::format( "install: target='{}' xlingsHome='{}'", pkg.target(), cfg.xlingsHome().string())); mcpp::log::debug("toolchain", std::format( @@ -409,13 +553,11 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, // Ensure sysroot dependencies (glibc, linux-headers) are installed. // These are required for C library + kernel headers during compilation. - // musl-gcc is self-contained and doesn't need these; neither do - // Windows (llvm/mingw — PE, own CRT) or macOS (SDK) toolchains, nor a - // Linux-hosted mingw CROSS toolchain (host is Linux but the TARGET is - // Windows PE with its own CRT — glibc/linux-headers are irrelevant). - // Mirrors the platform guard on prepare.cppm's first-run install. - if (!spec->isMusl - && pkg.ximName != "mingw-cross-gcc" + // Decided by the TARGET, not the payload name: musl targets are + // self-contained; PE targets (native mingw AND the Linux-hosted + // cross) bring their own CRT; Windows/macOS hosts never need the + // Linux sysroot. Mirrors the guard on prepare.cppm's first-run install. + if (!spec->target.is_musl() && !spec->target.is_pe() && !mcpp::platform::is_windows && !mcpp::platform::is_macos) { for (auto dep : {"xim:glibc", "xim:linux-headers"}) { mcpp::log::verbose("toolchain", std::format("installing dep: {}", dep)); @@ -451,15 +593,20 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, std::format("{} → {}", pkg.display_spec(), bin.string())); if (cfg.defaultToolchain.empty()) { std::println(""); - std::println("Tip: `mcpp toolchain default {}@{}` to make this the default.", - spec->compiler, spec->version); + std::println("Tip: `mcpp toolchain default {}{}` to make this the default.", + spec->spec_str(), + spec->target.empty() + ? std::string{} + : std::format(" --target {}", spec->target.str())); } return 0; } -// `mcpp toolchain default ` — persist the default toolchain. +// `mcpp toolchain default [--target ]` — persist the default +// (toolchain axis + target axis) pair. export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, - const std::string& pos0, const std::string& pos1) { + const std::string& pos0, const std::string& pos1, + const std::string& targetArg = {}) { auto pkgsDir = cfg.xlingsHome() / "data" / "xpkgs"; auto xlEnv = mcpp::config::make_xlings_env(cfg); // Accept three input shapes (mirrors `install`): @@ -468,10 +615,13 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, // mcpp toolchain default gcc 15 ← partial; picks highest 15.x.y auto spec = mcpp::toolchain::parse_toolchain_spec( pos0, pos1); - if (!spec || spec->compiler.empty()) { - mcpp::ui::error("missing spec; e.g. `mcpp toolchain default gcc@16.1.0`"); + if (!spec) { + mcpp::ui::error(std::format( + "{}; e.g. `mcpp toolchain default gcc@16.1.0`", spec.error())); return 2; } + mcpp::toolchain::print_compat_hint(*spec); + if (int rc = attach_target_arg(*spec, targetArg); rc != 0) return rc; // msvc@system: locate + identify the system MSVC, persist the stable // spec (never a concrete version — config survives VS updates). @@ -497,6 +647,15 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, mcpp::ui::error(wr.error().message); return 1; } + // Clear the target axis too: a stale default_target (e.g. + // x86_64-windows-gnu left by a previous mingw default) would + // otherwise hijack the next build — the target's convention pin + // overrides the toolchain, silently building with gcc instead + // of the just-selected cl.exe. Caught by ci-windows e2e 99. + if (auto wt = mcpp::config::write_default_target(cfg, ""); !wt) { + mcpp::ui::error(wt.error().message); + return 1; + } mcpp::ui::status("Default", std::format( "set to msvc@system (was: {})", cfg.defaultToolchain.empty() ? "" : cfg.defaultToolchain)); @@ -506,7 +665,6 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, } auto pkg = mcpp::toolchain::to_xim_package(*spec); - if (mingw_wrong_host(pkg)) return 1; // Partial-version resolution against installed payloads. if (auto picked = resolve_version_match( @@ -515,31 +673,39 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, pkg = mcpp::toolchain::to_xim_package(*spec); } - // Reconstruct the user-visible spec for messages / config.toml. - std::string displaySpec = pkg.display_spec(); - auto installDir = mcpp::xlings::paths::xim_tool(xlEnv, pkg.ximName, pkg.ximVersion); if (!std::filesystem::exists(installDir)) { mcpp::ui::error(std::format( - "{} is not installed. Run `mcpp toolchain install {} {}` first.", - displaySpec, spec->compiler, - spec->version.empty() ? pkg.ximVersion : spec->version)); + "{} is not installed. Run `mcpp toolchain install {} {}{}` first.", + spec->display(), mcpp::toolchain::family_name(spec->family), + spec->version.empty() ? pkg.ximVersion : spec->version, + spec->target.empty() + ? std::string{} + : std::format(" --target {}", spec->target.str()))); return 1; } - auto wr = mcpp::config::write_default_toolchain(cfg, displaySpec); + // Persist the pair: the toolchain axis in [toolchain].default, the + // target axis in [toolchain].default_target (empty = host). Legacy + // combined spellings in existing configs keep parsing via compat. + auto wr = mcpp::config::write_default_toolchain(cfg, spec->spec_str()); if (!wr) { mcpp::ui::error(wr.error().message); return 1; } + if (auto wt = mcpp::config::write_default_target(cfg, spec->target.str()); !wt) { + mcpp::ui::error(wt.error().message); + return 1; + } mcpp::ui::status("Default", std::format( - "set to {} (was: {})", displaySpec, + "set to {} (was: {})", spec->display(), cfg.defaultToolchain.empty() ? "" : cfg.defaultToolchain)); return 0; } -// `mcpp toolchain remove ` — uninstall a toolchain payload. +// `mcpp toolchain remove [--target ]` — uninstall a payload. export int toolchain_remove(const mcpp::config::GlobalConfig& cfg, - const std::string& pos0) { + const std::string& pos0, + const std::string& targetArg = {}) { auto xlEnv = mcpp::config::make_xlings_env(cfg); auto parsedSpec = mcpp::toolchain::parse_toolchain_spec(pos0); if (parsedSpec && mcpp::toolchain::is_system_toolchain(*parsedSpec)) { @@ -551,8 +717,10 @@ export int toolchain_remove(const mcpp::config::GlobalConfig& cfg, mcpp::ui::error(std::format("invalid spec '{}'", pos0)); return 2; } + mcpp::toolchain::print_compat_hint(*parsedSpec); + if (int rc = attach_target_arg(*parsedSpec, targetArg); rc != 0) return rc; auto pkg = mcpp::toolchain::to_xim_package(*parsedSpec); - auto spec = pkg.display_spec(); + auto spec = parsedSpec->display(); auto installDir = mcpp::xlings::paths::xim_tool(xlEnv, pkg.ximName, pkg.ximVersion); std::error_code ec; if (!std::filesystem::exists(installDir, ec)) { @@ -565,7 +733,12 @@ export int toolchain_remove(const mcpp::config::GlobalConfig& cfg, return 1; } mcpp::ui::status("Removed", spec); - if (cfg.defaultToolchain == spec) { + // The persisted default may be the removed payload under either the + // canonical or a legacy spelling — compare parsed identities. + if (auto def = mcpp::toolchain::parse_toolchain_spec(cfg.defaultToolchain); + def && def->family == parsedSpec->family + && def->version == parsedSpec->version + && def->target == parsedSpec->target) { mcpp::ui::warning(std::format( "default toolchain '{}' was just removed; consider `mcpp toolchain default `", spec)); diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index cf58569..7aec801 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -3,6 +3,7 @@ export module mcpp.toolchain.model; import std; +import mcpp.toolchain.triple; export namespace mcpp::toolchain { @@ -96,15 +97,23 @@ bool is_clang(const Toolchain& tc) { return tc.compiler == CompilerId::Clang; } +// Target-shape predicates read the parsed canonical Triple (triple.cppm is +// the single triple parser), with the old substring heuristics kept only as +// a fallback for triples outside the language. This makes them spelling- +// independent: "x86_64-w64-mingw32" and canonical "x86_64-windows-gnu" give +// the same answer. bool is_musl_target(const Toolchain& tc) { + if (auto t = triple::parse(tc.targetTriple)) return t->is_musl(); return tc.targetTriple.find("-musl") != std::string::npos; } bool is_msvc_target(const Toolchain& tc) { + if (auto t = triple::parse(tc.targetTriple)) return t->is_msvc_env(); return tc.targetTriple.find("msvc") != std::string::npos; } bool is_mingw_target(const Toolchain& tc) { + if (auto t = triple::parse(tc.targetTriple)) return t->is_windows_gnu(); // "x86_64-w64-mingw32" (mingw-w64) / legacy "*-pc-mingw32". return tc.targetTriple.find("mingw32") != std::string::npos; } diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index cbb660b..eec28fb 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -1,32 +1,70 @@ -// mcpp.toolchain.registry - user spec and package/provider mapping. +// mcpp.toolchain.registry — the two-axis toolchain identity model and its +// payload mapping. +// +// Identity (design §4.1–§4.3): a toolchain is `family@version` (family ∈ +// gcc | llvm | msvc), a target is a canonical Triple (triple.cppm). The two +// axes are orthogonal: "cross", "musl" and "mingw" are NOT names — the +// variant lives in the target's env segment, and cross is the host≠target +// relation. Which xim PACKAGE serves a (family, version, target, host) +// combination is a data mapping below — that's the distribution layer, where +// names like `mingw-cross-gcc` are current identity (not legacy) and stay. +// +// Legacy spellings (musl-gcc, gcc@V-musl, mingw, mingw-cross, clang, +// -gcc) are normalized by mcpp.toolchain.compat before this module +// ever sees them; core code deals in canonical form only. export module mcpp.toolchain.registry; import std; import mcpp.platform; import mcpp.toolchain.clang; +import mcpp.toolchain.compat; import mcpp.toolchain.gcc; import mcpp.toolchain.llvm; import mcpp.toolchain.model; import mcpp.toolchain.msvc; +import mcpp.toolchain.triple; export namespace mcpp::toolchain { +enum class Family { Gcc, Llvm, Msvc }; + +inline std::string_view family_name(Family f) { + switch (f) { + case Family::Gcc: return "gcc"; + case Family::Llvm: return "llvm"; + case Family::Msvc: return "msvc"; + } + return "?"; +} + struct ToolchainSpec { - std::string compiler; // user-facing compiler name, namespace-stripped - std::string version; // user-facing version, may include -musl - bool isMusl = false; - // Target triple (e.g. "aarch64-linux-musl") when building for a non-host - // target via `--target`. For musl toolchains the cross frontend program is - // named `-g++`, so this drives frontend selection. Empty = host. - std::string targetTriple; + Family family = Family::Gcc; + std::string version; // numeric (possibly partial), or "system" + triple::Triple target; // empty = host + // One-line canonical hint when the input used a legacy spelling + // (compat.cppm); empty otherwise. Printed at most once per process by + // print_compat_hint(). + std::string compatHint; + + bool is_host_target() const { return target.empty(); } + + // "gcc@16.1.0" — the toolchain axis alone (config persistence, matching). + std::string spec_str() const { + return std::format("{}@{}", family_name(family), version); + } + + // "gcc@16.1.0" or "gcc@16.1.0 → x86_64-windows-gnu" — user-facing. + std::string display() const { + if (target.empty()) return spec_str(); + return std::format("{} → {}", spec_str(), target.str()); + } }; struct XimToolchainPackage { std::string ximName; std::string ximVersion; - std::string displayCompiler; - std::string displayVersion; + std::string displaySpec; // canonical, from the spec std::vector frontendCandidates; bool needsGccPostInstallFixup = false; @@ -34,9 +72,7 @@ struct XimToolchainPackage { return std::format("xim:{}@{}", ximName, ximVersion); } - std::string display_spec() const { - return std::format("{}@{}", displayCompiler, displayVersion); - } + std::string display_spec() const { return displaySpec; } }; std::expected @@ -44,28 +80,47 @@ parse_toolchain_spec(std::string compilerArg, std::string versionArg = {}, bool requireCompiler = true); +// Print the spec's compat hint (once per process; no-op for canonical input). +void print_compat_hint(const ToolchainSpec& spec); + +// The (family, target, host) → xim package mapping — the distribution layer. XimToolchainPackage to_xim_package(const ToolchainSpec& spec); ToolchainSpec with_resolved_xim_version(const ToolchainSpec& spec, std::string_view ximVersion); -std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, - std::string_view compiler); - std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, const XimToolchainPackage& pkg); -std::string display_label(std::string_view compiler, std::string_view version); -bool matches_default_toolchain(std::string_view configuredDefault, - std::string_view compiler, - std::string_view version); +// Reverse mapping: an installed `xim-x-` payload directory back to its +// (family, target) identity. nullopt for non-toolchain xpkgs (ninja, glibc, +// python, …) — list/doctor use this to filter what they enumerate. +struct PayloadIdentity { + Family family; + triple::Triple target; // empty = host-target payload (gcc, llvm) +}; +std::optional identify_xim_payload(std::string_view ximDirName); + +// Does an installed payload row match the configured default (toolchain axis; +// version exact)? msvc matches on family alone — the persisted spec is the +// stable "msvc@system", never a concrete version. +bool spec_matches_payload(const ToolchainSpec& def, + const PayloadIdentity& id, + std::string_view payloadVersion); // System toolchains are located on the machine, never installed/removed by // mcpp. Today that's MSVC (`msvc@system`); the PATH-compiler escape hatch // (`[toolchain] … = "system"`) is a separate, older mechanism. bool is_system_toolchain(const ToolchainSpec& spec); -std::vector> available_toolchain_indexes(); +// xim index names to query for the Available section, with the family each +// one contributes versions to. Host-conditional: a host only lists payloads +// it can install. +struct AvailableIndex { + std::string ximName; + Family family; +}; +std::vector available_toolchain_indexes(); std::filesystem::path derive_c_compiler(const Toolchain& tc); std::filesystem::path archive_tool(const Toolchain& tc); @@ -86,34 +141,6 @@ bool ends_with(std::string_view s, std::string_view suf) { && s.compare(s.size() - suf.size(), suf.size(), suf) == 0; } -std::string strip_namespace(std::string compiler) { - if (auto colon = compiler.find(':'); colon != std::string::npos) - return compiler.substr(colon + 1); - return compiler; -} - -std::vector frontend_candidates_for(std::string_view ximName, - bool isMusl, - std::string_view targetTriple = {}) { - if (isMusl) { - // Cross musl toolchains expose `-g++` (e.g. - // aarch64-linux-musl-g++). Prefer the triple-specific frontend so a - // `--target aarch64-linux-musl` build never falls back to the host g++. - if (!targetTriple.empty()) - return {std::string(targetTriple) + "-g++", "g++"}; - return {"x86_64-linux-musl-g++", "g++"}; - } - if (ximName == "gcc") return {"g++"}; - if (ximName == "llvm") return mcpp::toolchain::llvm::frontend_candidates(); - if (ximName == "msvc") return {"cl.exe"}; - if (ximName == "mingw-gcc") return {"g++.exe", "g++"}; - // Linux-hosted MinGW-w64 cross toolchain: an ELF frontend that produces - // Windows PE. Triple-prefixed name (like the musl cross tools), never the - // host g++ — a cross build must never silently fall back to native. - if (ximName == "mingw-cross-gcc") return {"x86_64-w64-mingw32-g++"}; - return {"g++", "clang++", "x86_64-linux-musl-g++", "cl.exe"}; -} - std::filesystem::path derive_c_compiler_path(const std::filesystem::path& cxxPath) { auto stem = cxxPath.stem().string(); auto parent = cxxPath.parent_path(); @@ -130,6 +157,10 @@ std::filesystem::path derive_c_compiler_path(const std::filesystem::path& cxxPat return parent / (cc_stem + ext.string()); } +triple::Triple host_musl_triple() { + return { std::string(mcpp::platform::host_arch), "linux", "musl" }; +} + } // namespace std::expected @@ -140,69 +171,86 @@ parse_toolchain_spec(std::string compilerArg, if (versionArg.empty()) versionArg = compilerArg.substr(at + 1); compilerArg = compilerArg.substr(0, at); } - - compilerArg = strip_namespace(std::move(compilerArg)); if (compilerArg.empty() && requireCompiler) { return std::unexpected("missing compiler name"); } + auto norm = compat::normalize_spec(compilerArg, versionArg); + if (!norm) { + return std::unexpected(std::format( + "unknown toolchain '{}' (expected gcc | llvm | msvc, or a " + "supported alias like mingw / musl-gcc)", compilerArg)); + } + ToolchainSpec spec; - spec.compiler = std::move(compilerArg); - spec.version = std::move(versionArg); - // musl is signalled three ways: the canonical host-native `musl-gcc`, a - // `-musl` version suffix, or a target-named cross/native toolchain - // like `aarch64-linux-musl-gcc` (the compiler name carries the triple). - spec.isMusl = spec.compiler == "musl-gcc" - || ends_with(spec.version, "-musl") - || ends_with(spec.compiler, "-linux-musl-gcc"); + if (norm->family == "llvm") spec.family = Family::Llvm; + else if (norm->family == "msvc") spec.family = Family::Msvc; + else spec.family = Family::Gcc; + spec.version = std::move(norm->version); + spec.target = std::move(norm->target); + if (norm->changed) spec.compatHint = std::move(norm->hint); return spec; } +void print_compat_hint(const ToolchainSpec& spec) { + if (spec.compatHint.empty()) return; + compat::print_hint_once(spec.compatHint); +} + XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { XimToolchainPackage pkg; - pkg.displayCompiler = spec.compiler; - pkg.displayVersion = spec.version; - - // Target-named musl toolchain, e.g. "aarch64-linux-musl-gcc". The compiler - // name IS the xim package name and encodes the target triple, so it serves - // both cross (x86 host → aarch64) and on-target native builds — xlings' - // XLINGS_RES sentinel picks the host-matching prebuilt asset. The frontend - // is `-g++` (triple = name minus the trailing "-gcc"). - if (ends_with(spec.compiler, "-linux-musl-gcc")) { - pkg.ximName = spec.compiler; - pkg.ximVersion = spec.version; - std::string triple = spec.compiler.substr(0, spec.compiler.size() - 4); - pkg.frontendCandidates = {triple + "-g++", "g++"}; - pkg.needsGccPostInstallFixup = false; + pkg.displaySpec = spec.display(); + pkg.ximVersion = spec.version; + + if (spec.family == Family::Msvc) { + pkg.ximName = "msvc"; // never resolved via xim + pkg.frontendCandidates = {"cl.exe"}; + return pkg; + } + if (spec.family == Family::Llvm) { + pkg.ximName = mcpp::toolchain::llvm::package_name(); + pkg.frontendCandidates = mcpp::toolchain::llvm::frontend_candidates(); return pkg; } - std::string ximCompiler = spec.compiler; - if (mcpp::toolchain::llvm::is_alias(ximCompiler)) - ximCompiler = mcpp::toolchain::llvm::package_name(); - // Windows-native MinGW-w64 GCC: user-facing name `mingw`, xim package - // `mingw-gcc` (winlibs GCC+MinGW-w64 UCRT builds mirrored at xlings-res). - if (ximCompiler == "mingw") - ximCompiler = "mingw-gcc"; - // Linux→Windows MinGW-w64 cross: user-facing `mingw-cross`, xim package - // `mingw-cross-gcc` (from-source GCC-16 MSVCRT cross, xlings-res). host≠target: - // an ELF toolchain producing PE. See 2026-07-15-mingw-linux-cross-windows-design. - if (ximCompiler == "mingw-cross") - ximCompiler = "mingw-cross-gcc"; - - pkg.ximName = ximCompiler; - pkg.ximVersion = spec.version; - - if (spec.isMusl) { - if (pkg.ximName != "musl-gcc") - pkg.ximName = "musl-" + pkg.ximName; - if (ends_with(pkg.ximVersion, "-musl")) - pkg.ximVersion.resize(pkg.ximVersion.size() - 5); + // Family::Gcc — the target decides the payload. + const auto& t = spec.target; + + if (t.is_musl()) { + // Same target, two payload shapes: the host-native `musl-gcc` package + // (XLINGS_RES picks the host-matching asset) when target arch == host + // arch, else the triple-named cross package. Canonical linux-musl + // triples coincide with the GNU tool spelling, so `-g++` is + // the frontend either way. + bool native = mcpp::platform::is_linux + && t.arch == mcpp::platform::host_arch; + pkg.ximName = native ? "musl-gcc" : t.str() + "-gcc"; + pkg.frontendCandidates = { t.str() + "-g++", "g++" }; + return pkg; + } + + if (t.is_windows_gnu() + || (t.empty() && mcpp::platform::is_windows)) { + // GCC targeting Windows PE (GNU CRT) — ONE user-facing identity, + // host-split at the distribution layer only: + // Windows host → native winlibs UCRT build (PE frontend g++.exe) + // other hosts → Linux-hosted MSVCRT cross (ELF frontend, triple- + // prefixed so a cross build never silently falls + // back to a native g++) + if constexpr (mcpp::platform::is_windows) { + pkg.ximName = "mingw-gcc"; + pkg.frontendCandidates = {"g++.exe", "g++"}; + } else { + pkg.ximName = "mingw-cross-gcc"; + pkg.frontendCandidates = {"x86_64-w64-mingw32-g++"}; + } + return pkg; } - pkg.frontendCandidates = frontend_candidates_for(pkg.ximName, spec.isMusl, - spec.targetTriple); - pkg.needsGccPostInstallFixup = spec.compiler == "gcc" && !spec.isMusl; + // Host target (or linux-gnu): the glibc gcc package. + pkg.ximName = "gcc"; + pkg.frontendCandidates = {"g++"}; + pkg.needsGccPostInstallFixup = true; return pkg; } @@ -210,24 +258,9 @@ ToolchainSpec with_resolved_xim_version(const ToolchainSpec& spec, std::string_view ximVersion) { ToolchainSpec out = spec; out.version = std::string(ximVersion); - if (out.isMusl) out.version += "-musl"; return out; } -std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, - std::string_view compiler) { - bool isMusl = compiler == "musl-gcc"; - std::string ximName(compiler); - if (mcpp::toolchain::llvm::is_alias(ximName)) - ximName = mcpp::toolchain::llvm::package_name(); - - for (auto& cand : frontend_candidates_for(ximName, isMusl)) { - auto p = binDir / cand; - if (std::filesystem::exists(p)) return p; - } - return {}; -} - std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, const XimToolchainPackage& pkg) { for (auto& cand : pkg.frontendCandidates) { @@ -237,59 +270,47 @@ std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, return {}; } -std::string display_label(std::string_view compiler, std::string_view version) { - if (compiler == "musl-gcc") - return std::format("gcc {}-musl", version); - if (compiler == "mingw-gcc") - return std::format("mingw {}", version); - if (compiler == "mingw-cross-gcc") - return std::format("mingw-cross {}", version); - return std::format("{} {}", compiler, version); +std::optional identify_xim_payload(std::string_view ximDirName) { + if (ximDirName == "gcc") + return PayloadIdentity{ Family::Gcc, {} }; + if (ximDirName == mcpp::toolchain::llvm::package_name()) + return PayloadIdentity{ Family::Llvm, {} }; + if (ximDirName == "musl-gcc") + return PayloadIdentity{ Family::Gcc, host_musl_triple() }; + if (ximDirName == "mingw-gcc" || ximDirName == "mingw-cross-gcc") + return PayloadIdentity{ Family::Gcc, { "x86_64", "windows", "gnu" } }; + if (ends_with(ximDirName, "-gcc")) { + auto prefix = ximDirName.substr(0, ximDirName.size() - 4); + if (auto t = triple::parse(prefix)) + return PayloadIdentity{ Family::Gcc, *t }; + } + return std::nullopt; // not a toolchain payload (ninja, glibc, …) } -bool matches_default_toolchain(std::string_view configuredDefault, - std::string_view compiler, - std::string_view version) { - if (configuredDefault == std::format("{}@{}", compiler, version)) return true; - if (compiler == "musl-gcc" - && configuredDefault == std::format("gcc@{}-musl", version)) { - return true; - } - // The persisted msvc default is always the stable "msvc@system" (never a - // concrete version), so it matches whatever version detection reports. - if (compiler == "msvc" && configuredDefault == "msvc@system") return true; - // Installed-payload rows report the xim name (mingw-gcc); the user-facing - // spec is mingw@ (same shape as the musl-gcc clause above). - if (compiler == "mingw-gcc" - && configuredDefault == std::format("mingw@{}", version)) { - return true; - } - // Cross MinGW: installed-payload row reports xim name (mingw-cross-gcc); - // user-facing spec is mingw-cross@. - if (compiler == "mingw-cross-gcc" - && configuredDefault == std::format("mingw-cross@{}", version)) { - return true; - } - return false; +bool spec_matches_payload(const ToolchainSpec& def, + const PayloadIdentity& id, + std::string_view payloadVersion) { + if (def.family != id.family) return false; + if (def.family == Family::Msvc) return true; // msvc@system: family match + return def.version == payloadVersion; } bool is_system_toolchain(const ToolchainSpec& spec) { - return spec.compiler == "msvc"; + return spec.family == Family::Msvc; } -std::vector> available_toolchain_indexes() { - std::vector> out{ - {"gcc", "gcc"}, - {"musl-gcc", "musl-gcc"}, - {mcpp::toolchain::llvm::package_name(), "llvm"}, +std::vector available_toolchain_indexes() { + std::vector out{ + { "gcc", Family::Gcc }, + { "musl-gcc", Family::Gcc }, + { mcpp::toolchain::llvm::package_name(), Family::Llvm }, }; - // MinGW-w64 toolchains are host-conditional (host≠target axis): - // - native mingw-gcc (produces PE, runs on Windows) → Windows hosts - // - cross mingw-cross-gcc (ELF frontend, produces PE) → Linux hosts + // The Windows-PE gcc payload is host-split at the distribution layer + // (§4.3); each host lists the package it would actually install. if constexpr (mcpp::platform::is_windows) - out.emplace_back("mingw-gcc", "mingw-gcc"); + out.push_back({ "mingw-gcc", Family::Gcc }); else if constexpr (mcpp::platform::is_linux) - out.emplace_back("mingw-cross-gcc", "mingw-cross-gcc"); + out.push_back({ "mingw-cross-gcc", Family::Gcc }); return out; } diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm new file mode 100644 index 0000000..d2d9edd --- /dev/null +++ b/src/toolchain/triple.cppm @@ -0,0 +1,251 @@ +// mcpp.toolchain.triple — the single source of truth for target identity. +// +// mcpp owns its target-triple language: canonical form is `arch-os[-env]` +// (three segments, no vendor — Zig-style). `x86_64-linux-musl` was already +// canonical before this module existed; this extends the same convention to +// every target. GNU/LLVM spellings (`x86_64-w64-mingw32`, +// `x86_64-unknown-linux-gnu`, `arm64-apple-darwin24`) are permanent input +// aliases, normalized here. +// +// Everything that previously parsed triples ad hoc (cfgpred::context_for, +// abi_profile, model.cppm's is_*_target, registry's musl signals) consumes +// this module now. Vocabulary: os ∈ {linux, macos, windows} (never "darwin"), +// arch is the GNU spelling ({x86_64, aarch64, riscv64, …} — never "arm64"), +// env ∈ {gnu, musl, msvc} (empty on macos). `static` is NOT part of a triple: +// it is a target's default linkage property, flipped via [build]. +// +// The known-target table below is the closed vocabulary `--target` validates +// against (with an escape hatch for explicit [target.X] manifest sections) +// and the source the README platform table is drawn from. Adding a target = +// adding a row here (+ payload mapping in registry.cppm if a new payload +// shape is involved). +// +// See .agents/docs/2026-07-15-toolchain-target-naming-unification-design.md. + +export module mcpp.toolchain.triple; + +import std; +import mcpp.platform; + +export namespace mcpp::toolchain::triple { + +struct Triple { + std::string arch; // "x86_64" | "aarch64" | "riscv64" | ... (GNU spelling) + std::string os; // "linux" | "macos" | "windows" + std::string env; // "gnu" | "musl" | "msvc" | "" (always empty on macos) + + bool empty() const { return arch.empty() && os.empty(); } + + // Canonical rendering: "arch-os[-env]"; "" for an empty (= host) triple. + std::string str() const { + if (empty()) return {}; + std::string s = arch + "-" + os; + if (!env.empty()) { s += "-"; s += env; } + return s; + } + + bool is_musl() const { return env == "musl"; } + bool is_msvc_env() const { return env == "msvc"; } + bool is_windows_gnu() const { return os == "windows" && env == "gnu"; } + bool is_pe() const { return os == "windows"; } + + // cfg() `family` dimension: unix | windows. + std::string family() const { + if (os == "windows") return "windows"; + if (os == "linux" || os == "macos") return "unix"; + return {}; + } + + bool operator==(const Triple&) const = default; +}; + +// Lenient parse of any recognizable triple spelling into canonical fields. +// Handles mcpp-canonical ("x86_64-linux-musl"), GNU ("x86_64-w64-mingw32", +// "x86_64-pc-linux-gnu"), LLVM/Rust 4-segment ("x86_64-unknown-linux-musl", +// "x86_64-pc-windows-msvc") and Apple ("arm64-apple-darwin24.1.0", +// "arm64-apple-macosx15.0") forms. Returns nullopt when no OS is +// recognizable — the input is not a triple at all. +std::optional parse(std::string_view s); + +// ── Known-target registry (closed vocabulary; data, not code) ──────────────── +// +// tier semantics (Rust-style): +// verified — CI builds AND executes the artifact end-to-end (qemu/wine count) +// planned — registered intent; payload or CI row not wired yet +struct TargetInfo { + std::string_view canonical; // "x86_64-linux-musl" + std::string_view tier; // "verified" | "planned" + std::string_view note; // display annotation: "static" / "PE" / "" + // Convention toolchain pin for `--target ` with no explicit + // [target.X] toolchain override. Empty = no convention (host default). + std::string_view pin; + bool defaultStatic; // target's default linkage is static +}; + +// (note deliberately excludes "static" — the display layer derives that tag +// from defaultStatic, so listing it here would duplicate it.) +inline constexpr TargetInfo kKnownTargets[] = { + // canonical tier note pin defaultStatic + { "x86_64-linux-gnu", "verified", "", "", false }, + { "x86_64-linux-musl", "verified", "", "gcc@16.1.0", true }, + { "aarch64-linux-musl", "verified", "", "gcc@16.1.0", true }, + { "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", true }, + { "x86_64-windows-msvc", "verified", "PE", "", false }, + { "aarch64-macos", "verified", "", "", false }, + { "riscv64-linux-musl", "planned", "", "", true }, + { "aarch64-linux-gnu", "planned", "", "", false }, + { "x86_64-macos", "planned", "", "", false }, +}; + +inline std::span known_targets() { return kKnownTargets; } + +inline const TargetInfo* find_known_target(const Triple& t) { + auto s = t.str(); + for (auto& k : kKnownTargets) + if (k.canonical == s) return &k; + return nullptr; +} + +inline bool is_known_target(const Triple& t) { return find_known_target(t) != nullptr; } + +// Closest known-target canonical name for a mistyped `--target` (checked +// against canonical names AND common alias spellings). nullopt when nothing +// is plausibly close. +std::optional did_you_mean(std::string_view input); + +// Host coordinates as a canonical Triple (linux hosts report env=gnu — the +// user-facing host default, independent of how mcpp itself was linked). +inline Triple host_triple() { + Triple t; + t.arch = std::string(mcpp::platform::host_arch); + t.os = std::string(mcpp::platform::name); + if (t.os == "linux") t.env = "gnu"; + else if (t.os == "windows") t.env = "msvc"; + return t; +} + +// ── Version pins (single site; §4.6 of the design doc) ─────────────────────── +// Every default/convention toolchain version literal lives here. Help and +// error strings format these — never inline a pinned version elsewhere. +// Changing a pin: update this block, then sync docs/03-toolchains.md and the +// README platform table (drawn from kKnownTargets above). +namespace pins { + // First-run auto-install defaults (prepare.cppm), per host platform/arch. + inline constexpr std::string_view kFirstRunMacWin = "llvm@20.1.7"; + inline constexpr std::string_view kFirstRunLinuxX86_64 = "gcc@16.1.0"; + inline constexpr std::string_view kFirstRunLinuxOther = "gcc@15.1.0-musl"; + // Suggested install spellings used by help / MCPP_NO_AUTO_INSTALL errors. + inline constexpr std::string_view kSuggestLlvm = "llvm 20.1.7"; + inline constexpr std::string_view kSuggestGccMusl = "gcc 15.1.0-musl"; +} // namespace pins + +} // namespace mcpp::toolchain::triple + +namespace mcpp::toolchain::triple { + +namespace { + +bool starts_with(std::string_view s, std::string_view p) { + return s.size() >= p.size() && s.substr(0, p.size()) == p; +} + +std::string normalize_arch(std::string_view a) { + if (a == "arm64") return "aarch64"; // Apple/xlings spelling → GNU + if (a == "amd64") return "x86_64"; + return std::string(a); +} + +// Levenshtein distance (small inputs only). +std::size_t edit_distance(std::string_view a, std::string_view b) { + std::vector prev(b.size() + 1), cur(b.size() + 1); + for (std::size_t j = 0; j <= b.size(); ++j) prev[j] = j; + for (std::size_t i = 1; i <= a.size(); ++i) { + cur[0] = i; + for (std::size_t j = 1; j <= b.size(); ++j) { + std::size_t sub = prev[j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1); + cur[j] = std::min({ prev[j] + 1, cur[j - 1] + 1, sub }); + } + std::swap(prev, cur); + } + return prev[b.size()]; +} + +} // namespace + +std::optional parse(std::string_view s) { + if (s.empty()) return std::nullopt; + + // Split on '-'. + std::vector tok; + for (std::size_t b = 0; b <= s.size();) { + auto d = s.find('-', b); + if (d == std::string_view::npos) { tok.push_back(s.substr(b)); break; } + tok.push_back(s.substr(b, d - b)); + b = d + 1; + } + if (tok.size() < 2 || tok[0].empty()) return std::nullopt; + + Triple t; + t.arch = normalize_arch(tok[0]); + + bool sawOs = false; + for (std::size_t i = 1; i < tok.size(); ++i) { + std::string_view k = tok[i]; + if (k.empty()) return std::nullopt; + // Vendor segments carry no information — skip. ("w64" is mingw-w64's + // vendor; "apple" implies macOS when no OS token follows.) + if (k == "unknown" || k == "pc" || k == "w64" || k == "none") continue; + if (k == "apple") { if (!sawOs) { t.os = "macos"; sawOs = true; } continue; } + + if (k == "linux") { t.os = "linux"; sawOs = true; continue; } + if (k == "windows") { t.os = "windows"; sawOs = true; continue; } + if (starts_with(k, "darwin") + || starts_with(k, "macosx") + || starts_with(k, "macos")) { t.os = "macos"; sawOs = true; t.env.clear(); continue; } + // "mingw32" is the GNU os segment for ALL MinGW targets (64-bit + // included — historical residue); it means windows + gnu env. + if (starts_with(k, "mingw")) { t.os = "windows"; sawOs = true; t.env = "gnu"; continue; } + + if (t.os != "macos") { + if (k == "musl" || starts_with(k, "musleabi")) { t.env = "musl"; continue; } + if (k == "gnu" || starts_with(k, "gnueabi")) { t.env = "gnu"; continue; } + // starts_with: clang effective triples can carry a version suffix + // on the env segment ("…-windows-msvc19.44.35211"). + if (starts_with(k, "msvc")) { t.env = "msvc"; continue; } + } + // Unrecognized segment (androideabi, wasi, …): not in mcpp's target + // language — treat as unparseable rather than guessing. + return std::nullopt; + } + + if (!sawOs) return std::nullopt; + if (t.os == "macos") t.env.clear(); // macos carries no env segment + if (t.os == "linux" && t.env.empty()) t.env = "gnu"; // "x86_64-linux" alias + return t; +} + +std::optional did_you_mean(std::string_view input) { + // Compare against canonical names and the common alias spellings a user + // is likely to half-remember. + static constexpr std::string_view kAliases[] = { + "x86_64-w64-mingw32", "x86_64-unknown-linux-musl", + "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", + "x86_64-pc-windows-gnu", "aarch64-unknown-linux-musl", + }; + std::optional best; + std::size_t bestDist = std::string_view::npos; + auto consider = [&](std::string_view cand, std::string_view canonical) { + auto d = edit_distance(input, cand); + if (d < bestDist) { bestDist = d; best = std::string(canonical); } + }; + for (auto& k : kKnownTargets) consider(k.canonical, k.canonical); + for (auto& a : kAliases) { + if (auto t = parse(a); t && is_known_target(*t)) consider(a, t->str()); + } + // Only suggest when plausibly a typo: allow more slack for longer inputs. + std::size_t budget = std::max(2, input.size() / 4); + if (best && bestDist <= budget) return best; + return std::nullopt; +} + +} // namespace mcpp::toolchain::triple diff --git a/tests/e2e/102_mingw_cross_wine.sh b/tests/e2e/102_mingw_cross_wine.sh index 8fe39df..2993fc0 100644 --- a/tests/e2e/102_mingw_cross_wine.sh +++ b/tests/e2e/102_mingw_cross_wine.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash # requires: mingw-cross wine # Linux → Windows MinGW cross: build a multi-module + `import std` project for -# x86_64-w64-mingw32, verify the PE is statically self-contained, run it under -# wine. Realizes Part C of 2026-07-15-mingw-linux-cross-windows-design.md. +# the Windows PE target, verify the PE is statically self-contained, run it +# under wine. Realizes Part C of 2026-07-15-mingw-linux-cross-windows-design.md. +# Both triple spellings are exercised: canonical `x86_64-windows-gnu` for the +# build, plus the permanent GNU alias `x86_64-w64-mingw32` asserting it lands +# in the SAME canonical target/ directory (naming-unification design D1/§6.2). set -e TMP=$(mktemp -d) @@ -32,11 +35,25 @@ int main() { } EOF -TRIPLE=x86_64-w64-mingw32 +# Canonical triple names the output directory; the GNU spelling is a +# permanent input alias that must land in the same place. +TRIPLE=x86_64-windows-gnu +ALIAS_TRIPLE=x86_64-w64-mingw32 -# ── build for the Windows PE target ──────────────────────────────────────── +# ── build for the Windows PE target (canonical spelling) ──────────────────── "$MCPP" build --target "$TRIPLE" > "$TMP/build.log" 2>&1 || { echo "cross build failed:"; cat "$TMP/build.log"; exit 1; } +grep -q "Resolved gcc@16.1.0 → x86_64-windows-gnu" "$TMP/build.log" || { + echo "missing canonical Resolved line:"; cat "$TMP/build.log"; exit 1; } + +# ── alias spelling: same canonical directory, no second toolchain ────────── +"$MCPP" build --target "$ALIAS_TRIPLE" > "$TMP/build-alias.log" 2>&1 || { + echo "alias-spelling build failed:"; cat "$TMP/build-alias.log"; exit 1; } +[[ -d "target/$TRIPLE" ]] || { echo "canonical target dir missing"; exit 1; } +if [[ -d "target/$ALIAS_TRIPLE" ]]; then + echo "alias spelling created its own target/$ALIAS_TRIPLE — should normalize to target/$TRIPLE" + exit 1 +fi EXE="$(find target/$TRIPLE -name '*.exe' -type f | head -1)" [[ -n "$EXE" && -f "$EXE" ]] || { echo "no .exe produced under target/$TRIPLE"; exit 1; } diff --git a/tests/e2e/103_target_vocabulary.sh b/tests/e2e/103_target_vocabulary.sh new file mode 100755 index 0000000..0e53f72 --- /dev/null +++ b/tests/e2e/103_target_vocabulary.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# requires: gcc +# Target-axis vocabulary (naming-unification design §4.2/§4.5/§6.3): +# 1. --target typo → hard error + did-you-mean (never a silent host build) +# 2. planned-tier target → hard error (registered, not yet supported) +# 3. custom triple + explicit [target.X] section → escape hatch proceeds +# 4. [build] target = "" → default build target without --target +# 5. alias spelling (--target x86_64-unknown-linux-musl) → canonical dir +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +"$MCPP" new vocab > /dev/null +cd vocab + +# ── 1. typo: hard error with did-you-mean ──────────────────────────────────── +if "$MCPP" build --target x86_64-linux-mus > "$TMP/typo.log" 2>&1; then + echo "FAIL: typo'd --target built successfully (silent host fallthrough)" + cat "$TMP/typo.log"; exit 1 +fi +grep -q "unknown target 'x86_64-linux-mus'" "$TMP/typo.log" || { + echo "FAIL: missing unknown-target error"; cat "$TMP/typo.log"; exit 1; } +grep -q "did you mean 'x86_64-linux-musl'" "$TMP/typo.log" || { + echo "FAIL: missing did-you-mean suggestion"; cat "$TMP/typo.log"; exit 1; } + +# ── 2. planned tier: registered but unsupported → error, not a host build ─── +if "$MCPP" build --target riscv64-linux-musl > "$TMP/planned.log" 2>&1; then + echo "FAIL: planned-tier target built"; cat "$TMP/planned.log"; exit 1 +fi +grep -q "not yet supported" "$TMP/planned.log" || { + echo "FAIL: missing planned-tier error"; cat "$TMP/planned.log"; exit 1; } + +# ── 3. escape hatch: custom triple with an explicit [target.X] section ────── +# (pin the host gcc so resolution succeeds; the point is validation lets an +# explicitly-declared custom triple through.) +cat >> mcpp.toml <<'EOF' + +[target.myboard-custom-elf] +toolchain = "gcc@16.1.0" +EOF +"$MCPP" build --target myboard-custom-elf > "$TMP/custom.log" 2>&1 || { + echo "FAIL: explicit [target.X] escape hatch rejected"; cat "$TMP/custom.log"; exit 1; } +grep -q "unknown target" "$TMP/custom.log" && { + echo "FAIL: escape hatch still errored"; cat "$TMP/custom.log"; exit 1; } + +# ── 4. [build] target: project default build target (cargo build.target) ──── +git checkout -- mcpp.toml 2>/dev/null || sed -i '/\[target.myboard-custom-elf\]/,+1d' mcpp.toml +if [[ -d "$HOME/.xlings/data/xpkgs/xim-x-musl-gcc" ]] \ + || [[ -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-musl-gcc" ]]; then + printf '\n[build]\ntarget = "x86_64-linux-musl"\n' >> mcpp.toml + rm -rf target + "$MCPP" build > "$TMP/bt.log" 2>&1 || { + echo "FAIL: [build] target build failed"; cat "$TMP/bt.log"; exit 1; } + [[ -d target/x86_64-linux-musl ]] || { + echo "FAIL: [build] target did not select the musl target dir" + ls target/; exit 1; } + + # ── 5. alias spelling lands in the same canonical directory ───────────── + "$MCPP" build --target x86_64-unknown-linux-musl > "$TMP/alias.log" 2>&1 || { + echo "FAIL: alias-spelling build failed"; cat "$TMP/alias.log"; exit 1; } + [[ -d target/x86_64-unknown-linux-musl ]] && { + echo "FAIL: alias spelling minted its own target dir"; exit 1; } +else + echo "(musl toolchain not installed — steps 4-5 skipped)" +fi + +echo "OK" diff --git a/tests/e2e/95_msvc_system_toolchain.sh b/tests/e2e/95_msvc_system_toolchain.sh index 0057c92..5b73792 100755 --- a/tests/e2e/95_msvc_system_toolchain.sh +++ b/tests/e2e/95_msvc_system_toolchain.sh @@ -15,7 +15,7 @@ CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" ORIG_DEFAULT="" if [[ -f "$CONF" ]]; then ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ - | grep '^default' | head -1 | cut -d'"' -f2 || true) + | grep -E '^default[[:space:]]*=' | head -1 | cut -d'"' -f2 || true) fi TMP=$(mktemp -d) restore() { diff --git a/tests/e2e/97_mingw_toolchain.sh b/tests/e2e/97_mingw_toolchain.sh index a81864c..c9b9f83 100755 --- a/tests/e2e/97_mingw_toolchain.sh +++ b/tests/e2e/97_mingw_toolchain.sh @@ -12,8 +12,10 @@ set -e CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" ORIG_DEFAULT="" if [[ -f "$CONF" ]]; then + # NB: match `default =` exactly — `default_target =` also starts with + # "default" (the persisted pair since the naming unification). ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ - | grep '^default' | head -1 | cut -d'"' -f2 || true) + | grep -E '^default[[:space:]]*=' | head -1 | cut -d'"' -f2 || true) fi TMP=$(mktemp -d) restore() { @@ -32,12 +34,15 @@ out=$("$MCPP" toolchain install mingw 16.1.0 2>&1) \ [[ "$out" == *"Installed"* || "$out" == *"already"* || "$out" == *"mingw"* ]] \ || { echo "FAIL: install output: $out"; exit 1; } -# 2) switch default + list shows it starred +# 2) switch default (legacy spelling — normalizes to the pair +# gcc@16.1.0 + x86_64-windows-gnu) + list stars both axes "$MCPP" toolchain default mingw@16.1.0 \ || { echo "FAIL: default mingw@16.1.0"; exit 1; } out=$("$MCPP" toolchain list 2>&1) -echo "$out" | grep -E '\*\s*mingw 16\.1\.0' >/dev/null \ - || { echo "FAIL: mingw not starred: $out"; exit 1; } +echo "$out" | grep -E '\*\s*gcc 16\.1\.0' >/dev/null \ + || { echo "FAIL: gcc 16.1.0 not starred in Toolchains: $out"; exit 1; } +echo "$out" | grep -E '\*\s*x86_64-windows-gnu' >/dev/null \ + || { echo "FAIL: x86_64-windows-gnu not starred in Targets: $out"; exit 1; } # 3) real build: import std + named module through the gcm pipeline "$MCPP" new hello_mingw >/dev/null 2>&1 diff --git a/tests/e2e/99_msvc_native_build.sh b/tests/e2e/99_msvc_native_build.sh index 3a91af4..6485d3b 100755 --- a/tests/e2e/99_msvc_native_build.sh +++ b/tests/e2e/99_msvc_native_build.sh @@ -9,8 +9,10 @@ set -e CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" ORIG_DEFAULT="" if [[ -f "$CONF" ]]; then + # NB: match `default =` exactly — `default_target =` also starts with + # "default" (the persisted pair since the naming unification). ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ - | grep '^default' | head -1 | cut -d'"' -f2 || true) + | grep -E '^default[[:space:]]*=' | head -1 | cut -d'"' -f2 || true) fi TMP=$(mktemp -d) restore() { @@ -42,6 +44,10 @@ int main() { std::println("{}", hello::greet()); return 0; } EOF out=$("$MCPP" build 2>&1) || { echo "FAIL: msvc build: $out"; exit 1; } +# the build must actually resolve msvc — a stale default_target could +# otherwise silently reroute to another toolchain (that exact bug shipped +# once: mingw's leftover default_target hijacked `toolchain default msvc`) +grep -q "msvc" <<<"$out" || { echo "FAIL: build did not resolve msvc: $out"; exit 1; } run_out=$("$MCPP" run 2>&1) || { echo "FAIL: msvc run: $run_out"; exit 1; } [[ "$run_out" == *"cl-ok"* ]] || { echo "FAIL: run output: $run_out"; exit 1; } diff --git a/tests/unit/test_toolchain_compat.cpp b/tests/unit/test_toolchain_compat.cpp new file mode 100644 index 0000000..023665c --- /dev/null +++ b/tests/unit/test_toolchain_compat.cpp @@ -0,0 +1,108 @@ +#include + +import std; +import mcpp.platform; +import mcpp.toolchain.compat; + +using namespace mcpp::toolchain::compat; + +static std::string host_musl() { + return std::string(mcpp::platform::host_arch) + "-linux-musl"; +} + +// ── canonical spellings pass through unchanged ─────────────────────────────── + +TEST(Compat, CanonicalFamiliesPassThrough) { + auto g = normalize_spec("gcc", "16.1.0"); + ASSERT_TRUE(g.has_value()); + EXPECT_EQ(g->family, "gcc"); + EXPECT_EQ(g->version, "16.1.0"); + EXPECT_TRUE(g->target.empty()); + EXPECT_FALSE(g->changed); + + auto l = normalize_spec("llvm", "20.1.7"); + ASSERT_TRUE(l.has_value()); + EXPECT_EQ(l->family, "llvm"); + EXPECT_FALSE(l->changed); + + auto m = normalize_spec("msvc", "system"); + ASSERT_TRUE(m.has_value()); + EXPECT_EQ(m->family, "msvc"); + EXPECT_EQ(m->version, "system"); + EXPECT_FALSE(m->changed); +} + +// ── §6.2 alias table: every legacy row round-trips ─────────────────────────── + +TEST(Compat, MuslVersionSuffixBecomesTarget) { + auto s = normalize_spec("gcc", "15.1.0-musl"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(s->family, "gcc"); + EXPECT_EQ(s->version, "15.1.0"); + EXPECT_EQ(s->target.str(), host_musl()); + EXPECT_TRUE(s->changed); + EXPECT_FALSE(s->hint.empty()); +} + +TEST(Compat, MuslGccCompilerNameBecomesTarget) { + auto s = normalize_spec("musl-gcc", "15.1.0"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(s->family, "gcc"); + EXPECT_EQ(s->version, "15.1.0"); + EXPECT_EQ(s->target.str(), host_musl()); + EXPECT_TRUE(s->changed); +} + +TEST(Compat, TripleNamedGccBecomesTarget) { + auto s = normalize_spec("aarch64-linux-musl-gcc", "16.1.0"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(s->family, "gcc"); + EXPECT_EQ(s->version, "16.1.0"); + EXPECT_EQ(s->target.str(), "aarch64-linux-musl"); + EXPECT_TRUE(s->changed); +} + +TEST(Compat, MingwAndMingwCrossCollapseToWindowsGnu) { + // One concept, two host-split legacy names → the SAME normalized identity. + auto native = normalize_spec("mingw", "16.1.0"); + auto cross = normalize_spec("mingw-cross", "16.1.0"); + ASSERT_TRUE(native.has_value()); + ASSERT_TRUE(cross.has_value()); + EXPECT_EQ(native->family, "gcc"); + EXPECT_EQ(cross->family, "gcc"); + EXPECT_EQ(native->target.str(), "x86_64-windows-gnu"); + EXPECT_EQ(cross->target.str(), "x86_64-windows-gnu"); + EXPECT_TRUE(native->changed); + EXPECT_TRUE(cross->changed); +} + +TEST(Compat, ClangAliasBecomesLlvm) { + auto s = normalize_spec("clang", "20.1.7"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(s->family, "llvm"); + EXPECT_EQ(s->version, "20.1.7"); + EXPECT_TRUE(s->changed); +} + +TEST(Compat, PartialMuslVersionSuffix) { + // "gcc 15-musl" — partial version with the legacy suffix. + auto s = normalize_spec("gcc", "15-musl"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(s->version, "15"); + EXPECT_EQ(s->target.str(), host_musl()); +} + +TEST(Compat, XimNamespacePrefixStripped) { + auto s = normalize_spec("xim:gcc", "16.1.0"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(s->family, "gcc"); + EXPECT_FALSE(s->changed); +} + +// ── rejects ───────────────────────────────────────────────────────────────── + +TEST(Compat, RejectsUnknownFamilies) { + EXPECT_FALSE(normalize_spec("tcc", "1.0").has_value()); + EXPECT_FALSE(normalize_spec("llvm", "20.1.7-musl").has_value()); // llvm has no musl flavor + EXPECT_FALSE(normalize_spec("not-a-triple-gcc", "1.0").has_value()); +} diff --git a/tests/unit/test_toolchain_dialect.cpp b/tests/unit/test_toolchain_dialect.cpp index 7e7c972..23b4981 100644 --- a/tests/unit/test_toolchain_dialect.cpp +++ b/tests/unit/test_toolchain_dialect.cpp @@ -62,24 +62,47 @@ TEST(BmiTraitsSpellings, ModuleFlagRowsAreConsistent) { EXPECT_EQ(msvc.bmiSearchPrefix, " /ifcSearchDir "); } -TEST(MingwSpec, MapsToMingwGccPackage) { +TEST(MingwSpec, LegacyMingwSpellingIsGccTargetingWindowsGnu) { + // `mingw` is no longer a family: it normalizes to gcc targeting + // x86_64-windows-gnu. Which payload serves it is host-split at the + // distribution layer (winlibs mingw-gcc on Windows, mingw-cross-gcc + // elsewhere) — the user-facing identity is the same either way. auto spec = parse_toolchain_spec("mingw@16.1.0"); ASSERT_TRUE(spec.has_value()); EXPECT_FALSE(is_system_toolchain(*spec)); + EXPECT_EQ(spec->family, Family::Gcc); + EXPECT_EQ(spec->target.str(), "x86_64-windows-gnu"); + EXPECT_EQ(spec->display(), "gcc@16.1.0 → x86_64-windows-gnu"); + auto pkg = to_xim_package(*spec); - EXPECT_EQ(pkg.ximName, "mingw-gcc"); EXPECT_EQ(pkg.ximVersion, "16.1.0"); - EXPECT_EQ(pkg.display_spec(), "mingw@16.1.0"); ASSERT_FALSE(pkg.frontendCandidates.empty()); +#if defined(_WIN32) + EXPECT_EQ(pkg.ximName, "mingw-gcc"); EXPECT_EQ(pkg.frontendCandidates.front(), "g++.exe"); +#else + EXPECT_EQ(pkg.ximName, "mingw-cross-gcc"); + EXPECT_EQ(pkg.frontendCandidates.front(), "x86_64-w64-mingw32-g++"); +#endif EXPECT_FALSE(pkg.needsGccPostInstallFixup); } -TEST(MingwSpec, DisplayAndDefaultMatching) { - EXPECT_EQ(display_label("mingw-gcc", "16.1.0"), "mingw 16.1.0"); - EXPECT_TRUE(matches_default_toolchain("mingw@16.1.0", "mingw-gcc", "16.1.0")); - EXPECT_FALSE(matches_default_toolchain("mingw@16.1.0", "mingw-gcc", "15.1.0")); - EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "mingw-gcc", "16.1.0")); +TEST(MingwSpec, PayloadIdentityAndDefaultMatching) { + // Both host-split payload dirs identify as the SAME (gcc, windows-gnu). + for (auto dir : {"mingw-gcc", "mingw-cross-gcc"}) { + auto id = identify_xim_payload(dir); + ASSERT_TRUE(id.has_value()) << dir; + EXPECT_EQ(id->family, Family::Gcc) << dir; + EXPECT_EQ(id->target.str(), "x86_64-windows-gnu") << dir; + } + auto id = *identify_xim_payload("mingw-cross-gcc"); + auto def = parse_toolchain_spec("mingw-cross@16.1.0"); // legacy default string + ASSERT_TRUE(def.has_value()); + EXPECT_TRUE(spec_matches_payload(*def, id, "16.1.0")); + EXPECT_FALSE(spec_matches_payload(*def, id, "15.1.0")); + auto llvmDef = parse_toolchain_spec("llvm@16.1.0"); + ASSERT_TRUE(llvmDef.has_value()); + EXPECT_FALSE(spec_matches_payload(*llvmDef, id, "16.1.0")); } TEST(StdFlagFor, PerDialectSpelling) { diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index a504834..6fc873c 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -77,10 +77,18 @@ TEST(MsvcSpec, SystemToolchainClassification) { } TEST(MsvcSpec, StableDefaultMatchesAnyDetectedVersion) { - EXPECT_TRUE(matches_default_toolchain("msvc@system", "msvc", "19.44.35211")); - EXPECT_TRUE(matches_default_toolchain("msvc@system", "msvc", "19.29.30133")); - EXPECT_FALSE(matches_default_toolchain("msvc@system", "gcc", "16.1.0")); - EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "msvc", "19.44.35211")); + // The persisted default is always the stable "msvc@system" — it matches + // whatever concrete version detection reports (family-level match). + auto def = parse_toolchain_spec("msvc@system"); + ASSERT_TRUE(def.has_value()); + PayloadIdentity msvcId{ Family::Msvc, {} }; + EXPECT_TRUE(spec_matches_payload(*def, msvcId, "19.44.35211")); + EXPECT_TRUE(spec_matches_payload(*def, msvcId, "19.29.30133")); + PayloadIdentity gccId{ Family::Gcc, {} }; + EXPECT_FALSE(spec_matches_payload(*def, gccId, "16.1.0")); + auto gccDef = parse_toolchain_spec("gcc@16.1.0"); + ASSERT_TRUE(gccDef.has_value()); + EXPECT_FALSE(spec_matches_payload(*gccDef, msvcId, "19.44.35211")); } // ─── model traits ──────────────────────────────────────────────────────── diff --git a/tests/unit/test_toolchain_registry.cpp b/tests/unit/test_toolchain_registry.cpp index fb3706a..22d9950 100644 --- a/tests/unit/test_toolchain_registry.cpp +++ b/tests/unit/test_toolchain_registry.cpp @@ -1,54 +1,111 @@ #include import std; +import mcpp.platform; import mcpp.toolchain.registry; using namespace mcpp::toolchain; +static std::string host_musl() { + return std::string(mcpp::platform::host_arch) + "-linux-musl"; +} + +// The host-native `musl-gcc` package only exists for Linux hosts; on other +// hosts the payload mapping resolves the triple-named package (a linux-musl +// target from macOS/Windows is cross by definition). +static std::string expected_musl_xim() { + if constexpr (mcpp::platform::is_linux) return "musl-gcc"; + else return host_musl() + "-gcc"; +} + +// ── canonical two-axis identity ────────────────────────────────────────────── + TEST(ToolchainRegistry, MapsGccSpecToGccPackage) { auto spec = parse_toolchain_spec("gcc@16.1.0"); ASSERT_TRUE(spec.has_value()) << spec.error(); + EXPECT_EQ(spec->family, Family::Gcc); + EXPECT_TRUE(spec->is_host_target()); + EXPECT_EQ(spec->spec_str(), "gcc@16.1.0"); + EXPECT_EQ(spec->display(), "gcc@16.1.0"); + EXPECT_TRUE(spec->compatHint.empty()); auto pkg = to_xim_package(*spec); +#if defined(_WIN32) + // gcc family on a Windows host = MinGW-w64 (the GNU-env host toolchain). + EXPECT_EQ(pkg.ximName, "mingw-gcc"); +#else EXPECT_EQ(pkg.ximName, "gcc"); - EXPECT_EQ(pkg.ximVersion, "16.1.0"); - EXPECT_EQ(pkg.display_spec(), "gcc@16.1.0"); + EXPECT_TRUE(pkg.needsGccPostInstallFixup); ASSERT_FALSE(pkg.frontendCandidates.empty()); EXPECT_EQ(pkg.frontendCandidates.front(), "g++"); - EXPECT_TRUE(pkg.needsGccPostInstallFixup); +#endif + EXPECT_EQ(pkg.ximVersion, "16.1.0"); + EXPECT_EQ(pkg.display_spec(), "gcc@16.1.0"); } -TEST(ToolchainRegistry, MapsGccMuslSuffixToMuslGccPackage) { +TEST(ToolchainRegistry, LegacyMuslSuffixNormalizesToMuslTarget) { + // "gcc@15.1.0-musl" — the variant moves out of the version and into the + // target axis: (gcc, 15.1.0, -linux-musl). auto spec = parse_toolchain_spec("gcc@15.1.0-musl"); ASSERT_TRUE(spec.has_value()) << spec.error(); + EXPECT_EQ(spec->family, Family::Gcc); + EXPECT_EQ(spec->version, "15.1.0"); + EXPECT_EQ(spec->target.str(), host_musl()); + EXPECT_FALSE(spec->compatHint.empty()); // legacy spelling → hint auto pkg = to_xim_package(*spec); - EXPECT_TRUE(spec->isMusl); - EXPECT_EQ(pkg.ximName, "musl-gcc"); + EXPECT_EQ(pkg.ximName, expected_musl_xim()); EXPECT_EQ(pkg.ximVersion, "15.1.0"); - EXPECT_EQ(pkg.display_spec(), "gcc@15.1.0-musl"); ASSERT_FALSE(pkg.frontendCandidates.empty()); - EXPECT_EQ(pkg.frontendCandidates.front(), "x86_64-linux-musl-g++"); + EXPECT_EQ(pkg.frontendCandidates.front(), host_musl() + "-g++"); EXPECT_FALSE(pkg.needsGccPostInstallFixup); } -TEST(ToolchainRegistry, MapsMingwCrossSpecToCrossPackage) { - // Linux → Windows MinGW cross: user-facing `mingw-cross` maps to the xim - // package `mingw-cross-gcc`, an ELF frontend producing PE. Distinct from the - // Windows-native `mingw`/`mingw-gcc`. No ELF post-install fixup (PE target). - auto spec = parse_toolchain_spec("mingw-cross@16.1.0"); - ASSERT_TRUE(spec.has_value()) << spec.error(); +TEST(ToolchainRegistry, CrossArchMuslTargetPicksTripleNamedPackage) { + // Target arch ≠ host arch → the triple-named cross package. + ToolchainSpec spec; + spec.family = Family::Gcc; + spec.version = "16.1.0"; + spec.target = { "aarch64", "linux", "musl" }; + if (mcpp::platform::host_arch == std::string_view("aarch64")) + spec.target.arch = "riscv64"; // stay cross on any host + + auto pkg = to_xim_package(spec); + EXPECT_EQ(pkg.ximName, spec.target.str() + "-gcc"); + ASSERT_FALSE(pkg.frontendCandidates.empty()); + EXPECT_EQ(pkg.frontendCandidates.front(), spec.target.str() + "-g++"); + EXPECT_FALSE(pkg.needsGccPostInstallFixup); +} + +TEST(ToolchainRegistry, WindowsGnuTargetIsHostSplitAtDistributionLayer) { + // ONE identity (gcc → x86_64-windows-gnu); the payload is host-split: + // winlibs mingw-gcc on Windows hosts, the Linux-hosted MSVCRT cross + // elsewhere. "cross" appears only in the xim package name — never in + // the user-facing spec. + auto spec = parse_toolchain_spec("gcc@16.1.0"); + ASSERT_TRUE(spec.has_value()); + spec->target = { "x86_64", "windows", "gnu" }; auto pkg = to_xim_package(*spec); - EXPECT_FALSE(spec->isMusl); - EXPECT_EQ(pkg.ximName, "mingw-cross-gcc"); EXPECT_EQ(pkg.ximVersion, "16.1.0"); ASSERT_FALSE(pkg.frontendCandidates.empty()); +#if defined(_WIN32) + EXPECT_EQ(pkg.ximName, "mingw-gcc"); + EXPECT_EQ(pkg.frontendCandidates.front(), "g++.exe"); +#else + EXPECT_EQ(pkg.ximName, "mingw-cross-gcc"); EXPECT_EQ(pkg.frontendCandidates.front(), "x86_64-w64-mingw32-g++"); +#endif EXPECT_FALSE(pkg.needsGccPostInstallFixup); - EXPECT_EQ(display_label("mingw-cross-gcc", "16.1.0"), "mingw-cross 16.1.0"); - EXPECT_TRUE(matches_default_toolchain("mingw-cross@16.1.0", - "mingw-cross-gcc", "16.1.0")); + EXPECT_EQ(pkg.display_spec(), "gcc@16.1.0 → x86_64-windows-gnu"); +} + +TEST(ToolchainRegistry, LegacyMingwCrossSpellingCollapses) { + auto spec = parse_toolchain_spec("mingw-cross@16.1.0"); + ASSERT_TRUE(spec.has_value()) << spec.error(); + EXPECT_EQ(spec->family, Family::Gcc); + EXPECT_EQ(spec->target.str(), "x86_64-windows-gnu"); + EXPECT_FALSE(spec->compatHint.empty()); } TEST(ToolchainRegistry, MapsLlvmAndClangAliasesToLlvmPackage) { @@ -57,15 +114,16 @@ TEST(ToolchainRegistry, MapsLlvmAndClangAliasesToLlvmPackage) { ASSERT_TRUE(llvmSpec.has_value()) << llvmSpec.error(); ASSERT_TRUE(clangSpec.has_value()) << clangSpec.error(); + EXPECT_EQ(llvmSpec->family, Family::Llvm); + EXPECT_EQ(clangSpec->family, Family::Llvm); // alias family → llvm + EXPECT_TRUE(llvmSpec->compatHint.empty()); + EXPECT_FALSE(clangSpec->compatHint.empty()); + auto llvmPkg = to_xim_package(*llvmSpec); auto clangPkg = to_xim_package(*clangSpec); - EXPECT_EQ(llvmPkg.ximName, "llvm"); EXPECT_EQ(clangPkg.ximName, "llvm"); - EXPECT_EQ(llvmPkg.ximVersion, "20.1.7"); - EXPECT_EQ(clangPkg.ximVersion, "20.1.7"); - EXPECT_EQ(llvmPkg.display_spec(), "llvm@20.1.7"); - EXPECT_EQ(clangPkg.display_spec(), "clang@20.1.7"); + EXPECT_EQ(clangPkg.display_spec(), "llvm@20.1.7"); ASSERT_FALSE(clangPkg.frontendCandidates.empty()); #if defined(_WIN32) EXPECT_EQ(clangPkg.frontendCandidates.front(), "clang++.exe"); @@ -74,15 +132,49 @@ TEST(ToolchainRegistry, MapsLlvmAndClangAliasesToLlvmPackage) { #endif } -TEST(ToolchainRegistry, ResolvesPartialMuslVersionForDisplayAndPackage) { +TEST(ToolchainRegistry, ResolvesPartialMuslVersion) { auto spec = parse_toolchain_spec("gcc", "15-musl"); ASSERT_TRUE(spec.has_value()) << spec.error(); + EXPECT_EQ(spec->version, "15"); + EXPECT_EQ(spec->target.str(), host_musl()); auto resolved = with_resolved_xim_version(*spec, "15.1.0"); auto pkg = to_xim_package(resolved); - - EXPECT_EQ(resolved.version, "15.1.0-musl"); - EXPECT_EQ(pkg.ximName, "musl-gcc"); + EXPECT_EQ(resolved.version, "15.1.0"); + EXPECT_EQ(pkg.ximName, expected_musl_xim()); EXPECT_EQ(pkg.ximVersion, "15.1.0"); - EXPECT_EQ(pkg.display_spec(), "gcc@15.1.0-musl"); + EXPECT_EQ(pkg.display_spec(), + std::format("gcc@15.1.0 → {}", host_musl())); +} + +TEST(ToolchainRegistry, RejectsUnknownFamily) { + auto spec = parse_toolchain_spec("tcc@1.0"); + EXPECT_FALSE(spec.has_value()); +} + +// ── payload reverse mapping ────────────────────────────────────────────────── + +TEST(ToolchainRegistry, IdentifiesToolchainPayloadsAndSkipsOthers) { + auto gcc = identify_xim_payload("gcc"); + ASSERT_TRUE(gcc.has_value()); + EXPECT_EQ(gcc->family, Family::Gcc); + EXPECT_TRUE(gcc->target.empty()); + + auto musl = identify_xim_payload("musl-gcc"); + ASSERT_TRUE(musl.has_value()); + EXPECT_EQ(musl->target.str(), host_musl()); + + auto crossMusl = identify_xim_payload("aarch64-linux-musl-gcc"); + ASSERT_TRUE(crossMusl.has_value()); + EXPECT_EQ(crossMusl->target.str(), "aarch64-linux-musl"); + + auto llvm = identify_xim_payload("llvm"); + ASSERT_TRUE(llvm.has_value()); + EXPECT_EQ(llvm->family, Family::Llvm); + + // Non-toolchain xpkgs must not be identified (list/doctor filter on this). + EXPECT_FALSE(identify_xim_payload("ninja").has_value()); + EXPECT_FALSE(identify_xim_payload("glibc").has_value()); + EXPECT_FALSE(identify_xim_payload("python").has_value()); + EXPECT_FALSE(identify_xim_payload("linux-headers").has_value()); } diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp new file mode 100644 index 0000000..326c794 --- /dev/null +++ b/tests/unit/test_toolchain_triple.cpp @@ -0,0 +1,137 @@ +#include + +import std; +import mcpp.toolchain.triple; + +using namespace mcpp::toolchain::triple; + +// ── parse: canonical spellings round-trip ──────────────────────────────────── + +TEST(Triple, ParsesCanonicalThreeSegment) { + auto t = parse("x86_64-linux-musl"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(t->arch, "x86_64"); + EXPECT_EQ(t->os, "linux"); + EXPECT_EQ(t->env, "musl"); + EXPECT_EQ(t->str(), "x86_64-linux-musl"); + EXPECT_TRUE(t->is_musl()); + EXPECT_TRUE(is_known_target(*t)); +} + +TEST(Triple, ParsesCanonicalWindowsGnu) { + auto t = parse("x86_64-windows-gnu"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(t->str(), "x86_64-windows-gnu"); + EXPECT_TRUE(t->is_windows_gnu()); + EXPECT_TRUE(t->is_pe()); + EXPECT_EQ(t->family(), "windows"); + EXPECT_TRUE(is_known_target(*t)); +} + +TEST(Triple, ParsesCanonicalMacos) { + auto t = parse("aarch64-macos"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(t->arch, "aarch64"); + EXPECT_EQ(t->os, "macos"); + EXPECT_EQ(t->env, ""); + EXPECT_EQ(t->str(), "aarch64-macos"); + EXPECT_EQ(t->family(), "unix"); +} + +// ── parse: alias spellings normalize to canonical ──────────────────────────── + +TEST(Triple, NormalizesGnuMingwSpelling) { + // GNU vendor triple: "w64" = vendor, "mingw32" = os segment (historical — + // 64-bit targets still say mingw32). Canonicalizes to windows-gnu. + auto t = parse("x86_64-w64-mingw32"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(t->str(), "x86_64-windows-gnu"); + EXPECT_TRUE(t->is_windows_gnu()); +} + +TEST(Triple, NormalizesFourSegmentRustSpellings) { + EXPECT_EQ(parse("x86_64-unknown-linux-musl")->str(), "x86_64-linux-musl"); + EXPECT_EQ(parse("x86_64-unknown-linux-gnu")->str(), "x86_64-linux-gnu"); + EXPECT_EQ(parse("x86_64-pc-windows-msvc")->str(), "x86_64-windows-msvc"); + EXPECT_EQ(parse("x86_64-pc-windows-gnu")->str(), "x86_64-windows-gnu"); +} + +TEST(Triple, NormalizesAppleSpellings) { + // arm64 → aarch64 (GNU arch spelling); darwin/macosx version suffixes drop. + EXPECT_EQ(parse("arm64-apple-darwin24.1.0")->str(), "aarch64-macos"); + EXPECT_EQ(parse("arm64-apple-macosx15.0")->str(), "aarch64-macos"); + EXPECT_EQ(parse("aarch64-apple-darwin")->str(), "aarch64-macos"); +} + +TEST(Triple, NormalizesBareLinuxToGnuEnv) { + EXPECT_EQ(parse("x86_64-linux")->str(), "x86_64-linux-gnu"); +} + +TEST(Triple, NormalizesDumpmachineSpellings) { + // What real toolchains report via -dumpmachine. + EXPECT_EQ(parse("x86_64-pc-linux-gnu")->str(), "x86_64-linux-gnu"); + EXPECT_EQ(parse("x86_64-linux-gnu")->str(), "x86_64-linux-gnu"); + EXPECT_EQ(parse("aarch64-linux-musl")->str(), "aarch64-linux-musl"); +} + +// ── parse: rejects non-triples ─────────────────────────────────────────────── + +TEST(Triple, RejectsNonTriples) { + EXPECT_FALSE(parse("").has_value()); + EXPECT_FALSE(parse("gcc").has_value()); + EXPECT_FALSE(parse("x86_64").has_value()); + EXPECT_FALSE(parse("x86_64-linux-mus").has_value()); // typo'd env segment + EXPECT_FALSE(parse("wasm32-wasi").has_value()); // outside the language +} + +// ── known-target vocabulary ────────────────────────────────────────────────── + +TEST(Triple, KnownTargetTableExposesTierAndPins) { + auto t = parse("x86_64-linux-musl"); + ASSERT_TRUE(t.has_value()); + auto* info = find_known_target(*t); + ASSERT_NE(info, nullptr); + EXPECT_EQ(info->tier, "verified"); + EXPECT_EQ(info->pin, "gcc@16.1.0"); + EXPECT_TRUE(info->defaultStatic); + + auto w = parse("x86_64-w64-mingw32"); // alias resolves to the same row + ASSERT_TRUE(w.has_value()); + auto* winfo = find_known_target(*w); + ASSERT_NE(winfo, nullptr); + EXPECT_EQ(winfo->canonical, "x86_64-windows-gnu"); + EXPECT_EQ(winfo->pin, "gcc@16.1.0"); + EXPECT_TRUE(winfo->defaultStatic); +} + +TEST(Triple, UnknownButParseableTripleIsNotKnown) { + auto t = parse("riscv64-linux-gnu"); + ASSERT_TRUE(t.has_value()); + EXPECT_FALSE(is_known_target(*t)); +} + +// ── did-you-mean ───────────────────────────────────────────────────────────── + +TEST(Triple, DidYouMeanCatchesTypos) { + auto s = did_you_mean("x86_64-linux-mus"); + ASSERT_TRUE(s.has_value()); + EXPECT_EQ(*s, "x86_64-linux-musl"); + + auto w = did_you_mean("x86_64-w64-mingw"); + ASSERT_TRUE(w.has_value()); + EXPECT_EQ(*w, "x86_64-windows-gnu"); +} + +TEST(Triple, DidYouMeanStaysQuietOnGarbage) { + EXPECT_FALSE(did_you_mean("totally-unrelated-string-xyz").has_value()); +} + +// ── host ───────────────────────────────────────────────────────────────────── + +TEST(Triple, HostTripleIsCanonicalAndNonEmpty) { + auto h = host_triple(); + EXPECT_FALSE(h.empty()); + auto reparsed = parse(h.str()); + ASSERT_TRUE(reparsed.has_value()); + EXPECT_EQ(reparsed->str(), h.str()); +}