同一台云端 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"
重点比较 ruby 与 bundle 是否来自同一个前缀。若 Ruby 位于版本管理器目录,而 Bundler 来自系统目录,后续安装很可能写入错误的 gem 路径。还要检查 CI 是否设置了 GEM_HOME、GEM_PATH 或 BUNDLE_PATH;遗留变量会覆盖项目配置。
能在交互终端运行,不代表自动任务拥有相同环境。CI 应从明确的版本文件和项目配置启动,而不是依赖 shell 启动脚本碰巧修改 PATH。
建立一份诊断产物即可,不要在日志中输出令牌、证书内容或完整环境变量。路径和版本足以判断大多数解析问题。
固定 Ruby、Bundler 与 fastlane
仓库根目录应提交 .ruby-version、Gemfile 和 Gemfile.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.lock 的 BUNDLED 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 list 和 bundle 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、租用周期与部署节点,将开发、构建或实验任务放到一台独享物理机上。