Skip to content

Commit f43ea49

Browse files
committed
feat: macOS ARM64 support (LLVM toolchain) — initial implementation
- probe.cppm: platform-aware runtime dir discovery (macOS paths + xcrun sysroot fallback) - flags.cppm: skip -static on macOS (libSystem must be dynamic) - config.cppm: skip patchelf bootstrap on macOS (Mach-O, not ELF) - install.sh: add darwin-arm64/x86_64 platform detection + macOS sha256 compat - ci-macos.yml: lightweight macOS ARM64 smoke test workflow - Design doc: .agents/docs/2026-05-16-macos-support-design.md
1 parent d146b55 commit f43ea49

6 files changed

Lines changed: 336 additions & 6 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# macOS Support Design — LLVM/Clang 自含工具链方案
2+
3+
Date: 2026-05-16
4+
5+
## 目标
6+
7+
在 macOS (ARM64) 上达到与 Linux LLVM 工具链同等可用水平:
8+
- 非模块 C/C++ 编译
9+
- `import std` 支持
10+
- 多模块项目 + dyndep
11+
- BMI 缓存 + 增量构建
12+
13+
## 设计原则
14+
15+
1. **从 upstream LLVM 切入**,不依赖 Apple Clang
16+
2. **最小宿主依赖** — 仅需 macOS SDK(CommandLineTools),编译器/libc++/linker/runtime 全用 xlings 自含的 LLVM 包
17+
3. **不引入 xlings 外部依赖** — 通过 xlings 包生态获取工具链
18+
19+
## 前提条件
20+
21+
xlings LLVM 包(`xim:llvm@20.1.7`)macOS ARM64 版本:
22+
- 已有分发包:`LLVM-20.1.7-macOS-ARM64.tar.xz`
23+
- 包含:clang++, lld, llvm-ar, libc++, compiler-rt, libunwind
24+
- 安装时生成 `clang++.cfg` 自动配置 sysroot + libc++ 路径
25+
26+
macOS SDK(`/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk`)提供:
27+
- 系统头文件(`<unistd.h>`, `<mach/mach.h>` 等)
28+
- libSystem(macOS 的 libc 等价物)
29+
- 这是 macOS 平台的硬限制,无法绕过
30+
31+
## 代码改动清单
32+
33+
### 1. `src/toolchain/probe.cppm` — 平台感知的运行时路径发现
34+
35+
**问题**`discover_compiler_runtime_dirs()` 硬编码 `x86_64-unknown-linux-gnu` 和 Linux 系统路径。
36+
37+
**方案**
38+
```cpp
39+
// discover_compiler_runtime_dirs: 加 macOS 分支
40+
if (looksLikeLlvm) {
41+
append_existing_unique(dirs, root / "lib");
42+
#if defined(__linux__)
43+
append_existing_unique(dirs, root / "lib" / "x86_64-unknown-linux-gnu");
44+
append_existing_unique(dirs, "/lib/x86_64-linux-gnu");
45+
append_existing_unique(dirs, "/usr/lib/x86_64-linux-gnu");
46+
append_existing_unique(dirs, "/usr/lib64");
47+
#elif defined(__APPLE__)
48+
append_existing_unique(dirs, root / "lib" / "aarch64-apple-darwin");
49+
append_existing_unique(dirs, root / "lib" / "darwin");
50+
#endif
51+
}
52+
```
53+
54+
**问题**`discover_link_runtime_dirs()` 硬编码 `x86_64-unknown-linux-gnu` fallback。
55+
56+
**方案**:改为条件编译,macOS 下不添加 Linux 路径。
57+
58+
**问题**`probe_sysroot()` 在 Apple Clang / upstream LLVM on macOS 下 `-print-sysroot` 返回空。
59+
60+
**方案**:加 macOS fallback:
61+
```cpp
62+
// 在 probe_sysroot 末尾,如果结果为空且在 macOS 上:
63+
#if defined(__APPLE__)
64+
if (s.empty()) {
65+
auto xcrun_r = run_capture("xcrun --show-sdk-path 2>/dev/null");
66+
if (xcrun_r) {
67+
auto sdk = trim_line(*xcrun_r);
68+
if (!sdk.empty() && std::filesystem::exists(sdk)) return sdk;
69+
}
70+
}
71+
#endif
72+
```
73+
74+
### 2. `src/build/flags.cppm` — macOS 链接 flags 适配
75+
76+
**问题**
77+
- `-Wl,-rpath,<dir>` 在 macOS ld64 上语法相同,但 ELF-only flags 如 `--enable-new-dtags` 不存在
78+
- `-static` 在 macOS 上不可用(libSystem 必须动态链接)
79+
- `-static-libstdc++` 对 Clang 已跳过(现有代码已处理)
80+
81+
**方案**
82+
```cpp
83+
// flags.cppm: compute_flags 链接部分
84+
std::string full_static = "";
85+
#if !defined(__APPLE__)
86+
full_static = (f.linkage == "static") ? " -static" : "";
87+
#endif
88+
// macOS 不支持 full static,忽略该配置
89+
```
90+
91+
rpath 语法:macOS ld64 支持 `-rpath <path>`(通过 `-Wl,-rpath,<path>`),行为与 Linux 相同,无需改动。
92+
93+
### 3. `src/pack/pack.cppm` — macOS 打包支持(Phase 2)
94+
95+
**问题**:patchelf 只适用于 ELF。macOS 用 Mach-O,需要 `install_name_tool` 或直接用 `@rpath`
96+
97+
**方案(MVP 先跳过)**:macOS 首版不做 `mcpp pack`,focus on `mcpp build` 可用。后续用 `install_name_tool -add_rpath` 替代 patchelf。
98+
99+
### 4. `install.sh` — 增加 darwin-arm64
100+
101+
```bash
102+
case "${uname_s}-${uname_m}" in
103+
Linux-x86_64) PLAT="linux-x86_64" ;;
104+
Darwin-arm64) PLAT="darwin-arm64" ;;
105+
Darwin-x86_64) PLAT="darwin-x86_64" ;;
106+
*) ... ;;
107+
esac
108+
```
109+
110+
macOS 上 `sha256sum` 不存在,改用 `shasum -a 256`
111+
112+
### 5. `src/cli.cppm` — patchelf_walk 跳过 macOS
113+
114+
现有的 `patchelf_walk` / specs fixup 是 ELF-only。macOS 上跳过:
115+
```cpp
116+
#if !defined(__APPLE__)
117+
// existing patchelf logic
118+
#endif
119+
```
120+
121+
### 6. CI Workflow — `.github/workflows/ci-macos.yml`
122+
123+
轻量 macOS 验证 CI:
124+
- 运行环境:`macos-14`(ARM64 runner)
125+
- 步骤:安装 xlings → 安装 mcpp → `mcpp build``mcpp test`
126+
- 不跑全量 E2E(先验证核心编译链路)
127+
128+
## 验证计划
129+
130+
1. `mcpp build` 能在 macOS + xlings LLVM 20 下编译 hello world
131+
2. `import std` 模块项目能编译通过
132+
3. mcpp 自身能在 macOS 上 self-host 编译(长期目标)
133+
134+
## 不做的事
135+
136+
- Apple Clang 支持(用 upstream LLVM 即可)
137+
- macOS 上的 musl 静态链接(不适用)
138+
- `mcpp pack` 的 macOS Mach-O 支持(Phase 2)
139+
- Universal binary(arm64 + x86_64 fat binary)
140+
141+
## 风险
142+
143+
1. xlings LLVM macOS 包的 `clang++.cfg` 尚不完整(缺 lld/compiler-rt 配置)— 需要 xlings 上游补全
144+
2. `ld64.lld` 稳定性 — LLVM 20 的 Mach-O lld 已较成熟,但可能有边缘 case
145+
3. macOS SDK 版本差异 — CommandLineTools vs Xcode SDK 路径不同,需 fallback 链

