Skip to content

[temp][DO NOT MERGE] macOS 14 support experiment CI#115

Closed
Sunrisepeak wants to merge 12 commits into
mainfrom
test/macos14-support
Closed

[temp][DO NOT MERGE] macOS 14 support experiment CI#115
Sunrisepeak wants to merge 12 commits into
mainfrom
test/macos14-support

Conversation

@Sunrisepeak
Copy link
Copy Markdown
Member

目的(临时 PR,勿合并)

调研并验证 mcpp + xlings 对 macOS 14 的支持路径。

现状(调研结论)

  • 已发布的 macosx-arm64 产物(mcpp 0.0.49 / xlings 0.4.49)LC_BUILD_VERSION minos=15.0 sdk=15.0 —— macOS 14 的 dyld 直接拒载。根因:macos-15 runner 构建时未设 deployment target,取了 SDK 默认。
  • 两个二进制都动态链接系统 /usr/lib/libc++.1.dylib —— 降 target 需要通过 libc++ availability 门控(编译期)且无运行期缺符号(本实验验证点)。
  • mcpp 构建链(flags.cppm)无任何 deployment target 处理 → clang driver 尊重 MACOSX_DEPLOYMENT_TARGET 环境变量,机制上环境变量即可生效。

实验设计

job runner 内容
build-minos14 macos-15 bootstrap mcpp → MACOSX_DEPLOYMENT_TARGET=14.0 构建 mcpp@HEAD 与 xlings@main → 断言 minos=14.0 → 15 上冒烟
smoke-macos14 macos-14 对照:0.0.49 产物应被 dyld 拒载;minos14 产物 --version 启动;组装 xlings home 后 mcpp new/build/run 全链路(在 14 上安装 llvm/ninja 工具链包)

验证通过后将把 deployment target 固化进两 repo 的 release/CI 并发布新版本。

Removes all other workflows on this branch and adds a two-job
experiment: build mcpp@HEAD + xlings@main on macos-15 with
MACOSX_DEPLOYMENT_TARGET=14.0 (assert LC_BUILD_VERSION minos=14.0,
probe libc++ availability gating at compile time), then run the
artifacts end-to-end on a macos-14 runner (control: released minos15
binaries must be refused by dyld).
…jection

