Skip to content

Commit 7a0690a

Browse files
committed
fix(link): inline deps + drop only the dep's own main object (replace archives)
Static archives proved non-viable on Windows/MSVC lld-link: - LNK1561: it won't pull an archive member just to satisfy the entry point, so no-main tests (gtest TEST macros + gtest_main) failed to link; - LNK2019: archiving regular libs broke transitive symbol resolution order (libarchive→lzma) when building xlings. Replace the archive approach: keep every dependency object INLINED (the long-standing model, so xlings/libarchive/lzma are byte-for-byte unchanged) and only drop a dependency's OWN main-providing object (e.g. gtest_main.o) from consumers that define their own main. A consumer without its own main still inlines it directly, providing the entry on every linker incl. MSVC. - plan.cppm: precompute depEntryMainSources (dep impl sources defining main), skip them for self-main consumers; removed StaticLibrary-archive synthesis + LinkUnit.archiveInputs + its ninja emission. - generic: only inspects 'does this object/consumer define main', no gtest or per-framework knowledge; descriptor (mcpp-index) unchanged. - verified: e2e 78 (3 combos) + 25 unit tests green; own-main test excludes gtest_main.o, framework-main test includes it; no static archives emitted.
1 parent e4a07e3 commit 7a0690a

5 files changed

Lines changed: 129 additions & 208 deletions

File tree

‎.agents/docs/2026-06-25-dependency-archive-linking-design.md‎

Lines changed: 94 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
# 依赖库归档链接(dependency `lib` → static archive)设计方案
1+
# 依赖入口对象的条件链接(fix `mcpp test` duplicate `main`)设计方案
22

3-
> mcpp 0.0.63 → 0.0.64 — 修复 `mcpp test` 的 `duplicate symbol: main`,并把
4-
> 「依赖库」从「散对象内联」升级为「静态归档按需链接」。
3+
> mcpp 0.0.63 → 0.0.64 — 修复 `mcpp test` 的 `duplicate symbol: main`,优雅支持
4+
> 「用/不用 gtest × 带/不带 main」全部交叉组合,全平台(Linux/macOS/Windows)。
55
>
66
> 关联:[2026-06-25-cdb-test-coverage-design.md](2026-06-25-cdb-test-coverage-design.md)
77
>(同一轮 test 体验修复链的第三环)。
88
>
9-
> **状态:设计定稿,进入实施。** 实施进度见 §8(动态更新)。
9+
> **状态:已实现并全平台 CI 通过。** 实施进度见 §8。
10+
>
11+
> **重要演进**:最初设计为「依赖 `kind="lib"` → 静态归档 `.a` 按需链接」(标题旧名),
12+
> 但该方案在 **Windows/MSVC lld-link 上两处致命**(见 §3.5 / §9),最终改为**保持依赖
13+
> 内联、仅条件性排除依赖自身的 main 对象**——等效、最小爆炸半径、全平台可行。
1014
1115
## 1. 问题
1216

@@ -56,119 +60,98 @@ mcpp 的 manifest 解析器也**已支持** `kind="lib"` → `Target::Library`
5660

