同一个提交在开发机编译通过,换到云端 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,再判断属于哪一层。
- 若
bundle check失败,先核对 Ruby 与Gemfile.lock平台。 - 若严格安装要求改锁文件,检查是否有人修改了
Podfile却漏交锁文件。 - 若只有缓存恢复任务失败,用独立目录执行无缓存安装并比较结果。
- 若
Manifest.lock不一致,检查安装是否被中断,或多个任务是否共享Pods。 - 若本地 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 覆盖新加坡、东京、首尔、香港和美国西部,支持日、周、月、季计费。