Run A evidence: MACOSX_DEPLOYMENT_TARGET=14.0 with the status-quo
dynamic system-libc++ linkage compiles clean (availability gating does
not reject) and asserts minos=14.0, but dies at launch on macOS 14.8.7:
  dyld: Symbol not found: __ZNSt3__119__is_posix_terminalEP7__sFILE
  (std::print support symbol; macOS 14's system libc++ predates LLVM 18)
Lowering minos alone is not viable — run B tests the shipping route:
deployment target 11.0 + statically linked LLVM libc++/libc++abi,
injected via [build] ldflags (honored by the released bootstrap mcpp,
no code change needed for the experiment).
…salt

Run B attempt 1 failed with std.pcm config-mismatch: the warm build
(no deployment env -> target macosx15) reused run A's cached std.pcm
(compiled for macosx14). mcpp's BMI fingerprint does not include the
deployment target — real product bug, fix tracked for 0.0.50.
…it deployment target

Released macosx-arm64 binaries carried LC_BUILD_VERSION minos=15.0 and
dynamically linked the SYSTEM /usr/lib/libc++.1.dylib, so they only ran
on macOS 15: dyld refuses older minos, and lowering minos alone dies at
launch on macOS 14 with a missing libc++ symbol
(__ZNSt3__119__is_posix_terminalEP7__sFILE — std::print support added in
LLVM-18-era libc++; verified on macos-14 CI).

- flags.cppm: implement staticStdlib (the manifest default, previously
  silently ignored on the clang route) for the macOS link path — link
  LLVM's own libc++.a/libc++abi.a via -nostdlib++ instead of -lc++,
  falling back to -lc++ when the archives are absent. Mirror
  MACOSX_DEPLOYMENT_TARGET onto compile and link command lines so ninja
  commands don't depend on env propagation.
- cli.cppm: fold MACOSX_DEPLOYMENT_TARGET into the BMI fingerprint —
  the deployment target changes the effective compile triple
  (arm64-apple-macosxNN), and a std.pcm built for one target cannot be
  loaded by a TU compiled for another (config-mismatch observed on CI).
- main.cpp: __APPLE__ exit guard (_Exit after stream flush) — static
  libc++'s static destruction can SIGABRT on exit; same guard xlings
  uses.
- release.yml (macos job): MACOSX_DEPLOYMENT_TARGET=11.0 + staged-archive
  ldflags injection for the self-build (the bootstrap mcpp predates this
  change), with minos/no-dylib assertions.
- version 0.0.50.

Design: xlings .agents/docs/2026-06-05-macos-min-version-support.md
…ve injection

mcpp@HEAD now carries the flags.cppm staticStdlib implementation; xlings
is built by it with NO injection (product-path verification). The
bootstrap-built mcpp itself uses v2 injection: -L a dylib-free staged
dir so the hardcoded -lc++ resolves to libc++.a (run B2 showed
-nostdlib++ in user ldflags cannot remove the prepended -lc++).
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.
Run B3 post-mortem: the v2 injection never actually ran (a merge
conflict broke the command chain), so B3 re-tested B2's injection. But
injection is the wrong tool anyway — the bootstrap's hardcoded -lc++
precedes user ldflags. Two-stage self-host removes the problem: stage 1
(bootstrap-built, dynamic, runs on the build host) rebuilds itself as
stage 2 with flags.cppm's native staticStdlib implementation.
…njection)

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.
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.
# Conflicts:
#	.github/workflows/release.yml
Sunrisepeak added a commit that referenced this pull request Jun 4, 2026
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).
Sunrisepeak added a commit that referenced this pull request Jun 5, 2026
…k, explicit deployment target (#116)

* feat(0.0.50): macOS min-version support — static LLVM libc++ + explicit deployment target

Released macosx-arm64 binaries carried LC_BUILD_VERSION minos=15.0 and
dynamically linked the SYSTEM /usr/lib/libc++.1.dylib, so they only ran
on macOS 15: dyld refuses older minos, and lowering minos alone dies at
launch on macOS 14 with a missing libc++ symbol
(__ZNSt3__119__is_posix_terminalEP7__sFILE — std::print support added in
LLVM-18-era libc++; verified on macos-14 CI).

- flags.cppm: implement staticStdlib (the manifest default, previously
  silently ignored on the clang route) for the macOS link path — link
  LLVM's own libc++.a/libc++abi.a via -nostdlib++ instead of -lc++,
  falling back to -lc++ when the archives are absent. Mirror
  MACOSX_DEPLOYMENT_TARGET onto compile and link command lines so ninja
  commands don't depend on env propagation.
- cli.cppm: fold MACOSX_DEPLOYMENT_TARGET into the BMI fingerprint —
  the deployment target changes the effective compile triple
  (arm64-apple-macosxNN), and a std.pcm built for one target cannot be
  loaded by a TU compiled for another (config-mismatch observed on CI).
- main.cpp: __APPLE__ exit guard (_Exit after stream flush) — static
  libc++'s static destruction can SIGABRT on exit; same guard xlings
  uses.
- release.yml (macos job): MACOSX_DEPLOYMENT_TARGET=11.0 + staged-archive
  ldflags injection for the self-build (the bootstrap mcpp predates this
  change), with minos/no-dylib assertions.
- version 0.0.50.

Design: xlings .agents/docs/2026-06-05-macos-min-version-support.md

* ci(release): macos self-build static-libc++ injection v2 (staged -L dir)

Run B2 evidence: -nostdlib++ in user ldflags cannot remove the
hardcoded -lc++ that precedes it in the bootstrap mcpp's link line.
Stage the archives in a dylib-free dir instead and -L it so -lc++
resolves to libc++.a.

* ci(release): macos self-build via two-stage self-host (drop ldflags injection)

The bootstrap's hardcoded -lc++ precedes user ldflags, so manifest
injection cannot produce the static link (runs B2/B3). Stage 1
(bootstrap-built, dynamic) rebuilds itself as stage 2 with the native
staticStdlib implementation from this release.

* feat: macOS links via lld (same as the Linux clang path)

Xcode's system ld floats with the host: on macos-14 CI, Xcode 15.4's ld
aborted at launch (dyld resolved its libc++ against the LLVM payload's
libc++.1.0.dylib, missing __ZdaPv). lld ships with the exact LLVM
toolchain doing the compile and removes the host-Xcode dependency from
the link step.

* feat: [build] macos_deployment_target manifest field

First-class manifest support for the macOS minimum supported OS version
of produced binaries (LC_BUILD_VERSION minos), following ecosystem
conventions: the MACOSX_DEPLOYMENT_TARGET env var (which cargo/rustc and
the cc crate honor) stays as the explicit per-invocation override, the
new [build] macos_deployment_target field provides the project default
(SwiftPM platforms: style), and the toolchain/SDK default applies when
neither is set.

The resolved value feeds the same paths the env var already did:
explicit -mmacosx-version-min on compile+link command lines and the BMI
fingerprint (switching targets rebuilds the module cache). No effect off
macOS. Unit tests + docs/05-mcpp-toml.md.

* fix(macos): static libc++ via ld64 -hidden-l (gtest SIGABRT on exit)

Linking the archives by path left their symbols with default
visibility; the system libc++/libc++abi that libSystem pulls in
indirectly then clashes with the static copy and processes abort
during static destruction — every gtest binary exited 6 on macos CI
(the mcpp/xlings entry points only survived via their _Exit guards).
-hidden-l is the ld64 feature built for static libc++: it links the
archive (never the sibling dylib) and gives its symbols hidden
visibility, so the two copies can coexist.

* fix(macos): per-unit stdlib link — static libc++ for distributables, system libc++ for tests

Statically linked libc++ SIGABRTs during static destruction unless the
entry point guards with _Exit (mcpp/xlings do; gtest main does not):
with the static link applied globally, every mcpp-test binary on macOS
exited 6 (-hidden-l visibility isolation did not change that — the
abort is in libc++'s own exit path, not a symbol clash).

Split the stdlib choice per link unit via unit_ldflags: distributable
targets (Binary/SharedLibrary) keep the static LLVM libc++ (-hidden-l
archive form, portable across macOS versions), TestBinary targets link
the system -lc++ — they only ever run on the build host. Other
platforms unaffected (fields empty).

* test: TEMP forensics — static libc++ exit-abort matrix on macos (remove before merge)

* fix(macos): direct archive linking + floor 14.0 (forensics-driven)

CI forensics (3-mode matrix with a std::cout probe) overturned the
working theory:

- -Wl,-hidden-l under lld resolves like a plain -l and picks the
  SIBLING DYLIB in the same directory: binaries carried
  @rpath/libc++.1.dylib with no rpath and died at load (dyld 'Library
  not loaded' -> Abort trap 6). That — not static destruction — was
  what killed every gtest/e2e binary. Link the archives by path
  (the form already verified end-to-end on macos-14/15 in PR #115
  run B5), keeping the per-unit split (tests use system -lc++).
- The official LLVM-20.1.7-macOS-ARM64 static archives are built for
  macOS 14 (ld64.lld: 'has version 14.0.0, newer than target minimum
  of 11.0.0'): claiming a 11.0 floor would be false. Deployment target
  is now 14.0 everywhere — still fully covering the macOS 14 goal;
  11-13 would need a custom libc++ build (follow-up).

* docs: comment floor references 11.0 -> 14.0

* feat(macos): built-in default deployment floor 14.0 (rustc-style)

cargo's key portability win on macOS is that every Apple target carries
a built-in default deployment target (aarch64 = 11.0) instead of
floating with the host SDK — artifacts run on older systems with zero
configuration. Adopt the same: when neither MACOSX_DEPLOYMENT_TARGET
nor [build] macos_deployment_target is set and staticStdlib is on,
default the floor to 14.0 (the official LLVM static libc++ archives'
own floor). Mirrored in the fingerprint rule. Without staticStdlib the
toolchain/SDK default still applies (a dynamically-linked binary can't
promise a lower floor than the host libc++ anyway).

* test: TEMP — verbose retry on mcpp test failure (remove before merge)

* test: TEMP — unfiltered verbose tail on mcpp test failure

* test: TEMP — manual ninja rerun forensics

* Revert "feat(macos): built-in default deployment floor 14.0 (rustc-style)"

This reverts commit 3fa6f87.

* test: remove TEMP forensics; defer built-in default floor

The built-in default deployment floor (rustc-style) trips a std-module
prebuild/fingerprint boundary in the dev/test pipeline (the test
fingerprint changes but its std.pcm staging does not follow — import
std fails wholesale in the test build). Reverted for now: release
artifact floors are pinned by MACOSX_DEPLOYMENT_TARGET=14.0 in the
release workflow, and the env var + [build] macos_deployment_target
manifest field cover all explicit cases. Re-land the default once the
stdmod staging honors the same resolution (tracked in the design doc).

* fix(macos): gate static libc++ on an explicitly declared deployment floor

Semantics: declaring a minimum macOS (env var or [build]
macos_deployment_target) is what opts a build into the static LLVM
libc++ — that's the mechanism that makes the declared floor real. With
no declared floor the link stays on the dynamic system libc++ exactly
as in 0.0.49 (zero behavior change for existing users), which also
sidesteps a still-open SIGSEGV in mixed C/C++ static binaries
(e2e 36_llvm_toolchain; investigation tracked in the design doc).
Release pipelines declare 14.0, so the shipped mcpp/xlings binaries
remain static + portable.

* docs: declared-floor-implies-static-runtime semantics

* docs: TODO markers for deferred macos work (default-static flip, floor 11, stdmod boundary)
@Sunrisepeak
Copy link
Copy Markdown
Member Author

Final verification — shipped artifacts confirmed on macOS 14 & 15 ✅

Run: https://github.com/mcpp-community/mcpp/actions/runs/27007616498 (matrix: macos-15, macos-14, both green)

Check macos-14 macos-15
Control: released 0.0.49 (minos=15.0, dynamic libc++) refused as expected — dyld: Symbol not found: __ZNSt3__119__is_posix_terminalEP7__sFILE (the exact root cause this PR series identified) runs
xlings v0.4.50 release tarball self-install (minos=14.0, static libc++)
mcpp v0.0.50 release tarball (minos=14.0, static libc++, libSystem-only)
User flow: mcpp new hello → build → run (xlings-provisioned LLVM)
Split-brain coverage: cout << int with declared floor (MACOSX_DEPLOYMENT_TARGET=14.0 → static libc++ via -Wl,-load_hidden) int says 42, no libc++ dylib reference same

What this experiment series established (runs A–B5 → product changes → release)

  1. Root cause (run A): pre-0.0.50 macOS binaries carried LC_BUILD_VERSION minos=15.0 and dynamically linked libc++ symbols that only exist in macOS 15's system libc++ (__is_posix_terminal, LLVM-18-era). Either alone blocks macOS 14.
  2. Fix shape (runs B1–B5): declared deployment target ⇒ statically link LLVM's libc++.a/libc++abi.a with -Wl,-load_hidden (hidden visibility prevents the split-brain unification with the system libc++ in the dyld shared cache) + -fuse-ld=lld (drops the host-Xcode ld dependency).
  3. Floor = 14.0: the official LLVM 20.1.7 macOS-ARM64 static archives are themselves built for macOS 14; going lower needs a custom libc++ build (proven feasible separately in [temp][DO NOT MERGE] custom libc++ floor-11.0 build feasibility #118, deferred).
  4. Shipped in mcpp v0.0.50 (macos_deployment_target manifest field + MACOSX_DEPLOYMENT_TARGET env, fingerprint-aware) and xlings v0.4.50 (built with the floor declared; release CI asserts minos + static runtime).

Closing — this temp CI branch has served its purpose. Follow-ups tracked in #119 (resolver), and the deferred items recorded in #117/#118 closing notes.

@Sunrisepeak Sunrisepeak closed this Jun 5, 2026
@Sunrisepeak Sunrisepeak deleted the test/macos14-support branch June 5, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant