Skip to content

Commit d8fdbe5

Browse files
committed
feat(0.0.50): macOS min-version support — static LLVM libc++ + explicit 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
1 parent 9a301d1 commit d8fdbe5

6 files changed

Lines changed: 112 additions & 4 deletions

File tree

.github/workflows/release.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,16 +317,48 @@ jobs:
317317
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"
318318
319319
- name: Build mcpp from source (self-host)
320+
env:
321+
# macOS min-version support: target the first arm64 macOS so the
322+
# release runs on 11.0+ instead of only the runner's OS (15).
323+
# Requires static LLVM libc++ (injected below) — the system
324+
# libc++ on older macOS lacks LLVM-20-era C++23 symbols
325+
# (std::print's __is_posix_terminal etc.; verified on macos-14
326+
# CI). See xlings .agents/docs/2026-06-05-macos-min-version-support.md.
327+
MACOSX_DEPLOYMENT_TARGET: '11.0'
320328
run: |
321329
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
322330
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
331+
332+
# Warm the toolchain so the llvm payload exists, then inject the
333+
# static libc++ archives via [build] ldflags. The bootstrap mcpp
334+
# predates the flags.cppm staticStdlib/clang implementation
335+
# shipped in this very release — from the next release on, the
336+
# injection is redundant (but harmless).
323337
"$MCPP" build
338+
LLVM_ROOT=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" -maxdepth 1 -mindepth 1 -type d | head -1)
339+
test -f "$LLVM_ROOT/lib/libc++.a"
340+
test -f "$LLVM_ROOT/lib/libc++abi.a"
341+
if ! grep -q '^ldflags' mcpp.toml; then
342+
sed -i '' "s|^\[build\]|[build]\nldflags = [\"-nostdlib++\", \"$LLVM_ROOT/lib/libc++.a\", \"$LLVM_ROOT/lib/libc++abi.a\"]|" mcpp.toml
343+
fi
344+
grep -n 'ldflags' mcpp.toml
345+
346+
"$MCPP" build --no-cache
324347
MCPP_BIN=$(find target -path "*/bin/mcpp" | head -1)
325348
MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN")
326349
test -x "$MCPP_BIN"
327350
file "$MCPP_BIN"
328351
otool -L "$MCPP_BIN"
352+
echo "=== LC_BUILD_VERSION (must be minos 11.0) ==="
353+
otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | head -6
354+
otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos 11.0" \
355+
|| { echo "FAIL: expected minos 11.0"; exit 1; }
356+
if otool -L "$MCPP_BIN" | grep -q "libc++"; then
357+
echo "FAIL: still linked against system libc++"; exit 1
358+
fi
329359
"$MCPP_BIN" --version
360+
# Restore the manifest so packaging sees a clean tree.
361+
git checkout -- mcpp.toml
330362
echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV"
331363
332364
- name: Package macOS release

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.49"
3+
version = "0.0.50"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/flags.cppm

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
//
77
// See .agents/docs/2026-05-12-compile-commands-design.md.
88

9+
module;
10+
#include <cstdlib>
11+
912
export module mcpp.build.flags;
1013

