Skip to content

Commit 2709fe6

Browse files
authored
feat(0.0.50): macOS min-version support — static LLVM libc++, lld link, explicit deployment target (#116)
* 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 * ci(release): macos self-build static-libc++ injection v2 (staged -L dir) 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. * ci(release): macos self-build via two-stage self-host (drop ldflags injection) 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. * feat: macOS links via lld (same as the Linux clang path) 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. * feat: [build] macos_deployment_target manifest field 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. * fix(macos): static libc++ via ld64 -hidden-l (gtest SIGABRT on exit) 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. * fix(macos): per-unit stdlib link — static libc++ for distributables, 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). * test: TEMP forensics — static libc++ exit-abort matrix on macos (remove before merge) * fix(macos): direct archive linking + floor 14.0 (forensics-driven) 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). * docs: comment floor references 11.0 -> 14.0 * feat(macos): built-in default deployment floor 14.0 (rustc-style) 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). * test: TEMP — verbose retry on mcpp test failure (remove before merge) * test: TEMP — unfiltered verbose tail on mcpp test failure * test: TEMP — manual ninja rerun forensics * Revert "feat(macos): built-in default deployment floor 14.0 (rustc-style)" This reverts commit 3fa6f87. * test: remove TEMP forensics; defer built-in default floor 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). * fix(macos): gate static libc++ on an explicitly declared deployment floor 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. * docs: declared-floor-implies-static-runtime semantics * docs: TODO markers for deferred macos work (default-static flip, floor 11, stdmod boundary)
1 parent 9a301d1 commit 2709fe6

10 files changed

Lines changed: 240 additions & 7 deletions

File tree

.github/workflows/release.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,16 +316,45 @@ jobs:
316316
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
317317
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"
318318
319-
- name: Build mcpp from source (self-host)
319+
- name: Build mcpp from source (two-stage self-host)
320+
env:
321+
# macOS min-version support: target macOS 14 so the release runs
322+
# on 14.0+ instead of only the runner's OS (the official LLVM
323+
# static libc++ archives are built for macOS 14 — going lower
324+
# needs a custom libc++ build, tracked as follow-up). Needs
325+
# static LLVM libc++ — the system libc++ on older macOS lacks
326+
# LLVM-20-era C++23 symbols (std::print's __is_posix_terminal
327+
# etc.; minos-14 + dynamic libc++ dies at launch on macos-14 CI).
328+
# See xlings .agents/docs/2026-06-05-macos-min-version-support.md.
329+
MACOSX_DEPLOYMENT_TARGET: '14.0'
320330
run: |
321331
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
322332
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
333+
334+
# Stage 1: the bootstrap mcpp builds this release's source. The
335+
# bootstrap's macOS link path predates the staticStdlib
336+
# implementation (hardcoded -lc++), so stage 1 links the system
337+
# libc++ — fine, it only needs to RUN on this runner.
323338
"$MCPP" build
339+
STAGE1=$(find target -path "*/bin/mcpp" | head -1)
340+
STAGE1=$(cd "$(dirname "$STAGE1")" && pwd)/$(basename "$STAGE1")
341+
"$STAGE1" --version
342+
343+
# Stage 2: this release's mcpp rebuilds itself — flags.cppm's
344+
# native staticStdlib link produces the static minos-11 binary.
345+
"$STAGE1" build --no-cache
324346
MCPP_BIN=$(find target -path "*/bin/mcpp" | head -1)
325347
MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN")
326348
test -x "$MCPP_BIN"
327349
file "$MCPP_BIN"
328350
otool -L "$MCPP_BIN"
351+
echo "=== LC_BUILD_VERSION (must be minos 14.0) ==="
352+
otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | head -6
353+
otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos 14.0" \
354+
|| { echo "FAIL: expected minos 14.0"; exit 1; }
355+
if otool -L "$MCPP_BIN" | grep -q "libc++"; then
356+
echo "FAIL: still linked against system libc++"; exit 1
357+
fi
329358
"$MCPP_BIN" --version
330359
echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV"
331360

docs/05-mcpp-toml.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,23 @@ cflags = ["-DFOO=1"] # 额外 C 编译参数
9191
cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...)
9292
ldflags = ["-lfoo"] # 额外链接参数
9393
static_stdlib = true # 静态链接 libstdc++(默认 true)
94+
macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效)
9495
```
9596

97+
`macos_deployment_target` 设定产物 Mach-O 头里的最低系统版本
98+
(`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态
99+
惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖,
100+
cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的
101+
`platforms:`)> 工具链/SDK 默认。该值会进入 BMI 指纹——切换 target 会
102+
自动重建模块缓存。
103+
104+
**声明 floor 即静态运行时**:显式设置了 deployment target(env 或本
105+
字段)且 `static_stdlib = true`(默认)时,macOS 链接会静态链入 LLVM
106+
自带的 libc++/libc++abi —— 系统 libc++ 会把实际可运行版本钉死在构建机
107+
的 OS(老系统缺新符号,如 `std::print` 的支撑符号),静态化才能真正
108+
兑现声明的 floor。注意 LLVM 官方静态库自身的下限是 **14.0**。未声明
109+
floor 时保持动态系统 libc++(产物只保证在构建机同版本及以上运行)。
110+
96111
C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用:
97112