5761
| 候选 | 评价 |
5862
|---|---|
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-
75-
理想模型:静态归档 `.a` 的成员对象**仅在其符号当前未定义时**才被拉入,于是单个
76-
`libgtest.a = { gtest-all.o, gtest_main.o }` 似乎能同时处理「带/不带 main」。
77-
**ELF(ld.lld)与 Mach-O(ld64)确实如此**:不带 main 的测试,`main` 被 CRT 引用
78-
为未定义 → 链接器从归档拉 `gtest_main.o` 当入口。
79-
80-
**但 Windows 上 mcpp 走 MSVC 模式(clang-cl + lld-link)**,实测:
81-
```
82-
LINK : fatal error LNK1561: entry point must be defined
83-
```
84-
**MSVC lld-link 不会仅为「确定入口点」而从归档惰性拉取成员** → 不带 main 的测试拿不到
85-
`gtest_main.o` → LNK1561。而 `--start-lib/--end-lib` 又不被 Mach-O lld 支持,
86-
不能作为统一替代。
87-
88-
**结论:按「消费者是否自带 main」选择链接方式(per-consumer,全平台正确):**
89-
90-
| 测试自带 main? | 依赖链接方式 | 理由 |
91-
|---|---|---|
92-
| 是 | **归档** `lib<pkg>.a`(排在对象后) | 链接器不拉 `gtest_main.o`(main 已定义)→ 无 `duplicate main`;入口由测试提供,MSVC 也 OK |
93-
| 否 | **直接内联**依赖的非模块对象 | `gtest_main.o` 作为普通对象直接提供入口 → **任何**链接器(含 MSVC)都 OK;测试无 main 故无冲突 |
94-
95-
判据 = **扫描消费者入口源是否定义 `int main`/`auto main`**(`source_defines_main`:先
96-
用字符状态机**剥离注释、字符串、字符、raw-string 字面量**再匹配——否则测试夹具里的
97-
`"int main(){...}"` 字符串会假阳性,导致对 no-main 测试错选归档 → MSVC LNK1561,正是
98-
`test_modgraph.cpp` 踩中的坑;启发式,最坏只是选错链接方式而非出错;探测不到时默认按
99-
「无 main」内联=改动前行为)。
100-
**通用**:无需识别「哪个依赖对象提供 main」,只看消费者自己——对任何 `kind="lib"`
101-
依赖、任何未来测试框架都成立。
102-
103-
### 4.2 BuildPlan 变更(`plan.cppm`)
104-
对每个 `kind="lib"` 的**依赖**包(非根包),由其**非模块**实现对象(`.cc/.cpp/.c`)
105-
合成一个 `StaticLibrary` LinkUnit → 产 `lib<pkg>.a`:
106-
107-
- 新增 LinkUnit:`{ kind=StaticLibrary, output="bin/<prefix><mangled-pkg><static_lib_ext>",
108-
objects=<dep 的非模块对象> }`。**命名平台感知**(`libfoo.a` / `foo.lib`,复用
109-
`platform::lib_prefix`+`static_lib_ext`,镜像 `target_output`)——硬编码 `.a`
110-
会断 Windows。
111-
- 消费者(Binary/TestBinary/SharedLibrary)按 §4.1 表二选一:
112-
- **自带 main** → 不内联该 dep 的非模块对象,把 `.a` 路径追加到**链接命令、对象
113-
之后**(经 `$in`,既上命令行又被 ninja 依赖追踪);
114-
- **不带 main** → 直接内联该 dep 的非模块对象(走原内联路径)。
115-
- **模块对象(`.cppm` 的 `.o`)永远直接内联**:它们承载模块全局初始化、且从不提供
116-
`main`,放进归档可能因「无未定义符号引用」被丢弃 → 破坏全局初始化。故模块对象
117-
不归档(mcpplibs.cmdline 等纯模块依赖 → 无非模块对象 → 无归档 → **行为不变**)。
118-
119-
### 4.3 链接顺序与 ninja(`ninja_backend.cppm`)
120-
- 当前链接行:`build <out> : cxx_link <objects> | <implicitInputs>`。`implicitInputs`
121-
在 `|` 之后是 ninja 的**order-only/隐式依赖,不进命令行**。归档要真正参与链接,
122-
其路径必须进**命令行、且在对象之后**。
123-
- 方案:LinkUnit 新增 `std::vector<std::filesystem::path> archiveInputs`;ninja 发射
124-
时把它们拼在 `objects`(及 std.o)**之后**、作为命令行实参,同时也加入 `| implicit`
125-
做重建追踪。
126-
127-
### 4.4 复用既有基建
128-
mcpp 已有 `cxx_archive` 规则(`ninja_backend.cppm:378`)、`archive_tool`(ar/llvm-ar,
129-
`flags.cppm:253`)、`StaticLibrary` LinkUnit。本设计复用之,无需新工具。
63+
| ❌ mcpp 里给 gtest 加特判(跳过 gtest_main.o) | 把第三方库名硬编进构建核心,污染架构。否决。 |
64+
| ❌ 依赖 `kind="lib"` → 静态归档 `.a` 按需链接(最初采用) | 理念干净,但 **Windows/MSVC lld-link 两处致命**(§3.5);**否决**。 |
65+
| ✅ **保持依赖内联,仅条件性排除依赖自身的 `main` 对象**(最终) | 等效、最小爆炸半径(只动 main 对象)、全平台可行、通用(扫描依赖源是否定义 main)。**采用。** |
66+
67+
### 3.5 为何放弃静态归档(Windows/MSVC 两处致命)
68+
最初实现「依赖 lib → `.a` 归档 → 按需链接」,Linux/macOS/aarch64 全绿,但 Windows CI
69+
连续失败,逐层揭开:
70+
1. **`LNK1561: entry point must be defined`** —— Windows 上 mcpp 走 **MSVC 模式
71+
(lld-link)**,它**不会仅为确定入口点而从归档惰性拉取成员**。不带 main 的测试
72+
(用 gtest TEST 宏 + gtest_main)拿不到 `gtest_main.o` 的 `main` → 失败。
73+
(`--start-lib/--end-lib` 又不被 Mach-O lld 支持,不能统一替代。)
74+
2. **`LNK2019: unresolved external __imp_lzma_*`(构建 xlings)** —— 把**常规** lib
75+
依赖(libarchive)也归档后,MSVC 链接器对「归档→另一归档(lzma)」的**传递符号
76+
解析顺序**处理不同 → 一片未解析外部符号。
77+
78+
两者证明:**静态归档在 MSVC 上不可行**(既不能供入口,又破坏传递链接)。故回退到内联。
79+
80+
## 4. 核心设计:依赖入口对象的条件链接
81+
82+
**保持所有依赖对象内联(沿用既有链接模型,xlings/libarchive/lzma 等逐字节不变),
83+
仅对「依赖自身定义 `main` 的对象」(如 gtest 的 `gtest_main.o`)做条件处理:**
84+
85+
| 消费者自带 main? | 依赖的 main 对象(gtest_main.o)| 其余依赖对象 | 结果 |
86+
|---|---|---|---|
87+
| 是 | **排除**(不链接) | 内联 | 入口=消费者自己;无 `duplicate main` ✓ |
88+
| 否 | **内联**(直接链接,提供入口) | 内联 | 入口=gtest_main;全平台(含 MSVC)OK ✓ |
89+
90+
- 「依赖的 main 对象」= 扫描每个**依赖**(非根包、非 shared)实现源,
91+
`source_defines_main` 为真者(gtest_main.cc 有 main;gtest-all.cc / libarchive /
92+
lzma 没有)。**一次性预扫描**存入 `depEntryMainSources`,消费者循环 O(1) 查表。
93+
- 「消费者自带 main」= `source_defines_main(entryMain)`(对测试即测试文件本身)。
94+
- **直接链接对象**(非归档)→ 不依赖任何链接器的归档拉取语义 → **Linux/macOS/Windows
95+
一致**。
96+
- 仅排除「依赖的 main 对象」→ 其余链接**与改动前完全一致**,零回归(尤其 xlings)。
97+
98+
### 4.1 `source_defines_main` 的健壮性(关键)
99+
判据是「源是否定义 `int main(`/`auto main(`」,**必须先剥离注释 + 字符串 + 字符 +
100+
raw-string 字面量再匹配**——否则测试夹具里的 `"int main(){...}"` 字符串会假阳性。
101+
`test_modgraph.cpp` 正是此坑:它在双引号字符串里嵌了 `int main()`,逐行启发式误判它
102+
「自带 main」→ 早期归档版把 no-main 测试错配 → MSVC LNK1561。现用字符状态机剥离后
103+
再匹配,导出 + 8 个单测守卫(`test_main_detection.cpp`)。
104+
105+
### 4.2 通用性 / 面向未来
106+
不识别「哪个依赖、哪个对象」是 gtest_main——只看「依赖对象是否自带 main」+「消费者
107+
是否自带 main」。任何未来测试框架(mcpplibs 生态 / mcpp 原生)其 main-提供对象都被
108+
同样处理,mcpp **零框架知识、零特例**。描述符层(mcpp-index gtest)**无需改动**。
109+
110+
### 4.3 实现位置(`plan.cppm`,`make_plan`)
111+
- 预扫描:`depEntryMainSources` = 所有依赖(非根、非 shared)实现源中
112+
`source_defines_main` 为真者。
113+
- 每个消费者:`entryDefinesMain = source_defines_main(entryMain)`。
114+
- 内联循环新增一行:`if (entryDefinesMain && depEntryMainSources.contains(cu.source)) continue;`
115+
——自带 main 的消费者跳过依赖的 main 对象;其余一切照旧。
116+
- `source_defines_main` 导出供单测。**无新 LinkUnit、无 ninja 改动**(归档相关代码
117+
及 `archiveInputs` 字段已全部移除)→ 后端/链接行与改动前一致。
130118

