From 180d516672b538212d2a935856e362705563ad2b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 26 Jun 2026 18:20:50 +0800 Subject: [PATCH] fix: link libatomic --as-needed for clang/llvm atomic users (v0.0.66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 16-byte / oversized std::atomic lowers to __atomic_* libcalls that live in libatomic (a GCC runtime lib — LLVM ships no equivalent), and compiler drivers don't auto-link it. So genuine atomic users failed at link with `undefined __atomic_*`, and LLVM toolchains whose libc++ over-links libatomic (a 0-symbol phantom NEEDED) crashed even hello-world at runtime when the slim package omitted libatomic. mcpp now injects `-Wl,--push-state,--as-needed -latomic -Wl,--pop-state` into the Linux link line (after the -L runtime dirs): used → kept, unused → dropped, so binaries get no spurious libatomic dependency. The injection is self-guarding via atomic_link_flag(): emitted only when a link-resolvable libatomic actually exists on a toolchain link dir (dynamic: libatomic.so/.a; static: libatomic.a) — zero regression for toolchains that ship none, and correct across gcc/llvm/musl-static and macOS/Windows (which never call it). - src/build/flags.cppm: atomic_link_flag() + wire into the Linux ldflags - tests/unit/test_build_flags.cpp: 7 cases (present/absent, soname-only, multi-dir, static-vs-dynamic) - tests/e2e/47_llvm_atomic_link.sh: 16-byte atomic links+runs; non-atomic binary stays libatomic-free (--as-needed) - .agents/docs/2026-06-26-llvm22-libatomic-self-containment-design.md: full root-cause + ecosystem fix plan (① mcpp here; ③ package bundles libatomic) Companion packaging change (③, in xim-pkgindex/xlings-res): ship libatomic in the llvm asset lib//. ② (libc++ phantom NEEDED) is upstream-bound. --- ...lvm22-libatomic-self-containment-design.md | 162 ++++++++++++++++++ CHANGELOG.md | 14 ++ mcpp.toml | 2 +- src/build/flags.cppm | 38 +++- src/toolchain/fingerprint.cppm | 2 +- tests/e2e/47_llvm_atomic_link.sh | 112 ++++++++++++ tests/unit/test_build_flags.cpp | 94 ++++++++++ 7 files changed, 420 insertions(+), 4 deletions(-) create mode 100644 .agents/docs/2026-06-26-llvm22-libatomic-self-containment-design.md create mode 100755 tests/e2e/47_llvm_atomic_link.sh create mode 100644 tests/unit/test_build_flags.cpp diff --git a/.agents/docs/2026-06-26-llvm22-libatomic-self-containment-design.md b/.agents/docs/2026-06-26-llvm22-libatomic-self-containment-design.md new file mode 100644 index 0000000..a216589 --- /dev/null +++ b/.agents/docs/2026-06-26-llvm22-libatomic-self-containment-design.md @@ -0,0 +1,162 @@ +# LLVM libatomic 自包含缺口分析与修复设计(libatomic.so.1 cannot open) + +Date: 2026-06-26 + +## Summary + +升级到 `llvm@22.1.8` 后,`mcpp run` 编译通过但**运行期崩溃**: + +``` +error while loading shared libraries: libatomic.so.1: cannot open shared object file +``` + +切回 `llvm@20.1.7` 一切正常。根因不在 mcpp 的构建/RUNPATH 逻辑,而是 **llvm 22.1.8 的 "slim self-contained" 重打包(xim-pkgindex #317)把 `libatomic.so.1` 从包里删掉了**,而该包的 `libc++.so.1` 仍带着一个对 `libatomic.so.1` 的依赖。配合排查还暴露出第二个**早已存在(20/22 皆有)**的问题:真正使用 16 字节原子的用户程序因链接行缺 `-latomic` 而链接失败。 + +本文记录完整证据、根因、以及落地修复方案。结论: + +- **bug #1(22 回归,全崩)**:任何产物(连 hello world)运行期挂 libatomic。 +- **bug #2(20+22 旧问题,小众)**:真用 `std::atomic<16字节>` 的程序链接报 `undefined __atomic_*_16`。 + +**采纳的修复:① + ③(本仓 + 打包),② 反馈上游 LLVM。** + +- **①(mcpp)**:链接行追加 `-latomic -Wl,--as-needed`,修 bug #2。 +- **③(llvm 打包)**:把 `libatomic.so.1` 带回包内 `lib/x86_64-unknown-linux-gnu/`(对齐 20.1.7),是 bug #1 与 bug #2 的共同基石。 +- **②(上游)**:libc++.so.1 的"幽灵依赖"(`-latomic` 未配 `--as-needed`)反馈给上游 LLVM/libc++ 打包,本仓不做 patchelf 摘除。 + +## 复现与现场 + +项目为 `mcpp new` 模板(`import std; std::println(...)`)。 + +``` +mcpp toolchain default llvm 22.1.8 && mcpp run + Compiling ... Finished ... Running ... + : error while loading shared libraries: libatomic.so.1: cannot open shared object file + +mcpp toolchain default llvm 20 && mcpp run + Hello from llvm-test! # 正常 +``` + +## 根因调查(证据) + +### 1. 产物本身不直接依赖 libatomic —— 缺的是传递依赖 + +20 与 22 的最终产物 `DT_NEEDED` **完全一致**,都没有直接 `libatomic`: + +``` +NEEDED libc++.so.1 libc++abi.so.1 libunwind.so.1 libm.so.6 libc.so.6 +RUNPATH /lib/x86_64-unknown-linux-gnu : /lib : /lib64 +``` + +真正带 `NEEDED libatomic.so.1` 的是 `libc++.so.1`(20、22 都带)。 + +### 2. 差异只在打包:20 自带 libatomic,22 整包没有 + +``` +xim-x-llvm/20.1.7/lib/x86_64-unknown-linux-gnu/libatomic.so.1 → 存在(软链 → .so.1.2.0) +xim-x-llvm/22.1.8/... → find libatomic* 为空 +``` + +libc++.so.1 的 RUNPATH 第一项正是 `/lib/x86_64-unknown-linux-gnu`。20 在该目录里有 libatomic → 解析成功;22 该目录里没有 → 失败。 + +### 3. 为什么不回退系统 libatomic —— 自包含 loader + +产物 PT_INTERP = 沙箱 `xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2`。该 loader 默认搜索/ld.so.cache 不含宿主 `/lib/x86_64-linux-gnu`;且可执行文件的 `DT_RUNPATH` **非透传**(不用于解析 libc++ 的子依赖)。这正是 mcpp/xlings"不依赖 host"的设计 —— 因此宿主即便有 libatomic 也不该、也不会被用上。 + +### 4. RUNPATH 是 mcpp 自己设的(不是上游/xlings) + +`src/toolchain/lifecycle.cppm:376-419` 在 llvm 包 post-install 阶段,用 patchelf 对 `lib/` 下所有 .so `--set-rpath` 为 +`/lib/x86_64-unknown-linux-gnu : /lib : /lib64`(`post_install.cppm` 的 `patchelf_walk`)。其代码注释明确写着这么设就是为了让 +"libatomic.so.1 这种传递依赖被找到 —— 它住在同一个 xpkg 里"。 + +即 **mcpp 显式假设 libatomic 打在包内**,RUNPATH 已正确指向该目录;22.1.8 slim 删掉文件 = 直接违背 mcpp 代码写明的契约。RUNPATH 没错,错在文件缺失。 + +### 5. 反转:libc++ 的 libatomic 依赖是"幽灵依赖"(over-link) + +`libc++.so.1`(20、22 都一样)带 `NEEDED libatomic.so.1`,但对 libatomic 的**未定义符号数 = 0**(libc++abi、libunwind 同为 0): + +``` +readelf --dyn-syms libc++.so.1 | grep UND | grep atomic → 空 +``` + +即构建 libc++ 时挂了 `-latomic` 但**没配 `--as-needed`**,留下一个没人引用的 `NEEDED`。ELF loader 对每个 `DT_NEEDED` 仍做存在性加载(不管用不用),所以文件缺失即启动失败。 + +### libatomic 的归属 + +`libatomic` 不是 LLVM 的,也不是 glibc 的,而是 **GCC 运行时库**(`__atomic_*` 是编译器无关 ABI;GNU 平台由 GCC 独家提供,LLVM/compiler-rt 不发布独立 libatomic)。实测:`xim-x-glibc/2.39` 无 libatomic;`xim-x-gcc/16.1.0/lib64`、`xim-x-gcc-runtime` 有。20.1.7 包内那份就是从 GCC 拷进去的(软链 → `libatomic.so.1.2.0`,与 gcc 同版本)。包内 `libunwind`/`libc++abi` 是 LLVM 自家的,唯 libatomic 借 GCC。 + +## 本地验证矩阵(已实测) + +两个 mcpp 项目钉 `llvm@22.1.8`:`normal`(import std)、`atomic`(`std::atomic`,强制 `__atomic_*_16`): + +| | normal(import std) | atomic(16 字节,真用 libatomic) | +|---|---|---| +| 出厂基线 | ❌ 运行期 `libatomic cannot open` | ❌ **链接**就挂 `undefined __atomic_load_16/store_16/compare_exchange_16` | +| 仅摘 libc++ 幽灵依赖(②) | ✅ 正常 | ❌ 仍链接失败(无 libatomic 可链) | +| 仅带回 libatomic(③) | ✅ 正常 | ❌ 仍链接失败(mcpp 链接行无 `-latomic`,库在场也白搭) | +| 手动 `clang++ -L -latomic` | — | ✅ 链接+运行 `a=1 b=2 ok=1 lockfree=0` | +| 对照:同一 atomic 程序在 llvm 20.1.7(无 -latomic) | — | ❌ **同样**链接失败 → bug #2 非 22 回归 | + +结论: +- bug #1 由 ③(带回库)单独即可修好(20.1.7 即此形态);② 是上游正确做法,可选清洁。 +- bug #2 需要 ①(链接行 `-latomic`)+ ③(库在场)同时满足;`lockfree=0` 证实 16 字节原子按 ABI 走 libatomic 锁实现。 + +## 为什么会有这两个 bug(统一视角) + +二者是"libatomic 链接处理不一致"同一根源的两面,核心是没人一致地用 `-latomic -Wl,--as-needed`: + +| 在哪链接 | 当前做法 | 症状 | +|---|---|---| +| 上游构建 libc++ | 加了 `-latomic`,**未配** `--as-needed` | 没用却留 NEEDED → bug #1(幽灵依赖) | +| mcpp 链接用户程序 | **根本没加** `-latomic` | 真用却没链上 → bug #2(undefined) | + +`--as-needed` 恰好提供"没用就丢、真用就留"的条件行为。把它补在这两处,加上 libatomic 始终在场(③),两个症状都消失。 + +16 字节原子为何必须 out-of-line:x86-64 上 ≤8 字节原子映射单条 `lock` 指令(内联);16 字节需 `cmpxchg16b`(基线不保证、且纯读 load 会写内存破坏 const 语义),故编译器保守发 `__atomic_*_16` libcall,由 libatomic 锁实现 —— 这是 ABI 规范,非 bug。 + +## 修复设计 + +### ①(本仓 mcpp):链接行追加 `-latomic --as-needed` + +- **落点**:`src/build/flags.cppm` 的 LLVM/clang 分支(约 193-195,设 `-stdlib=libc++ -fuse-ld=lld --rtlib=compiler-rt --unwindlib=libunwind` 处),把原子库追加进 ldflags(`f.ld`)。 +- **链接规则**:`ninja_backend.cppm:375` 为 `command = $cxx $in -o $out $ldflags $unit_ldflags`。`$in`(目标文件)在 `$ldflags` 之前,满足 `--as-needed` 的顺序要求(库须出现在引用它的目标文件之后)。 +- **建议片段**(放在 ldflags 末尾): + + ``` + -Wl,--push-state,--as-needed -latomic -Wl,--pop-state + ``` + + `--push-state/--pop-state` 隔离 `--as-needed`,不污染其它库的链接语义。 +- **效果**:用户程序真用原子 → 自动链上并保留 NEEDED;没用 → `--as-needed` 自动丢弃,产物零额外依赖。 +- **前提**:链接器要能找到 libatomic → 依赖 ③ 把库放进 `lib/x86_64-unknown-linux-gnu`(已在 -L/RUNPATH 内,与 libc++ 同目录)。 +- **范围**:本设计聚焦 LLVM 工具链。GCC 工具链同样有 bug #2(其 libatomic 已在 gcc lib 目录 + RUNPATH 内),建议 ① 做成对 gcc/llvm 通用;最低限度先覆盖 LLVM。 + +### ③(llvm 打包):把 libatomic 带回包内 + +- **落点**:llvm 子打包脚本 `.agents/tools/build-llvm-subpkg.sh`(及 `.agents/skills/llvm-subpackaging/`)。 +- **做法**:打包时从 GCC 运行时拷 `libatomic.so.1.2.0` 进 `lib/x86_64-unknown-linux-gnu/`,并建 `libatomic.so.1 → libatomic.so.1.2.0`、`libatomic.so → libatomic.so.1` 软链,对齐 20.1.7 的布局。 +- **效果**:libc++ 的(幽灵)NEEDED 可解析(修 bug #1);为 bug #2 提供可链接/可加载的库;且全程不依赖 host。 +- **代价**:每个 llvm 包多约 187KB 一个小库,换取"任何场景不比 20 倒退"。 +- **重发**:更新 asset → XLINGS_RES → 用户 `mcpp toolchain remove llvm 22.1.8 && reinstall`。 + +### ②(反馈上游,本仓不做):libc++ 幽灵依赖 + +- **现象**:上游(或 xlings 重打包所基于的)libc++.so.1 以 `-latomic` 链接但未配 `--as-needed`,产生 0 引用的 `NEEDED libatomic.so.1`。 +- **上游正确修法**:构建 libc++ 时用 `-latomic -Wl,--as-needed`,或对发布产物 `patchelf --remove-needed libatomic.so.1 libc++.so.1`。 +- **本仓决策**:不在 mcpp post-install 摘除(避免对上游产物做隐式改写),改为向上游 LLVM/打包反馈。在 ③ 已带回 libatomic 的前提下,该幽灵依赖不影响正确性,仅是可加载性上的轻微噪音。 + +### 方案组合判定 + +- 最小可用集 = **① + ③**(等于"20.1.7 形态" + "mcpp 自动补 -latomic"),两个 bug 全好。 +- ②(摘幽灵)为上游清洁项,做了更干净(普通进程根本不加载 libatomic),不做也不影响正确性。 + +## 验收标准 + +- `mcpp run`(import std hello,toolchain=llvm@22.1.8)正常输出,产物 `ldd`/`readelf -d` 无未解析依赖。 +- 16 字节原子程序 `mcpp build` 链接通过、运行正确(`is_lock_free()==false` 属预期),产物 `NEEDED libatomic.so.1` 且可经 RUNPATH 解析。 +- 不使用原子的程序产物**不含** `NEEDED libatomic`(验证 `--as-needed` 生效)。 +- 全程不依赖宿主:在无系统 libatomic 的环境(容器/最小化系统)上述两类程序均可运行。 + +## 关联文档 + +- `2026-05-22-llvm-runpath-bug-analysis.md` / `2026-05-22-fix-llvm-shared-lib-runpath.md`(同一 RUNPATH/post-install 区域) +- `2026-05-13-llvm-clang-toolchain-support-design.md`(LLVM 工具链抽象) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b5fcb5..c4c5dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.66] — 2026-06-26 + +### 修复 + +- **LLVM 工具链产物运行期 `libatomic.so.1: cannot open` / 真用原子时链接报 `undefined __atomic_*`**: + 16 字节及超宽 `std::atomic` 会降级成 `__atomic_*` 外部调用,这些符号位于 **libatomic** + (GCC 运行时库,LLVM 无对应物),而编译器驱动**不会自动链接** libatomic。mcpp 现在在 + Linux 链接行注入 `-Wl,--push-state,--as-needed -latomic -Wl,--pop-state`:真正用到原子的 + 程序自动链上并保留依赖,未用到的程序经 `--as-needed` 自动丢弃、产物零额外依赖。注入是 + **自守卫**的——仅当工具链链接目录里存在可解析的 libatomic(动态链接 `libatomic.so`/`.a`, + 静态链接 `libatomic.a`)时才发出 `-latomic`,因此对不附带 libatomic 的工具链零回归。 + 与之配套的 llvm 资源包需把 libatomic 打入 `lib//`(详见 + `.agents/docs/2026-06-26-llvm22-libatomic-self-containment-design.md`)。 + ## [0.0.65] — 2026-06-25 ### 修复 diff --git a/mcpp.toml b/mcpp.toml index 0a9643b..4c1f21f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.65" +version = "0.0.66" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/flags.cppm b/src/build/flags.cppm index d3d7d5b..79f3d12 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -45,6 +45,21 @@ struct CompileFlags { CompileFlags compute_flags(const BuildPlan& plan); +// Return the linker flag that pulls in libatomic, or "" when it should be +// omitted. libatomic carries the out-of-line __atomic_* libcalls that +// 16-byte / oversized std::atomic lowers to (a GCC runtime lib — LLVM ships +// no equivalent, and compiler drivers don't auto-link it), so a genuine +// atomic user otherwise fails at link with `undefined __atomic_*`. We guard +// it with --as-needed so binaries that don't use it get no dependency. But +// --as-needed does NOT skip a missing library (the linker still has to open +// it), so the flag is emitted ONLY when a link-resolvable libatomic actually +// exists on one of the toolchain's link dirs — otherwise it would break +// toolchains that ship no libatomic at all. `staticLink` (a `-static` build, +// e.g. musl targets) narrows the resolvable form to `libatomic.a`; a dynamic +// link also accepts `libatomic.so`. +std::string atomic_link_flag(const std::vector& linkDirs, + bool staticLink); + } // namespace mcpp::build namespace mcpp::build { @@ -90,6 +105,18 @@ std::string normalize_ldflag(const std::filesystem::path& root, const std::strin } // namespace +std::string atomic_link_flag(const std::vector& linkDirs, + bool staticLink) { + for (auto& dir : linkDirs) { + std::error_code ec; + if (std::filesystem::exists(dir / "libatomic.a", ec) + || (!staticLink && std::filesystem::exists(dir / "libatomic.so", ec))) { + return " -Wl,--push-state,--as-needed -latomic -Wl,--pop-state"; + } + } + return {}; +} + CompileFlags compute_flags(const BuildPlan& plan) { CompileFlags f; @@ -431,8 +458,15 @@ CompileFlags compute_flags(const BuildPlan& plan) { f.ld = std::format("{}{}{}{} -fuse-ld=lld{}{}{}", full_static, static_stdlib, b_flag, macos_sdk, version_min, user_ldflags, link_extra); } else { - f.ld = std::format("{}{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag, - runtime_dirs, payload_ld, user_ldflags, link_extra); + // libatomic: 16-byte / oversized std::atomic needs the out-of-line + // __atomic_* libcalls from libatomic, which the driver won't add on + // its own. Inject `-latomic` (under --as-needed) after runtime_dirs + // so its -L entries are on the search path; self-guards on the lib + // actually being present (see atomic_link_flag). + std::string atomic_ld = atomic_link_flag(plan.toolchain.linkRuntimeDirs, + !full_static.empty()); + f.ld = std::format("{}{}{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag, + runtime_dirs, atomic_ld, payload_ld, user_ldflags, link_extra); } return f; diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 055931b..8a2134f 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.65"; +inline constexpr std::string_view MCPP_VERSION = "0.0.66"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/47_llvm_atomic_link.sh b/tests/e2e/47_llvm_atomic_link.sh new file mode 100755 index 0000000..dac2f8d --- /dev/null +++ b/tests/e2e/47_llvm_atomic_link.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# requires: import-std-libcxx +# 47_llvm_atomic_link.sh — a program using a 16-byte std::atomic lowers to +# __atomic_*_16 libcalls that live in libatomic. Compiler drivers don't +# auto-link libatomic, so without mcpp injecting `-latomic` this fails at link +# with `undefined symbol __atomic_load_16`. Verifies the fix links + runs, and +# that a program NOT using atomics gets no libatomic dependency (--as-needed). +set -e + +OS="$(uname -s)" +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + echo "SKIP: libatomic/-latomic path is Linux-only" + exit 0 +fi +if [[ "$OS" == Darwin* ]]; then + echo "SKIP: libatomic is a GNU/Linux runtime; not applicable on macOS" + exit 0 +fi + +LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" +if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then + echo "SKIP: xlings llvm@20.1.7 is not installed" + exit 0 +fi +# The fix only emits -latomic when a link-resolvable libatomic is present in +# the toolchain (self-guarding). 20.1.7 bundles it; if a slimmed package does +# not, the genuine-atomic case is out of scope here. +if [[ ! -e "$LLVM_ROOT/lib/x86_64-unknown-linux-gnu/libatomic.so" \ + && ! -e "$LLVM_ROOT/lib/x86_64-unknown-linux-gnu/libatomic.a" ]]; then + echo "SKIP: llvm@20.1.7 package ships no link-resolvable libatomic" + exit 0 +fi + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml <<'EOF' +[package] +name = "atomic_link" +version = "0.1.0" + +[toolchain] +linux = "llvm@20.1.7" +EOF + +# 16-byte atomic → __atomic_load_16 / __atomic_compare_exchange_16 libcalls. +cat > src/main.cpp <<'EOF' +#include +#include +struct alignas(16) Big { long long a, b; }; +std::atomic g{{0, 0}}; +int main() { + g.store(Big{1, 2}); + Big r = g.load(); + Big e{1, 2}, d{9, 9}; + bool ok = g.compare_exchange_strong(e, d); + std::printf("atomic %lld %lld %d\n", r.a, r.b, (int)ok); + return 0; +} +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: 16-byte atomic build failed (libatomic not linked?)" + exit 1 +} + +binary=$(find target -type f -path '*/bin/atomic_link' | head -1) +[[ -n "$binary" && -x "$binary" ]] || { + echo "FAIL: atomic_link binary missing" + exit 1 +} + +out=$("$binary") +[[ "$out" == "atomic 1 2 1" ]] || { + echo "FAIL: wrong runtime output: $out" + exit 1 +} + +# The genuine-atomic binary must actually pull libatomic (used → kept). +if command -v readelf >/dev/null 2>&1; then + readelf -d "$binary" 2>/dev/null | grep -q 'libatomic\.so' || { + echo "FAIL: atomic binary missing libatomic NEEDED" + exit 1 + } +fi + +# A program that does NOT use atomics must get no libatomic dependency, +# proving --as-needed drops the spurious link. +cat > src/main.cpp <<'EOF' +#include +int main() { std::printf("noatomic\n"); return 0; } +EOF +"$MCPP" build --no-cache > "$TMP/build2.log" 2>&1 || { + cat "$TMP/build2.log" + echo "FAIL: non-atomic rebuild failed" + exit 1 +} +binary=$(find target -type f -path '*/bin/atomic_link' | head -1) +if command -v readelf >/dev/null 2>&1; then + if readelf -d "$binary" 2>/dev/null | grep -q 'libatomic\.so'; then + echo "FAIL: non-atomic binary should not depend on libatomic (--as-needed)" + exit 1 + fi +fi + +echo "OK" diff --git a/tests/unit/test_build_flags.cpp b/tests/unit/test_build_flags.cpp new file mode 100644 index 0000000..737500a --- /dev/null +++ b/tests/unit/test_build_flags.cpp @@ -0,0 +1,94 @@ +#include + +import std; +import mcpp.build.flags; + +namespace { + +struct Tmp { + std::filesystem::path path; + Tmp() { + path = std::filesystem::temp_directory_path() + / std::format("mcpp_flags_test_{}", std::random_device{}()); + std::filesystem::create_directories(path); + } + ~Tmp() { + std::error_code ec; + std::filesystem::remove_all(path, ec); + } +}; + +void touch(const std::filesystem::path& p) { + std::ofstream(p) << "x"; +} + +// libatomic (a GCC runtime lib; LLVM ships no equivalent) provides the +// out-of-line __atomic_* libcalls that 16-byte/oversized std::atomic lowers +// to. Compiler drivers don't auto-link it, so mcpp must inject `-latomic`. +// But `--as-needed` does NOT skip a missing library — the linker still has to +// open it — so the flag may only be emitted when a libatomic actually exists +// on the toolchain's link dirs, else it would break toolchains that omit it. + +constexpr bool kDynamic = false; // staticLink arg +constexpr bool kStatic = true; + +TEST(BuildFlagsAtomic, EmittedWhenLibatomicSharedPresent) { + Tmp dir; + touch(dir.path / "libatomic.so"); + auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); + EXPECT_NE(flag.find("-latomic"), std::string::npos); + EXPECT_NE(flag.find("--as-needed"), std::string::npos); +} + +TEST(BuildFlagsAtomic, EmittedWhenLibatomicArchivePresent) { + Tmp dir; + touch(dir.path / "libatomic.a"); + auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); + EXPECT_NE(flag.find("-latomic"), std::string::npos); +} + +TEST(BuildFlagsAtomic, EmptyWhenLibatomicAbsent) { + Tmp dir; + touch(dir.path / "libc++.so.1"); // some other lib, but no libatomic + auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); + EXPECT_TRUE(flag.empty()) << "got: " << flag; +} + +// `-latomic` resolves only `libatomic.so` / `libatomic.a` — a bare +// soname-versioned `libatomic.so.1` (no dev symlink, no archive) is NOT +// link-resolvable, so the flag must stay empty or the link breaks with +// "cannot find -latomic". +TEST(BuildFlagsAtomic, EmptyWhenOnlySonameVersionedPresent) { + Tmp dir; + touch(dir.path / "libatomic.so.1"); + touch(dir.path / "libatomic.so.1.2.0"); + auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); + EXPECT_TRUE(flag.empty()) << "got: " << flag; +} + +TEST(BuildFlagsAtomic, ScansAllDirsNotJustFirst) { + Tmp a, b; + touch(b.path / "libatomic.so"); // link-resolvable, in a later dir + auto flag = mcpp::build::atomic_link_flag({a.path, b.path}, kDynamic); + EXPECT_NE(flag.find("-latomic"), std::string::npos); +} + +// Cross-platform / cross-linkage: a full-static link (`-static`, e.g. musl +// targets) resolves `-latomic` only from `libatomic.a`. A lone `libatomic.so` +// is not usable, so the flag must stay empty under static linkage to avoid +// "cannot find -latomic". +TEST(BuildFlagsAtomic, StaticLinkEmptyWhenOnlySharedPresent) { + Tmp dir; + touch(dir.path / "libatomic.so"); + auto flag = mcpp::build::atomic_link_flag({dir.path}, kStatic); + EXPECT_TRUE(flag.empty()) << "got: " << flag; +} + +TEST(BuildFlagsAtomic, StaticLinkEmittedWhenArchivePresent) { + Tmp dir; + touch(dir.path / "libatomic.a"); + auto flag = mcpp::build::atomic_link_flag({dir.path}, kStatic); + EXPECT_NE(flag.find("-latomic"), std::string::npos); +} + +} // namespace