工程实践

云端 Mac CI 的 Ruby 与 fastlane 版本隔离实战

云端 Mac CI 的 Ruby 与 fastlane 版本隔离实战

同一台云端 Mac 上,开发者从终端执行 fastlane 成功,CI 任务却提示 gem 缺失;修好 runner 后,登录终端又解析到另一套 Ruby。这类故障通常不是 fastlane 本身的问题,而是系统 Ruby、版本管理器、Bundler 配置和任务环境共同造成的路径漂移。解决方法不是反复安装 gem,而是把整条 Ruby 工具链变成项目的一部分。

先确认实际执行的是哪套 Ruby

不要先运行安装命令。分别在登录终端和 CI 任务中记录以下结果:

command -v ruby
ruby -v
command -v bundle
bundle -v
gem env home
bundle config list
printf '%s
' "$PATH"

重点比较 rubybundle 是否来自同一个前缀。若 Ruby 位于版本管理器目录,而 Bundler 来自系统目录,后续安装很可能写入错误的 gem 路径。还要检查 CI 是否设置了 GEM_HOMEGEM_PATHBUNDLE_PATH;遗留变量会覆盖项目配置。

能在交互终端运行,不代表自动任务拥有相同环境。CI 应从明确的版本文件和项目配置启动,而不是依赖 shell 启动脚本碰巧修改 PATH。

建立一份诊断产物即可,不要在日志中输出令牌、证书内容或完整环境变量。路径和版本足以判断大多数解析问题。

固定 Ruby、Bundler 与 fastlane

仓库根目录应提交 .ruby-versionGemfileGemfile.lock。Ruby 版本必须写成可安装的完整版本,而不是模糊的 3.3。先由团队选定版本,再执行:

printf '%s
' '3.3.6' > .ruby-version
bundle init
bundle add fastlane --version '~> 2.0'
bundle lock

Gemfile 表达允许范围,Gemfile.lock 记录本次解析出的精确版本。CI 不应在每次构建前运行 bundle update,否则锁文件失去约束作用。安装完成后,所有命令都通过 Bundler 入口执行:

bundle exec fastlane --version
bundle exec fastlane ios ci

不要把全局 fastlane 当作快捷方式。它可能来自另一套 Ruby,也可能在系统更新或人工调试后发生变化。若项目要求特定 Bundler 版本,可从 Gemfile.lockBUNDLED WITH 段读取并安装该版本,再运行 bundle install

把依赖安装限制在项目目录

项目级配置应写入仓库旁的 .bundle/config,不要修改整台主机的全局 Bundler 设置:

bundle config set --local path vendor/bundle
bundle config set --local clean true
bundle install --jobs 4 --retry 2

vendor/bundle 通常不提交到 Git,而是交给 CI 缓存。这样既不会污染系统 gem,也能让多个项目使用不同版本。安装后可用下面的检查确认解析链完整:

test -f Gemfile.lock
test "$(ruby -e 'print RUBY_VERSION')" = "$(cat .ruby-version)"
bundle check
bundle exec ruby -e 'require "fastlane"; puts Fastlane::VERSION'

如果最后一步失败,不要立即删除全部缓存。先检查 bundle config listbundle env,确认任务读取的是当前仓库的 Gemfile,而不是父目录或通过 BUNDLE_GEMFILE 指向的其他文件。

模拟干净的非交互环境

可在正式接入 runner 前,用最小环境做一次验收:

RUBY_BIN="$(dirname "$(command -v ruby)")"
env -i \
  HOME="$HOME" \
  PATH="$RUBY_BIN:/usr/bin:/bin:/usr/sbin:/sbin" \
  BUNDLE_GEMFILE="$PWD/Gemfile" \
  bundle exec fastlane --version

这一步能提前暴露只写在个人 shell 配置中的路径、别名和环境变量。若命令失败,应修正 runner 的显式初始化,而不是让 CI 加载整份个人配置。

设计不会串用的缓存键

带原生扩展的 gem 不能只按 Gemfile.lock 建缓存。Ruby 版本、CPU 架构和 macOS 主次版本不同,都可能让已编译文件无法加载。缓存键至少应包含以下字段:

字段 获取方式 作用
Ruby 版本 ruby -e 'print RUBY_VERSION' 隔离 ABI 差异
CPU 架构 uname -m 区分不同原生代码
macOS 版本 sw_vers -productVersion 避免系统库差异
锁文件摘要 shasum -a 256 Gemfile.lock 依赖变化时失效

可生成一段稳定指纹:

RUBY_VERSION_KEY="$(ruby -e 'print RUBY_VERSION')"
ARCH_KEY="$(uname -m)"
OS_KEY="$(sw_vers -productVersion | cut -d. -f1,2)"
LOCK_KEY="$(shasum -a 256 Gemfile.lock | cut -d' ' -f1)"
printf '%s-%s-%s-%s
' "$RUBY_VERSION_KEY" "$ARCH_KEY" "$OS_KEY" "$LOCK_KEY"

恢复缓存后仍要运行 bundle check。检查失败时再执行 bundle install 补齐内容,不能把“成功解压缓存”当成依赖可用的证明。

将失败变成可定位的门禁

最终任务应按“环境验收、依赖验收、业务 lane”三段执行。第一段检查版本和路径,第二段运行 bundle check,第三段才调用 fastlane。任何一段失败都应立即停止,避免错误在后续步骤中表现成难以理解的插件缺失或常量加载异常。

建议把以下项目加入合并检查:

  • .ruby-version 与 runner 实际 Ruby 完全一致。
  • Gemfile.lock 已提交,任务执行前后没有变化。
  • fastlane 只通过 bundle exec 调用。
  • .bundle/config 使用项目级依赖目录。
  • 缓存键包含 Ruby、架构、系统版本和锁文件摘要。
  • 日志只保留版本、路径和校验结果,不输出敏感环境值。
  • runner 与登录终端使用同一份显式初始化逻辑。

完成这些约束后,Ruby 工具链就从“主机上装过的一组命令”变成了可审计的构建输入。以后升级 Ruby 或 fastlane 时,可以通过独立提交更新版本文件和锁文件,并用同一套验收步骤确认变化范围,而不必在失败后猜测任务究竟调用了哪套环境。

常见问题

为什么已经提交 Gemfile.lock,fastlane 版本仍可能变化?

如果任务直接调用全局 fastlane,或执行前重新生成锁文件,Gemfile.lock 就没有参与解析。应始终使用 bundle exec fastlane,并把锁文件作为只读输入。

vendor/bundle 可以在不同云端 Mac 之间直接复用吗?

不能无条件复用。缓存键至少应包含 Ruby 版本、CPU 架构、macOS 主次版本和 Gemfile.lock 摘要,避免带原生扩展的 gem 被错误加载。

CI 能运行但登录终端失败时先检查什么?

先比较 command -v ruby、ruby -v、bundle config list 和环境变量。两种会话通常加载了不同的 PATH、Ruby 管理器初始化脚本或项目级 Bundler 配置。

独享物理节点

在云端持续运行你的 Mac mini 工作流

选择 RunAMac M4、租用周期与部署节点,将开发、构建或实验任务放到一台独享物理机上。

选择租用方案