跳到正文
博客园· zhanggui·· 3 小时前AI 评分22

在 Apple Silicon Mac 上用 RVM 安装老版本 Ruby 的三个坑与自动化脚本

在 Apple Silicon Mac 上安装老版本 Ruby - zhanggui

AI 导读

在 arm64 Mac 上用 RVM 安装老版本 Ruby 会踩三个坑:Ruby ≤ 2.7.1 因 SDK 移除 legacy closure API 导致 ffi_prep_closure 未声明,Ruby < 3.1 因 pkg-config 解析到 OpenSSL 3/4 报弃用错误,以及 configure 缺 __arm64__ 分支。

正文

逃离 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 里有一段硬编码:

#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):

#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:

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 正常返回,但——

$ 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 型号:

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 前补一段:

#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。

一次到位:自动化安装脚本

坑都清楚了,但每次手敲这套流程太痛苦,我把它封装成了版本感知、架构感知的脚本:

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       # 后台构建,轮询日志

它的决策逻辑:

  1. 优先走正常构建:干净 PATH + (Ruby < 3.1 时)PKG_CONFIG_PATH 钉到 OpenSSL 1.1。能不补丁就不补丁。
  2. arm64 + Ruby <= 2.7.1 直接走补丁构建:原生构建注定失败,没必要先撞一次墙。
  3. OpenSSL 1.1 四级解析:已有 Homebrew 安装 → ~/.local/openssl-1.1 旧构建 → brew install openssl@1.1 → 从官方源码包构建 1.1.1w。
  4. 装完自动验证: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 目录即可:

# 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

理解了边界,「哪个版本在哪个架构上需要什么」就变成一张查表可知的矩阵,剩下的交给脚本。

来源:博客园 · cnblogs.com