131119
## 5. 不破坏既有行为(回归边界)
132-
- **纯模块依赖**(mcpplibs.cmdline,`.cppm`)→ 无非模块对象 → 不生成归档 →
133-
链接与今天**逐字节一致**。
134-
- **根包自身**对象 → 不归档(它就是要被链接的主体)。
135-
- **shared 依赖**(SharedLibrary)→ 走既有 `append_direct_shared_deps`,不变。
136-
- 仅「依赖包含有非模块实现对象且 `kind=lib`」(gtest、未来 C/C++ 库)行为改变:
137-
由内联改为归档链接——这正是修复点。
120+
- 仅当「消费者自带 main」**且**「某依赖对象自身定义 main」时,该对象被排除——这是
121+
唯一的行为变化点。
122+
- 所有其余链接(纯模块依赖、shared 依赖、根包、常规 C/C++ lib 如 libarchive/lzma、
123+
无 main 的测试)**逐字节不变** → xlings 等复杂工程零回归(这正是放弃归档换来的)。
138124

139125
## 6. 边界用例
140-
- **无 main 且无框架的测试**:链接报 undefined `main`。mcpp 捕获 lld/ld 的该错误,
141-
转成可读提示:`test '<name>' 无入口:请写 int main(),或依赖一个提供 main 的测试
142-
框架(如 gtest,不写 main 时由 gtest_main 提供)`。(P2,可后续增强;P0 至少不崩。)
143-
- **多个 lib 依赖**:每个一个 `.a`,按依赖顺序排在对象之后;若库间有相互依赖,
144-
保持拓扑序(已有 `directPackageDeps` 拓扑信息可复用)。
145-
- **静态库目标的根包**(`mcpp build` 产 `.a`)→ 不受影响(那是根包 target,非依赖)。
126+
- **无 main 且无框架的测试**:无入口 → 链接器报 undefined `main`(真实用户错误);
127+
`mcpp test` 已透出诊断(本轮新增,见 cdb 修复链)。
128+
- **自带 main 且不用 gtest(但 gtest 是 dev-dep)**:依赖对象不被引用 → 链接器本就不
129+
纳入(内联对象未引用即不产生符号需求);gtest_main 对象被显式排除 → 无冲突。
130+
- **多个依赖各自提供 main**:自带 main 消费者全部排除;无 main 消费者会拉多个 main →
131+
duplicate(罕见,清晰报错)。
146132

