Skip to content

Commit af88aba

Browse files
committed
feat(standard): make c++20 a first-class level, keep c++23 the default
`import std;` is a C++23 library feature, but named modules are C++20 and all three implementations provide the std module in C++20 mode too — libstdc++'s bits/std.cc and libc++'s std.cppm carry no __cplusplus guard, and MSVC STL unblocked it in microsoft/STL#3977 ("purely a policy choice, no technical reason"). So the level and the ability to import std are separate questions. Verified by replaying mcpp's own recorded std-module build commands (sysroot, -B, -isystem and hermetic link model included) with -std=c++20 substituted, then compiling, linking and running a consumer TU: gcc 15.1.0 / 16.1.0 across glibc / musl / mingw-cross and clang 22.1.8 + libc++ (std and std.compat) all pass. musl and mingw run via -static natively and under wine. - allow-list: c++20 / c++2a / gnu++20 / gnu++2a (level 20). C++20 is the floor — named modules do not exist below it. Default stays c++23: unset manifests fingerprint byte-identically, and `mcpp new` templates keep using std::println. - `import std` availability becomes two-dimensional: Toolchain::importStdMinLevel, filled by each provider next to hasImportStd (GCC and libc++ answer 20; MSVC answers 20 from cl 19.38 = VS 2022 17.8 per STL#3977, 23 below that, so those users get an actionable error instead of one from inside std.ixx). - no cache work needed: the standard was already part of the fingerprint, the std BMI identity and the dependency build-cache key. The compiler is the backstop — cross-level BMI reuse is rejected with "language dialect differs". Also fixes build.mcpp under standard = "c++fly" / "c++latest": it concatenated "-std=" + canonical, and those canonicals are not valid -std= spellings, so the host compile died on an unknown dialect. It now goes through the same dialect layer as the main build, resolved against the host toolchain that runs it. The previous Manifest.RejectImportStdWithoutCpp23 test 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. It now asserts what it always meant to. Design: .agents/docs/2026-07-31-cpp20-standard-support-design.md Plan: .agents/docs/2026-07-31-cpp20-implementation-plan.md
1 parent 7af26ba commit af88aba

23 files changed

