Skip to content

Commit 9c5365b

Browse files
committed
fix(build): macOS test binaries link the toolchain's own libc++ (A1 root fix)
TestBinary previously linked the SYSTEM -lc++ while compiling against the toolchain's libc++ HEADERS — a header/dylib version split that detonated when libc++ 22 moved string hashing out of line (__hash_memory undefined against Apple's older dylib; every gtest link on macOS+llvm-22.1.8 failed). Tests are host-only by definition, so an rpath into the toolchain registry is acceptable for them in a way it isn't for distributables. ldStdlibTest becomes: -nostdlib++ -L<llvm>/lib -lc++ -lc++abi -Wl,-rpath,<llvm>/lib (when the toolchain ships a libc++ dylib; system -lc++ fallback otherwise). Same-version headers and dylib by construction — future libc++ out-of-lining is self-consistent. Dynamic teardown also dissolves the static-destruction SIGABRT that originally motivated the system-lib exception. Distributables keep the static -load_hidden LLVM libc++. ci-macos returns to latest-first llvm install (the 20.1.7 pin's exit criterion): the job now PROVES each new llvm default self-consistent. New e2e 94_macos_test_stdlib (requires: macos): unordered_map<string> hashing under mcpp test + otool -L asserts no /usr/lib/libc++ reference. Design: .agents/docs/2026-07-08-root-cause-remediation-design.md A1.
1 parent 401c13f commit 9c5365b

4 files changed

Lines changed: 80 additions & 9 deletions

File tree

.github/workflows/ci-macos.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ jobs:
5858
5959
- name: Install LLVM via xlings
6060
run: |
61-
# Pin 20.1.7: llvm 22.1.8's libc++ headers reference __hash_memory,
62-
# absent from the libc++ the link resolves on macOS (ld64.lld
63-
# undefined symbol; hermetic-link follow-up tracks the real fix).
64-
xlings install llvm@20.1.7 -y || xlings install llvm -y
61+
# latest-first: test binaries now link the toolchain's own libc++
62+
# (A1 root fix in flags.cppm), so new llvm releases are
63+
# self-consistent — this job is the proof for each new default.
64+
xlings install llvm -y || xlings install llvm@20.1.7 -y
6565
# Verify clang++ is available
6666
LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname)
6767
echo "LLVM_ROOT=$LLVM_ROOT"

