feat(0.0.50): macOS min-version support — static LLVM libc++, lld link, explicit deployment target#116
Conversation
…it deployment target Released macosx-arm64 binaries carried LC_BUILD_VERSION minos=15.0 and dynamically linked the SYSTEM /usr/lib/libc++.1.dylib, so they only ran on macOS 15: dyld refuses older minos, and lowering minos alone dies at launch on macOS 14 with a missing libc++ symbol (__ZNSt3__119__is_posix_terminalEP7__sFILE — std::print support added in LLVM-18-era libc++; verified on macos-14 CI). - flags.cppm: implement staticStdlib (the manifest default, previously silently ignored on the clang route) for the macOS link path — link LLVM's own libc++.a/libc++abi.a via -nostdlib++ instead of -lc++, falling back to -lc++ when the archives are absent. Mirror MACOSX_DEPLOYMENT_TARGET onto compile and link command lines so ninja commands don't depend on env propagation. - cli.cppm: fold MACOSX_DEPLOYMENT_TARGET into the BMI fingerprint — the deployment target changes the effective compile triple (arm64-apple-macosxNN), and a std.pcm built for one target cannot be loaded by a TU compiled for another (config-mismatch observed on CI). - main.cpp: __APPLE__ exit guard (_Exit after stream flush) — static libc++'s static destruction can SIGABRT on exit; same guard xlings uses. - release.yml (macos job): MACOSX_DEPLOYMENT_TARGET=11.0 + staged-archive ldflags injection for the self-build (the bootstrap mcpp predates this change), with minos/no-dylib assertions. - version 0.0.50. Design: xlings .agents/docs/2026-06-05-macos-min-version-support.md
Run B2 evidence: -nostdlib++ in user ldflags cannot remove the hardcoded -lc++ that precedes it in the bootstrap mcpp's link line. Stage the archives in a dylib-free dir instead and -L it so -lc++ resolves to libc++.a.
…njection) The bootstrap's hardcoded -lc++ precedes user ldflags, so manifest injection cannot produce the static link (runs B2/B3). Stage 1 (bootstrap-built, dynamic) rebuilds itself as stage 2 with the native staticStdlib implementation from this release.
Xcode's system ld floats with the host: on macos-14 CI, Xcode 15.4's ld aborted at launch (dyld resolved its libc++ against the LLVM payload's libc++.1.0.dylib, missing __ZdaPv). lld ships with the exact LLVM toolchain doing the compile and removes the host-Xcode dependency from the link step.
First-class manifest support for the macOS minimum supported OS version of produced binaries (LC_BUILD_VERSION minos), following ecosystem conventions: the MACOSX_DEPLOYMENT_TARGET env var (which cargo/rustc and the cc crate honor) stays as the explicit per-invocation override, the new [build] macos_deployment_target field provides the project default (SwiftPM platforms: style), and the toolchain/SDK default applies when neither is set. The resolved value feeds the same paths the env var already did: explicit -mmacosx-version-min on compile+link command lines and the BMI fingerprint (switching targets rebuilds the module cache). No effect off macOS. Unit tests + docs/05-mcpp-toml.md.
|
Added on top: Design follows ecosystem conventions: [build]
macos_deployment_target = "11.0" |
Linking the archives by path left their symbols with default visibility; the system libc++/libc++abi that libSystem pulls in indirectly then clashes with the static copy and processes abort during static destruction — every gtest binary exited 6 on macos CI (the mcpp/xlings entry points only survived via their _Exit guards). -hidden-l is the ld64 feature built for static libc++: it links the archive (never the sibling dylib) and gives its symbols hidden visibility, so the two copies can coexist.
…match) (#318) The workflow pins MACOSX_DEPLOYMENT_TARGET=11.0, but the cached ~/.mcpp BMI store contains a std.pcm built for arm64-apple-macosx15 under the same fingerprint — mcpp <= 0.0.49 does not include the deployment target in the BMI fingerprint, so the stale module is reused and every TU fails with a config mismatch. Salt the cache so a coherent (all-11.0) store is rebuilt. mcpp 0.0.50 folds the deployment target into the fingerprint, fixing this class permanently (mcpp-community/mcpp#116).
…system libc++ for tests Statically linked libc++ SIGABRTs during static destruction unless the entry point guards with _Exit (mcpp/xlings do; gtest main does not): with the static link applied globally, every mcpp-test binary on macOS exited 6 (-hidden-l visibility isolation did not change that — the abort is in libc++'s own exit path, not a symbol clash). Split the stdlib choice per link unit via unit_ldflags: distributable targets (Binary/SharedLibrary) keep the static LLVM libc++ (-hidden-l archive form, portable across macOS versions), TestBinary targets link the system -lc++ — they only ever run on the build host. Other platforms unaffected (fields empty).
CI forensics (3-mode matrix with a std::cout probe) overturned the working theory: - -Wl,-hidden-l under lld resolves like a plain -l and picks the SIBLING DYLIB in the same directory: binaries carried @rpath/libc++.1.dylib with no rpath and died at load (dyld 'Library not loaded' -> Abort trap 6). That — not static destruction — was what killed every gtest/e2e binary. Link the archives by path (the form already verified end-to-end on macos-14/15 in PR #115 run B5), keeping the per-unit split (tests use system -lc++). - The official LLVM-20.1.7-macOS-ARM64 static archives are built for macOS 14 (ld64.lld: 'has version 14.0.0, newer than target minimum of 11.0.0'): claiming a 11.0 floor would be false. Deployment target is now 14.0 everywhere — still fully covering the macOS 14 goal; 11-13 would need a custom libc++ build (follow-up).
cargo's key portability win on macOS is that every Apple target carries a built-in default deployment target (aarch64 = 11.0) instead of floating with the host SDK — artifacts run on older systems with zero configuration. Adopt the same: when neither MACOSX_DEPLOYMENT_TARGET nor [build] macos_deployment_target is set and staticStdlib is on, default the floor to 14.0 (the official LLVM static libc++ archives' own floor). Mirrored in the fingerprint rule. Without staticStdlib the toolchain/SDK default still applies (a dynamically-linked binary can't promise a lower floor than the host libc++ anyway).
…yle)" This reverts commit 3fa6f87.
The built-in default deployment floor (rustc-style) trips a std-module prebuild/fingerprint boundary in the dev/test pipeline (the test fingerprint changes but its std.pcm staging does not follow — import std fails wholesale in the test build). Reverted for now: release artifact floors are pinned by MACOSX_DEPLOYMENT_TARGET=14.0 in the release workflow, and the env var + [build] macos_deployment_target manifest field cover all explicit cases. Re-land the default once the stdmod staging honors the same resolution (tracked in the design doc).
…loor Semantics: declaring a minimum macOS (env var or [build] macos_deployment_target) is what opts a build into the static LLVM libc++ — that's the mechanism that makes the declared floor real. With no declared floor the link stays on the dynamic system libc++ exactly as in 0.0.49 (zero behavior change for existing users), which also sidesteps a still-open SIGSEGV in mixed C/C++ static binaries (e2e 36_llvm_toolchain; investigation tracked in the design doc). Release pipelines declare 14.0, so the shipped mcpp/xlings binaries remain static + portable.
…r 11, stdmod boundary)
Deferred work registry (tracked, not in this PR)Marked as
End state when #1+#2 land: static libc++ + built-in 14.0 floor become the unconditional macOS defaults (cargo-style portable-by-default); the declared-floor gate stays only as the opt-out boundary. |
Goal
macosx-arm64 release binaries should run on macOS 11.0+ (first Apple Silicon release) instead of only macOS 15. Verified end-to-end on CI (#115, runs A→B5).
Evidence chain (temp PR #115)
dyld: Symbol not found: __ZNSt3__119__is_posix_terminalEP7__sFILE(std::print support symbol; macOS 14's libc++ predates LLVM 18) — lowering minos alone is not viable[build] ldflagsinjection of static archives-lc++before user ldflagsstd.pcmconfig-mismatch — BMI fingerprint didn't cover the deployment targetldaborting at launch (its libc++ resolution diverted to the LLVM payload dylib, missing__ZdaPv)-fuse-ld=lldmcpp new/build/runincl. llvm/ninja installChanges
staticStdlib(the manifest default, previously silently ignored on the clang route) for the macOS link path:-nostdlib++ <llvm>/lib/libc++.a <llvm>/lib/libc++abi.ainstead of-lc++(falls back when archives absent); link via-fuse-ld=lld(same as the Linux clang path — removes the host-Xcodelddependency); mirrorMACOSX_DEPLOYMENT_TARGETonto compile+link command lines.MACOSX_DEPLOYMENT_TARGETinto the BMI fingerprint (deployment target changes the effective compile triple; stalestd.pcmreuse otherwise hard-fails).__APPLE___Exitguard after stream flush (static libc++ static-dtor abort prevention; same guard xlings uses).MACOSX_DEPLOYMENT_TARGET=11.0+ two-stage self-host (stage 1 bootstrap-built dynamic → stage 2 rebuilds itself static) + minos/no-dylib assertions.Design doc
xlings
.agents/docs/2026-06-05-macos-min-version-support.md(kept updated through the experiments).Compatibility
needs_explicit_libcxxbranch is macOS-only; gcc-static-libstdc++behavior unchanged).mcpp new/build/rune2e. Host-SDK-default minos for user binaries unchanged.