Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d8fdbe5
feat(0.0.50): macOS min-version support — static LLVM libc++ + explic…
Sunrisepeak Jun 4, 2026
d03000f
ci(release): macos self-build static-libc++ injection v2 (staged -L dir)
Sunrisepeak Jun 4, 2026
af0d075
ci(release): macos self-build via two-stage self-host (drop ldflags i…
Sunrisepeak Jun 4, 2026
e5b877a
feat: macOS links via lld (same as the Linux clang path)
Sunrisepeak Jun 4, 2026
cf975ca
feat: [build] macos_deployment_target manifest field
Sunrisepeak Jun 4, 2026
1a96088
fix(macos): static libc++ via ld64 -hidden-l (gtest SIGABRT on exit)
Sunrisepeak Jun 4, 2026
a900c05
fix(macos): per-unit stdlib link — static libc++ for distributables, …
Sunrisepeak Jun 4, 2026
2055f4b
test: TEMP forensics — static libc++ exit-abort matrix on macos (remo…
Sunrisepeak Jun 4, 2026
89df206
fix(macos): direct archive linking + floor 14.0 (forensics-driven)
Sunrisepeak Jun 4, 2026
563c203
docs: comment floor references 11.0 -> 14.0
Sunrisepeak Jun 4, 2026
3fa6f87
feat(macos): built-in default deployment floor 14.0 (rustc-style)
Sunrisepeak Jun 4, 2026
eaa62f3
test: TEMP — verbose retry on mcpp test failure (remove before merge)
Sunrisepeak Jun 4, 2026
01f074d
test: TEMP — unfiltered verbose tail on mcpp test failure
Sunrisepeak Jun 4, 2026
838040c
test: TEMP — manual ninja rerun forensics
Sunrisepeak Jun 4, 2026
b2d48d4
Revert "feat(macos): built-in default deployment floor 14.0 (rustc-st…
Sunrisepeak Jun 4, 2026
8731c65
test: remove TEMP forensics; defer built-in default floor
Sunrisepeak Jun 4, 2026
84d11d1
fix(macos): gate static libc++ on an explicitly declared deployment f…
Sunrisepeak Jun 4, 2026
2d5381e
docs: declared-floor-implies-static-runtime semantics
Sunrisepeak Jun 4, 2026
032593d
docs: TODO markers for deferred macos work (default-static flip, floo…
Sunrisepeak Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,16 +316,45 @@ jobs:
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"

- name: Build mcpp from source (self-host)
- name: Build mcpp from source (two-stage self-host)
env:
# macOS min-version support: target macOS 14 so the release runs
# on 14.0+ instead of only the runner's OS (the official LLVM
# static libc++ archives are built for macOS 14 — going lower
# needs a custom libc++ build, tracked as follow-up). Needs
# static LLVM libc++ — the system libc++ on older macOS lacks
# LLVM-20-era C++23 symbols (std::print's __is_posix_terminal
# etc.; minos-14 + dynamic libc++ dies at launch on macos-14 CI).
# See xlings .agents/docs/2026-06-05-macos-min-version-support.md.
MACOSX_DEPLOYMENT_TARGET: '14.0'
run: |
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"

# Stage 1: the bootstrap mcpp builds this release's source. The
# bootstrap's macOS link path predates the staticStdlib
# implementation (hardcoded -lc++), so stage 1 links the system
# libc++ — fine, it only needs to RUN on this runner.
"$MCPP" build
STAGE1=$(find target -path "*/bin/mcpp" | head -1)
STAGE1=$(cd "$(dirname "$STAGE1")" && pwd)/$(basename "$STAGE1")
"$STAGE1" --version

# Stage 2: this release's mcpp rebuilds itself — flags.cppm's
# native staticStdlib link produces the static minos-11 binary.
"$STAGE1" build --no-cache
MCPP_BIN=$(find target -path "*/bin/mcpp" | head -1)
MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN")
test -x "$MCPP_BIN"
file "$MCPP_BIN"
otool -L "$MCPP_BIN"
echo "=== LC_BUILD_VERSION (must be minos 14.0) ==="
otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | head -6
otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos 14.0" \
|| { echo "FAIL: expected minos 14.0"; exit 1; }
if otool -L "$MCPP_BIN" | grep -q "libc++"; then
echo "FAIL: still linked against system libc++"; exit 1
fi
"$MCPP_BIN" --version
echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV"

Expand Down
15 changes: 15 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,23 @@ cflags = ["-DFOO=1"] # 额外 C 编译参数
cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...)
ldflags = ["-lfoo"] # 额外链接参数
static_stdlib = true # 静态链接 libstdc++(默认 true)
macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效)
```

`macos_deployment_target` 设定产物 Mach-O 头里的最低系统版本
(`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态
惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖,
cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的
`platforms:`)> 工具链/SDK 默认。该值会进入 BMI 指纹——切换 target 会
自动重建模块缓存。

**声明 floor 即静态运行时**:显式设置了 deployment target(env 或本
字段)且 `static_stdlib = true`(默认)时,macOS 链接会静态链入 LLVM
自带的 libc++/libc++abi —— 系统 libc++ 会把实际可运行版本钉死在构建机
的 OS(老系统缺新符号,如 `std::print` 的支撑符号),静态化才能真正
兑现声明的 floor。注意 LLVM 官方静态库自身的下限是 **14.0**。未声明
floor 时保持动态系统 libc++(产物只保证在构建机同版本及以上运行)。

C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用:

```toml
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.49"
version = "0.0.50"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
106 changes: 105 additions & 1 deletion src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//
// See .agents/docs/2026-05-12-compile-commands-design.md.

module;
#include <cstdlib>

export module mcpp.build.flags;

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

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

// macOS minimum supported OS version for produced binaries.
// Precedence: MACOSX_DEPLOYMENT_TARGET env (explicit per-invocation
// override, the convention cargo/rustc/cc honor) > the manifest's
// [build] macos_deployment_target (project default, SwiftPM-style) >
// empty (toolchain/SDK default).
std::string macosDeploymentTarget;
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) {
macosDeploymentTarget = dt;
} else {
macosDeploymentTarget = plan.manifest.buildConfig.macosDeploymentTarget;
}

f.cxxBinary = plan.toolchain.binaryPath;
f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain);

Expand Down Expand Up @@ -120,6 +144,9 @@ CompileFlags compute_flags(const BuildPlan& plan) {
std::string link_toolchain_flags;
bool isClangWithCfg = false;
std::filesystem::path cfgPath;
// LLVM root of a clang-with-cfg toolchain — used by the macOS link
// path below to locate libc++.a/libc++abi.a for staticStdlib.
std::filesystem::path llvmRootForStdlib;
if (mcpp::toolchain::is_clang(plan.toolchain)) {
cfgPath = plan.toolchain.binaryPath.parent_path()
/ (plan.toolchain.binaryPath.stem().string() + ".cfg");
Expand All @@ -131,6 +158,19 @@ CompileFlags compute_flags(const BuildPlan& plan) {
auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path();
auto libcxxInclude = llvmRoot / "include" / "c++" / "v1";
compile_toolchain_flags = " --no-default-config -nostdinc++";
// macOS deployment target: make the resolved value explicit on
// the command line so (a) the ninja commands don't depend on env
// propagation and (b) the value participates in the BMI
// fingerprint via canonical flags — mixing targets in one sandbox
// otherwise reuses a std.pcm built for a different
// arm64-apple-macosxNN triple and dies with a config mismatch
// (observed on macos CI). The link side is added to f.ld below
// (the macOS link path doesn't consume link_toolchain_flags).
if (mcpp::platform::is_macos && !macosDeploymentTarget.empty()) {
compile_toolchain_flags +=
" -mmacosx-version-min=" + macosDeploymentTarget;
}
llvmRootForStdlib = llvmRoot;
// libc++ headers
compile_toolchain_flags += " -isystem" + escape_path(libcxxInclude);
if (!plan.toolchain.targetTriple.empty()) {
Expand Down Expand Up @@ -309,7 +349,71 @@ CompileFlags compute_flags(const BuildPlan& plan) {
if constexpr (mcpp::platform::is_windows) {
f.ld = user_ldflags + link_extra;
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
f.ld = std::format("{}{}{} -lc++{}{}", full_static, static_stdlib, b_flag, user_ldflags, link_extra);
// macOS. Two min-version concerns (see xlings
// .agents/docs/2026-06-05-macos-min-version-support.md):
//
// 1. stdlib linkage — `-lc++` resolves to the SYSTEM
// /usr/lib/libc++.1.dylib, which caps the deployment floor at
// the build host's OS: e.g. std::print's __is_posix_terminal
// support symbol only exists in macOS 15's libc++, so a
// minos-14 binary dies at launch on 14 (dyld missing-symbol
// abort; verified on macos-14 CI). With staticStdlib (the
// manifest default — previously silently ignored on the clang
// route), link LLVM's own libc++.a/libc++abi.a instead:
// runtime deps shrink to libSystem and the floor drops to
// 14.0 — the floor of the official LLVM static archives;
// lower needs a custom libc++ build. Falls back to -lc++ when the
// archives are absent.
// 2. deployment target — mirror MACOSX_DEPLOYMENT_TARGET onto the
// link command line so it doesn't depend on env propagation.
// 3. linker — use LLVM's own lld (same as the Linux clang path)
// instead of Xcode's ld: the system ld's version floats with
// the host Xcode (observed: Xcode 15.4's ld aborting at launch
// on macos-14 CI when its libc++ resolution was diverted), and
// lld ships with the exact toolchain doing the compile.
f.ldStdlibDefault = " -lc++";
f.ldStdlibTest = " -lc++";
// Static libc++ is tied to an EXPLICIT deployment floor: when the
// user (or the release pipeline) declares a minimum macOS via the
// env var or [build] macos_deployment_target, the static LLVM
// libc++ is what makes that floor real (the system libc++ caps it
// at the build host's OS). With no declared floor, keep the
// 0.0.49 behavior — dynamic system libc++, host-coupled.
//
// TODO(macos-static-default): flip static to the unconditional
// default (rust-style "portable by default") once two tracked
// issues are fixed — (1) mixed C/C++ static binaries SIGSEGV at
// runtime (e2e 36_llvm_toolchain: answer.c + std::cout main.cpp,
// exit 139; root cause not yet isolated), (2) the std-module
// staging/fingerprint boundary (see canonical_compile_flags).
// TODO(macos-floor-11): the official LLVM archives are built for
// macOS 14; supporting 11-13 needs a custom libc++ build shipped
// via xlings-res (data-only change — swap the archive source).
// Both tracked in xlings
// .agents/docs/2026-06-05-macos-min-version-support.md §5.
if (f.staticStdlib && !macosDeploymentTarget.empty()
&& !llvmRootForStdlib.empty()) {
auto libDir = llvmRootForStdlib / "lib";
auto libcxxA = libDir / "libc++.a";
auto libcxxAbiA = libDir / "libc++abi.a";
if (std::filesystem::exists(libcxxA)
&& std::filesystem::exists(libcxxAbiA)) {
// Link the archives BY PATH. (-Wl,-hidden-l looked like
// the canonical choice, but lld resolves it like a plain
// -l and picks the sibling dylib in the same directory —
// the binary then carries @rpath/libc++.1.dylib with no
// rpath and dies at load. Observed on macos CI; path
// form verified end-to-end incl. macos-14.)
f.ldStdlibDefault = " -nostdlib++ " + escape_path(libcxxA)
+ " " + escape_path(libcxxAbiA);
}
}
std::string version_min;
if (!macosDeploymentTarget.empty()) {
version_min = " -mmacosx-version-min=" + macosDeploymentTarget;
}
f.ld = std::format("{}{}{} -fuse-ld=lld{}{}{}", full_static, static_stdlib,
b_flag, 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);
Expand Down
13 changes: 11 additions & 2 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -639,8 +639,17 @@ std::string emit_ninja_string(const BuildPlan& plan) {
implicit.empty() ? std::string{} : " |" + implicit);
if (auto flag = shared_soname_flag(lu); !flag.empty())
out_line += " soname_flag = " + flag + "\n";
if (auto flags = join_flags(lu.linkFlags); !flags.empty())
out_line += " unit_ldflags =" + flags + "\n";
{
// Per-unit C++ stdlib link (macOS; empty elsewhere): test
// binaries run on the build host and use the system -lc++,
// distributable targets get the static LLVM libc++. See
// CompileFlags::ldStdlibDefault/ldStdlibTest.
std::string unit = join_flags(lu.linkFlags);
unit += (lu.kind == mcpp::build::LinkUnit::TestBinary)
? flags.ldStdlibTest : flags.ldStdlibDefault;
if (!unit.empty())
out_line += " unit_ldflags =" + unit + "\n";
}
append(std::move(out_line));

for (auto const& alias : lu.runtimeAliases) {
Expand Down
26 changes: 26 additions & 0 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,32 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
std::string s;
s += "-std="; s += m.package.standard;
s += " -fmodules";
// macOS deployment target changes the effective compile triple
// (arm64-apple-macosxNN) — a std.pcm built for one target cannot be
// loaded by a TU compiled for another. Fold the resolved value
// (env override > [build] macos_deployment_target manifest default)
// into the fingerprint so switching targets rebuilds the BMI cache
// instead of dying with a module config mismatch.
//
// TODO(macos-default-floor): a built-in default floor (rustc-style,
// see the 0.0.50 revert) cannot land until the std-module prebuild /
// staging pipeline consumes the SAME resolved value as this rule and
// flags.cppm — injecting a default here alone left the test build's
// std.pcm unstaged (import std failed wholesale on macos CI).
// Centralize the resolution in one helper, then re-land.
// See xlings .agents/docs/2026-06-05-macos-min-version-support.md §5.
if constexpr (mcpp::platform::is_macos) {
std::string dtv;
if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) {
dtv = dt;
} else {
dtv = m.buildConfig.macosDeploymentTarget;
}
if (!dtv.empty()) {
s += " macos_deployment_target=";
s += dtv;
}
}
if (!m.buildConfig.cStandard.empty()) {
s += " c_standard=";
s += m.buildConfig.cStandard;
Expand Down
14 changes: 13 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,17 @@ import std;
import mcpp.cli;

int main(int argc, char* argv[]) {
return mcpp::cli::run(argc, argv);
int rc = mcpp::cli::run(argc, argv);
#ifdef __APPLE__
// With statically linked libc++ (the macOS release linkage since
// 0.0.50), static destruction can SIGABRT on exit — same issue xlings
// guards against. A CLI tool needs no destructor-based cleanup; skip
// static dtors entirely. _Exit bypasses atexit handlers too, so flush
// the standard streams explicitly first.
std::cout.flush();
std::cerr.flush();
std::_Exit(rc);
#else
return rc;
#endif
}
10 changes: 10 additions & 0 deletions src/manifest.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ struct BuildConfig {
std::vector<std::string> cxxflags;
std::vector<std::string> ldflags;
std::string cStandard;
// macOS minimum supported OS version for produced binaries
// (LC_BUILD_VERSION minos), e.g. "14.0". Mirrors the ecosystem
// conventions around deployment targets (the MACOSX_DEPLOYMENT_TARGET
// env var that cargo/rustc/cc honor; SwiftPM's `platforms:` manifest
// field; CMAKE_OSX_DEPLOYMENT_TARGET). Precedence: the env var (an
// explicit per-invocation override) wins over this manifest default;
// empty + no env = toolchain/SDK default. No effect off macOS.
std::string macosDeploymentTarget;
// Resolved build-profile knobs (from [profile.<name>] + built-in defaults).
std::string optLevel = "2"; // -O level
bool debug = false; // -g
Expand Down Expand Up @@ -888,6 +896,8 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v;
if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v;
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;
if (auto v = doc->get_string("build.macos_deployment_target"))
m.buildConfig.macosDeploymentTarget = *v;
for (auto const& flag : m.buildConfig.cxxflags) {
if (starts_with_std_flag(flag)) {
return std::unexpected(error(origin,
Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "0.0.49";
inline constexpr std::string_view MCPP_VERSION = "0.0.50";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_manifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,34 @@ kind = "lib"
EXPECT_EQ(m->buildConfig.cStandard, "c11");
}

TEST(Manifest, BuildMacosDeploymentTarget) {
constexpr auto src = R"(
[package]
name = "x"
version = "0.1.0"
[build]
macos_deployment_target = "11.0"
[targets.x]
kind = "lib"
)";
auto m = mcpp::manifest::parse_string(src);
ASSERT_TRUE(m.has_value()) << m.error().format();
EXPECT_EQ(m->buildConfig.macosDeploymentTarget, "11.0");
}

TEST(Manifest, BuildMacosDeploymentTargetDefaultsEmpty) {
constexpr auto src = R"(
[package]
name = "x"
version = "0.1.0"
[targets.x]
kind = "lib"
)";
auto m = mcpp::manifest::parse_string(src);
ASSERT_TRUE(m.has_value()) << m.error().format();
EXPECT_TRUE(m->buildConfig.macosDeploymentTarget.empty());
}

TEST(Manifest, RuntimeConfig) {
constexpr auto src = R"(
[package]
Expand Down
Loading