Lines changed: 1058 additions & 23 deletions
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# C++20 档位支持 — 实施计划(单 PR,目标 2026.7.31.1)
2+
3+
> 设计:`.agents/docs/2026-07-31-cpp20-standard-support-design.md`(review 已定案)
4+
> 基线:`7af26ba` · 分支:`feat/cpp20-standard`
5+
> 定案回顾:MSVC 取 20(带 `cl ≥ 19.38` 版本阈值)· 不动 scaffold/模板 · 不加"显式声明"位
6+
7+
## 步骤(按依赖顺序,每步都能独立验证)
8+
9+
### S1 解析层:白名单 + 别名 + 文案
10+
- `src/manifest/types.cppm::normalize_cpp_standard`:加 `c++20`/`c++2a`(level 20,gnu=false)、
11+
`gnu++20`/`gnu++2a`(level 20,gnu=true);错误文案补全新值。
12+
- 验证:`tests/unit/test_manifest.cpp` 新增用例;`mcpp build``standard="c++19"` 仍报错且文案含 c++20。
13+
14+
### S2 能力层:`hasImportStd` → 加 `importStdMinLevel`
15+
- `src/toolchain/model.cppm`:`int importStdMinLevel = 0;`(0 = 未知,走旧布尔语义)。
16+
- provider 赋值(与 `hasImportStd` 同点,知识留在各自 provider):
17+
- `gcc.cppm::enrich_toolchain` → 20
18+
- `clang.cppm::enrich_toolchain` libc++ 分支 → 20;MSVC STL 回退分支 → `msvc_std_min_level(tc)`
19+
- `msvc.cppm``msvc_std_min_level(tc)`,由本地 `toolset_at_least(tc, 19, 38)` 决定 20 / 23
20+
- 验证:单测构造 `Toolchain{MSVC, "19.37"}` → 23,`"19.44"` → 20。
21+
22+
### S3 门:prepare 里从一维变二维
23+
- `src/build/prepare.cppm:3735` 之后追加档位判断 + 可行动文案(设计 §4.2)。
24+
- 本地小函数 `std_level_name(int)`(20/23/26 → "c++20"…),不造第二张表。
25+
- 验证:单测覆盖文案;e2e 177(Windows)覆盖真机。
26+
27+
### S4 顺带缺陷:build.mcpp 的 `-std=` 走方言层
28+
- `src/build/build_program.cppm`:`run_build_program``cppStandard` 形参改为
29+
`const manifest::CppStandardConfig&`,内部用 `cppfly::std_flag(hostTc, canonical, level)`
30+
- 调用点:`prepare.cppm:3479`(依赖)、`prepare.cppm:3606`(根)传 `.cppStandard` 整体。
31+
- 验证:单测/或 e2e 92(build.mcpp import)保持绿;新增 c++fly 工程 + build.mcpp 的回归(单测层足够)。
32+
33+
### S5 单元测试(6 组,设计 §5.1)
34+
`test_manifest.cpp` / `test_toolchain_dialect.cpp` / `test_cppfly.cpp` /
35+
`test_cache_key.cpp` / `test_fingerprint.cpp` / 新增门测试。
36+
37+
### S6 e2e(设计 §5.2)
38+
- `175_cpp20_import_std.sh` — c++20 + import std,构建/运行/`-std=c++20` 断言(POSIX)
39+
- `176_cpp20_std_bmi_isolation.sh` — c++23 与 c++20 交替构建,两套缓存并存
40+
- `177_cpp20_msvc.sh` — Windows:`/std:c++20` + import std 硬断言 + 打印 `cl` 版本
41+
42+
### S7 版本 + 文档
43+
- `src/toolchain/fingerprint.cppm::MCPP_VERSION``2026.7.31.1`
44+
- bootstrap pin(`.xlings.json` / `MCPP_PIN`)**不动**(自举起点,不随发布走)
45+
- `scripts/check_version_pins.sh` 通过
46+
- 文档已在设计阶段更新(EN/ZH),与本 PR 同批合入
47+
48+
### S8 本地验证
49+
`mcpp build -p mcpp` → 单测全绿 → `tests/e2e/run_all.sh`(至少 175/176 + 回归面)
50+
51+
### S9 PR → CI → 合入
52+
单 PR,bypass squash 合入 main。
53+
54+
### S10 发布闭环
55+
release(四平台)→ gitcode 资源本地 `gtc` 补传 + GET 核验 → xim-pkgindex PR →
56+
`xlings install mcpp` 真装验证 → bootstrap pin bump。
57+
58+
## 不做(设计已定案)
59+
scaffold/模板改动、`mcpp new --standard`、依赖 floor 检查、"显式声明"位、C++17。

.agents/docs/2026-07-31-cpp20-standard-support-design.md

