From d8fdbe5f012743c0329e9041dbbf8bc5d93e6aac Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 01:47:28 +0800 Subject: [PATCH 01/19] =?UTF-8?q?feat(0.0.50):=20macOS=20min-version=20sup?= =?UTF-8?q?port=20=E2=80=94=20static=20LLVM=20libc++=20+=20explicit=20depl?= =?UTF-8?q?oyment=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/release.yml | 32 ++++++++++++++++++++ mcpp.toml | 2 +- src/build/flags.cppm | 55 +++++++++++++++++++++++++++++++++- src/cli.cppm | 11 +++++++ src/main.cpp | 14 ++++++++- src/toolchain/fingerprint.cppm | 2 +- 6 files changed, 112 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a398ade1..2b0e4c4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -317,16 +317,48 @@ jobs: echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - name: Build mcpp from source (self-host) + env: + # macOS min-version support: target the first arm64 macOS so the + # release runs on 11.0+ instead of only the runner's OS (15). + # Requires static LLVM libc++ (injected below) — the system + # libc++ on older macOS lacks LLVM-20-era C++23 symbols + # (std::print's __is_posix_terminal etc.; verified on macos-14 + # CI). See xlings .agents/docs/2026-06-05-macos-min-version-support.md. + MACOSX_DEPLOYMENT_TARGET: '11.0' run: | export PATH="$HOME/.xlings/subos/default/bin:$PATH" export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + + # Warm the toolchain so the llvm payload exists, then inject the + # static libc++ archives via [build] ldflags. The bootstrap mcpp + # predates the flags.cppm staticStdlib/clang implementation + # shipped in this very release — from the next release on, the + # injection is redundant (but harmless). "$MCPP" build + LLVM_ROOT=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" -maxdepth 1 -mindepth 1 -type d | head -1) + test -f "$LLVM_ROOT/lib/libc++.a" + test -f "$LLVM_ROOT/lib/libc++abi.a" + if ! grep -q '^ldflags' mcpp.toml; then + sed -i '' "s|^\[build\]|[build]\nldflags = [\"-nostdlib++\", \"$LLVM_ROOT/lib/libc++.a\", \"$LLVM_ROOT/lib/libc++abi.a\"]|" mcpp.toml + fi + grep -n 'ldflags' mcpp.toml + + "$MCPP" 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 11.0) ===" + otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | head -6 + otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos 11.0" \ + || { echo "FAIL: expected minos 11.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 + # Restore the manifest so packaging sees a clean tree. + git checkout -- mcpp.toml echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - name: Package macOS release diff --git a/mcpp.toml b/mcpp.toml index f94556d3..c514a62b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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"] diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 234955cf..5e42ca3c 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -6,6 +6,9 @@ // // See .agents/docs/2026-05-12-compile-commands-design.md. +module; +#include + export module mcpp.build.flags; import std; @@ -120,6 +123,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"); @@ -131,6 +137,22 @@ 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 MACOSX_DEPLOYMENT_TARGET 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) { + if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); + dt && *dt) { + compile_toolchain_flags += + std::string(" -mmacosx-version-min=") + dt; + } + } + llvmRootForStdlib = llvmRoot; // libc++ headers compile_toolchain_flags += " -isystem" + escape_path(libcxxInclude); if (!plan.toolchain.targetTriple.empty()) { @@ -309,7 +331,38 @@ 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 + // 11.0 (first arm64 macOS). 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. + std::string stdlib_link = " -lc++"; + if (f.staticStdlib && !llvmRootForStdlib.empty()) { + auto libcxxA = llvmRootForStdlib / "lib" / "libc++.a"; + auto libcxxAbiA = llvmRootForStdlib / "lib" / "libc++abi.a"; + if (std::filesystem::exists(libcxxA) + && std::filesystem::exists(libcxxAbiA)) { + stdlib_link = " -nostdlib++ " + escape_path(libcxxA) + + " " + escape_path(libcxxAbiA); + } + } + std::string version_min; + if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) { + version_min = std::string(" -mmacosx-version-min=") + dt; + } + f.ld = std::format("{}{}{}{}{}{}{}", full_static, static_stdlib, b_flag, + version_min, stdlib_link, 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); diff --git a/src/cli.cppm b/src/cli.cppm index 346fb8e8..782f0c73 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -587,6 +587,17 @@ 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 it into the fingerprint + // so switching MACOSX_DEPLOYMENT_TARGET rebuilds the BMI cache + // instead of dying with a module config mismatch. + if constexpr (mcpp::platform::is_macos) { + if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) { + s += " macos_deployment_target="; + s += dt; + } + } if (!m.buildConfig.cStandard.empty()) { s += " c_standard="; s += m.buildConfig.cStandard; diff --git a/src/main.cpp b/src/main.cpp index 8dc40daa..b62c9b3f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 234b4877..13eaee5c 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -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; From d03000f434226045a27f6f3d0590c823e84be4e0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 01:48:41 +0800 Subject: [PATCH 02/19] 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. --- .github/workflows/release.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b0e4c4a..e667b9df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -329,17 +329,23 @@ jobs: export PATH="$HOME/.xlings/subos/default/bin:$PATH" export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Warm the toolchain so the llvm payload exists, then inject the - # static libc++ archives via [build] ldflags. The bootstrap mcpp - # predates the flags.cppm staticStdlib/clang implementation - # shipped in this very release — from the next release on, the - # injection is redundant (but harmless). + # Warm the toolchain so the llvm payload exists, then stage the + # static libc++ archives in a dylib-free directory and inject + # `-L -lc++abi` via [build] ldflags: the bootstrap + # mcpp's macOS link line hardcodes ` -lc++` BEFORE user ldflags, + # and the staged -L path makes it resolve to libc++.a (no dylib + # competes there). The bootstrap predates the flags.cppm + # staticStdlib implementation shipped in this very release — + # from the next release on, the injection is redundant. "$MCPP" build LLVM_ROOT=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" -maxdepth 1 -mindepth 1 -type d | head -1) test -f "$LLVM_ROOT/lib/libc++.a" test -f "$LLVM_ROOT/lib/libc++abi.a" + STATICDIR="$RUNNER_TEMP/static-libcxx" + mkdir -p "$STATICDIR" + cp "$LLVM_ROOT/lib/libc++.a" "$LLVM_ROOT/lib/libc++abi.a" "$STATICDIR/" if ! grep -q '^ldflags' mcpp.toml; then - sed -i '' "s|^\[build\]|[build]\nldflags = [\"-nostdlib++\", \"$LLVM_ROOT/lib/libc++.a\", \"$LLVM_ROOT/lib/libc++abi.a\"]|" mcpp.toml + sed -i '' "s|^\[build\]|[build]\nldflags = [\"-L$STATICDIR\", \"-lc++abi\"]|" mcpp.toml fi grep -n 'ldflags' mcpp.toml From af0d07548d6c15e25bb7ead596a6c78117985d7f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 01:58:10 +0800 Subject: [PATCH 03/19] 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. --- .github/workflows/release.yml | 43 +++++++++++++---------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e667b9df..4596fb2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -316,40 +316,31 @@ 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 the first arm64 macOS so the - # release runs on 11.0+ instead of only the runner's OS (15). - # Requires static LLVM libc++ (injected below) — the system - # libc++ on older macOS lacks LLVM-20-era C++23 symbols - # (std::print's __is_posix_terminal etc.; verified on macos-14 - # CI). See xlings .agents/docs/2026-06-05-macos-min-version-support.md. + # release runs on 11.0+ instead of only the runner's OS. 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: '11.0' run: | export PATH="$HOME/.xlings/subos/default/bin:$PATH" export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Warm the toolchain so the llvm payload exists, then stage the - # static libc++ archives in a dylib-free directory and inject - # `-L -lc++abi` via [build] ldflags: the bootstrap - # mcpp's macOS link line hardcodes ` -lc++` BEFORE user ldflags, - # and the staged -L path makes it resolve to libc++.a (no dylib - # competes there). The bootstrap predates the flags.cppm - # staticStdlib implementation shipped in this very release — - # from the next release on, the injection is redundant. + # 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 - LLVM_ROOT=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" -maxdepth 1 -mindepth 1 -type d | head -1) - test -f "$LLVM_ROOT/lib/libc++.a" - test -f "$LLVM_ROOT/lib/libc++abi.a" - STATICDIR="$RUNNER_TEMP/static-libcxx" - mkdir -p "$STATICDIR" - cp "$LLVM_ROOT/lib/libc++.a" "$LLVM_ROOT/lib/libc++abi.a" "$STATICDIR/" - if ! grep -q '^ldflags' mcpp.toml; then - sed -i '' "s|^\[build\]|[build]\nldflags = [\"-L$STATICDIR\", \"-lc++abi\"]|" mcpp.toml - fi - grep -n 'ldflags' mcpp.toml + STAGE1=$(find target -path "*/bin/mcpp" | head -1) + STAGE1=$(cd "$(dirname "$STAGE1")" && pwd)/$(basename "$STAGE1") + "$STAGE1" --version - "$MCPP" build --no-cache + # 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" @@ -363,8 +354,6 @@ jobs: echo "FAIL: still linked against system libc++"; exit 1 fi "$MCPP_BIN" --version - # Restore the manifest so packaging sees a clean tree. - git checkout -- mcpp.toml echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - name: Package macOS release From e5b877a7899556b54362226a1e93c5a5d57d3c61 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 02:19:17 +0800 Subject: [PATCH 04/19] 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. --- src/build/flags.cppm | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 5e42ca3c..6e2f017d 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -347,6 +347,11 @@ CompileFlags compute_flags(const BuildPlan& plan) { // 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. std::string stdlib_link = " -lc++"; if (f.staticStdlib && !llvmRootForStdlib.empty()) { auto libcxxA = llvmRootForStdlib / "lib" / "libc++.a"; @@ -361,8 +366,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) { version_min = std::string(" -mmacosx-version-min=") + dt; } - f.ld = std::format("{}{}{}{}{}{}{}", full_static, static_stdlib, b_flag, - version_min, stdlib_link, user_ldflags, link_extra); + f.ld = std::format("{}{}{} -fuse-ld=lld{}{}{}{}", full_static, static_stdlib, + b_flag, version_min, stdlib_link, 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); From cf975cafcab00fa079c88b3a75646c8244594178 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 02:45:23 +0800 Subject: [PATCH 05/19] 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. --- docs/05-mcpp-toml.md | 8 ++++++++ src/build/flags.cppm | 31 ++++++++++++++++++++----------- src/cli.cppm | 13 ++++++++++--- src/manifest.cppm | 10 ++++++++++ tests/unit/test_manifest.cpp | 28 ++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 14 deletions(-) diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 2d3be8c9..90c022f8 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -91,8 +91,16 @@ cflags = ["-DFOO=1"] # 额外 C 编译参数 cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...) ldflags = ["-lfoo"] # 额外链接参数 static_stdlib = true # 静态链接 libstdc++(默认 true) +macos_deployment_target = "11.0" # macOS 产物的最低支持系统版本(仅 macOS 生效) ``` +`macos_deployment_target` 设定产物 Mach-O 头里的最低系统版本 +(`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态 +惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖, +cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的 +`platforms:`)> 工具链/SDK 默认。该值会进入 BMI 指纹——切换 target 会 +自动重建模块缓存。 + C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: ```toml diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 6e2f017d..1e0ea002 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -89,6 +89,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); @@ -137,20 +149,17 @@ 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 MACOSX_DEPLOYMENT_TARGET explicit - // on the command line so (a) the ninja commands don't depend on - // env propagation and (b) the value participates in the BMI + // 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) { - if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); - dt && *dt) { - compile_toolchain_flags += - std::string(" -mmacosx-version-min=") + dt; - } + if (mcpp::platform::is_macos && !macosDeploymentTarget.empty()) { + compile_toolchain_flags += + " -mmacosx-version-min=" + macosDeploymentTarget; } llvmRootForStdlib = llvmRoot; // libc++ headers @@ -363,8 +372,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { } } std::string version_min; - if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) { - version_min = std::string(" -mmacosx-version-min=") + dt; + if (!macosDeploymentTarget.empty()) { + version_min = " -mmacosx-version-min=" + macosDeploymentTarget; } f.ld = std::format("{}{}{} -fuse-ld=lld{}{}{}{}", full_static, static_stdlib, b_flag, version_min, stdlib_link, user_ldflags, link_extra); diff --git a/src/cli.cppm b/src/cli.cppm index 782f0c73..86ced89e 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -589,13 +589,20 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { 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 it into the fingerprint - // so switching MACOSX_DEPLOYMENT_TARGET rebuilds the BMI cache + // 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. 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 += dt; + s += dtv; } } if (!m.buildConfig.cStandard.empty()) { diff --git a/src/manifest.cppm b/src/manifest.cppm index fdf57969..2d2a99d1 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -104,6 +104,14 @@ struct BuildConfig { std::vector cxxflags; std::vector ldflags; std::string cStandard; + // macOS minimum supported OS version for produced binaries + // (LC_BUILD_VERSION minos), e.g. "11.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.] + built-in defaults). std::string optLevel = "2"; // -O level bool debug = false; // -g @@ -888,6 +896,8 @@ std::expected 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, diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 45416d57..80c43229 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -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] From 1a960886d47d7af5a789c50ae6f9446eb511a6dd Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 02:56:24 +0800 Subject: [PATCH 06/19] fix(macos): static libc++ via ld64 -hidden-l (gtest SIGABRT on exit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/build/flags.cppm | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 1e0ea002..bde42d1d 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -363,12 +363,20 @@ CompileFlags compute_flags(const BuildPlan& plan) { // lld ships with the exact toolchain doing the compile. std::string stdlib_link = " -lc++"; if (f.staticStdlib && !llvmRootForStdlib.empty()) { - auto libcxxA = llvmRootForStdlib / "lib" / "libc++.a"; - auto libcxxAbiA = llvmRootForStdlib / "lib" / "libc++abi.a"; + auto libDir = llvmRootForStdlib / "lib"; + auto libcxxA = libDir / "libc++.a"; + auto libcxxAbiA = libDir / "libc++abi.a"; if (std::filesystem::exists(libcxxA) && std::filesystem::exists(libcxxAbiA)) { - stdlib_link = " -nostdlib++ " + escape_path(libcxxA) - + " " + escape_path(libcxxAbiA); + // -hidden-l: ld64/lld feature made for exactly this — + // links the ARCHIVE (never the sibling dylib) and gives + // its symbols hidden visibility. Without it the static + // libc++/libc++abi symbols clash with the system copies + // that libSystem pulls in indirectly, and processes + // SIGABRT during static destruction (observed: every + // gtest binary exiting 6 on macos CI). + stdlib_link = " -nostdlib++ -L" + escape_path(libDir) + + " -Wl,-hidden-lc++ -Wl,-hidden-lc++abi"; } } std::string version_min; From a900c051d3d3c3ce45faa6efe6f81779cdfe4be6 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 03:42:35 +0800 Subject: [PATCH 07/19] =?UTF-8?q?fix(macos):=20per-unit=20stdlib=20link=20?= =?UTF-8?q?=E2=80=94=20static=20libc++=20for=20distributables,=20system=20?= =?UTF-8?q?libc++=20for=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/build/flags.cppm | 32 ++++++++++++++++++++------------ src/build/ninja_backend.cppm | 13 +++++++++++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index bde42d1d..3eb38884 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -32,6 +32,15 @@ struct CompileFlags { std::string bFlag; // -B (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); @@ -361,30 +370,29 @@ CompileFlags compute_flags(const BuildPlan& plan) { // 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. - std::string stdlib_link = " -lc++"; + f.ldStdlibDefault = " -lc++"; + f.ldStdlibTest = " -lc++"; if (f.staticStdlib && !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)) { - // -hidden-l: ld64/lld feature made for exactly this — - // links the ARCHIVE (never the sibling dylib) and gives - // its symbols hidden visibility. Without it the static - // libc++/libc++abi symbols clash with the system copies - // that libSystem pulls in indirectly, and processes - // SIGABRT during static destruction (observed: every - // gtest binary exiting 6 on macos CI). - stdlib_link = " -nostdlib++ -L" + escape_path(libDir) - + " -Wl,-hidden-lc++ -Wl,-hidden-lc++abi"; + // -hidden-l links the ARCHIVE (never the sibling dylib) + // and gives its symbols hidden visibility so the static + // copy can coexist with the system libc++ that libSystem + // pulls in indirectly. Distributable targets only — see + // the field comments in CompileFlags. + f.ldStdlibDefault = " -nostdlib++ -L" + escape_path(libDir) + + " -Wl,-hidden-lc++ -Wl,-hidden-lc++abi"; } } 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, stdlib_link, user_ldflags, link_extra); + 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); diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 374ea9bf..3db2f9a1 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -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) { From 2055f4b51a7b9d111a08888887ec5eeafffc8a90 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 04:26:40 +0800 Subject: [PATCH 08/19] =?UTF-8?q?test:=20TEMP=20forensics=20=E2=80=94=20st?= =?UTF-8?q?atic=20libc++=20exit-abort=20matrix=20on=20macos=20(remove=20be?= =?UTF-8?q?fore=20merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-macos.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 4aa5e60c..fada64ef 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -327,6 +327,29 @@ jobs: "$MCPP" self config --mirror GLOBAL "$MCPP" test + - name: "TEMP forensics: static libc++ exit-abort matrix (remove before merge)" + if: always() + run: | + set +e + LLVM="$LLVM_ROOT" + SDK=$(xcrun --show-sdk-path) + cat > /tmp/probe.cpp <<'PEOF' + #include + int main() { std::cout << "hi" << std::endl; return 0; } + PEOF + run_mode() { + echo "=== mode: $1 ===" + shift + "$LLVM/bin/clang++" --no-default-config -std=c++17 -nostdinc++ -isystem "$LLVM/include/c++/v1" --sysroot="$SDK" -mmacosx-version-min=11.0 -fuse-ld=lld /tmp/probe.cpp -o /tmp/probe "$@" 2>&1 | head -5 + otool -L /tmp/probe | grep -E "libc|probe" | head -4 + /tmp/probe + echo "exit=$?" + } + run_mode dylib -stdlib=libc++ + run_mode direct -nostdlib++ "$LLVM/lib/libc++.a" "$LLVM/lib/libc++abi.a" + run_mode hidden -nostdlib++ -L"$LLVM/lib" -Wl,-hidden-lc++ -Wl,-hidden-lc++abi + exit 0 + - name: E2E suite # See ci-linux.yml — fail-fast on hung tests instead of burning the # whole job budget. Per-test 600s timeout lives in run_all.sh. From 89df206dcec4a34c9a36b384fd136e3af03b7463 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 04:39:40 +0800 Subject: [PATCH 09/19] fix(macos): direct archive linking + floor 14.0 (forensics-driven) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .github/workflows/ci-macos.yml | 3 +-- .github/workflows/release.yml | 14 ++++++++------ docs/05-mcpp-toml.md | 2 +- src/build/flags.cppm | 15 ++++++++------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index fada64ef..3501fcbf 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -340,14 +340,13 @@ jobs: run_mode() { echo "=== mode: $1 ===" shift - "$LLVM/bin/clang++" --no-default-config -std=c++17 -nostdinc++ -isystem "$LLVM/include/c++/v1" --sysroot="$SDK" -mmacosx-version-min=11.0 -fuse-ld=lld /tmp/probe.cpp -o /tmp/probe "$@" 2>&1 | head -5 + "$LLVM/bin/clang++" --no-default-config -std=c++17 -nostdinc++ -isystem "$LLVM/include/c++/v1" --sysroot="$SDK" -mmacosx-version-min=14.0 -fuse-ld=lld /tmp/probe.cpp -o /tmp/probe "$@" 2>&1 | head -40 otool -L /tmp/probe | grep -E "libc|probe" | head -4 /tmp/probe echo "exit=$?" } run_mode dylib -stdlib=libc++ run_mode direct -nostdlib++ "$LLVM/lib/libc++.a" "$LLVM/lib/libc++abi.a" - run_mode hidden -nostdlib++ -L"$LLVM/lib" -Wl,-hidden-lc++ -Wl,-hidden-lc++abi exit 0 - name: E2E suite diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4596fb2a..4e941034 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -318,13 +318,15 @@ jobs: - name: Build mcpp from source (two-stage self-host) env: - # macOS min-version support: target the first arm64 macOS so the - # release runs on 11.0+ instead of only the runner's OS. Needs + # 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: '11.0' + MACOSX_DEPLOYMENT_TARGET: '14.0' run: | export PATH="$HOME/.xlings/subos/default/bin:$PATH" export MCPP_VENDORED_XLINGS="$XLINGS_BIN" @@ -346,10 +348,10 @@ jobs: test -x "$MCPP_BIN" file "$MCPP_BIN" otool -L "$MCPP_BIN" - echo "=== LC_BUILD_VERSION (must be minos 11.0) ===" + 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 11.0" \ - || { echo "FAIL: expected minos 11.0"; exit 1; } + 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 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 90c022f8..323ea538 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -91,7 +91,7 @@ cflags = ["-DFOO=1"] # 额外 C 编译参数 cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...) ldflags = ["-lfoo"] # 额外链接参数 static_stdlib = true # 静态链接 libstdc++(默认 true) -macos_deployment_target = "11.0" # macOS 产物的最低支持系统版本(仅 macOS 生效) +macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效) ``` `macos_deployment_target` 设定产物 Mach-O 头里的最低系统版本 diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 3eb38884..75dffcc4 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -378,13 +378,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { auto libcxxAbiA = libDir / "libc++abi.a"; if (std::filesystem::exists(libcxxA) && std::filesystem::exists(libcxxAbiA)) { - // -hidden-l links the ARCHIVE (never the sibling dylib) - // and gives its symbols hidden visibility so the static - // copy can coexist with the system libc++ that libSystem - // pulls in indirectly. Distributable targets only — see - // the field comments in CompileFlags. - f.ldStdlibDefault = " -nostdlib++ -L" + escape_path(libDir) - + " -Wl,-hidden-lc++ -Wl,-hidden-lc++abi"; + // 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; From 563c203841db83b332333084e1767935f295a369 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 04:41:04 +0800 Subject: [PATCH 10/19] docs: comment floor references 11.0 -> 14.0 --- src/build/flags.cppm | 3 ++- src/manifest.cppm | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 75dffcc4..91ab8ec7 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -361,7 +361,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { // 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 - // 11.0 (first arm64 macOS). Falls back to -lc++ when the + // 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. diff --git a/src/manifest.cppm b/src/manifest.cppm index 2d2a99d1..a3619454 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -105,7 +105,7 @@ struct BuildConfig { std::vector ldflags; std::string cStandard; // macOS minimum supported OS version for produced binaries - // (LC_BUILD_VERSION minos), e.g. "11.0". Mirrors the ecosystem + // (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 From 3fa6f87c2b299a74bb98637b14cd5ac69ce84e01 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 05:22:08 +0800 Subject: [PATCH 11/19] feat(macos): built-in default deployment floor 14.0 (rustc-style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/05-mcpp-toml.md | 6 ++++-- src/build/flags.cppm | 12 ++++++++++++ src/cli.cppm | 6 ++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 323ea538..05850832 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -98,8 +98,10 @@ macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 (`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态 惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖, cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的 -`platforms:`)> 工具链/SDK 默认。该值会进入 BMI 指纹——切换 target 会 -自动重建模块缓存。 +`platforms:`)> **内建默认 14.0**(rustc 式:target 自带默认 floor 而非 +跟随构建机 SDK;14.0 是工具链静态 libc++ 的下限,`static_stdlib = false` +时回退工具链/SDK 默认)。该值会进入 BMI 指纹——切换 target 会自动重建 +模块缓存。 C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 91ab8ec7..5e706e82 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -109,6 +109,18 @@ CompileFlags compute_flags(const BuildPlan& plan) { } else { macosDeploymentTarget = plan.manifest.buildConfig.macosDeploymentTarget; } + // Built-in default floor (rustc-style: every Apple target carries a + // default deployment target instead of floating with the host SDK). + // When neither the env var nor the manifest pins one, default to the + // floor of the toolchain's static libc++ archives (the official LLVM + // darwin archives are built for macOS 14) — artifacts are portable by + // default and don't silently change floor when the build host's SDK + // moves. Resolved here so the value also reaches the fingerprint via + // the same canonical-flags rule (see cli.cppm). + if (macosDeploymentTarget.empty() && mcpp::platform::is_macos + && plan.manifest.buildConfig.staticStdlib) { + macosDeploymentTarget = "14.0"; + } f.cxxBinary = plan.toolchain.binaryPath; f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain); diff --git a/src/cli.cppm b/src/cli.cppm index 86ced89e..b6ea57c4 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -600,6 +600,12 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { } else { dtv = m.buildConfig.macosDeploymentTarget; } + // Mirror flags.cppm's built-in default floor (14.0 with + // staticStdlib) so the fingerprint matches the flags actually + // emitted. + if (dtv.empty() && m.buildConfig.staticStdlib) { + dtv = "14.0"; + } if (!dtv.empty()) { s += " macos_deployment_target="; s += dtv; From eaa62f3e54475deb0cdaf034da26a73b7c588d6b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 06:07:52 +0800 Subject: [PATCH 12/19] =?UTF-8?q?test:=20TEMP=20=E2=80=94=20verbose=20retr?= =?UTF-8?q?y=20on=20mcpp=20test=20failure=20(remove=20before=20merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-macos.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 3501fcbf..02a0cd6a 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -325,7 +325,11 @@ jobs: MCPP=$(find target -path "*/bin/mcpp" | head -1) MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") "$MCPP" self config --mirror GLOBAL - "$MCPP" test + "$MCPP" test || { + echo "=== TEMP forensics: retry verbose (remove before merge) ===" + "$MCPP" test -v 2>&1 | grep -B3 -A12 -iE "error|undefined|ld64" | head -120 + exit 1 + } - name: "TEMP forensics: static libc++ exit-abort matrix (remove before merge)" if: always() From 01f074d2bdb07dc09cb51150934459a61d7deda5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 06:17:27 +0800 Subject: [PATCH 13/19] =?UTF-8?q?test:=20TEMP=20=E2=80=94=20unfiltered=20v?= =?UTF-8?q?erbose=20tail=20on=20mcpp=20test=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-macos.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 02a0cd6a..b5d57cb8 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -326,8 +326,13 @@ jobs: MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") "$MCPP" self config --mirror GLOBAL "$MCPP" test || { - echo "=== TEMP forensics: retry verbose (remove before merge) ===" - "$MCPP" test -v 2>&1 | grep -B3 -A12 -iE "error|undefined|ld64" | head -120 + echo "=== TEMP forensics: retry verbose unfiltered (remove before merge) ===" + vm_stat | head -5 + "$MCPP" test -v > /tmp/mtv.log 2>&1 + echo "--- verbose tail ---" + tail -200 /tmp/mtv.log + echo "--- ninja FAILED lines ---" + grep -B2 -A20 "FAILED" /tmp/mtv.log | head -80 exit 1 } From 838040c68dcb44f964bea3a8811dead323600a68 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 06:27:57 +0800 Subject: [PATCH 14/19] =?UTF-8?q?test:=20TEMP=20=E2=80=94=20manual=20ninja?= =?UTF-8?q?=20rerun=20forensics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-macos.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index b5d57cb8..4644818a 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -326,13 +326,14 @@ jobs: MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") "$MCPP" self config --mirror GLOBAL "$MCPP" test || { - echo "=== TEMP forensics: retry verbose unfiltered (remove before merge) ===" - vm_stat | head -5 - "$MCPP" test -v > /tmp/mtv.log 2>&1 - echo "--- verbose tail ---" - tail -200 /tmp/mtv.log - echo "--- ninja FAILED lines ---" - grep -B2 -A20 "FAILED" /tmp/mtv.log | head -80 + echo "=== TEMP forensics: manual ninja rerun (remove before merge) ===" + for N in $(find target -name build.ninja); do + D=$(dirname "$N") + echo "--- dir: $D ---" + NINJA=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-ninja" -name ninja -type f | head -1) + "$NINJA" -C "$D" -v 2>&1 | tail -40 + echo "--- ninja exit: $? ---" + done exit 1 } From b2d48d453730b10f6816f08a2673b881404d9504 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 06:37:24 +0800 Subject: [PATCH 15/19] Revert "feat(macos): built-in default deployment floor 14.0 (rustc-style)" This reverts commit 3fa6f87c2b299a74bb98637b14cd5ac69ce84e01. --- docs/05-mcpp-toml.md | 6 ++---- src/build/flags.cppm | 12 ------------ src/cli.cppm | 6 ------ 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 05850832..323ea538 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -98,10 +98,8 @@ macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 (`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态 惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖, cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的 -`platforms:`)> **内建默认 14.0**(rustc 式:target 自带默认 floor 而非 -跟随构建机 SDK;14.0 是工具链静态 libc++ 的下限,`static_stdlib = false` -时回退工具链/SDK 默认)。该值会进入 BMI 指纹——切换 target 会自动重建 -模块缓存。 +`platforms:`)> 工具链/SDK 默认。该值会进入 BMI 指纹——切换 target 会 +自动重建模块缓存。 C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 5e706e82..91ab8ec7 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -109,18 +109,6 @@ CompileFlags compute_flags(const BuildPlan& plan) { } else { macosDeploymentTarget = plan.manifest.buildConfig.macosDeploymentTarget; } - // Built-in default floor (rustc-style: every Apple target carries a - // default deployment target instead of floating with the host SDK). - // When neither the env var nor the manifest pins one, default to the - // floor of the toolchain's static libc++ archives (the official LLVM - // darwin archives are built for macOS 14) — artifacts are portable by - // default and don't silently change floor when the build host's SDK - // moves. Resolved here so the value also reaches the fingerprint via - // the same canonical-flags rule (see cli.cppm). - if (macosDeploymentTarget.empty() && mcpp::platform::is_macos - && plan.manifest.buildConfig.staticStdlib) { - macosDeploymentTarget = "14.0"; - } f.cxxBinary = plan.toolchain.binaryPath; f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain); diff --git a/src/cli.cppm b/src/cli.cppm index b6ea57c4..86ced89e 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -600,12 +600,6 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { } else { dtv = m.buildConfig.macosDeploymentTarget; } - // Mirror flags.cppm's built-in default floor (14.0 with - // staticStdlib) so the fingerprint matches the flags actually - // emitted. - if (dtv.empty() && m.buildConfig.staticStdlib) { - dtv = "14.0"; - } if (!dtv.empty()) { s += " macos_deployment_target="; s += dtv; From 8731c654ade5db14b97b6eba26891758b8c17c35 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 06:38:55 +0800 Subject: [PATCH 16/19] test: remove TEMP forensics; defer built-in default floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .github/workflows/ci-macos.yml | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 4644818a..4aa5e60c 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -325,39 +325,7 @@ jobs: MCPP=$(find target -path "*/bin/mcpp" | head -1) MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") "$MCPP" self config --mirror GLOBAL - "$MCPP" test || { - echo "=== TEMP forensics: manual ninja rerun (remove before merge) ===" - for N in $(find target -name build.ninja); do - D=$(dirname "$N") - echo "--- dir: $D ---" - NINJA=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-ninja" -name ninja -type f | head -1) - "$NINJA" -C "$D" -v 2>&1 | tail -40 - echo "--- ninja exit: $? ---" - done - exit 1 - } - - - name: "TEMP forensics: static libc++ exit-abort matrix (remove before merge)" - if: always() - run: | - set +e - LLVM="$LLVM_ROOT" - SDK=$(xcrun --show-sdk-path) - cat > /tmp/probe.cpp <<'PEOF' - #include - int main() { std::cout << "hi" << std::endl; return 0; } - PEOF - run_mode() { - echo "=== mode: $1 ===" - shift - "$LLVM/bin/clang++" --no-default-config -std=c++17 -nostdinc++ -isystem "$LLVM/include/c++/v1" --sysroot="$SDK" -mmacosx-version-min=14.0 -fuse-ld=lld /tmp/probe.cpp -o /tmp/probe "$@" 2>&1 | head -40 - otool -L /tmp/probe | grep -E "libc|probe" | head -4 - /tmp/probe - echo "exit=$?" - } - run_mode dylib -stdlib=libc++ - run_mode direct -nostdlib++ "$LLVM/lib/libc++.a" "$LLVM/lib/libc++abi.a" - exit 0 + "$MCPP" test - name: E2E suite # See ci-linux.yml — fail-fast on hung tests instead of burning the From 84d11d180c18330cd512e4ffb6726d3bd6890c48 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 07:23:29 +0800 Subject: [PATCH 17/19] fix(macos): gate static libc++ on an explicitly declared deployment floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/build/flags.cppm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 91ab8ec7..dd8a7376 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -373,7 +373,16 @@ CompileFlags compute_flags(const BuildPlan& plan) { // lld ships with the exact toolchain doing the compile. f.ldStdlibDefault = " -lc++"; f.ldStdlibTest = " -lc++"; - if (f.staticStdlib && !llvmRootForStdlib.empty()) { + // 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 — which + // also sidesteps a still-open SIGSEGV in mixed C/C++ static + // binaries (e2e 36; tracked in the design doc). + if (f.staticStdlib && !macosDeploymentTarget.empty() + && !llvmRootForStdlib.empty()) { auto libDir = llvmRootForStdlib / "lib"; auto libcxxA = libDir / "libc++.a"; auto libcxxAbiA = libDir / "libc++abi.a"; From 2d5381ecf399409fab57f466e02b629a482c71b0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 07:24:19 +0800 Subject: [PATCH 18/19] docs: declared-floor-implies-static-runtime semantics --- docs/05-mcpp-toml.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 323ea538..31f9b606 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -101,6 +101,13 @@ cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftP `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 From 032593d4402b1ffecab6b969e26324c6bd1fa1a7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 07:42:37 +0800 Subject: [PATCH 19/19] docs: TODO markers for deferred macos work (default-static flip, floor 11, stdmod boundary) --- src/build/flags.cppm | 16 +++++++++++++--- src/cli.cppm | 8 ++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index dd8a7376..cb5c46d3 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -378,9 +378,19 @@ CompileFlags compute_flags(const BuildPlan& plan) { // 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 — which - // also sidesteps a still-open SIGSEGV in mixed C/C++ static - // binaries (e2e 36; tracked in the design doc). + // 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"; diff --git a/src/cli.cppm b/src/cli.cppm index 86ced89e..b1bc6d57 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -593,6 +593,14 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { // (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) {