.github/workflows/ci-macos.yml

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
name: ci-macos
2+
3+
# Lightweight macOS validation CI.
4+
# Verifies that mcpp can build simple projects on macOS ARM64
5+
# using the xlings LLVM toolchain (upstream Clang 20 + bundled libc++).
6+
7+
on:
8+
push:
9+
branches: [ feat/macos-support ]
10+
pull_request:
11+
branches: [ main ]
12+
paths:
13+
- 'src/toolchain/**'
14+
- 'src/build/**'
15+
- 'install.sh'
16+
- '.github/workflows/ci-macos.yml'
17+
workflow_dispatch:
18+
19+
concurrency:
20+
group: ci-macos-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
macos-llvm-smoke:
25+
name: macOS ARM64 LLVM smoke test
26+
runs-on: macos-14
27+
timeout-minutes: 30
28+
env:
29+
MCPP_HOME: /Users/runner/.mcpp
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Cache mcpp sandbox
34+
uses: actions/cache@v4
35+
with:
36+
path: ~/.mcpp
37+
key: mcpp-sandbox-macos-arm64-${{ hashFiles('mcpp.toml') }}
38+
restore-keys: |
39+
mcpp-sandbox-macos-arm64-
40+
41+
- name: Cache xlings
42+
uses: actions/cache@v4
43+
with:
44+
path: ~/.xlings
45+
key: xlings-macos-arm64-${{ hashFiles('.xlings.json') }}
46+
restore-keys: |
47+
xlings-macos-arm64-
48+
49+
- name: Verify CommandLineTools SDK exists
50+
run: |
51+
xcrun --show-sdk-path
52+
# If this fails, the runner doesn't have CLT installed.
53+
# macos-14 runners come with Xcode which includes the SDK.
54+
55+
- name: Bootstrap mcpp via xlings
56+
env:
57+
XLINGS_NON_INTERACTIVE: '1'
58+
XLINGS_VERSION: '0.4.30'
59+
run: |
60+
# Download and install xlings for macOS ARM64
61+
tarball="xlings-${XLINGS_VERSION}-macos-arm64.tar.gz"
62+
curl -fsSL -o "/tmp/${tarball}" \
63+
"https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" || {
64+
echo "::warning::xlings macOS ARM64 tarball not available at v${XLINGS_VERSION}"
65+
echo "Trying alternative naming convention..."
66+
tarball="xlings-${XLINGS_VERSION}-darwin-arm64.tar.gz"
67+
curl -fsSL -o "/tmp/${tarball}" \
68+
"https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}"
69+
}
70+
tar -xzf "/tmp/${tarball}" -C /tmp
71+
# Find the extracted directory
72+
XLINGS_DIR=$(find /tmp -maxdepth 1 -name "xlings-*" -type d | head -1)
73+
echo "xlings dir: $XLINGS_DIR"
74+
ls "$XLINGS_DIR/"
75+
# Install xlings
76+
if [ -f "$XLINGS_DIR/subos/default/bin/xlings" ]; then
77+
"$XLINGS_DIR/subos/default/bin/xlings" self install
78+
elif [ -f "$XLINGS_DIR/bin/xlings" ]; then
79+
"$XLINGS_DIR/bin/xlings" self install
80+
else
81+
echo "::error::Cannot find xlings binary in extracted tarball"
82+
find "$XLINGS_DIR" -name xlings -type f
83+
exit 1
84+
fi
85+
export PATH="$HOME/.xlings/subos/default/bin:$HOME/.xlings/bin:$PATH"
86+
xlings --version
87+
# Install mcpp
88+
xlings install mcpp -y
89+
MCPP=$(find "$HOME/.xlings" -name mcpp -type f -perm +111 | head -1)
90+
test -x "$MCPP"
91+
"$MCPP" --version
92+
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
93+
94+
- name: Install LLVM toolchain
95+
run: |
96+
"$MCPP" self config --mirror GLOBAL
97+
"$MCPP" toolchain install llvm 20.1.7
98+
"$MCPP" toolchain list
99+
100+
- name: Smoke test - hello world (no modules)
101+
run: |
102+
TMPDIR=$(mktemp -d)
103+
cd "$TMPDIR"
104+
cat > mcpp.toml << 'EOF'
105+
[project]
106+
name = "hello"
107+
version = "0.1.0"
108+
standard = "c++23"
109+
110+
[toolchain]
111+
macos = "llvm@20.1.7"
112+
default = "llvm@20.1.7"
113+
EOF
114+
mkdir src
115+
cat > src/main.cpp << 'EOF'
116+
#include <iostream>
117+
int main() {
118+
std::cout << "Hello from mcpp on macOS!" << std::endl;
119+
return 0;
120+
}
121+
EOF
122+
"$MCPP" build
123+
# Find and run the built binary
124+
find target -name hello -type f -perm +111 -exec {} \;
125+
126+
- name: Smoke test - import std
127+
run: |
128+
TMPDIR=$(mktemp -d)
129+
cd "$TMPDIR"
130+
cat > mcpp.toml << 'EOF'
131+
[project]
132+
name = "modtest"
133+
version = "0.1.0"
134+
standard = "c++23"
135+
136+
[toolchain]
137+
macos = "llvm@20.1.7"
138+
default = "llvm@20.1.7"
139+
EOF
140+
mkdir src
141+
cat > src/main.cpp << 'EOF'
142+
import std;
143+
int main() {
144+
std::println("C++23 modules work on macOS!");
145+
return 0;
146+
}
147+
EOF
148+
"$MCPP" build
149+
find target -name modtest -type f -perm +111 -exec {} \;

