Skip to content

Commit 27b8b00

Browse files
committed
docs: finalize gtest_main fix design (src=feature / dev=detection / add --dev)
Converged two-track design for #168: - src/regular dep: declarative feature control (no guessing), gtest_main default off - dev dep: existing 0.0.64 int-main detection (already shipped) - mcpp add --dev (missing capability) Records why required_features can't gate dep sources, why src can't 'guess' (blake2 preprocessor-guarded main), and the compat/release-ordering options.
1 parent 453faba commit 27b8b00

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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

Comments
 (0)