diff --git a/.agents/docs/2026-05-15-clang-parity-and-toolchain-abstraction.md b/.agents/docs/2026-05-15-clang-parity-and-toolchain-abstraction.md new file mode 100644 index 0000000..3c690df --- /dev/null +++ b/.agents/docs/2026-05-15-clang-parity-and-toolchain-abstraction.md @@ -0,0 +1,465 @@ +# Clang 编译管线平权 + 工具链抽象层设计 + +> 2026-05-15 — 让 Linux Clang 达到与 GCC 同等的模块编译支持,并建立平台/工具链抽象为 macOS/Windows 铺路 +> 基于 mcpp 0.0.15 代码库分析 + +## 0. 一句话 + +当前 Clang 能跑 `import std` 的单文件项目,但**多模块项目的整条核心管线(dyndep、BMI 路径、缓存布局、编译规则)都是按 GCC 硬编码的**。本文设计一层 `ToolchainTraits` 抽象,让 GCC/Clang 共享管线骨架、各自提供参数,同时为未来 MSVC/macOS 留好接口。 + +## 1. 差距总表(12 个 Blocker) + +| # | 文件 | 行号 | 问题 | 类型 | +|---|---|---|---|---| +| G1 | `ninja_backend.cppm` | 129–130 | `dyndep` 被 `is_gcc()` 门控,Clang 直接回退到全量静态依赖 | Blocker | +| G2 | `ninja_backend.cppm` | 187–198 | `cxx_module` 规则缺少 Clang 必须的 `-fmodule-output=$bmi_out` | Blocker | +| G3 | `ninja_backend.cppm` | 234–239 | `cxx_scan` 规则传 `-fmodules` 给 Clang(GCC 专属旗标) | 中等 | +| G4 | `ninja_backend.cppm` | 260–265 | `bmi_path` lambda 硬编码 `gcm.cache/` + `.gcm` 扩展名 | Blocker | +| G5 | `flags.cppm` | — | 缺 Clang 必须的 `-fprebuilt-module-path=pcm.cache` | Blocker | +| G6 | `bmi_cache.cppm` | 41, 145, 230 | `gcmDir()` / `projectGcm` 硬编码 `gcm.cache/` | Blocker | +| G7 | `bmi_cache.cppm` | 47, 94–98, 110 | manifest 前缀 `gcm:` 不适用 Clang | Blocker | +| G8 | `dyndep.cppm` | 152–157 | `bmi_basename()` 硬编码 `.gcm` 扩展名 | Blocker | +| G9 | `dyndep.cppm` | 258, 307 | dyndep emit 硬编码 `gcm.cache/` 目录 | Blocker | +| G10 | `dyndep.cppm` | 289–313 | `emit_dyndep_*` 无工具链参数,无法区分 GCC/Clang | Blocker | +| G11 | `cli.cppm` | 3553–3573 | `cmd_dyndep` 子命令无工具链上下文传递给 dyndep 模块 | Blocker | +| G12 | `modgraph/p1689.cppm` | 342 | `scan_file` 对 Clang 仍传 `-fmodules` | 中等 | + +### 已修复(无需改动) + +| 功能 | 位置 | 说明 | +|---|---|---| +| `-fmodules` 跳过 Clang | `flags.cppm:127` | `isClang ? "" : " -fmodules"` | +| `-fmodule-file=std=` | `flags.cppm:129-131` | Clang 时注入 std pcm 路径 | +| `-static-libstdc++` 跳过 Clang | `flags.cppm:141` | `!isClang` 条件 | +| `-B` 跳过 Clang | `flags.cppm:93-100` | 条件过滤 | +| stdmod 双路径 | `stdmod.cppm:96-128` | GCC/Clang 分别使用各自的 std 构建命令 | +| std BMI 路径分支 | `clang.cppm:127-133` | `pcm.cache/std.pcm` | +| std 构建两步流程 | `clang.cppm:135-159` | `--precompile` + `-c` | +| Clang 检测 | `clang.cppm:64-69` | 识别 `clang version` 和 `apple clang version` | + +## 2. 核心设计:`ToolchainTraits` + +### 2.1 设计目标 + +1. GCC/Clang **共享同一套** ninja 规则生成、dyndep 发射、BMI 缓存管线骨架 +2. 差异点通过统一接口 **参数化**,不在调用侧 `if/else` +3. 接口为将来的 MSVC / Apple Clang 预留扩展点 + +### 2.2 接口定义 + +在 `src/toolchain/model.cppm` 新增: + +```cpp +// BMI layout traits — 编译器间差异的单一聚合点 +struct BmiTraits { + std::string_view bmiDir; // "gcm.cache" | "pcm.cache" + std::string_view bmiExt; // ".gcm" | ".pcm" + std::string_view manifestPrefix; // "gcm" | "pcm" + + // 编译 module interface unit 时,是否需要显式 -fmodule-output= + bool needsExplicitModuleOutput = false; + + // 是否需要 -fprebuilt-module-path= + bool needsPrebuiltModulePath = false; + + // P1689 扫描时是否需要 -fmodules 标志 + bool scanNeedsFModules = true; +}; + +BmiTraits bmi_traits(const Toolchain& tc); +``` + +实现(可在 `model.cppm` 底部): + +```cpp +BmiTraits bmi_traits(const Toolchain& tc) { + if (is_clang(tc)) { + return { + .bmiDir = "pcm.cache", + .bmiExt = ".pcm", + .manifestPrefix = "pcm", + .needsExplicitModuleOutput = true, + .needsPrebuiltModulePath = true, + .scanNeedsFModules = false, + }; + } + // GCC (default) + return { + .bmiDir = "gcm.cache", + .bmiExt = ".gcm", + .manifestPrefix = "gcm", + .needsExplicitModuleOutput = false, + .needsPrebuiltModulePath = false, + .scanNeedsFModules = true, + }; +} +``` + +### 2.3 为什么不用继承/虚函数 + +`BmiTraits` 是 **值类型聚合**,因为: +- 消费者(ninja_backend、dyndep、bmi_cache)只需要几个字符串/布尔值; +- 不需要多态行为,避免虚表开销; +- 未来 MSVC 只需 return 第三组值,无需新类; +- 所有差异在编译时可知,仅 `model.cppm` 一处 switch。 + +## 3. 逐模块改造方案 + +### 3.1 `ninja_backend.cppm` — Ninja 规则生成 + +#### 3.1.1 放开 dyndep 门控(G1) + +```cpp +// Before (line 129-130) +bool dyndep = dyndep_mode_enabled() + && mcpp::toolchain::is_gcc(plan.toolchain); + +// After +bool dyndep = dyndep_mode_enabled(); +// dyndep 支持 GCC 16+ 和 Clang 18+,两者都实现了 P1689 +// MSVC 暂不支持 → 未来在此加 && !is_msvc() +``` + +#### 3.1.2 参数化 `bmi_path` lambda(G4) + +```cpp +auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); +auto bmi_path = [&traits](std::string_view name) { + std::string s(traits.bmiDir); + s += '/'; + for (char c : name) s.push_back(c == ':' ? '-' : c); + s += traits.bmiExt; + return s; +}; +``` + +#### 3.1.3 分离 GCC/Clang 的 `cxx_module` 规则(G2) + +思路:不拆成两个 rule,而是在 rule command 中用 `$module_output_flag` 变量: + +```cpp +// Clang 需要 -fmodule-output=$bmi_out,GCC 不需要(隐式写 gcm.cache/) +std::string module_output_in_rule = traits.needsExplicitModuleOutput + ? " -fmodule-output=$bmi_out" : ""; + +append("rule cxx_module\n"); +append(std::format( + " command = " + "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then " + "cp -p \"$bmi_out\" \"$bmi_out.bak\"; " + "fi && " + "$toolenv $cxx $cxxflags{} -c $in -o $out && " + "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && " + "cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then " + "mv \"$bmi_out.bak\" \"$bmi_out\"; " + "else " + "rm -f \"$bmi_out.bak\"; " + "fi\n", + module_output_in_rule)); +``` + +#### 3.1.4 `cxx_scan` 规则条件化 `-fmodules`(G3) + +```cpp +std::string scan_modules_flag = traits.scanNeedsFModules ? "-fmodules " : ""; +append(std::format( + "rule cxx_scan\n" + " command = $toolenv $cxx $cxxflags {}-fdeps-format=p1689r5 " + "-fdeps-file=$out -fdeps-target=$compile_target " + "-M -MM -MF $out.dep -E $in -o $compile_target\n" + " description = SCAN $out\n\n", + scan_modules_flag)); +``` + +### 3.2 `flags.cppm` — 编译 flag 生成 + +#### 3.2.1 新增 `-fprebuilt-module-path`(G5) + +```cpp +auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); +std::string prebuilt_module_flag; +if (traits.needsPrebuiltModulePath) { + prebuilt_module_flag = std::format(" -fprebuilt-module-path={}", traits.bmiDir); +} + +// 拼入 cxxflags: +f.cxx = std::format("-std={}{}{}{}{}{}{}{}{}", ... + prebuilt_module_flag, ...); +``` + +### 3.3 `dyndep.cppm` — 动态依赖发射 + +#### 3.3.1 `bmi_basename` 参数化(G8) + +```cpp +// Before +std::string bmi_basename(std::string_view logicalName) { + ... + out += ".gcm"; + return out; +} + +// After +std::string bmi_basename(std::string_view logicalName, + std::string_view ext = ".gcm") { + std::string out; + out.reserve(logicalName.size() + ext.size()); + for (char c : logicalName) out.push_back(c == ':' ? '-' : c); + out += ext; + return out; +} +``` + +#### 3.3.2 `emit_dyndep_single` / `emit_dyndep_from_files` 接受 traits(G9, G10) + +```cpp +// Before (函数签名) +std::string emit_dyndep_single(const std::filesystem::path& ddi); +std::string emit_dyndep_from_files(std::span ddi_paths, ...); + +// After +struct DyndepOptions { + std::string_view bmiDir = "gcm.cache"; // default GCC + std::string_view bmiExt = ".gcm"; +}; +std::string emit_dyndep_single(const std::filesystem::path& ddi, + const DyndepOptions& opts = {}); +std::string emit_dyndep_from_files(std::span ddi_paths, + ..., + const DyndepOptions& opts = {}); +``` + +内部所有 `"gcm.cache/"` 和 `.gcm` 替换为 `opts.bmiDir` / `opts.bmiExt`。 + +#### 3.3.3 `cmd_dyndep` 传递工具链信息(G11, G12) + +**问题**:`cmd_dyndep` 是 Ninja 在构建时调用的子命令(`mcpp dyndep --single`),运行在 ninja 进程内,**不经过 prepare_build**,无法访问 `BuildPlan` 对象。 + +**方案**:通过命令行参数 `--bmi-dir` / `--bmi-ext` 传入: + +Ninja 规则侧(`ninja_backend.cppm` 中 `cxx_dyndep` 规则): + +```cpp +// Before +append("rule cxx_dyndep\n"); +append(" command = $mcpp dyndep --single --output $out $in\n"); + +// After +append(std::format( + "rule cxx_dyndep\n" + " command = $mcpp dyndep --single --bmi-dir {} --bmi-ext {} --output $out $in\n", + traits.bmiDir, traits.bmiExt)); +``` + +`cli.cppm` 的 `cmd_dyndep` 解析这两个新参数并构造 `DyndepOptions`: + +```cpp +int cmd_dyndep(const ParsedArgs& parsed) { + DyndepOptions opts; + if (auto d = parsed.value("bmi-dir")) opts.bmiDir = *d; + if (auto e = parsed.value("bmi-ext")) opts.bmiExt = *e; + ... + body = emit_dyndep_single(ddi, opts); +} +``` + +### 3.4 `bmi_cache.cppm` — 跨项目 BMI 缓存 + +#### 3.4.1 `CacheKey` 感知 BMI 布局(G6, G7) + +```cpp +struct CacheKey { + ...existing fields... + std::string bmiDirName = "gcm.cache"; // "gcm.cache" | "pcm.cache" + std::string manifestTag = "gcm"; // "gcm" | "pcm" + + std::filesystem::path bmiDir() const { return dir() / bmiDirName; } + // 原来的 gcmDir() 改为 bmiDir() +}; +``` + +#### 3.4.2 Manifest 格式升级 + +```cpp +// serialize_manifest:使用 key.manifestTag +std::string serialize_manifest(const CacheKey& key, const DepArtifacts& a) { + std::string out = "# Auto-generated by mcpp bmi_cache. Do not edit.\n"; + for (auto& g : a.bmiFiles) out += std::format("{}: {}\n", key.manifestTag, g); + for (auto& o : a.objFiles) out += std::format("obj: {}\n", o); + return out; +} + +// parse_manifest:同时接受 "gcm:" 和 "pcm:" 前缀 +std::expected parse_manifest(...) { + ... + if (line.starts_with("gcm: ")) a.bmiFiles.push_back(line.substr(5)); + else if (line.starts_with("pcm: ")) a.bmiFiles.push_back(line.substr(5)); + else if (line.starts_with("obj: ")) a.objFiles.push_back(line.substr(5)); +} +``` + +`DepArtifacts::gcmFiles` 重命名为 `bmiFiles`(语义中性化)。 + +#### 3.4.3 `stage_into` / `populate_from` 使用 `bmiDir()` + +全部 `projectGcm` 替换为 `projectBmi`,由 `projectTargetDir / key.bmiDirName` 生成。 + +### 3.5 `modgraph/p1689.cppm` — 扫描命令(G12) + +```cpp +// scan_file 中的 -fmodules 条件化 +std::string modules_flag = mcpp::toolchain::is_clang(tc) ? "" : " -fmodules"; +std::string cmd = std::format( + "{} -std=c++23{}{} -fdeps-format=p1689r5 ...", + shq(tc.binaryPath.string()), modules_flag, sysroot_flag, ...); +``` + +## 4. 目录布局变化 + +### 4.1 构建目录 + +``` +target/// +├── build.ninja +├── gcm.cache/ ← GCC: BMI 文件 (.gcm) +│ ├── std.gcm +│ └── myapp.greet.gcm +├── pcm.cache/ ← Clang: BMI 文件 (.pcm) +│ ├── std.pcm +│ └── myapp.greet.pcm +├── obj/ +│ ├── greet.m.o +│ └── main.o +└── bin/ + └── myapp +``` + +GCC 和 Clang 的构建**各自使用不同的 fingerprint 目录**(因为编译器不同 → fingerprint 不同),所以 `gcm.cache/` 和 `pcm.cache/` 不会在同一个目录下共存。但用中性化命名可以让代码统一: + +**方案 A**(推荐):保留各自命名 `gcm.cache` / `pcm.cache`,通过 `BmiTraits.bmiDir` 参数化。好处:与 GCC/Clang 的约定一致,开发者能一眼看出工具链。 + +**方案 B**:统一用 `bmi/` 目录。好处:代码更简单。坏处:丢失工具链信息,且 GCC 硬编码寻找 `gcm.cache/`(需要 `-fmodule-mapper` 覆盖)。 + +**选择方案 A**。 + +### 4.2 BMI 缓存目录 + +``` +$MCPP_HOME/bmi//deps//@/ +├── gcm.cache/ 或 pcm.cache/ ← 由 CacheKey.bmiDirName 决定 +│ └── *.gcm 或 *.pcm +├── obj/ +│ └── *.m.o +└── manifest.txt ← "gcm: ..." 或 "pcm: ..." +``` + +## 5. 实施计划 + +### Phase 1:核心管线(1 个 PR,必须) + +| 步骤 | 改动文件 | 内容 | +|---|---|---| +| 1a | `model.cppm` | 新增 `BmiTraits` + `bmi_traits()` | +| 1b | `ninja_backend.cppm` | 放开 dyndep 门控;`bmi_path` 参数化;`cxx_module` 加 `-fmodule-output`;`cxx_scan` 条件化 `-fmodules`;`cxx_dyndep` 规则传 `--bmi-dir --bmi-ext` | +| 1c | `flags.cppm` | 加 `-fprebuilt-module-path=` | +| 1d | `dyndep.cppm` | `bmi_basename(name, ext)` 参数化;`emit_dyndep_*` 接受 `DyndepOptions`;硬编码路径替换 | +| 1e | `cli.cppm` | `cmd_dyndep` 解析 `--bmi-dir`/`--bmi-ext` | +| 1f | `modgraph/p1689.cppm` | `scan_file` 条件化 `-fmodules` | + +### Phase 2:BMI 缓存(1 个 PR) + +| 步骤 | 改动文件 | 内容 | +|---|---|---| +| 2a | `bmi_cache.cppm` | `gcmDir()` → `bmiDir()`;`gcmFiles` → `bmiFiles`;manifest 双前缀兼容 | +| 2b | `cli.cppm` | `prepare_build` 中构造 `CacheKey` 时设置 `bmiDirName` / `manifestTag` | + +### Phase 3:E2E 验证(1 个 PR) + +| 步骤 | 改动文件 | 内容 | +|---|---|---| +| 3a | `tests/e2e/38_llvm_modules.sh` | 多模块 Clang 项目:`greet.cppm` + `main.cpp`(`import myapp.greet`),验证编译+运行 | +| 3b | `tests/e2e/39_llvm_incremental.sh` | Touch 单个 .cppm → 只重编该文件(增量验证) | +| 3c | `tests/e2e/40_llvm_bmi_cache.sh` | 依赖包的 BMI 缓存命中(Clang 路径) | + +### Phase 4:平台抽象预留(后续) + +在 `BmiTraits` 基础上,未来只需: +- macOS Apple Clang → 沿用 `is_clang` 分支(BMI 格式相同) +- Windows MSVC → 新增 `is_msvc` 分支,返回 `.ifc` 扩展名 + MSVC 专有 flag 集 + +## 6. GCC vs Clang 编译命令对比速查 + +### P1689 扫描 + +```bash +# GCC +g++ -std=c++23 -fmodules \ + -fdeps-format=p1689r5 -fdeps-file=foo.ddi -fdeps-target=foo.o \ + -M -MM -MF foo.dep -E src/foo.cppm -o foo.o + +# Clang (相同标志集, 去掉 -fmodules) +clang++ -std=c++23 \ + -fdeps-format=p1689r5 -fdeps-file=foo.ddi -fdeps-target=foo.o \ + -M -MM -MF foo.dep -E src/foo.cppm -o /dev/null +``` + +### 模块编译 + +```bash +# GCC — 隐式写 gcm.cache/myapp.greet.gcm +g++ -std=c++23 -fmodules -c src/greet.cppm -o obj/greet.m.o + +# Clang — 显式指定 pcm 输出 +clang++ -std=c++23 \ + -fprebuilt-module-path=pcm.cache \ + -fmodule-output=pcm.cache/myapp.greet.pcm \ + -c src/greet.cppm -o obj/greet.m.o +``` + +### 消费模块 + +```bash +# GCC — 自动从 cwd/gcm.cache/ 查找 +g++ -std=c++23 -fmodules -c src/main.cpp -o obj/main.o + +# Clang — 需要 -fprebuilt-module-path +clang++ -std=c++23 \ + -fprebuilt-module-path=pcm.cache \ + -c src/main.cpp -o obj/main.o +``` + +### std 模块预编译 + +```bash +# GCC — 单步,隐式产出 gcm.cache/std.gcm + std.o +g++ -std=c++23 -fmodules -c bits/std.cc -o std.o + +# Clang — 两步 +clang++ -std=c++23 --precompile std.cppm -o pcm.cache/std.pcm +clang++ -std=c++23 pcm.cache/std.pcm -c -o std.o +``` + +## 7. 验收指标 + +- [ ] `mcpp new hello && cd hello && mcpp build` 在 `[toolchain] default = "llvm@20"` 下成功 +- [ ] 多模块项目(`greet.cppm` + `main.cpp`)在 Clang 下编译+运行正确 +- [ ] `touch greet.cppm && mcpp build` 只重编 `greet` + link(dyndep 增量) +- [ ] `mcpp test` 在 Clang 工具链下通过 +- [ ] BMI 缓存命中:第二次 build 显示 `Cached` 而非 `Compiling` +- [ ] 所有现有 GCC E2E 测试继续通过(无回归) +- [ ] `--print-fingerprint` 对 GCC 和 Clang 产出不同的 fingerprint + +## 8. 不应该做的事 + +1. **不要引入虚函数/继承**。`BmiTraits` 作为值类型聚合足够,避免 vtable 开销和继承层次膨胀。 +2. **不要统一目录名为 `bmi/`**。GCC 硬编码寻找 `gcm.cache/`,除非用 `-fmodule-mapper` 覆盖(增加复杂度)。 +3. **不要在 flags.cppm 里用 `if(isGCC) ... else if(isClang) ...`**。用 `BmiTraits` 的布尔字段驱动,消费者不感知编译器类型。 +4. **不要合并 GCC 和 Clang 的 std 模块构建流程**。两者本质不同(单步 vs 两步),`stdmod.cppm` 的分支是正确的。 +5. **不要在这个 PR 里动 macOS/Windows 的事**。只做 Linux Clang 平权 + 抽象接口预留。 + +## 9. 关联 + +- 前置 PR:mcpp-community/mcpp#33(fingerprint 稳定化,已合入) +- 上一份分析:`2026-05-15-fingerprint-stability-and-fastpath-coherence.md` +- LLVM 设计文档:`2026-05-13-llvm-clang-toolchain-support-design.md` +- 跨平台分析报告:见本会话聊天记录(macOS 6 个 blocker、Windows 10+ 个 blocker) diff --git a/.agents/docs/README.md b/.agents/docs/README.md new file mode 100644 index 0000000..839f736 --- /dev/null +++ b/.agents/docs/README.md @@ -0,0 +1 @@ +# 开发/方案文档目录 \ No newline at end of file diff --git a/src/bmi_cache.cppm b/src/bmi_cache.cppm index a466bfe..9be6ce0 100644 --- a/src/bmi_cache.cppm +++ b/src/bmi_cache.cppm @@ -2,7 +2,7 @@ // // Layout (per docs/26-bmi-cache.md): // $MCPP_HOME/bmi//deps//@/ -// gcm.cache/.gcm +// {gcm,pcm}.cache/.{gcm,pcm} // obj/.m.o + .o // manifest.txt (sentinel + file list) // @@ -31,6 +31,8 @@ struct CacheKey { std::string indexName; // "mcpplibs" / "xim" / ... std::string packageName; // "mcpplibs.cmdline" std::string version; // "0.0.1" + std::string bmiDirName = "gcm.cache"; // "gcm.cache" | "pcm.cache" + std::string manifestTag = "gcm"; // "gcm" | "pcm" std::filesystem::path dir() const { return mcppHome / "bmi" / fingerprint / "deps" @@ -38,13 +40,13 @@ struct CacheKey { } std::filesystem::path manifestFile() const { return dir() / "manifest.txt"; } - std::filesystem::path gcmDir() const { return dir() / "gcm.cache"; } + std::filesystem::path bmiDir() const { return dir() / bmiDirName; } std::filesystem::path objDir() const { return dir() / "obj"; } }; // File names (basename only) that belong to one dep package's cache entry. struct DepArtifacts { - std::vector gcmFiles; // basenames in gcm.cache/ + std::vector bmiFiles; // basenames in bmiDir/ std::vector objFiles; // basenames in obj/ }; @@ -55,14 +57,14 @@ bool is_cached(const CacheKey& key); std::expected read_manifest(const CacheKey& key); -// Copy missing cached files into projectTarget/{gcm.cache,obj}. Existing -// project outputs are left untouched: GCC BMIs may differ byte-for-byte between +// Copy missing cached files into projectTarget/{bmiDirName,obj}. Existing +// project outputs are left untouched: BMIs may differ byte-for-byte between // equivalent builds, and overwriting them would dirty downstream modules. std::expected stage_into(const CacheKey& key, const std::filesystem::path& projectTargetDir); -// Copy fresh build outputs from projectTarget/{gcm.cache,obj} → cache dir +// Copy fresh build outputs from projectTarget/{bmiDirName,obj} → cache dir // and write manifest.txt last (atomic-ish sentinel). std::expected populate_from(const CacheKey& key, @@ -91,9 +93,9 @@ bool copy_one(const std::filesystem::path& from, return !ec; } -std::string serialize_manifest(const DepArtifacts& a) { +std::string serialize_manifest(std::string_view tag, const DepArtifacts& a) { std::string out = "# Auto-generated by mcpp bmi_cache. Do not edit.\n"; - for (auto& g : a.gcmFiles) out += std::format("gcm: {}\n", g); + for (auto& g : a.bmiFiles) out += std::format("{}: {}\n", tag, g); for (auto& o : a.objFiles) out += std::format("obj: {}\n", o); return out; } @@ -107,7 +109,8 @@ parse_manifest(const std::filesystem::path& p) { while (std::getline(is, line)) { while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); if (line.empty() || line[0] == '#') continue; - if (line.starts_with("gcm: ")) a.gcmFiles.push_back(line.substr(5)); + if (line.starts_with("gcm: ")) a.bmiFiles.push_back(line.substr(5)); + else if (line.starts_with("pcm: ")) a.bmiFiles.push_back(line.substr(5)); else if (line.starts_with("obj: ")) a.objFiles.push_back(line.substr(5)); } return a; @@ -121,8 +124,8 @@ bool is_cached(const CacheKey& key) { auto arts = parse_manifest(mf); if (!arts) return false; // Verify every listed file actually exists on disk. - for (auto& g : arts->gcmFiles) { - if (!std::filesystem::exists(key.gcmDir() / g)) return false; + for (auto& g : arts->bmiFiles) { + if (!std::filesystem::exists(key.bmiDir() / g)) return false; } for (auto& o : arts->objFiles) { if (!std::filesystem::exists(key.objDir() / o)) return false; @@ -142,15 +145,15 @@ stage_into(const CacheKey& key, auto arts = parse_manifest(key.manifestFile()); if (!arts) return std::unexpected(arts.error()); - auto projectGcm = projectTargetDir / "gcm.cache"; + auto projectBmi = projectTargetDir / key.bmiDirName; auto projectObj = projectTargetDir / "obj"; std::error_code ec; - std::filesystem::create_directories(projectGcm, ec); + std::filesystem::create_directories(projectBmi, ec); std::filesystem::create_directories(projectObj, ec); - for (auto& g : arts->gcmFiles) { - auto from = key.gcmDir() / g; - auto to = projectGcm / g; + for (auto& g : arts->bmiFiles) { + auto from = key.bmiDir() / g; + auto to = projectBmi / g; if (std::filesystem::exists(to, ec)) { ec.clear(); continue; @@ -158,7 +161,7 @@ stage_into(const CacheKey& key, ec.clear(); if (!copy_one(from, to, ec)) { return std::unexpected(std::format( - "stage gcm '{}': {}", g, ec.message())); + "stage bmi '{}': {}", g, ec.message())); } touch_now(to); } @@ -221,25 +224,25 @@ populate_from(const CacheKey& key, ~LockGuard() { release_lock(fd); } } guard{ lockFd }; - auto cacheGcm = key.gcmDir(); + auto cacheBmi = key.bmiDir(); auto cacheObj = key.objDir(); std::error_code ec; - std::filesystem::create_directories(cacheGcm, ec); + std::filesystem::create_directories(cacheBmi, ec); std::filesystem::create_directories(cacheObj, ec); - auto projectGcm = projectTargetDir / "gcm.cache"; + auto projectBmi = projectTargetDir / key.bmiDirName; auto projectObj = projectTargetDir / "obj"; - for (auto& g : arts.gcmFiles) { - auto from = projectGcm / g; - auto to = cacheGcm / g; + for (auto& g : arts.bmiFiles) { + auto from = projectBmi / g; + auto to = cacheBmi / g; if (!std::filesystem::exists(from)) { return std::unexpected(std::format( "expected build output missing: {}", from.string())); } if (!copy_one(from, to, ec)) { return std::unexpected(std::format( - "populate gcm '{}': {}", g, ec.message())); + "populate bmi '{}': {}", g, ec.message())); } } for (auto& o : arts.objFiles) { @@ -260,7 +263,7 @@ populate_from(const CacheKey& key, tmp += ".tmp"; { std::ofstream os(tmp); - os << serialize_manifest(arts); + os << serialize_manifest(key.manifestTag, arts); } std::filesystem::rename(tmp, key.manifestFile(), ec); if (ec) { diff --git a/src/build/flags.cppm b/src/build/flags.cppm index b1418ca..4f6562f 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -129,7 +129,13 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (isClang && !plan.stdBmiPath.empty()) { std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan)); } - f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}", module_flag, std_module_flag, + auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); + std::string prebuilt_module_flag; + if (traits.needsPrebuiltModulePath) { + prebuilt_module_flag = std::format(" -fprebuilt-module-path={}", traits.bmiDir); + } + f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}{}", module_flag, std_module_flag, + prebuilt_module_flag, opt_flag, pic_flag, sysroot_flag, b_flag, include_flags, user_cxxflags); f.cc = std::format("-std={}{}{}{}{}{}{}", c_std, opt_flag, pic_flag, sysroot_flag, b_flag, include_flags, user_cflags); diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index c28d85e..10eb595 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -126,8 +126,13 @@ bool is_c_source(const std::filesystem::path& src) { } // namespace std::string emit_ninja_string(const BuildPlan& plan) { - bool dyndep = dyndep_mode_enabled() - && mcpp::toolchain::is_gcc(plan.toolchain); + // dyndep requires P1689 scanning capability: + // GCC: built-in -fdeps-format=p1689r5 + // Clang: external clang-scan-deps tool (same P1689 output format) + bool has_scanner = mcpp::toolchain::is_gcc(plan.toolchain) + || !plan.scanDepsPath.empty(); + bool dyndep = dyndep_mode_enabled() && has_scanner; + auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); std::string out; auto append = [&](std::string s) { out += std::move(s); }; @@ -162,6 +167,9 @@ std::string emit_ninja_string(const BuildPlan& plan) { } if (dyndep) { append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path()))); + if (!plan.scanDepsPath.empty()) { + append(std::format("scan_deps = {}\n", escape_ninja_path(plan.scanDepsPath))); + } } append("\n"); @@ -170,10 +178,12 @@ std::string emit_ninja_string(const BuildPlan& plan) { append(" description = STAGE $out\n\n"); // P1: per-file dyndep rule. Converts one .ddi → .dd independently. - append("rule cxx_dyndep\n"); - append(" command = $mcpp dyndep --single --output $out $in\n"); - append(" description = DYNDEP $out\n"); - append(" restat = 1\n\n"); + append(std::format( + "rule cxx_dyndep\n" + " command = $mcpp dyndep --single --bmi-dir {} --bmi-ext {} --output $out $in\n" + " description = DYNDEP $out\n" + " restat = 1\n\n", + traits.bmiDir, traits.bmiExt)); // P2: cxx_module preserves BMI timestamps when interface is unchanged. // GCC always updates the .gcm timestamp even if content is identical. @@ -184,18 +194,20 @@ std::string emit_ninja_string(const BuildPlan& plan) { // // $bmi_out is set per build edge to the BMI path (gcm.cache/.gcm). // If $bmi_out is empty (no module provided), we just compile normally. + std::string module_output_flag = traits.needsExplicitModuleOutput + ? " -fmodule-output=$bmi_out" : ""; append("rule cxx_module\n"); - append(" command = " + append(std::format(" command = " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then " "cp -p \"$bmi_out\" \"$bmi_out.bak\"; " "fi && " - "$toolenv $cxx $cxxflags -c $in -o $out && " + "$toolenv $cxx $cxxflags{} -c $in -o $out && " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && " "cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then " "mv \"$bmi_out.bak\" \"$bmi_out\"; " "else " "rm -f \"$bmi_out.bak\"; " - "fi\n"); + "fi\n", module_output_flag)); append(" description = MOD $out\n"); if (dyndep) append(" restat = 1\n"); @@ -231,18 +243,31 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (dyndep) { // Scan rule: produce P1689 .ddi for one TU. - // -E -M -MM -MF gives us the dep file; -fdeps-* gives us the .ddi. + // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing. + // Clang: external clang-scan-deps tool with -format=p1689. append("rule cxx_scan\n"); - append(" command = $toolenv $cxx $cxxflags -fdeps-format=p1689r5 " - "-fdeps-file=$out -fdeps-target=$compile_target " - "-M -MM -MF $out.dep -E $in -o $compile_target\n"); + if (plan.scanDepsPath.empty()) { + // GCC path: compiler-integrated P1689 scanning. + append(" command = $toolenv $cxx $cxxflags -fmodules " + "-fdeps-format=p1689r5 " + "-fdeps-file=$out -fdeps-target=$compile_target " + "-M -MM -MF $out.dep -E $in -o $compile_target\n"); + } else { + // Clang path: clang-scan-deps produces P1689 JSON to stdout, + // then we redirect to $out. The -- separator passes the full + // compile command so clang-scan-deps knows the flags/sysroot. + append(" command = $toolenv $scan_deps -format=p1689 -- " + "$cxx $cxxflags -c $in -o $compile_target > $out\n"); + } append(" description = SCAN $out\n\n"); // Aggregate .ddi files into a Ninja dyndep file. - append("rule cxx_collect\n"); - append(" command = $mcpp dyndep --output $out $in\n"); - append(" description = COLLECT $out\n"); - append(" restat = 1\n\n"); + append(std::format( + "rule cxx_collect\n" + " command = $mcpp dyndep --bmi-dir {} --bmi-ext {} --output $out $in\n" + " description = COLLECT $out\n" + " restat = 1\n\n", + traits.bmiDir, traits.bmiExt)); } // Stage prebuilt std artifacts into the compiler-specific BMI cache. @@ -257,11 +282,12 @@ std::string emit_ninja_string(const BuildPlan& plan) { escape_ninja_path(plan.stdObjectPath))); } - auto bmi_path = [](std::string_view name) { - std::string s = "gcm.cache/"; + auto bmi_path = [&traits](std::string_view name) { + std::string s(traits.bmiDir); + s += '/'; for (char c : name) s.push_back(c == ':' ? '-' : c); - s += ".gcm"; + s += traits.bmiExt; return s; }; @@ -360,12 +386,18 @@ std::string emit_ninja_string(const BuildPlan& plan) { std::string out_line = "build " + escape_ninja_path(cu.object); if (cu.providesModule) { - out_line += " " + bmi_path(*cu.providesModule); + // Use implicit output (|) so $out only contains the .o file. + // GCC writes BMI implicitly; Clang uses -fmodule-output=$bmi_out. + out_line += " | " + bmi_path(*cu.providesModule); } out_line += std::format(" : {} {}", rule, escape_ninja_path(cu.source)); if (!implicit.empty()) out_line += " |" + implicit; out_line += "\n"; + // Clang needs $bmi_out to emit -fmodule-output=$bmi_out + if (cu.providesModule) { + out_line += " bmi_out = " + bmi_path(*cu.providesModule) + "\n"; + } append(std::move(out_line)); } append("\n"); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 701e661..f38d233 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -37,6 +37,7 @@ struct BuildPlan { std::filesystem::path outputDir; // target/// std::filesystem::path stdBmiPath; // absolute path to prebuilt std.gcm std::filesystem::path stdObjectPath; // absolute path to prebuilt std.o + std::filesystem::path scanDepsPath; // clang-scan-deps binary (Clang only) std::vector compileUnits; // topologically sorted std::vector linkUnits; diff --git a/src/cli.cppm b/src/cli.cppm index f4fa66c..b8b48d2 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1991,6 +1991,14 @@ prepare_build(bool print_fingerprint, ctx.plan = mcpp::build::make_plan(*m, *tc, fp, scan.graph, report.topoOrder, *root, ctx.outputDir, stdBmiPath, stdObjectPath); + // Clang: discover clang-scan-deps for P1689 dyndep scanning. + if (mcpp::toolchain::is_clang(*tc)) { + auto sd = tc->binaryPath.parent_path() / "clang-scan-deps"; + if (std::filesystem::exists(sd)) { + ctx.plan.scanDepsPath = sd; + } + } + // ─── M3.2: BMI cache stage / populate-task collection ───────────── // For each version-based dep package (i.e. fetched from a registry, // not a path dep), check the global BMI cache. If cached → stage into @@ -2023,12 +2031,15 @@ prepare_build(bool print_fingerprint, } if (skipCache) continue; + auto bmiT = mcpp::toolchain::bmi_traits(*tc); mcpp::bmi_cache::CacheKey key { .mcppHome = (*cfg2)->mcppHome, .fingerprint = fp.hex, .indexName = (*cfg2)->defaultIndex, .packageName = depName, .version = depVer, + .bmiDirName = std::string(bmiT.bmiDir), + .manifestTag = std::string(bmiT.manifestPrefix), }; // Compute the artifacts list from the build plan: every @@ -2042,11 +2053,11 @@ prepare_build(bool print_fingerprint, if (rels.starts_with("..")) continue; // not under depRoot if (cu.providesModule) { - std::string gcm; + std::string bmi; for (char c : *cu.providesModule) - gcm.push_back(c == ':' ? '-' : c); - gcm += ".gcm"; - arts.gcmFiles.push_back(std::move(gcm)); + bmi.push_back(c == ':' ? '-' : c); + bmi += std::string(bmiT.bmiExt); + arts.bmiFiles.push_back(std::move(bmi)); } arts.objFiles.push_back(cu.object.filename().string()); } @@ -3558,18 +3569,26 @@ int cmd_dyndep(const mcpplibs::cmdline::ParsedArgs& parsed) { bool single = parsed.is_flag_set("single"); + mcpp::dyndep::DyndepOptions opts; + std::string bmiDirStorage = parsed.option_or_empty("bmi-dir").value(); + std::string bmiExtStorage = parsed.option_or_empty("bmi-ext").value(); + if (!bmiDirStorage.empty()) + opts.bmiDir = bmiDirStorage; + if (!bmiExtStorage.empty()) + opts.bmiExt = bmiExtStorage; + std::expected body; if (single) { if (parsed.positional_count() != 1) { std::println(stderr, "error: --single requires exactly one .ddi input"); return 2; } - body = mcpp::dyndep::emit_dyndep_single(parsed.positional(0)); + body = mcpp::dyndep::emit_dyndep_single(parsed.positional(0), opts); } else { std::vector ddis; for (std::size_t i = 0; i < parsed.positional_count(); ++i) ddis.emplace_back(parsed.positional(i)); - body = mcpp::dyndep::emit_dyndep_from_files(ddis, /*stdImports=*/{}); + body = mcpp::dyndep::emit_dyndep_from_files(ddis, /*stdImports=*/{}, opts); } if (!body) { @@ -3887,6 +3906,10 @@ int run(int argc, char** argv) { .option(cl::Option("output").short_name('o').takes_value().value_name("PATH") .help("Path to write dyndep file")) .option(cl::Option("single").help("Single-file mode: one .ddi → one .dd")) + .option(cl::Option("bmi-dir").takes_value().value_name("DIR") + .help("BMI cache directory name (default: gcm.cache)")) + .option(cl::Option("bmi-ext").takes_value().value_name("EXT") + .help("BMI file extension (default: .gcm)")) .action(wrap_rc(cmd_dyndep))) ; diff --git a/src/dyndep.cppm b/src/dyndep.cppm index ecb2d84..dfbb070 100644 --- a/src/dyndep.cppm +++ b/src/dyndep.cppm @@ -28,9 +28,15 @@ struct UnitInfo { std::vector requires_; // logical names }; -// Stable convention: gcm.cache/.gcm, where ':' → '-'. +// Stable convention: /, where ':' → '-'. // Centralized here + ninja_backend so the two stay in sync. -std::string bmi_basename(std::string_view logicalName); +std::string bmi_basename(std::string_view logicalName, + std::string_view ext = ".gcm"); + +struct DyndepOptions { + std::string_view bmiDir = "gcm.cache"; + std::string_view bmiExt = ".gcm"; +}; // Parse a single .ddi JSON body to a UnitInfo. Returns unexpected on JSON error. std::expected parse_ddi(std::string_view body); @@ -44,18 +50,21 @@ std::expected parse_ddi(std::string_view body); // resolved to gcm.cache/.gcm regardless of whether any unit // "provides" them. std::string emit_dyndep(const std::vector& units, - const std::set& stdImports); + const std::set& stdImports, + const DyndepOptions& opts = {}); // Convenience: read .ddi files from disk, parse them, and emit a dyndep // file string. Returns unexpected if any .ddi fails to parse. std::expected emit_dyndep_from_files(const std::vector& ddiPaths, - const std::set& stdImports); + const std::set& stdImports, + const DyndepOptions& opts = {}); // P1: emit a single-unit dyndep file from one .ddi file. // Used by the per-file dyndep mode to convert each .ddi → .dd independently. std::expected -emit_dyndep_single(const std::filesystem::path& ddiPath); +emit_dyndep_single(const std::filesystem::path& ddiPath, + const DyndepOptions& opts = {}); } // namespace mcpp::dyndep @@ -149,11 +158,12 @@ std::size_t find_key(std::string_view s, std::size_t start, std::string_view key } // namespace -std::string bmi_basename(std::string_view logicalName) { +std::string bmi_basename(std::string_view logicalName, + std::string_view ext) { std::string out; - out.reserve(logicalName.size() + 4); + out.reserve(logicalName.size() + ext.size()); for (char c : logicalName) out.push_back(c == ':' ? '-' : c); - out += ".gcm"; + out += ext; return out; } @@ -232,7 +242,8 @@ std::expected parse_ddi(std::string_view body) { } std::string emit_dyndep(const std::vector& units, - const std::set& stdImports) + const std::set& stdImports, + const DyndepOptions& opts) { // Per ninja's dyndep contract: the dyndep file's `build : dyndep` // line augments the main build edge with **additional implicit inputs** @@ -255,7 +266,8 @@ std::string emit_dyndep(const std::vector& units, bool selfProvides = false; for (auto& p : u.provides) if (p == r) { selfProvides = true; break; } if (selfProvides) continue; - add_implicit("gcm.cache/" + bmi_basename(r)); + std::string bmiDir(opts.bmiDir); + add_implicit(bmiDir + "/" + bmi_basename(r, opts.bmiExt)); } line += "\n restat = 1\n"; out += line; @@ -267,7 +279,8 @@ std::string emit_dyndep(const std::vector& units, std::expected emit_dyndep_from_files(const std::vector& ddiPaths, - const std::set& stdImports) + const std::set& stdImports, + const DyndepOptions& opts) { std::vector units; units.reserve(ddiPaths.size()); @@ -283,11 +296,12 @@ emit_dyndep_from_files(const std::vector& ddiPaths, } units.push_back(std::move(*u)); } - return emit_dyndep(units, stdImports); + return emit_dyndep(units, stdImports, opts); } std::expected -emit_dyndep_single(const std::filesystem::path& ddiPath) +emit_dyndep_single(const std::filesystem::path& ddiPath, + const DyndepOptions& opts) { std::ifstream is(ddiPath); if (!is) return std::unexpected(std::format("cannot read '{}'", ddiPath.string())); @@ -304,7 +318,8 @@ emit_dyndep_single(const std::filesystem::path& ddiPath) for (auto& p : u->provides) if (p == r) { selfProvides = true; break; } if (selfProvides) continue; if (firstImplicit) { line += " |"; firstImplicit = false; } - line += " gcm.cache/" + bmi_basename(r); + std::string bmiDir(opts.bmiDir); + line += " " + bmiDir + "/" + bmi_basename(r, opts.bmiExt); } line += "\n restat = 1\n"; out += line; diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 6ffe5ba..4271a49 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -28,6 +28,9 @@ std::vector std_module_build_commands(const Toolchain& tc, std::filesystem::path archive_tool(const Toolchain& tc); +// Locate clang-scan-deps in the same bin/ directory as clang++. +std::optional find_scan_deps(const Toolchain& tc); + } // namespace mcpp::toolchain::clang namespace mcpp::toolchain::clang { @@ -164,4 +167,10 @@ std::filesystem::path archive_tool(const Toolchain& tc) { return {}; } +std::optional find_scan_deps(const Toolchain& tc) { + auto p = tc.binaryPath.parent_path() / "clang-scan-deps"; + if (std::filesystem::exists(p)) return p; + return std::nullopt; +} + } // namespace mcpp::toolchain::clang diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 169ba30..0a01488 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -42,6 +42,17 @@ bool is_gcc(const Toolchain& tc); bool is_clang(const Toolchain& tc); bool is_musl_target(const Toolchain& tc); +struct BmiTraits { + std::string_view bmiDir; // "gcm.cache" | "pcm.cache" + std::string_view bmiExt; // ".gcm" | ".pcm" + std::string_view manifestPrefix; // "gcm" | "pcm" + bool needsExplicitModuleOutput = false; + bool needsPrebuiltModulePath = false; + bool scanNeedsFModules = true; +}; + +BmiTraits bmi_traits(const Toolchain& tc); + } // namespace mcpp::toolchain namespace mcpp::toolchain { @@ -58,4 +69,25 @@ bool is_musl_target(const Toolchain& tc) { return tc.targetTriple.find("-musl") != std::string::npos; } +BmiTraits bmi_traits(const Toolchain& tc) { + if (is_clang(tc)) { + return { + .bmiDir = "pcm.cache", + .bmiExt = ".pcm", + .manifestPrefix = "pcm", + .needsExplicitModuleOutput = true, + .needsPrebuiltModulePath = true, + .scanNeedsFModules = false, + }; + } + return { + .bmiDir = "gcm.cache", + .bmiExt = ".gcm", + .manifestPrefix = "gcm", + .needsExplicitModuleOutput = false, + .needsPrebuiltModulePath = false, + .scanNeedsFModules = true, + }; +} + } // namespace mcpp::toolchain diff --git a/tests/e2e/38_llvm_modules.sh b/tests/e2e/38_llvm_modules.sh new file mode 100755 index 0000000..31016a4 --- /dev/null +++ b/tests/e2e/38_llvm_modules.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# 38_llvm_modules.sh — multi-module project with LLVM/Clang. +# +# Tests: module interface (.cppm) with `export module`, cross-module import, +# dyndep pipeline, BMI path parameterization (pcm.cache/*.pcm), and +# -fmodule-output / -fprebuilt-module-path flags. +set -e + +LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" +if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then + echo "SKIP: xlings llvm@20.1.7 is not installed" + exit 0 +fi +if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then + echo "SKIP: xlings llvm@20.1.7 has no libc++ std.cppm" + exit 0 +fi + +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 = "llvm_modules" +version = "0.1.0" + +[toolchain] +linux = "llvm@20.1.7" +EOF + +# Module interface unit: exports greet() +cat > src/greet.cppm <<'EOF' +export module llvm_modules.greet; + +import std; + +export std::string greet(std::string_view name) { + return std::format("Hello, {}!", name); +} +EOF + +# Main imports the module +cat > src/main.cpp <<'EOF' +import std; +import llvm_modules.greet; + +int main() { + std::println("{}", greet("clang")); + return 0; +} +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: llvm multi-module build failed" + exit 1 +} + +binary=$(find target -type f -path '*/bin/llvm_modules' | head -1) +[[ -n "$binary" && -x "$binary" ]] || { + find target -maxdepth 5 -type f + echo "FAIL: llvm_modules binary missing" + exit 1 +} + +out=$("$binary") +[[ "$out" == "Hello, clang!" ]] || { + echo "FAIL: wrong runtime output: $out" + exit 1 +} + +# Verify BMI went to pcm.cache/, not gcm.cache/ +pcm_dir=$(find target -type d -name 'pcm.cache' | head -1) +[[ -n "$pcm_dir" ]] || { + echo "FAIL: pcm.cache/ directory not found (Clang should use pcm.cache)" + exit 1 +} +gcm_dir=$(find target -type d -name 'gcm.cache' | head -1) +[[ -z "$gcm_dir" ]] || { + echo "FAIL: gcm.cache/ directory found (Clang should NOT use gcm.cache)" + exit 1 +} + +echo "OK" diff --git a/tests/unit/test_bmi_cache.cpp b/tests/unit/test_bmi_cache.cpp index c6a602c..cd39828 100644 --- a/tests/unit/test_bmi_cache.cpp +++ b/tests/unit/test_bmi_cache.cpp @@ -46,7 +46,7 @@ TEST(BmiCache, KeyDirLayoutMatchesDocs26) { EXPECT_EQ(k.dir().string(), "/home/u/.mcpp/bmi/deadbeef0123abcd/deps/mcpplibs/mcpplibs.cmdline@0.0.1"); EXPECT_EQ(k.manifestFile().filename().string(), "manifest.txt"); - EXPECT_EQ(k.gcmDir().filename().string(), "gcm.cache"); + EXPECT_EQ(k.bmiDir().filename().string(), "gcm.cache"); EXPECT_EQ(k.objDir().filename().string(), "obj"); } @@ -68,7 +68,7 @@ TEST(BmiCache, PopulateThenStageRoundTrip) { writeFile(project / "obj" / "cmdline.m.o", "OBJ-A"); DepArtifacts arts { - .gcmFiles = { "mcpplibs.cmdline.gcm", "mcpplibs.cmdline-options.gcm" }, + .bmiFiles = { "mcpplibs.cmdline.gcm", "mcpplibs.cmdline-options.gcm" }, .objFiles = { "cmdline.m.o" }, }; @@ -77,8 +77,8 @@ TEST(BmiCache, PopulateThenStageRoundTrip) { ASSERT_TRUE(pop) << pop.error(); EXPECT_TRUE(std::filesystem::exists(k.manifestFile())); - EXPECT_TRUE(std::filesystem::exists(k.gcmDir() / "mcpplibs.cmdline.gcm")); - EXPECT_TRUE(std::filesystem::exists(k.gcmDir() / "mcpplibs.cmdline-options.gcm")); + EXPECT_TRUE(std::filesystem::exists(k.bmiDir() / "mcpplibs.cmdline.gcm")); + EXPECT_TRUE(std::filesystem::exists(k.bmiDir() / "mcpplibs.cmdline-options.gcm")); EXPECT_TRUE(std::filesystem::exists(k.objDir() / "cmdline.m.o")); EXPECT_TRUE(is_cached(k)); @@ -86,7 +86,7 @@ TEST(BmiCache, PopulateThenStageRoundTrip) { auto project2 = t.path / "proj2" / "target"; auto staged = stage_into(k, project2); ASSERT_TRUE(staged) << staged.error(); - EXPECT_EQ(staged->gcmFiles.size(), 2u); + EXPECT_EQ(staged->bmiFiles.size(), 2u); EXPECT_EQ(staged->objFiles.size(), 1u); EXPECT_TRUE(std::filesystem::exists(project2 / "gcm.cache" / "mcpplibs.cmdline.gcm")); EXPECT_TRUE(std::filesystem::exists(project2 / "obj" / "cmdline.m.o")); @@ -108,7 +108,7 @@ TEST(BmiCache, StageIntoDoesNotTouchIdenticalOutputs) { writeFile(project / "obj" / "cmdline.m.o", "OBJ-A"); DepArtifacts arts { - .gcmFiles = { "mcpplibs.cmdline.gcm" }, + .bmiFiles = { "mcpplibs.cmdline.gcm" }, .objFiles = { "cmdline.m.o" }, }; @@ -140,7 +140,7 @@ TEST(BmiCache, StageIntoDoesNotOverwriteExistingOutputs) { writeFile(cacheProject / "obj" / "cmdline.m.o", "CACHE-OBJ"); DepArtifacts arts { - .gcmFiles = { "mcpplibs.cmdline.gcm" }, + .bmiFiles = { "mcpplibs.cmdline.gcm" }, .objFiles = { "cmdline.m.o" }, }; @@ -178,7 +178,7 @@ TEST(BmiCache, IsCachedFalseWhenSentinelExistsButFileMissing) { writeFile(project / "gcm.cache" / "lib.gcm", "G"); writeFile(project / "obj" / "lib.m.o", "O"); - DepArtifacts arts { .gcmFiles = {"lib.gcm"}, .objFiles = {"lib.m.o"} }; + DepArtifacts arts { .bmiFiles = {"lib.gcm"}, .objFiles = {"lib.m.o"} }; auto k = makeKey(home); ASSERT_TRUE(populate_from(k, project, arts)); ASSERT_TRUE(is_cached(k)); @@ -193,7 +193,7 @@ TEST(BmiCache, PopulateFailsIfBuildOutputMissing) { auto home = t.path / "home"; auto project = t.path / "proj" / "target"; std::filesystem::create_directories(project / "gcm.cache"); - DepArtifacts arts { .gcmFiles = {"missing.gcm"}, .objFiles = {} }; + DepArtifacts arts { .bmiFiles = {"missing.gcm"}, .objFiles = {} }; auto k = makeKey(home); auto pop = populate_from(k, project, arts); EXPECT_FALSE(pop); @@ -219,7 +219,7 @@ TEST(BmiCache, PopulateSkipsWhenLockHeld) { ASSERT_GE(fd, 0); ASSERT_EQ(::flock(fd, LOCK_EX | LOCK_NB), 0); - DepArtifacts arts { .gcmFiles = {"lib.gcm"}, .objFiles = {"lib.m.o"} }; + DepArtifacts arts { .bmiFiles = {"lib.gcm"}, .objFiles = {"lib.m.o"} }; auto pop = populate_from(k, project, arts); EXPECT_TRUE(pop) << "should silently skip when lock is held"; // manifest.txt must NOT have been written by the second writer.