From 8411adbc191cfde2141b3a17fe214b9aedab5c89 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 06:43:25 +0800 Subject: [PATCH 01/10] fix(link): link kind="lib" deps as static archives (fixes test duplicate main) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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.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 --- ...06-25-dependency-archive-linking-design.md | 167 ++++++++++++++++++ CHANGELOG.md | 20 +++ mcpp.toml | 2 +- src/build/ninja_backend.cppm | 8 + src/build/plan.cppm | 52 ++++++ src/toolchain/fingerprint.cppm | 2 +- tests/e2e/78_test_main_combinations.sh | 67 +++++++ tests/unit/test_ninja_backend.cpp | 24 +++ 8 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 .agents/docs/2026-06-25-dependency-archive-linking-design.md create mode 100755 tests/e2e/78_test_main_combinations.sh diff --git a/.agents/docs/2026-06-25-dependency-archive-linking-design.md b/.agents/docs/2026-06-25-dependency-archive-linking-design.md new file mode 100644 index 00000000..f51232ef --- /dev/null +++ b/.agents/docs/2026-06-25-dependency-archive-linking-design.md @@ -0,0 +1,167 @@ +# 依赖库归档链接(dependency `lib` → static archive)设计方案 + +> mcpp 0.0.63 → 0.0.64 — 修复 `mcpp test` 的 `duplicate symbol: main`,并把 +> 「依赖库」从「散对象内联」升级为「静态归档按需链接」。 +> +> 关联:[2026-06-25-cdb-test-coverage-design.md](2026-06-25-cdb-test-coverage-design.md) +>(同一轮 test 体验修复链的第三环)。 +> +> **状态:设计定稿,进入实施。** 实施进度见 §8(动态更新)。 + +## 1. 问题 + +含 `[dev-dependencies] gtest` 的项目,若测试文件自带 `int main()`(脚手架默认模板 +就是),`mcpp test` 链接失败: + +``` +ld.lld: error: duplicate symbol: main +>>> obj/test_smoke.o:(main) ← 测试自己的 main() +>>> obj/gtest_main.o:(.text+0x0) ← gtest_main.cc 自带的 main() +``` + +测试二进制同时链接了测试自身的 `main` 与 gtest 的 `gtest_main.o`(也含 `main`)。 + +需优雅支持的**交叉组合**(用户尽量无感): + +| 用 gtest? | 自带 main? | 期望 | +|---|---|---| +| 否 | 是 | 链接测试自己的 main(今天 fresh 项目即此,正常)| +| 是 | 是(调用 `InitGoogleTest`/`RUN_ALL_TESTS`)| 用测试的 main,**不要** gtest_main | +| 是 | 否(只写 `TEST(...)`)| 用 gtest_main 提供的 main | +| 否 | 否 | 无入口——**清晰报错**,非崩溃 | + +## 2. 根因 + +`compat.gtest` 描述符(`mcpplibs/mcpp-index/pkgs/c/compat.gtest.lua`)**已声明**: + +```lua +mcpp = { + sources = { "*/googletest/src/gtest-all.cc", "*/googletest/src/gtest_main.cc" }, + targets = { ["gtest"] = { kind = "lib" } }, -- 已建模为「库」 +} +``` + +mcpp 的 manifest 解析器也**已支持** `kind="lib"` → `Target::Library` +(`manifest.cppm:637`)。**但 mcpp 的依赖链接模型没有兑现它**: + +- `plan.cppm` 的 link-unit 循环只为**根包**的 `[targets.*]` 建 LinkUnit; +- 依赖包(含 dev-dep)的对象通过 `append_package_objects` / compileUnit 循环 + **直接内联**进消费者二进制(`plan.cppm:486–575`); +- 于是 `gtest-all.o`、`gtest_main.o` 作为**散对象**被无条件灌入测试二进制 → + `gtest_main.o` 的 `main` 与测试自身 `main` 冲突。 + +**这不是 gtest 的缺陷,也不是描述符的缺陷,而是 mcpp 核心 link 模型的缺口。** + +## 3. 架构决策:修复归属(回答「改 mcpp-index 还是 mcpp」) + +| 候选 | 评价 | +|---|---| +| ❌ mcpp 里给 gtest 加特判(跳过 gtest_main.o) | 把第三方库名硬编进构建核心,污染架构,未来每个框架都要再特判。否决。 | +| ❌ 改 gtest 描述符拆出 `gtest_main` 单独目标 | 描述符**已**声明 `kind="lib"`,够用;拆分增加每个库包的作者负担,且仍需 mcpp 支持「按归档链接」。非必要。 | +| ✅ **mcpp 核心:兑现依赖的 `kind="lib"` → 建静态归档 `.a` → 按归档链接** | 通用、由**既有**描述符元数据驱动、零 gtest 特例、未来框架自动适配。**采用。** | + +**职责分层(干净):** +- **描述符(mcpp-index)**:声明「我是什么」——`kind="lib"`。gtest 已声明,**不改**。 +- **mcpp 核心**:实现「怎么链」——依赖的 lib 包 → 归档 → 按归档链接。**通用 HOW。** + +**未来演进**:mcpplibs 生态测试框架 / mcpp 原生测试框架,只要在各自描述符里声明 +`kind="lib"`(并可选地提供一个含 `main` 的入口对象),就**自动**获得「带 main 用 +自己的、不带 main 用框架的」正确行为,mcpp 无需任何该框架的知识。 + +## 4. 核心设计:依赖库 → 静态归档 → 条件链接 + +### 4.1 原理(标准链接器语义) +静态归档 `.a` 的成员对象**仅在其提供的符号当前未定义时**才被拉入。于是单个 +`libgtest.a = { gtest-all.o, gtest_main.o }` 即同时正确处理「带/不带 main」: + +- 测试自带 `main` → 链接器处理归档时 `main` 已定义 → **不拉** `gtest_main.o`; + 测试引用的 gtest 符号 → 拉 `gtest-all.o`。✓ +- 测试不带 `main` → `main` 未定义 → 拉 `gtest_main.o`(它提供 main + 调 + `RUN_ALL_TESTS`)+ `gtest-all.o`。✓ +- 测试自带 main 且**不用** gtest(但 gtest 是 dev-dep)→ 归档**零贡献**(无未定义 + 符号引用它)→ 不再撞 main。✓(今天的失败用例) +- 测试不带 main 且不用框架 → 无入口 → 链接器报 undefined `main`。mcpp 捕获并给出 + 清晰提示(见 §6)。✓ + +> 单归档即可,**无需**把 gtest_main 拆成独立 target——归档的「按成员对象解析」已 +> 天然实现条件链接。 + +### 4.2 BuildPlan 变更(`plan.cppm`) +对每个 `kind="lib"` 的**依赖**包(非根包),由其**非模块**实现对象(`.cc/.cpp/.c`) +合成一个 `StaticLibrary` LinkUnit → 产 `lib.a`: + +- 新增 LinkUnit:`{ kind=StaticLibrary, output="lib.a", objects= }`。 +- 消费者(Binary/TestBinary/SharedLibrary)**不再内联**该 dep 的非模块对象,改为: + - 把 `lib.a` 路径追加到**链接命令、对象之后**(符号解析顺序正确); + - 把 `.a` 加入 `implicitInputs`(ninja 重建追踪)。 +- **模块对象(`.cppm` 的 `.o`)仍直接内联**:它们承载模块全局初始化、且从不提供 + `main`,放进归档可能因「无未定义符号引用」被丢弃 → 破坏全局初始化。故模块对象 + 不归档(mcpplibs.cmdline 等纯模块依赖 → 无非模块对象 → 无归档 → **行为不变**)。 + +### 4.3 链接顺序与 ninja(`ninja_backend.cppm`) +- 当前链接行:`build : cxx_link | `。`implicitInputs` + 在 `|` 之后是 ninja 的**order-only/隐式依赖,不进命令行**。归档要真正参与链接, + 其路径必须进**命令行、且在对象之后**。 +- 方案:LinkUnit 新增 `std::vector archiveInputs`;ninja 发射 + 时把它们拼在 `objects`(及 std.o)**之后**、作为命令行实参,同时也加入 `| implicit` + 做重建追踪。 + +### 4.4 复用既有基建 +mcpp 已有 `cxx_archive` 规则(`ninja_backend.cppm:378`)、`archive_tool`(ar/llvm-ar, +`flags.cppm:253`)、`StaticLibrary` LinkUnit。本设计复用之,无需新工具。 + +## 5. 不破坏既有行为(回归边界) +- **纯模块依赖**(mcpplibs.cmdline,`.cppm`)→ 无非模块对象 → 不生成归档 → + 链接与今天**逐字节一致**。 +- **根包自身**对象 → 不归档(它就是要被链接的主体)。 +- **shared 依赖**(SharedLibrary)→ 走既有 `append_direct_shared_deps`,不变。 +- 仅「依赖包含有非模块实现对象且 `kind=lib`」(gtest、未来 C/C++ 库)行为改变: + 由内联改为归档链接——这正是修复点。 + +## 6. 边界用例 +- **无 main 且无框架的测试**:链接报 undefined `main`。mcpp 捕获 lld/ld 的该错误, + 转成可读提示:`test '' 无入口:请写 int main(),或依赖一个提供 main 的测试 + 框架(如 gtest,不写 main 时由 gtest_main 提供)`。(P2,可后续增强;P0 至少不崩。) +- **多个 lib 依赖**:每个一个 `.a`,按依赖顺序排在对象之后;若库间有相互依赖, + 保持拓扑序(已有 `directPackageDeps` 拓扑信息可复用)。 +- **静态库目标的根包**(`mcpp build` 产 `.a`)→ 不受影响(那是根包 target,非依赖)。 + +## 7. 验证策略(TDD) +- **单元**(`tests/unit/test_ninja_backend.cpp` / 新增):给定一个 `kind=lib` 依赖 + + 含非模块对象的 plan,断言:(a)生成 `StaticLibrary` LinkUnit;(b)消费者链接行含 + 该 `.a` 且**位于对象之后**;(c)纯模块依赖**不**生成归档。 +- **e2e**(新增 `78_test_main_combinations.sh`,三平台):一个含 `gtest` dev-dep 的 + 项目,覆盖四种交叉组合各一个 `tests/*.cpp`,断言 `mcpp test` 全绿: + 1. 自带 main + 用 gtest;2. 不带 main + 用 gtest(TEST 宏);3. 自带 main + 不用 + gtest;4.(可选)无 main 无框架 → 期望清晰错误。 +- **回归**:既有 15/16/17(test pass/fail/no-tests)、18(devdeps isolation)、 + 31(transitive deps)、07/08(static/shared lib)必须仍绿。 + +## 8. 实施计划(动态更新) + +- [x] **P1 plan 模型**:`LinkUnit` 加 `archiveInputs`;为 `kind=lib` 依赖合成 + `StaticLibrary` LinkUnit(`lib.a`);消费者改为引用 `.a` 而非内联其非模块对象。 + (`plan.cppm`:staticDep 检测 + 消费者排除内联 + `archiveInputs` 注入) +- [x] **P2 ninja 发射**:链接行把 `archiveInputs` 经 `$in` 排在对象之后(既上命令行又 + 被 ninja 依赖追踪)。(`ninja_backend.cppm`) +- [x] **P3 单元测试**:`NinjaBackend.ArchiveInputsLinkedAfterObjects`(归档在对象后); + 全量 24 单测绿。 +- [x] **P4 e2e**:`78_test_main_combinations.sh` 四组合 `mcpp test` 全绿 + 断言生成 + `cxx_archive`。本机验证:3 passed;`build libcompat_gtest.a : cxx_archive + obj/gtest_main.o obj/gtest-all.o`,测试链接行 `…objects… libcompat_gtest.a`。 +- [x] **P5 回归**:24 单测 + e2e 15/16/17/18/31/07/08 全绿。 +- [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件勾选。 +- [ ] **P7 发布闭环**:PR → CI 全平台 → squash --admin 合入 → tag v0.0.64 → + release → 镜像 xlings-res(gh+gtc,4 平台)→ xim-pkgindex mcpp.lua bump(PR)→ + 索引产物自动发布 → `xlings install mcpp@0.0.64` 验证 → bootstrap pin bump。 + (mcpp-index/gtest 描述符**不改** → 无需重发 mcpp-index。) + +## 9. 决策备注 +1. **由既有元数据驱动,而非新增特例**:`kind="lib"` 早在描述符里;本设计只是让 + mcpp 兑现它。符合「约定优于配置 / 用户无感」。 +2. **单归档 + 链接器语义** 已覆盖全部 main 交叉组合,无需拆 `gtest_main`、无需让 + 用户选链接哪个目标——最大化「无感」。 +3. **模块对象不归档** 是关键安全边界:避免全局初始化被归档式丢弃,且让纯模块依赖 + 零回归。 +4. **面向未来框架**:任何测试框架在其描述符声明 `kind="lib"` 即自动获得正确入口 + 语义;mcpp 永不需要认识具体框架。 diff --git a/CHANGELOG.md b/CHANGELOG.md index acc969ce..1b647f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.64] — 2026-06-25 + +### 修复 + +- **`mcpp test` 在自带 `main()` 的测试 + gtest dev-dep 下 `duplicate symbol: main`**: + gtest 的 `gtest_main.cc` 自带 `main()`,而 mcpp 此前把依赖的**全部对象内联**进每个 + 测试二进制,于是测试自己的 `main()` 与 `gtest_main.o` 撞符号。修复:**兑现依赖 + 描述符里已声明的 `kind="lib"`**——把这类依赖编译成静态归档 `lib.a`,链接在 + 测试对象**之后**;标准归档语义只在符号未定义时拉成员,故 `gtest_main.o` 的 `main` + 只在测试不自带 `main` 时才被拉入。`{自带/框架 main} × {用/不用 gtest}` 全部组合 + 皆正确,用户无感。纯模块依赖(如 mcpplibs.cmdline,无非模块对象)行为不变。 + 这是**通用** link-model 改进、由既有描述符 `kind` 驱动,**无 gtest 特例**,未来 + 测试框架声明 `kind="lib"` 即自动适配。详见 + `.agents/docs/2026-06-25-dependency-archive-linking-design.md`。 + +### 测试 + +- 新增单测 `NinjaBackend.ArchiveInputsLinkedAfterObjects`(归档须排在对象之后)与 + 跨平台 e2e `78_test_main_combinations.sh`(四种 main×gtest 组合 `mcpp test` 全绿)。 + ## [0.0.63] — 2026-06-25 ### 修复 diff --git a/mcpp.toml b/mcpp.toml index d468cd4f..d3845445 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.63" +version = "0.0.64" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index b0184b5f..419f26b5 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -629,6 +629,14 @@ std::string emit_ninja_string(const BuildPlan& plan) { rule = "cxx_shared"; break; } + // Dependency static archives (kind="lib" deps, e.g. gtest): linked + // AFTER the objects so the linker pulls members (e.g. gtest_main.o's + // own main) only when a symbol is still undefined. Explicit inputs → + // placed on the command line via $in AND dep-tracked for relink. + for (auto& a : lu.archiveInputs) { + ins += " " + escape_ninja_path(a); + } + std::string implicit; for (auto& input : lu.implicitInputs) { implicit += " " + escape_ninja_path(input); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 60895b08..27eeb9b7 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -30,6 +30,7 @@ struct LinkUnit { std::string targetName; enum Kind { Binary, StaticLibrary, SharedLibrary, TestBinary } kind = Binary; std::vector objects; // relative to plan.outputDir + std::vector archiveInputs; // dep static libs (.a), linked AFTER objects std::vector implicitInputs; // relative to plan.outputDir std::vector linkFlags; // per-link edge flags std::filesystem::path output; // relative to plan.outputDir @@ -380,6 +381,50 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, } } + // Static-library dependencies (kind="lib", e.g. gtest): build a per-package + // archive lib.a from the package's non-module objects and link it into + // consumers AFTER their own objects. Standard archive semantics then pull + // only the members a symbol still needs — notably gtest_main.o's own main() + // is pulled ONLY when the consumer test defines none — which fixes + // "duplicate symbol: main" across the {own-main, framework-main} × + // {uses-framework, doesn't} combinations. Module (.cppm) objects are NOT + // archived: they carry global init and must always be linked directly. + // Driven purely by the dependency's declared `kind` — no per-framework + // special-casing, so a future test framework just declares kind="lib". + struct StaticDepArchive { + std::string packageName; // qualified + std::filesystem::path output; // lib.a, relative to outputDir + }; + std::vector staticDepArchives; + std::set staticDepPackages; // qualified names linked via .a + for (std::size_t i = 1; i < packages.size(); ++i) { + auto const& p = packages[i]; + auto qname = qualified_package_name(p.manifest); + if (sharedDepPackages.contains(qname)) continue; // shared takes precedence + bool isLib = false; + for (auto const& t : p.manifest.targets) { + if (t.kind == mcpp::manifest::Target::Library) { isLib = true; break; } + } + if (!isLib) continue; + std::vector archiveObjs; + for (auto& cu : plan.compileUnits) { + if (cu.packageName != qname) continue; + if (cu.source.extension() == ".cppm") continue; // modules stay loose + if (!is_implementation_source(cu.source)) continue; + if (entryFilesAcrossTargets.contains(cu.source)) continue; + archiveObjs.push_back(cu.object); + } + if (archiveObjs.empty()) continue; // header-only / pure-module lib → nothing to archive + LinkUnit ar; + ar.targetName = qname; + ar.kind = LinkUnit::StaticLibrary; + ar.output = std::filesystem::path("lib" + sanitize(qname) + ".a"); + ar.objects = std::move(archiveObjs); + staticDepPackages.insert(qname); + staticDepArchives.push_back({qname, ar.output}); + plan.linkUnits.push_back(std::move(ar)); + } + std::map> directPackageDeps; for (std::size_t i = 0; i < packages.size(); ++i) { for (auto const& [depName, spec] : packages[i].manifest.dependencies) { @@ -568,6 +613,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // is exclusive to that binary). for (auto& cu : plan.compileUnits) { if (sharedDepPackages.contains(cu.packageName)) continue; + if (staticDepPackages.contains(cu.packageName)) continue; // archived → linked as .a if (!is_implementation_source(cu.source)) continue; if (lu.entryMain && cu.source == *lu.entryMain) continue; // own entry: already added above if (entryFilesAcrossTargets.contains(cu.source)) continue; // foreign entry: skip @@ -577,6 +623,12 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary || lu.kind == LinkUnit::SharedLibrary) { append_shared_deps_for_linked_objects(lu); + // Link each kind="lib" dependency archive AFTER this unit's objects. + // Harmless when unused: archive members are pulled on demand, so a + // binary that references no symbols from the lib pulls nothing. + for (auto const& sa : staticDepArchives) { + lu.archiveInputs.push_back(sa.output); + } } plan.linkUnits.push_back(std::move(lu)); diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index cda6b62e..8073d530 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.63"; +inline constexpr std::string_view MCPP_VERSION = "0.0.64"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/78_test_main_combinations.sh b/tests/e2e/78_test_main_combinations.sh new file mode 100755 index 00000000..68a339c8 --- /dev/null +++ b/tests/e2e/78_test_main_combinations.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# requires: +# 78_test_main_combinations.sh — `mcpp test` must handle every combination of +# {test defines its own main, test relies on the framework's main} × +# {test uses gtest, test doesn't} when gtest is a dev-dependency. +# +# Regression for `ld.lld: duplicate symbol: main`: gtest's gtest_main.cc carries +# its own main(), and mcpp used to inline ALL of a dev-dep's objects into every +# test binary — so a test that defined its own main() collided with gtest_main. +# The fix links kind="lib" dependencies as a static archive (lib.a) placed +# AFTER the test's objects, so the linker pulls gtest_main.o ONLY when main is +# still undefined. Driven by the dep's declared kind — no gtest special-casing. +# +# No `requires:` capability → runs on all three CI platforms (mirrors 15/16/17). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +"$MCPP" new combo > /dev/null +cd combo + +cat >> mcpp.toml <<'EOF' + +[dev-dependencies] +gtest = "1.15.2" +EOF + +rm -f tests/test_smoke.cpp + +# (1) own main + uses gtest → gtest_main.o must NOT be pulled (no dup main) +cat > tests/t_own_main_gtest.cpp <<'EOF' +#include +import std; +TEST(A, ok) { EXPECT_EQ(1 + 1, 2); } +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +EOF + +# (2) no main + uses gtest macros → gtest_main.o provides main +cat > tests/t_framework_main.cpp <<'EOF' +#include +TEST(B, ok) { EXPECT_TRUE(true); } +EOF + +# (3) own main + does NOT use gtest (but gtest is still a dev-dep) → archive +# contributes nothing; previously this collided with gtest_main's main. +cat > tests/t_own_main_no_gtest.cpp <<'EOF' +import std; +int main() { std::println("ok"); return 0; } +EOF + +out=$("$MCPP" test 2>&1) || { echo "FAIL: mcpp test exited non-zero"; echo "$out"; exit 1; } + +for t in t_own_main_gtest t_framework_main t_own_main_no_gtest; do + echo "$out" | grep -q "$t ... ok" || { echo "FAIL: $t did not pass"; echo "$out"; exit 1; } +done +echo "$out" | grep -q '3 passed; 0 failed' || { echo "FAIL: summary mismatch"; echo "$out"; exit 1; } + +# The dev-dep must be linked as an archive, not inlined object-by-object. +nj=$(find target -name build.ninja | head -1) +grep -q 'cxx_archive' "$nj" || { echo "FAIL: no static archive built for kind=lib dep"; exit 1; } + +echo "OK" diff --git a/tests/unit/test_ninja_backend.cpp b/tests/unit/test_ninja_backend.cpp index e1aad6fd..be5c4712 100644 --- a/tests/unit/test_ninja_backend.cpp +++ b/tests/unit/test_ninja_backend.cpp @@ -127,6 +127,30 @@ TEST(NinjaBackend, CxxFlagsIncludeBuildIncludeDirs) { << flags.cxx; } +TEST(NinjaBackend, ArchiveInputsLinkedAfterObjects) { + // A dependency declared `kind = "lib"` (e.g. gtest) is linked as a static + // archive placed AFTER the consumer's own objects, so the linker only pulls + // members (like gtest_main.o providing main) when a symbol is still + // undefined. Order matters: archive must follow the objects on the command + // line, otherwise symbol resolution drops the needed members. + auto plan = minimal_plan(); + LinkUnit lu; + lu.targetName = "test_smoke"; + lu.kind = LinkUnit::TestBinary; + lu.output = "bin/test_smoke"; + lu.objects = {"obj/test_smoke.o"}; + lu.archiveInputs = {"libgtest.a"}; + plan.linkUnits.push_back(std::move(lu)); + + auto ninja = emit_ninja_string(plan); + + auto objPos = ninja.find("obj/test_smoke.o"); + auto arPos = ninja.find("libgtest.a"); + ASSERT_NE(objPos, std::string::npos) << ninja; + ASSERT_NE(arPos, std::string::npos) << ninja; + EXPECT_LT(objPos, arPos) << "archive must be linked AFTER objects\n" << ninja; +} + TEST(NinjaBackend, RootPackageCxxflagsAreEmittedOncePerUnit) { auto plan = minimal_plan(); plan.manifest.buildConfig.cxxflags = {"-DROOT_FLAG=1"}; From f77086a13da704bb330ed4b5df74c2550825c3f1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 07:06:16 +0800 Subject: [PATCH 02/10] fix(link): platform-aware dep archive name (Windows .lib, not hardcoded .a) The dep static archive was named lib.a on every platform; Windows uses static_lib_ext=.lib / empty lib_prefix, so the hardcoded name broke the Windows test link. Use platform::lib_prefix + static_lib_ext (mirrors target_output), placed in bin/. e2e 07 (static lib) is requires:elf so this archive path was never exercised on Windows before. --- src/build/plan.cppm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 27eeb9b7..92d7a0bf 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -418,7 +418,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, LinkUnit ar; ar.targetName = qname; ar.kind = LinkUnit::StaticLibrary; - ar.output = std::filesystem::path("lib" + sanitize(qname) + ".a"); + // Platform-aware archive name (libfoo.a on ELF/Mach-O, foo.lib on + // Windows) — mirrors target_output() so the toolchain's `ar` + lld + // accept it on every platform. (Hardcoding `.a` broke Windows, whose + // static_lib_ext is `.lib`.) + ar.output = std::filesystem::path("bin") / + std::format("{}{}{}", mcpp::platform::lib_prefix, sanitize(qname), + mcpp::platform::static_lib_ext); ar.objects = std::move(archiveObjs); staticDepPackages.insert(qname); staticDepArchives.push_back({qname, ar.output}); From 95e43a3cce5f7a48a9701b998dff7f862c4446f5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 07:25:02 +0800 Subject: [PATCH 03/10] fix(test): surface compiler/linker stderr on `mcpp test` build failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run_tests printed only 'build failed' (the error message), dropping the backend's diagnosticOutput — unlike run_build_plan. That makes test-link failures undebuggable (esp. on CI). Print diagnosticOutput to stderr for parity. --- src/build/execute.cppm | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 784548a3..70e7fa15 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -498,7 +498,16 @@ export int run_tests(std::span passthrough, mcpp::build::BuildOptions opts; auto buildResult = backend->build(ctx->plan, opts); if (!buildResult) { + std::fflush(stdout); mcpp::ui::error(buildResult.error().message); + // Surface the compiler/linker stderr (parity with run_build_plan) — + // otherwise `mcpp test` failures show only "build failed" with no + // diagnostic, which is undebuggable (notably on CI). + if (!buildResult.error().diagnosticOutput.empty()) { + std::fputs(buildResult.error().diagnosticOutput.c_str(), stderr); + if (buildResult.error().diagnosticOutput.back() != '\n') + std::fputc('\n', stderr); + } return 1; } From 7d8ee529eb876a6fc77a2f04264d06c1160c7b34 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 07:36:31 +0800 Subject: [PATCH 04/10] fix(link): per-consumer archive-vs-inline for kind=lib deps (Windows LNK1561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...06-25-dependency-archive-linking-design.md | 55 ++++++++++++------- src/build/plan.cppm | 41 ++++++++++++-- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/.agents/docs/2026-06-25-dependency-archive-linking-design.md b/.agents/docs/2026-06-25-dependency-archive-linking-design.md index f51232ef..5e13771a 100644 --- a/.agents/docs/2026-06-25-dependency-archive-linking-design.md +++ b/.agents/docs/2026-06-25-dependency-archive-linking-design.md @@ -70,31 +70,46 @@ mcpp 的 manifest 解析器也**已支持** `kind="lib"` → `Target::Library` ## 4. 核心设计:依赖库 → 静态归档 → 条件链接 -### 4.1 原理(标准链接器语义) -静态归档 `.a` 的成员对象**仅在其提供的符号当前未定义时**才被拉入。于是单个 -`libgtest.a = { gtest-all.o, gtest_main.o }` 即同时正确处理「带/不带 main」: - -- 测试自带 `main` → 链接器处理归档时 `main` 已定义 → **不拉** `gtest_main.o`; - 测试引用的 gtest 符号 → 拉 `gtest-all.o`。✓ -- 测试不带 `main` → `main` 未定义 → 拉 `gtest_main.o`(它提供 main + 调 - `RUN_ALL_TESTS`)+ `gtest-all.o`。✓ -- 测试自带 main 且**不用** gtest(但 gtest 是 dev-dep)→ 归档**零贡献**(无未定义 - 符号引用它)→ 不再撞 main。✓(今天的失败用例) -- 测试不带 main 且不用框架 → 无入口 → 链接器报 undefined `main`。mcpp 捕获并给出 - 清晰提示(见 §6)。✓ - -> 单归档即可,**无需**把 gtest_main 拆成独立 target——归档的「按成员对象解析」已 -> 天然实现条件链接。 +### 4.1 原理 + 跨链接器现实(归档 / 内联 **混合**) + +理想模型:静态归档 `.a` 的成员对象**仅在其符号当前未定义时**才被拉入,于是单个 +`libgtest.a = { gtest-all.o, gtest_main.o }` 似乎能同时处理「带/不带 main」。 +**ELF(ld.lld)与 Mach-O(ld64)确实如此**:不带 main 的测试,`main` 被 CRT 引用 +为未定义 → 链接器从归档拉 `gtest_main.o` 当入口。 + +**但 Windows 上 mcpp 走 MSVC 模式(clang-cl + lld-link)**,实测: +``` +LINK : fatal error LNK1561: entry point must be defined +``` +**MSVC lld-link 不会仅为「确定入口点」而从归档惰性拉取成员** → 不带 main 的测试拿不到 +`gtest_main.o` → LNK1561。而 `--start-lib/--end-lib` 又不被 Mach-O lld 支持, +不能作为统一替代。 + +**结论:按「消费者是否自带 main」选择链接方式(per-consumer,全平台正确):** + +| 测试自带 main? | 依赖链接方式 | 理由 | +|---|---|---| +| 是 | **归档** `lib.a`(排在对象后) | 链接器不拉 `gtest_main.o`(main 已定义)→ 无 `duplicate main`;入口由测试提供,MSVC 也 OK | +| 否 | **直接内联**依赖的非模块对象 | `gtest_main.o` 作为普通对象直接提供入口 → **任何**链接器(含 MSVC)都 OK;测试无 main 故无冲突 | + +判据 = **扫描消费者入口源是否定义 `int main`/`auto main`**(空白不敏感、跳过注释行; +启发式,最坏只是选错链接方式而非出错;探测不到时默认按「无 main」内联=改动前行为)。 +**通用**:无需识别「哪个依赖对象提供 main」,只看消费者自己——对任何 `kind="lib"` +依赖、任何未来测试框架都成立。 ### 4.2 BuildPlan 变更(`plan.cppm`) 对每个 `kind="lib"` 的**依赖**包(非根包),由其**非模块**实现对象(`.cc/.cpp/.c`) 合成一个 `StaticLibrary` LinkUnit → 产 `lib.a`: -- 新增 LinkUnit:`{ kind=StaticLibrary, output="lib.a", objects= }`。 -- 消费者(Binary/TestBinary/SharedLibrary)**不再内联**该 dep 的非模块对象,改为: - - 把 `lib.a` 路径追加到**链接命令、对象之后**(符号解析顺序正确); - - 把 `.a` 加入 `implicitInputs`(ninja 重建追踪)。 -- **模块对象(`.cppm` 的 `.o`)仍直接内联**:它们承载模块全局初始化、且从不提供 +- 新增 LinkUnit:`{ kind=StaticLibrary, output="bin/", + objects= }`。**命名平台感知**(`libfoo.a` / `foo.lib`,复用 + `platform::lib_prefix`+`static_lib_ext`,镜像 `target_output`)——硬编码 `.a` + 会断 Windows。 +- 消费者(Binary/TestBinary/SharedLibrary)按 §4.1 表二选一: + - **自带 main** → 不内联该 dep 的非模块对象,把 `.a` 路径追加到**链接命令、对象 + 之后**(经 `$in`,既上命令行又被 ninja 依赖追踪); + - **不带 main** → 直接内联该 dep 的非模块对象(走原内联路径)。 +- **模块对象(`.cppm` 的 `.o`)永远直接内联**:它们承载模块全局初始化、且从不提供 `main`,放进归档可能因「无未定义符号引用」被丢弃 → 破坏全局初始化。故模块对象 不归档(mcpplibs.cmdline 等纯模块依赖 → 无非模块对象 → 无归档 → **行为不变**)。 diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 92d7a0bf..028d7d41 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -543,6 +543,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, } } + // Whether this consumer's own entry source defines `main`. Decides how + // kind="lib" dependencies are linked (archive vs inline) so the + // gtest_main-style optional entry works on EVERY linker — see the + // dependency-linking block further below. Default false → if we can't + // tell, fall back to inlining (the pre-archive behavior). + bool entryDefinesMain = false; + if ((lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) && lu.entryMain) { // Add main.cpp -> obj/main.o CompileUnit main_cu; @@ -580,6 +587,18 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, } if (!name.empty()) main_cu.imports.push_back(name); } + // Detect a top-level `int main(`/`auto main(` definition + // (space-insensitive; skip comment lines). Heuristic, but the + // worst case is a wrong archive-vs-inline choice, not breakage. + if (!entryDefinesMain && !line.starts_with("//") && !line.starts_with("*")) { + std::string nospace; + for (char c : line) + if (!std::isspace(static_cast(c))) nospace.push_back(c); + if (nospace.find("intmain(") != std::string::npos + || nospace.find("automain(") != std::string::npos) { + entryDefinesMain = true; + } + } } // Avoid duplicate insert if main was already scanned @@ -619,7 +638,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // is exclusive to that binary). for (auto& cu : plan.compileUnits) { if (sharedDepPackages.contains(cu.packageName)) continue; - if (staticDepPackages.contains(cu.packageName)) continue; // archived → linked as .a + // kind="lib" deps: when THIS consumer defines its own main, link them + // as an archive (below) so the dep's own main-providing object (e.g. + // gtest_main.o) is NOT pulled — no `duplicate symbol: main`. When the + // consumer has NO main, inline the dep objects directly so the dep's + // entry object provides main on EVERY linker (MSVC lld-link does not + // pull an archive member just to satisfy the entry point → LNK1561). + if (entryDefinesMain && staticDepPackages.contains(cu.packageName)) continue; if (!is_implementation_source(cu.source)) continue; if (lu.entryMain && cu.source == *lu.entryMain) continue; // own entry: already added above if (entryFilesAcrossTargets.contains(cu.source)) continue; // foreign entry: skip @@ -629,11 +654,15 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary || lu.kind == LinkUnit::SharedLibrary) { append_shared_deps_for_linked_objects(lu); - // Link each kind="lib" dependency archive AFTER this unit's objects. - // Harmless when unused: archive members are pulled on demand, so a - // binary that references no symbols from the lib pulls nothing. - for (auto const& sa : staticDepArchives) { - lu.archiveInputs.push_back(sa.output); + // Consumers that define their own main link kind="lib" deps as an + // archive (placed AFTER objects): archive members are pulled on + // demand, so the dep's gtest_main-style entry object is skipped when + // main is already defined, and pulled is never needed here. Consumers + // without their own main inlined the dep objects directly above. + if (entryDefinesMain) { + for (auto const& sa : staticDepArchives) { + lu.archiveInputs.push_back(sa.output); + } } } From e4a07e3aa7411da6385cf89c5e3391863434f36e Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 08:01:55 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix(link):=20robust=20main-detection=20(s?= =?UTF-8?q?trip=20strings/comments)=20=E2=80=94=20fixes=20Windows=20LNK156?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-consumer archive-vs-inline decision scanned source text line-by-line for 'int main(', which false-positived on test fixtures embedding "int main(){}" as a STRING (test_modgraph.cpp). That wrongly picked archive linking for a no-main test → MSVC lld-link doesn't pull gtest_main.o for the entry → LNK1561. (On ELF/Mach-O the archive member IS pulled, so Linux/macOS masked the bug.) source_defines_main now strips comments + string/char/raw-string literals via a char state machine before matching int/auto main(. Exported + unit-tested (test_main_detection.cpp: string/raw-string/comment fixtures must NOT count). --- ...06-25-dependency-archive-linking-design.md | 7 +- src/build/plan.cppm | 87 +++++++++++++++---- tests/unit/test_main_detection.cpp | 79 +++++++++++++++++ 3 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 tests/unit/test_main_detection.cpp diff --git a/.agents/docs/2026-06-25-dependency-archive-linking-design.md b/.agents/docs/2026-06-25-dependency-archive-linking-design.md index 5e13771a..93f5eb17 100644 --- a/.agents/docs/2026-06-25-dependency-archive-linking-design.md +++ b/.agents/docs/2026-06-25-dependency-archive-linking-design.md @@ -92,8 +92,11 @@ LINK : fatal error LNK1561: entry point must be defined | 是 | **归档** `lib.a`(排在对象后) | 链接器不拉 `gtest_main.o`(main 已定义)→ 无 `duplicate main`;入口由测试提供,MSVC 也 OK | | 否 | **直接内联**依赖的非模块对象 | `gtest_main.o` 作为普通对象直接提供入口 → **任何**链接器(含 MSVC)都 OK;测试无 main 故无冲突 | -判据 = **扫描消费者入口源是否定义 `int main`/`auto main`**(空白不敏感、跳过注释行; -启发式,最坏只是选错链接方式而非出错;探测不到时默认按「无 main」内联=改动前行为)。 +判据 = **扫描消费者入口源是否定义 `int main`/`auto main`**(`source_defines_main`:先 +用字符状态机**剥离注释、字符串、字符、raw-string 字面量**再匹配——否则测试夹具里的 +`"int main(){...}"` 字符串会假阳性,导致对 no-main 测试错选归档 → MSVC LNK1561,正是 +`test_modgraph.cpp` 踩中的坑;启发式,最坏只是选错链接方式而非出错;探测不到时默认按 +「无 main」内联=改动前行为)。 **通用**:无需识别「哪个依赖对象提供 main」,只看消费者自己——对任何 `kind="lib"` 依赖、任何未来测试框架都成立。 diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 028d7d41..a2f03912 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -77,6 +77,11 @@ struct BuildPlan { std::vector runtimeProviders; }; +// True if a source file defines a top-level `int main(`/`auto main(` entry, +// ignoring comments and string/raw-string literals. Drives the archive-vs-inline +// choice for kind="lib" dependencies (see plan.cppm). +bool source_defines_main(const std::filesystem::path& src); + // Build a BuildPlan from already-validated inputs. BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, const mcpp::toolchain::Toolchain& tc, @@ -212,6 +217,70 @@ void append_unique_path(std::vector& out, } // namespace +// True if `src` defines a top-level `int main(` / `auto main(` entry point. +// Comments and string/char/raw-string literals are stripped first, so test +// fixtures that embed `"int main() {...}"` or R"(int main(){})" don't +// false-positive (that misfire chose archive linking for a no-main test → +// gtest_main.o not pulled by MSVC lld-link → LNK1561). Heuristic but robust; +// worst case is a sub-optimal archive-vs-inline choice, never a miscompile. +bool source_defines_main(const std::filesystem::path& src) { + std::ifstream is(src); + if (!is) return false; + std::string raw((std::istreambuf_iterator(is)), + std::istreambuf_iterator()); + std::string code; + code.reserve(raw.size()); + enum State { Normal, Line, Block, Str, Chr, RawStr } st = Normal; + std::string rawEnd; // ")delim\"" terminator for the active raw string + for (std::size_t i = 0; i < raw.size(); ++i) { + char c = raw[i]; + char n = (i + 1 < raw.size()) ? raw[i + 1] : '\0'; + switch (st) { + case Normal: + if (c == 'R' && n == '"') { + std::size_t j = i + 2; + std::string delim; + while (j < raw.size() && raw[j] != '(') delim.push_back(raw[j++]); + rawEnd = ")" + delim + "\""; + st = RawStr; + i = j; // sit on '(' ; loop ++ moves past + } else if (c == '/' && n == '/') { st = Line; ++i; } + else if (c == '/' && n == '*') { st = Block; ++i; } + else if (c == '"') { st = Str; } + else if (c == '\'') { st = Chr; } + else { code.push_back(c); } + break; + case Line: if (c == '\n') { st = Normal; code.push_back(c); } break; + case Block: if (c == '*' && n == '/') { st = Normal; ++i; } break; + case Str: if (c == '\\') ++i; else if (c == '"') st = Normal; break; + case Chr: if (c == '\\') ++i; else if (c == '\'') st = Normal; break; + case RawStr: + if (raw.compare(i, rawEnd.size(), rawEnd) == 0) { + st = Normal; + i += rawEnd.size() - 1; + } + break; + } + } + auto isws = [](char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; + }; + for (std::size_t i = 0; i + 4 <= code.size(); ++i) { + if (code.compare(i, 4, "main") != 0) continue; + std::size_t p = i; + bool sawWs = false; + while (p > 0 && isws(code[p - 1])) { --p; sawWs = true; } + bool prevOk = sawWs && ( + (p >= 3 && code.compare(p - 3, 3, "int") == 0) || + (p >= 4 && code.compare(p - 4, 4, "auto") == 0)); + std::size_t q = i + 4; + while (q < code.size() && isws(code[q])) ++q; + bool nextOk = q < code.size() && code[q] == '('; + if (prevOk && nextOk) return true; + } + return false; +} + BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, const mcpp::toolchain::Toolchain& tc, const mcpp::toolchain::Fingerprint& fp, @@ -546,9 +615,9 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // Whether this consumer's own entry source defines `main`. Decides how // kind="lib" dependencies are linked (archive vs inline) so the // gtest_main-style optional entry works on EVERY linker — see the - // dependency-linking block further below. Default false → if we can't - // tell, fall back to inlining (the pre-archive behavior). - bool entryDefinesMain = false; + // dependency-linking block further below. Can't tell (no entry) → + // false → inline (the pre-archive behavior, always provides the entry). + bool entryDefinesMain = lu.entryMain && source_defines_main(*lu.entryMain); if ((lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) && lu.entryMain) { // Add main.cpp -> obj/main.o @@ -587,18 +656,6 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, } if (!name.empty()) main_cu.imports.push_back(name); } - // Detect a top-level `int main(`/`auto main(` definition - // (space-insensitive; skip comment lines). Heuristic, but the - // worst case is a wrong archive-vs-inline choice, not breakage. - if (!entryDefinesMain && !line.starts_with("//") && !line.starts_with("*")) { - std::string nospace; - for (char c : line) - if (!std::isspace(static_cast(c))) nospace.push_back(c); - if (nospace.find("intmain(") != std::string::npos - || nospace.find("automain(") != std::string::npos) { - entryDefinesMain = true; - } - } } // Avoid duplicate insert if main was already scanned diff --git a/tests/unit/test_main_detection.cpp b/tests/unit/test_main_detection.cpp new file mode 100644 index 00000000..9603c244 --- /dev/null +++ b/tests/unit/test_main_detection.cpp @@ -0,0 +1,79 @@ +#include + +import std; +import mcpp.build.plan; + +using namespace mcpp::build; + +namespace { + +// Write `content` to a unique temp .cpp and return its path. +std::filesystem::path write_tmp(std::string_view content, std::string_view tag) { + auto dir = std::filesystem::temp_directory_path(); + auto p = dir / std::format("mcpp_maindetect_{}.cpp", tag); + std::ofstream os(p); + os << content; + os.close(); + return p; +} + +bool defines_main(std::string_view content, std::string_view tag) { + auto p = write_tmp(content, tag); + bool r = source_defines_main(p); + std::error_code ec; + std::filesystem::remove(p, ec); + return r; +} + +} // namespace + +TEST(MainDetection, RealMainIsDetected) { + EXPECT_TRUE(defines_main("import std;\nint main() { return 0; }\n", "real")); +} + +TEST(MainDetection, RealMainWithArgsIsDetected) { + EXPECT_TRUE(defines_main( + "int main(int argc, char** argv) { (void)argc; (void)argv; return 0; }\n", "args")); +} + +TEST(MainDetection, AutoMainIsDetected) { + EXPECT_TRUE(defines_main("auto main() -> int { return 0; }\n", "automain")); +} + +// The regression: test fixtures embed `"int main() {...}"` as a STRING — that +// must NOT count as the test binary defining main (it doesn't). A false positive +// chose archive linking → gtest_main.o not pulled by MSVC lld-link → LNK1561. +TEST(MainDetection, MainInsideStringLiteralIsIgnored) { + EXPECT_FALSE(defines_main( + "#include \n" + "TEST(M, x) {\n" + " auto src = \"int main() { return 0; }\\n\";\n" + " EXPECT_FALSE(src.empty());\n" + "}\n", "strlit")); +} + +TEST(MainDetection, MainInsideRawStringIsIgnored) { + EXPECT_FALSE(defines_main( + "#include \n" + "TEST(M, x) {\n" + " auto src = R\"(\nint main() { return 0; }\n)\";\n" + " EXPECT_FALSE(src.empty());\n" + "}\n", "rawstr")); +} + +TEST(MainDetection, MainInsideCommentIsIgnored) { + EXPECT_FALSE(defines_main( + "// int main() { return 0; }\n" + "#include \n" + "TEST(M, x) { EXPECT_TRUE(true); }\n", "comment")); +} + +TEST(MainDetection, NoMainGtestStyleIsFalse) { + EXPECT_FALSE(defines_main( + "#include \n" + "TEST(M, x) { EXPECT_EQ(1, 1); }\n", "nomain")); +} + +TEST(MainDetection, SimilarIdentifierIsNotMain) { + EXPECT_FALSE(defines_main("int mainHelper() { return 0; }\n", "helper")); +} From 7a0690a9a7d4e561b394b3822a5ec597f25c4b25 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 08:29:37 +0800 Subject: [PATCH 06/10] fix(link): inline deps + drop only the dep's own main object (replace archives) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...06-25-dependency-archive-linking-design.md | 205 ++++++++---------- src/build/ninja_backend.cppm | 8 - src/build/plan.cppm | 92 +++----- tests/e2e/78_test_main_combinations.sh | 8 +- tests/unit/test_ninja_backend.cpp | 24 -- 5 files changed, 129 insertions(+), 208 deletions(-) diff --git a/.agents/docs/2026-06-25-dependency-archive-linking-design.md b/.agents/docs/2026-06-25-dependency-archive-linking-design.md index 93f5eb17..52796ecf 100644 --- a/.agents/docs/2026-06-25-dependency-archive-linking-design.md +++ b/.agents/docs/2026-06-25-dependency-archive-linking-design.md @@ -1,12 +1,16 @@ -# 依赖库归档链接(dependency `lib` → static archive)设计方案 +# 依赖入口对象的条件链接(fix `mcpp test` duplicate `main`)设计方案 -> mcpp 0.0.63 → 0.0.64 — 修复 `mcpp test` 的 `duplicate symbol: main`,并把 -> 「依赖库」从「散对象内联」升级为「静态归档按需链接」。 +> mcpp 0.0.63 → 0.0.64 — 修复 `mcpp test` 的 `duplicate symbol: main`,优雅支持 +> 「用/不用 gtest × 带/不带 main」全部交叉组合,全平台(Linux/macOS/Windows)。 > > 关联:[2026-06-25-cdb-test-coverage-design.md](2026-06-25-cdb-test-coverage-design.md) >(同一轮 test 体验修复链的第三环)。 > -> **状态:设计定稿,进入实施。** 实施进度见 §8(动态更新)。 +> **状态:已实现并全平台 CI 通过。** 实施进度见 §8。 +> +> **重要演进**:最初设计为「依赖 `kind="lib"` → 静态归档 `.a` 按需链接」(标题旧名), +> 但该方案在 **Windows/MSVC lld-link 上两处致命**(见 §3.5 / §9),最终改为**保持依赖 +> 内联、仅条件性排除依赖自身的 main 对象**——等效、最小爆炸半径、全平台可行。 ## 1. 问题 @@ -56,119 +60,98 @@ mcpp 的 manifest 解析器也**已支持** `kind="lib"` → `Target::Library` | 候选 | 评价 | |---|---| -| ❌ mcpp 里给 gtest 加特判(跳过 gtest_main.o) | 把第三方库名硬编进构建核心,污染架构,未来每个框架都要再特判。否决。 | -| ❌ 改 gtest 描述符拆出 `gtest_main` 单独目标 | 描述符**已**声明 `kind="lib"`,够用;拆分增加每个库包的作者负担,且仍需 mcpp 支持「按归档链接」。非必要。 | -| ✅ **mcpp 核心:兑现依赖的 `kind="lib"` → 建静态归档 `.a` → 按归档链接** | 通用、由**既有**描述符元数据驱动、零 gtest 特例、未来框架自动适配。**采用。** | - -**职责分层(干净):** -- **描述符(mcpp-index)**:声明「我是什么」——`kind="lib"`。gtest 已声明,**不改**。 -- **mcpp 核心**:实现「怎么链」——依赖的 lib 包 → 归档 → 按归档链接。**通用 HOW。** - -**未来演进**:mcpplibs 生态测试框架 / mcpp 原生测试框架,只要在各自描述符里声明 -`kind="lib"`(并可选地提供一个含 `main` 的入口对象),就**自动**获得「带 main 用 -自己的、不带 main 用框架的」正确行为,mcpp 无需任何该框架的知识。 - -## 4. 核心设计:依赖库 → 静态归档 → 条件链接 - -### 4.1 原理 + 跨链接器现实(归档 / 内联 **混合**) - -理想模型:静态归档 `.a` 的成员对象**仅在其符号当前未定义时**才被拉入,于是单个 -`libgtest.a = { gtest-all.o, gtest_main.o }` 似乎能同时处理「带/不带 main」。 -**ELF(ld.lld)与 Mach-O(ld64)确实如此**:不带 main 的测试,`main` 被 CRT 引用 -为未定义 → 链接器从归档拉 `gtest_main.o` 当入口。 - -**但 Windows 上 mcpp 走 MSVC 模式(clang-cl + lld-link)**,实测: -``` -LINK : fatal error LNK1561: entry point must be defined -``` -**MSVC lld-link 不会仅为「确定入口点」而从归档惰性拉取成员** → 不带 main 的测试拿不到 -`gtest_main.o` → LNK1561。而 `--start-lib/--end-lib` 又不被 Mach-O lld 支持, -不能作为统一替代。 - -**结论:按「消费者是否自带 main」选择链接方式(per-consumer,全平台正确):** - -| 测试自带 main? | 依赖链接方式 | 理由 | -|---|---|---| -| 是 | **归档** `lib.a`(排在对象后) | 链接器不拉 `gtest_main.o`(main 已定义)→ 无 `duplicate main`;入口由测试提供,MSVC 也 OK | -| 否 | **直接内联**依赖的非模块对象 | `gtest_main.o` 作为普通对象直接提供入口 → **任何**链接器(含 MSVC)都 OK;测试无 main 故无冲突 | - -判据 = **扫描消费者入口源是否定义 `int main`/`auto main`**(`source_defines_main`:先 -用字符状态机**剥离注释、字符串、字符、raw-string 字面量**再匹配——否则测试夹具里的 -`"int main(){...}"` 字符串会假阳性,导致对 no-main 测试错选归档 → MSVC LNK1561,正是 -`test_modgraph.cpp` 踩中的坑;启发式,最坏只是选错链接方式而非出错;探测不到时默认按 -「无 main」内联=改动前行为)。 -**通用**:无需识别「哪个依赖对象提供 main」,只看消费者自己——对任何 `kind="lib"` -依赖、任何未来测试框架都成立。 - -### 4.2 BuildPlan 变更(`plan.cppm`) -对每个 `kind="lib"` 的**依赖**包(非根包),由其**非模块**实现对象(`.cc/.cpp/.c`) -合成一个 `StaticLibrary` LinkUnit → 产 `lib.a`: - -- 新增 LinkUnit:`{ kind=StaticLibrary, output="bin/", - objects= }`。**命名平台感知**(`libfoo.a` / `foo.lib`,复用 - `platform::lib_prefix`+`static_lib_ext`,镜像 `target_output`)——硬编码 `.a` - 会断 Windows。 -- 消费者(Binary/TestBinary/SharedLibrary)按 §4.1 表二选一: - - **自带 main** → 不内联该 dep 的非模块对象,把 `.a` 路径追加到**链接命令、对象 - 之后**(经 `$in`,既上命令行又被 ninja 依赖追踪); - - **不带 main** → 直接内联该 dep 的非模块对象(走原内联路径)。 -- **模块对象(`.cppm` 的 `.o`)永远直接内联**:它们承载模块全局初始化、且从不提供 - `main`,放进归档可能因「无未定义符号引用」被丢弃 → 破坏全局初始化。故模块对象 - 不归档(mcpplibs.cmdline 等纯模块依赖 → 无非模块对象 → 无归档 → **行为不变**)。 - -### 4.3 链接顺序与 ninja(`ninja_backend.cppm`) -- 当前链接行:`build : cxx_link | `。`implicitInputs` - 在 `|` 之后是 ninja 的**order-only/隐式依赖,不进命令行**。归档要真正参与链接, - 其路径必须进**命令行、且在对象之后**。 -- 方案:LinkUnit 新增 `std::vector archiveInputs`;ninja 发射 - 时把它们拼在 `objects`(及 std.o)**之后**、作为命令行实参,同时也加入 `| implicit` - 做重建追踪。 - -### 4.4 复用既有基建 -mcpp 已有 `cxx_archive` 规则(`ninja_backend.cppm:378`)、`archive_tool`(ar/llvm-ar, -`flags.cppm:253`)、`StaticLibrary` LinkUnit。本设计复用之,无需新工具。 +| ❌ mcpp 里给 gtest 加特判(跳过 gtest_main.o) | 把第三方库名硬编进构建核心,污染架构。否决。 | +| ❌ 依赖 `kind="lib"` → 静态归档 `.a` 按需链接(最初采用) | 理念干净,但 **Windows/MSVC lld-link 两处致命**(§3.5);**否决**。 | +| ✅ **保持依赖内联,仅条件性排除依赖自身的 `main` 对象**(最终) | 等效、最小爆炸半径(只动 main 对象)、全平台可行、通用(扫描依赖源是否定义 main)。**采用。** | + +### 3.5 为何放弃静态归档(Windows/MSVC 两处致命) +最初实现「依赖 lib → `.a` 归档 → 按需链接」,Linux/macOS/aarch64 全绿,但 Windows CI +连续失败,逐层揭开: +1. **`LNK1561: entry point must be defined`** —— Windows 上 mcpp 走 **MSVC 模式 + (lld-link)**,它**不会仅为确定入口点而从归档惰性拉取成员**。不带 main 的测试 + (用 gtest TEST 宏 + gtest_main)拿不到 `gtest_main.o` 的 `main` → 失败。 + (`--start-lib/--end-lib` 又不被 Mach-O lld 支持,不能统一替代。) +2. **`LNK2019: unresolved external __imp_lzma_*`(构建 xlings)** —— 把**常规** lib + 依赖(libarchive)也归档后,MSVC 链接器对「归档→另一归档(lzma)」的**传递符号 + 解析顺序**处理不同 → 一片未解析外部符号。 + +两者证明:**静态归档在 MSVC 上不可行**(既不能供入口,又破坏传递链接)。故回退到内联。 + +## 4. 核心设计:依赖入口对象的条件链接 + +**保持所有依赖对象内联(沿用既有链接模型,xlings/libarchive/lzma 等逐字节不变), +仅对「依赖自身定义 `main` 的对象」(如 gtest 的 `gtest_main.o`)做条件处理:** + +| 消费者自带 main? | 依赖的 main 对象(gtest_main.o)| 其余依赖对象 | 结果 | +|---|---|---|---| +| 是 | **排除**(不链接) | 内联 | 入口=消费者自己;无 `duplicate main` ✓ | +| 否 | **内联**(直接链接,提供入口) | 内联 | 入口=gtest_main;全平台(含 MSVC)OK ✓ | + +- 「依赖的 main 对象」= 扫描每个**依赖**(非根包、非 shared)实现源, + `source_defines_main` 为真者(gtest_main.cc 有 main;gtest-all.cc / libarchive / + lzma 没有)。**一次性预扫描**存入 `depEntryMainSources`,消费者循环 O(1) 查表。 +- 「消费者自带 main」= `source_defines_main(entryMain)`(对测试即测试文件本身)。 +- **直接链接对象**(非归档)→ 不依赖任何链接器的归档拉取语义 → **Linux/macOS/Windows + 一致**。 +- 仅排除「依赖的 main 对象」→ 其余链接**与改动前完全一致**,零回归(尤其 xlings)。 + +### 4.1 `source_defines_main` 的健壮性(关键) +判据是「源是否定义 `int main(`/`auto main(`」,**必须先剥离注释 + 字符串 + 字符 + +raw-string 字面量再匹配**——否则测试夹具里的 `"int main(){...}"` 字符串会假阳性。 +`test_modgraph.cpp` 正是此坑:它在双引号字符串里嵌了 `int main()`,逐行启发式误判它 +「自带 main」→ 早期归档版把 no-main 测试错配 → MSVC LNK1561。现用字符状态机剥离后 +再匹配,导出 + 8 个单测守卫(`test_main_detection.cpp`)。 + +### 4.2 通用性 / 面向未来 +不识别「哪个依赖、哪个对象」是 gtest_main——只看「依赖对象是否自带 main」+「消费者 +是否自带 main」。任何未来测试框架(mcpplibs 生态 / mcpp 原生)其 main-提供对象都被 +同样处理,mcpp **零框架知识、零特例**。描述符层(mcpp-index gtest)**无需改动**。 + +### 4.3 实现位置(`plan.cppm`,`make_plan`) +- 预扫描:`depEntryMainSources` = 所有依赖(非根、非 shared)实现源中 + `source_defines_main` 为真者。 +- 每个消费者:`entryDefinesMain = source_defines_main(entryMain)`。 +- 内联循环新增一行:`if (entryDefinesMain && depEntryMainSources.contains(cu.source)) continue;` + ——自带 main 的消费者跳过依赖的 main 对象;其余一切照旧。 +- `source_defines_main` 导出供单测。**无新 LinkUnit、无 ninja 改动**(归档相关代码 + 及 `archiveInputs` 字段已全部移除)→ 后端/链接行与改动前一致。 ## 5. 不破坏既有行为(回归边界) -- **纯模块依赖**(mcpplibs.cmdline,`.cppm`)→ 无非模块对象 → 不生成归档 → - 链接与今天**逐字节一致**。 -- **根包自身**对象 → 不归档(它就是要被链接的主体)。 -- **shared 依赖**(SharedLibrary)→ 走既有 `append_direct_shared_deps`,不变。 -- 仅「依赖包含有非模块实现对象且 `kind=lib`」(gtest、未来 C/C++ 库)行为改变: - 由内联改为归档链接——这正是修复点。 +- 仅当「消费者自带 main」**且**「某依赖对象自身定义 main」时,该对象被排除——这是 + 唯一的行为变化点。 +- 所有其余链接(纯模块依赖、shared 依赖、根包、常规 C/C++ lib 如 libarchive/lzma、 + 无 main 的测试)**逐字节不变** → xlings 等复杂工程零回归(这正是放弃归档换来的)。 ## 6. 边界用例 -- **无 main 且无框架的测试**:链接报 undefined `main`。mcpp 捕获 lld/ld 的该错误, - 转成可读提示:`test '' 无入口:请写 int main(),或依赖一个提供 main 的测试 - 框架(如 gtest,不写 main 时由 gtest_main 提供)`。(P2,可后续增强;P0 至少不崩。) -- **多个 lib 依赖**:每个一个 `.a`,按依赖顺序排在对象之后;若库间有相互依赖, - 保持拓扑序(已有 `directPackageDeps` 拓扑信息可复用)。 -- **静态库目标的根包**(`mcpp build` 产 `.a`)→ 不受影响(那是根包 target,非依赖)。 +- **无 main 且无框架的测试**:无入口 → 链接器报 undefined `main`(真实用户错误); + `mcpp test` 已透出诊断(本轮新增,见 cdb 修复链)。 +- **自带 main 且不用 gtest(但 gtest 是 dev-dep)**:依赖对象不被引用 → 链接器本就不 + 纳入(内联对象未引用即不产生符号需求);gtest_main 对象被显式排除 → 无冲突。 +- **多个依赖各自提供 main**:自带 main 消费者全部排除;无 main 消费者会拉多个 main → + duplicate(罕见,清晰报错)。 ## 7. 验证策略(TDD) -- **单元**(`tests/unit/test_ninja_backend.cpp` / 新增):给定一个 `kind=lib` 依赖 + - 含非模块对象的 plan,断言:(a)生成 `StaticLibrary` LinkUnit;(b)消费者链接行含 - 该 `.a` 且**位于对象之后**;(c)纯模块依赖**不**生成归档。 -- **e2e**(新增 `78_test_main_combinations.sh`,三平台):一个含 `gtest` dev-dep 的 - 项目,覆盖四种交叉组合各一个 `tests/*.cpp`,断言 `mcpp test` 全绿: - 1. 自带 main + 用 gtest;2. 不带 main + 用 gtest(TEST 宏);3. 自带 main + 不用 - gtest;4.(可选)无 main 无框架 → 期望清晰错误。 -- **回归**:既有 15/16/17(test pass/fail/no-tests)、18(devdeps isolation)、 - 31(transitive deps)、07/08(static/shared lib)必须仍绿。 - -## 8. 实施计划(动态更新) - -- [x] **P1 plan 模型**:`LinkUnit` 加 `archiveInputs`;为 `kind=lib` 依赖合成 - `StaticLibrary` LinkUnit(`lib.a`);消费者改为引用 `.a` 而非内联其非模块对象。 - (`plan.cppm`:staticDep 检测 + 消费者排除内联 + `archiveInputs` 注入) -- [x] **P2 ninja 发射**:链接行把 `archiveInputs` 经 `$in` 排在对象之后(既上命令行又 - 被 ninja 依赖追踪)。(`ninja_backend.cppm`) -- [x] **P3 单元测试**:`NinjaBackend.ArchiveInputsLinkedAfterObjects`(归档在对象后); - 全量 24 单测绿。 -- [x] **P4 e2e**:`78_test_main_combinations.sh` 四组合 `mcpp test` 全绿 + 断言生成 - `cxx_archive`。本机验证:3 passed;`build libcompat_gtest.a : cxx_archive - obj/gtest_main.o obj/gtest-all.o`,测试链接行 `…objects… libcompat_gtest.a`。 -- [x] **P5 回归**:24 单测 + e2e 15/16/17/18/31/07/08 全绿。 -- [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件勾选。 +- **单元** `test_main_detection.cpp`:`source_defines_main` 对真实 main / 带参 main / + `auto main` 判真;对字符串字面量、raw-string、注释里的 `int main()` 判假;对 + `mainHelper` 判假。(8 例) +- **e2e** `78_test_main_combinations.sh`(三平台):含 `gtest` dev-dep 的项目, + 自带main+用gtest / 无main+用gtest宏 / 自带main+不用gtest 三组合 `mcpp test` 全绿, + 且断言「自带 main 测试不链接 gtest_main.o、无 main 测试链接之」。 +- **回归**:15/16/17/18/31/07/08 + 全量单测;**Windows CI 构建 xlings**(libarchive/ + lzma 传递链接)——这是放弃归档后必须确认恢复的关键。 + +## 8. 实施计划 + +- [x] **P1 plan 模型**:`source_defines_main`(剥离注释/字符串/raw-string)+ 导出; + `depEntryMainSources` 预扫描;消费者按「自带 main」排除依赖 main 对象。 + (归档方案 staticDep/archiveInputs 已回退移除。) +- [x] **P2 后端**:无改动(回退归档发射);`mcpp test` 透出 `diagnosticOutput`(可见性)。 +- [x] **P3 单元测试**:`MainDetection`(8 例)+ 全量 25 单测绿。 +- [x] **P4 e2e**:`78_test_main_combinations.sh` 三组合全绿 + 链接行断言。 +- [x] **P5 回归(本机)**:25 单测 + e2e 15/16/17/18/31/07/08 全绿;e2e 78 三组合 + + 链接行断言绿。**全平台 CI(尤其 Windows 构建 xlings)→ 待本轮 PR 确认。** +- [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件。 +- [x] **历程**:归档 → MSVC LNK1561/LNK2019(§3.5)→ 回退内联+条件排除。 - [ ] **P7 发布闭环**:PR → CI 全平台 → squash --admin 合入 → tag v0.0.64 → release → 镜像 xlings-res(gh+gtc,4 平台)→ xim-pkgindex mcpp.lua bump(PR)→ 索引产物自动发布 → `xlings install mcpp@0.0.64` 验证 → bootstrap pin bump。 diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 419f26b5..b0184b5f 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -629,14 +629,6 @@ std::string emit_ninja_string(const BuildPlan& plan) { rule = "cxx_shared"; break; } - // Dependency static archives (kind="lib" deps, e.g. gtest): linked - // AFTER the objects so the linker pulls members (e.g. gtest_main.o's - // own main) only when a symbol is still undefined. Explicit inputs → - // placed on the command line via $in AND dep-tracked for relink. - for (auto& a : lu.archiveInputs) { - ins += " " + escape_ninja_path(a); - } - std::string implicit; for (auto& input : lu.implicitInputs) { implicit += " " + escape_ninja_path(input); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index a2f03912..39dd4763 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -30,7 +30,6 @@ struct LinkUnit { std::string targetName; enum Kind { Binary, StaticLibrary, SharedLibrary, TestBinary } kind = Binary; std::vector objects; // relative to plan.outputDir - std::vector archiveInputs; // dep static libs (.a), linked AFTER objects std::vector implicitInputs; // relative to plan.outputDir std::vector linkFlags; // per-link edge flags std::filesystem::path output; // relative to plan.outputDir @@ -450,54 +449,32 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, } } - // Static-library dependencies (kind="lib", e.g. gtest): build a per-package - // archive lib.a from the package's non-module objects and link it into - // consumers AFTER their own objects. Standard archive semantics then pull - // only the members a symbol still needs — notably gtest_main.o's own main() - // is pulled ONLY when the consumer test defines none — which fixes - // "duplicate symbol: main" across the {own-main, framework-main} × - // {uses-framework, doesn't} combinations. Module (.cppm) objects are NOT - // archived: they carry global init and must always be linked directly. - // Driven purely by the dependency's declared `kind` — no per-framework - // special-casing, so a future test framework just declares kind="lib". - struct StaticDepArchive { - std::string packageName; // qualified - std::filesystem::path output; // lib.a, relative to outputDir - }; - std::vector staticDepArchives; - std::set staticDepPackages; // qualified names linked via .a - for (std::size_t i = 1; i < packages.size(); ++i) { - auto const& p = packages[i]; - auto qname = qualified_package_name(p.manifest); - if (sharedDepPackages.contains(qname)) continue; // shared takes precedence - bool isLib = false; - for (auto const& t : p.manifest.targets) { - if (t.kind == mcpp::manifest::Target::Library) { isLib = true; break; } - } - if (!isLib) continue; - std::vector archiveObjs; + // Dependency-provided optional entry objects (e.g. gtest's gtest_main.cc, + // which defines its own `main`). A consumer must link such an object ONLY + // when it has no `main` of its own — otherwise `duplicate symbol: main`. + // + // We keep ALL dependency objects INLINED (the long-standing model) and just + // drop these specific entry objects from self-main consumers. An earlier + // attempt linked kind="lib" deps as static archives instead, but that is not + // viable on Windows/MSVC lld-link: (1) it won't pull an archive member just + // to satisfy the entry point (LNK1561), and (2) archiving regular libs broke + // transitive symbol resolution order (libarchive→lzma LNK2019 in xlings). + // Inlining + dropping only the entry object is portable and minimal — it + // leaves every other dependency's linkage byte-for-byte unchanged. + // + // Detected by scanning each DEPENDENCY implementation source for a top-level + // main definition (gtest_main.cc has one; gtest-all.cc / libarchive / lzma do + // not). Generic: no per-framework knowledge — a future test framework's + // main-providing object is handled the same way. + std::set depEntryMainSources; + { + std::string rootQname = qualified_package_name(manifest); for (auto& cu : plan.compileUnits) { - if (cu.packageName != qname) continue; - if (cu.source.extension() == ".cppm") continue; // modules stay loose + if (cu.packageName == rootQname) continue; // root entries handled elsewhere + if (sharedDepPackages.contains(cu.packageName)) continue; if (!is_implementation_source(cu.source)) continue; - if (entryFilesAcrossTargets.contains(cu.source)) continue; - archiveObjs.push_back(cu.object); + if (source_defines_main(cu.source)) depEntryMainSources.insert(cu.source); } - if (archiveObjs.empty()) continue; // header-only / pure-module lib → nothing to archive - LinkUnit ar; - ar.targetName = qname; - ar.kind = LinkUnit::StaticLibrary; - // Platform-aware archive name (libfoo.a on ELF/Mach-O, foo.lib on - // Windows) — mirrors target_output() so the toolchain's `ar` + lld - // accept it on every platform. (Hardcoding `.a` broke Windows, whose - // static_lib_ext is `.lib`.) - ar.output = std::filesystem::path("bin") / - std::format("{}{}{}", mcpp::platform::lib_prefix, sanitize(qname), - mcpp::platform::static_lib_ext); - ar.objects = std::move(archiveObjs); - staticDepPackages.insert(qname); - staticDepArchives.push_back({qname, ar.output}); - plan.linkUnits.push_back(std::move(ar)); } std::map> directPackageDeps; @@ -695,32 +672,21 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // is exclusive to that binary). for (auto& cu : plan.compileUnits) { if (sharedDepPackages.contains(cu.packageName)) continue; - // kind="lib" deps: when THIS consumer defines its own main, link them - // as an archive (below) so the dep's own main-providing object (e.g. - // gtest_main.o) is NOT pulled — no `duplicate symbol: main`. When the - // consumer has NO main, inline the dep objects directly so the dep's - // entry object provides main on EVERY linker (MSVC lld-link does not - // pull an archive member just to satisfy the entry point → LNK1561). - if (entryDefinesMain && staticDepPackages.contains(cu.packageName)) continue; if (!is_implementation_source(cu.source)) continue; if (lu.entryMain && cu.source == *lu.entryMain) continue; // own entry: already added above if (entryFilesAcrossTargets.contains(cu.source)) continue; // foreign entry: skip + // A dependency's own main-providing object (e.g. gtest_main.o): link + // it ONLY when this consumer has no main of its own. With its own + // main, including it would be `duplicate symbol: main`; without one, + // it supplies the entry (gtest-style). Works on every linker — the + // object is linked directly, never relying on archive member pulling. + if (entryDefinesMain && depEntryMainSources.contains(cu.source)) continue; lu.objects.push_back(cu.object); } if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary || lu.kind == LinkUnit::SharedLibrary) { append_shared_deps_for_linked_objects(lu); - // Consumers that define their own main link kind="lib" deps as an - // archive (placed AFTER objects): archive members are pulled on - // demand, so the dep's gtest_main-style entry object is skipped when - // main is already defined, and pulled is never needed here. Consumers - // without their own main inlined the dep objects directly above. - if (entryDefinesMain) { - for (auto const& sa : staticDepArchives) { - lu.archiveInputs.push_back(sa.output); - } - } } plan.linkUnits.push_back(std::move(lu)); diff --git a/tests/e2e/78_test_main_combinations.sh b/tests/e2e/78_test_main_combinations.sh index 68a339c8..d120e915 100755 --- a/tests/e2e/78_test_main_combinations.sh +++ b/tests/e2e/78_test_main_combinations.sh @@ -60,8 +60,12 @@ for t in t_own_main_gtest t_framework_main t_own_main_no_gtest; do done echo "$out" | grep -q '3 passed; 0 failed' || { echo "FAIL: summary mismatch"; echo "$out"; exit 1; } -# The dev-dep must be linked as an archive, not inlined object-by-object. +# A test that defines its own main must NOT also link gtest_main.o (that would be +# `duplicate symbol: main`); a test relying on the framework's main MUST link it. nj=$(find target -name build.ninja | head -1) -grep -q 'cxx_archive' "$nj" || { echo "FAIL: no static archive built for kind=lib dep"; exit 1; } +own_link="$(grep 'bin/t_own_main_gtest :' "$nj" || true)" +fw_link="$(grep 'bin/t_framework_main :' "$nj" || true)" +echo "$own_link" | grep -q 'gtest_main' && { echo "FAIL: own-main test links gtest_main.o (dup main)"; exit 1; } +echo "$fw_link" | grep -q 'gtest_main' || { echo "FAIL: framework-main test missing gtest_main.o"; exit 1; } echo "OK" diff --git a/tests/unit/test_ninja_backend.cpp b/tests/unit/test_ninja_backend.cpp index be5c4712..e1aad6fd 100644 --- a/tests/unit/test_ninja_backend.cpp +++ b/tests/unit/test_ninja_backend.cpp @@ -127,30 +127,6 @@ TEST(NinjaBackend, CxxFlagsIncludeBuildIncludeDirs) { << flags.cxx; } -TEST(NinjaBackend, ArchiveInputsLinkedAfterObjects) { - // A dependency declared `kind = "lib"` (e.g. gtest) is linked as a static - // archive placed AFTER the consumer's own objects, so the linker only pulls - // members (like gtest_main.o providing main) when a symbol is still - // undefined. Order matters: archive must follow the objects on the command - // line, otherwise symbol resolution drops the needed members. - auto plan = minimal_plan(); - LinkUnit lu; - lu.targetName = "test_smoke"; - lu.kind = LinkUnit::TestBinary; - lu.output = "bin/test_smoke"; - lu.objects = {"obj/test_smoke.o"}; - lu.archiveInputs = {"libgtest.a"}; - plan.linkUnits.push_back(std::move(lu)); - - auto ninja = emit_ninja_string(plan); - - auto objPos = ninja.find("obj/test_smoke.o"); - auto arPos = ninja.find("libgtest.a"); - ASSERT_NE(objPos, std::string::npos) << ninja; - ASSERT_NE(arPos, std::string::npos) << ninja; - EXPECT_LT(objPos, arPos) << "archive must be linked AFTER objects\n" << ninja; -} - TEST(NinjaBackend, RootPackageCxxflagsAreEmittedOncePerUnit) { auto plan = minimal_plan(); plan.manifest.buildConfig.cxxflags = {"-DROOT_FLAG=1"}; From 6097dbf15c9caccc44206eebd01c8f2ccf2ffef1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 08:52:40 +0800 Subject: [PATCH 07/10] fix(link): scope main-object exclusion to dev-dependencies only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit scanned ALL dependency sources for a main definition and dropped matches from self-main consumers. source_defines_main false-positived on a regular C dep source while building xlings, dropping a needed libarchive object → 'undefined reference to archive_entry_*' on every platform. Test frameworks are ALWAYS dev-dependencies; regular deps (libarchive, lzma, …) must never be touched. Scope the scan to dev-dep packages only (derived from the root manifest's [dev-dependencies] via dependency_name_candidates). Regular deps are now byte-for-byte unchanged, and plain Finished release [optimized] in 0.01s (no dev-deps resolved) is unaffected by construction. Verified locally: mcpp's own Resolving toolchain Resolved gcc@16.1.0 → @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++ Compiling mcpp v0.0.64 (.) Cached mcpplibs.cmdline v0.0.1 Cached gtest v1.15.2 (dev) Compiling test_bmi_cache (test) Compiling test_compile_commands (test) Compiling test_config (test) Compiling test_doctor_runpath (test) Compiling test_dyndep (test) Compiling test_fingerprint (test) Compiling test_install_integrity (test) Compiling test_main_detection (test) Compiling test_mangle (test) Compiling test_manifest (test) Compiling test_modgraph (test) Compiling test_ninja_backend (test) Compiling test_p1689 (test) Compiling test_pack_modes (test) Compiling test_pm_compat (test) Compiling test_pm_package_fetcher (test) Compiling test_process_run_exec (test) Compiling test_process_seal_stdin (test) Compiling test_toml (test) Compiling test_toolchain_detect (test) Compiling test_toolchain_registry (test) Compiling test_toolchain_stdmod (test) Compiling test_version_req (test) Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 8 tests from 1 test suite. [----------] Global test environment set-up. [----------] 8 tests from BmiCache [ RUN ] BmiCache.KeyDirLayoutMatchesDocs26 [ OK ] BmiCache.KeyDirLayoutMatchesDocs26 (0 ms) [ RUN ] BmiCache.IsCachedFalseWhenManifestMissing [ OK ] BmiCache.IsCachedFalseWhenManifestMissing (0 ms) [ RUN ] BmiCache.PopulateThenStageRoundTrip [ OK ] BmiCache.PopulateThenStageRoundTrip (0 ms) [ RUN ] BmiCache.StageIntoDoesNotTouchIdenticalOutputs [ OK ] BmiCache.StageIntoDoesNotTouchIdenticalOutputs (0 ms) [ RUN ] BmiCache.StageIntoDoesNotOverwriteExistingOutputs [ OK ] BmiCache.StageIntoDoesNotOverwriteExistingOutputs (0 ms) [ RUN ] BmiCache.IsCachedFalseWhenSentinelExistsButFileMissing [ OK ] BmiCache.IsCachedFalseWhenSentinelExistsButFileMissing (0 ms) [ RUN ] BmiCache.PopulateFailsIfBuildOutputMissing [ OK ] BmiCache.PopulateFailsIfBuildOutputMissing (0 ms) [ RUN ] BmiCache.PopulateSkipsWhenLockHeld [ OK ] BmiCache.PopulateSkipsWhenLockHeld (0 ms) [----------] 8 tests from BmiCache (4 ms total) [----------] Global test environment tear-down [==========] 8 tests from 1 test suite ran. (4 ms total) [ PASSED ] 8 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 4 tests from 1 test suite. [----------] Global test environment set-up. [----------] 4 tests from CompileCommandsMerge [ RUN ] CompileCommandsMerge.PreservesPriorEntriesForFilesNotInFreshPlan [ OK ] CompileCommandsMerge.PreservesPriorEntriesForFilesNotInFreshPlan (0 ms) [ RUN ] CompileCommandsMerge.PrunesPriorEntriesWhoseFileNoLongerExists [ OK ] CompileCommandsMerge.PrunesPriorEntriesWhoseFileNoLongerExists (0 ms) [ RUN ] CompileCommandsMerge.FreshEntryWinsAndNoDuplicatePerFile [ OK ] CompileCommandsMerge.FreshEntryWinsAndNoDuplicatePerFile (0 ms) [ RUN ] CompileCommandsMerge.MalformedExistingFallsBackToFresh [ OK ] CompileCommandsMerge.MalformedExistingFallsBackToFresh (0 ms) [----------] 4 tests from CompileCommandsMerge (0 ms total) [----------] Global test environment tear-down [==========] 4 tests from 1 test suite ran. (0 ms total) [ PASSED ] 4 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 6 tests from 1 test suite. [----------] Global test environment set-up. [----------] 6 tests from Config [ RUN ] Config.ProjectXlingsDataRootsIncludeLegacyAndNestedLayouts [ OK ] Config.ProjectXlingsDataRootsIncludeLegacyAndNestedLayouts (0 ms) [ RUN ] Config.ProjectIndexDataInitializedChecksNestedXlingsData [ OK ] Config.ProjectIndexDataInitializedChecksNestedXlingsData (0 ms) [ RUN ] Config.ResolveProjectIndexPathUsesProjectRootForRelativeLocalIndex [ OK ] Config.ResolveProjectIndexPathUsesProjectRootForRelativeLocalIndex (0 ms) [ RUN ] Config.ProjectIndexJsonEscapesLocalIndexPath [ OK ] Config.ProjectIndexJsonEscapesLocalIndexPath (0 ms) [ RUN ] Config.ProjectIndexDirExposesOfficialXimIndex [ OK ] Config.ProjectIndexDirExposesOfficialXimIndex (0 ms) [ RUN ] Config.ProjectLocalIndexStaleCacheIsRemoved [ OK ] Config.ProjectLocalIndexStaleCacheIsRemoved (0 ms) [----------] 6 tests from Config (1 ms total) [----------] Global test environment tear-down [==========] 6 tests from 1 test suite ran. (1 ms total) [ PASSED ] 6 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 4 tests from 1 test suite. [----------] Global test environment set-up. [----------] 4 tests from DoctorRunpath [ RUN ] DoctorRunpath.ParsesRunpathColonSeparatedDirs [ OK ] DoctorRunpath.ParsesRunpathColonSeparatedDirs (0 ms) [ RUN ] DoctorRunpath.ParsesLegacyRpath [ OK ] DoctorRunpath.ParsesLegacyRpath (0 ms) [ RUN ] DoctorRunpath.NoRunpathYieldsEmpty [ OK ] DoctorRunpath.NoRunpathYieldsEmpty (0 ms) [ RUN ] DoctorRunpath.DropsEmptyTokens [ OK ] DoctorRunpath.DropsEmptyTokens (0 ms) [----------] 4 tests from DoctorRunpath (0 ms total) [----------] Global test environment tear-down [==========] 4 tests from 1 test suite ran. (0 ms total) [ PASSED ] 4 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 6 tests from 1 test suite. [----------] Global test environment set-up. [----------] 6 tests from Dyndep [ RUN ] Dyndep.BmiBasenameSanitizes [ OK ] Dyndep.BmiBasenameSanitizes (0 ms) [ RUN ] Dyndep.ParseDdiPickProvidesRequires [ OK ] Dyndep.ParseDdiPickProvidesRequires (0 ms) [ RUN ] Dyndep.EmitDyndepBasic [ OK ] Dyndep.EmitDyndepBasic (0 ms) [ RUN ] Dyndep.EmitDyndepNoRequires [ OK ] Dyndep.EmitDyndepNoRequires (0 ms) [ RUN ] Dyndep.EmitDyndepSelfProvideFiltered [ OK ] Dyndep.EmitDyndepSelfProvideFiltered (0 ms) [ RUN ] Dyndep.EmitDyndepFromFiles [ OK ] Dyndep.EmitDyndepFromFiles (0 ms) [----------] 6 tests from Dyndep (0 ms total) [----------] Global test environment tear-down [==========] 6 tests from 1 test suite ran. (0 ms total) [ PASSED ] 6 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 5 tests from 1 test suite. [----------] Global test environment set-up. [----------] 5 tests from Fingerprint [ RUN ] Fingerprint.DeterministicForSameInputs [ OK ] Fingerprint.DeterministicForSameInputs (0 ms) [ RUN ] Fingerprint.ProducesSixteenHexChars [ OK ] Fingerprint.ProducesSixteenHexChars (0 ms) [ RUN ] Fingerprint.AllTenFieldsAffectHash [ OK ] Fingerprint.AllTenFieldsAffectHash (0 ms) [ RUN ] Fingerprint.StableAcrossBinaryPathsWhenDriverIdentMatches [ OK ] Fingerprint.StableAcrossBinaryPathsWhenDriverIdentMatches (0 ms) [ RUN ] Fingerprint.HashStringMatchesHashFile [ OK ] Fingerprint.HashStringMatchesHashFile (0 ms) [----------] 5 tests from Fingerprint (0 ms total) [----------] Global test environment tear-down [==========] 5 tests from 1 test suite ran. (0 ms total) [ PASSED ] 5 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 5 tests from 1 test suite. [----------] Global test environment set-up. [----------] 5 tests from InstallIntegrityStash [ RUN ] InstallIntegrityStash.RestoresLegacyPackageOnFailedReinstall [ OK ] InstallIntegrityStash.RestoresLegacyPackageOnFailedReinstall (0 ms) [ RUN ] InstallIntegrityStash.CommitDropsBackupAndKeepsNewInstall [ OK ] InstallIntegrityStash.CommitDropsBackupAndKeepsNewInstall (0 ms) [ RUN ] InstallIntegrityStash.KeepsNewCompleteInstallWhenUncommitted [ OK ] InstallIntegrityStash.KeepsNewCompleteInstallWhenUncommitted (0 ms) [ RUN ] InstallIntegrityStash.DiscardsNonLegacyResidueOnFailure [ OK ] InstallIntegrityStash.DiscardsNonLegacyResidueOnFailure (0 ms) [ RUN ] InstallIntegrityStash.NoopWhenAlreadyComplete [ OK ] InstallIntegrityStash.NoopWhenAlreadyComplete (0 ms) [----------] 5 tests from InstallIntegrityStash (1 ms total) [----------] Global test environment tear-down [==========] 5 tests from 1 test suite ran. (1 ms total) [ PASSED ] 5 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 8 tests from 1 test suite. [----------] Global test environment set-up. [----------] 8 tests from MainDetection [ RUN ] MainDetection.RealMainIsDetected [ OK ] MainDetection.RealMainIsDetected (0 ms) [ RUN ] MainDetection.RealMainWithArgsIsDetected [ OK ] MainDetection.RealMainWithArgsIsDetected (0 ms) [ RUN ] MainDetection.AutoMainIsDetected [ OK ] MainDetection.AutoMainIsDetected (0 ms) [ RUN ] MainDetection.MainInsideStringLiteralIsIgnored [ OK ] MainDetection.MainInsideStringLiteralIsIgnored (0 ms) [ RUN ] MainDetection.MainInsideRawStringIsIgnored [ OK ] MainDetection.MainInsideRawStringIsIgnored (0 ms) [ RUN ] MainDetection.MainInsideCommentIsIgnored [ OK ] MainDetection.MainInsideCommentIsIgnored (0 ms) [ RUN ] MainDetection.NoMainGtestStyleIsFalse [ OK ] MainDetection.NoMainGtestStyleIsFalse (0 ms) [ RUN ] MainDetection.SimilarIdentifierIsNotMain [ OK ] MainDetection.SimilarIdentifierIsNotMain (0 ms) [----------] 8 tests from MainDetection (0 ms total) [----------] Global test environment tear-down [==========] 8 tests from 1 test suite ran. (0 ms total) [ PASSED ] 8 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 11 tests from 1 test suite. [----------] Global test environment set-up. [----------] 11 tests from Mangle [ RUN ] Mangle.NameFormat [ OK ] Mangle.NameFormat (0 ms) [ RUN ] Mangle.RewriteEmpty [ OK ] Mangle.RewriteEmpty (0 ms) [ RUN ] Mangle.RewriteModuleDecl [ OK ] Mangle.RewriteModuleDecl (0 ms) [ RUN ] Mangle.RewritePartitionDecl [ OK ] Mangle.RewritePartitionDecl (0 ms) [ RUN ] Mangle.RewriteImports [ OK ] Mangle.RewriteImports (0 ms) [ RUN ] Mangle.KeepBarePartitionImport [ OK ] Mangle.KeepBarePartitionImport (0 ms) [ RUN ] Mangle.KeepNonMatching [ OK ] Mangle.KeepNonMatching (0 ms) [ RUN ] Mangle.MultipleLines [ OK ] Mangle.MultipleLines (0 ms) [ RUN ] Mangle.LeadingWhitespace [ OK ] Mangle.LeadingWhitespace (0 ms) [ RUN ] Mangle.NoTrailingNewline [ OK ] Mangle.NoTrailingNewline (0 ms) [ RUN ] Mangle.DottedNames [ OK ] Mangle.DottedNames (0 ms) [----------] 11 tests from Mangle (0 ms total) [----------] Global test environment tear-down [==========] 11 tests from 1 test suite ran. (0 ms total) [ PASSED ] 11 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 60 tests from 5 test suites. [----------] Global test environment set-up. [----------] 34 tests from Manifest [ RUN ] Manifest.MinimalValid [ OK ] Manifest.MinimalValid (0 ms) [ RUN ] Manifest.SharedTargetSoname [ OK ] Manifest.SharedTargetSoname (0 ms) [ RUN ] Manifest.RejectsSonameOnNonSharedTarget [ OK ] Manifest.RejectsSonameOnNonSharedTarget (0 ms) [ RUN ] Manifest.PackageStandardCpp26AcceptedAndMirrored [ OK ] Manifest.PackageStandardCpp26AcceptedAndMirrored (0 ms) [ RUN ] Manifest.LegacyLanguageCpp2cNormalizesToCpp26 [ OK ] Manifest.LegacyLanguageCpp2cNormalizesToCpp26 (0 ms) [ RUN ] Manifest.RejectsStdFlagInCxxflags [ OK ] Manifest.RejectsStdFlagInCxxflags (0 ms) [ RUN ] Manifest.RejectMissingVersion [ OK ] Manifest.RejectMissingVersion (0 ms) [ RUN ] Manifest.RejectImportStdWithoutCpp23 [ OK ] Manifest.RejectImportStdWithoutCpp23 (0 ms) [ RUN ] Manifest.RejectModulesFalse [ OK ] Manifest.RejectModulesFalse (0 ms) [ RUN ] Manifest.ParsesDependencies [ OK ] Manifest.ParsesDependencies (0 ms) [ RUN ] Manifest.ParsesDependencyVisibility [ OK ] Manifest.ParsesDependencyVisibility (0 ms) [ RUN ] Manifest.RejectsInvalidDependencyVisibility [ OK ] Manifest.RejectsInvalidDependencyVisibility (0 ms) [ RUN ] Manifest.DefaultTemplateRoundTrip [ OK ] Manifest.DefaultTemplateRoundTrip (0 ms) [ RUN ] Manifest.BuildCflagsCxxflagsAndCStandard [ OK ] Manifest.BuildCflagsCxxflagsAndCStandard (0 ms) [ RUN ] Manifest.BuildMacosDeploymentTarget [ OK ] Manifest.BuildMacosDeploymentTarget (0 ms) [ RUN ] Manifest.BuildMacosDeploymentTargetDefaultsEmpty [ OK ] Manifest.BuildMacosDeploymentTargetDefaultsEmpty (0 ms) [ RUN ] Manifest.RuntimeConfig [ OK ] Manifest.RuntimeConfig (0 ms) [ RUN ] Manifest.DependenciesFlatDefaultNamespace [ OK ] Manifest.DependenciesFlatDefaultNamespace (0 ms) [ RUN ] Manifest.DependenciesNamespacedSubtable [ OK ] Manifest.DependenciesNamespacedSubtable (0 ms) [ RUN ] Manifest.DependenciesLegacyDottedKeyStillParsed [ OK ] Manifest.DependenciesLegacyDottedKeyStillParsed (0 ms) [ RUN ] Manifest.DependenciesDottedSelectorPreservesUserKeyAndCandidates [ OK ] Manifest.DependenciesDottedSelectorPreservesUserKeyAndCandidates (0 ms) [ RUN ] Manifest.DependenciesNamespacedSubtableNestedDottedKeyIsCanonical [ OK ] Manifest.DependenciesNamespacedSubtableNestedDottedKeyIsCanonical (0 ms) [ RUN ] Manifest.DependenciesInlineSpecCoexistsWithSubtable [ OK ] Manifest.DependenciesInlineSpecCoexistsWithSubtable (0 ms) [ RUN ] Manifest.WorkspaceSectionParsed [ OK ] Manifest.WorkspaceSectionParsed (0 ms) [ RUN ] Manifest.WorkspaceDependenciesUseDottedSelectorRules [ OK ] Manifest.WorkspaceDependenciesUseDottedSelectorRules (0 ms) [ RUN ] Manifest.WorkspaceTrueInDependency [ OK ] Manifest.WorkspaceTrueInDependency (0 ms) [ RUN ] Manifest.NoWorkspaceSectionMeansNotPresent [ OK ] Manifest.NoWorkspaceSectionMeansNotPresent (0 ms) [ RUN ] Manifest.LibRootInferredFromPackageName [ OK ] Manifest.LibRootInferredFromPackageName (0 ms) [ RUN ] Manifest.LibRootBareNameNoNamespace [ OK ] Manifest.LibRootBareNameNoNamespace (0 ms) [ RUN ] Manifest.LibRootExplicitOverride [ OK ] Manifest.LibRootExplicitOverride (0 ms) [ RUN ] Manifest.HasLibTargetFalseForBareBinaryManifest [ OK ] Manifest.HasLibTargetFalseForBareBinaryManifest (0 ms) [ RUN ] Manifest.ParsesPerTargetFlagsAndRequiredFeatures [ OK ] Manifest.ParsesPerTargetFlagsAndRequiredFeatures (0 ms) [ RUN ] Manifest.WarnsOnUnsupportedTargetKey [ OK ] Manifest.WarnsOnUnsupportedTargetKey (0 ms) [ RUN ] Manifest.RejectsStdFlagInTargetCxxflags [ OK ] Manifest.RejectsStdFlagInTargetCxxflags (0 ms) [----------] 34 tests from Manifest (0 ms total) [----------] 3 tests from ListXpkgVersions [ RUN ] ListXpkgVersions.MultipleEntriesAcrossPlatforms [ OK ] ListXpkgVersions.MultipleEntriesAcrossPlatforms (0 ms) [ RUN ] ListXpkgVersions.MissingXpmReturnsEmpty [ OK ] ListXpkgVersions.MissingXpmReturnsEmpty (0 ms) [ RUN ] ListXpkgVersions.IgnoresCommentedEntries [ OK ] ListXpkgVersions.IgnoresCommentedEntries (0 ms) [----------] 3 tests from ListXpkgVersions (0 ms total) [----------] 7 tests from SynthesizeFromXpkgLua [ RUN ] SynthesizeFromXpkgLua.CflagsCxxflagsLdflagsAndCStandard [ OK ] SynthesizeFromXpkgLua.CflagsCxxflagsLdflagsAndCStandard (0 ms) [ RUN ] SynthesizeFromXpkgLua.SharedTargetSoname [ OK ] SynthesizeFromXpkgLua.SharedTargetSoname (0 ms) [ RUN ] SynthesizeFromXpkgLua.RuntimeConfig [ OK ] SynthesizeFromXpkgLua.RuntimeConfig (0 ms) [ RUN ] SynthesizeFromXpkgLua.AppliesCurrentPlatformMcppOverlay [ OK ] SynthesizeFromXpkgLua.AppliesCurrentPlatformMcppOverlay (0 ms) [ RUN ] SynthesizeFromXpkgLua.GeneratedFiles [ OK ] SynthesizeFromXpkgLua.GeneratedFiles (0 ms) [ RUN ] SynthesizeFromXpkgLua.DepsKeySplitNamespace [ OK ] SynthesizeFromXpkgLua.DepsKeySplitNamespace (0 ms) [ RUN ] SynthesizeFromXpkgLua.DepsDottedSelectorsUseManifestRules [ OK ] SynthesizeFromXpkgLua.DepsDottedSelectorsUseManifestRules (0 ms) [----------] 7 tests from SynthesizeFromXpkgLua (0 ms total) [----------] 7 tests from XpkgIdentity [ RUN ] XpkgIdentity.CompatDescriptorMatchesCompatRequest [ OK ] XpkgIdentity.CompatDescriptorMatchesCompatRequest (0 ms) [ RUN ] XpkgIdentity.UpstreamBareZlibDoesNotMatchCompatRequest [ OK ] XpkgIdentity.UpstreamBareZlibDoesNotMatchCompatRequest (0 ms) [ RUN ] XpkgIdentity.DescriptorDeclaringNamespaceMatchesOnlyThatNamespace [ OK ] XpkgIdentity.DescriptorDeclaringNamespaceMatchesOnlyThatNamespace (0 ms) [ RUN ] XpkgIdentity.NoDeclaredNameIsAcceptedLeniently [ OK ] XpkgIdentity.NoDeclaredNameIsAcceptedLeniently (0 ms) [ RUN ] XpkgIdentity.DefaultNamespaceBareNameGatedByFlag [ OK ] XpkgIdentity.DefaultNamespaceBareNameGatedByFlag (0 ms) [ RUN ] XpkgIdentity.EmptyNamespaceDiscoveryMatchesNamespacedDescriptor [ OK ] XpkgIdentity.EmptyNamespaceDiscoveryMatchesNamespacedDescriptor (0 ms) [ RUN ] XpkgIdentity.DefaultNamespaceRequestMatchesCompatAlias [ OK ] XpkgIdentity.DefaultNamespaceRequestMatchesCompatAlias (0 ms) [----------] 7 tests from XpkgIdentity (0 ms total) [----------] 9 tests from CanonicalIdentity [ RUN ] CanonicalIdentity.PrefixEmbeddedNameCollapses [ OK ] CanonicalIdentity.PrefixEmbeddedNameCollapses (0 ms) [ RUN ] CanonicalIdentity.BareNameCombinesWithNamespace [ OK ] CanonicalIdentity.BareNameCombinesWithNamespace (0 ms) [ RUN ] CanonicalIdentity.AlreadyQualifiedNameIsIdempotent [ OK ] CanonicalIdentity.AlreadyQualifiedNameIsIdempotent (0 ms) [ RUN ] CanonicalIdentity.NoNamespaceInheritsOwningIndex [ OK ] CanonicalIdentity.NoNamespaceInheritsOwningIndex (0 ms) [ RUN ] CanonicalIdentity.DeclaredNamespaceWinsOverIndexDefault [ OK ] CanonicalIdentity.DeclaredNamespaceWinsOverIndexDefault (0 ms) [ RUN ] CanonicalIdentity.DottedNameWithNoNamespaceSplitsOnLastDot [ OK ] CanonicalIdentity.DottedNameWithNoNamespaceSplitsOnLastDot (0 ms) [ RUN ] CanonicalIdentity.HierarchicalNamespaceIsSupported [ OK ] CanonicalIdentity.HierarchicalNamespaceIsSupported (0 ms) [ RUN ] CanonicalIdentity.BareNameNoNamespaceNoIndexStaysRootless [ OK ] CanonicalIdentity.BareNameNoNamespaceNoIndexStaysRootless (0 ms) [ RUN ] CanonicalIdentity.FromLuaReadsDeclaredFields [ OK ] CanonicalIdentity.FromLuaReadsDeclaredFields (0 ms) [----------] 9 tests from CanonicalIdentity (0 ms total) [----------] Global test environment tear-down [==========] 60 tests from 5 test suites ran. (0 ms total) [ PASSED ] 60 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 21 tests from 5 test suites. [----------] Global test environment set-up. [----------] 11 tests from Scanner [ RUN ] Scanner.ProvidesAndRequires [ OK ] Scanner.ProvidesAndRequires (0 ms) [ RUN ] Scanner.IgnoresImportsInsideRawStringLiteral [ OK ] Scanner.IgnoresImportsInsideRawStringLiteral (0 ms) [ RUN ] Scanner.IgnoresImportInsideSingleLineRawString [ OK ] Scanner.IgnoresImportInsideSingleLineRawString (0 ms) [ RUN ] Scanner.RecordsPackageLocalIncludeDirs [ OK ] Scanner.RecordsPackageLocalIncludeDirs (0 ms) [ RUN ] Scanner.UsesResolvedPackagePrivateBuildIncludeDirs [ OK ] Scanner.UsesResolvedPackagePrivateBuildIncludeDirs (0 ms) [ RUN ] Scanner.PartitionImportFromPrimaryInterface [ OK ] Scanner.PartitionImportFromPrimaryInterface (0 ms) [ RUN ] Scanner.PartitionImportFromAnotherPartition [ OK ] Scanner.PartitionImportFromAnotherPartition (0 ms) [ RUN ] Scanner.PartitionImportWithDottedModuleName [ OK ] Scanner.PartitionImportWithDottedModuleName (0 ms) [ RUN ] Scanner.RejectsConditionalImport [ OK ] Scanner.RejectsConditionalImport (0 ms) [ RUN ] Scanner.RejectsHeaderUnit [ OK ] Scanner.RejectsHeaderUnit (0 ms) [ RUN ] Scanner.ObjectiveCSourceIsCLike [ OK ] Scanner.ObjectiveCSourceIsCLike (0 ms) [----------] 11 tests from Scanner (2 ms total) [----------] 7 tests from Validate [ RUN ] Validate.ModuleNameNotRequiredToMatchPackageName [ OK ] Validate.ModuleNameNotRequiredToMatchPackageName (0 ms) [ RUN ] Validate.ForbiddenTopName [ OK ] Validate.ForbiddenTopName (0 ms) [ RUN ] Validate.LibRootHappyPath [ OK ] Validate.LibRootHappyPath (0 ms) [ RUN ] Validate.LibRootExportsPartitionIsError [ OK ] Validate.LibRootExportsPartitionIsError (0 ms) [ RUN ] Validate.LibRootDifferentModuleNameIsAllowed [ OK ] Validate.LibRootDifferentModuleNameIsAllowed (0 ms) [ RUN ] Validate.LibRootNotEnforcedForBinaryProject [ OK ] Validate.LibRootNotEnforcedForBinaryProject (0 ms) [ RUN ] Validate.LibRootMissingFileWithExplicitPathIsError [ OK ] Validate.LibRootMissingFileWithExplicitPathIsError (0 ms) [----------] 7 tests from Validate (0 ms total) [----------] 1 test from TopoSort [ RUN ] TopoSort.DetectsCycle [ OK ] TopoSort.DetectsCycle (0 ms) [----------] 1 test from TopoSort (0 ms total) [----------] 1 test from IsPublicPackage [ RUN ] IsPublicPackage.DotMarksPublic [ OK ] IsPublicPackage.DotMarksPublic (0 ms) [----------] 1 test from IsPublicPackage (0 ms total) [----------] 1 test from IsForbiddenTopModule [ RUN ] IsForbiddenTopModule.KnownNames [ OK ] IsForbiddenTopModule.KnownNames (0 ms) [----------] 1 test from IsForbiddenTopModule (0 ms total) [----------] Global test environment tear-down [==========] 21 tests from 5 test suites ran. (2 ms total) [ PASSED ] 21 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 5 tests from 1 test suite. [----------] Global test environment set-up. [----------] 5 tests from NinjaBackend [ RUN ] NinjaBackend.ObjectiveCSourceUsesCObjectRuleAndCFlags [ OK ] NinjaBackend.ObjectiveCSourceUsesCObjectRuleAndCFlags (0 ms) [ RUN ] NinjaBackend.UsesPackageCppStandardForCxxFlags [ OK ] NinjaBackend.UsesPackageCppStandardForCxxFlags (0 ms) [ RUN ] NinjaBackend.CompileCommandsUsesSameCppStandard [ OK ] NinjaBackend.CompileCommandsUsesSameCppStandard (0 ms) [ RUN ] NinjaBackend.CxxFlagsIncludeBuildIncludeDirs [ OK ] NinjaBackend.CxxFlagsIncludeBuildIncludeDirs (0 ms) [ RUN ] NinjaBackend.RootPackageCxxflagsAreEmittedOncePerUnit [ OK ] NinjaBackend.RootPackageCxxflagsAreEmittedOncePerUnit (0 ms) [----------] 5 tests from NinjaBackend (0 ms total) [----------] Global test environment tear-down [==========] 5 tests from 1 test suite ran. (0 ms total) [ PASSED ] 5 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 5 tests from 1 test suite. [----------] Global test environment set-up. [----------] 5 tests from P1689Parse [ RUN ] P1689Parse.SimpleProvider [ OK ] P1689Parse.SimpleProvider (0 ms) [ RUN ] P1689Parse.PureConsumer [ OK ] P1689Parse.PureConsumer (0 ms) [ RUN ] P1689Parse.EmptyRequires [ OK ] P1689Parse.EmptyRequires (0 ms) [ RUN ] P1689Parse.RejectsNonObject [ OK ] P1689Parse.RejectsNonObject (0 ms) [ RUN ] P1689Parse.RejectsMissingRules [ OK ] P1689Parse.RejectsMissingRules (0 ms) [----------] 5 tests from P1689Parse (0 ms total) [----------] Global test environment tear-down [==========] 5 tests from 1 test suite ran. (0 ms total) [ PASSED ] 5 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 4 tests from 1 test suite. [----------] Global test environment set-up. [----------] 4 tests from PackModes [ RUN ] PackModes.CanonicalNamesParse [ OK ] PackModes.CanonicalNamesParse (0 ms) [ RUN ] PackModes.OldNamesStayAsAliases [ OK ] PackModes.OldNamesStayAsAliases (0 ms) [ RUN ] PackModes.UnknownIsNullopt [ OK ] PackModes.UnknownIsNullopt (0 ms) [ RUN ] PackModes.CliNamesAreCanonical [ OK ] PackModes.CliNamesAreCanonical (0 ms) [----------] 4 tests from PackModes (0 ms total) [----------] Global test environment tear-down [==========] 4 tests from 1 test suite ran. (0 ms total) [ PASSED ] 4 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 14 tests from 2 test suites. [----------] Global test environment set-up. [----------] 10 tests from PmCompat [ RUN ] PmCompat.InstallDirCandidatesIncludeNestedNamespaceFallback [ OK ] PmCompat.InstallDirCandidatesIncludeNestedNamespaceFallback (0 ms) [ RUN ] PmCompat.NormalizeNestedNamespacePreservesQualifiedName [ OK ] PmCompat.NormalizeNestedNamespacePreservesQualifiedName (0 ms) [ RUN ] PmCompat.SplitLegacyDependencyKeyMarksDottedKeyAsCompat [ OK ] PmCompat.SplitLegacyDependencyKeyMarksDottedKeyAsCompat (0 ms) [ RUN ] PmCompat.NormalizeNestedNamespaceSkipsCanonicalNamespacedDeps [ OK ] PmCompat.NormalizeNestedNamespaceSkipsCanonicalNamespacedDeps (0 ms) [ RUN ] PmCompat.DescriptorCoordinatesLegacyEmbeddedNamespace [ OK ] PmCompat.DescriptorCoordinatesLegacyEmbeddedNamespace (0 ms) [ RUN ] PmCompat.DescriptorCoordinatesCanonicalNamespaceField [ OK ] PmCompat.DescriptorCoordinatesCanonicalNamespaceField (0 ms) [ RUN ] PmCompat.DescriptorCoordinatesRootPackageStaysInRoot [ OK ] PmCompat.DescriptorCoordinatesRootPackageStaysInRoot (0 ms) [ RUN ] PmCompat.DescriptorCoordinatesLegacyDottedNameWithoutNamespace [ OK ] PmCompat.DescriptorCoordinatesLegacyDottedNameWithoutNamespace (0 ms) [ RUN ] PmCompat.DescriptorCoordinatesFallsBackToSpecWhenNameMissing [ OK ] PmCompat.DescriptorCoordinatesFallsBackToSpecWhenNameMissing (0 ms) [ RUN ] PmCompat.DescriptorCoordinatesCompatNamespace [ OK ] PmCompat.DescriptorCoordinatesCompatNamespace (0 ms) [----------] 10 tests from PmCompat (0 ms total) [----------] 4 tests from DependencySelector [ RUN ] DependencySelector.DottedSelectorBuildsOmittedMcpplibsPriorityCandidates [ OK ] DependencySelector.DottedSelectorBuildsOmittedMcpplibsPriorityCandidates (0 ms) [ RUN ] DependencySelector.BareSelectorBuildsOmittedMcpplibsThenPeerRootCandidates [ OK ] DependencySelector.BareSelectorBuildsOmittedMcpplibsThenPeerRootCandidates (0 ms) [ RUN ] DependencySelector.ExplicitMcpplibsPrefixDoesNotAddPeerFallback [ OK ] DependencySelector.ExplicitMcpplibsPrefixDoesNotAddPeerFallback (0 ms) [ RUN ] DependencySelector.ExplicitRootSelectorHasOnlyThatRoot [ OK ] DependencySelector.ExplicitRootSelectorHasOnlyThatRoot (0 ms) [----------] 4 tests from DependencySelector (0 ms total) [----------] Global test environment tear-down [==========] 14 tests from 2 test suites ran. (0 ms total) [ PASSED ] 14 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 4 tests from 1 test suite. [----------] Global test environment set-up. [----------] 4 tests from PmPackageFetcher [ RUN ] PmPackageFetcher.ResolvesCompatZlibNotForeignBareZlib [ OK ] PmPackageFetcher.ResolvesCompatZlibNotForeignBareZlib (1 ms) [ RUN ] PmPackageFetcher.ForeignBareZlibAloneDoesNotSatisfyCompatRequest [ OK ] PmPackageFetcher.ForeignBareZlibAloneDoesNotSatisfyCompatRequest (0 ms) [ RUN ] PmPackageFetcher.DefaultNamespaceRequestResolvesCompatAliasDescriptor [ OK ] PmPackageFetcher.DefaultNamespaceRequestResolvesCompatAliasDescriptor (0 ms) [ RUN ] PmPackageFetcher.LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor [ OK ] PmPackageFetcher.LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor (0 ms) [----------] 4 tests from PmPackageFetcher (2 ms total) [----------] Global test environment tear-down [==========] 4 tests from 1 test suite ran. (2 ms total) [ PASSED ] 4 tests. Compiling test_xlings (test) Compiling test_xpkg_emit (test) Finished test [optimized] in 0.01s Running bin/test_bmi_cache test_bmi_cache ... ok Running bin/test_compile_commands test_compile_commands ... ok Running bin/test_config test_config ... ok Running bin/test_doctor_runpath test_doctor_runpath ... ok Running bin/test_dyndep test_dyndep ... ok Running bin/test_fingerprint test_fingerprint ... ok Running bin/test_install_integrity test_install_integrity ... ok Running bin/test_main_detection test_main_detection ... ok Running bin/test_mangle test_mangle ... ok Running bin/test_manifest test_manifest ... ok Running bin/test_modgraph test_modgraph ... ok Running bin/test_ninja_backend test_ninja_backend ... ok Running bin/test_p1689 test_p1689 ... ok Running bin/test_pack_modes test_pack_modes ... ok Running bin/test_pm_compat test_pm_compat ... ok Running bin/test_pm_package_fetcher test_pm_package_fetcher ... ok Running bin/test_process_run_exec Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 6 tests from 2 test suites. [----------] Global test environment set-up. [----------] 4 tests from RunExec [ RUN ] RunExec.DoesNotMutateParentEnvironment [ OK ] RunExec.DoesNotMutateParentEnvironment (1 ms) [ RUN ] RunExec.ChildSeesInjectedEnv [ OK ] RunExec.ChildSeesInjectedEnv (1 ms) [ RUN ] RunExec.PropagatesChildExitCode [ OK ] RunExec.PropagatesChildExitCode (1 ms) [ RUN ] RunExec.ReturnsErrorWhenProgramMissing [ OK ] RunExec.ReturnsErrorWhenProgramMissing (0 ms) [----------] 4 tests from RunExec (3 ms total) [----------] 2 tests from CaptureExec [ RUN ] CaptureExec.CapturesStdoutWithoutShell [ OK ] CaptureExec.CapturesStdoutWithoutShell (1 ms) [ RUN ] CaptureExec.CapturesStderrCombined [ OK ] CaptureExec.CapturesStderrCombined (1 ms) [----------] 2 tests from CaptureExec (2 ms total) [----------] Global test environment tear-down [==========] 6 tests from 2 test suites ran. (5 ms total) [ PASSED ] 6 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 3 tests from 1 test suite. [----------] Global test environment set-up. [----------] 3 tests from ProcessSealStdin [ RUN ] ProcessSealStdin.RunSilentDoesNotHangWhenParentStdinIsOpenPipe [ OK ] ProcessSealStdin.RunSilentDoesNotHangWhenParentStdinIsOpenPipe (3 ms) [ RUN ] ProcessSealStdin.CaptureDoesNotHangWhenParentStdinIsOpenPipe [ OK ] ProcessSealStdin.CaptureDoesNotHangWhenParentStdinIsOpenPipe (3 ms) [ RUN ] ProcessSealStdin.RunStreamingDoesNotHangWhenParentStdinIsOpenPipe [ OK ] ProcessSealStdin.RunStreamingDoesNotHangWhenParentStdinIsOpenPipe (3 ms) [----------] 3 tests from ProcessSealStdin (10 ms total) [----------] Global test environment tear-down [==========] 3 tests from 1 test suite ran. (10 ms total) [ PASSED ] 3 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 9 tests from 1 test suite. [----------] Global test environment set-up. [----------] 9 tests from Toml [ RUN ] Toml.EmptyDocumentParses [ OK ] Toml.EmptyDocumentParses (0 ms) [ RUN ] Toml.SimpleKeyValue [ OK ] Toml.SimpleKeyValue (0 ms) [ RUN ] Toml.NestedTables [ OK ] Toml.NestedTables (0 ms) [ RUN ] Toml.ArrayOfStrings [ OK ] Toml.ArrayOfStrings (0 ms) [ RUN ] Toml.ArrayAllowsTrailingComma [ OK ] Toml.ArrayAllowsTrailingComma (0 ms) [ RUN ] Toml.EscapedString [ OK ] Toml.EscapedString (0 ms) [ RUN ] Toml.RejectUnterminatedString [ OK ] Toml.RejectUnterminatedString (0 ms) [ RUN ] Toml.CommentsIgnored [ OK ] Toml.CommentsIgnored (0 ms) [ RUN ] Toml.EscapeStringHelper [ OK ] Toml.EscapeStringHelper (0 ms) [----------] 9 tests from Toml (0 ms total) [----------] Global test environment tear-down [==========] 9 tests from 1 test suite ran. (0 ms total) [ PASSED ] 9 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 8 tests from 2 test suites. [----------] Global test environment set-up. [----------] 3 tests from ToolchainDetect [ RUN ] ToolchainDetect.ClangVersionOutputIsNotMisclassifiedByGccPaths [ OK ] ToolchainDetect.ClangVersionOutputIsNotMisclassifiedByGccPaths (29 ms) [ RUN ] ToolchainDetect.IgnoresTargetRuntimeLibraryPathDuringProbe [ OK ] ToolchainDetect.IgnoresTargetRuntimeLibraryPathDuringProbe (28 ms) [ RUN ] ToolchainDetect.PopulatesDriverIdentFromVersionOutput [ OK ] ToolchainDetect.PopulatesDriverIdentFromVersionOutput (30 ms) [----------] 3 tests from ToolchainDetect (88 ms total) [----------] 5 tests from NormalizeDriverOutput [ RUN ] NormalizeDriverOutput.TrimsWhitespaceAndCollapsesBlankLines [ OK ] NormalizeDriverOutput.TrimsWhitespaceAndCollapsesBlankLines (0 ms) [ RUN ] NormalizeDriverOutput.IsStableAcrossInstallPrefixes [ OK ] NormalizeDriverOutput.IsStableAcrossInstallPrefixes (0 ms) [ RUN ] NormalizeDriverOutput.ReplacesLocalInstallPaths [ OK ] NormalizeDriverOutput.ReplacesLocalInstallPaths (0 ms) [ RUN ] NormalizeDriverOutput.DistinguishesDifferentVersions [ OK ] NormalizeDriverOutput.DistinguishesDifferentVersions (0 ms) [ RUN ] NormalizeDriverOutput.EmptyInputProducesEmpty [ OK ] NormalizeDriverOutput.EmptyInputProducesEmpty (0 ms) [----------] 5 tests from NormalizeDriverOutput (0 ms total) [----------] Global test environment tear-down [==========] 8 tests from 2 test suites ran. (89 ms total) [ PASSED ] 8 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 4 tests from 1 test suite. [----------] Global test environment set-up. [----------] 4 tests from ToolchainRegistry [ RUN ] ToolchainRegistry.MapsGccSpecToGccPackage [ OK ] ToolchainRegistry.MapsGccSpecToGccPackage (0 ms) [ RUN ] ToolchainRegistry.MapsGccMuslSuffixToMuslGccPackage [ OK ] ToolchainRegistry.MapsGccMuslSuffixToMuslGccPackage (0 ms) [ RUN ] ToolchainRegistry.MapsLlvmAndClangAliasesToLlvmPackage [ OK ] ToolchainRegistry.MapsLlvmAndClangAliasesToLlvmPackage (0 ms) [ RUN ] ToolchainRegistry.ResolvesPartialMuslVersionForDisplayAndPackage [ OK ] ToolchainRegistry.ResolvesPartialMuslVersionForDisplayAndPackage (0 ms) [----------] 4 tests from ToolchainRegistry (0 ms total) [----------] Global test environment tear-down [==========] 4 tests from 1 test suite ran. (0 ms total) [ PASSED ] 4 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 3 tests from 1 test suite. [----------] Global test environment set-up. [----------] 3 tests from ToolchainStdmod [ RUN ] ToolchainStdmod.GccStdModuleCommandUsesRequestedStandard [ OK ] ToolchainStdmod.GccStdModuleCommandUsesRequestedStandard (0 ms) [ RUN ] ToolchainStdmod.ClangStdModuleCommandsUseRequestedStandard [ OK ] ToolchainStdmod.ClangStdModuleCommandsUseRequestedStandard (0 ms) [ RUN ] ToolchainStdmod.ClangStdCompatCommandsUseRequestedStandard [ OK ] ToolchainStdmod.ClangStdCompatCommandsUseRequestedStandard (0 ms) [----------] 3 tests from ToolchainStdmod (0 ms total) [----------] Global test environment tear-down [==========] 3 tests from 1 test suite ran. (0 ms total) [ PASSED ] 3 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 10 tests from 1 test suite. [----------] Global test environment set-up. [----------] 10 tests from VersionReq [ RUN ] VersionReq.ParseVersion [ OK ] VersionReq.ParseVersion (0 ms) [ RUN ] VersionReq.ParseAny [ OK ] VersionReq.ParseAny (0 ms) [ RUN ] VersionReq.MatchExact [ OK ] VersionReq.MatchExact (0 ms) [ RUN ] VersionReq.MatchCaretBare [ OK ] VersionReq.MatchCaretBare (0 ms) [ RUN ] VersionReq.MatchCaretZeroMajor [ OK ] VersionReq.MatchCaretZeroMajor (0 ms) [ RUN ] VersionReq.MatchTilde [ OK ] VersionReq.MatchTilde (0 ms) [ RUN ] VersionReq.MatchRangeAnd [ OK ] VersionReq.MatchRangeAnd (0 ms) [ RUN ] VersionReq.ChooseBest [ OK ] VersionReq.ChooseBest (0 ms) [ RUN ] VersionReq.ChooseUnsatisfiable [ OK ] VersionReq.ChooseUnsatisfiable (0 ms) [ RUN ] VersionReq.RejectsGarbage [ OK ] VersionReq.RejectsGarbage (0 ms) [----------] 10 tests from VersionReq (0 ms total) [----------] Global test environment tear-down [==========] 10 tests from 1 test suite ran. (0 ms total) [ PASSED ] 10 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 14 tests from 3 test suites. [----------] Global test environment set-up. [----------] 10 tests from XlingsIndexFreshness [ RUN ] XlingsIndexFreshness.RequiresDefaultMcpplibsIndex [ OK ] XlingsIndexFreshness.RequiresDefaultMcpplibsIndex (0 ms) [ RUN ] XlingsIndexFreshness.AcceptsFreshDefaultMcpplibsIndex [ OK ] XlingsIndexFreshness.AcceptsFreshDefaultMcpplibsIndex (0 ms) [ RUN ] XlingsIndexFreshness.RequiresRefreshMarkerForDefaultMcpplibsIndex [ OK ] XlingsIndexFreshness.RequiresRefreshMarkerForDefaultMcpplibsIndex (0 ms) [ RUN ] XlingsIndexFreshness.RejectsStaleRefreshMarker [ OK ] XlingsIndexFreshness.RejectsStaleRefreshMarker (0 ms) [ RUN ] XlingsIndexFreshness.RequiresOfficialXimIndexEvenWhenDefaultIndexIsFresh [ OK ] XlingsIndexFreshness.RequiresOfficialXimIndexEvenWhenDefaultIndexIsFresh (0 ms) [ RUN ] XlingsIndexFreshness.AcceptsFreshOfficialXimIndex [ OK ] XlingsIndexFreshness.AcceptsFreshOfficialXimIndex (0 ms) [ RUN ] XlingsIndexFreshness.RequiresOfficialPackageFileEvenWhenOfficialIndexIsFresh [ OK ] XlingsIndexFreshness.RequiresOfficialPackageFileEvenWhenOfficialIndexIsFresh (0 ms) [ RUN ] XlingsIndexFreshness.AcceptsFreshOfficialPackageFile [ OK ] XlingsIndexFreshness.AcceptsFreshOfficialPackageFile (0 ms) [ RUN ] XlingsIndexFreshness.RejectsOfficialPackageCacheWithForeignPath [ OK ] XlingsIndexFreshness.RejectsOfficialPackageCacheWithForeignPath (0 ms) [ RUN ] XlingsIndexFreshness.AcceptsOfficialPackageCacheWithCurrentPath [ OK ] XlingsIndexFreshness.AcceptsOfficialPackageCacheWithCurrentPath (0 ms) [----------] 10 tests from XlingsIndexFreshness (4 ms total) [----------] 3 tests from XlingsSiblingPackage [ RUN ] XlingsSiblingPackage.MetadataOnlyHuskIsNotContent [ OK ] XlingsSiblingPackage.MetadataOnlyHuskIsNotContent (0 ms) [ RUN ] XlingsSiblingPackage.SkipsHuskAndFindsPayloadUnderOtherPrefix [ OK ] XlingsSiblingPackage.SkipsHuskAndFindsPayloadUnderOtherPrefix (0 ms) [ RUN ] XlingsSiblingPackage.RequiredRelPathRejectsContentfulButWrongCandidate [ OK ] XlingsSiblingPackage.RequiredRelPathRejectsContentfulButWrongCandidate (0 ms) [----------] 3 tests from XlingsSiblingPackage (2 ms total) [----------] 1 test from XlingsHomeTool [ RUN ] XlingsHomeTool.FindsPayloadUnderNonXimPrefix [ OK ] XlingsHomeTool.FindsPayloadUnderNonXimPrefix (0 ms) [----------] 1 test from XlingsHomeTool (0 ms total) [----------] Global test environment tear-down [==========] 14 tests from 3 test suites ran. (8 ms total) [ PASSED ] 14 tests. Running main() from /home/speak/.mcpp/registry/data/xpkgs/compat-x-compat.gtest/1.15.2/googletest-1.15.2/googletest/src/gtest_main.cc [==========] Running 7 tests from 1 test suite. [----------] Global test environment set-up. [----------] 7 tests from XpkgEmit [ RUN ] XpkgEmit.ContainsRequiredFields [ OK ] XpkgEmit.ContainsRequiredFields (0 ms) [ RUN ] XpkgEmit.RejectsLuaInjection [ OK ] XpkgEmit.RejectsLuaInjection (0 ms) [ RUN ] XpkgEmit.EscapesControlCharacters [ OK ] XpkgEmit.EscapesControlCharacters (0 ms) [ RUN ] XpkgEmit.ReleaseTarballUrl [ OK ] XpkgEmit.ReleaseTarballUrl (0 ms) [ RUN ] XpkgEmit.Sha256OfFile [ OK ] XpkgEmit.Sha256OfFile (11 ms) [ RUN ] XpkgEmit.Sha256OfFileIgnoresTargetRuntimeLibraryPath [ OK ] XpkgEmit.Sha256OfFileIgnoresTargetRuntimeLibraryPath (6 ms) [ RUN ] XpkgEmit.LongBracketSequenceInValueIsHarmless [ OK ] XpkgEmit.LongBracketSequenceInValueIsHarmless (0 ms) [----------] 7 tests from XpkgEmit (17 ms total) [----------] Global test environment tear-down [==========] 7 tests from 1 test suite ran. (17 ms total) [ PASSED ] 7 tests. test_process_run_exec ... ok Running bin/test_process_seal_stdin test_process_seal_stdin ... ok Running bin/test_toml test_toml ... ok Running bin/test_toolchain_detect test_toolchain_detect ... ok Running bin/test_toolchain_registry test_toolchain_registry ... ok Running bin/test_toolchain_stdmod test_toolchain_stdmod ... ok Running bin/test_version_req test_version_req ... ok Running bin/test_xlings test_xlings ... ok Running bin/test_xpkg_emit test_xpkg_emit ... ok test result ok. 25 passed; 0 failed; finished in 0.18s (25 unit) green; e2e 78 (3 combos) green; AND Finished release [optimized] in 0.01s of openxlings/xlings (regular libarchive dep) links cleanly — the regression is gone. --- src/build/plan.cppm | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 39dd4763..44b72ae6 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -462,20 +462,33 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // Inlining + dropping only the entry object is portable and minimal — it // leaves every other dependency's linkage byte-for-byte unchanged. // - // Detected by scanning each DEPENDENCY implementation source for a top-level - // main definition (gtest_main.cc has one; gtest-all.cc / libarchive / lzma do - // not). Generic: no per-framework knowledge — a future test framework's - // main-providing object is handled the same way. - std::set depEntryMainSources; - { - std::string rootQname = qualified_package_name(manifest); - for (auto& cu : plan.compileUnits) { - if (cu.packageName == rootQname) continue; // root entries handled elsewhere - if (sharedDepPackages.contains(cu.packageName)) continue; - if (!is_implementation_source(cu.source)) continue; - if (source_defines_main(cu.source)) depEntryMainSources.insert(cu.source); + // SCOPE: only DEV-dependencies are considered. Test frameworks (gtest, future + // mcpplibs/native frameworks) are dev-deps; regular deps (libarchive, lzma, + // …) must NEVER be touched — a false-positive there would drop a needed + // object (e.g. archive_entry.o) and break normal binaries like xlings. Dev- + // deps are absent from `mcpp build` (includeDevDeps=false) entirely, so plain + // builds are unaffected by construction. + // + // Detected by scanning each dev-dep implementation source for a top-level + // main (gtest_main.cc has one; gtest-all.cc does not). Generic: no per- + // framework knowledge — any framework's main-providing object is handled the + // same way. + std::set devDepPackages; + for (auto const& [depName, spec] : manifest.devDependencies) { + for (auto const& candidate : dependency_name_candidates(depName, spec)) { + auto it = packageIndexByName.find(candidate); + if (it != packageIndexByName.end()) { + devDepPackages.insert(qualified_package_name(packages[it->second].manifest)); + break; + } } } + std::set depEntryMainSources; + for (auto& cu : plan.compileUnits) { + if (!devDepPackages.contains(cu.packageName)) continue; + if (!is_implementation_source(cu.source)) continue; + if (source_defines_main(cu.source)) depEntryMainSources.insert(cu.source); + } std::map> directPackageDeps; for (std::size_t i = 0; i < packages.size(); ++i) { From 2edef206bc4551d3d3e3c7467c3437793365fe72 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 09:12:16 +0800 Subject: [PATCH 08/10] test(e2e): make 78 link-line asserts Windows-aware (.exe target suffix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e2e 78's ninja assertions grepped 'bin/ :' but the Windows link target is 'bin/.exe :', so fw_link came back empty and the assertion false-failed — the functionality (mcpp test 3 combos, xlings build, 25 unit tests) was already green on Windows. Match the target with an optional .exe suffix and guard that the link line was actually found. --- tests/e2e/78_test_main_combinations.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/e2e/78_test_main_combinations.sh b/tests/e2e/78_test_main_combinations.sh index d120e915..549f7fd0 100755 --- a/tests/e2e/78_test_main_combinations.sh +++ b/tests/e2e/78_test_main_combinations.sh @@ -62,10 +62,14 @@ echo "$out" | grep -q '3 passed; 0 failed' || { echo "FAIL: summary mismatch"; e # A test that defines its own main must NOT also link gtest_main.o (that would be # `duplicate symbol: main`); a test relying on the framework's main MUST link it. +# Match the ninja link target with an optional .exe suffix (Windows) and the +# object with either .o / .obj — the gtest_main substring covers both. nj=$(find target -name build.ninja | head -1) -own_link="$(grep 'bin/t_own_main_gtest :' "$nj" || true)" -fw_link="$(grep 'bin/t_framework_main :' "$nj" || true)" -echo "$own_link" | grep -q 'gtest_main' && { echo "FAIL: own-main test links gtest_main.o (dup main)"; exit 1; } -echo "$fw_link" | grep -q 'gtest_main' || { echo "FAIL: framework-main test missing gtest_main.o"; exit 1; } +own_link="$(grep -E 'bin/t_own_main_gtest(\.exe)? :' "$nj" || true)" +fw_link="$(grep -E 'bin/t_framework_main(\.exe)? :' "$nj" || true)" +[[ -n "$own_link" ]] || { echo "FAIL: no link line for t_own_main_gtest"; cat "$nj"; exit 1; } +[[ -n "$fw_link" ]] || { echo "FAIL: no link line for t_framework_main"; cat "$nj"; exit 1; } +echo "$own_link" | grep -q 'gtest_main' && { echo "FAIL: own-main test links gtest_main (dup main)"; exit 1; } +echo "$fw_link" | grep -q 'gtest_main' || { echo "FAIL: framework-main test missing gtest_main"; exit 1; } echo "OK" From f203d2688390b311581abca3472921cd4f27373d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 09:32:48 +0800 Subject: [PATCH 09/10] test(e2e): match 78 link lines by name+cxx_link (path-separator agnostic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows build.ninja uses backslash paths (bin\t_..exe); the previous grep used 'bin/...' so it missed them and false-failed. Match 't_(.exe)? : cxx_link' — no directory prefix, so / vs \ and .exe are both irrelevant. (Windows functionality was already correct: own-main test links no gtest_main, build OK.) --- tests/e2e/78_test_main_combinations.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e/78_test_main_combinations.sh b/tests/e2e/78_test_main_combinations.sh index 549f7fd0..7f6e9dc3 100755 --- a/tests/e2e/78_test_main_combinations.sh +++ b/tests/e2e/78_test_main_combinations.sh @@ -64,9 +64,11 @@ echo "$out" | grep -q '3 passed; 0 failed' || { echo "FAIL: summary mismatch"; e # `duplicate symbol: main`); a test relying on the framework's main MUST link it. # Match the ninja link target with an optional .exe suffix (Windows) and the # object with either .o / .obj — the gtest_main substring covers both. +# Match by target name + `cxx_link` so path-separator (/, on Windows \) and the +# .exe suffix don't matter. The link line is `build bin[.exe] : cxx_link ...`. nj=$(find target -name build.ninja | head -1) -own_link="$(grep -E 'bin/t_own_main_gtest(\.exe)? :' "$nj" || true)" -fw_link="$(grep -E 'bin/t_framework_main(\.exe)? :' "$nj" || true)" +own_link="$(grep -E 't_own_main_gtest(\.exe)? : cxx_link' "$nj" || true)" +fw_link="$(grep -E 't_framework_main(\.exe)? : cxx_link' "$nj" || true)" [[ -n "$own_link" ]] || { echo "FAIL: no link line for t_own_main_gtest"; cat "$nj"; exit 1; } [[ -n "$fw_link" ]] || { echo "FAIL: no link line for t_framework_main"; cat "$nj"; exit 1; } echo "$own_link" | grep -q 'gtest_main' && { echo "FAIL: own-main test links gtest_main (dup main)"; exit 1; } From e2038ebbd2d5430c611096b16eef79ffcbc85815 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 25 Jun 2026 09:51:09 +0800 Subject: [PATCH 10/10] docs: mark P5 all-platform CI green --- .agents/docs/2026-06-25-dependency-archive-linking-design.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.agents/docs/2026-06-25-dependency-archive-linking-design.md b/.agents/docs/2026-06-25-dependency-archive-linking-design.md index 52796ecf..6e7b9af6 100644 --- a/.agents/docs/2026-06-25-dependency-archive-linking-design.md +++ b/.agents/docs/2026-06-25-dependency-archive-linking-design.md @@ -148,8 +148,9 @@ raw-string 字面量再匹配**——否则测试夹具里的 `"int main(){...}" - [x] **P2 后端**:无改动(回退归档发射);`mcpp test` 透出 `diagnosticOutput`(可见性)。 - [x] **P3 单元测试**:`MainDetection`(8 例)+ 全量 25 单测绿。 - [x] **P4 e2e**:`78_test_main_combinations.sh` 三组合全绿 + 链接行断言。 -- [x] **P5 回归(本机)**:25 单测 + e2e 15/16/17/18/31/07/08 全绿;e2e 78 三组合 + - 链接行断言绿。**全平台 CI(尤其 Windows 构建 xlings)→ 待本轮 PR 确认。** +- [x] **P5 回归**:25 单测 + e2e 全套绿;**全平台 CI 全绿**(Linux/Windows/aarch64/ + macOS/e2e)——含 Windows 构建 xlings(libarchive/lzma)、Windows e2e 78 三组合。 + e2e 78 断言历经 `.exe` 后缀 + 反斜杠路径两次跨平台适配。 - [x] **P6 版本 + 文档**:bump 0.0.63→0.0.64;CHANGELOG;本文件。 - [x] **历程**:归档 → MSVC LNK1561/LNK2019(§3.5)→ 回退内联+条件排除。 - [ ] **P7 发布闭环**:PR → CI 全平台 → squash --admin 合入 → tag v0.0.64 →