install.sh

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ PREFIX="${MCPP_PREFIX:-$HOME/.mcpp}"
2626
uname_s=$(uname -s)
2727
uname_m=$(uname -m)
2828
case "${uname_s}-${uname_m}" in
29-
Linux-x86_64) PLAT="linux-x86_64" ;;
29+
Linux-x86_64) PLAT="linux-x86_64" ;;
30+
Darwin-arm64) PLAT="darwin-arm64" ;;
31+
Darwin-x86_64) PLAT="darwin-x86_64" ;;
3032
*)
3133
echo "error: unsupported platform ${uname_s}-${uname_m}." >&2
32-
echo " v0.0.3 ships only linux-x86_64. Build from source instead:" >&2
34+
echo " Currently supported: linux-x86_64, darwin-arm64, darwin-x86_64." >&2
35+
echo " Build from source instead:" >&2
3336
echo " https://github.com/${REPO}#从源码构建开发者" >&2
3437
exit 1
3538
;;
@@ -58,7 +61,11 @@ curl --fail --location --silent --show-error -o "$WORK/mcpp.sha256" "$SHA_URL" |
5861
# ---- verify ---------------------------------------------------------------
5962
if [[ -s "$WORK/mcpp.sha256" ]]; then
6063
expected=$(awk '{print $1}' "$WORK/mcpp.sha256")
61-
actual=$(sha256sum "$WORK/mcpp.tar.gz" | awk '{print $1}')
64+
if command -v sha256sum >/dev/null 2>&1; then
65+
actual=$(sha256sum "$WORK/mcpp.tar.gz" | awk '{print $1}')
66+
else
67+
actual=$(shasum -a 256 "$WORK/mcpp.tar.gz" | awk '{print $1}')
68+
fi
6269
if [[ "$expected" != "$actual" ]]; then
6370
echo "error: sha256 mismatch" >&2
6471
echo " expected: $expected" >&2

