diff --git a/.agents/docs/2026-07-31-cpp20-implementation-plan.md b/.agents/docs/2026-07-31-cpp20-implementation-plan.md new file mode 100644 index 00000000..19da5934 --- /dev/null +++ b/.agents/docs/2026-07-31-cpp20-implementation-plan.md @@ -0,0 +1,59 @@ +# C++20 档位支持 — 实施计划(单 PR,目标 2026.7.31.1) + +> 设计:`.agents/docs/2026-07-31-cpp20-standard-support-design.md`(review 已定案) +> 基线:`7af26ba` · 分支:`feat/cpp20-standard` +> 定案回顾:MSVC 取 20(带 `cl ≥ 19.38` 版本阈值)· 不动 scaffold/模板 · 不加"显式声明"位 + +## 步骤(按依赖顺序,每步都能独立验证) + +### S1 解析层:白名单 + 别名 + 文案 +- `src/manifest/types.cppm::normalize_cpp_standard`:加 `c++20`/`c++2a`(level 20,gnu=false)、 + `gnu++20`/`gnu++2a`(level 20,gnu=true);错误文案补全新值。 +- 验证:`tests/unit/test_manifest.cpp` 新增用例;`mcpp build` 对 `standard="c++19"` 仍报错且文案含 c++20。 + +### S2 能力层:`hasImportStd` → 加 `importStdMinLevel` +- `src/toolchain/model.cppm`:`int importStdMinLevel = 0;`(0 = 未知,走旧布尔语义)。 +- provider 赋值(与 `hasImportStd` 同点,知识留在各自 provider): + - `gcc.cppm::enrich_toolchain` → 20 + - `clang.cppm::enrich_toolchain` libc++ 分支 → 20;MSVC STL 回退分支 → `msvc_std_min_level(tc)` + - `msvc.cppm` → `msvc_std_min_level(tc)`,由本地 `toolset_at_least(tc, 19, 38)` 决定 20 / 23 +- 验证:单测构造 `Toolchain{MSVC, "19.37"}` → 23,`"19.44"` → 20。 + +### S3 门:prepare 里从一维变二维 +- `src/build/prepare.cppm:3735` 之后追加档位判断 + 可行动文案(设计 §4.2)。 +- 本地小函数 `std_level_name(int)`(20/23/26 → "c++20"…),不造第二张表。 +- 验证:单测覆盖文案;e2e 177(Windows)覆盖真机。 + +### S4 顺带缺陷:build.mcpp 的 `-std=` 走方言层 +- `src/build/build_program.cppm`:`run_build_program` 的 `cppStandard` 形参改为 + `const manifest::CppStandardConfig&`,内部用 `cppfly::std_flag(hostTc, canonical, level)`。 +- 调用点:`prepare.cppm:3479`(依赖)、`prepare.cppm:3606`(根)传 `.cppStandard` 整体。 +- 验证:单测/或 e2e 92(build.mcpp import)保持绿;新增 c++fly 工程 + build.mcpp 的回归(单测层足够)。 + +### S5 单元测试(6 组,设计 §5.1) +`test_manifest.cpp` / `test_toolchain_dialect.cpp` / `test_cppfly.cpp` / +`test_cache_key.cpp` / `test_fingerprint.cpp` / 新增门测试。 + +### S6 e2e(设计 §5.2) +- `175_cpp20_import_std.sh` — c++20 + import std,构建/运行/`-std=c++20` 断言(POSIX) +- `176_cpp20_std_bmi_isolation.sh` — c++23 与 c++20 交替构建,两套缓存并存 +- `177_cpp20_msvc.sh` — Windows:`/std:c++20` + import std 硬断言 + 打印 `cl` 版本 + +### S7 版本 + 文档 +- `src/toolchain/fingerprint.cppm::MCPP_VERSION` → `2026.7.31.1` +- bootstrap pin(`.xlings.json` / `MCPP_PIN`)**不动**(自举起点,不随发布走) +- `scripts/check_version_pins.sh` 通过 +- 文档已在设计阶段更新(EN/ZH),与本 PR 同批合入 + +### S8 本地验证 +`mcpp build -p mcpp` → 单测全绿 → `tests/e2e/run_all.sh`(至少 175/176 + 回归面) + +### S9 PR → CI → 合入 +单 PR,bypass squash 合入 main。 + +### S10 发布闭环 +release(四平台)→ gitcode 资源本地 `gtc` 补传 + GET 核验 → xim-pkgindex PR → +`xlings install mcpp` 真装验证 → bootstrap pin bump。 + +## 不做(设计已定案) +scaffold/模板改动、`mcpp new --standard`、依赖 floor 检查、"显式声明"位、C++17。 diff --git a/.agents/docs/2026-07-31-cpp20-standard-support-design.md b/.agents/docs/2026-07-31-cpp20-standard-support-design.md new file mode 100644 index 00000000..b66642d9 --- /dev/null +++ b/.agents/docs/2026-07-31-cpp20-standard-support-design.md @@ -0,0 +1,415 @@ +# C++20 作为一等 `standard` 档位:`import std` 全平台可用性设计 + +> 日期:2026-07-31 · 基线:`7af26ba`(2026.7.30.3 已发布)· 目标版本:2026.7.31.1 +> 输入:用户需求——"给 mcpp 增加 C++20 的支持,C++23 依然作为默认;核心是**目前支持的平台和工具链都可用**" +> 外部依据:[gcc PR106852](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106852)、 +> [microsoft/STL#3945](https://github.com/microsoft/STL/issues/3945)(已 fixed,PR #3977,2023-08-31 合入)、 +> [libc++ Modules](https://libcxx.llvm.org/Modules.html) +> 前置文档:`2026-06-01-cpp-standard-first-class-design.md`(standard 一等化)、 +> `2026-07-14-std-features-experimental-gate-design.md`(c++fly / `cppfly.cppm` 数据表范式) +> 本文所有工具链结论均由**本机实机探针**得出(§2.2),不是文档推断。 +> **review 定案(2026-07-31)**:MSVC 侧取 20(带版本阈值,§2.3);不加 `mcpp new --standard`、 +> 模板保持 C++23(§4.6);不引入"是否显式声明"位(§9-Q3)。 + +## 0. 一句话结论 + +**在 `normalize_cpp_standard` 的白名单里增加 `c++20` / `c++2a` / `gnu++20`(level = 20), +默认值仍是 `c++23`;并把 `import std` 的可用性从今天的布尔 `Toolchain::hasImportStd` +升级为"最低档位" `Toolchain::importStdMinLevel`,由各 provider 自己填。** + +```toml +[package] +standard = "c++20" # 新增:仍然可以 import std;(GCC≥15 / Clang≥17+libc++ 实测通过) +``` + +机制上**没有新管线**:标准值已经是图全局单值,已经进指纹、进 std BMI 身份、进依赖构建 +缓存键;`-std=` 拼写已经走 `dialect.cppm::std_flag_for`(MSVC 侧 `level <= 20 → /std:c++20` +这一行**从写下那天起就是不可达代码**,本设计第一次让它可达)。真正要新增的只有两样: + +1. 解析层三个新的别名行(白名单 + level 数值); +2. **能力门**:`import std` 不是所有工具链在所有档位都能提供 —— 今天的门只问 + "有没有 std 模块源",C++20 落地后必须问"**这个工具链在这个档位上能不能建 std 模块**" + (三家现代实现的答案都是"能",唯一的例外是 STL#3977 解禁之前的老 MSVC,见 §2.3), + 否则那批用户拿到的是 `std.ixx` 内部错误而不是可行动诊断。 + +## 1. 为什么是 C++20,以及为什么默认仍是 C++23 + +### 1.1 语言学依据(用户输入的核心论点) + +`import std;` 是 C++23 的库特性,但**命名模块本身是 C++20 语言特性**。三家实现都已经 +(或已同意)在 C++20 模式下提供 `std` 模块: + +| 实现 | 依据 | 状态 | +|---|---|---| +| libstdc++ / GCC | PR106852(std 模块实现);`bits/std.cc` **无任何 `__cplusplus` 版本守卫**(实机核对) | C++20 下可编译(§2.2 实测) | +| libc++ / Clang | [libc++ Modules 文档](https://libcxx.llvm.org/Modules.html):std 模块支持 C++20/23/26,要求 Clang 17+ | C++20 下可编译(§2.2 实测) | +| MSVC STL | STL#3945 "Supporting `import std;` in C++20" → **PR #3977 已合入**(2023-08-31),原话:"blocking 是纯策略选择,没有技术原因" | STL 侧已解禁,**但官方文档仍写 `/std:c++latest`**(§2.3) | + +结论:C++20 + `import std` 在 mcpp 支持的 GNU 侧工具链上是**已验证可用**的组合, +不是理论推演。 + +### 1.2 为什么地板是 C++20,不再往下 + +命名模块 = C++20 特性。mcpp 的整个构建模型(模块图、BMI、dyndep、`import std`)在 +C++17 及以下**不存在**。所以 `c++20` 是这个白名单的天然下界,`c++17` 不在本设计范围 +(§8 非目标)。 + +### 1.3 为什么默认仍是 C++23 + +- `mcpp new` 生成的模板用 `std::println`(`src/scaffold/create.cppm:258-261`),这是 C++23 库特性; +- 生态(mcpp-index)包描述符默认 `standard = "c++23"`(`src/manifest/xpkg.cppm:1070`); +- C++23 是"import std 一定在标准里"的最低档位,C++20 下的 std 模块是**实现扩展**; +- 改默认值 = 全生态指纹翻车 + 一次全量重编。 + +**C++20 是给"必须跟老约束共处"的工程(嵌入式 SDK、公司内规、第三方 API 只到 C++20) +的逃生舱,不是新推荐值。** + +## 2. 现状盘点与实机证据 + +### 2.1 触点清单(改动面就是这张表) + +| 触点 | 位置 | C++20 落地后 | +|---|---|---| +| 白名单 + level 数值化 | `src/manifest/types.cppm:635-681` `normalize_cpp_standard` | **改**:加 `c++20`/`c++2a`/`gnu++20`(level 20) | +| 错误文案 | `src/manifest/types.cppm:679-681` | **改**:列出新值 | +| canonical 回写 | `src/manifest/toml.cppm:211-214`、`src/manifest/xpkg.cppm:1720-1725` | 不改(已把 `package.standard` 归一为 canonical → 指纹天然免别名抖动) | +| 各家 `-std=` 拼写 | `src/toolchain/dialect.cppm:116-123` | 不改。`level <= 20 → /std:c++20` 这行**首次可达** | +| c++fly / c++latest 解析 | `src/toolchain/cppfly.cppm:140-200` | 不改(999/1000 档位与 20 无交集) | +| 图全局标准 | `src/build/plan.cppm:388-396`、`src/build/prepare.cppm:3665-3690` | 不改(单值来自根 manifest) | +| 全 TU 注入 | `src/build/flags.cppm:383-390` | 不改 | +| p1689 扫描 | `src/modgraph/p1689.cppm:343` | 不改(接收 `stdFlagAndDialect`;`-std=c++23` 只是兜底);C++20 下扫描实测正常 | +| std BMI 身份 | `src/toolchain/stdmod.cppm:114-138` `metadata_for` | 不改(`cpp_standard` + `std_flag` 已在身份里) | +| 依赖构建缓存键 | `cache_key::BuildAxes.cppStandard/cppStandardFlag` | 不改 | +| 指纹 | `src/build/prepare.cppm:3745` | 不改 | +| **import std 能力门** | `src/build/prepare.cppm:3735-3740` | **改**:布尔 → 档位比较(§4.2) | +| build.mcpp helper | `src/build/build_program.cppm:647` | **改(顺带缺陷)**:`"-std=" + canonical` 对 `c++fly`/`c++latest` 拼出非法旗标(§4.5) | +| 文档 | `docs/05-mcpp-toml.md:44-58`、`docs/zh/05-mcpp-toml.md:44-56`、两处能力表 | **改**(ZH 侧还缺 `c++fly` 条目) | + +### 2.2 实机证据矩阵(2026-07-31,本机) + +方法:**回放 mcpp 自己写下的真实 std 模块构建命令**(`~/.mcpp/build-cache/v1/std/*/std-module.json` +里的 `std_build_commands`),只把 `-std=c++23` 替换成 `-std=c++20`,再用同一套旗标编译 +一个 `import std;` 的消费者 TU 并链接、运行。这样验证的是 mcpp 真正会发出的命令行 +(含 `--sysroot` / `-B` / `-isystem` / hermetic link model),不是"裸 g++ 能不能过"。 + +| 工具链 | target triple | std 模块 @ c++20 | 消费者编译+链接 | 运行 | +|---|---|---|---|---| +| gcc 16.1.0 | `x86_64-linux-gnu` | ✅ | ✅ | ✅ `hi 1` | +| gcc 15.1.0 | `x86_64-linux-gnu` | ✅ | ✅ | ✅ `hi 1` | +| gcc 16.1.0 (musl) | `x86_64-linux-musl` | ✅ | ✅ | ✅(`-static` 本机跑通) | +| gcc 15.1.0 (musl) | `x86_64-linux-musl` | ✅ | ✅ | ✅(`-static` 本机跑通) | +| gcc 16.1.0 (mingw cross) | `x86_64-w64-mingw32` | ✅ | ✅ | ✅(`-static` + wine 跑通) | +| clang 22.1.8 + libc++ | `x86_64-unknown-linux-gnu` | ✅(含 `std.compat`) | ✅ | ✅ `hi 1` | +| MSVC(`cl 19.51.36252`,VS 18 / MSVC 14.51) | `x86_64-windows-msvc` | ✅ **Windows CI 实测**(PR#325,`177_cpp20_msvc.sh`) | ✅ | ✅ `cl20 3` | +| clang + MSVC STL | Windows | 未单独验证 → 保持 23(`tc.version` 是 clang 的,答不了 cl 的问题) | — | — | +| llvm 20.1.7(本机 payload) | — | 不适用:该 payload 只有 `bin/ lib/`,**无 `share/libc++/v1/std.cppm`** → `hasImportStd=false`,与档位无关 | + +附带抓到的两条硬事实: + +1. **跨档位 BMI 绝对不可复用**——用 C++23 建的 `std.gcm` 去喂 C++20 的 TU: + + ``` + std: error: language dialect differs 'C++20', expected 'C++23' + std: error: failed to read compiled module: Bad file data + ``` + + 这正好**反证了缓存设计是对的**:`cpp_standard` + `std_flag` 早已在 std 身份键和 + `BuildAxes` 里,c++20 与 c++23 天然落到不同目录,不需要为本特性动任何缓存代码。 + +2. **实验门旗标与档位强耦合**:回放 `c++fly` 那份缓存条目时,`-freflection` 直接报 + `'-freflection' only supported with '-std=c++26' or '-std=gnu++26'`。这不影响 c++20 + (c++fly 是独立值),但它明确了一条不变量:**任何"档位 × 方言旗标"的组合都必须同源计算** + ——今天由 `cppfly::effective_dialect_flags` 单点保证,C++20 不得绕开它。 + +复现脚本(留档):见 §6 "验证与复现"。 + +### 2.3 MSVC:唯一未实机验证的一格(review 已定案取 20) + +- STL 侧:#3945 已 fixed(PR #3977 合入,2023-08-31),即 `std.ixx` 在 C++20 模式下不再被 STL 主动拦; +- 但 Microsoft Learn 的 [import STL named module 教程](https://learn.microsoft.com/en-us/cpp/cpp/tutorial-import-stl-named-module) + 至今仍写 **`/std:c++latest` 是 `import std;` 的必要条件**,且未标注最低 VS 版本 —— 文档滞后于实现; +- mcpp 的 MSVC 支持是 detection-first 的 `msvc@system`(`src/toolchain/msvc.cppm`), + 编译器与 STL 版本由用户机器决定,mcpp 无法保证用户装的是解禁后的 STL。 + +**review 定案(2026-07-31):取 20**,与 GNU 侧一致 —— C++20 是用户主动压低档位的知情选择, +不为"可能存在的老 STL"给全体 MSVC 用户加一道拒绝。 + +但"取 20"不等于"无条件 20":PR #3977 之前的 STL(VS 2022 17.8 / `cl` 19.38 之前)确实会拦, +那些机器上 `import std;` + `/std:c++20` 会落到 `std.ixx` 内部报错。所以这一行写成 +**带版本阈值的数据行**(与 `cppfly::kLatestStd` 的 `minMajor` 同范式): + +| MSVC 版本 | `importStdMinLevel` | +|---|---| +| `cl` ≥ 19.38(VS 2022 17.8,STL#3977 首个可能的发布档) | **20** | +| 更老 | 23(落到可行动诊断,而不是 `std.ixx` 内部错误) | + +这样做的额外好处:**门不会变成不可达代码**。若无条件取 20,则全部 provider 都是 20、 +白名单下界也是 20,`level < importStdMinLevel` 永远为假 —— 又是一处"写下即不可达"的分支 +(`dialect.cppm:119` 就这样躺了一年)。版本阈值让它既可达、又可被单测覆盖。 + +⚠️ 19.38 这个阈值来自 PR 合入时间推算,**必须由 Windows CI 的实测作业确认**(§5.3); +若实测显示更早/更晚,改的仍然是这一行数据。 + +## 3. 概念模型:两个正交轴 + +``` + 语言档位轴 (level) 能力轴 (per toolchain) + 20 ── 23 ── 26 ── latest(999) ── fly(1000) hasImportStd + importStdMinLevel + │ │ │ + └─────┴── 用户在 mcpp.toml 声明 ────────────────────┴── provider 探测时填 + │ + └──→ 门:needsStdModule && level < importStdMinLevel → 可行动错误 +``` + +今天的门是**一维**的(`hasImportStd` 布尔),因为最低档位就是 23,永远满足。 +放开 C++20 后门必须变成**二维**:现代工具链全部答 20(门恒真、零影响), +只有 STL#3977 之前的老 MSVC 会命中 23,拿到可行动诊断而不是 `std.ixx` 内部错误。 + +## 4. 设计 + +### 4.1 解析层:三个新白名单条目 + +`src/manifest/types.cppm::normalize_cpp_standard`: + +```cpp +if (s == "c++20" || s == "c++2a") { + out.canonical = "c++20"; out.flag = "-std=c++20"; out.level = 20; out.gnuDialect = false; + return out; +} +if (s == "gnu++20" || s == "gnu++2a") { + out.canonical = "gnu++20"; out.flag = "-std=gnu++20"; out.level = 20; out.gnuDialect = true; + return out; +} +``` + +错误文案同步:`expected c++20, c++23, c++26, c++2c, gnu++20, gnu++23, gnu++26, c++latest, or c++fly`。 + +注意 `out.flag` 只是 GNU 侧静态兜底;真正下发的拼写永远来自 +`cppfly::std_flag(tc, canonical, level)` → `dialect_for(tc)` → `std_flag_for`, +所以 MSVC 自动得到 `/std:c++20`(`dialect.cppm:119`,首次可达)。 + +### 4.2 能力层:`hasImportStd` 布尔 → 档位 + +`src/toolchain/model.cppm`,紧挨 `hasImportStd`: + +```cpp +bool hasImportStd = false; +// 该工具链能在多低的 -std= 档位上构建 std 模块。0 = 未知(按 hasImportStd 走旧语义)。 +int importStdMinLevel = 0; +``` + +**知识留在各 provider 里**(与 `hasImportStd` 同一处赋值,不新建中心表——中心表会 +和 provider 的探测结果两处推导同一决策,这正是历史上反复踩的坑): + +| provider | 赋值点 | 值 | 依据 | +|---|---|---|---| +| GCC | `src/toolchain/gcc.cppm:132-134` | **20** | §2.2 实测 gcc 15/16 × glibc/musl/mingw 全通过 | +| Clang + libc++ | `src/toolchain/clang.cppm:145-147` | **20** | §2.2 实测 clang 22.1.8;libc++ 文档声明 C++20 起支持 | +| Clang + MSVC STL | `src/toolchain/clang.cppm:154-156` | **23** | 用的是 MSVC 的 `std.ixx`(STL 的策略说了算),但此处 `tc.version` 是 **clang 的版本**,答不了 cl banner 的问题 ⇒ 保持严格 | +| MSVC | `src/toolchain/msvc.cppm:548-552` | **20**(`cl` ≥ 19.38)/ **23**(更老) | §2.3,review 定案取 20 + 版本阈值 | + +MSVC 那行需要 major.minor 两段(`19.38`),而 `cppfly::compiler_major` 只取前导整数 +(MSVC 一律得 19)。**不要**去改 `compiler_major` 的语义(它有别的消费者),在 `msvc.cppm` +里加一个本地的 `toolset_at_least(tc, 19, 38)` 即可 —— 版本比较的知识本来就属于该 provider。 + +门(`src/build/prepare.cppm:3735-3740`,原地扩写): + +```cpp +if (needsStdModule && !tc->hasImportStd) { /* 原文案不变 */ } +if (needsStdModule && tc->importStdMinLevel > 0 + && m->cppStandard.level < tc->importStdMinLevel) { + return std::unexpected(std::format( + "source imports std but toolchain '{}' provides the std module only from {} up " + "(project [package].standard = \"{}\"); raise the standard or drop `import std;`", + tc->label(), level_name(tc->importStdMinLevel), m->package.standard)); +} +``` + +`level_name(20|23|26)` 是本地小函数,别造第二张表。 + +### 4.3 图语义:根 manifest 单值,依赖侧的诚实说明 + +今天的事实(已核实,`plan.cppm:388` 是唯一读取点):**依赖包 manifest 里的 +`[package].standard` 在构建时被完全忽略**,全图用根包的档位。这是 BMI 兼容性 +强制的(§2.2 证据 1:跨档位 BMI 直接拒读,同图不可能存在两个档位)。 + +C++20 让这条一直存在的语义**第一次变得可见**:根设 c++20,而某个依赖用了 +`std::print` / `std::expected`,报错会出现在依赖的 TU 里。 + +评估过的"依赖最低档位检查"方案及其陷阱: + +> 想法:依赖声明 `standard = "c++23"` 而根解析出 c++20 ⇒ 提前报错。 +> **陷阱:默认值与显式声明不可区分**——`xpkg.cppm:1070` 和 toml 解析都在缺省时 +> 写死 `"c++23"`,所以这条检查会把**整个 mcpp-index 生态**判成"要求 C++23", +> 直接封死 c++20 的所有实用场景。 + +⇒ **v1 不做 floor 检查**。要做必须先加"是否显式声明"位(`std::optional` 或 +`standardDeclared` bool),那是独立一步(§9-Q3)。v1 只做**诊断增强**:当编译失败发生在 +依赖包的 TU 且根档位 < 23 时,在错误尾部追加一行提示"根工程 standard = c++20, +依赖 `` 可能需要更高档位"。 + +### 4.4 缓存、指纹、跨档位隔离(零改动,附证明) + +| 层 | 是否含标准 | 结论 | +|---|---|---| +| 指纹 → `target/-` | `fpi.cppStandard = m->package.standard`(**已归一为 canonical**,`toml.cppm:214`) | c++20 与 c++23 落不同产物目录 | +| std BMI 身份 | `metadata_for` 的 `cpp_standard` + `std_flag` + 完整命令串 | 天然分桶,且命中校验会拒绝错档位 | +| 依赖构建缓存 | `BuildAxes.cppStandard` + `cppStandardFlag` | 同上 | +| GCC/Clang 自身 | `language dialect differs` 硬拒(§2.2) | **最后一道保险**:即便键写错也不会静默错编 | + +**本特性不新增任何缓存代码。** 这是设计能收敛到"三行白名单 + 一个门"的根本原因。 + +### 4.5 顺带缺陷:`build.mcpp` 的 `-std=` 拼写绕过了方言层 + +`src/build/build_program.cppm:647`: + +```cpp +std::string std_flag = "-std=" + std::string(cppStandard.empty() ? "c++23" : cppStandard); +``` + +传入的是 `m->cppStandard.canonical`(`prepare.cppm:3606`)。当工程是 `standard = "c++fly"` +或 `"c++latest"` 时,这里拼出的是 **`-std=c++fly` / `-std=c++latest`** —— GNU 驱动直接 +拒绝的非法拼写。这正是 `cppfly.cppm` 头注释里记的 §11-Q5 老坑,在 build.mcpp 路径上 +从未修好(因为没人用 c++fly 跑过 build.mcpp)。 + +C++20 本身不触发这个 bug(`-std=c++20` 合法),但本设计要求**同一决策只有一处推导**: + +```cpp +// host 工具链决定拼写(build.mcpp 用宿主编译器编译并运行) +std::string std_flag = mcpp::toolchain::cppfly::std_flag(hostTc, canonical, level); +``` + +⇒ 接口从 `std::string_view cppStandard` 改为传 `const CppStandardConfig&`(canonical + level)。 +调用点两处:`prepare.cppm:3479`(依赖)、`prepare.cppm:3606`(根)。 + +### 4.6 scaffold:零改动(review 定案) + +`mcpp new` 生成的 `main.cpp` 用 `std::println`(C++23)。**模板与 `mcpp new` 一律不动, +也不加 `--standard` 选项。** + +定案理由:默认档位是 c++23,用户把它改成 c++20 这个动作本身就说明**知道自己在放弃哪些特性** +——C++20 支持的定位是"给特殊约束下的用户多一个选择",不是新的推荐路径。所有模板、脚手架、 +文档示例继续以 C++23 为准。代价(改档位后模板里的 `std::println` 需要自己换成 `std::cout`) +已写进两份文档的 `c++20` 条目。 + +### 4.7 c++fly / c++latest 不受影响 + +`cppfly::std_flag` 只在 `level >= 999` 时重解析;`kLatestStd` 表的下界是 c++23。 +C++20 与实验门的组合(`-freflection` 要求 c++26,§2.2 证据 2)天然不可达: +c++fly 自己解析档位,不接受外部档位。**不需要为 c++20 增加任何 c++fly 侧的守卫。** + +## 5. 测试计划 + +### 5.1 单元测试 + +| 文件 | 用例 | +|---|---| +| `tests/unit/test_manifest.cpp` | `c++20`/`c++2a` → canonical `c++20`,level 20,`gnuDialect=false`;`gnu++20`/`gnu++2a` → `gnu++20`,level 20,`gnuDialect=true`;`c++19` 仍报错且文案含 `c++20` | +| `tests/unit/test_toolchain_dialect.cpp` | `std_flag_for(msvc, "c++20", 20) == "/std:c++20"`(**首次覆盖这条分支**);`std_flag_for(gnu, "gnu++20", 20) == "-std=gnu++20"` | +| `tests/unit/test_cppfly.cpp` | `cppfly::std_flag(gcc16, "c++20", 20) == "-std=c++20"`(证明低档位不被 latest 逻辑劫持) | +| `tests/unit/test_cache_key.cpp` | `cppStandardFlag = "-std=c++20"` 与 c++23 键不同 | +| `tests/unit/test_fingerprint.cpp` | 同上,指纹分桶 | +| 新增门测试 | `importStdMinLevel=23` 且 level=20 且 needsStdModule → 错误文案含 "provides the std module only from" | + +### 5.2 e2e(编号接 175+) + +| 脚本 | 内容 | 平台 | +|---|---|---| +| `175_cpp20_import_std.sh` | `standard = "c++20"` 的工程 + `import std;` + 自有模块:`mcpp build` 成功、`mcpp run` 输出正确;断言 `compile_commands.json` 里出现 `-std=c++20` 且**不出现** `-std=c++23` | Linux/macOS(GNU 侧) | +| `176_cpp20_std_bmi_isolation.sh` | 同一工程先 c++23 后 c++20 构建,断言两个 std 缓存目录并存、两个 target 目录并存、两次都成功(锁住 §2.2 证据 1) | Linux | +| `177_cpp20_msvc.sh` | MSVC 上 `standard = "c++20"`:纯 C++20 工程构建成功;`import std;` 工程在 `cl` ≥ 19.38 的 runner 上**构建并运行成功**(定案取 20 的实测锁);`compile_commands.json` 里出现 `/std:c++20` | Windows(CI) | +| 单测(非 e2e) | 老 MSVC 分支不可能在 CI 上有真机 → 用构造的 `Toolchain{compiler=MSVC, version="19.37"}` 单测 `importStdMinLevel==23` 与门的错误文案 | 全平台 | + +CI 矩阵:`ci-linux-e2e.yml` / `ci-macos-e2e.yml` / `ci-windows-e2e.yml` 已按目录跑 +`run_all.sh`,新增脚本自动进矩阵。musl / mingw 交叉档位由 `cross-build-test.yml` +覆盖一条 c++20 冒烟即可(§2.2 已实机跑通,CI 只做回归)。 + +### 5.3 MSVC 的实测义务(定案取 20 的前提)—— ✅ 已兑现 + +**结论(PR#325,2026-07-31):Windows CI 上 `177_cpp20_msvc.sh` 通过。** +runner 的 `cl 19.51.36252`(VS 18 Enterprise / MSVC 14.51.36231)用 `/std:c++20` +真实编译并运行了 `import std;`,`compile_commands.json` 里是 `/std:c++20` 而非 +`/std:c++latest`。取 20 的前提成立;19.38 阈值仍只保护更老的 STL。 + + +取 20 是**乐观决策**,所以它必须由实测背书,而不是由文档背书 —— 恰恰因为 +微软自己的文档还写着 `/std:c++latest`(#3945 已 fixed 但 Learn 未更新)。 + +1. Windows CI 的 `177_cpp20_msvc.sh` 是硬断言(不是 observe-only):MSVC + `/std:c++20` + + `import std;` 必须真的构建并运行成功。它挂 = 定案的前提不成立,回退到 23。 +2. 该作业需打印 runner 的 `cl` 版本(`cl /Bv` 或 detect 的 `tc.version`),把 + "在哪个版本上验证过 20" 变成日志里的事实,而不是文档里的推算。 +3. 19.38 阈值若被实测证伪(更早或更晚),改的仍然只有 `msvc.cppm` 里那一行。 + +## 6. 工作量分解(单 PR,目标 2026.7.31.1) + +| # | 改动 | 文件 | 规模 | +|---|---|---|---| +| 1 | 白名单 + 文案 | `src/manifest/types.cppm` | ~20 行 | +| 2 | `importStdMinLevel` 字段 | `src/toolchain/model.cppm` | 2 行 | +| 3 | 四个 provider 赋值 + `toolset_at_least(19,38)` | `gcc.cppm` / `clang.cppm`(×2)/ `msvc.cppm` | ~15 行 | +| 4 | 二维能力门 + 文案 | `src/build/prepare.cppm:3735` | ~12 行 | +| 5 | build.mcpp `-std=` 走方言层(顺带缺陷) | `build_program.cppm` + 两处调用点 | ~15 行 | +| 6 | 单测 6 组 | `tests/unit/*` | ~80 行 | +| 7 | e2e 3 个 | `tests/e2e/175-177_*` | ~150 行 | +| 8 | 文档 EN/ZH | `docs/05-mcpp-toml.md`、`docs/zh/05-mcpp-toml.md` | ~30 行 | + +**不需要**:新模块、新缓存字段、新 CLI、schema 迁移、生态侧改动。 + +### 验证与复现(本设计所用探针) + +```bash +# 1) 取 mcpp 自己写下的真实 std 模块命令,替换档位后回放 +python3 - <<'PY' +import json, os, subprocess +CACHE = os.path.expanduser("~/.mcpp/build-cache/v1/std") +for d in os.listdir(CACHE): + m = json.load(open(f"{CACHE}/{d}/std-module.json")) + out = f"/tmp/replay-{d}-c++20"; os.makedirs(out + "/pcm.cache", exist_ok=True) + for c in m["std_build_commands"]: + c = c.replace(f"{CACHE}/{d}", out).replace(f'-std={m["cpp_standard"]}', "-std=c++20") + print(d, m["compiler"], m["compiler_version"], + subprocess.run(c, shell=True, capture_output=True, text=True).returncode) +PY +# 2) 跨档位 BMI 不可复用(负向证据) +# 在 c++20 建好的 gcm.cache 目录里用 -std=c++23 编译 import std 的 TU: +# → "std: error: language dialect differs 'C++20', expected 'C++23'" +``` + +## 7. 验收标准 + +1. `standard = "c++20"` 的工程,在 gcc 15/16(glibc、musl、mingw 交叉)与 clang+libc++ 上 + `mcpp build` / `mcpp run` / `mcpp test` 全绿,且源码可以 `import std;`; +2. `compile_commands.json` / `build.ninja` 里出现且仅出现 `-std=c++20`; +3. c++20 与 c++23 的 std BMI、target 目录、依赖构建缓存三者互不污染(e2e 176 锁死); +4. MSVC 上:纯 C++20 工程可构建;`cl` ≥ 19.38 时 `import std;` + `/std:c++20` **构建并运行成功** + (Windows CI 硬断言);更老的 `cl` 得到**可行动错误**而非 `std.ixx` 内部错误; +5. 默认值仍是 c++23:未声明 `standard` 的工程指纹与本 PR 前**逐字节一致**(无全生态重编); +6. `c++fly` / `c++latest` 行为零变化(现有 e2e 100/101 不动即绿); +7. 两份文档(EN/ZH)同步,ZH 补齐 `c++fly` 条目。 + +## 8. 非目标(显式出圈) + +- **不支持 C++17 及以下**:命名模块是 C++20 特性,mcpp 的构建模型在其下不成立; +- **不降低工具链下限**:仍需 GCC ≥ 15 / Clang ≥ 17(+libc++)/ MSVC(带 std 模块的 VS 2022); + "支持 C++20"指的是**语言档位**,不是"老编译器能用了"; +- **不做同图多档位**:跨档位 BMI 硬不兼容(§2.2),图全局单值是物理约束不是偷懒; +- **不改默认值、不改默认模板**(§4.6 的 `--standard` 是可选项); +- **不做依赖最低档位 floor 检查**(§4.3 的默认值不可区分陷阱)。 + +## 9. Review 定案(2026-07-31) + +**Q1 MSVC 的 `importStdMinLevel`:取 20。** 与 GNU 侧一致 —— 压低档位是用户的知情选择, +不为"可能存在的老 STL"给全体 MSVC 用户加拒绝。落地形态是**带版本阈值的一行数据** +(`cl` ≥ 19.38 → 20,更老 → 23,§2.3),这样既尊重"取 20",又让门保持可达可测, +并且老 VS 用户拿到的是可行动诊断而不是 `std.ixx` 内部错误。阈值待 Windows CI 实测确认。 + +**Q2 不加 `mcpp new --standard`,模板不动。** 默认 c++23;用户主动改成 c++20 即表示知道 +自己放弃了哪些特性。C++20 的定位是"给特殊约束的用户多一个选择",模板与文档示例继续推荐 C++23。 + +**Q3 不引入"standard 是否显式声明"位。** 今天没有消费者,加了就是无人验证的字段。 +留待真正要做依赖 floor 检查的那一步再加,届时必须:字段放 `Manifest::package` +(`std::optional`),**`toml.cppm:194` 与 `xpkg.cppm:1070` 两条解析路径同时填**, +否则又是"同一决策两处推导"。本条保留在此文档,作为那一步的前置约束。 + +**Q4 收 `c++2a` / `gnu++2a` 别名**(与既有 `c++2b` / `c++2c` 对称,零成本)。 diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d14615..2eb47a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,24 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.7.31.1] — 2026-07-31 + +### 新增 + +- **`standard = "c++20"` 成为一等档位(默认仍是 `c++23`)。** 白名单新增 `c++20` / `c++2a` / `gnu++20` / `gnu++2a`。C++20 是这个白名单的地板 —— 命名模块本身就是 C++20 特性,再往下 mcpp 的构建模型不存在。 + + **`import std;` 在 c++20 依然可用。** 它虽然是 C++23 的*库*特性,但三家实现都在 C++20 模式下提供 `std` 模块:libstdc++ 的 `bits/std.cc` 与 libc++ 的 `std.cppm` 都没有 `__cplusplus` 守卫,MSVC STL 在 [microsoft/STL#3977](https://github.com/microsoft/STL/pull/3977) 解禁(修复 [#3945](https://github.com/microsoft/STL/issues/3945),原话:C++20 的封锁"是纯策略选择,没有技术原因")。本次实机验证了 gcc 15.1.0 / 16.1.0 × glibc / musl / mingw-cross 与 clang 22.1.8 + libc++(含 `std.compat`):在 `-std=c++20` 下建 std 模块、编译消费者、链接、运行全部通过(musl 与 mingw 分别以 `-static` 在本机与 wine 跑通)。 + + **不改默认、不改模板**:未声明 `standard` 的工程指纹逐字节不变,`mcpp new` 与所有示例继续用 C++23。C++20 是给外部约束(公司内规、只到 C++20 的第三方 API)准备的逃生舱,不是新推荐值 —— 它意味着放弃 `std::print` / `std::expected` 等 C++23 库设施。 + + 缓存零改动:标准早已进入指纹、`import std` 的 BMI 身份与依赖构建缓存键,所以 c++20 与 c++23 各自拥有产物目录与 std BMI。这一点由编译器本身兜底 —— 跨档位复用 BMI 会被直接拒绝(`language dialect differs 'C++20', expected 'C++23'`)。 + +- **`import std` 的可用性判定从「有没有」升级为「从哪一档起」。** `Toolchain::importStdMinLevel` 由各 provider 自己填(GCC / libc++ 答 20;MSVC 按 cl banner 版本答 20 或 23)。STL#3977 之前的 MSVC 仍然封锁 C++20,那些机器上 `import std;` + `/std:c++20` 现在得到一句指名工具链与工程档位的可行动错误,而不是 `std.ixx` 内部的报错。 + +### 修复 + +- **`build.mcpp` 在 `standard = "c++fly"` / `"c++latest"` 下拼出非法的 `-std=` 旗标。** 它此前用 `"-std=" + canonical` 直接拼接,而这两个值的 canonical 不是合法的 `-std=` 拼写,宿主编译因未知方言失败。现在与主构建走同一个方言层(`cppfly::std_flag`),按真正执行编译的宿主工具链解析。 + ## [2026.7.30.3] — 2026-07-30 ### 变更 diff --git a/README.md b/README.md index b3351cf3..0968290e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,38 @@ irm https://d2learn.org/xlings-install.ps1.txt | iex +
+Optional — short commands (mp, mbuild, mrun, …) + +```bash +xlings install mcpp-short-cmd -y +``` + +Registers 30 shims, so `mcpp build` becomes `mbuild`. Naming rule: initial of +every word but the last, plus the last word in full — `mcpp self doctor` → +`msdoctor`. `mp` is bare `mcpp`. They alias the `mcpp` shim rather than a fixed +binary, so `xlings use mcpp ` switches them too. + +| Short | Expands to | Short | Expands to | +| --- | --- | --- | --- | +| `mp` | `mcpp` | `mexpkg` | `mcpp emit xpkg` | +| `mnew` | `mcpp new` | `mxparse` | `mcpp xpkg parse` | +| `mbuild` | `mcpp build` | `mtinstall` | `mcpp toolchain install` | +| `mrun` | `mcpp run` | `mtlist` | `mcpp toolchain list` | +| `mtest` | `mcpp test` | `mtdefault` | `mcpp toolchain default` | +| `mclean` | `mcpp clean` | `mcdir` | `mcpp cache dir` | +| `madd` | `mcpp add` | `mclist` | `mcpp cache list` | +| `mremove` | `mcpp remove` | `mcinfo` | `mcpp cache info` | +| `mupdate` | `mcpp update` | `mcgc` | `mcpp cache gc` | +| `msearch` | `mcpp search` | `milist` | `mcpp index list` | +| `mpublish` | `mcpp publish` | `miadd` | `mcpp index add` | +| `mpack` | `mcpp pack` | `miremove` | `mcpp index remove` | +| `msdoctor` | `mcpp self doctor` | `miupdate` | `mcpp index update` | +| `msenv` | `mcpp self env` | `msconfig` | `mcpp self config` | +| `msversion` | `mcpp self version` | `msexplain` | `mcpp self explain` | + +
+ **Other options**
diff --git a/README.zh-CN.md b/README.zh-CN.md index db1601b4..113ee89e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -67,6 +67,37 @@ irm https://d2learn.org/xlings-install.ps1.txt | iex
+
+可选 —— 短命令(mpmbuildmrun …) + +```bash +xlings install mcpp-short-cmd -y +``` + +装 30 个短命令,`mcpp build` 就是 `mbuild`。命名规则:除最后一个词外每词取首字母, +最后一个词写全 —— `mcpp self doctor` → `msdoctor`;`mp` 就是裸 `mcpp`。 +它们指向 `mcpp` 这个 shim 而非固定路径,所以 `xlings use mcpp ` 也会一起切换。 + +| 短命令 | 展开 | 短命令 | 展开 | +| --- | --- | --- | --- | +| `mp` | `mcpp` | `mexpkg` | `mcpp emit xpkg` | +| `mnew` | `mcpp new` | `mxparse` | `mcpp xpkg parse` | +| `mbuild` | `mcpp build` | `mtinstall` | `mcpp toolchain install` | +| `mrun` | `mcpp run` | `mtlist` | `mcpp toolchain list` | +| `mtest` | `mcpp test` | `mtdefault` | `mcpp toolchain default` | +| `mclean` | `mcpp clean` | `mcdir` | `mcpp cache dir` | +| `madd` | `mcpp add` | `mclist` | `mcpp cache list` | +| `mremove` | `mcpp remove` | `mcinfo` | `mcpp cache info` | +| `mupdate` | `mcpp update` | `mcgc` | `mcpp cache gc` | +| `msearch` | `mcpp search` | `milist` | `mcpp index list` | +| `mpublish` | `mcpp publish` | `miadd` | `mcpp index add` | +| `mpack` | `mcpp pack` | `miremove` | `mcpp index remove` | +| `msdoctor` | `mcpp self doctor` | `miupdate` | `mcpp index update` | +| `msenv` | `mcpp self env` | `msconfig` | `mcpp self config` | +| `msversion` | `mcpp self version` | `msexplain` | `mcpp self explain` | + +
+ **其他方式**
diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index a5ff1870..b8f6d121 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -41,7 +41,7 @@ lib-root convention: the primary module interface defaults to `src/mylib.cppm` ( [package] name = "myapp" # Package name (required) version = "0.1.0" # Semantic version (required) -standard = "c++23" # C++ standard (default c++23; can be set to c++26) +standard = "c++23" # C++ standard (default c++23; can be set to c++20 / c++26) description = "My awesome app" # Description (optional) license = "MIT" # License (optional) authors = ["Alice", "Bob"] # Author list (optional) @@ -51,12 +51,27 @@ repo = "https://github.com/user/myapp" # Repository URL (optional) `standard` is the first-class setting for the C++ language standard. Recommended values: - `c++23`: the default, suited to the current module-based default templates. +- `c++20`: the lowest level mcpp accepts — named modules are a C++20 feature, so nothing below it exists for this build model. Use it when an external constraint (an older in-house rule, a third-party API that stops at C++20) forces the level down. `import std;` still works there: it is a C++23 *library* feature, but GCC (≥ 15), Clang + libc++ (≥ 17) and the MSVC STL (from VS 2022 17.8) all provide the `std` module in C++20 mode as well. Note that C++23 library facilities (`std::print`, `std::expected`, …) are not available, including in the code generated by `mcpp new`. - `c++26`: use when you need C++26 language features. -- `c++2c`: a compatibility alias, normalized to `c++26` after parsing. -- `gnu++23` / `gnu++26`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key. +- `c++2a` / `c++2c`: compatibility aliases, normalized to `c++20` / `c++26` after parsing. +- `gnu++20` / `gnu++23` / `gnu++26`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key. - `c++latest`: resolves to the newest standard level the resolved toolchain supports. Good for local experimentation, but not recommended for release packages that require reproducibility. - `c++fly`: `c++latest` **plus every experimental standard feature the resolved toolchain can enable** (language + standard library). On GCC ≥ 16 this turns on C++26 reflection (`-freflection`) and contracts; on Clang/libc++ it adds `-fexperimental-library`; unsupported gates are skipped with a printed summary. Deliberately toolchain-dependent — the bleeding-edge playground mode, never for published packages. +Two properties worth knowing: + +- **The standard is module-graph-global.** The root package's `standard` applies to every + translation unit in the build, dependencies included — a dependency's own `standard` is not + used when it is being built as a dependency. This is not a simplification: BMIs are not + compatible across levels (GCC rejects them with `language dialect differs`), so a single + graph physically cannot hold two levels. +- **Levels never share caches.** The standard is part of the fingerprint, the `import std` BMI + identity and the dependency build-cache key, so switching between `c++20` and `c++23` gives + each level its own target directory and its own std BMI instead of a corrupt hit. + +If the sources `import std;` at a level the resolved toolchain does not provide the `std` +module for, mcpp fails before compiling and names both the toolchain and the project level. + ### 2.2 `[targets.]` — Build Targets ```toml @@ -1011,7 +1026,7 @@ mcpp build --target x86_64-linux-musl | Source files | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | Scanned recursively and automatically | | Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred | | Library root | `src/.cppm` | Override with `[lib].path` | -| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` / `c++latest` / `c++fly` (experimental playground) | +| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly` (experimental playground) | | C standard | `c11` | `.c` files go through the C compiler automatically | | Static stdlib | `true` | Portable binary | | Headers | `include/` (if present) | Added to `-I` automatically | diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index dc622085..367cfae5 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -41,7 +41,7 @@ lib-root 约定:主模块接口默认在 `src/mylib.cppm`(包名的最后一段) [package] name = "myapp" # 包名(必填) version = "0.1.0" # 语义化版本(必填) -standard = "c++23" # C++ 标准(默认 c++23; 可设 c++26) +standard = "c++23" # C++ 标准(默认 c++23; 可设 c++20 / c++26) description = "My awesome app" # 简介(可选) license = "MIT" # 许可证(可选) authors = ["Alice", "Bob"] # 作者列表(可选) @@ -51,10 +51,24 @@ repo = "https://github.com/user/myapp" # 仓库地址(可选) `standard` 是 C++ 语言标准的一等配置。推荐值: - `c++23`:默认值,适合当前模块化默认模板。 +- `c++20`:mcpp 接受的最低档位——命名模块本身是 C++20 特性,再往下这套构建模型就不存在了。当外部约束(公司内规、只到 C++20 的第三方 API)必须压低档位时使用。**`import std;` 在这一档依然可用**:它虽然是 C++23 的*库*特性,但 GCC(≥ 15)、Clang + libc++(≥ 17)与 MSVC STL(VS 2022 17.8 起)都在 C++20 模式下提供 `std` 模块。代价是 C++23 库设施(`std::print`、`std::expected` 等)不可用——包括 `mcpp new` 生成的模板代码。 - `c++26`:需要 C++26 语言特性时使用。 -- `c++2c`:兼容别名,解析后归一为 `c++26`。 -- `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。 +- `c++2a` / `c++2c`:兼容别名,解析后分别归一为 `c++20` / `c++26`。 +- `gnu++20` / `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。 - `c++latest`:跟随当前 mcpp 支持的最新标准,适合本地试验,不推荐要求可复现的发布包使用。 +- `c++fly`:`c++latest` **再加上该工具链能开启的全部实验性标准特性**(语言 + 标准库)。GCC ≥ 16 上会打开 C++26 反射(`-freflection`)与契约;Clang/libc++ 上追加 `-fexperimental-library`;不支持的门会跳过并打印 summary。刻意是工具链相关的——最前沿的试验场模式,永远不要用于发布包。 + +两条需要知道的性质: + +- **标准是模块图全局的。** 根包的 `standard` 作用于本次构建的每一个 TU,依赖也不例外—— + 依赖自己 manifest 里的 `standard` 在它作为依赖被构建时不生效。这不是简化:BMI 跨档位 + 不兼容(GCC 直接报 `language dialect differs`),同一张图物理上不可能存在两个档位。 +- **档位之间从不共用缓存。** 标准同时进入 fingerprint、`import std` 的 BMI 身份和依赖构建 + 缓存键,所以在 `c++20` 与 `c++23` 之间切换只会各自拿到独立的产物目录和独立的 std BMI, + 不会出现错误命中。 + +如果源码在某个档位上 `import std;` 而解析出的工具链在该档位不提供 `std` 模块, +mcpp 会在编译前失败,并同时报出工具链与工程档位。 ### 2.2 `[targets.]` — 构建目标 @@ -779,7 +793,7 @@ mcpp build --target x86_64-linux-musl | 源文件 | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | 自动递归扫描 | | 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 | | 库根 | `src/.cppm` | 可用 `[lib].path` 覆盖 | -| C++ 标准 | `c++23` | 用 `[package].standard` 配置; 支持 `c++26` / `c++2c` | +| C++ 标准 | `c++23` | 用 `[package].standard` 配置; 支持 `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly`(实验试验场) | | C 标准 | `c11` | `.c` 文件自动走 C 编译器 | | 静态 stdlib | `true` | 便携二进制 | | 头文件 | `include/`(如果存在) | 自动加到 `-I` | diff --git a/mcpp.toml b/mcpp.toml index b44e6c9c..6177f431 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.7.30.3" +version = "2026.7.31.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index eee56f78..699dbbaf 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -16,6 +16,7 @@ import std; import mcpp.manifest; import mcpp.platform; import mcpp.platform.process; +import mcpp.toolchain.cppfly; // std_flag (dialect- and c++fly-aware -std= spelling) import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex) import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target @@ -60,7 +61,7 @@ std::expected run_build_program( const std::filesystem::path& root, const std::filesystem::path& hostCompiler, const mcpp::toolchain::Toolchain& tc, - std::string_view cppStandard, + const mcpp::manifest::CppStandardConfig& cppStandard, const BuildProgramEnv& env); } // namespace mcpp::build @@ -580,7 +581,7 @@ std::expected run_build_program( const fs::path& root, const fs::path& hostCompiler, const mcpp::toolchain::Toolchain& tc, - std::string_view cppStandard, + const mcpp::manifest::CppStandardConfig& cppStandard, const BuildProgramEnv& env) { fs::path src = root / "build.mcpp"; @@ -644,7 +645,15 @@ std::expected run_build_program( ? "build.mcpp.exe" : "build.mcpp.bin"); // ── Compile build.mcpp with the host toolchain ────────────────────────── - std::string std_flag = "-std=" + std::string(cppStandard.empty() ? "c++23" : cppStandard); + // Spelled by the dialect layer, not concatenated here: the canonical of + // `standard = "c++fly"` / `"c++latest"` is not a valid -std= spelling, so + // the old `"-std=" + canonical` produced `-std=c++fly` and the host compile + // died on an unknown dialect. cppfly::std_flag resolves those against the + // toolchain that will actually run the compile — the host one, here. + std::string std_flag = mcpp::toolchain::cppfly::std_flag( + tc, cppStandard.canonical.empty() ? std::string_view("c++23") + : std::string_view(cppStandard.canonical), + cppStandard.level); auto base = host_base_flags(tc); // Only wire the bundled `mcpp` module when build.mcpp actually imports it — diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 5342eb7b..2749d08c 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -3476,7 +3476,7 @@ prepare_build(bool print_fingerprint, const auto ldN = bcDep.ldflags.size(); if (auto r = mcpp::build::run_build_program( pkg.manifest, pkg.root, host->first, host->second, - pkg.manifest.cppStandard.canonical, bpEnv); + pkg.manifest.cppStandard, bpEnv); !r) { return std::unexpected(std::format( "dependency '{}': {}", pkg.manifest.package.name, r.error())); @@ -3603,7 +3603,7 @@ prepare_build(bool print_fingerprint, rmodN = m->modules.sources.size(); if (auto bp = mcpp::build::run_build_program( *m, *root, host->first, host->second, - m->cppStandard.canonical, bpEnv); + m->cppStandard, bpEnv); !bp) { return std::unexpected(bp.error()); } @@ -3738,6 +3738,21 @@ prepare_build(bool print_fingerprint, "source imports std but toolchain '{}' provides no std module source", tc->label())); } + // `import std` availability is two-dimensional once C++20 is a legal level: + // having a std module source is not the same as being able to build it at + // the project's level. Every toolchain mcpp ships answers 20; only an MSVC + // STL older than microsoft/STL#3977 answers 23, and those users would + // otherwise get an error from inside std.ixx. + if (needsStdModule && tc->importStdMinLevel > 0 + && m->cppStandard.level < tc->importStdMinLevel) { + return std::unexpected(std::format( + "source imports std but toolchain '{}' provides the std module only " + "from {} up, while [package].standard resolves to '{}'; raise the " + "standard or drop `import std;`", + tc->label(), + mcpp::manifest::cpp_standard_level_name(tc->importStdMinLevel), + m->package.standard)); + } // Compute fingerprint (no lockfile in M1 → empty hash) mcpp::toolchain::FingerprintInputs fpi; diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index c7d56dfd..cbbe0053 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -537,6 +537,11 @@ struct ManifestError { std::expected normalize_cpp_standard(std::string_view raw); +// A CppStandardConfig::level back as the value users write ("c++23"). The +// inverse of the whitelist above, so it lives next to it instead of becoming a +// second table somewhere in the build layer. +std::string cpp_standard_level_name(int level); + // The module-graph-global dialect flag set: explicit [build] dialect_cxxflags // plus KNOWN dialect-class flags auto-promoted out of [build] cxxflags // (they also stay per-unit there — duplication is harmless and keeps the @@ -632,6 +637,26 @@ std::expected normalize_cpp_standard(std::string for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); CppStandardConfig out; + // C++20 is the floor: named modules are a C++20 feature, so mcpp's whole + // build model does not exist below it. `import std;` still works here on + // every toolchain mcpp ships (GCC >= 15, Clang >= 17 + libc++, MSVC STL + // since STL#3977) — it is a C++23 *library* feature that all three + // implementations also provide in C++20 mode. See + // .agents/docs/2026-07-31-cpp20-standard-support-design.md. + if (s == "c++20" || s == "c++2a") { + out.canonical = "c++20"; + out.flag = "-std=c++20"; + out.level = 20; + out.gnuDialect = false; + return out; + } + if (s == "gnu++20" || s == "gnu++2a") { + out.canonical = "gnu++20"; + out.flag = "-std=gnu++20"; + out.level = 20; + out.gnuDialect = true; + return out; + } if (s.empty() || s == "c++23" || s == "c++2b") { out.canonical = "c++23"; out.flag = "-std=c++23"; @@ -677,10 +702,22 @@ std::expected normalize_cpp_standard(std::string } return std::unexpected(std::format( - "unsupported C++ standard '{}'; expected c++23, c++26, c++2c, gnu++23, gnu++26, c++latest, or c++fly", + "unsupported C++ standard '{}'; expected c++20, c++23, c++26, c++2a, c++2c, " + "gnu++20, gnu++23, gnu++26, c++latest, or c++fly", raw)); } +std::string cpp_standard_level_name(int level) { + switch (level) { + case 20: return "c++20"; + case 23: return "c++23"; + case 26: return "c++26"; + case 999: return "c++latest"; + case 1000: return "c++fly"; + default: return std::format("c++{}", level); + } +} + bool has_lib_target(const Manifest& manifest) { for (auto& t : manifest.targets) { if (t.kind == Target::Library || t.kind == Target::SharedLibrary) { diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index cf27c437..27c91b09 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -145,6 +145,10 @@ void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) { if (auto p = find_libcxx_std_module_source(tc.binaryPath, envPrefix)) { tc.stdModuleSource = *p; tc.hasImportStd = true; + // libc++ documents the std module for C++20 and later, and its + // std.cppm carries no __cplusplus guard. Verified on clang 22.1.8 + + // libc++ (std and std.compat) at -std=c++20 (design §2.2). + tc.importStdMinLevel = 20; } #if defined(_WIN32) @@ -154,6 +158,10 @@ void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) { if (auto p = mcpp::toolchain::msvc::find_std_module_source()) { tc.stdModuleSource = *p; tc.hasImportStd = true; + // This is MSVC STL's std.ixx — the STL's own C++20 policy applies, + // not libc++'s. tc.version is clang's here, so it cannot answer the + // cl-banner question; stay strict (the STL is the binding side). + tc.importStdMinLevel = 23; } } #endif diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index df831d60..3c9da298 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 = "2026.7.30.3"; +inline constexpr std::string_view MCPP_VERSION = "2026.7.31.1"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index aedaf7a3..ceeabbab 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -132,6 +132,10 @@ void enrich_toolchain(Toolchain& tc) { if (auto p = find_std_module_source(tc.binaryPath, tc.version)) { tc.stdModuleSource = *p; tc.hasImportStd = true; + // libstdc++'s bits/std.cc carries no __cplusplus guard: any GCC that + // ships it builds the std module at C++20 too. Verified on gcc 15.1.0 + // and 16.1.0 across glibc / musl / mingw-cross targets (design §2.2). + tc.importStdMinLevel = 20; } } diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 7aec8019..992eb53f 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -45,6 +45,14 @@ struct Toolchain { // member added to this exported class: "failed to load pendings".) std::vector envOverrides; bool hasImportStd = false; + // Lowest -std= level this toolchain can build the std module at. 0 = the + // provider did not say, callers fall back to the plain hasImportStd + // question. Filled next to hasImportStd by each provider, because "which + // std module does this compiler ship" is provider-local knowledge — a + // central table would derive the same decision in a second place. + // GCC/libc++ answer 20; MSVC answers 20 from cl 19.38 (VS 2022 17.8, + // microsoft/STL#3977) and 23 below that. + int importStdMinLevel = 0; std::string label() const { return std::format("{} {} ({})", compiler_name(), version, targetTriple); diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 28c98cbc..aadf27e8 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -36,6 +36,17 @@ std::optional find_std_module_source(); // Find cl.exe (for future MSVC toolchain support). std::optional find_cl(); +// Lowest -std= level MSVC STL builds the `std` module at, for a toolchain +// whose `version` is a cl banner version ("19.44.35211"). +// +// microsoft/STL#3945 ("Supporting `import std;` in C++20") was fixed by +// STL#3977 (merged 2023-08-31) — the C++20 block was a policy choice with no +// technical reason behind it. That first ships in VS 2022 17.8, i.e. cl 19.38; +// older STLs still refuse and would fail inside std.ixx, so they answer 23 and +// get an actionable diagnostic from the caller instead. This is also what keeps +// the level gate reachable: every other provider answers 20. +int std_module_min_level(const Toolchain& tc); + // ─── System-toolchain detection (msvc@system) ──────────────────────────── // // mcpp treats MSVC as a *system* toolchain: it locates and identifies an @@ -503,6 +514,30 @@ std::string cl_stage_command(const Toolchain& tc, } // namespace +int std_module_min_level(const Toolchain& tc) { + // Two-segment compare: cppfly::compiler_major only reads the leading + // integer, which is 19 for every MSVC ever shipped. Keep that function's + // meaning intact (it has other consumers) and do the version knowledge + // here, where the rest of the cl banner handling already lives. + int major = 0, minor = 0; + auto it = tc.version.begin(); + const auto end = tc.version.end(); + auto read = [&](int& out) { + bool any = false; + while (it != end && *it >= '0' && *it <= '9') { + out = out * 10 + (*it - '0'); + ++it; + any = true; + } + return any; + }; + if (!read(major)) return 23; // unparseable banner: stay strict + if (it != end && *it == '.') ++it; + read(minor); + const bool atLeast_19_38 = major > 19 || (major == 19 && minor >= 38); + return atLeast_19_38 ? 20 : 23; +} + std::vector std_module_build_commands( const Toolchain& tc, const std::filesystem::path& cacheDir, std::string_view cppStandardFlag) { @@ -551,6 +586,9 @@ std::expected enrich_toolchain_from_cl(Toolchain& tc) { tc.stdModuleSource = *found; tc.hasImportStd = true; } + if (tc.hasImportStd) { + tc.importStdMinLevel = std_module_min_level(tc); + } if (auto compat = toolsDir / "modules" / "std.compat.ixx"; std::filesystem::exists(compat, ec)) { tc.stdCompatSource = compat; diff --git a/tests/e2e/175_cpp20_import_std.sh b/tests/e2e/175_cpp20_import_std.sh new file mode 100755 index 00000000..84b04d89 --- /dev/null +++ b/tests/e2e/175_cpp20_import_std.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# requires: gcc +# 175_cpp20_import_std.sh — standard = "c++20" is a first-class level, and +# `import std;` still works there. +# +# `import std;` is a C++23 *library* feature, but named modules are C++20 and +# every implementation mcpp ships provides the std module in C++20 mode too +# (libstdc++'s bits/std.cc and libc++'s std.cppm carry no __cplusplus guard; +# MSVC STL unblocked it in microsoft/STL#3977). Verified by on-machine probes +# across gcc 15/16 x glibc/musl/mingw and clang 22 + libc++ — see +# .agents/docs/2026-07-31-cpp20-standard-support-design.md §2.2. +# +# Asserts: build + run succeed, -std=c++20 reaches every surface (global +# cxxflags, compile_commands.json, the std BMI prebuild), and no c++23 leaks in. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml <<'EOF' +[package] +name = "cpp20demo" +version = "0.1.0" +standard = "c++20" +EOF + +# Deliberately C++20-only library usage: no std::print / std::expected here. +cat > src/util.cppm <<'EOF' +export module cpp20demo.util; + +import std; + +export namespace cpp20demo { +inline int count_evens(const std::vector& v) { + return static_cast(std::ranges::count_if(v, [](int x) { return x % 2 == 0; })); +} +} +EOF + +cat > src/main.cpp <<'EOF' +import std; +import cpp20demo.util; + +int main() { + std::vector v{1, 2, 3, 4, 5, 6}; + std::cout << "evens=" << cpp20demo::count_evens(v) << "\n"; + static_assert(__cplusplus == 202002L, "expected C++20 mode"); + return 0; +} +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: c++20 build failed" + exit 1 +} + +binary=$(find target -type f -path '*/bin/cpp20demo' | head -1) +out=$("$binary") +[[ "$out" == "evens=3" ]] || { + echo "FAIL: unexpected output: $out" + exit 1 +} + +# The level reaches the module-graph-global cxxflags (every TU, deps included). +build_ninja="$(find target -name build.ninja | head -1)" +grep -qE '^cxxflags = -std=c\+\+20' "$build_ninja" || { + grep '^cxxflags' "$build_ninja" || true + echo "FAIL: build.ninja cxxflags is not -std=c++20" + exit 1 +} +if grep -q -- '-std=c++23' "$build_ninja" compile_commands.json; then + echo "FAIL: -std=c++23 leaked into a command line under standard = c++20" + exit 1 +fi +grep -q -- '-std=c++20' compile_commands.json || { + echo "FAIL: compile_commands.json missing -std=c++20" + exit 1 +} + +# The std BMI prebuild must use the SAME level as the TUs importing it — a +# c++23 std.gcm fed to a c++20 TU is rejected outright ("language dialect +# differs"), so this is the cache-identity assertion, not cosmetics. +grep -rl '"std_flag": "-std=c++20"' "$MCPP_HOME/build-cache/v1/std" >/dev/null 2>&1 || { + find "$MCPP_HOME/build-cache/v1/std" -name std-module.json \ + -exec grep -H '"std_flag"' {} \; 2>/dev/null + echo "FAIL: no std-module.json records std_flag -std=c++20" + exit 1 +} + +# gnu++20 is accepted too and keeps the GNU dialect spelling. +mkdir -p "$TMP/gnu/src" +cd "$TMP/gnu" +cat > mcpp.toml <<'EOF' +[package] +name = "gnu20demo" +version = "0.1.0" +standard = "gnu++20" +EOF +cat > src/main.cpp <<'EOF' +import std; +int main() { std::cout << "gnu20 ok\n"; } +EOF +"$MCPP" build --no-cache > "$TMP/gnu-build.log" 2>&1 || { + cat "$TMP/gnu-build.log" + echo "FAIL: gnu++20 build failed" + exit 1 +} +gnu_ninja="$(find target -name build.ninja | head -1)" +grep -qE '^cxxflags = -std=gnu\+\+20' "$gnu_ninja" || { + grep '^cxxflags' "$gnu_ninja" || true + echo "FAIL: gnu++20 did not reach cxxflags" + exit 1 +} + +# Below the floor stays rejected, and the message names the floor. +mkdir -p "$TMP/old/src" +cd "$TMP/old" +cat > mcpp.toml <<'EOF' +[package] +name = "olddemo" +version = "0.1.0" +standard = "c++17" +EOF +cat > src/main.cpp <<'EOF' +int main() { return 0; } +EOF +if "$MCPP" build --no-cache > "$TMP/old-build.log" 2>&1; then + echo "FAIL: standard = c++17 was accepted" + exit 1 +fi +grep -q 'c++20' "$TMP/old-build.log" || { + cat "$TMP/old-build.log" + echo "FAIL: rejection message does not advertise the c++20 floor" + exit 1 +} + +echo "PASS: c++20 is a first-class level, import std works, gnu++20 ok, c++17 rejected" diff --git a/tests/e2e/176_cpp20_std_bmi_isolation.sh b/tests/e2e/176_cpp20_std_bmi_isolation.sh new file mode 100755 index 00000000..5acc0415 --- /dev/null +++ b/tests/e2e/176_cpp20_std_bmi_isolation.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# requires: gcc +# 176_cpp20_std_bmi_isolation.sh — switching [package].standard between c++20 +# and c++23 must give each level its own std BMI and its own target dir. +# +# This is not a hygiene preference: a std BMI built at one level is REJECTED by +# the compiler at another ("std: error: language dialect differs 'C++20', +# expected 'C++23'"). The standard is part of the fingerprint, of the std BMI +# identity and of the dependency build-cache key precisely so a level switch +# cannot produce a corrupt hit — this test locks that in both directions, +# including going back (the second c++23 build must still work). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > src/main.cpp <<'EOF' +import std; +int main() { std::cout << "level ok\n"; return 0; } +EOF + +write_manifest() { + cat > mcpp.toml < "$TMP/build-$1.log" 2>&1 || { + cat "$TMP/build-$1.log" + echo "FAIL: build at $1 failed" + exit 1 + } + binary=$(find target -type f -path '*/bin/leveldemo' -newer mcpp.toml | head -1) + [[ -n "$binary" ]] || binary=$(find target -type f -path '*/bin/leveldemo' | head -1) + out=$("$binary") + [[ "$out" == "level ok" ]] || { + echo "FAIL: bad output at $1: $out" + exit 1 + } +} + +build_at "c++23" +build_at "c++20" +# Back again: the c++23 artifacts must still be valid and reusable, not +# clobbered by the c++20 run. +build_at "c++23" + +# Two distinct std identities on disk, one per level. +std20=$(grep -rl '"std_flag": "-std=c++20"' "$MCPP_HOME/build-cache/v1/std" 2>/dev/null | wc -l) +std23=$(grep -rl '"std_flag": "-std=c++23"' "$MCPP_HOME/build-cache/v1/std" 2>/dev/null | wc -l) +[[ "$std20" -ge 1 && "$std23" -ge 1 ]] || { + find "$MCPP_HOME/build-cache/v1/std" -name std-module.json \ + -exec grep -H '"std_flag"' {} \; 2>/dev/null + echo "FAIL: expected a std BMI for each level (c++20=$std20 c++23=$std23)" + exit 1 +} + +# Two distinct fingerprint dirs (target///) — the level is +# a fingerprint axis, so the two builds cannot share a product directory. +dirs=$(find target -mindepth 2 -maxdepth 2 -type d | wc -l) +[[ "$dirs" -ge 2 ]] || { + find target -mindepth 2 -maxdepth 2 -type d + echo "FAIL: expected separate target dirs per standard, found $dirs" + exit 1 +} + +echo "PASS: c++20 and c++23 keep separate std BMIs and target dirs" diff --git a/tests/e2e/177_cpp20_msvc.sh b/tests/e2e/177_cpp20_msvc.sh new file mode 100755 index 00000000..76bd9379 --- /dev/null +++ b/tests/e2e/177_cpp20_msvc.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# requires: msvc +# 177_cpp20_msvc.sh — standard = "c++20" on native cl.exe. +# +# This is the hard assertion behind the review decision to let MSVC answer 20 +# (design §2.3/§5.3): microsoft/STL#3945 was fixed by STL#3977 (first shipping +# in VS 2022 17.8 = cl 19.38), yet Microsoft Learn still documents +# /std:c++latest as a requirement for `import std;`. Documentation cannot settle +# this — a real cl must. If this test fails on a 19.38+ runner, the premise is +# wrong and msvc::std_module_min_level goes back to 23. +# +# Asserts: /std:c++20 is what reaches the command lines (not /std:c++latest), +# `import std;` compiles and runs, and the runner's cl version is printed so the +# log records WHICH toolset the claim was verified on. +set -e + +CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" +ORIG_DEFAULT="" +if [[ -f "$CONF" ]]; then + ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ + | grep -E '^default[[:space:]]*=' | head -1 | cut -d'"' -f2 || true) +fi +TMP=$(mktemp -d) +restore() { + if [[ -n "$ORIG_DEFAULT" ]]; then + "$MCPP" toolchain default "$ORIG_DEFAULT" >/dev/null 2>&1 || true + fi + rm -rf "$TMP" +} +trap restore EXIT + +cd "$TMP" +"$MCPP" toolchain default msvc >/dev/null \ + || { echo "FAIL: toolchain default msvc"; exit 1; } + +# Record the toolset this run verifies (design §5.3: the version must be a fact +# in the log, not an estimate in a document). +"$MCPP" toolchain list 2>&1 | grep -i msvc || true + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" +cat > mcpp.toml <<'EOF' +[package] +name = "cpp20cl" +version = "0.1.0" +standard = "c++20" +EOF +cat > src/main.cpp <<'EOF' +import std; +int main() { + std::vector v{1, 2, 3}; + std::cout << "cl20 " << v.size() << "\n"; + return 0; +} +EOF + +build_out=$("$MCPP" build 2>&1) || { + echo "$build_out" + echo "FAIL: msvc c++20 + import std build failed." + echo " If this runner's cl is >= 19.38, the design premise (MSVC" + echo " answers 20) is wrong — revert msvc::std_module_min_level to 23." + exit 1 +} +run_out=$("$MCPP" run 2>&1) || { echo "FAIL: msvc c++20 run: $run_out"; exit 1; } +[[ "$run_out" == *"cl20 3"* ]] || { echo "FAIL: run output: $run_out"; exit 1; } + +# The MSVC spelling of level 20 is /std:c++20 — the `level <= 20` row in +# dialect.cppm that was unreachable until c++20 joined the allow-list. +grep -q '/std:c++20' compile_commands.json || { + echo "FAIL: compile_commands.json missing /std:c++20" + exit 1 +} +if grep -q '/std:c++latest' compile_commands.json; then + echo "FAIL: /std:c++latest reached the command line under standard = c++20" + exit 1 +fi + +echo "PASS: msvc builds and runs standard = c++20 with import std (/std:c++20)" diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index 529732a1..1b5a1dbd 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -101,6 +101,10 @@ TEST(CacheKey, LanguageAndDialectChangeTheKey) { { auto b = axes(); b.cppStandard = "c++26"; EXPECT_NE(ck::key_hex(b, pkg()), base); } { auto b = axes(); b.cppStandardFlag = "-std=c++2c"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + // A dependency built at c++20 must never be handed to a c++23 graph: BMIs + // are not compatible across levels (GCC: "language dialect differs"). + { auto b = axes(); b.cppStandard = "c++20"; + b.cppStandardFlag = "-std=c++20"; EXPECT_NE(ck::key_hex(b, pkg()), base); } { auto b = axes(); b.dialectFlags = {"-freflection"}; EXPECT_NE(ck::key_hex(b, pkg()), base); } { auto b = axes(); b.cStandard = "c17"; EXPECT_NE(ck::key_hex(b, pkg()), base); } diff --git a/tests/unit/test_cppfly.cpp b/tests/unit/test_cppfly.cpp index 806da080..45235068 100644 --- a/tests/unit/test_cppfly.cpp +++ b/tests/unit/test_cppfly.cpp @@ -101,6 +101,12 @@ TEST(CppFly, StdFlagResolvesLatestAndFly) { EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++fly", 1000), "-std=c++2c"); EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++fly", 1000), "/std:c++latest"); EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++latest", 999), "/std:c++latest"); + // A plain level is passed through untouched — c++20 must NOT be lifted to + // the family latest by the >= 999 branch. + EXPECT_EQ(cppfly::std_flag(gcc16, "c++20", 20), "-std=c++20"); + EXPECT_EQ(cppfly::std_flag(gcc16, "gnu++20", 20), "-std=gnu++20"); + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++20", 20), "-std=c++20"); + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++20", 20), "/std:c++20"); } TEST(CppFly, EffectiveDialectFlagsDedup) { diff --git a/tests/unit/test_fingerprint.cpp b/tests/unit/test_fingerprint.cpp index 29c75511..f6e10143 100644 --- a/tests/unit/test_fingerprint.cpp +++ b/tests/unit/test_fingerprint.cpp @@ -57,6 +57,9 @@ TEST(Fingerprint, AllTenFieldsAffectHash) { EXPECT_DIFFERENT(in.toolchain.targetTriple = "aarch64-linux-gnu"); EXPECT_DIFFERENT(in.toolchain.stdlibId = "libc++"); EXPECT_DIFFERENT(in.cppStandard = "c++26"); + // c++20 gets its own target dir for the same reason c++26 does — cross-level + // BMI reuse is rejected by the compiler itself. + EXPECT_DIFFERENT(in.cppStandard = "c++20"); EXPECT_DIFFERENT(in.compileFlags = "-O3"); // mcpp version is hardcoded inside compute_fingerprint, can't mutate from here. EXPECT_DIFFERENT(in.dependencyLockHash = ""); diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 69bdd65d..08e6587b 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -20,6 +20,44 @@ TEST(Manifest, CppFlyStandard) { EXPECT_NE(bad.error().find("c++fly"), std::string::npos); // listed in the allow-list message } +TEST(Manifest, Cpp20Standard) { + // C++20 is the floor of the allow-list: named modules are a C++20 feature, + // so nothing below it exists for this build model + // (.agents/docs/2026-07-31-cpp20-standard-support-design.md). + auto cfg = mcpp::manifest::normalize_cpp_standard("c++20"); + ASSERT_TRUE(cfg.has_value()); + EXPECT_EQ(cfg->canonical, "c++20"); + EXPECT_EQ(cfg->flag, "-std=c++20"); + EXPECT_EQ(cfg->level, 20); + EXPECT_FALSE(cfg->gnuDialect); + EXPECT_FALSE(cfg->experimental); + + // Aliases, symmetric with the existing c++2b / c++2c handling. + auto alias = mcpp::manifest::normalize_cpp_standard("C++2a"); // also case-folded + ASSERT_TRUE(alias.has_value()); + EXPECT_EQ(alias->canonical, "c++20"); + EXPECT_EQ(alias->level, 20); + + auto gnu = mcpp::manifest::normalize_cpp_standard("gnu++20"); + ASSERT_TRUE(gnu.has_value()); + EXPECT_EQ(gnu->canonical, "gnu++20"); + EXPECT_EQ(gnu->flag, "-std=gnu++20"); + EXPECT_EQ(gnu->level, 20); + EXPECT_TRUE(gnu->gnuDialect); + EXPECT_EQ(mcpp::manifest::normalize_cpp_standard("gnu++2a")->canonical, "gnu++20"); + + // Below the floor stays rejected, and the message advertises the floor. + auto bad = mcpp::manifest::normalize_cpp_standard("c++17"); + ASSERT_FALSE(bad.has_value()); + EXPECT_NE(bad.error().find("c++20"), std::string::npos) << bad.error(); +} + +TEST(Manifest, CppStandardLevelName) { + EXPECT_EQ(mcpp::manifest::cpp_standard_level_name(20), "c++20"); + EXPECT_EQ(mcpp::manifest::cpp_standard_level_name(23), "c++23"); + EXPECT_EQ(mcpp::manifest::cpp_standard_level_name(26), "c++26"); +} + TEST(Manifest, MinimalValid) { constexpr auto src = R"( [package] @@ -147,7 +185,13 @@ main = "src/main.cpp" EXPECT_NE(m.error().message.find("package.version"), std::string::npos); } -TEST(Manifest, RejectImportStdWithoutCpp23) { +TEST(Manifest, AcceptsCpp20WithImportStd) { + // Was RejectImportStdWithoutCpp23: it asserted this manifest is rejected, + // but the only thing rejecting it was the standard allow-list (there has + // never been an import_std-specific rule) — so the test's name described + // coverage it did not have. C++20 is legal now, and `import std;` is + // available there on every toolchain mcpp ships, so the same manifest must + // parse, with the legacy [language] section still mirroring to the new home. auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -160,14 +204,27 @@ sources = ["src/**/*.cppm"] [targets.x] kind = "bin" main = "src/main.cpp" +)"); + ASSERT_TRUE(m.has_value()) << m.error().message; + EXPECT_EQ(m->package.standard, "c++20"); // canonical, mirrored from [language] + EXPECT_EQ(m->language.standard, "c++20"); + EXPECT_EQ(m->cppStandard.level, 20); + EXPECT_EQ(m->cppStandard.flag, "-std=c++20"); +} + +TEST(Manifest, RejectsStandardBelowCpp20) { + auto m = mcpp::manifest::parse_string(R"( +[package] +name = "x" +version = "0.1.0" +standard = "c++17" +[targets.x] +kind = "bin" +main = "src/main.cpp" )"); ASSERT_FALSE(m.has_value()); - // M5.0: validator now bails on c++20 first ("MVP only supports c++23"), - // before reaching an import_std-specific check. Either signal is fine. - auto& msg = m.error().message; - EXPECT_TRUE(msg.find("import_std") != std::string::npos - || msg.find("c++23") != std::string::npos) - << "actual: " << msg; + EXPECT_NE(m.error().message.find("c++17"), std::string::npos) + << m.error().message; } TEST(Manifest, RejectModulesFalse) { diff --git a/tests/unit/test_toolchain_dialect.cpp b/tests/unit/test_toolchain_dialect.cpp index 23b49814..0beecb8e 100644 --- a/tests/unit/test_toolchain_dialect.cpp +++ b/tests/unit/test_toolchain_dialect.cpp @@ -110,7 +110,12 @@ TEST(StdFlagFor, PerDialectSpelling) { const auto& msvc = dialect_for(make_tc(CompilerId::MSVC)); EXPECT_EQ(std_flag_for(gnu, "c++26", 26), "-std=c++26"); EXPECT_EQ(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23"); + EXPECT_EQ(std_flag_for(gnu, "c++20", 20), "-std=c++20"); + EXPECT_EQ(std_flag_for(gnu, "gnu++20", 20), "-std=gnu++20"); + // The `level <= 20` row: unreachable until c++20 entered the allow-list, + // because 23 was the lowest level a manifest could produce. EXPECT_EQ(std_flag_for(msvc, "c++20", 20), "/std:c++20"); + EXPECT_EQ(std_flag_for(msvc, "gnu++20", 20), "/std:c++20"); // no MSVC gnu dialect EXPECT_EQ(std_flag_for(msvc, "c++23", 23), "/std:c++latest"); EXPECT_EQ(std_flag_for(msvc, "c++26", 26), "/std:c++latest"); } diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index 6fc873c9..5b9123f1 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -102,3 +102,30 @@ TEST(MsvcModel, BmiTraitsUseIfc) { EXPECT_TRUE(t.needsExplicitModuleOutput); EXPECT_FALSE(t.scanNeedsFModules); } + +// ─── std module minimum standard level ─────────────────────────────────── +// +// microsoft/STL#3945 ("Supporting `import std;` in C++20") was fixed by +// STL#3977, first shipping in VS 2022 17.8 = cl 19.38. Older STLs still block +// C++20 and would fail inside std.ixx; they answer 23 so the build layer can +// refuse with an actionable message instead. +// See .agents/docs/2026-07-31-cpp20-standard-support-design.md §2.3. + +TEST(MsvcStdModule, MinLevelFollowsStlUnblockVersion) { + auto tc_of = [](std::string ver) { + Toolchain tc; + tc.compiler = CompilerId::MSVC; + tc.version = std::move(ver); + return tc; + }; + // VS 2022 17.8 and newer: C++20 is allowed. + EXPECT_EQ(msvc::std_module_min_level(tc_of("19.38.33130")), 20); + EXPECT_EQ(msvc::std_module_min_level(tc_of("19.44.35211")), 20); + EXPECT_EQ(msvc::std_module_min_level(tc_of("20.0")), 20); + // Older toolsets keep the C++23 floor. + EXPECT_EQ(msvc::std_module_min_level(tc_of("19.37.32825")), 23); + EXPECT_EQ(msvc::std_module_min_level(tc_of("19.29.30153")), 23); + // Unparseable banner version: stay strict rather than guess. + EXPECT_EQ(msvc::std_module_min_level(tc_of("")), 23); + EXPECT_EQ(msvc::std_module_min_level(tc_of("unknown")), 23); +}