|
| 1 | +# gtest_main 冲突修复:src 轨 feature 控制 + dev 轨 main 检测 + `mcpp add --dev` |
| 2 | + |
| 3 | +> 关联 issue: [#168](https://github.com/mcpp-community/mcpp/issues/168) |
| 4 | +> (`mcpp add gtest` → `[dependencies]` → `mcpp build` 报 `LNK2005: main already defined`) |
| 5 | +> |
| 6 | +> 前序:[2026-06-25-dependency-archive-linking-design.md](2026-06-25-dependency-archive-linking-design.md) |
| 7 | +> (0.0.64 的 **dev 轨** 实现 + 为何放弃静态归档) |
| 8 | +> |
| 9 | +> 跨仓库:**mcpp**(核心)+ **mcpp-index**(`compat.gtest.lua`)。 |
| 10 | +> |
| 11 | +> **状态:设计定稿。dev 轨已实现(0.0.64);src 轨 + `mcpp add --dev` 待实现。** |
| 12 | +
|
| 13 | +## 1. 问题与根因 |
| 14 | + |
| 15 | +`gtest` 把入口拆成两块:框架 `gtest-all.cc` 与**可选入口** `gtest_main.cc`(后者自带 |
| 16 | +`int main(){ InitGoogleTest; RUN_ALL_TESTS; }`)。冲突是普适的:**任何"自带 main 的 |
| 17 | +二进制"一旦链入 `gtest_main.o` 就 `duplicate symbol: main`**。 |
| 18 | + |
| 19 | +#168 触发路径:`mcpp add gtest` 写进 **`[dependencies]`**(常规依赖,`commands.cppm:82`) |
| 20 | +→ `mcpp build` 把 gtest 全部对象(含 `gtest_main.o`)内联进应用 `hello.exe` → 与 |
| 21 | +`src/main.cpp` 的 `main` 撞车(Windows `LNK2005`)。 |
| 22 | + |
| 23 | +## 2. 最终设计:按依赖所在表分两轨 |
| 24 | + |
| 25 | +| 轨 | 场景 | gtest_main 策略 | 机制 | 状态 | |
| 26 | +|---|---|---|---|---| |
| 27 | +| **A. src / 常规依赖**(`[dependencies]` + `mcpp build`)| 应用/库代码 | 默认**不含**;`features=["main"]` 才含 | **feature 控制(声明式,不猜)** | **待做** | |
| 28 | +| **B. dev 依赖**(`[dev-dependencies]` + `mcpp test`)| 测试 | 测试**自带 main → 不链**;否则链 | **判 `int main` 检测**(`source_defines_main`,限 dev-dep 作用域)| **已实现 0.0.64** | |
| 29 | + |
| 30 | +两轨正交互补:src 轨干净声明式、默认安全(#168 默认消失);dev 轨维持 `mcpp test` |
| 31 | +"一个 gtest 依赖 + 带/不带 main 测试混用"全自动跑通。 |
| 32 | + |
| 33 | +### 2.1 为什么 src 轨用 feature「不猜」、dev 轨用「检测」 |
| 34 | + |
| 35 | +- **src 轨不能猜**:常规依赖可能是任意大型 C 库;文本级 `main` 检测**看不懂预处理器 |
| 36 | + 守卫**——libarchive 的 `archive_blake2s_ref.c` 里 `#if defined(BLAKE2S_SELFTEST) |
| 37 | + int main(void)` 是死代码,但文本扫描会误判 → 误排除 → `undefined reference`。 |
| 38 | + (0.0.64 正因此把检测**限定到 dev-dep**。)所以 src 轨必须**声明式 feature**,零误判。 |
| 39 | +- **dev 轨可以检测**:它只扫**用户自己的测试文件**(普通代码,无预处理器守卫 main, |
| 40 | + 可靠),并让 `mcpp test` 对"写/不写 main"都无感;且**已上线验证**。 |
| 41 | + |
| 42 | +### 2.2 为什么现有 feature/target 机制不够,src 轨需要新增一环 |
| 43 | + |
| 44 | +mcpp 现有 features 能力边界(实证): |
| 45 | +- `featuresMap`:feature → **隐含 feature**(`manifest.cppm:245`); |
| 46 | +- `required_features`:**target 级**门控 —— `std::erase_if(m->targets, …)` |
| 47 | + (`prepare.cppm:2180`),只决定"是否发射某个 link unit",且作用于**根包** targets; |
| 48 | +- 依赖级 feature **选择**(`dep_spec.features`)。 |
| 49 | + |
| 50 | +**缺的那一环**:依赖的 **sources 是包级全局**(`BuildConfig.sources`,`Target` 无 |
| 51 | +`sources` 字段),依赖链接是**按包名内联全部实现对象**(`plan.cppm` 消费者内联循环), |
| 52 | +**不经过 target**。所以: |
| 53 | +> 即便给 gtest 写 `gtest_main` target + `required_features=["main"]`,删 target ≠ 它 |
| 54 | +> 名下的源不编译——源在"源→CompileUnit→按包名内联"这条**不经 target** 的路上,照样 |
| 55 | +> 被编译并内联。`required_features` 门控的是 **link unit**,门不住 **依赖源**。 |
| 56 | +
|
| 57 | +→ src 轨 feature 控制需要把作用点从「删 target」**下沉到「挑 CompileUnit / 挑内联 |
| 58 | +对象」**:即 **「已激活 feature ↔ 依赖源的纳入/排除」**。这是真正的新增点,也是旧 |
| 59 | +mcpp 没有、需考虑兼容的根源。 |
| 60 | + |
| 61 | +## 3. src 轨实现 |
| 62 | + |
| 63 | +### 3.1 mcpp-index(`compat.gtest.lua`) |
| 64 | +```lua |
| 65 | +mcpp = { |
| 66 | + language = "c++23", |
| 67 | + sources = { "*/googletest/src/gtest-all.cc" }, -- 默认仅框架 |
| 68 | + include_dirs = { "*/googletest/include", "*/googletest" }, |
| 69 | + targets = { ["gtest"] = { kind = "lib" } }, |
| 70 | + -- 新增:feature 门控的源组。激活 "main" 时才纳入 gtest_main.cc。 |
| 71 | + features = { |
| 72 | + ["main"] = { sources = { "*/googletest/src/gtest_main.cc" } }, |
| 73 | + }, |
| 74 | +} |
| 75 | +``` |
| 76 | +- 默认 `gtest = "1.15.2"` → 不含 gtest_main → 任何二进制都不撞 main → **#168 默认消失**; |
| 77 | +- `gtest = { version="1.15.2", features=["main"] }` → 纳入 gtest_main(进阶 opt-in)。 |
| 78 | + |
| 79 | +### 3.2 mcpp 核心:依赖的「feature 门控源组」 |
| 80 | +- 解析依赖描述符的 `[mcpp].features.<name>.sources`(feature → 附加源 glob)。 |
| 81 | +- 合成依赖 manifest 时,把**已激活 feature**对应的源组**并入**该包 sources;未激活则 |
| 82 | + 不并入 → 不编译 → 不内联。 |
| 83 | +- 复用现有依赖级 feature 激活(`prepare.cppm:2083` 的 activated(pkg) 集合),只是把 |
| 84 | + 作用对象从"target 门控"扩展到"源组纳入"。 |
| 85 | +- **不改 dev 轨、不改链接器、不引入 gtest 特例**——任何库都能用此通用能力。 |
| 86 | + |
| 87 | +## 4. `mcpp add --dev`(补齐缺失能力) |
| 88 | +现状:`cmd_add`(`cli.cppm:255`)只写 `[dependencies]`,**无 `--dev`**。 |
| 89 | +- 新增 `mcpp add --dev <pkg>` → 写入 `[dev-dependencies]`(含命名空间子表,与现有 |
| 90 | + `[dependencies.<ns>]` 对称);`mcpp remove` 对称。 |
| 91 | +- 引导:测试框架(如 gtest)**建议**放 dev(走 dev 轨,体验最佳)。**非强制**——因为 |
| 92 | + src 轨已让常规依赖 gtest 默认安全。 |
| 93 | + |
| 94 | +## 5. 各组合最终行为(验收矩阵) |
| 95 | + |
| 96 | +| 场景 | gtest 位置 | 写 main? | 预期 | |
| 97 | +|---|---|---|---| |
| 98 | +| 应用 `mcpp build`(#168)| `[dependencies]` 默认 | 是 | 只框架,**不撞 main** ✓ | |
| 99 | +| 应用本身用 gtest、要框架 main | `[dependencies]` `features=["main"]` | 否 | 链 gtest_main,跑通 ✓ | |
| 100 | +| 测试 `mcpp test`,只写 TEST 宏 | `[dev-dependencies]` | 否 | 检测无 main → 链 gtest_main ✓(0.0.64)| |
| 101 | +| 测试 `mcpp test`,自带 main | `[dev-dependencies]` | 是 | 检测有 main → 不链 gtest_main ✓(0.0.64)| |
| 102 | +| 常规库(libarchive)blake2 守卫 main | `[dependencies]` | (死码)| src 轨不猜、dev 检测不扫常规库 → 不误判 ✓ | |
| 103 | + |
| 104 | +## 6. 兼容与发布顺序(实现前须定) |
| 105 | + |
| 106 | +`compat.gtest` 现含 gtest_main;src 轨改成"默认不含"会影响**旧 mcpp**: |
| 107 | +- 旧 mcpp 不认 `features.*.sources`;若描述符把 gtest_main.cc 从基础 sources 移走, |
| 108 | + 旧 mcpp 下 gtest 将**永远无 main**(连 dev 轨无-main 测试也缺入口)。 |
| 109 | +- 候选过渡: |
| 110 | + 1. **先发认识"feature 门控源组"的新 mcpp,再切描述符**(旧版本仍在野,但 0.0.x |
| 111 | + 快速迭代、bootstrap pin 驱动升级,可接受); |
| 112 | + 2. 描述符**版本化/兼容写法**,旧 mcpp 维持现状(含 main)、新 mcpp 读新字段; |
| 113 | + 3. 确认旧 mcpp 对未知键**非 --strict 下是 warning 非 error**(已见 schema warning |
| 114 | + 机制,`manifest.cppm:268`),据此选最小破坏方案。 |
| 115 | +- **实现前敲定** 1/2/3。dev 轨(0.0.64)不依赖描述符改动,始终工作。 |
| 116 | + |
| 117 | +## 7. 实施计划 |
| 118 | + |
| 119 | +- [x] **dev 轨**:`source_defines_main` 检测 + dev-dep 作用域(**已发布 0.0.64**)。 |
| 120 | +- [ ] **src-core**:mcpp 支持依赖的「feature 门控源组」(`[mcpp].features.*.sources`), |
| 121 | + 默认不并入、激活后并入。 |
| 122 | +- [ ] **src-index**:`compat.gtest.lua` 加 `main` feature,基础 sources 仅框架(按 §6 |
| 123 | + 过渡方案落地)。 |
| 124 | +- [ ] **add**:`mcpp add --dev` / `mcpp remove` 对称 + 测试框架建议提示。 |
| 125 | +- [ ] **测试**: |
| 126 | + - e2e:`mcpp add gtest && mcpp build`(应用自带 main)→ 成功(#168 哨兵); |
| 127 | + `features=["main"]` → 链 gtest_main 成功;`mcpp add --dev gtest && mcpp test` |
| 128 | + 带/不带 main 两类全绿(已有 78 覆盖 dev 轨)。 |
| 129 | + - 单元:feature 门控源组解析与并入;`mcpp add --dev` 写表。 |
| 130 | +- [ ] **发布闭环**:mcpp bump + release + 镜像 xlings-res(gh/gtc)+ xim-pkgindex + |
| 131 | + 索引发布;**mcpp-index 改了 → 重发 mcpp-index 索引产物**;按 §6 控制发布顺序。 |
| 132 | + |
| 133 | +## 8. 决策备注 |
| 134 | + |
| 135 | +1. **两轨并存是刻意的**:src 要干净声明式(feature,不猜),dev 维持现状(检测,已实现、 |
| 136 | + 省事)。src 不猜是因为常规库的预处理器守卫 main 会让文本检测误判(blake2 实锤)。 |
| 137 | +2. **feature 是整库/整源组粒度**,不在链接器层写 gtest 特判;gtest 只是在描述符里 |
| 138 | + 填了 `main` feature 的数据 → 未来任何测试框架同样适配,mcpp 零框架知识。 |
| 139 | +3. **默认不含 main**:依赖不应劫持消费者入口;"含 main"会把 #168 设成默认崩溃。 |
| 140 | +4. **放弃过的路**:静态归档(Windows/MSVC `LNK1561`/`LNK2019` 不可行,见前序文档); |
| 141 | + src 轨"扫所有依赖判 main"(blake2 误判)。 |
0 commit comments