云端 Mac 的 CocoaPods 可复现安装与依赖漂移治理

CI/CD 实践 ·约 6 分钟阅读

云端 Mac 的 CocoaPods 可复现安装与依赖漂移治理

同一个提交在开发机编译通过,换到云端 Mac 的新任务却突然解析出不同依赖,或者在 pod install 后悄悄改写 Podfile.lock。这类问题往往不是 Xcode 本身,而是 CocoaPods 工具版本、缓存和安装模式没有被当作构建输入管理。正确目标不是“每次都能装完”,而是同一组输入必然得到同一份依赖图,出现变化时立即停止。

先定义可复现安装的输入

CocoaPods 的结果至少受五项输入影响:Podfile、Podfile.lock、CocoaPods 版本、Ruby 与 Bundler 环境,以及本地开发 Pod 指向的源码。只提交锁文件但让执行器使用任意 CocoaPods 版本,仍可能遇到工程生成格式或安装行为差异。

仓库应提交 .ruby-version、Gemfile、Gemfile.lock、Podfile 和 Podfile.lock。Gemfile 中明确 CocoaPods 版本,不要依赖机器全局安装的最新版:

source ENV.fetch("BUNDLE_GEM_SOURCE")

ruby "3.3.4"
gem "cocoapods", "1.15.2"

CI 入口统一使用 bundle exec pod。这样日志里看到的版本与锁定环境一致,也避免某个交互式 Shell 恰好优先找到全局命令。

Podfile.lock 锁定的是依赖解析结果,不会替你锁定执行解析与生成工程的 CocoaPods 程序。

把安装命令改成严格模式

日常构建不能运行 pod update。它会在版本约束范围内重新解析依赖,适合人工发起的升级任务,不适合验证已有提交。CI 可以采用下面的安装入口:

#!/bin/bash
set -euo pipefail

export LANG=en_US.UTF-8
export COCOAPODS_DISABLE_STATS=true
export CP_HOME_DIR="${RUNNER_TEMP}/cocoapods-home"

bundle config set path "${RUNNER_TEMP}/bundle"
bundle check || bundle install
bundle exec pod install --deployment --clean-install
git diff --exit-code -- Podfile.lock

--deployment 会在锁文件需要变化时失败,--clean-install 则降低旧安装状态掩盖问题的概率。最后的 git diff 是第二道门:即使某段脚本在安装过程中改写了锁文件,任务也不能继续进入编译。

如果仓库包含多个工作区,应让每个工作区有独立的安装目录和日志文件,不要在并发任务中共享同一个 Pods 目录。

隔离缓存,但不要盲目禁用缓存

缓存本身不是问题,边界不清才是问题。云端 Mac 上常见的污染路径是多个仓库共用 CocoaPods 下载缓存,或者两个任务同时读写同一个 Bundler 目录。建议以“锁文件摘要 + 工具版本”构造缓存键,并让写入动作只发生在安装成功之后。

缓存对象 建议缓存键 失效条件
Bundler 安装目录 Gemfile.lock 摘要 + Ruby 版本 任一输入变化
CocoaPods 下载缓存 Podfile.lock 摘要 + CocoaPods 版本 锁文件或工具变化
Pods 目录 Podfile.lock 摘要 + 工程配置摘要 Podfile、脚本或配置变化

不要缓存 Pods/Manifest.lock 后再覆盖新安装结果。它是当前沙箱实际安装状态的记录,应由本次安装生成。若缓存恢复后出现异常,先在隔离目录执行一次无缓存安装;只有无缓存结果正常,才能把问题归因到缓存。

在编译前验证依赖图

安装成功不等于依赖图正确。至少检查 Podfile.lock 与 Pods/Manifest.lock 是否一致,并确认没有未提交变化:

test -f Podfile.lock
test -f Pods/Manifest.lock

cmp -s Podfile.lock Pods/Manifest.lock || {
  echo "CocoaPods sandbox differs from Podfile.lock" >&2
  exit 1
}

git status --porcelain -- Podfile Podfile.lock Gemfile Gemfile.lock

最后一条命令有输出时应失败,而不是只打印警告。对于使用 :path 的本地 Pod,还要额外记录目标目录的提交号或内容摘要。否则锁文件不变,目录内容却能变化,可复现性仍然不存在。

为升级建立单独通道

依赖升级任务可以运行指定目标的 pod update SomePod,但必须输出三类差异:锁文件中的版本变化、SPEC CHECKSUMS 变化,以及新增或删除的传递依赖。升级提交只做依赖变更,不要同时混入业务代码,回退时才能直接恢复上一份锁文件。

保留失败现场并按顺序排查

失败后不要立即删除全部缓存。先保留 CocoaPods 版本、Ruby 版本、锁文件摘要、安装日志和 git status,再判断属于哪一层。

  1. 若 bundle check 失败,先核对 Ruby 与 Gemfile.lock 平台。
  2. 若严格安装要求改锁文件,检查是否有人修改了 Podfile 却漏交锁文件。
  3. 若只有缓存恢复任务失败,用独立目录执行无缓存安装并比较结果。
  4. 若 Manifest.lock 不一致,检查安装是否被中断,或多个任务是否共享 Pods。
  5. 若本地 Pod 内容漂移,给源码目录增加提交号或摘要校验。

稳定的 CocoaPods 流程最终应当很无聊:输入未变时,安装结果不变;输入变化时,差异进入代码审查;缓存损坏时,只影响速度,不改变依赖图。把这些检查放在 Xcode 编译之前,通常能用几十秒的失败替代数十分钟后的模糊报错。

常见问题

CI 中应该执行 pod install 还是 pod update?

日常 CI 只执行带 --deployment 的 pod install。pod update 会重新解析允许范围内的版本,应放在独立依赖升级任务中,并由代码审查确认 Podfile.lock 的变化。

已经提交 Podfile.lock,为什么不同任务仍可能得到不同结果?

常见原因包括 CocoaPods 或 Ruby 版本不同、本地开发 Pod 指向可变目录、缓存损坏,以及脚本在安装前修改 Podfile。锁文件必须与工具版本、缓存边界和安装命令一起治理。

Pods 目录需要提交到 Git 吗?

通常不需要。更稳妥的做法是提交 Podfile、Podfile.lock、Gemfile 与 Gemfile.lock,让 CI 从锁定输入恢复依赖;只有无法稳定取得源码的特殊依赖才需要另行评估。

独享物理节点

按任务周期选择 LemonVM 云端 Mac

Lemon M4 与 Lemon M4 Pro 覆盖新加坡、东京、首尔、香港和美国西部,支持日、周、月、季计费。

立即租用云端 Mac