src/build/flags.cppm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,12 @@ CompileFlags compute_flags(const BuildPlan& plan) {
149149
// Link flags
150150
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
151151
f.linkage = plan.manifest.buildConfig.linkage;
152+
#if defined(__APPLE__)
153+
// macOS does not support full static linking (libSystem must be dynamic)
154+
std::string full_static;
155+
#else
152156
std::string full_static = (f.linkage == "static") ? " -static" : "";
157+
#endif
153158
std::string static_stdlib = (f.staticStdlib && !isClang) ? " -static-libstdc++" : "";
154159
std::string runtime_dirs;
155160
for (auto& dir : plan.toolchain.linkRuntimeDirs) {

src/config.cppm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,10 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
535535
// upstream (see docs/short-term-vs-long-track plan).
536536
ensure_sandbox_xlings_binary(cfg, quiet);
537537
ensure_sandbox_init(cfg, quiet);
538+
#if !defined(__APPLE__)
539+
// patchelf is ELF-only; macOS uses Mach-O and does not need it.
538540
ensure_sandbox_patchelf(cfg, quiet, onBootstrapProgress);
541+
#endif
539542
ensure_sandbox_ninja(cfg, quiet, onBootstrapProgress);
540543

541544
return cfg;

0 commit comments

Comments
 (0)