Skip to content

Commit 7d8ee52

Browse files
committed
fix(link): per-consumer archive-vs-inline for kind=lib deps (Windows LNK1561)
A pure-archive approach broke Windows: mcpp links via MSVC lld-link there, which does NOT pull an archive member just to satisfy the entry point — so tests with no own main (relying on gtest_main) failed with LNK1561 'entry point must be defined'. (--start-lib isn't an option: Mach-O lld doesn't support it.) Decide per consumer by scanning its entry source for a main definition: - defines main → link the dep as an archive (member with main not pulled → no duplicate main; entry comes from the test, fine on every linker) - no main → inline the dep's objects directly, so the dep's gtest_main -style entry object provides main on every linker incl. MSVC Generic (only inspects the consumer, not which dep object holds main) and covers all {own/framework main} × {uses/doesn't use gtest} combinations on Linux, macOS and Windows. Also: dep archive now uses platform-aware naming. Verified locally: e2e 78 green; nomain test inlines gtest_main.o, ownmain test links libcompat_gtest.a.
1 parent 95e43a3 commit 7d8ee52

2 files changed

Lines changed: 70 additions & 26 deletions

File tree

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

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,46 @@ mcpp 的 manifest 解析器也**已支持** `kind="lib"` → `Target::Library`
7070

7171
## 4. 核心设计:依赖库 → 静态归档 → 条件链接
7272

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-
> 天然实现条件链接。
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`**(空白不敏感、跳过注释行;
96+
启发式,最坏只是选错链接方式而非出错;探测不到时默认按「无 main」内联=改动前行为)。
97+
**通用**:无需识别「哪个依赖对象提供 main」,只看消费者自己——对任何 `kind="lib"`
98+
依赖、任何未来测试框架都成立。
8899

89100
### 4.2 BuildPlan 变更(`plan.cppm`
90101
对每个 `kind="lib"`**依赖**包(非根包),由其**非模块**实现对象(`.cc/.cpp/.c`)
91102
合成一个 `StaticLibrary` LinkUnit → 产 `lib<pkg>.a`:
92103

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`)仍直接内联**:它们承载模块全局初始化、且从不提供
104+
- 新增 LinkUnit:`{ kind=StaticLibrary, output="bin/<prefix><mangled-pkg><static_lib_ext>",
105+
objects=<dep 的非模块对象> }`。**命名平台感知**(`libfoo.a` / `foo.lib`,复用
106+
`platform::lib_prefix`+`static_lib_ext`,镜像 `target_output`)——硬编码 `.a`
107+
会断 Windows。
108+
- 消费者(Binary/TestBinary/SharedLibrary)按 §4.1 表二选一:
109+
- **自带 main** → 不内联该 dep 的非模块对象,把 `.a` 路径追加到**链接命令、对象
110+
之后**(经 `$in`,既上命令行又被 ninja 依赖追踪);
111+
- **不带 main** → 直接内联该 dep 的非模块对象(走原内联路径)。
112+
- **模块对象(`.cppm``.o`)永远直接内联**:它们承载模块全局初始化、且从不提供
98113
`main`,放进归档可能因「无未定义符号引用」被丢弃 → 破坏全局初始化。故模块对象
99114
不归档(mcpplibs.cmdline 等纯模块依赖 → 无非模块对象 → 无归档 → **行为不变**)。
100115

src/build/plan.cppm

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
543543
}
544544
}
545545

546+
// Whether this consumer's own entry source defines `main`. Decides how
547+
// kind="lib" dependencies are linked (archive vs inline) so the
548+
// gtest_main-style optional entry works on EVERY linker — see the
549+
// dependency-linking block further below. Default false → if we can't
550+
// tell, fall back to inlining (the pre-archive behavior).
551+
bool entryDefinesMain = false;
552+
546553
if ((lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) && lu.entryMain) {
547554
// Add main.cpp -> obj/main.o
548555
CompileUnit main_cu;
@@ -580,6 +587,18 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
580587
}
581588
if (!name.empty()) main_cu.imports.push_back(name);
582589
}
590+
// Detect a top-level `int main(`/`auto main(` definition
591+
// (space-insensitive; skip comment lines). Heuristic, but the
592+
// worst case is a wrong archive-vs-inline choice, not breakage.
593+
if (!entryDefinesMain && !line.starts_with("//") && !line.starts_with("*")) {
594+
std::string nospace;
595+
for (char c : line)
596+
if (!std::isspace(static_cast<unsigned char>(c))) nospace.push_back(c);
597+
if (nospace.find("intmain(") != std::string::npos
598+
|| nospace.find("automain(") != std::string::npos) {
599+
entryDefinesMain = true;
600+
}
601+
}
583602
}
584603

585604
// Avoid duplicate insert if main was already scanned
@@ -619,7 +638,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
619638
// is exclusive to that binary).
620639
for (auto& cu : plan.compileUnits) {
621640
if (sharedDepPackages.contains(cu.packageName)) continue;
622-
if (staticDepPackages.contains(cu.packageName)) continue; // archived → linked as .a
641+
// kind="lib" deps: when THIS consumer defines its own main, link them
642+
// as an archive (below) so the dep's own main-providing object (e.g.
643+
// gtest_main.o) is NOT pulled — no `duplicate symbol: main`. When the
644+
// consumer has NO main, inline the dep objects directly so the dep's
645+
// entry object provides main on EVERY linker (MSVC lld-link does not
646+
// pull an archive member just to satisfy the entry point → LNK1561).
647+
if (entryDefinesMain && staticDepPackages.contains(cu.packageName)) continue;
623648
if (!is_implementation_source(cu.source)) continue;
624649
if (lu.entryMain && cu.source == *lu.entryMain) continue; // own entry: already added above
625650
if (entryFilesAcrossTargets.contains(cu.source)) continue; // foreign entry: skip
@@ -629,11 +654,15 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
629654
if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary
630655
|| lu.kind == LinkUnit::SharedLibrary) {
631656
append_shared_deps_for_linked_objects(lu);
632-
// Link each kind="lib" dependency archive AFTER this unit's objects.
633-
// Harmless when unused: archive members are pulled on demand, so a
634-
// binary that references no symbols from the lib pulls nothing.
635-
for (auto const& sa : staticDepArchives) {
636-
lu.archiveInputs.push_back(sa.output);
657+
// Consumers that define their own main link kind="lib" deps as an
658+
// archive (placed AFTER objects): archive members are pulled on
659+
// demand, so the dep's gtest_main-style entry object is skipped when
660+
// main is already defined, and pulled is never needed here. Consumers
661+
// without their own main inlined the dep objects directly above.
662+
if (entryDefinesMain) {
663+
for (auto const& sa : staticDepArchives) {
664+
lu.archiveInputs.push_back(sa.output);
665+
}
637666
}
638667
}
639668

0 commit comments

Comments
 (0)