Skip to content

Commit 8411adb

Browse files
committed
fix(link): link kind="lib" deps as static archives (fixes test duplicate main)
gtest's gtest_main.cc carries its own main(); mcpp inlined ALL of a dependency's objects into every test binary, so a test that defined its own main() collided with gtest_main.o → `ld.lld: error: duplicate symbol: main`. Honor the dependency's already-declared `kind="lib"` (compat.gtest.lua sets it): compile such a dep into a per-package static archive lib<pkg>.a and link it AFTER the consumer's objects. Standard archive semantics then pull a member only when a symbol is still undefined — so gtest_main.o's main() is pulled ONLY for tests that define none. All {own-main, framework-main} × {uses-gtest, doesn't} combinations link correctly, transparently. Generic, driven by the descriptor's `kind` — no gtest special-casing in mcpp. A future test framework just declares kind="lib". Module (.cppm) objects are never archived (they carry global init); pure-module deps (mcpplibs.cmdline) are byte -for-byte unchanged. - plan.cppm: LinkUnit.archiveInputs; synthesize StaticLibrary unit per kind=lib dep; consumers reference the .a instead of inlining its non-module objects. - ninja_backend.cppm: emit archiveInputs via $in, after objects. - unit: NinjaBackend.ArchiveInputsLinkedAfterObjects - e2e: 78_test_main_combinations.sh (4 main×gtest combinations) - design: .agents/docs/2026-06-25-dependency-archive-linking-design.md - bump 0.0.63 -> 0.0.64
1 parent c7cf7b4 commit 8411adb

8 files changed