Lines changed: 408 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.7.31.1] — 2026-07-31
7+
8+
### 新增
9+
10+
- **`standard = "c++20"` 成为一等档位(默认仍是 `c++23`)。** 白名单新增 `c++20` / `c++2a` / `gnu++20` / `gnu++2a`。C++20 是这个白名单的地板 —— 命名模块本身就是 C++20 特性,再往下 mcpp 的构建模型不存在。
11+
12+
**`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 跑通)。
13+
14+
**不改默认、不改模板**:未声明 `standard` 的工程指纹逐字节不变,`mcpp new` 与所有示例继续用 C++23。C++20 是给外部约束(公司内规、只到 C++20 的第三方 API)准备的逃生舱,不是新推荐值 —— 它意味着放弃 `std::print` / `std::expected` 等 C++23 库设施。
15+
16+
缓存零改动:标准早已进入指纹、`import std` 的 BMI 身份与依赖构建缓存键,所以 c++20 与 c++23 各自拥有产物目录与 std BMI。这一点由编译器本身兜底 —— 跨档位复用 BMI 会被直接拒绝(`language dialect differs 'C++20', expected 'C++23'`)。
17+
18+
- **`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` 内部的报错。
19+
20+
### 修复
21+
22+
- **`build.mcpp``standard = "c++fly"` / `"c++latest"` 下拼出非法的 `-std=` 旗标。** 它此前用 `"-std=" + canonical` 直接拼接,而这两个值的 canonical 不是合法的 `-std=` 拼写,宿主编译因未知方言失败。现在与主构建走同一个方言层(`cppfly::std_flag`),按真正执行编译的宿主工具链解析。
23+
624
## [2026.7.30.3] — 2026-07-30
725

826
### 变更

docs/05-mcpp-toml.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ lib-root convention: the primary module interface defaults to `src/mylib.cppm` (
4141
[package]
4242
name = "myapp" # Package name (required)
4343
version = "0.1.0" # Semantic version (required)
44-
standard = "c++23" # C++ standard (default c++23; can be set to c++26)
44+
standard = "c++23" # C++ standard (default c++23; can be set to c++20 / c++26)
4545
description = "My awesome app" # Description (optional)
4646
license = "MIT" # License (optional)
4747
authors = ["Alice", "Bob"] # Author list (optional)
@@ -51,12 +51,27 @@ repo = "https://github.com/user/myapp" # Repository URL (optional)
5151
`standard` is the first-class setting for the C++ language standard. Recommended values:
5252

5353
- `c++23`: the default, suited to the current module-based default templates.
54+
- `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 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`.
5455
- `c++26`: use when you need C++26 language features.
55-
- `c++2c`: a compatibility alias, normalized to `c++26` after parsing.
56-
- `gnu++23` / `gnu++26`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key.
56+
- `c++2a` / `c++2c`: compatibility aliases, normalized to `c++20` / `c++26` after parsing.
57+
- `gnu++20` / `gnu++23` / `gnu++26`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key.
5758
- `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.
5859
- `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.
5960

61+
Two properties worth knowing:
62+
63+
- **The standard is module-graph-global.** The root package's `standard` applies to every
64+
translation unit in the build, dependencies included — a dependency's own `standard` is not
65+
used when it is being built as a dependency. This is not a simplification: BMIs are not
66+
compatible across levels (GCC rejects them with `language dialect differs`), so a single
67+
graph physically cannot hold two levels.
68+
- **Levels never share caches.** The standard is part of the fingerprint, the `import std` BMI
69+
identity and the dependency build-cache key, so switching between `c++20` and `c++23` gives
70+
each level its own target directory and its own std BMI instead of a corrupt hit.
71+
72+
If the sources `import std;` at a level the resolved toolchain does not provide the `std`
73+
module for, mcpp fails before compiling and names both the toolchain and the project level.
74+
6075
### 2.2 `[targets.<name>]` — Build Targets
6176

6277
```toml
@@ -1011,7 +1026,7 @@ mcpp build --target x86_64-linux-musl
10111026
| Source files | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | Scanned recursively and automatically |
10121027
| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred |
10131028
| Library root | `src/<pkg-tail>.cppm` | Override with `[lib].path` |
1014-
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` / `c++latest` / `c++fly` (experimental playground) |
1029+
| 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) |
10151030
| C standard | `c11` | `.c` files go through the C compiler automatically |
10161031
| Static stdlib | `true` | Portable binary |
10171032
| Headers | `include/` (if present) | Added to `-I` automatically |

docs/zh/05-mcpp-toml.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ lib-root 约定:主模块接口默认在 `src/mylib.cppm`(包名的最后一段)
4141
[package]
4242
name = "myapp" # 包名(必填)
4343
version = "0.1.0" # 语义化版本(必填)
44-
standard = "c++23" # C++ 标准(默认 c++23; 可设 c++26)
44+
standard = "c++23" # C++ 标准(默认 c++23; 可设 c++20 / c++26)
4545
description = "My awesome app" # 简介(可选)
4646
license = "MIT" # 许可证(可选)
4747
authors = ["Alice", "Bob"] # 作者列表(可选)
@@ -51,10 +51,24 @@ repo = "https://github.com/user/myapp" # 仓库地址(可选)
5151
`standard` 是 C++ 语言标准的一等配置。推荐值:
5252

5353
- `c++23`:默认值,适合当前模块化默认模板。
54+
- `c++20`:mcpp 接受的最低档位——命名模块本身是 C++20 特性,再往下这套构建模型就不存在了。当外部约束(公司内规、只到 C++20 的第三方 API)必须压低档位时使用。**`import std;` 在这一档依然可用**:它虽然是 C++23 的**特性,但 GCC(≥ 15)、Clang + libc++(≥ 17)与 MSVC STL 都在 C++20 模式下提供 `std` 模块。代价是 C++23 库设施(`std::print``std::expected` 等)不可用——包括 `mcpp new` 生成的模板代码。
5455
- `c++26`:需要 C++26 语言特性时使用。
55-
- `c++2c`:兼容别名,解析后归一为 `c++26`
56-
- `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。
56+
- `c++2a` / `c++2c`:兼容别名,解析后分别归一为 `c++20` / `c++26`
57+
- `gnu++20` / `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。
5758
- `c++latest`:跟随当前 mcpp 支持的最新标准,适合本地试验,不推荐要求可复现的发布包使用。
59+
- `c++fly``c++latest` **再加上该工具链能开启的全部实验性标准特性**(语言 + 标准库)。GCC ≥ 16 上会打开 C++26 反射(`-freflection`)与契约;Clang/libc++ 上追加 `-fexperimental-library`;不支持的门会跳过并打印 summary。刻意是工具链相关的——最前沿的试验场模式,永远不要用于发布包。
60+
61+
两条需要知道的性质:
62+
63+
- **标准是模块图全局的。** 根包的 `standard` 作用于本次构建的每一个 TU,依赖也不例外——
64+
依赖自己 manifest 里的 `standard` 在它作为依赖被构建时不生效。这不是简化:BMI 跨档位
65+
不兼容(GCC 直接报 `language dialect differs`),同一张图物理上不可能存在两个档位。
66+
- **档位之间从不共用缓存。** 标准同时进入 fingerprint、`import std` 的 BMI 身份和依赖构建
67+
缓存键,所以在 `c++20``c++23` 之间切换只会各自拿到独立的产物目录和独立的 std BMI,
68+
不会出现错误命中。
69+
70+
如果源码在某个档位上 `import std;` 而解析出的工具链在该档位不提供 `std` 模块,
71+
mcpp 会在编译前失败,并同时报出工具链与工程档位。
5872

5973
### 2.2 `[targets.<name>]` — 构建目标
6074

@@ -779,7 +793,7 @@ mcpp build --target x86_64-linux-musl
779793
| 源文件 | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | 自动递归扫描 |
780794
| 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 |
781795
| 库根 | `src/<pkg-tail>.cppm` | 可用 `[lib].path` 覆盖 |
782-
| C++ 标准 | `c++23` |`[package].standard` 配置; 支持 `c++26` / `c++2c` |
796+
| C++ 标准 | `c++23` |`[package].standard` 配置; 支持 `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly`(实验试验场) |
783797
| C 标准 | `c11` | `.c` 文件自动走 C 编译器 |
784798
| 静态 stdlib | `true` | 便携二进制 |
785799
| 头文件 | `include/`(如果存在) | 自动加到 `-I` |

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.7.30.3"
3+
version = "2026.7.31.1"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/build_program.cppm

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import std;
1616
import mcpp.manifest;
1717
import mcpp.platform;
1818
import mcpp.platform.process;
19+
import mcpp.toolchain.cppfly; // std_flag (dialect- and c++fly-aware -std= spelling)
1920
import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
2021
import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
2122
import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target
@@ -60,7 +61,7 @@ std::expected<void, std::string> run_build_program(
6061
const std::filesystem::path& root,
6162
const std::filesystem::path& hostCompiler,
6263
const mcpp::toolchain::Toolchain& tc,
63-
std::string_view cppStandard,
64+
const mcpp::manifest::CppStandardConfig& cppStandard,
6465
const BuildProgramEnv& env);
6566

6667
} // namespace mcpp::build
@@ -580,7 +581,7 @@ std::expected<void, std::string> run_build_program(
580581
const fs::path& root,
581582
const fs::path& hostCompiler,
582583
const mcpp::toolchain::Toolchain& tc,
583-
std::string_view cppStandard,
584+
const mcpp::manifest::CppStandardConfig& cppStandard,
584585
const BuildProgramEnv& env) {
585586

586587
fs::path src = root / "build.mcpp";
@@ -644,7 +645,15 @@ std::expected<void, std::string> run_build_program(
644645
? "build.mcpp.exe" : "build.mcpp.bin");
645646

646647
// ── Compile build.mcpp with the host toolchain ──────────────────────────
647-
std::string std_flag = "-std=" + std::string(cppStandard.empty() ? "c++23" : cppStandard);
648+
// Spelled by the dialect layer, not concatenated here: the canonical of
649+
// `standard = "c++fly"` / `"c++latest"` is not a valid -std= spelling, so
650+
// the old `"-std=" + canonical` produced `-std=c++fly` and the host compile
651+
// died on an unknown dialect. cppfly::std_flag resolves those against the
652+
// toolchain that will actually run the compile — the host one, here.
653+
std::string std_flag = mcpp::toolchain::cppfly::std_flag(
654+
tc, cppStandard.canonical.empty() ? std::string_view("c++23")
655+
: std::string_view(cppStandard.canonical),
656+
cppStandard.level);
648657
auto base = host_base_flags(tc);
649658

650659
// Only wire the bundled `mcpp` module when build.mcpp actually imports it —

src/build/prepare.cppm

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3476,7 +3476,7 @@ prepare_build(bool print_fingerprint,
34763476
const auto ldN = bcDep.ldflags.size();
34773477
if (auto r = mcpp::build::run_build_program(
34783478
pkg.manifest, pkg.root, host->first, host->second,
3479-
pkg.manifest.cppStandard.canonical, bpEnv);
3479+
pkg.manifest.cppStandard, bpEnv);
34803480
!r) {
34813481
return std::unexpected(std::format(
34823482
"dependency '{}': {}", pkg.manifest.package.name, r.error()));
@@ -3603,7 +3603,7 @@ prepare_build(bool print_fingerprint,
36033603
rmodN = m->modules.sources.size();
36043604
if (auto bp = mcpp::build::run_build_program(
36053605
*m, *root, host->first, host->second,
3606-
m->cppStandard.canonical, bpEnv);
3606+
m->cppStandard, bpEnv);
36073607
!bp) {
36083608
return std::unexpected(bp.error());
36093609
}
@@ -3738,6 +3738,21 @@ prepare_build(bool print_fingerprint,
37383738
"source imports std but toolchain '{}' provides no std module source",
37393739
tc->label()));
37403740
}
3741+
// `import std` availability is two-dimensional once C++20 is a legal level:
3742+
// having a std module source is not the same as being able to build it at
3743+
// the project's level. Every toolchain mcpp ships answers 20; only an MSVC
3744+
// STL older than microsoft/STL#3977 answers 23, and those users would
3745+
// otherwise get an error from inside std.ixx.
3746+
if (needsStdModule && tc->importStdMinLevel > 0
3747+
&& m->cppStandard.level < tc->importStdMinLevel) {
3748+
return std::unexpected(std::format(
3749+
"source imports std but toolchain '{}' provides the std module only "
3750+
"from {} up, while [package].standard resolves to '{}'; raise the "
3751+
"standard or drop `import std;`",
3752+
tc->label(),
3753+
mcpp::manifest::cpp_standard_level_name(tc->importStdMinLevel),
3754+
m->package.standard));
3755+
}
37413756

37423757
// Compute fingerprint (no lockfile in M1 → empty hash)
37433758
mcpp::toolchain::FingerprintInputs fpi;

0 commit comments

Comments
 (0)