98113
```toml

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: 105 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;
@@ -29,6 +32,15 @@ struct CompileFlags {
2932
std::string bFlag; // -B<binutils> (for ninja ldflags)
3033
bool staticStdlib = true;
3134
std::string linkage; // "static" or ""
35+
// macOS per-unit C++ stdlib link (appended via unit_ldflags):
36+
// distributable targets get the static LLVM libc++ (portable across
37+
// macOS versions), TestBinary targets get the system -lc++ — they
38+
// only ever run on the build host, and statically linked libc++
39+
// SIGABRTs during static destruction unless the entry point guards
40+
// with _Exit (mcpp/xlings do; gtest main does not). Empty on other
41+
// platforms (stdlib handled by their existing paths).
42+
std::string ldStdlibDefault;
43+
std::string ldStdlibTest;
3244
};
3345

3446
CompileFlags compute_flags(const BuildPlan& plan);
@@ -86,6 +98,18 @@ CompileFlags compute_flags(const BuildPlan& plan) {
8698
// any new branching added to this function.
8799
auto caps = mcpp::toolchain::capabilities_for(plan.toolchain);
88100

101+
// macOS minimum supported OS version for produced binaries.
102+
// Precedence: MACOSX_DEPLOYMENT_TARGET env (explicit per-invocation
103+
// override, the convention cargo/rustc/cc honor) > the manifest's
104+
// [build] macos_deployment_target (project default, SwiftPM-style) >
105+
// empty (toolchain/SDK default).
106+
std::string macosDeploymentTarget;
107+
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) {
108+
macosDeploymentTarget = dt;
109+
} else {
110+
macosDeploymentTarget = plan.manifest.buildConfig.macosDeploymentTarget;
111+
}
112+
89113
f.cxxBinary = plan.toolchain.binaryPath;
90114
f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain);
91115

@@ -120,6 +144,9 @@ CompileFlags compute_flags(const BuildPlan& plan) {
120144
std::string link_toolchain_flags;
121145
bool isClangWithCfg = false;
122146
std::filesystem::path cfgPath;
147+
// LLVM root of a clang-with-cfg toolchain — used by the macOS link
148+
// path below to locate libc++.a/libc++abi.a for staticStdlib.
149+
std::filesystem::path llvmRootForStdlib;
123150
if (mcpp::toolchain::is_clang(plan.toolchain)) {
124151
cfgPath = plan.toolchain.binaryPath.parent_path()
125152
/ (plan.toolchain.binaryPath.stem().string() + ".cfg");
@@ -131,6 +158,19 @@ CompileFlags compute_flags(const BuildPlan& plan) {
131158
auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path();
132159
auto libcxxInclude = llvmRoot / "include" / "c++" / "v1";
133160
compile_toolchain_flags = " --no-default-config -nostdinc++";
161+
// macOS deployment target: make the resolved value explicit on
162+
// the command line so (a) the ninja commands don't depend on env
163+
// propagation and (b) the value participates in the BMI
164+
// fingerprint via canonical flags — mixing targets in one sandbox
165+
// otherwise reuses a std.pcm built for a different
166+
// arm64-apple-macosxNN triple and dies with a config mismatch
167+
// (observed on macos CI). The link side is added to f.ld below
168+
// (the macOS link path doesn't consume link_toolchain_flags).
169+
if (mcpp::platform::is_macos && !macosDeploymentTarget.empty()) {
170+
compile_toolchain_flags +=
171+
" -mmacosx-version-min=" + macosDeploymentTarget;
172+
}
173+
llvmRootForStdlib = llvmRoot;
134174
// libc++ headers
135175
compile_toolchain_flags += " -isystem" + escape_path(libcxxInclude);
136176
if (!plan.toolchain.targetTriple.empty()) {
@@ -309,7 +349,71 @@ CompileFlags compute_flags(const BuildPlan& plan) {
309349
if constexpr (mcpp::platform::is_windows) {
310350
f.ld = user_ldflags + link_extra;
311351
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
312-
f.ld = std::format("{}{}{} -lc++{}{}", full_static, static_stdlib, b_flag, user_ldflags, link_extra);
352+
// macOS. Two min-version concerns (see xlings
353+
// .agents/docs/2026-06-05-macos-min-version-support.md):
354+
//
355+
// 1. stdlib linkage — `-lc++` resolves to the SYSTEM
356+
// /usr/lib/libc++.1.dylib, which caps the deployment floor at
357+
// the build host's OS: e.g. std::print's __is_posix_terminal
358+
// support symbol only exists in macOS 15's libc++, so a
359+
// minos-14 binary dies at launch on 14 (dyld missing-symbol
360+
// abort; verified on macos-14 CI). With staticStdlib (the
361+
// manifest default — previously silently ignored on the clang
362+
// route), link LLVM's own libc++.a/libc++abi.a instead:
363+
// runtime deps shrink to libSystem and the floor drops to
364+
// 14.0 — the floor of the official LLVM static archives;
365+
// lower needs a custom libc++ build. Falls back to -lc++ when the
366+
// archives are absent.
367+
// 2. deployment target — mirror MACOSX_DEPLOYMENT_TARGET onto the
368+
// link command line so it doesn't depend on env propagation.
369+
// 3. linker — use LLVM's own lld (same as the Linux clang path)
370+
// instead of Xcode's ld: the system ld's version floats with
371+
// the host Xcode (observed: Xcode 15.4's ld aborting at launch
372+
// on macos-14 CI when its libc++ resolution was diverted), and
373+
// lld ships with the exact toolchain doing the compile.
374+
f.ldStdlibDefault = " -lc++";
375+
f.ldStdlibTest = " -lc++";
376+
// Static libc++ is tied to an EXPLICIT deployment floor: when the
377+
// user (or the release pipeline) declares a minimum macOS via the
378+
// env var or [build] macos_deployment_target, the static LLVM
379+
// libc++ is what makes that floor real (the system libc++ caps it
380+
// at the build host's OS). With no declared floor, keep the
381+
// 0.0.49 behavior — dynamic system libc++, host-coupled.
382+
//
383+
// TODO(macos-static-default): flip static to the unconditional
384+
// default (rust-style "portable by default") once two tracked
385+
// issues are fixed — (1) mixed C/C++ static binaries SIGSEGV at
386+
// runtime (e2e 36_llvm_toolchain: answer.c + std::cout main.cpp,
387+
// exit 139; root cause not yet isolated), (2) the std-module
388+
// staging/fingerprint boundary (see canonical_compile_flags).
389+
// TODO(macos-floor-11): the official LLVM archives are built for
390+
// macOS 14; supporting 11-13 needs a custom libc++ build shipped
391+
// via xlings-res (data-only change — swap the archive source).
392+
// Both tracked in xlings
393+
// .agents/docs/2026-06-05-macos-min-version-support.md §5.
394+
if (f.staticStdlib && !macosDeploymentTarget.empty()
395+
&& !llvmRootForStdlib.empty()) {
396+
auto libDir = llvmRootForStdlib / "lib";
397+
auto libcxxA = libDir / "libc++.a";
398+
auto libcxxAbiA = libDir / "libc++abi.a";
399+
if (std::filesystem::exists(libcxxA)
400+
&& std::filesystem::exists(libcxxAbiA)) {
401+
// Link the archives BY PATH. (-Wl,-hidden-l looked like
402+
// the canonical choice, but lld resolves it like a plain
403+
// -l and picks the sibling dylib in the same directory —
404+
// the binary then carries @rpath/libc++.1.dylib with no
405+
// rpath and dies at load. Observed on macos CI; path
406+
// form verified end-to-end incl. macos-14.)
407+
f.ldStdlibDefault = " -nostdlib++ " + escape_path(libcxxA)
408+
+ " " + escape_path(libcxxAbiA);
409+
}
410+
}
411+
std::string version_min;
412+
if (!macosDeploymentTarget.empty()) {
413+
version_min = " -mmacosx-version-min=" + macosDeploymentTarget;
414+
}
415+
f.ld = std::format("{}{}{} -fuse-ld=lld{}{}{}", full_static, static_stdlib,
416+
b_flag, version_min, user_ldflags, link_extra);
313417
} else {
314418
f.ld = std::format("{}{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag,
315419
runtime_dirs, payload_ld, user_ldflags, link_extra);

src/build/ninja_backend.cppm

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,8 +639,17 @@ std::string emit_ninja_string(const BuildPlan& plan) {
639639
implicit.empty() ? std::string{} : " |" + implicit);
640640
if (auto flag = shared_soname_flag(lu); !flag.empty())
641641
out_line += " soname_flag = " + flag + "\n";
642-
if (auto flags = join_flags(lu.linkFlags); !flags.empty())
643-
out_line += " unit_ldflags =" + flags + "\n";
642+
{
643+
// Per-unit C++ stdlib link (macOS; empty elsewhere): test
644+
// binaries run on the build host and use the system -lc++,
645+
// distributable targets get the static LLVM libc++. See
646+
// CompileFlags::ldStdlibDefault/ldStdlibTest.
647+
std::string unit = join_flags(lu.linkFlags);
648+
unit += (lu.kind == mcpp::build::LinkUnit::TestBinary)
649+
? flags.ldStdlibTest : flags.ldStdlibDefault;
650+
if (!unit.empty())
651+
out_line += " unit_ldflags =" + unit + "\n";
652+
}
644653
append(std::move(out_line));
645654

646655
for (auto const& alias : lu.runtimeAliases) {

src/cli.cppm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,32 @@ 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 the resolved value
593+
// (env override > [build] macos_deployment_target manifest default)
594+
// into the fingerprint so switching targets rebuilds the BMI cache
595+
// instead of dying with a module config mismatch.
596+
//
597+
// TODO(macos-default-floor): a built-in default floor (rustc-style,
598+
// see the 0.0.50 revert) cannot land until the std-module prebuild /
599+
// staging pipeline consumes the SAME resolved value as this rule and
600+
// flags.cppm — injecting a default here alone left the test build's
601+
// std.pcm unstaged (import std failed wholesale on macos CI).
602+
// Centralize the resolution in one helper, then re-land.
603+
// See xlings .agents/docs/2026-06-05-macos-min-version-support.md §5.
604+
if constexpr (mcpp::platform::is_macos) {
605+
std::string dtv;
606+
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) {
607+
dtv = dt;
608+
} else {
609+
dtv = m.buildConfig.macosDeploymentTarget;
610+
}
611+
if (!dtv.empty()) {
612+
s += " macos_deployment_target=";
613+
s += dtv;
614+
}
615+
}
590616
if (!m.buildConfig.cStandard.empty()) {
591617
s += " c_standard=";
592618
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/manifest.cppm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ struct BuildConfig {
104104
std::vector<std::string> cxxflags;
105105
std::vector<std::string> ldflags;
106106
std::string cStandard;
107+
// macOS minimum supported OS version for produced binaries
108+
// (LC_BUILD_VERSION minos), e.g. "14.0". Mirrors the ecosystem
109+
// conventions around deployment targets (the MACOSX_DEPLOYMENT_TARGET
110+
// env var that cargo/rustc/cc honor; SwiftPM's `platforms:` manifest
111+
// field; CMAKE_OSX_DEPLOYMENT_TARGET). Precedence: the env var (an
112+
// explicit per-invocation override) wins over this manifest default;
113+
// empty + no env = toolchain/SDK default. No effect off macOS.
114+
std::string macosDeploymentTarget;
107115
// Resolved build-profile knobs (from [profile.<name>] + built-in defaults).
108116
std::string optLevel = "2"; // -O level
109117
bool debug = false; // -g
@@ -888,6 +896,8 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
888896
if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v;
889897
if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v;
890898
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;
899+
if (auto v = doc->get_string("build.macos_deployment_target"))
900+
m.buildConfig.macosDeploymentTarget = *v;
891901
for (auto const& flag : m.buildConfig.cxxflags) {
892902
if (starts_with_std_flag(flag)) {
893903
return std::unexpected(error(origin,

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;

tests/unit/test_manifest.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,34 @@ kind = "lib"
295295
EXPECT_EQ(m->buildConfig.cStandard, "c11");
296296
}
297297

298+
TEST(Manifest, BuildMacosDeploymentTarget) {
299+
constexpr auto src = R"(
300+
[package]
301+
name = "x"
302+
version = "0.1.0"
303+
[build]
304+
macos_deployment_target = "11.0"
305+
[targets.x]
306+
kind = "lib"
307+
)";
308+
auto m = mcpp::manifest::parse_string(src);
309+
ASSERT_TRUE(m.has_value()) << m.error().format();
310+
EXPECT_EQ(m->buildConfig.macosDeploymentTarget, "11.0");
311+
}
312+
313+
TEST(Manifest, BuildMacosDeploymentTargetDefaultsEmpty) {
314+
constexpr auto src = R"(
315+
[package]
316+
name = "x"
317+
version = "0.1.0"
318+
[targets.x]
319+
kind = "lib"
320+
)";
321+
auto m = mcpp::manifest::parse_string(src);
322+
ASSERT_TRUE(m.has_value()) << m.error().format();
323+
EXPECT_TRUE(m->buildConfig.macosDeploymentTarget.empty());
324+
}
325+
298326
TEST(Manifest, RuntimeConfig) {
299327
constexpr auto src = R"(
300328
[package]

0 commit comments

Comments
 (0)