Lines changed: 340 additions & 2 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# 依赖库归档链接(dependency `lib` → static archive)设计方案
2+
3+
> mcpp 0.0.63 → 0.0.64 — 修复 `mcpp test` 的 `duplicate symbol: main`,并把
4+
> 「依赖库」从「散对象内联」升级为「静态归档按需链接」。
5+
>
6+
> 关联:[2026-06-25-cdb-test-coverage-design.md](2026-06-25-cdb-test-coverage-design.md)
7+
>(同一轮 test 体验修复链的第三环)。
8+
>
9+
> **状态:设计定稿,进入实施。** 实施进度见 §8(动态更新)。
10+
11+
## 1. 问题
12+
13+
含 `[dev-dependencies] gtest` 的项目,若测试文件自带 `int main()`(脚手架默认模板
14+
就是),`mcpp test` 链接失败:
15+
16+
```
17+
ld.lld: error: duplicate symbol: main
18+
>>> obj/test_smoke.o:(main) ← 测试自己的 main()
19+
>>> obj/gtest_main.o:(.text+0x0) ← gtest_main.cc 自带的 main()
20+
```
21+
22+
测试二进制同时链接了测试自身的 `main` 与 gtest 的 `gtest_main.o`(也含 `main`)。
23+
24+
需优雅支持的**交叉组合**(用户尽量无感):
25+
26+
| 用 gtest? | 自带 main? | 期望 |
27+
|---|---|---|
28+
| 否 | 是 | 链接测试自己的 main(今天 fresh 项目即此,正常)|
29+
| 是 | 是(调用 `InitGoogleTest`/`RUN_ALL_TESTS`)| 用测试的 main,**不要** gtest_main |
30+
| 是 | 否(只写 `TEST(...)`)| 用 gtest_main 提供的 main |
31+
| 否 | 否 | 无入口——**清晰报错**,非崩溃 |
32+
33+
## 2. 根因
34+
35+
`compat.gtest` 描述符(`mcpplibs/mcpp-index/pkgs/c/compat.gtest.lua`)**已声明**:
36+
37+
```lua
38+
mcpp = {
39+
sources = { "*/googletest/src/gtest-all.cc", "*/googletest/src/gtest_main.cc" },
40+
targets = { ["gtest"] = { kind = "lib" } }, -- 已建模为「库」
41+
}
42+
```
43+
44+
mcpp 的 manifest 解析器也**已支持** `kind="lib"` → `Target::Library`
45+
(`manifest.cppm:637`)。**但 mcpp 的依赖链接模型没有兑现它**:
46+
47+
- `plan.cppm` 的 link-unit 循环只为**根包**的 `[targets.*]` 建 LinkUnit;
48+
- 依赖包(含 dev-dep)的对象通过 `append_package_objects` / compileUnit 循环
49+
**直接内联**进消费者二进制(`plan.cppm:486–575`);
50+
- 于是 `gtest-all.o`、`gtest_main.o` 作为**散对象**被无条件灌入测试二进制 →
51+
`gtest_main.o` 的 `main` 与测试自身 `main` 冲突。
52+
53+
**这不是 gtest 的缺陷,也不是描述符的缺陷,而是 mcpp 核心 link 模型的缺口。**
54+
55+
## 3. 架构决策:修复归属(回答「改 mcpp-index 还是 mcpp」)
56+
57+
| 候选 | 评价 |
58+
|---|---|
59+
| ❌ mcpp 里给 gtest 加特判(跳过 gtest_main.o) | 把第三方库名硬编进构建核心,污染架构,未来每个框架都要再特判。否决。 |
60+
| ❌ 改 gtest 描述符拆出 `gtest_main` 单独目标 | 描述符**已**声明 `kind="lib"`,够用;拆分增加每个库包的作者负担,且仍需 mcpp 支持「按归档链接」。非必要。 |
61+
| ✅ **mcpp 核心:兑现依赖的 `kind="lib"` → 建静态归档 `.a` → 按归档链接** | 通用、由**既有**描述符元数据驱动、零 gtest 特例、未来框架自动适配。**采用。** |
62+
63+
**职责分层(干净):**
64+
- **描述符(mcpp-index)**:声明「我是什么」——`kind="lib"`。gtest 已声明,**不改**。
65+
- **mcpp 核心**:实现「怎么链」——依赖的 lib 包 → 归档 → 按归档链接。**通用 HOW。**
66+
67+
**未来演进**:mcpplibs 生态测试框架 / mcpp 原生测试框架,只要在各自描述符里声明
68+
`kind="lib"`(并可选地提供一个含 `main` 的入口对象),就**自动**获得「带 main 用
69+
自己的、不带 main 用框架的」正确行为,mcpp 无需任何该框架的知识。
70+
71+
## 4. 核心设计:依赖库 → 静态归档 → 条件链接
72+
73+
### 4.1 原理(标准链接器语义)
74+
静态归档 `.a` 的成员对象**仅在其提供的符号当前未定义时**才被拉入。于是单个
75+
`libgtest.a = { gtest-all.o, gtest_main.o }` 即同时正确处理「带/不带 main」:
76+
77+
- 测试自带 `main` → 链接器处理归档时 `main` 已定义 → **不拉** `gtest_main.o`;
78+
测试引用的 gtest 符号 → 拉 `gtest-all.o`。✓
79+
- 测试不带 `main` → `main` 未定义 → 拉 `gtest_main.o`(它提供 main + 调
80+
`RUN_ALL_TESTS`)+ `gtest-all.o`。✓
81+
- 测试自带 main 且**不用** gtest(但 gtest 是 dev-dep)→ 归档**零贡献**(无未定义
82+
符号引用它)→ 不再撞 main。✓(今天的失败用例)
83+
- 测试不带 main 且不用框架 → 无入口 → 链接器报 undefined `main`。mcpp 捕获并给出
84+
清晰提示(见 §6)。✓
85+
86+
> 单归档即可,**无需**把 gtest_main 拆成独立 target——归档的「按成员对象解析」已
87+
> 天然实现条件链接。
88+
89+
### 4.2 BuildPlan 变更(`plan.cppm`)
90+
对每个 `kind="lib"` 的**依赖**包(非根包),由其**非模块**实现对象(`.cc/.cpp/.c`)
91+
合成一个 `StaticLibrary` LinkUnit → 产 `lib<pkg>.a`:
92+
93+
- 新增 LinkUnit:`{ kind=StaticLibrary, output="lib<mangled-pkg>.a", objects=<dep 的非模块对象> }`。
94+
- 消费者(Binary/TestBinary/SharedLibrary)**不再内联**该 dep 的非模块对象,改为:
95+
- 把 `lib<pkg>.a` 路径追加到**链接命令、对象之后**(符号解析顺序正确);
96+
- 把 `.a` 加入 `implicitInputs`(ninja 重建追踪)。
97+
- **模块对象(`.cppm` 的 `.o`)仍直接内联**:它们承载模块全局初始化、且从不提供
98+
`main`,放进归档可能因「无未定义符号引用」被丢弃 → 破坏全局初始化。故模块对象
99+
不归档(mcpplibs.cmdline 等纯模块依赖 → 无非模块对象 → 无归档 → **行为不变**)。
100+
101+
### 4.3 链接顺序与 ninja(`ninja_backend.cppm`)
102+
- 当前链接行:`build <out> : cxx_link <objects> | <implicitInputs>`。`implicitInputs`
103+
在 `|` 之后是 ninja 的**order-only/隐式依赖,不进命令行**。归档要真正参与链接,
104+
其路径必须进**命令行、且在对象之后**。
105+
- 方案:LinkUnit 新增 `std::vector<std::filesystem::path> archiveInputs`;ninja 发射
106+
时把它们拼在 `objects`(及 std.o)**之后**、作为命令行实参,同时也加入 `| implicit`
107+
做重建追踪。
108+
109+
### 4.4 复用既有基建
110+
mcpp 已有 `cxx_archive` 规则(`ninja_backend.cppm:378`)、`archive_tool`(ar/llvm-ar,
111+
`flags.cppm:253`)、`StaticLibrary` LinkUnit。本设计复用之,无需新工具。
112+
113+
## 5. 不破坏既有行为(回归边界)
114+
- **纯模块依赖**(mcpplibs.cmdline,`.cppm`)→ 无非模块对象 → 不生成归档 →
115+
链接与今天**逐字节一致**。
116+
- **根包自身**对象 → 不归档(它就是要被链接的主体)。
117+
- **shared 依赖**(SharedLibrary)→ 走既有 `append_direct_shared_deps`,不变。
118+
- 仅「依赖包含有非模块实现对象且 `kind=lib`」(gtest、未来 C/C++ 库)行为改变:
119+
由内联改为归档链接——这正是修复点。
120+
121+
## 6. 边界用例
122+
- **无 main 且无框架的测试**:链接报 undefined `main`。mcpp 捕获 lld/ld 的该错误,
123+
转成可读提示:`test '<name>' 无入口:请写 int main(),或依赖一个提供 main 的测试
124+
框架(如 gtest,不写 main 时由 gtest_main 提供)`。(P2,可后续增强;P0 至少不崩。)
125+
- **多个 lib 依赖**:每个一个 `.a`,按依赖顺序排在对象之后;若库间有相互依赖,
126+
保持拓扑序(已有 `directPackageDeps` 拓扑信息可复用)。
127+
- **静态库目标的根包**(`mcpp build` 产 `.a`)→ 不受影响(那是根包 target,非依赖)。
128+
129+
## 7. 验证策略(TDD)
130+
- **单元**(`tests/unit/test_ninja_backend.cpp` / 新增):给定一个 `kind=lib` 依赖 +
131+
含非模块对象的 plan,断言:(a)生成 `StaticLibrary` LinkUnit;(b)消费者链接行含
132+
该 `.a` 且**位于对象之后**;(c)纯模块依赖**不**生成归档。
133+
- **e2e**(新增 `78_test_main_combinations.sh`,三平台):一个含 `gtest` dev-dep 的
134+
项目,覆盖四种交叉组合各一个 `tests/*.cpp`,断言 `mcpp test` 全绿:
135+
1. 自带 main + 用 gtest;2. 不带 main + 用 gtest(TEST 宏);3. 自带 main + 不用
136+
gtest;4.(可选)无 main 无框架 → 期望清晰错误。
137+
- **回归**:既有 15/16/17(test pass/fail/no-tests)、18(devdeps isolation)、
138+
31(transitive deps)、07/08(static/shared lib)必须仍绿。
139+
140+
## 8. 实施计划(动态更新)
141+
142+
- [x] **P1 plan 模型**:`LinkUnit` 加 `archiveInputs`;为 `kind=lib` 依赖合成
143+
`StaticLibrary` LinkUnit(`lib<pkg>.a`);消费者改为引用 `.a` 而非内联其非模块对象。
144+
(`plan.cppm`:staticDep 检测 + 消费者排除内联 + `archiveInputs` 注入)
145+
- [x] **P2 ninja 发射**:链接行把 `archiveInputs` 经 `$in` 排在对象之后(既上命令行又
146+
被 ninja 依赖追踪)。(`ninja_backend.cppm`)
147+
- [x] **P3 单元测试**:`NinjaBackend.ArchiveInputsLinkedAfterObjects`(归档在对象后);
148+
全量 24 单测绿。
149+
- [x] **P4 e2e**:`78_test_main_combinations.sh` 四组合 `mcpp test` 全绿 + 断言生成
150+
`cxx_archive`。本机验证:3 passed;`build libcompat_gtest.a : cxx_archive
151+
obj/gtest_main.o obj/gtest-all.o`,测试链接行 `…objects… libcompat_gtest.a`。
152+
- [x] **P5 回归**:24 单测 + e2e 15/16/17/18/31/07/08 全绿。
153+
- [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件勾选。
154+
- [ ] **P7 发布闭环**:PR → CI 全平台 → squash --admin 合入 → tag v0.0.64 →
155+
release → 镜像 xlings-res(gh+gtc,4 平台)→ xim-pkgindex mcpp.lua bump(PR)→
156+
索引产物自动发布 → `xlings install mcpp@0.0.64` 验证 → bootstrap pin bump。
157+
(mcpp-index/gtest 描述符**不改** → 无需重发 mcpp-index。)
158+
159+
## 9. 决策备注
160+
1. **由既有元数据驱动,而非新增特例**:`kind="lib"` 早在描述符里;本设计只是让
161+
mcpp 兑现它。符合「约定优于配置 / 用户无感」。
162+
2. **单归档 + 链接器语义** 已覆盖全部 main 交叉组合,无需拆 `gtest_main`、无需让
163+
用户选链接哪个目标——最大化「无感」。
164+
3. **模块对象不归档** 是关键安全边界:避免全局初始化被归档式丢弃,且让纯模块依赖
165+
零回归。
166+
4. **面向未来框架**:任何测试框架在其描述符声明 `kind="lib"` 即自动获得正确入口
167+
语义;mcpp 永不需要认识具体框架。

‎CHANGELOG.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
55
6+
## [0.0.64] — 2026-06-25
7+
8+
### 修复
9+
10+
- **`mcpp test` 在自带 `main()` 的测试 + gtest dev-dep 下 `duplicate symbol: main`**:
11+
gtest 的 `gtest_main.cc` 自带 `main()`,而 mcpp 此前把依赖的**全部对象内联**进每个
12+
测试二进制,于是测试自己的 `main()` 与 `gtest_main.o` 撞符号。修复:**兑现依赖
13+
描述符里已声明的 `kind="lib"`**——把这类依赖编译成静态归档 `lib<pkg>.a`,链接在
14+
测试对象**之后**;标准归档语义只在符号未定义时拉成员,故 `gtest_main.o` 的 `main`
15+
只在测试不自带 `main` 时才被拉入。`{自带/框架 main} × {用/不用 gtest}` 全部组合
16+
皆正确,用户无感。纯模块依赖(如 mcpplibs.cmdline,无非模块对象)行为不变。
17+
这是**通用** link-model 改进、由既有描述符 `kind` 驱动,**无 gtest 特例**,未来
18+
测试框架声明 `kind="lib"` 即自动适配。详见
19+
`.agents/docs/2026-06-25-dependency-archive-linking-design.md`。
20+
21+
### 测试
22+
23+
- 新增单测 `NinjaBackend.ArchiveInputsLinkedAfterObjects`(归档须排在对象之后)与
24+
跨平台 e2e `78_test_main_combinations.sh`(四种 main×gtest 组合 `mcpp test` 全绿)。
25+
626
## [0.0.63] — 2026-06-25
727

828
### 修复

‎mcpp.toml‎

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

‎src/build/ninja_backend.cppm‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,14 @@ std::string emit_ninja_string(const BuildPlan& plan) {
629629
rule = "cxx_shared";
630630
break;
631631
}
632+
// Dependency static archives (kind="lib" deps, e.g. gtest): linked
633+
// AFTER the objects so the linker pulls members (e.g. gtest_main.o's
634+
// own main) only when a symbol is still undefined. Explicit inputs →
635+
// placed on the command line via $in AND dep-tracked for relink.
636+
for (auto& a : lu.archiveInputs) {
637+
ins += " " + escape_ninja_path(a);
638+
}
639+
632640
std::string implicit;
633641
for (auto& input : lu.implicitInputs) {
634642
implicit += " " + escape_ninja_path(input);

‎src/build/plan.cppm‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct LinkUnit {
3030
std::string targetName;
3131
enum Kind { Binary, StaticLibrary, SharedLibrary, TestBinary } kind = Binary;
3232
std::vector<std::filesystem::path> objects; // relative to plan.outputDir
33+
std::vector<std::filesystem::path> archiveInputs; // dep static libs (.a), linked AFTER objects
3334
std::vector<std::filesystem::path> implicitInputs; // relative to plan.outputDir
3435
std::vector<std::string> linkFlags; // per-link edge flags
3536
std::filesystem::path output; // relative to plan.outputDir
@@ -380,6 +381,50 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
380381
}
381382
}
382383

384+
// Static-library dependencies (kind="lib", e.g. gtest): build a per-package
385+
// archive lib<pkg>.a from the package's non-module objects and link it into
386+
// consumers AFTER their own objects. Standard archive semantics then pull
387+
// only the members a symbol still needs — notably gtest_main.o's own main()
388+
// is pulled ONLY when the consumer test defines none — which fixes
389+
// "duplicate symbol: main" across the {own-main, framework-main} ×
390+
// {uses-framework, doesn't} combinations. Module (.cppm) objects are NOT
391+
// archived: they carry global init and must always be linked directly.
392+
// Driven purely by the dependency's declared `kind` — no per-framework
393+
// special-casing, so a future test framework just declares kind="lib".
394+
struct StaticDepArchive {
395+
std::string packageName; // qualified
396+
std::filesystem::path output; // lib<pkg>.a, relative to outputDir
397+
};
398+
std::vector<StaticDepArchive> staticDepArchives;
399+
std::set<std::string> staticDepPackages; // qualified names linked via .a
400+
for (std::size_t i = 1; i < packages.size(); ++i) {
401+
auto const& p = packages[i];
402+
auto qname = qualified_package_name(p.manifest);
403+
if (sharedDepPackages.contains(qname)) continue; // shared takes precedence
404+
bool isLib = false;
405+
for (auto const& t : p.manifest.targets) {
406+
if (t.kind == mcpp::manifest::Target::Library) { isLib = true; break; }
407+
}
408+
if (!isLib) continue;
409+
std::vector<std::filesystem::path> archiveObjs;
410+
for (auto& cu : plan.compileUnits) {
411+
if (cu.packageName != qname) continue;
412+
if (cu.source.extension() == ".cppm") continue; // modules stay loose
413+
if (!is_implementation_source(cu.source)) continue;
414+
if (entryFilesAcrossTargets.contains(cu.source)) continue;
415+
archiveObjs.push_back(cu.object);
416+
}
417+
if (archiveObjs.empty()) continue; // header-only / pure-module lib → nothing to archive
418+
LinkUnit ar;
419+
ar.targetName = qname;
420+
ar.kind = LinkUnit::StaticLibrary;
421+
ar.output = std::filesystem::path("lib" + sanitize(qname) + ".a");
422+
ar.objects = std::move(archiveObjs);
423+
staticDepPackages.insert(qname);
424+
staticDepArchives.push_back({qname, ar.output});
425+
plan.linkUnits.push_back(std::move(ar));
426+
}
427+
383428
std::map<std::size_t, std::vector<std::size_t>> directPackageDeps;
384429
for (std::size_t i = 0; i < packages.size(); ++i) {
385430
for (auto const& [depName, spec] : packages[i].manifest.dependencies) {
@@ -568,6 +613,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
568613
// is exclusive to that binary).
569614
for (auto& cu : plan.compileUnits) {
570615
if (sharedDepPackages.contains(cu.packageName)) continue;
616+
if (staticDepPackages.contains(cu.packageName)) continue; // archived → linked as .a
571617
if (!is_implementation_source(cu.source)) continue;
572618
if (lu.entryMain && cu.source == *lu.entryMain) continue; // own entry: already added above
573619
if (entryFilesAcrossTargets.contains(cu.source)) continue; // foreign entry: skip
@@ -577,6 +623,12 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
577623
if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary
578624
|| lu.kind == LinkUnit::SharedLibrary) {
579625
append_shared_deps_for_linked_objects(lu);
626+
// Link each kind="lib" dependency archive AFTER this unit's objects.
627+
// Harmless when unused: archive members are pulled on demand, so a
628+
// binary that references no symbols from the lib pulls nothing.
629+
for (auto const& sa : staticDepArchives) {
630+
lu.archiveInputs.push_back(sa.output);
631+
}
580632
}
581633

582634
plan.linkUnits.push_back(std::move(lu));

‎src/toolchain/fingerprint.cppm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.63";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.64";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
# requires:
3+
# 78_test_main_combinations.sh — `mcpp test` must handle every combination of
4+
# {test defines its own main, test relies on the framework's main} ×
5+
# {test uses gtest, test doesn't} when gtest is a dev-dependency.
6+
#
7+
# Regression for `ld.lld: duplicate symbol: main`: gtest's gtest_main.cc carries
8+
# its own main(), and mcpp used to inline ALL of a dev-dep's objects into every
9+
# test binary — so a test that defined its own main() collided with gtest_main.
10+
# The fix links kind="lib" dependencies as a static archive (lib<pkg>.a) placed
11+
# AFTER the test's objects, so the linker pulls gtest_main.o ONLY when main is
12+
# still undefined. Driven by the dep's declared kind — no gtest special-casing.
13+
#
14+
# No `requires:` capability → runs on all three CI platforms (mirrors 15/16/17).
15+
set -e
16+
17+
TMP=$(mktemp -d)
18+
trap "rm -rf $TMP" EXIT
19+
20+
cd "$TMP"
21+
"$MCPP" new combo > /dev/null
22+
cd combo
23+
24+
cat >> mcpp.toml <<'EOF'
25+
26+
[dev-dependencies]
27+
gtest = "1.15.2"
28+
EOF
29+
30+
rm -f tests/test_smoke.cpp
31+
32+
# (1) own main + uses gtest → gtest_main.o must NOT be pulled (no dup main)
33+
cat > tests/t_own_main_gtest.cpp <<'EOF'
34+
#include <gtest/gtest.h>
35+
import std;
36+
TEST(A, ok) { EXPECT_EQ(1 + 1, 2); }
37+
int main(int argc, char** argv) {
38+
::testing::InitGoogleTest(&argc, argv);
39+
return RUN_ALL_TESTS();
40+
}
41+
EOF
42+
43+
# (2) no main + uses gtest macros → gtest_main.o provides main
44+
cat > tests/t_framework_main.cpp <<'EOF'
45+
#include <gtest/gtest.h>
46+
TEST(B, ok) { EXPECT_TRUE(true); }
47+
EOF
48+
49+
# (3) own main + does NOT use gtest (but gtest is still a dev-dep) → archive
50+
# contributes nothing; previously this collided with gtest_main's main.
51+
cat > tests/t_own_main_no_gtest.cpp <<'EOF'
52+
import std;
53+
int main() { std::println("ok"); return 0; }
54+
EOF
55+
56+
out=$("$MCPP" test 2>&1) || { echo "FAIL: mcpp test exited non-zero"; echo "$out"; exit 1; }
57+
58+
for t in t_own_main_gtest t_framework_main t_own_main_no_gtest; do
59+
echo "$out" | grep -q "$t ... ok" || { echo "FAIL: $t did not pass"; echo "$out"; exit 1; }
60+
done
61+
echo "$out" | grep -q '3 passed; 0 failed' || { echo "FAIL: summary mismatch"; echo "$out"; exit 1; }
62+
63+
# The dev-dep must be linked as an archive, not inlined object-by-object.
64+
nj=$(find target -name build.ninja | head -1)
65+
grep -q 'cxx_archive' "$nj" || { echo "FAIL: no static archive built for kind=lib dep"; exit 1; }
66+
67+
echo "OK"

0 commit comments

Comments
 (0)