diff --git a/.agents/docs/2026-06-01-ci-toolchain-cache-optimization.md b/.agents/docs/2026-06-01-ci-toolchain-cache-optimization.md new file mode 100644 index 0000000..af98b14 --- /dev/null +++ b/.agents/docs/2026-06-01-ci-toolchain-cache-optimization.md @@ -0,0 +1,225 @@ +# CI 工具链缓存优化分析 + +> 日期: 2026-06-01 +> 状态: draft +> 范围: Linux CI/e2e 中工具链 registry 与 xpkg payload 的重复下载、临时 `MCPP_HOME` 隔离策略,以及后续可扩展的工具链缓存架构。 + +## 0. 结论 + +当前 CI 慢点不是单纯的 actions/cache 命中率问题。Linux workflow 已经缓存了 `~/.mcpp` 和 `~/.xlings`,但部分 e2e 脚本为了隔离配置会创建新的 `MCPP_HOME`。如果这些临时 home 没有继承已安装的 xpkg payload,测试内部的 `mcpp build` / first-run auto-install 就会把同一份大工具链下载到临时目录,测试结束后又被删除。 + +最典型的触发链: + +```text +ci-linux + -> persistent ~/.mcpp 已有/将安装 gcc、musl-gcc + -> tests/e2e/run_all.sh + -> 29_toolchain_partial_versions.sh 创建 $TMP/h2 + -> first-run auto-install gcc@15.1.0-musl 到 $TMP/h2 + -> trap 删除 $TMP/h2 + -> 31_transitive_deps.sh 再创建 $TMP/mcpp-home + -> 再次安装/下载 gcc@15.1.0-musl + -> 后续 "Toolchain: musl-gcc" step 在 persistent ~/.mcpp 里还可能再安装一次 +``` + +合理修法不是取消测试隔离,而是把隔离拆成两层: + +1. 配置状态隔离: 每个测试仍可拥有自己的 `config.toml`、lock/cache、project state。 +2. 工具链 payload 复用: 大体积、只读的 `registry/data/xpkgs` 从 persistent sandbox 或 xlings cache 继承。 + +## 1. 现状证据 + +### 1.1 workflow 已经缓存 persistent sandbox + +- `.github/workflows/ci-linux.yml`: `Cache mcpp sandbox` 缓存 `~/.mcpp`,用于保留 musl-gcc、binutils、glibc、linux-headers、patchelf、ninja 等 payload。 +- 同一个 workflow 也缓存 `~/.xlings`,用于保留 xlings 自己安装的包。 +- E2E step 会设置 `MCPP_HOME=/home/runner/.mcpp`,并设置 `MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL`。 + +所以 CI 的正确方向应该是让临时 home 继承这两处 cache,而不是在临时 home 冷启动。 + +### 1.2 e2e 的临时 home 有两种语义 + +有些脚本创建临时 `MCPP_HOME` 是为了验证“空配置”或“fresh sandbox”行为,例如: + +- `14_toolchain_fallback.sh`: 验证无 toolchain 且 `MCPP_NO_AUTO_INSTALL=1` 时的硬错误。 +- `26_toolchain_management.sh`: 显式验证 `toolchain install/list/default/remove`。 +- `29_toolchain_partial_versions.sh`: 第一段验证 partial version/default 解析,第二段验证 first-run auto-install。 + +这类测试不能直接复制全局 `config.toml`,否则会掩盖被测行为。但它们通常可以继承 `registry/data/xpkgs`,因为 payload 是否已经存在不应改变“配置为空时会设置默认 toolchain”的语义。 + +另一些脚本创建临时 `MCPP_HOME` 只是为了隔离 BMI、git/cache 或测试产物,例如: + +- `31_transitive_deps.sh`: 目标是验证传递依赖 include_dirs,不是验证工具链安装。 +- LLVM/import std/BMI cache 类测试: 目标是编译行为或 cache 行为,不是下载行为。 + +这类测试应该默认继承 payload 和必要配置。 + +### 1.3 `_inherit_toolchain.sh` 的模型不完整 + +旧 helper 只优先继承 `$HOME/.mcpp/registry/data/xpkgs`,但 `run_all.sh` 的能力检测同时承认: + +- `$HOME/.xlings/data/xpkgs/xim-x-musl-gcc/...` +- `$MCPP_HOME/registry/data/xpkgs/xim-x-musl-gcc/...` + +这会导致能力检测认为 musl 可用,但临时 `MCPP_HOME` helper 没有把 `.xlings` payload 继承进去,脚本实际构建时仍可能走下载路径。 + +### 1.4 慢点掩盖了 31 的真实功能失败 + +Linux PR CI 的失败链显示 `31_transitive_deps.sh` 先在临时 home 下载了 `xim:musl-gcc@15.1.0` 的 808 MB payload,然后才失败在: + +```text +child/ch/src/ch.cppm:2:10: fatal error: gc/gc.h: No such file or directory +``` + +这说明 CI 慢不是唯一问题。即使下载复用做好,31 仍会失败,因为依赖 include_dirs 的传播模型也不完整: 依赖解析只把 dep 的 include_dirs 追加到 root manifest,而没有追加到实际发起依赖的 consumer package。`top -> ch -> gc` 里真正需要 `` 的是 `ch`,不是 root `top`。 + +## 2. 本 PR 的优化策略 + +### 2.1 payload 继承从“整目录 symlink”改为“逐 package merge” + +`tests/e2e/_inherit_toolchain.sh` 现在会从以下来源继承 xpkg payload: + +- `$HOME/.mcpp/registry/data/xpkgs` +- `$HOME/.xlings/data/xpkgs` +- Windows/Git Bash 下的 `$USERPROFILE/.xlings/data/xpkgs` + +目标目录是临时 home 的: + +```text +$MCPP_HOME/registry/data/xpkgs +``` + +逐 package merge 比整目录 symlink 更稳,因为 CI 可能同时存在两套 payload 来源: mcpp 自己安装的 toolchain 在 `~/.mcpp`,xlings bootstrap 安装的包在 `~/.xlings`。逐项链接可以把两边合并进临时 home,而不会因为先 symlink 了一个根目录导致另一个来源无法补充。 + +### 2.2 first-run 测试只继承 payload,不继承配置 + +`29_toolchain_partial_versions.sh` 的 second home 继续保持: + +```text +无 config/default state +无 inherited subos +``` + +但会继承 xpkg payload。这样仍能验证 first-run auto-install 的用户语义: + +- 生成项目没有 `[toolchain]`。 +- 第一次 `mcpp build` 会出现 First run。 +- 默认选择 `gcc@15.1.0-musl`。 +- default 会被持久化,第二次 build 不再打印 First run。 + +区别是 install 阶段可以发现 payload 已存在,不再把大归档下载到临时目录。 + +### 2.3 传递依赖测试不再承担工具链冷启动 + +`31_transitive_deps.sh` 的目标是验证: + +- top 只声明 child。 +- child 自己声明 grandchild。 +- grandchild 的 `[build].include_dirs` 能传到 child 编译命令。 + +这个测试不应该下载工具链。现在它会继承 payload-only,并在没有可复用 musl xpkg payload 时直接 skip。musl 的安装/构建路径由 workflow 的专门 toolchain step 覆盖。 + +### 2.4 Linux CI 预热一次 musl-gcc + +Linux e2e step 在运行 `tests/e2e/run_all.sh` 前执行: + +```bash +"$MCPP" toolchain install gcc 15.1.0-musl +``` + +这有两个作用: + +1. 让 29/31 这类临时 home 测试通过 helper 复用 persistent payload。 +2. 让后续 `"Toolchain: musl-gcc — build mcpp (--target)"` 复用同一份安装。 + +冷 cache 时最多下载一次 musl-gcc;热 cache 时这个命令应快速命中本地 payload。 + +### 2.5 include_dirs 按 dependency edge 传播 + +`src/cli.cppm` 的依赖解析现在把 include_dirs 当作 edge 属性处理: + +```text +consumer package -> dependency package +``` + +每个 unique dependency 仍只解析/扫描一次,但每个 consumer 都会获得该 dependency 的 public include dirs。这样: + +- root 直接依赖 header-providing package 时,root compile units 能看到 headers。 +- child 依赖 grandchild 时,child compile units 能看到 grandchild headers。 +- 同一个 dependency 被多个 package 复用时,每条边都能得到 include dirs,而不会因为 resolved map 命中就跳过传播。 + +这比“全部追加到 root 全局 flags”更接近 package-owned build metadata 的长期方向,也避免传递依赖的 header 只在 root 上可见、在真正 consumer 上不可见。 + +## 3. 通用架构建议 + +### 3.1 把 e2e home 分成三种模式 + +建议后续显式化 e2e helper API: + +```bash +source tests/e2e/_home.sh payload-only +source tests/e2e/_home.sh payload-and-config +source tests/e2e/_home.sh empty +``` + +语义: + +| 模式 | 继承 xpkgs | 继承 config | 用途 | +|---|---:|---:|---| +| `payload-only` | 是 | 否 | first-run、空配置、install/default 语义测试 | +| `payload-and-config` | 是 | 是 | 普通编译、BMI、dependency、import std 测试 | +| `empty` | 否 | 否 | 专门验证冷启动、错误提示、install 下载路径 | + +这样每个测试脚本不用手写 `MCPP_INHERIT_CONFIG=0 MCPP_INHERIT_SUBOS=0`,也能避免未来新增测试重新引入冷下载。 + +### 3.2 把“下载路径测试”集中到少数专门 job + +大体积工具链下载只应该出现在这些地方: + +1. `26_toolchain_management.sh`: CLI install/list/default/remove。 +2. Linux workflow 的 toolchain matrix: GCC、musl-gcc、LLVM。 +3. fresh-install workflow: 验证发布包在空环境中的安装体验。 + +其他 e2e 默认应复用 payload。这样失败定位也更清楚: + +- 下载失败: 看 toolchain/fresh-install job。 +- build/module/dependency 失败: 看 e2e。 + +### 3.3 cache key 和 install marker 要区分 payload 与配置 + +长期建议把工具链 install 状态拆开: + +```text +registry/data/xpkgs/// # payload, content-addressable-ish +registry/toolchains/@.json # mcpp view: compiler path, target, stdlib, source payload +config.toml # user default and mirror +``` + +临时 home 可以安全 symlink/copy payload,但不必继承 default toolchain。`toolchain install` 应该在 payload 已存在时只补 mcpp 的 toolchain metadata,不重新下载。 + +### 3.4 CI 可观测性 + +建议后续给 e2e runner 增加轻量统计: + +```text +downloads_before= +downloads_after= +toolchain_install_seconds= +``` + +可以先用日志 grep 实现: + +- `Downloading xim:` +- `Downloading compat.` +- `Installing ...` + +目标不是精确计费,而是让 PR 上能直接看到“这次 e2e 是否触发了工具链冷下载”。 + +## 4. 验证计划 + +本 PR 应至少验证: + +1. `bash -n tests/e2e/_inherit_toolchain.sh tests/e2e/29_toolchain_partial_versions.sh tests/e2e/31_transitive_deps.sh` +2. `29_toolchain_partial_versions.sh` 日志不再在临时 home 冷下载 musl-gcc。 +3. `31_transitive_deps.sh` 在可复用 musl payload 存在时通过;不存在时 skip,而不是下载。 +4. Linux CI e2e 和后续 musl target step 都通过。 diff --git a/.agents/docs/2026-06-01-cpp-standard-first-class-design.md b/.agents/docs/2026-06-01-cpp-standard-first-class-design.md new file mode 100644 index 0000000..0400f5c --- /dev/null +++ b/.agents/docs/2026-06-01-cpp-standard-first-class-design.md @@ -0,0 +1,510 @@ +# C++ 标准一等配置设计 + +> 日期: 2026-06-01 +> 状态: draft +> 范围: 让 `standard = "c++26"` 这类语言标准配置贯通 manifest、fingerprint、编译 flags、`import std` BMI cache、CDB 和文档。 + +## 0. 结论 + +这不是单个 `-std=c++23` 硬编码点的问题,而是 mcpp 同时存在四套没有闭环的语言标准表达: + +1. manifest 有 `[package].standard` 和旧 `[language].standard` 两个入口。 +2. fingerprint 有 `cppStandard` 字段,但构建命令没有真正消费同一个值。 +3. backend 仍把 C++ 标准当作全局 flags 字符串的一部分,并且把用户 `cxxflags` 同时放进全局和 compile unit。 +4. `import std` BMI cache 只看产物是否存在,构建命令和 cache 元数据都没有记录实际 C++ 标准。 + +合理修法是把 C++ 标准提升为一个归一化后的 build graph 属性: manifest 只负责解析和兼容旧字段,prepare/build plan 只传递一个已归一的 active standard,所有 compiler flag、fingerprint、std module build、cache metadata 和文档都消费这个同一个值。 + +## 1. 现状证据 + +### 1.1 manifest 只完成了解析入口 + +- `src/manifest.cppm:23-28`: `Package::standard` 默认是 `c++23`。 +- `src/manifest.cppm:34-38`: `Language::standard` 也默认是 `c++23`。 +- `src/manifest.cppm:319-331`: `[package].standard` 是新 home,旧 `[language].standard` 会镜像到 package。 +- `src/manifest.cppm:335-339`: 统一后的校验仍只允许 `c++23` 和 `c++latest`,错误信息仍说 MVP only supports c++23。 + +这说明 schema 已经有新字段,但合法值、归一化、调用侧消费还没有完成。 + +### 1.2 fingerprint 有字段,但不等于构建命令闭环 + +- `src/toolchain/fingerprint.cppm:23-29`: `FingerprintInputs::cppStandard` 默认 `c++23`。 +- `src/toolchain/fingerprint.cppm:101`: fingerprint 第 6 字段记录 `cppStandard`。 +- `src/cli.cppm:2505-2512`: build 准备阶段把 `m->language.standard` 写入 fingerprint。 +- `src/cli.cppm:584-604`: canonical compile flags 字符串也拼了 `-std=`。 + +问题是实际 C++ 编译命令不读这个值,fingerprint 记录和真实 compiler dialect 可以分裂。 + +### 1.3 编译 flags 仍硬编码 C++23 + +- `src/build/flags.cppm:221-224`: C 标准已经有 `build.c_standard` 模型。 +- `src/build/flags.cppm:254-256`: C++ 规则仍硬编码 `-std=c++23`,用户只能用 `build.cxxflags = ["-std=c++26"]` 追加覆盖。 + +这会产生两个问题: + +1. 标准配置没有一等入口,只能绕进附加 flags。 +2. 当 `cxxflags` 后追加 `-std=c++26` 时,fingerprint 和 std module cache 仍可能以 `c++23` 的世界观工作。 + +### 1.4 `import std` BMI 构建命令固定 C++23 + +- `src/toolchain/stdmod.cppm:46-49`: `ensure_built()` 只接收 toolchain、fingerprint 和 cache root。 +- `src/toolchain/stdmod.cppm:149-177`: 复用判断只检查 BMI 和 object 是否存在。 +- `src/toolchain/gcc.cppm:105-120`: GCC std module build command 固定 `-std=c++23`。 +- `src/toolchain/clang.cppm:174-228`: Clang std module build command 也固定 `-std=c++23`。 +- `src/toolchain/clang.cppm:265-296`: `std.compat` build command 同样固定 `-std=c++23`。 + +所以项目源文件最终用 C++26 编译时,GCC/Clang 仍可能读到 C++23 预编译出的 std BMI。GCC 报 `language dialect differs 'C++23', expected 'C++26/contracts'` 是这个裂缝的直接结果。 + +### 1.5 package-owned flags 已经是 per-unit 方向,但主包仍重复 + +- `src/modgraph/scanner.cppm:376-379`: scanner 把每个 package 的 `cflags` / `cxxflags` 放到 compile unit。 +- `src/build/ninja_backend.cppm:475-476`, `src/build/ninja_backend.cppm:523-527`, `src/build/ninja_backend.cppm:570-574`: Ninja 再把 per-unit flags 写入 `unit_cxxflags` / `unit_cflags`。 +- `src/build/compile_commands.cppm:120-125`: CDB 也追加 per-unit package flags。 +- `src/build/flags.cppm:213-214`, `src/build/flags.cppm:254-258`: root manifest 的 `cxxflags` / `cflags` 仍被加入全局 baseline。 + +这导致主包 flags 可能在 `cxxflags` 和 `unit_cxxflags` 中各出现一次。对 `-std=` 这种有顺序语义的 flag 来说,重复不仅是噪音,还会掩盖标准模型缺失。 + +### 1.6 文档落后于实现 + +- `docs/05-mcpp-toml.md:40-48`: `[package]` 示例没有 `standard`。 +- `docs/05-mcpp-toml.md:67-78`: `[build]` 只写了 `c_standard`,没有 C++ 标准字段说明。 +- `docs/05-mcpp-toml.md:281-290`: 默认值表只说标准默认 `c++23`,没有说明怎么配置。 + +用户自然会把 C++ 标准塞进 `cxxflags`,因为文档没有给出一等配置路径。 + +## 2. 设计目标 + +1. `standard = "c++26"` 是公开、稳定、文档化的配置,不再需要 `cxxflags = ["-std=..."]`。 +2. 同一次 build graph 中,所有 C++ compile unit、module scan、std BMI 构建、CDB 都使用同一个 active C++ standard。 +3. std module cache 复用前验证真实构建元数据,而不是只看 `std.gcm` / `std.pcm` 是否存在。 +4. fingerprint 使用归一化标准值,避免 `c++2c` 和 `c++26` 这类别名产生无意义缓存分裂。 +5. `cxxflags` 回归到附加编译参数语义,不承担语言标准选择职责。 +6. 保持旧 `[language].standard` 和 xpkg Lua `language = ...` 兼容,但内部尽快归一到新模型。 + +## 3. 非目标 + +1. 不在本轮引入每个 package 独立 C++ 标准。C++ module BMI 和 `import std` 的 dialect 兼容性要求太强,第一阶段应采用 graph-wide active standard。 +2. 不把 `c++latest` 变成可复现语义。`latest` 本质依赖 compiler 版本,fingerprint 必须包含 compiler 版本和实际 flag spelling。 +3. 不重写整个 flags/backend 架构。只建立标准模型和必要去重边界。 + +## 4. 推荐架构 + +### 4.1 新增归一化标准模型 + +建议新增一个小的值类型,放在 `src/manifest.cppm` 或独立模块 `src/build/language.cppm`。如果要避免 manifest 过大,推荐独立模块: + +```cpp +struct CppStandard { + std::string canonical; // "c++23", "c++26", "gnu++26", "c++latest" + std::string compilerFlag; // "-std=c++23", "-std=c++2c", ... + int level = 23; // 23, 26, or max for latest + bool gnuDialect = false; +}; + +std::expected +normalize_cpp_standard(std::string_view raw, + const mcpp::toolchain::Toolchain* tc = nullptr); +``` + +归一化规则: + +| 输入 | canonical | 说明 | +|---|---|---| +| `c++23`, `c++2b` | `c++23` | `c++2b` 作为旧拼写兼容 | +| `gnu++23`, `gnu++2b` | `gnu++23` | 保留 GNU dialect,因为它影响 compiler 行为和 BMI | +| `c++26`, `c++2c` | `c++26` | 语义归一,flag spelling 可由工具链层决定 | +| `gnu++26`, `gnu++2c` | `gnu++26` | 保留 GNU dialect | +| `c++latest` | `c++latest` | 允许,但提示其随 compiler 漂移 | + +`compilerFlag` 不应简单等于 canonical。不同 GCC/Clang 版本可能接受 `-std=c++2c` 而不是 `-std=c++26`。更通用的方式是让 toolchain 层提供 flag spelling: + +```cpp +std::expected +cxx_standard_flag(const Toolchain& tc, const CppStandard& standard); +``` + +第一阶段可以用映射表实现: + +- `c++23` -> `-std=c++23` +- `gnu++23` -> `-std=gnu++23` +- `c++26` -> 优先 `-std=c++26`,必要时 fallback `-std=c++2c` +- `gnu++26` -> 优先 `-std=gnu++26`,必要时 fallback `-std=gnu++2c` + +如果要更稳,可以在 toolchain detect 阶段探测可接受的 `-std=` spelling,并把结果缓存进 `Toolchain`。 + +### 4.2 单一 active standard 流向 + +推荐数据流: + +```text +mcpp.toml / xpkg lua + -> manifest parse + -> normalize_cpp_standard() + -> BuildLanguageConfig + -> FingerprintInputs.cppStandard + -> BuildPlan.cppStandard + -> compute_flags() + -> stdmod::ensure_built() + -> ninja + compile_commands +``` + +关键点: + +1. manifest 解析后立即把 `[package].standard` 和旧 `[language].standard` 归一成同一个值。 +2. `prepare_build()` 只读取归一后的 active standard,不再在 `package.standard` 与 `language.standard` 之间选择。 +3. `BuildPlan` 增加 `cppStandard` 字段,backend 不再回读 manifest 字符串。 +4. `canonical_compile_flags()` 和 fingerprint 使用 `cppStandard.canonical`,不是用户原始输入。 +5. `compute_flags()` 使用 `cppStandard.compilerFlag`,不再硬编码 `-std=c++23`。 + +这样 `manifest -> fingerprint -> flags -> std BMI` 形成闭环。 + +### 4.3 graph-wide 标准策略 + +第一阶段建议采用 graph-wide active standard: + +1. root package 的 `[package].standard` 选择 active standard,默认 `c++23`。 +2. dependency package 的 `standard` 表示最低需求,而不是独立 dialect。 +3. 如果 dependency 需要的标准高于 root active standard,直接报错: + +```text +dependency 'foo' requires c++26, but root package uses c++23. +Set [package].standard = "c++26" in the root manifest. +``` + +4. 如果 root 是 `c++26`,dependency 默认 `c++23`,依赖也用 `c++26` 编译,保证同一 build graph 的 module BMI dialect 一致。 + +这个策略比 per-package standard 更保守,但符合当前模块管线和 cache 设计。未来如果要支持 per-package dialect,必须先设计独立 BMI namespace、跨 dialect module import 规则和 cache key,这不应混进本轮修复。 + +### 4.4 std module cache 元数据 + +`ensure_built()` 应改为: + +```cpp +std::expected ensure_built( + const Toolchain& tc, + std::string_view fingerprint_hex, + const CppStandard& cppStandard, + const std::filesystem::path& cache_root = default_cache_root()); +``` + +std module build command 统一使用 `cppStandard.compilerFlag`: + +- GCC: `g++ -fmodules ... bits/std.cc` +- Clang std: `clang++ --precompile std.cppm ...` +- Clang std object: `clang++ std.pcm -c ...` +- Clang std.compat: `clang++ -fmodule-file=std=...` + +cache dir 下新增元数据文件,建议名为 `std-module.json`: + +```json +{ + "schema": 1, + "compiler": "gcc", + "compiler_version": "16.1.0", + "driver_identity": "hash-or-normalized-driver-ident", + "target_triple": "x86_64-linux-gnu", + "stdlib": "libstdc++", + "stdlib_version": "16.1.0", + "cpp_standard": "c++26", + "std_flag": "-std=c++2c", + "std_module_source": "/path/to/bits/std.cc", + "std_module_source_hash": "hex", + "build_command_hash": "hex", + "mcpp_version": "0.0.41" +} +``` + +复用条件: + +1. BMI 文件存在。 +2. object 文件存在。 +3. metadata 存在。 +4. metadata 的 compiler、stdlib、target、standard、std flag、source hash、build command hash 与当前请求一致。 + +若 metadata 缺失,按 stale cache 处理,重建一次。这样可以兼容旧 cache 目录,又不会继续复用 C++23 生成的 std BMI。 + +### 4.5 fingerprint 与 std BMI hash 的边界 + +当前 fingerprint 第 10 字段是 `stdBmiHash`,但 prepare 阶段先算 fingerprint 再用它作为 std module cache 目录名,存在 chicken-and-egg 注释。 + +本轮可以不重构这个顺序,只做两个约束: + +1. fingerprint 第 6 字段必须使用 active standard canonical 值。 +2. std module metadata 必须独立校验 actual std flag 和 build command。 + +原因: + +- active standard 变了,fingerprint 目录自然变。 +- 同一 fingerprint 目录内,如果旧版本 mcpp 产物或命令变化导致 metadata 不匹配,`ensure_built()` 会重建。 + +后续如果要把 std BMI content hash 真正放进 fingerprint,第 10 字段需要重新设计为两阶段 build 或独立 std module cache key,不适合和 C++26 修复绑在一起。 + +### 4.6 flags 语义整理 + +推荐采用更清晰方案: + +1. 全局 `cxxflags` 只放 build graph baseline: + - C++ standard flag。 + - module flags。 + - toolchain/sysroot/backend flags。 + - project-wide include baseline。 +2. 所有 package-owned `cflags` / `cxxflags` 只通过 compile unit 追加: + - root package 也是一个 package,不特殊放进全局。 + - dependencies 与 root 行为一致。 +3. `build.cxxflags` 中出现 `-std=` 时给迁移诊断: + +```text +build.cxxflags must not set the C++ language standard. +Use [package].standard = "c++26" instead. +``` + +兼容策略可以分两步: + +- P0: 如果 `-std=` 与 active standard 冲突,报错。相同则警告。 +- P1: 全部 `-std=` in `cxxflags` 报错,彻底收口语义。 + +这个策略能避免 `cxxflags` 继续绕过 std BMI dialect 和 fingerprint。 + +### 4.7 compile_commands 与 Ninja 一致性 + +CDB 应继续从 `BuildPlan + CompileFlags` 生成,但需要满足两个不变量: + +1. `compile_commands.json` 与 `build.ninja` 中每个 compile unit 的标准 flag 完全一致。 +2. 标准 flag 只出现一次。 + +建议新增一个共享 helper,避免 Ninja 和 CDB 各自拼接: + +```cpp +std::vector cxx_args_for_unit(const BuildPlan& plan, + const CompileFlags& flags, + const CompileUnit& cu); +``` + +Ninja 可以继续输出字符串变量,CDB 输出 arguments array,但二者应消费同一组分层 flag: + +```text +local includes -> baseline flags -> unit package flags -> -c source -o output +``` + +## 5. 错误提示优化 + +需要在两个位置给直接诊断。 + +### 5.1 manifest/cxxflags 层 + +检测到 `build.cxxflags` 含 `-std=`: + +```text +build.cxxflags contains '-std=c++26'. +C++ language standard is a first-class package setting. +Move it to: + +[package] +standard = "c++26" +``` + +### 5.2 std BMI metadata 层 + +如果 metadata 与当前 active standard 不一致: + +```text +import std requires std module BMI built with the same C++ standard. +Current build uses c++26 (-std=c++2c), but cached std.gcm was built as c++23 (-std=c++23). +Rebuilding std module cache. +``` + +如果重建失败,再附加 compiler 原始输出。 + +## 6. 文档更新 + +`docs/05-mcpp-toml.md` 需要补三块: + +### 6.1 `[package].standard` + +推荐示例: + +```toml +[package] +name = "myapp" +version = "0.1.0" +standard = "c++26" +``` + +说明: + +- 默认 `c++23`。 +- `c++26` 可用于需要 C++26 语言特性的项目。 +- `c++2c` 作为别名兼容,但文档推荐写 `c++26`。 +- `gnu++26` 表示 GNU dialect,会进入 fingerprint 和 std BMI cache key。 + +### 6.2 `[language]` 迁移说明 + +```toml +[language] +standard = "c++26" +``` + +仍可读,但标记为 legacy compatibility。新项目使用 `[package].standard`。 + +### 6.3 `cxxflags` 边界 + +明确写: + +```toml +[build] +cxxflags = ["-Wall", "-Wextra"] # 附加 C++ flags,不用于 -std= +``` + +语言标准只能通过 `[package].standard` 配置。 + +## 7. 实施拆分 + +### P0.1 标准归一化与 manifest 校验 + +改动: + +- 新增 `normalize_cpp_standard()`。 +- `[package].standard` 和 `[language].standard` 都进入归一化。 +- `synthesize_from_xpkg_lua()` 中的 `language = ...` 同步填充归一后的 package standard。 +- 支持 `c++26`、`c++2c`、`gnu++26`、`gnu++2c`。 + +验证: + +- `tests/unit/test_manifest.cpp`: `[package] standard = "c++26"` 通过。 +- `tests/unit/test_manifest.cpp`: `[language] standard = "c++2c"` 映射到 `c++26`。 +- `tests/unit/test_manifest.cpp`: 非法值给出包含 allowed values 的错误。 + +### P0.2 BuildPlan 与 compute_flags 消费 active standard + +改动: + +- `BuildPlan` 增加 active `CppStandard` 字段。 +- `prepare_build()` 填入 active standard。 +- `FingerprintInputs.cppStandard` 使用 canonical。 +- `compute_flags()` 使用 `cppStandard.compilerFlag`。 +- 移除 C++ 标准硬编码 `-std=c++23`。 + +验证: + +- unit test 生成 `standard = "c++26"` 的 plan,`build.ninja` 只含 `-std=c++26` 或工具链选出的 `-std=c++2c`。 +- `compile_commands.json` 与 `build.ninja` 标准 flag 一致。 + +### P0.3 std module build 跟随 active standard + +改动: + +- `ensure_built(tc, fp.hex, cppStandard)`。 +- GCC/Clang/std.compat build command 都使用 active standard flag。 +- cache metadata 写入并在复用前校验。 +- metadata 缺失或不匹配时重建。 + +验证: + +- unit test 或 command-generation test: C++26 下 GCC std module command 含 C++26 flag,不含 C++23。 +- Clang std 和 std.compat command 同样覆盖。 +- metadata mismatch 时不复用旧 BMI。 + +### P1 cxxflags 去重与标准绕行收口 + +改动: + +- `compute_flags()` 不再把 root `buildConfig.cxxflags` 和 `cflags` 放进全局 baseline。 +- root 与 dependency 一样,只通过 compile unit `packageCxxflags` / `packageCflags` 进入 Ninja/CDB。 +- 检测 `-std=` in `cxxflags`,先报冲突错误或迁移 warning。 + +验证: + +- 主包 `-DROOT=1` 在每个 root compile unit 只出现一次。 +- dependency flags 仍只作用于 dependency compile units。 +- `-std=` in `cxxflags` 给出迁移提示。 + +### P1 文档对齐 + +改动: + +- `docs/05-mcpp-toml.md` 增加 `[package].standard` 字段。 +- 默认值表写明配置方式。 +- 增加 `[language]` legacy note。 +- 增加 `cxxflags` 不承担语言标准的说明。 + +验证: + +- 文档示例中不再推荐或暗示用 `cxxflags` 配置 `-std=`。 + +### P2 更直接的错误提示 + +改动: + +- std BMI metadata mismatch 输出清晰原因。 +- 如果 compiler 不支持选定 standard,提示当前 toolchain 和建议 fallback。 + +验证: + +- 人工构造 metadata mismatch,错误或 warning 能说明 cached/current standard。 + +## 8. 测试矩阵 + +| 层级 | 用例 | 目的 | +|---|---|---| +| Manifest | `[package] standard = "c++26"` | 新字段可用 | +| Manifest | `[language] standard = "c++2c"` | 旧字段兼容和别名归一 | +| Manifest | `build.cxxflags = ["-std=c++26"]` | 给迁移诊断 | +| Fingerprint | `c++2c` 与 `c++26` | canonical 相同,fingerprint 不分裂 | +| Fingerprint | `c++23` 与 `c++26` | fingerprint 必须不同 | +| Flags | `standard = "c++26"` | Ninja/CDB 没有 `-std=c++23` | +| Flags | root `cxxflags = ["-DROOT=1"]` | 不重复 | +| stdmod GCC | C++26 + `import std` | std.gcm build command 使用同一 standard | +| stdmod Clang | C++26 + `import std.compat` | std.pcm 和 std.compat.pcm 使用同一 standard | +| Cache | metadata 缺失 | 旧 cache 触发重建 | +| Cache | metadata standard mismatch | 不复用旧 BMI | +| E2E | C++26 非 import std 项目 | 编译命令贯通 | +| E2E | C++26 import std 项目 | std BMI dialect 一致 | + +E2E 应按工具链能力 gate。不是所有 CI compiler 都一定支持 C++26 std module,不能让环境能力不足变成主线失败。 + +## 9. 风险与取舍 + +### 9.1 `c++26` flag spelling + +风险: 某些 compiler 支持 `-std=c++2c` 但不支持 `-std=c++26`。 + +取舍: 用户-facing canonical 推荐 `c++26`,compiler-facing flag 由 toolchain 层决定。fingerprint 记录 canonical 和实际 flag spelling,避免 cache 误用。 + +### 9.2 GNU dialect + +风险: `gnu++26` 和 `c++26` 不应混用同一 BMI cache。 + +取舍: canonical 保留 `gnu++` 前缀,metadata 记录 `std_flag`。这会分出不同 cache,是正确行为。 + +### 9.3 dependency standard + +风险: dependency 声明 `c++23` 但 root 用 `c++26` 编译,可能遇到语义变化。 + +取舍: 第一阶段把 dependency standard 当作 minimum requirement。模块 BMI 兼容性比逐包保留原 dialect 更重要。如果未来需要 per-package exact dialect,必须设计单独的模块 ABI 边界。 + +### 9.4 `cxxflags` 兼容性 + +风险: 现有用户可能已经用 `cxxflags = ["-std=c++26"]` 绕过。 + +取舍: 支持一等 `standard` 后,应给明确迁移提示。短期可以 warning,长期必须禁止,否则 std BMI cache 和 fingerprint 仍会被绕过。 + +## 10. 最小落地顺序 + +1. 先打通 `standard -> active standard -> compute_flags`,让 `build.ninja` 不再硬编码 C++23。 +2. 同一 PR 内把 `active standard` 传入 `stdmod::ensure_built()`,否则 `import std` 仍会撞 BMI dialect。 +3. 写入 std module cache metadata,修复存在即复用的问题。 +4. 再整理 root `cxxflags` 重复和 `-std=` 迁移诊断。 +5. 最后补 `docs/05-mcpp-toml.md` 和更友好的错误信息。 + +如果要切 PR,建议: + +- PR 1: 一等 standard + stdmod 同步 + metadata。解决 C++26/import std 的正确性问题。 +- PR 2: cxxflags 去重和 `-std=` 迁移诊断。解决语义边界和输出一致性。 +- PR 3: 文档与示例。也可以并入 PR 1,但单独做更容易 review。 + +## 11. 后续可选优化 + +1. 把 C、C++ 标准都统一进 `LanguageConfig`,让 `c_standard` 也获得别名归一和工具链 capability probe。 +2. 把 Ninja 和 CDB 的 per-unit args 生成合并成共享 helper,减少未来 flags 顺序漂移。 +3. 将 std module cache key 从 output fingerprint 中独立出来,形成 `std-module////...` 的显式 cache namespace。 +4. 在 `mcpp doctor` 中展示当前 active C++ standard、compiler flag spelling、std module cache metadata 和 std BMI 状态。 diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 36bd0b6..fbd75d8 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -134,6 +134,10 @@ jobs: # default-toolchain path) gets a deterministic GNU answer # instead of whatever auto-install picks on a fresh sandbox. "$MCPP" toolchain default gcc@16.1.0 + # Warm musl once in the persistent sandbox. Fresh-home e2e tests + # inherit this payload, and the later --target musl job reuses it + # instead of downloading a second copy into another home. + "$MCPP" toolchain install gcc 15.1.0-musl bash tests/e2e/run_all.sh - name: Save freshly-built mcpp for toolchain tests diff --git a/CHANGELOG.md b/CHANGELOG.md index f077686..ff4bb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.42] — 2026-06-01 + +### 新增 + +- 将 `[package].standard` 打通为一等 C++ 标准配置,默认仍为 `c++23`, + 并支持 `c++26` / `c++2c` 等写法。 + +### 修复 + +- 编译 flags、`compile_commands.json`、fingerprint 与 `import std` 标准库 + BMI 预构建命令现在使用同一个 active C++ 标准。 +- `std.gcm` / `std.pcm` cache 增加元数据校验,只有 compiler、stdlib、target、 + standard、source 与 build command 匹配时才复用。 +- `build.cxxflags` 回归附加 C++ flags 语义,若写入 `-std=` 会提示迁移到 + `[package].standard`。 + ## [0.0.41] — 2026-06-01 ### 修复 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 0d1c844..f87f9a7 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -41,12 +41,21 @@ lib-root 约定:主模块接口默认在 `src/mylib.cppm`(包名的最后一段) [package] name = "myapp" # 包名(必填) version = "0.1.0" # 语义化版本(必填) +standard = "c++23" # C++ 标准(默认 c++23; 可设 c++26) description = "My awesome app" # 简介(可选) license = "MIT" # 许可证(可选) authors = ["Alice", "Bob"] # 作者列表(可选) repo = "https://github.com/user/myapp" # 仓库地址(可选) ``` +`standard` 是 C++ 语言标准的一等配置。推荐值: + +- `c++23`:默认值,适合当前模块化默认模板。 +- `c++26`:需要 C++26 语言特性时使用。 +- `c++2c`:兼容别名,解析后归一为 `c++26`。 +- `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。 +- `c++latest`:跟随当前 mcpp 支持的最新标准,适合本地试验,不推荐要求可复现的发布包使用。 + ### 2.2 `[targets.]` — 构建目标 ```toml @@ -72,11 +81,20 @@ sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/* include_dirs = ["include", "third_party/include"] # 头文件搜索路径 c_standard = "c11" # C 源文件的标准(默认 c11) cflags = ["-DFOO=1"] # 额外 C 编译参数 -cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数 +cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...) ldflags = ["-lfoo"] # 额外链接参数 static_stdlib = true # 静态链接 libstdc++(默认 true) ``` +C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: + +```toml +[package] +standard = "c++26" +``` + +mcpp 会把同一个标准用于普通 C++ 编译、模块扫描、`compile_commands.json` 和 `import std` 的标准库 BMI 构建。 + **glob 排除**(`!` 前缀,mcpp 0.0.4+): ```toml @@ -285,9 +303,20 @@ mcpp build --target x86_64-linux-musl | 源文件 | `src/**/*.{cppm,cpp,cc,c}` | 自动递归扫描 | | 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 | | 库根 | `src/.cppm` | 可用 `[lib].path` 覆盖 | -| 标准 | `c++23` | C++23 模块是一等公民 | +| C++ 标准 | `c++23` | 用 `[package].standard` 配置; 支持 `c++26` / `c++2c` | | C 标准 | `c11` | `.c` 文件自动走 C 编译器 | | 静态 stdlib | `true` | 便携二进制 | | 头文件 | `include/`(如果存在) | 自动加到 `-I` | | 测试 | `tests/**/*.cpp` | `mcpp test` 自动发现 | | 依赖命名空间 | `mcpp`(默认) | 平铺写法走默认 ns | + +### 4.1 旧 `[language]` 兼容层 + +旧配置仍可读取: + +```toml +[language] +standard = "c++26" +``` + +新项目请使用 `[package].standard`。如果两个位置都出现,`[package].standard` 是权威配置。 diff --git a/mcpp.toml b/mcpp.toml index 3f46caa..b6a4865 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.41" +version = "0.0.42" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 112c380..b9be1fb 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -201,17 +201,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Opt level (musl ICE workaround) std::string opt_flag = isMuslTc ? " -Og" : " -O2"; - // User flags - auto join = [](const std::vector& v) { - std::string s; - for (auto& f : v) { - s += ' '; - s += f; - } - return s; - }; - std::string user_cxxflags = join(plan.manifest.buildConfig.cxxflags); - std::string user_cflags = join(plan.manifest.buildConfig.cflags); + // User link flags std::string user_ldflags; for (auto const& flag : plan.manifest.buildConfig.ldflags) { user_ldflags += ' '; @@ -251,11 +241,13 @@ CompileFlags compute_flags(const BuildPlan& plan) { prebuilt_module_flag = std::format(" -fprebuilt-module-path={}", escape_path(plan.outputDir / traits.bmiDir)); } - f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}{}{}", module_flag, std_module_flag, + std::string cxx_std_flag = + plan.cppStandardFlag.empty() ? std::string("-std=c++23") : plan.cppStandardFlag; + f.cxx = std::format("{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag, std_compat_module_flag, prebuilt_module_flag, - opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags, user_cxxflags); - f.cc = std::format("-std={}{}{}{}{}{}{}", c_std, opt_flag, pic_flag, compile_toolchain_flags, - b_flag, include_flags, user_cflags); + opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags); + f.cc = std::format("-std={}{}{}{}{}{}", c_std, opt_flag, pic_flag, compile_toolchain_flags, + b_flag, include_flags); // Link flags f.staticStdlib = plan.manifest.buildConfig.staticStdlib; diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 77335e9..d859834 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -40,6 +40,8 @@ struct BuildPlan { mcpp::manifest::Manifest manifest; mcpp::toolchain::Toolchain toolchain; mcpp::toolchain::Fingerprint fingerprint; + std::string cppStandard = "c++23"; + std::string cppStandardFlag = "-std=c++23"; std::filesystem::path projectRoot; // where mcpp.toml lives std::filesystem::path outputDir; // target/// @@ -153,6 +155,17 @@ std::vector shared_library_link_flags(const mcpp::manifest::Target& return flags; } +std::vector +local_include_dirs_for_manifest(const std::filesystem::path& root, + const mcpp::manifest::Manifest& manifest) +{ + std::vector dirs; + for (auto const& inc : manifest.buildConfig.includeDirs) { + dirs.push_back(inc.is_absolute() ? inc : root / inc); + } + return dirs; +} + } // namespace BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, @@ -170,6 +183,10 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, plan.manifest = manifest; plan.toolchain = tc; plan.fingerprint = fp; + if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) { + plan.cppStandard = stdCfg->canonical; + plan.cppStandardFlag = stdCfg->flag; + } plan.projectRoot = projectRoot; plan.outputDir = outputDir; plan.stdBmiPath = stdBmiPath; @@ -391,6 +408,9 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, main_cu.source = *lu.entryMain; main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain); main_cu.packageName = qualified_package_name(manifest); + main_cu.localIncludeDirs = local_include_dirs_for_manifest(projectRoot, manifest); + main_cu.packageCflags = manifest.buildConfig.cflags; + main_cu.packageCxxflags = manifest.buildConfig.cxxflags; // We didn't scan main.cpp earlier (it's not in scanner output unless globbed in). // Best-effort: scan its imports here. diff --git a/src/cli.cppm b/src/cli.cppm index ba113f3..7eb5e6f 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -583,7 +583,7 @@ struct CliInstallProgress : mcpp::fetcher::EventHandler { // Compose a stable canonical compile-flags string for fingerprinting. std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { std::string s; - s += "-std="; s += m.language.standard; + s += "-std="; s += m.package.standard; s += " -fmodules"; if (!m.buildConfig.cStandard.empty()) { s += " c_standard="; @@ -1874,30 +1874,74 @@ prepare_build(bool print_fingerprint, return std::pair{effRoot, std::move(*manifest)}; }; - // Append a dep's [build].include_dirs onto the main manifest's, glob- - // expanded against the dep's root. Returns the absolute paths actually - // appended so the caller can later evict them on a SemVer-merge re-fetch. - auto propagateIncludeDirs = [&](const std::filesystem::path& depRoot, - const mcpp::manifest::Manifest& depManifest) + auto appendIncludeDirsTo = [&](mcpp::manifest::Manifest& target, + const std::filesystem::path& depRoot, + const mcpp::manifest::Manifest& depManifest) -> std::vector { std::vector added; + auto append_unique = [&](const std::filesystem::path& dir) { + auto& dirs = target.buildConfig.includeDirs; + if (std::find(dirs.begin(), dirs.end(), dir) != dirs.end()) return; + dirs.push_back(dir); + added.push_back(dir); + }; for (auto& inc : depManifest.buildConfig.includeDirs) { if (inc.is_absolute()) { - m->buildConfig.includeDirs.push_back(inc); - added.push_back(inc); + append_unique(inc); continue; } auto matches = mcpp::modgraph::expand_dir_glob(depRoot, inc.generic_string()); if (matches.empty()) continue; for (auto& d : matches) { - m->buildConfig.includeDirs.push_back(d); - added.push_back(d); + append_unique(d); } } return added; }; + auto syncMainPackageIncludes = [&] { + if (!packages.empty()) { + packages[0].manifest.buildConfig.includeDirs = m->buildConfig.includeDirs; + } + }; + + // Append a dep's [build].include_dirs onto the main manifest's, glob- + // expanded against the dep's root. Returns the absolute paths actually + // appended so the caller can later evict them on a SemVer-merge re-fetch. + auto propagateIncludeDirs = [&](const std::filesystem::path& depRoot, + const mcpp::manifest::Manifest& depManifest) + -> std::vector + { + auto added = appendIncludeDirsTo(*m, depRoot, depManifest); + syncMainPackageIncludes(); + return added; + }; + + auto propagateIncludeDirsToConsumer = + [&](std::size_t consumerDepIndex, + const std::filesystem::path& depRoot, + const mcpp::manifest::Manifest& depManifest) + { + if (consumerDepIndex == kMainConsumer) { + (void)propagateIncludeDirs(depRoot, depManifest); + return; + } + if (consumerDepIndex >= dep_manifests.size() + || consumerDepIndex + 1 >= packages.size()) { + return; + } + auto added = appendIncludeDirsTo(*dep_manifests[consumerDepIndex], + depRoot, depManifest); + auto& packageManifest = packages[consumerDepIndex + 1].manifest; + for (auto const& dir : added) { + auto& dirs = packageManifest.buildConfig.includeDirs; + if (std::find(dirs.begin(), dirs.end(), dir) == dirs.end()) { + dirs.push_back(dir); + } + } + }; + // Drop earlier include_dirs that came from a now-superseded dep version. // Erases by value match — safe because the outer code only ever appends, // and on re-fetch we re-record the new entries afterwards. @@ -1907,6 +1951,7 @@ prepare_build(bool print_fingerprint, auto pos = std::find(dirs.begin(), dirs.end(), p); if (pos != dirs.end()) dirs.erase(pos); } + syncMainPackageIncludes(); }; auto normalizeDepLdflag = [](const std::filesystem::path& depRoot, @@ -2297,7 +2342,16 @@ prepare_build(bool print_fingerprint, continue; } // Same key, same version (or compatible path/git) — already - // processed; skip. + // processed; still attach its public include dirs to this + // consumer before skipping. Include propagation is per edge, not + // per unique package: two consumers can need the same dep's + // headers even though the dep itself is fetched/scanned once. + if (it->second.depIndex + 1 < packages.size()) { + auto const& existing = packages[it->second.depIndex + 1]; + propagateIncludeDirsToConsumer(item.consumerDepIndex, + existing.root, + existing.manifest); + } continue; } @@ -2422,6 +2476,11 @@ prepare_build(bool print_fingerprint, // by depIndex (the SemVer merger needs to overwrite the slot). dep_manifests.push_back( std::make_unique(std::move(*dep_manifest))); + if (item.consumerDepIndex != kMainConsumer) { + propagateIncludeDirsToConsumer(item.consumerDepIndex, + dep_root, + *dep_manifests.back()); + } dep_cache_identities.push_back({ .indexName = cache_index_name(key.ns), .packageName = name, @@ -2467,7 +2526,7 @@ prepare_build(bool print_fingerprint, auto tmp = std::filesystem::temp_directory_path() / std::format("mcpp_p1689_{}", std::random_device{}()); std::filesystem::create_directories(tmp); - return mcpp::modgraph::scan_packages_p1689(packages, *tc, tmp); + return mcpp::modgraph::scan_packages_p1689(packages, *tc, tmp, m->cppStandard.flag); } return mcpp::modgraph::scan_packages(packages); }(); @@ -2504,7 +2563,7 @@ prepare_build(bool print_fingerprint, // Compute fingerprint (no lockfile in M1 → empty hash) mcpp::toolchain::FingerprintInputs fpi; fpi.toolchain = *tc; - fpi.cppStandard = m->language.standard; + fpi.cppStandard = m->package.standard; fpi.compileFlags = canonical_compile_flags(*m) + canonical_package_build_metadata(packages); fpi.dependencyLockHash = ""; // M2 @@ -2517,7 +2576,8 @@ prepare_build(bool print_fingerprint, std::filesystem::path stdCompatBmiPath; std::filesystem::path stdCompatObjectPath; if (needsStdModule) { - auto sm = mcpp::toolchain::ensure_built(*tc, fp.hex); + auto sm = mcpp::toolchain::ensure_built( + *tc, fp.hex, m->package.standard, m->cppStandard.flag); if (!sm) return std::unexpected(sm.error().message); stdBmiPath = sm->bmiPath; stdObjectPath = sm->objectPath; diff --git a/src/manifest.cppm b/src/manifest.cppm index 67d3fb8..db5671a 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -20,6 +20,13 @@ export namespace mcpp::manifest { using DependencySpec = mcpp::pm::DependencySpec; inline constexpr auto kDefaultNamespace = mcpp::pm::kDefaultNamespace; +struct CppStandardConfig { + std::string canonical = "c++23"; + std::string flag = "-std=c++23"; + int level = 23; + bool gnuDialect = false; +}; + struct Package { std::string name; std::string namespace_; // xpkg V1 namespace field (0.0.6+); empty = infer from name @@ -191,6 +198,7 @@ struct Manifest { std::map indices; // M5.0: post-parse computed/inferred state + CppStandardConfig cppStandard; bool usesModules = true; // refined by scanner bool usesImportStd = true; // refined by scanner std::vector inferredNotes; // for `Inferred ...` banner @@ -212,6 +220,7 @@ struct ManifestError { std::expected parse_string(std::string_view content, const std::filesystem::path& origin = "mcpp.toml"); std::expected load(const std::filesystem::path& path); +std::expected normalize_cpp_standard(std::string_view raw); // For `mcpp new` scaffolding. std::string default_template(std::string_view packageName); @@ -272,6 +281,10 @@ namespace t = mcpp::libs::toml; namespace { +bool starts_with_std_flag(std::string_view flag) { + return flag == "-std" || flag.starts_with("-std="); +} + ManifestError error(const std::filesystem::path& origin, const std::string& msg, t::Position pos = {0, 0}) { @@ -280,6 +293,66 @@ ManifestError error(const std::filesystem::path& origin, } // namespace +std::expected normalize_cpp_standard(std::string_view raw) { + auto trim_copy = [](std::string_view input) { + std::size_t begin = 0; + while (begin < input.size() + && std::isspace(static_cast(input[begin]))) { + ++begin; + } + std::size_t end = input.size(); + while (end > begin + && std::isspace(static_cast(input[end - 1]))) { + --end; + } + return std::string(input.substr(begin, end - begin)); + }; + + std::string s = trim_copy(raw); + for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); + + CppStandardConfig out; + if (s.empty() || s == "c++23" || s == "c++2b") { + out.canonical = "c++23"; + out.flag = "-std=c++23"; + out.level = 23; + out.gnuDialect = false; + return out; + } + if (s == "gnu++23" || s == "gnu++2b") { + out.canonical = "gnu++23"; + out.flag = "-std=gnu++23"; + out.level = 23; + out.gnuDialect = true; + return out; + } + if (s == "c++26" || s == "c++2c") { + out.canonical = "c++26"; + out.flag = "-std=c++26"; + out.level = 26; + out.gnuDialect = false; + return out; + } + if (s == "gnu++26" || s == "gnu++2c") { + out.canonical = "gnu++26"; + out.flag = "-std=gnu++26"; + out.level = 26; + out.gnuDialect = true; + return out; + } + if (s == "c++latest") { + out.canonical = "c++latest"; + out.flag = "-std=c++26"; + out.level = 999; + out.gnuDialect = false; + return out; + } + + return std::unexpected(std::format( + "unsupported C++ standard '{}'; expected c++23, c++26, c++2c, gnu++23, gnu++26, or c++latest", + raw)); +} + std::expected parse_string(std::string_view content, const std::filesystem::path& origin) { auto doc = t::parse(content); @@ -332,11 +405,13 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_bool("language.modules")) m.language.modules = *v; if (auto v = doc->get_bool("language.import_std")) m.language.importStd = *v; - // Validation on the unified standard - if (m.package.standard != "c++23" && m.package.standard != "c++latest") { - return std::unexpected(error(origin, - std::format("MVP only supports c++23; got '{}'", m.package.standard))); - } + // Validation on the unified standard. Store the canonical spelling so all + // downstream build surfaces consume one active value. + auto stdCfg = normalize_cpp_standard(m.package.standard); + if (!stdCfg) return std::unexpected(error(origin, stdCfg.error())); + m.cppStandard = *stdCfg; + m.package.standard = m.cppStandard.canonical; + m.language.standard = m.cppStandard.canonical; if (had_language_section && !m.language.modules) { return std::unexpected(error(origin, "language.modules must be true (mcpp is modules-only)")); @@ -632,6 +707,13 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *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; + for (auto const& flag : m.buildConfig.cxxflags) { + if (starts_with_std_flag(flag)) { + return std::unexpected(error(origin, + std::format("build.cxxflags contains '{}'; use [package].standard to configure the C++ language standard", + flag))); + } + } // [lib] — library root convention (cargo-style). if (auto v = doc->get_string("lib.path")) { @@ -1290,6 +1372,7 @@ synthesize_from_xpkg_lua(std::string_view luaContent, m.sourcePath = std::format("xpkg-lua://{}@{}", packageName, packageVersion); m.package.name = std::string(packageName); m.package.version = std::string(packageVersion); + m.package.standard = "c++23"; m.language.standard = "c++23"; m.language.modules = true; m.language.importStd = true; @@ -1317,7 +1400,10 @@ synthesize_from_xpkg_lua(std::string_view luaContent, if (key == "language") { auto v = cur.read_string(); - if (!v.empty()) m.language.standard = v; + if (!v.empty()) { + m.language.standard = v; + m.package.standard = v; + } } else if (key == "import_std") { auto v = cur.read_bareword(); @@ -1482,6 +1568,12 @@ synthesize_from_xpkg_lua(std::string_view luaContent, : (key == "cxxflags" ? m.buildConfig.cxxflags : m.buildConfig.ldflags); while (!cur.eof() && cur.peek() != '}') { auto s = cur.read_string(); + if (key == "cxxflags" && starts_with_std_flag(s)) { + return std::unexpected(ManifestError{ + std::format("cxxflags contains '{}'; use language/package standard to configure the C++ language standard", + s), + m.sourcePath, 0, 0}); + } if (!s.empty()) target.push_back(std::move(s)); cur.skip_ws_and_comments(); } @@ -1517,6 +1609,14 @@ synthesize_from_xpkg_lua(std::string_view luaContent, m.targets.push_back(std::move(t)); } + auto stdCfg = normalize_cpp_standard(m.package.standard); + if (!stdCfg) { + return std::unexpected(ManifestError{stdCfg.error(), m.sourcePath, 0, 0}); + } + m.cppStandard = *stdCfg; + m.package.standard = m.cppStandard.canonical; + m.language.standard = m.cppStandard.canonical; + return m; } diff --git a/src/modgraph/p1689.cppm b/src/modgraph/p1689.cppm index 7e8a386..6062622 100644 --- a/src/modgraph/p1689.cppm +++ b/src/modgraph/p1689.cppm @@ -3,7 +3,7 @@ // Replaces (under MCPP_SCANNER=p1689) the regex-based scanner. Each source // file gets fed to: // -// g++ -std=c++23 -fmodules -fdeps-format=p1689r5 \ +// g++ -fmodules -fdeps-format=p1689r5 \ // -fdeps-file=/x.ddi -fdeps-target=/x.o \ // -M -MM -MF /x.dep -E -o /x.o // @@ -48,7 +48,8 @@ std::expected scan_file(const std::filesystem::path& source, const std::string& packageName, const mcpp::toolchain::Toolchain& tc, - const std::filesystem::path& tmpDir); + const std::filesystem::path& tmpDir, + std::string_view cppStandardFlag); } // namespace mcpp::modgraph::p1689 @@ -317,7 +318,8 @@ std::expected scan_file(const std::filesystem::path& source, const std::string& packageName, const mcpp::toolchain::Toolchain& tc, - const std::filesystem::path& tmpDir) + const std::filesystem::path& tmpDir, + std::string_view cppStandardFlag) { std::error_code ec; std::filesystem::create_directories(tmpDir, ec); @@ -335,14 +337,18 @@ scan_file(const std::filesystem::path& source, if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot={}", shell_escape(tc.sysroot)); } + std::string std_flag = cppStandardFlag.empty() + ? std::string("-std=c++23") + : std::string(cppStandardFlag); std::string cmd = std::format( - "{} -std=c++23 -fmodules{}" + "{} {} -fmodules{}" " -fdeps-format=p1689r5" " -fdeps-file={}" " -fdeps-target={}" " -M -MM -MF {}" " -E {} -o {} 2>&1", shell_escape(tc.binaryPath), + std_flag, sysroot_flag, shell_escape(ddi), shell_escape(obj), diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm index 08a2b80..a29eefd 100644 --- a/src/modgraph/scanner.cppm +++ b/src/modgraph/scanner.cppm @@ -65,7 +65,8 @@ ScanResult scan_packages(const std::vector& packages); // cli when MCPP_SCANNER=p1689 (see docs/27). ScanResult scan_packages_p1689(const std::vector& packages, const mcpp::toolchain::Toolchain& tc, - const std::filesystem::path& tmpDir); + const std::filesystem::path& tmpDir, + std::string_view cppStandardFlag); } // namespace mcpp::modgraph @@ -435,7 +436,8 @@ ScanResult scan_packages(const std::vector& packages) { ScanResult scan_packages_p1689(const std::vector& packages, const mcpp::toolchain::Toolchain& tc, - const std::filesystem::path& tmpDir) + const std::filesystem::path& tmpDir, + std::string_view cppStandardFlag) { ScanResult result; for (auto const& p : packages) { @@ -446,7 +448,7 @@ ScanResult scan_packages_p1689(const std::vector& packages, const auto localIncludeDirs = local_include_dirs_for(p.root, p.manifest); for (auto const& f : all_files) { auto r = mcpp::modgraph::p1689::scan_file( - f, p.manifest.package.name, tc, tmpDir); + f, p.manifest.package.name, tc, tmpDir, cppStandardFlag); if (!r) { result.errors.push_back(ScanError{ f, 0, r.error() }); continue; diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 1d9460f..cf27c43 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -26,7 +26,8 @@ std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir std::vector std_module_build_commands(const Toolchain& tc, const std::filesystem::path& cacheDir, const std::filesystem::path& bmiPath, - std::string_view sysrootFlag); + std::string_view sysrootFlag, + std::string_view cppStandardFlag); std::optional find_libcxx_std_compat_source( const std::filesystem::path& cxx_binary, @@ -39,7 +40,8 @@ std::vector std_compat_build_commands(const Toolchain& tc, const std::filesystem::path& cacheDir, const std::filesystem::path& bmiPath, const std::filesystem::path& stdBmiPath, - std::string_view sysrootFlag); + std::string_view sysrootFlag, + std::string_view cppStandardFlag); std::filesystem::path archive_tool(const Toolchain& tc); @@ -174,7 +176,8 @@ std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir std::vector std_module_build_commands(const Toolchain& tc, const std::filesystem::path& cacheDir, const std::filesystem::path& bmiPath, - std::string_view sysrootFlag) { + std::string_view sysrootFlag, + std::string_view cppStandardFlag) { auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string(); #if defined(_WIN32) // Windows: use absolute paths, raw binary path as first token @@ -191,17 +194,19 @@ std::vector std_module_build_commands(const Toolchain& tc, : ""; return { std::format( - "{} -std=c++23{}{} " + "{} {}{}{} " "--precompile {} -o {}", tc.binaryPath.string(), + cppStandardFlag, ixxFlags, sysrootFlag, mcpp::xlings::shq(tc.stdModuleSource.string()), mcpp::xlings::shq(absBmi)), std::format( - "{} -std=c++23{} " + "{} {}{} " "{} -c -o {}", tc.binaryPath.string(), + cppStandardFlag, sysrootFlag, mcpp::xlings::shq(absBmi), mcpp::xlings::shq((cacheDir / "std.o").string())) @@ -209,20 +214,22 @@ std::vector std_module_build_commands(const Toolchain& tc, #else return { std::format( - "cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " + "cd {} && {}{} {} -Wno-reserved-module-identifier{} " "--precompile {} -o {} 2>&1", mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, sysrootFlag, mcpp::xlings::shq(tc.stdModuleSource.string()), mcpp::xlings::shq(relBmi)), std::format( - "cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " + "cd {} && {}{} {} -Wno-reserved-module-identifier{} " "{} -c -o std.o 2>&1", mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, sysrootFlag, mcpp::xlings::shq(relBmi)) }; @@ -266,7 +273,8 @@ std::vector std_compat_build_commands(const Toolchain& tc, const std::filesystem::path& cacheDir, const std::filesystem::path& bmiPath, const std::filesystem::path& stdBmiPath, - std::string_view sysrootFlag) + std::string_view sysrootFlag, + std::string_view cppStandardFlag) { auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string(); auto relStdBmi = std::filesystem::relative(stdBmiPath, cacheDir).string(); @@ -274,22 +282,24 @@ std::vector std_compat_build_commands(const Toolchain& tc, // Note: the path after = must NOT be shell-quoted separately; the // entire -fmodule-file flag is a single token to the compiler. return { - std::format("cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " + std::format("cd {} && {}{} {} -Wno-reserved-module-identifier{} " "-fmodule-file=std={} " "--precompile {} -o {} 2>&1", mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, sysrootFlag, relStdBmi, mcpp::xlings::shq(tc.stdCompatSource.string()), mcpp::xlings::shq(relBmi)), - std::format("cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " + std::format("cd {} && {}{} {} -Wno-reserved-module-identifier{} " "-fmodule-file=std={} " "{} -c -o std.compat.o 2>&1", mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, sysrootFlag, relStdBmi, mcpp::xlings::shq(relBmi)) diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index e6a063d..cc4104b 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.41"; +inline constexpr std::string_view MCPP_VERSION = "0.0.42"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index 3b47e56..ac3c7a8 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -26,7 +26,8 @@ std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir std::string std_module_build_command(const Toolchain& tc, const std::filesystem::path& cacheDir, - std::string_view sysrootFlag); + std::string_view sysrootFlag, + std::string_view cppStandardFlag); } // namespace mcpp::toolchain::gcc @@ -104,17 +105,19 @@ std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir std::string std_module_build_command(const Toolchain& tc, const std::filesystem::path& cacheDir, - std::string_view sysrootFlag) { + std::string_view sysrootFlag, + std::string_view cppStandardFlag) { std::string bFlag; if (auto binutilsBin = find_binutils_bin(tc.binaryPath)) { bFlag = std::format(" -B'{}'", binutilsBin->string()); } return std::format( - "cd {} && {}{} -std=c++23 -fmodules -O2{}{} -c {} -o std.o 2>&1", + "cd {} && {}{} {} -fmodules -O2{}{} -c {} -o std.o 2>&1", mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, sysrootFlag, bFlag, mcpp::xlings::shq(tc.stdModuleSource.string())); diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index b72ed07..3487f00 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -23,9 +23,11 @@ module; export module mcpp.toolchain.stdmod; import std; +import mcpp.libs.json; import mcpp.platform; import mcpp.toolchain.clang; import mcpp.toolchain.detect; +import mcpp.toolchain.fingerprint; import mcpp.toolchain.gcc; export namespace mcpp::toolchain { @@ -46,6 +48,8 @@ std::filesystem::path default_cache_root(); std::expected ensure_built( const Toolchain& tc, std::string_view fingerprint_hex, + std::string_view cpp_standard, + std::string_view cpp_standard_flag, const std::filesystem::path& cache_root = default_cache_root()); } // namespace mcpp::toolchain @@ -63,6 +67,93 @@ std::expected run_capture_command(const std::string& c return r.output; } +std::filesystem::path metadata_path(const std::filesystem::path& cacheDir) { + return cacheDir / "std-module.json"; +} + +nlohmann::json metadata_for(const Toolchain& tc, + std::string_view cppStandard, + std::string_view cppStandardFlag, + const std::vector& stdCommands, + const std::vector& compatCommands) { + nlohmann::json j; + j["schema"] = 1; + j["compiler"] = std::string(tc.compiler_name()); + j["compiler_version"] = tc.version; + j["driver_identity"] = tc.driverIdent.empty() + ? (tc.binaryPath.empty() ? "" : hash_file(tc.binaryPath)) + : hash_string(tc.driverIdent); + j["target_triple"] = tc.targetTriple; + j["stdlib"] = tc.stdlibId; + j["stdlib_version"] = tc.stdlibVersion; + j["cpp_standard"] = std::string(cppStandard); + j["std_flag"] = std::string(cppStandardFlag); + j["std_module_source"] = tc.stdModuleSource.generic_string(); + j["std_module_source_hash"] = hash_file(tc.stdModuleSource); + j["std_compat_source"] = tc.stdCompatSource.generic_string(); + j["std_compat_source_hash"] = tc.stdCompatSource.empty() ? "" : hash_file(tc.stdCompatSource); + j["std_build_commands"] = stdCommands; + j["std_compat_build_commands"] = compatCommands; + return j; +} + +bool metadata_matches(const std::filesystem::path& path, const nlohmann::json& expected) { + std::ifstream is(path); + if (!is) return false; + nlohmann::json actual; + try { + is >> actual; + } catch (...) { + return false; + } + static constexpr std::array keys = { + "schema", + "compiler", + "compiler_version", + "driver_identity", + "target_triple", + "stdlib", + "stdlib_version", + "cpp_standard", + "std_flag", + "std_module_source", + "std_module_source_hash", + "std_compat_source", + "std_compat_source_hash", + "std_build_commands", + }; + for (auto key : keys) { + auto k = std::string(key); + if (!actual.contains(k) || actual[k] != expected[k]) return false; + } + return actual.value("std_compat_build_commands", nlohmann::json::array()) + == expected["std_compat_build_commands"]; +} + +std::expected write_metadata(const std::filesystem::path& path, + const nlohmann::json& metadata) { + std::ofstream os(path, std::ios::binary); + if (!os) { + return std::unexpected(StdModError{ + std::format("cannot write std module metadata '{}'", path.string())}); + } + os << metadata.dump(2) << "\n"; + if (!os) { + return std::unexpected(StdModError{ + std::format("failed while writing std module metadata '{}'", path.string())}); + } + return {}; +} + +std::expected run_commands(const std::vector& commands) { + std::string out; + for (auto const& cmd : commands) { + if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); + else out += *r; + } + return out; +} + } // namespace std::filesystem::path default_cache_root() { @@ -78,6 +169,8 @@ std::filesystem::path default_cache_root() { std::expected ensure_built( const Toolchain& tc, std::string_view fingerprint_hex, + std::string_view cpp_standard, + std::string_view cpp_standard_flag, const std::filesystem::path& cache_root) { if (tc.stdModuleSource.empty()) { @@ -146,7 +239,26 @@ std::expected ensure_built( sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); } - bool std_cached = std::filesystem::exists(sm.bmiPath) && std::filesystem::exists(sm.objectPath); + std::vector stdCommands; + if (is_clang(tc)) { + stdCommands = mcpp::toolchain::clang::std_module_build_commands( + tc, sm.cacheDir, sm.bmiPath, sysroot_flag, cpp_standard_flag); + } else { + stdCommands.push_back(mcpp::toolchain::gcc::std_module_build_command( + tc, sm.cacheDir, sysroot_flag, cpp_standard_flag)); + } + std::vector compatCommands; + if (is_clang(tc) && !tc.stdCompatSource.empty()) { + auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); + compatCommands = mcpp::toolchain::clang::std_compat_build_commands( + tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag, cpp_standard_flag); + } + auto metadata = metadata_for(tc, cpp_standard, cpp_standard_flag, stdCommands, compatCommands); + auto metaPath = metadata_path(sm.cacheDir); + bool std_cached = std::filesystem::exists(sm.bmiPath) + && std::filesystem::exists(sm.objectPath) + && metadata_matches(metaPath, metadata); + bool rebuiltStd = false; if (!std_cached) { std::error_code ec; @@ -154,43 +266,34 @@ std::expected ensure_built( if (ec) return std::unexpected(StdModError{ std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())}); - std::string out; - - if (is_clang(tc)) { - for (auto& cmd : mcpp::toolchain::clang::std_module_build_commands( - tc, sm.cacheDir, sm.bmiPath, sysroot_flag)) { - if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); - else out += *r; - } - } else { - auto cmd = mcpp::toolchain::gcc::std_module_build_command( - tc, sm.cacheDir, sysroot_flag); - if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); - else out += *r; - } + auto out = run_commands(stdCommands); + if (!out) return std::unexpected(out.error()); if (!std::filesystem::exists(sm.bmiPath)) { return std::unexpected(StdModError{ std::format("expected BMI at '{}' but it wasn't produced; output:\n{}", - sm.bmiPath.string(), out)}); + sm.bmiPath.string(), *out)}); } + rebuiltStd = true; } - // Build std.compat after std (std.compat depends on std, Clang only) + // Build std.compat after std (std.compat depends on std, Clang only). if (is_clang(tc) && !tc.stdCompatSource.empty()) { auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); - if (!std::filesystem::exists(compatBmi)) { - std::string out; - for (auto& cmd : mcpp::toolchain::clang::std_compat_build_commands( - tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag)) { - if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); - else out += *r; + if (rebuiltStd || !std::filesystem::exists(compatBmi) + || !metadata_matches(metaPath, metadata)) { + if (auto out = run_commands(compatCommands); !out) { + return std::unexpected(out.error()); } } sm.compatBmiPath = compatBmi; sm.compatObjectPath = sm.cacheDir / "std.compat.o"; } + if (auto r = write_metadata(metaPath, metadata); !r) { + return std::unexpected(r.error()); + } + return sm; } diff --git a/tests/e2e/26_c_language_support.sh b/tests/e2e/26_c_language_support.sh index eb20a02..8668496 100755 --- a/tests/e2e/26_c_language_support.sh +++ b/tests/e2e/26_c_language_support.sh @@ -45,8 +45,8 @@ int main() { } EOF -# Add user-supplied cflags/cxxflags so we also exercise the flag-forwarding -# path through the manifest into the per-rule baselines. +# Add user-supplied cflags/cxxflags so we also exercise the package-owned +# per-unit flag-forwarding path through the manifest. cat > mcpp.toml <<'EOF' [package] name = "cmix" @@ -67,10 +67,14 @@ grep -q '^rule c_object' "$ninja_file" || { cat "$ninja_file"; echo "missing c_o # cc / cflags must be defined and pick a C compiler (gcc / cc / clang). grep -qE '^cc = .*(gcc|cc|clang)' "$ninja_file" || { echo "cc variable not pointing at a C compiler"; exit 1; } -grep -qE '^cflags = -std=c11.*-DCMIX_C_BUILD=1' "$ninja_file" || { - echo "cflags missing -std=c11 or user cflags tail"; exit 1; } -grep -qE '^cxxflags = -std=c\+\+23.*-DCMIX_CXX_BUILD=1' "$ninja_file" || { - echo "cxxflags missing -std=c++23 or user cxxflags tail"; exit 1; } +grep -qE '^cflags = -std=c11' "$ninja_file" || { + echo "cflags missing -std=c11 baseline"; exit 1; } +grep -qE '^cxxflags = -std=c\+\+23' "$ninja_file" || { + echo "cxxflags missing -std=c++23 baseline"; exit 1; } +grep -q -- 'unit_cflags = -DCMIX_C_BUILD=1' "$ninja_file" || { + echo "unit cflags missing user C flag"; exit 1; } +grep -q -- 'unit_cxxflags = -DCMIX_CXX_BUILD=1' "$ninja_file" || { + echo "unit cxxflags missing user C++ flag"; exit 1; } # The .c source must be routed through c_object (not cxx_object). grep -qE 'build obj/cmix_core\.o : c_object .*cmix_core\.c' "$ninja_file" || { echo "cmix_core.c not routed to c_object rule"; exit 1; } diff --git a/tests/e2e/29_toolchain_partial_versions.sh b/tests/e2e/29_toolchain_partial_versions.sh index 5a8b5d3..4e84853 100755 --- a/tests/e2e/29_toolchain_partial_versions.sh +++ b/tests/e2e/29_toolchain_partial_versions.sh @@ -50,10 +50,13 @@ grep -q 'gcc@16.1.0' "$TMP/def2.log" || { cat "$TMP/def2.log"; echo "default 'gcc@16' didn't resolve to 16.1.0"; exit 1; } # ─── Section 2: first-run auto-install ────────────────────────────────── -# Brand-new MCPP_HOME, brand-new package with no [toolchain] declared — -# `mcpp build` should auto-install the canonical default (musl-gcc 15.1 -# for portable static binaries) + use it. Output should be a static ELF. +# Brand-new MCPP_HOME with no config/default state, brand-new package with no +# [toolchain] declared — `mcpp build` should auto-install the canonical +# default (musl-gcc 15.1 for portable static binaries) + use it. We still +# inherit payloads so CI does not download the same large archives into a +# throw-away home. export MCPP_HOME="$TMP/h2" +inherit_payloads_only configure_e2e_mirror mkdir -p "$TMP/proj" cd "$TMP/proj" diff --git a/tests/e2e/31_transitive_deps.sh b/tests/e2e/31_transitive_deps.sh index f11a7e0..ac05767 100755 --- a/tests/e2e/31_transitive_deps.sh +++ b/tests/e2e/31_transitive_deps.sh @@ -10,6 +10,14 @@ set -e TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT export MCPP_HOME="$TMP/mcpp-home" +MCPP_INHERIT_CONFIG=0 MCPP_INHERIT_SUBOS=0 source "$(dirname "$0")/_inherit_toolchain.sh" + +MUSL_PAYLOAD="$MCPP_HOME/registry/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-linux-musl-g++" +if [[ ! -x "$MUSL_PAYLOAD" ]]; then + echo "SKIP: no reusable musl-gcc xpkg payload" + echo "OK" + exit 0 +fi # ── 1. Grandchild: a header-providing C lib whose `[build].include_dirs` # is what consumers care about. Plays the role of mbedtls in the diff --git a/tests/e2e/59_cpp_standard_config.sh b/tests/e2e/59_cpp_standard_config.sh new file mode 100644 index 0000000..6eacd02 --- /dev/null +++ b/tests/e2e/59_cpp_standard_config.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# requires: gcc +# C++ standard config: [package].standard drives build flags, CDB, and std BMI. +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 = "cpp26_std" +version = "0.1.0" +standard = "c++26" +EOF + +cat > src/main.cpp <<'EOF' +import std; + +int main() { + std::println("cpp standard {}", 26); + return 0; +} +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: C++26 import-std build failed" + exit 1 +} + +binary=$(find target -type f -path '*/bin/cpp26_std' | head -1) +[[ -n "$binary" && -x "$binary" ]] || { + find target -maxdepth 5 -type f + echo "FAIL: cpp26_std binary missing" + exit 1 +} + +out=$("$binary") +[[ "$out" == "cpp standard 26" ]] || { + echo "FAIL: wrong runtime output: $out" + exit 1 +} + +build_ninja="$(find target -name build.ninja | head -1)" +[[ -n "$build_ninja" ]] || { echo "FAIL: build.ninja missing"; exit 1; } + +grep -qE '^cxxflags = -std=c\+\+26' "$build_ninja" || { + echo "FAIL: build.ninja missing C++26 standard flag" + cat "$build_ninja" + exit 1 +} +if grep -q -- "-std=c++23" "$build_ninja"; then + echo "FAIL: build.ninja still contains C++23" + cat "$build_ninja" + exit 1 +fi + +grep -q '"-std=c++26"' compile_commands.json || { + echo "FAIL: compile_commands.json missing C++26 standard flag" + cat compile_commands.json + exit 1 +} +if grep -q -- "-std=c++23" compile_commands.json; then + echo "FAIL: compile_commands.json still contains C++23" + cat compile_commands.json + exit 1 +fi + +metadata="$(find "$MCPP_HOME/bmi" -name std-module.json | head -1)" +[[ -n "$metadata" ]] || { echo "FAIL: std module metadata missing"; exit 1; } +grep -q '"cpp_standard": "c++26"' "$metadata" || { + echo "FAIL: std module metadata missing C++26 standard" + cat "$metadata" + exit 1 +} +grep -q '"std_flag": "-std=c++26"' "$metadata" || { + echo "FAIL: std module metadata missing C++26 flag" + cat "$metadata" + exit 1 +} + +rm -rf target compile_commands.json +MCPP_SCANNER=p1689 "$MCPP" build --no-cache > "$TMP/build-p1689.log" 2>&1 || { + cat "$TMP/build-p1689.log" + echo "FAIL: C++26 P1689 scanner build failed" + exit 1 +} +p1689_ninja="$(find target -name build.ninja | head -1)" +grep -qE '^cxxflags = -std=c\+\+26' "$p1689_ninja" || { + echo "FAIL: P1689 build.ninja missing C++26 standard flag" + cat "$p1689_ninja" + exit 1 +} +if grep -q -- "-std=c++23" "$TMP/build-p1689.log" "$p1689_ninja" compile_commands.json; then + echo "FAIL: P1689 path still contains C++23" + cat "$TMP/build-p1689.log" + cat "$p1689_ninja" + cat compile_commands.json + exit 1 +fi + +echo "OK" diff --git a/tests/e2e/_inherit_toolchain.sh b/tests/e2e/_inherit_toolchain.sh index e870433..8d7a4e3 100644 --- a/tests/e2e/_inherit_toolchain.sh +++ b/tests/e2e/_inherit_toolchain.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Tests that override $MCPP_HOME for isolation (fresh BMI cache / git cache / -# etc.) but still need a working toolchain should source this. It symlinks -# the user's installed toolchains and copies the config so the default +# etc.) but still need a working toolchain should source this. It links the +# user's installed toolchain payloads and copies the config so the default # toolchain pin resolves. # # Usage: source "$(dirname "$0")/_inherit_toolchain.sh" @@ -13,16 +13,36 @@ if [[ -z "${MCPP_HOME:-}" ]]; then fi mkdir -p "$MCPP_HOME" -# On Windows, HOME may differ from USERPROFILE; try both +# On Windows, HOME may differ from USERPROFILE; try both. USER_MCPP="${HOME}/.mcpp" if [[ ! -d "$USER_MCPP" && -n "${USERPROFILE:-}" ]]; then USER_MCPP="$USERPROFILE/.mcpp" fi -if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then + +link_xpkg_payloads() { + local source_dir="$1" + local target_dir="$MCPP_HOME/registry/data/xpkgs" + [[ -d "$source_dir" ]] || return 0 + mkdir -p "$target_dir" + + local entry base + shopt -s nullglob + for entry in "$source_dir"/*; do + base="$(basename "$entry")" + [[ -e "$target_dir/$base" ]] && continue + ln -sf "$entry" "$target_dir/$base" 2>/dev/null \ + || cp -r "$entry" "$target_dir/$base" + done + shopt -u nullglob +} + +if [[ -d "$USER_MCPP/registry/data/xpkgs" || -d "$HOME/.xlings/data/xpkgs" || ( -n "${USERPROFILE:-}" && -d "$USERPROFILE/.xlings/data/xpkgs" ) ]]; then mkdir -p "$MCPP_HOME/registry/data" - [[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \ - || ln -sf "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" 2>/dev/null \ - || cp -r "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" + link_xpkg_payloads "$USER_MCPP/registry/data/xpkgs" + link_xpkg_payloads "$HOME/.xlings/data/xpkgs" + if [[ -n "${USERPROFILE:-}" ]]; then + link_xpkg_payloads "$USERPROFILE/.xlings/data/xpkgs" + fi fi if [[ -d "$USER_MCPP/registry/data/xim-pkgindex" ]]; then mkdir -p "$MCPP_HOME/registry/data" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 81365bf..5d36739 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -28,6 +28,61 @@ main = "src/main.cpp" EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::Binary); } +TEST(Manifest, PackageStandardCpp26AcceptedAndMirrored) { + constexpr auto src = R"( +[package] +name = "hello26" +version = "0.1.0" +standard = "c++26" +[modules] +sources = ["src/**/*.cppm"] +[targets.hello26] +kind = "bin" +main = "src/main.cpp" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + EXPECT_EQ(m->package.standard, "c++26"); + EXPECT_EQ(m->language.standard, "c++26"); +} + +TEST(Manifest, LegacyLanguageCpp2cNormalizesToCpp26) { + constexpr auto src = R"( +[package] +name = "hello26" +version = "0.1.0" +[language] +standard = "c++2c" +[modules] +sources = ["src/**/*.cppm"] +[targets.hello26] +kind = "bin" +main = "src/main.cpp" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + EXPECT_EQ(m->package.standard, "c++26"); + EXPECT_EQ(m->language.standard, "c++26"); +} + +TEST(Manifest, RejectsStdFlagInCxxflags) { + auto m = mcpp::manifest::parse_string(R"( +[package] +name = "x" +version = "0.1.0" +[build] +cxxflags = ["-std=c++26"] +[modules] +sources = ["src/**/*.cppm"] +[targets.x] +kind = "bin" +main = "src/main.cpp" +)"); + ASSERT_FALSE(m.has_value()); + EXPECT_NE(m.error().message.find("[package].standard"), std::string::npos) + << m.error().message; +} + TEST(Manifest, RejectMissingVersion) { auto m = mcpp::manifest::parse_string(R"( [package] diff --git a/tests/unit/test_ninja_backend.cpp b/tests/unit/test_ninja_backend.cpp index d3b3210..01c2740 100644 --- a/tests/unit/test_ninja_backend.cpp +++ b/tests/unit/test_ninja_backend.cpp @@ -1,6 +1,8 @@ #include import std; +import mcpp.build.compile_commands; +import mcpp.build.flags; import mcpp.build.ninja; import mcpp.build.plan; import mcpp.manifest; @@ -10,6 +12,16 @@ using namespace mcpp::build; namespace { +std::size_t count_occurrences(std::string_view haystack, std::string_view needle) { + std::size_t count = 0; + std::size_t pos = 0; + while ((pos = haystack.find(needle, pos)) != std::string_view::npos) { + ++count; + pos += needle.size(); + } + return count; +} + BuildPlan minimal_plan() { BuildPlan plan; plan.projectRoot = "/tmp/mcpp-ninja-test"; @@ -46,3 +58,62 @@ TEST(NinjaBackend, ObjectiveCSourceUsesCObjectRuleAndCFlags) { EXPECT_EQ(ninja.find("unit_cxxflags = -DWRONG_CXX_FLAG=1"), std::string::npos) << ninja; } + +TEST(NinjaBackend, UsesPackageCppStandardForCxxFlags) { + auto plan = minimal_plan(); + plan.manifest.package.standard = "c++26"; + plan.manifest.language.standard = "c++26"; + plan.cppStandard = "c++26"; + plan.cppStandardFlag = "-std=c++26"; + plan.compileUnits.push_back({ + .source = "src/main.cpp", + .object = "obj/main.o", + .packageName = "cpp26_test", + }); + + auto ninja = emit_ninja_string(plan); + + EXPECT_NE(ninja.find("cxxflags = -std=c++26"), std::string::npos) + << ninja; + EXPECT_EQ(ninja.find("-std=c++23"), std::string::npos) + << ninja; +} + +TEST(NinjaBackend, CompileCommandsUsesSameCppStandard) { + auto plan = minimal_plan(); + plan.manifest.package.standard = "c++26"; + plan.manifest.language.standard = "c++26"; + plan.cppStandard = "c++26"; + plan.cppStandardFlag = "-std=c++26"; + plan.compileUnits.push_back({ + .source = "src/main.cpp", + .object = "obj/main.o", + .packageName = "cpp26_test", + }); + + auto flags = compute_flags(plan); + auto cdb = emit_compile_commands(plan, flags); + + EXPECT_NE(cdb.find("\"-std=c++26\""), std::string::npos) + << cdb; + EXPECT_EQ(cdb.find("\"-std=c++23\""), std::string::npos) + << cdb; +} + +TEST(NinjaBackend, RootPackageCxxflagsAreEmittedOncePerUnit) { + auto plan = minimal_plan(); + plan.manifest.buildConfig.cxxflags = {"-DROOT_FLAG=1"}; + plan.compileUnits.push_back({ + .source = "src/main.cpp", + .object = "obj/main.o", + .packageName = "root_flag_test", + .packageCxxflags = {"-DROOT_FLAG=1"}, + }); + + auto ninja = emit_ninja_string(plan); + + EXPECT_EQ(count_occurrences(ninja, "unit_cxxflags = -DROOT_FLAG=1"), 2u) + << ninja; + EXPECT_EQ(ninja.find("cxxflags = -std=c++23 -O2 -DROOT_FLAG=1"), std::string::npos) + << ninja; +} diff --git a/tests/unit/test_toolchain_stdmod.cpp b/tests/unit/test_toolchain_stdmod.cpp new file mode 100644 index 0000000..dc337b4 --- /dev/null +++ b/tests/unit/test_toolchain_stdmod.cpp @@ -0,0 +1,72 @@ +#include + +import std; +import mcpp.toolchain.clang; +import mcpp.toolchain.gcc; +import mcpp.toolchain.model; + +using namespace mcpp::toolchain; + +namespace { + +Toolchain gcc_toolchain() { + Toolchain tc; + tc.compiler = CompilerId::GCC; + tc.version = "16.1.0"; + tc.binaryPath = "g++"; + tc.targetTriple = "x86_64-linux-gnu"; + tc.stdlibId = "libstdc++"; + tc.stdlibVersion = "16.1.0"; + tc.stdModuleSource = "bits/std.cc"; + return tc; +} + +Toolchain clang_toolchain() { + Toolchain tc; + tc.compiler = CompilerId::Clang; + tc.version = "20.1.7"; + tc.binaryPath = "clang++"; + tc.targetTriple = "x86_64-linux-gnu"; + tc.stdlibId = "libc++"; + tc.stdlibVersion = "20.1.7"; + tc.stdModuleSource = "std.cppm"; + tc.stdCompatSource = "std.compat.cppm"; + return tc; +} + +} // namespace + +TEST(ToolchainStdmod, GccStdModuleCommandUsesRequestedStandard) { + auto cmd = gcc::std_module_build_command( + gcc_toolchain(), "cache", "", "-std=c++26"); + + EXPECT_NE(cmd.find("-std=c++26"), std::string::npos) << cmd; + EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; +} + +TEST(ToolchainStdmod, ClangStdModuleCommandsUseRequestedStandard) { + auto cmds = clang::std_module_build_commands( + clang_toolchain(), "cache", "cache/pcm.cache/std.pcm", "", "-std=c++26"); + + ASSERT_EQ(cmds.size(), 2u); + for (auto const& cmd : cmds) { + EXPECT_NE(cmd.find("-std=c++26"), std::string::npos) << cmd; + EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; + } +} + +TEST(ToolchainStdmod, ClangStdCompatCommandsUseRequestedStandard) { + auto cmds = clang::std_compat_build_commands( + clang_toolchain(), + "cache", + "cache/pcm.cache/std.compat.pcm", + "cache/pcm.cache/std.pcm", + "", + "-std=c++26"); + + ASSERT_EQ(cmds.size(), 2u); + for (auto const& cmd : cmds) { + EXPECT_NE(cmd.find("-std=c++26"), std::string::npos) << cmd; + EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; + } +}