1114
import std;
@@ -120,6 +123,9 @@ CompileFlags compute_flags(const BuildPlan& plan) {
120123
std::string link_toolchain_flags;
121124
bool isClangWithCfg = false;
122125
std::filesystem::path cfgPath;
126+
// LLVM root of a clang-with-cfg toolchain — used by the macOS link
127+
// path below to locate libc++.a/libc++abi.a for staticStdlib.
128+
std::filesystem::path llvmRootForStdlib;
123129
if (mcpp::toolchain::is_clang(plan.toolchain)) {
124130
cfgPath = plan.toolchain.binaryPath.parent_path()
125131
/ (plan.toolchain.binaryPath.stem().string() + ".cfg");
@@ -131,6 +137,22 @@ CompileFlags compute_flags(const BuildPlan& plan) {
131137
auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path();
132138
auto libcxxInclude = llvmRoot / "include" / "c++" / "v1";
133139
compile_toolchain_flags = " --no-default-config -nostdinc++";
140+
// macOS deployment target: make MACOSX_DEPLOYMENT_TARGET explicit
141+
// on the command line so (a) the ninja commands don't depend on
142+
// env propagation and (b) the value participates in the BMI
143+
// fingerprint via canonical flags — mixing targets in one sandbox
144+
// otherwise reuses a std.pcm built for a different
145+
// arm64-apple-macosxNN triple and dies with a config mismatch
146+
// (observed on macos CI). The link side is added to f.ld below
147+
// (the macOS link path doesn't consume link_toolchain_flags).
148+
if (mcpp::platform::is_macos) {
149+
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET");
150+
dt && *dt) {
151+
compile_toolchain_flags +=
152+
std::string(" -mmacosx-version-min=") + dt;
153+
}
154+
}
155+
llvmRootForStdlib = llvmRoot;
134156
// libc++ headers
135157
compile_toolchain_flags += " -isystem" + escape_path(libcxxInclude);
136158
if (!plan.toolchain.targetTriple.empty()) {
@@ -309,7 +331,38 @@ CompileFlags compute_flags(const BuildPlan& plan) {
309331
if constexpr (mcpp::platform::is_windows) {
310332
f.ld = user_ldflags + link_extra;
311333
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
312-
f.ld = std::format("{}{}{} -lc++{}{}", full_static, static_stdlib, b_flag, user_ldflags, link_extra);
334+
// macOS. Two min-version concerns (see xlings
335+
// .agents/docs/2026-06-05-macos-min-version-support.md):
336+
//
337+
// 1. stdlib linkage — `-lc++` resolves to the SYSTEM
338+
// /usr/lib/libc++.1.dylib, which caps the deployment floor at
339+
// the build host's OS: e.g. std::print's __is_posix_terminal
340+
// support symbol only exists in macOS 15's libc++, so a
341+
// minos-14 binary dies at launch on 14 (dyld missing-symbol
342+
// abort; verified on macos-14 CI). With staticStdlib (the
343+
// manifest default — previously silently ignored on the clang
344+
// route), link LLVM's own libc++.a/libc++abi.a instead:
345+
// runtime deps shrink to libSystem and the floor drops to
346+
// 11.0 (first arm64 macOS). Falls back to -lc++ when the
347+
// archives are absent.
348+
// 2. deployment target — mirror MACOSX_DEPLOYMENT_TARGET onto the
349+
// link command line so it doesn't depend on env propagation.
350+
std::string stdlib_link = " -lc++";
351+
if (f.staticStdlib && !llvmRootForStdlib.empty()) {
352+
auto libcxxA = llvmRootForStdlib / "lib" / "libc++.a";
353+
auto libcxxAbiA = llvmRootForStdlib / "lib" / "libc++abi.a";
354+
if (std::filesystem::exists(libcxxA)
355+
&& std::filesystem::exists(libcxxAbiA)) {
356+
stdlib_link = " -nostdlib++ " + escape_path(libcxxA)
357+
+ " " + escape_path(libcxxAbiA);
358+
}
359+
}
360+
std::string version_min;
361+
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) {
362+
version_min = std::string(" -mmacosx-version-min=") + dt;
363+
}
364+
f.ld = std::format("{}{}{}{}{}{}{}", full_static, static_stdlib, b_flag,
365+
version_min, stdlib_link, user_ldflags, link_extra);
313366
} else {
314367
f.ld = std::format("{}{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag,
315368
runtime_dirs, payload_ld, user_ldflags, link_extra);

src/cli.cppm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,17 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
587587
std::string s;
588588
s += "-std="; s += m.package.standard;
589589
s += " -fmodules";
590+
// macOS deployment target changes the effective compile triple
591+
// (arm64-apple-macosxNN) — a std.pcm built for one target cannot be
592+
// loaded by a TU compiled for another. Fold it into the fingerprint
593+
// so switching MACOSX_DEPLOYMENT_TARGET rebuilds the BMI cache
594+
// instead of dying with a module config mismatch.
595+
if constexpr (mcpp::platform::is_macos) {
596+
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) {
597+
s += " macos_deployment_target=";
598+
s += dt;
599+
}
600+
}
590601
if (!m.buildConfig.cStandard.empty()) {
591602
s += " c_standard=";
592603
s += m.buildConfig.cStandard;

src/main.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,17 @@ import std;
55
import mcpp.cli;
66

77
int main(int argc, char* argv[]) {
8-
return mcpp::cli::run(argc, argv);
8+
int rc = mcpp::cli::run(argc, argv);
9+
#ifdef __APPLE__
10+
// With statically linked libc++ (the macOS release linkage since
11+
// 0.0.50), static destruction can SIGABRT on exit — same issue xlings
12+
// guards against. A CLI tool needs no destructor-based cleanup; skip
13+
// static dtors entirely. _Exit bypasses atexit handlers too, so flush
14+
// the standard streams explicitly first.
15+
std::cout.flush();
16+
std::cerr.flush();
17+
std::_Exit(rc);
18+
#else
19+
return rc;
20+
#endif
921
}

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.49";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.50";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;

0 commit comments

Comments
 (0)