逃离 Intel 黑苹果 / 老 MacBook,换上 M 系列 Mac 的第一件事,往往就是被历史项目按在地上摩擦------比如那个只认 Ruby 2.6.5 的祖传工程。本文记录在 arm64 Mac 上用 RVM 安装任意版本 Ruby 时会踩的三个坑、它们的根因,以及我封装的一套自动化安装脚本(已开源,可作为 Claude Code / CodeBuddy 的 skill 使用)。
背景
新 Mac 都是 Apple Silicon(arm64),但很多存量项目还锁在老版本 Ruby 上。直接 rvm install 2.6.5,在 M 系列 Mac 上大概率以失败告终。这不是玄学,而是三个边界明确的兼容性问题:
| Ruby 版本 | Apple Silicon (arm64) | Intel (x86_64) |
|---|---|---|
| <= 2.7.1(含 2.3.x ~ 2.6.x) | 需要补丁 + OpenSSL 1.1 | 只需 OpenSSL 1.1 |
| 2.7.2 ~ 3.0.x | 只需 OpenSSL 1.1 | 只需 OpenSSL 1.1 |
| >= 3.1 | 无需特殊处理 | 无需特殊处理 |
两个关键边界(都对着官方源码包验证过):
- 2.7.2 是第一个原生支持 arm64 的 Ruby:
configure的__arm64__分支和 fiddle 的HAVE_FFI_CLOSURE_ALLOC特性检测都是这个版本才合入的。 - 3.1 是第一个支持 OpenSSL 3.x 的 Ruby;更老的版本只能对着 OpenSSL 1.1 编译。
下面逐个拆解三个拦路虎。
拦路虎一:ffi_prep_closure 未声明(编译期失败)
症状
make.log 里的决定性错误:
closure.c:264:14: error: call to undeclared function 'ffi_prep_closure'
根因
Ruby <= 2.7.1 的 ext/fiddle/closure.c 里有一段硬编码:
c
#elif defined(__OpenBSD__) || defined(__APPLE__) || defined(__linux__)
# define USE_FFI_CLOSURE_ALLOC 0
即 Apple 平台强制走"legacy closure API"(ffi_prep_closure)。而新版 macOS SDK 的 arm64 libffi 中 FFI_LEGACY_CLOSURE_API 为 0,旧 API 被整个移除了,于是编译直接报"函数未声明"。
修复
在 arm64 上改用新 API(ffi_closure_alloc + ffi_prep_closure_loc):
c
#if defined(__APPLE__) && defined(__arm64__
/* macOS SDK libffi (>= macOS 26) removed the legacy ffi_prep_closure on arm64:
* FFI_LEGACY_CLOSURE_API is 0, so use ffi_closure_alloc + ffi_prep_closure_loc. */
# define USE_FFI_CLOSURE_ALLOC 1
#endif
注意这不只是编译修复,还是运行时正确性修复:arm64 上旧路径把 closure 结构体指针当作入口点存起来,那根本不是可执行代码。不打这个补丁,即使想办法编过了,fiddle closure 在运行时也是坏的。
上游在 2.7.2 改成了特性检测(#elif !defined(HAVE_FFI_CLOSURE_ALLOC)),所以 2.7.2+ 永远不需要这个补丁。
顺带一提,另一个看似可行的方案------链到 Homebrew 的 libffi 3.8(它还导出废弃的 ffi_prep_closure)------不推荐:能编过,但 legacy 入口点在 arm64 上是错的,闭包在运行时会坏掉。
拦路虎二:OpenSSL 弃用错误(编译期失败)
症状
ossl.c:311:14: error: 'ERR_get_error_line_data' is deprecated [-Werror,-Wdeprecated-declarations]
错误下方的 note 行会揭示真正用了哪套头文件,比如:
/opt/homebrew/Cellar/openssl@4/4.0.3/include/openssl/err.h:380: note: ... marked deprecated here
根因
Ruby < 3.1 只支持 OpenSSL 1.1,而 ext/openssl/extconf.rb 会先执行 pkg_config("openssl")------它解析到的是 Homebrew 的 OpenSSL 3.x/4.x。于是 Ruby 拿着 1.1 时代的代码对着 3.x/4.x 的头文件编译,弃用警告直接升级成 -Werror 错误。
修复
把 PKG_CONFIG_PATH 钉死到 OpenSSL 1.1:
bash
PKG_CONFIG_PATH=/opt/homebrew/opt/openssl@1.1/lib/pkgconfig \
rvm install 2.6.5
一个常见误区是只传 --with-openssl-dir------没用,因为 pkg-config 的结果优先于它。
但更大的麻烦在后头 :OpenSSL 1.1 已于 2024-09-30 EOL,Homebrew 正在下架 openssl@1.1 formula,新机器上 brew install openssl@1.1 可能直接失败。我的脚本对此的兜底是:从 openssl.org 官方源码包构建 1.1.1w(校验 sha256)装到 ~/.local/openssl-1.1,然后把 PKG_CONFIG_PATH 指向 ~/.local/openssl-1.1/lib/pkgconfig。
拦路虎三:ruby -v 输出 [-darwin25](装完了,但装了个坏的)
这是最阴险的一个:编译全部通过,rvm install 正常返回,但------
bash
$ rvm list
=* ruby-2.6.5 [ x86_64 ] # 或干脆显示 [ broken ]
$ rvm 2.6.5 do ruby -v
ruby 2.6.5p114 (2020-03-31) [ -darwin25 ] # CPU 是空的!
RbConfig::CONFIG["sitearch"] 会是字面量 ${arch},一用就炸。
根因
Ruby <= 2.7.1 的 configure 靠预处理一段 C 代码来探测 Apple 的 CPU 型号:
sh
target_cpu=`$CC -E - 2>/dev/null <<EOF |
#ifdef __x86_64__
"processor-name=x86_64"
#endif
...
#ifdef __ppc64__
"processor-name=powerpc64"
#endif
EOF
只认 x86_64/i386/ppc/ppc64------压根没有 __arm64__ 分支(2.7.2 才加上)。于是在 Apple Silicon 上 target_cpu 为空。
修复
在 EOF 前补一段:
c
#ifdef __arm64__
"processor-name=arm64"
#endif
装完后验证:ruby -v 必须输出 [arm64-darwin25],sitearch 必须是 arm64-darwin25。
一个容易翻车的隐藏坑:IDE 注入的 rm shim
如果 PATH 上有 IDE 注入的 rm 安全删除 shim(有些 IDE 会给 rm 包一层确认逻辑),autoconf 会以一句莫名其妙的 configure: error: failed to create .ext/include/<arch>/ruby/config.h 失败,且 configure 异常缓慢。解法:构建时用干净的 PATH,并在脚本里显式用 /bin/rm。
一次到位:自动化安装脚本
坑都清楚了,但每次手敲这套流程太痛苦,我把它封装成了版本感知、架构感知的脚本:
bash
scripts/install_ruby.sh <version> # 如 2.6.5、2.7.1、3.3.8
scripts/install_ruby.sh <version> --dry-run # 只打印计划,不构建
scripts/install_ruby.sh <version> --bg # 后台构建,轮询日志
它的决策逻辑:
- 优先走正常构建 :干净 PATH + (Ruby < 3.1 时)
PKG_CONFIG_PATH钉到 OpenSSL 1.1。能不补丁就不补丁。 - arm64 + Ruby <= 2.7.1 直接走补丁构建:原生构建注定失败,没必要先撞一次墙。
- OpenSSL 1.1 四级解析 :已有 Homebrew 安装 →
~/.local/openssl-1.1旧构建 →brew install openssl@1.1→ 从官方源码包构建 1.1.1w。 - 装完自动验证 :arch 正确性、OpenSSL 版本、fiddle closure 运行时行为(真的跑一个
BlockCaller算 21×2)。
两个使用注意:
- 必须传精确的
X.Y.Z。传2.7会被 rvm 解析成 2.7.8------而 2.7.2+ 已经带了上游的__arm64__分支,补丁的 configure 段会冲突,绝不能打。 - 补丁(
assets/ruby-pre-2.7.2-apple-silicon.patch)经patch --dry-run验证可干净应用于 2.3.8 ~ 2.7.1 全系;完整构建实测过 2.6.5(arm64,macOS 26 SDK)。更老的分支可能再撞上与这三个坑无关的编译器问题。
让 AI 替你装
这套东西还打包成了 AI 编码工具的 skill,克隆到 skills 目录即可:
bash
# Claude Code
git clone https://github.com/ScottZg/ruby-macos-compat-install.git \
~/.claude/skills/ruby-macos-compat-install
# CodeBuddy Code
git clone https://github.com/ScottZg/ruby-macos-compat-install.git \
~/.codebuddy/skills/ruby-macos-compat-install
然后直接说「帮我用 rvm 安装 ruby 2.6.5」------skill 会自动触发,AI 读文档、跑脚本、验证结果,全程不用你碰终端。
仓库地址:https://github.com/ScottZg/ruby-macos-compat-install (MIT 协议),包含完整的错误签名速查、rvm 日志定位方法和补丁再生成指南(references/troubleshooting.md)。
总结
老 Ruby 在新 Mac 上的失败不是玄学,三个问题各有明确边界:
| # | 症状 | 根因 | 边界 |
|---|---|---|---|
| 1 | ffi_prep_closure 未声明 |
SDK arm64 libffi 移除 legacy closure API | Ruby <= 2.7.1,仅 arm64 |
| 2 | ERR_get_error_line_data deprecated |
pkg-config 解析到 OpenSSL 3/4 | Ruby < 3.1,双架构 |
| 3 | [-darwin25] / [ broken ] |
configure 无 __arm64__ 分支 |
Ruby <= 2.7.1,仅 arm64 |
理解了边界,「哪个版本在哪个架构上需要什么」就变成一张查表可知的矩阵,剩下的交给脚本。