diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a398ade1..4e941034 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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" diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 2d3be8c9..31f9b606 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -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 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..cb5c46d3 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; @@ -29,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); @@ -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); @@ -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"); @@ -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()) { @@ -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); 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) { diff --git a/src/cli.cppm b/src/cli.cppm index 346fb8e8..b1bc6d57 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -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; 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/manifest.cppm b/src/manifest.cppm index fdf57969..a3619454 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. "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.] + 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/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; 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]