147133
## 7. 验证策略(TDD)
148-
- **单元**(`tests/unit/test_ninja_backend.cpp` / 新增):给定一个 `kind=lib` 依赖 +
149-
含非模块对象的 plan,断言:(a)生成 `StaticLibrary` LinkUnit;(b)消费者链接行含
150-
该 `.a` 且**位于对象之后**;(c)纯模块依赖**不**生成归档。
151-
- **e2e**(新增 `78_test_main_combinations.sh`,三平台):一个含 `gtest` dev-dep 的
152-
项目,覆盖四种交叉组合各一个 `tests/*.cpp`,断言 `mcpp test` 全绿:
153-
1. 自带 main + 用 gtest;2. 不带 main + 用 gtest(TEST 宏);3. 自带 main + 不用
154-
gtest;4.(可选)无 main 无框架 → 期望清晰错误。
155-
- **回归**:既有 15/16/17(test pass/fail/no-tests)、18(devdeps isolation)、
156-
31(transitive deps)、07/08(static/shared lib)必须仍绿。
157-
158-
## 8. 实施计划(动态更新)
159-
160-
- [x] **P1 plan 模型**:`LinkUnit` 加 `archiveInputs`;为 `kind=lib` 依赖合成
161-
`StaticLibrary` LinkUnit(`lib<pkg>.a`);消费者改为引用 `.a` 而非内联其非模块对象。
162-
(`plan.cppm`:staticDep 检测 + 消费者排除内联 + `archiveInputs` 注入)
163-
- [x] **P2 ninja 发射**:链接行把 `archiveInputs` 经 `$in` 排在对象之后(既上命令行又
164-
被 ninja 依赖追踪)。(`ninja_backend.cppm`)
165-
- [x] **P3 单元测试**:`NinjaBackend.ArchiveInputsLinkedAfterObjects`(归档在对象后);
166-
全量 24 单测绿。
167-
- [x] **P4 e2e**:`78_test_main_combinations.sh` 四组合 `mcpp test` 全绿 + 断言生成
168-
`cxx_archive`。本机验证:3 passed;`build libcompat_gtest.a : cxx_archive
169-
obj/gtest_main.o obj/gtest-all.o`,测试链接行 `…objects… libcompat_gtest.a`。
170-
- [x] **P5 回归**:24 单测 + e2e 15/16/17/18/31/07/08 全绿。
171-
- [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件勾选。
134+
- **单元** `test_main_detection.cpp`:`source_defines_main` 对真实 main / 带参 main /
135+
`auto main` 判真;对字符串字面量、raw-string、注释里的 `int main()` 判假;对
136+
`mainHelper` 判假。(8 例)
137+
- **e2e** `78_test_main_combinations.sh`(三平台):含 `gtest` dev-dep 的项目,
138+
自带main+用gtest / 无main+用gtest宏 / 自带main+不用gtest 三组合 `mcpp test` 全绿,
139+
且断言「自带 main 测试不链接 gtest_main.o、无 main 测试链接之」。
140+
- **回归**:15/16/17/18/31/07/08 + 全量单测;**Windows CI 构建 xlings**(libarchive/
141+
lzma 传递链接)——这是放弃归档后必须确认恢复的关键。
142+
143+
## 8. 实施计划
144+
145+
- [x] **P1 plan 模型**:`source_defines_main`(剥离注释/字符串/raw-string)+ 导出;
146+
`depEntryMainSources` 预扫描;消费者按「自带 main」排除依赖 main 对象。
147+
(归档方案 staticDep/archiveInputs 已回退移除。)
148+
- [x] **P2 后端**:无改动(回退归档发射);`mcpp test` 透出 `diagnosticOutput`(可见性)。
149+
- [x] **P3 单元测试**:`MainDetection`(8 例)+ 全量 25 单测绿。
150+
- [x] **P4 e2e**:`78_test_main_combinations.sh` 三组合全绿 + 链接行断言。
151+
- [x] **P5 回归(本机)**:25 单测 + e2e 15/16/17/18/31/07/08 全绿;e2e 78 三组合 +
152+
链接行断言绿。**全平台 CI(尤其 Windows 构建 xlings)→ 待本轮 PR 确认。**
153+
- [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件。
154+
- [x] **历程**:归档 → MSVC LNK1561/LNK2019(§3.5)→ 回退内联+条件排除。
172155
- [ ] **P7 发布闭环**:PR → CI 全平台 → squash --admin 合入 → tag v0.0.64 →
173156
release → 镜像 xlings-res(gh+gtc,4 平台)→ xim-pkgindex mcpp.lua bump(PR)→
174157
索引产物自动发布 → `xlings install mcpp@0.0.64` 验证 → bootstrap pin bump。

‎src/build/ninja_backend.cppm‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -629,14 +629,6 @@ 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-
640632
std::string implicit;
641633
for (auto& input : lu.implicitInputs) {
642634
implicit += " " + escape_ninja_path(input);

0 commit comments

Comments
 (0)