src/build/flags.cppm

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ struct CompileFlags {
3535
std::string linkage; // "static" or ""
3636
// macOS per-unit C++ stdlib link (appended via unit_ldflags):
3737
// distributable targets get the static LLVM libc++ (portable across
38-
// macOS versions), TestBinary targets get the system -lc++ — they
39-
// only ever run on the build host, and statically linked libc++
40-
// SIGABRTs during static destruction unless the entry point guards
41-
// with _Exit (mcpp/xlings do; gtest main does not). Empty on other
38+
// macOS versions); TestBinary targets get the toolchain's own libc++
39+
// DYNAMICALLY (-L + -lc++ + rpath into the toolchain) — host-only
40+
// binaries, so the rpath is fine, and it keeps headers and dylib the
41+
// same version (the system -lc++ they used before was a version split
42+
// that broke on libc++ 22's out-of-line __hash_memory). Empty on other
4243
// platforms (stdlib handled by their existing paths).
4344
std::string ldStdlibDefault;
4445
std::string ldStdlibTest;
@@ -385,6 +386,27 @@ CompileFlags compute_flags(const BuildPlan& plan) {
385386
+ " -Wl,-load_hidden," + escape_path(libcxxAbiA);
386387
}
387388
}
389+
// TestBinary: link the toolchain's OWN libc++, dynamically. Tests
390+
// previously took the SYSTEM -lc++ while compiling against the
391+
// toolchain's libc++ HEADERS — a header/dylib version split that
392+
// detonated when libc++ 22 moved string hashing out of line
393+
// (undefined __hash_memory against Apple's older dylib, 2026-07-08).
394+
// Tests are host-only by definition, so an rpath into the toolchain
395+
// registry is fine here in a way it isn't for distributables:
396+
// same-version headers and dylib by construction, and dynamic
397+
// teardown avoids the static-destruction SIGABRT that motivated the
398+
// system-lib exception in the first place (gtest's main has no _Exit
399+
// guard). Falls back to the system -lc++ when the toolchain ships no
400+
// dylib. Design: .agents/docs/2026-07-08-root-cause-remediation-design.md A1.
401+
if (!llvmRootForStdlib.empty()) {
402+
auto libDir = llvmRootForStdlib / "lib";
403+
if (std::filesystem::exists(libDir / "libc++.dylib")
404+
|| std::filesystem::exists(libDir / "libc++.1.dylib")) {
405+
f.ldStdlibTest = " -nostdlib++ -L" + escape_path(libDir)
406+
+ " -lc++ -lc++abi"
407+
+ " -Wl,-rpath," + escape_path(libDir);
408+
}
409+
}
388410
std::string version_min;
389411
if (!macosDeploymentTarget.empty()) {
390412
version_min = " -mmacosx-version-min=" + macosDeploymentTarget;

tests/e2e/94_macos_test_stdlib.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# requires: macos
3+
# 94_macos_test_stdlib.sh — A1 regression: test binaries must link the
4+
# TOOLCHAIN's libc++, not the system one. The old system -lc++ exception
5+
# split headers (toolchain libc++) from the dylib (Apple's older libc++)
6+
# and broke on libc++ 22's out-of-line __hash_memory — exercised here via
7+
# std::unordered_map<std::string,...> string hashing.
8+
# Design: .agents/docs/2026-07-08-root-cause-remediation-design.md A1.
9+
set -e
10+
11+
TMP=$(mktemp -d)
12+
trap "rm -rf $TMP" EXIT
13+
cd "$TMP"
14+
15+
"$MCPP" new hashapp > /dev/null
16+
cd hashapp
17+
18+
mkdir -p tests
19+
cat > tests/hash_test.cpp <<'EOF'
20+
// Forces the string-hash path (__hash_memory-class symbols on libc++ >= 22).
21+
import std;
22+
23+
int main() {
24+
std::unordered_map<std::string, int> m;
25+
for (int i = 0; i < 100; ++i) m["key" + std::to_string(i)] = i;
26+
return (m.size() == 100 && m.at("key42") == 42) ? 0 : 1;
27+
}
28+
EOF
29+
30+
"$MCPP" test > test.log 2>&1 || { cat test.log; echo "FAIL: mcpp test failed"; exit 1; }
31+
grep -q "1 passed" test.log
32+
33+
# The link assertion only applies to clang/llvm toolchains (gcc on macOS is
34+
# not a supported config; if resolution picked something else, the behavior
35+
# test above already covered the regression).
36+
TEST_BIN=$(find target -path "*/bin/hash_test" | head -1)
37+
test -x "$TEST_BIN"
38+
if grep -q "Resolved llvm@" test.log || otool -L "$TEST_BIN" | grep -q "libc++"; then
39+
echo "otool -L:"
40+
otool -L "$TEST_BIN"
41+
if otool -L "$TEST_BIN" | grep -q "/usr/lib/libc++"; then
42+
echo "FAIL: test binary links the SYSTEM libc++ (header/dylib split)"
43+
exit 1
44+
fi
45+
# Either the toolchain's dylib (rpath link) or no libc++ dylib at all
46+
# (static fallback) is acceptable — never Apple's.
47+
fi
48+
49+
echo "PASS 94_macos_test_stdlib"

tests/e2e/run_all.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ case "$OS" in
5858
fi
5959
;;
6060
Darwin)
61-
CAPS+=(unix-shell fresh-sandbox)
61+
CAPS+=(unix-shell fresh-sandbox macos)
6262
# macOS g++ is Apple Clang, not real GCC — don't add gcc capability.
6363
# Tests requiring gcc need actual GNU GCC (modules, gcm.cache, etc.)
6464
;;

0 commit comments

Comments
 (0)