云端 Mac 的 SwiftPM 依赖锁定、缓存隔离与可复现构建

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

云端 Mac 的 SwiftPM 依赖锁定、缓存隔离与可复现构建

同一提交在开发机通过,换到另一台云端 Mac 却解析出不同依赖,通常不是编译器随机失效,而是解析文件、Xcode 版本和缓存边界没有一起固定。短租节点尤其容易暴露这个问题:机器交付时目录是新的,但流水线脚本可能默认复用旧缓存,也可能在构建过程中悄悄改写依赖版本。

先定义什么叫同一次构建

可复现不等于每次都保留整个工作目录,而是相同输入必须得到相同依赖集合。至少记录四项:代码提交、Package.resolved 内容、Xcode 构建版本和处理器架构。只记录 Xcode 的大版本不够,同一大版本下的工具链补丁也可能不同。

先在节点上保存环境快照:

set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD

应用工程通常应提交 Package.resolved。工作区常见路径是 App.xcworkspace/xcshareddata/swiftpm/Package.resolved;仅有工程文件时,也可能位于工程内部的工作区目录。不要同时维护两份解析文件,应先确认流水线实际读取哪一份。

判断标准很直接:如果构建脚本执行后 git status --porcelain 出现解析文件变更,本次构建就不能视为对原提交的验收。

固定解析结果而不是只固定版本范围

Package.swift 中的版本范围表达“允许选择什么”,Package.resolved 才记录“本次实际选择什么”。节点首次准备时可以显式解析,正式构建则只接受已经记录的版本。

set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$CACHE"

git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=macOS' \
  -clonedSourcePackagesDirPath "$CACHE" \
  -onlyUsePackageVersionsFromResolvedFile \
  build

如果目标是 iOS,应把 destination 改为流水线已经定义的通用设备或模拟器目标,不要让脚本自动选择当前启动的设备。纯 Swift 包可以用 swift package resolveswift build,但工程与工作区应统一走 xcodebuild,避免两套解析入口产生不同目录状态。

用输入摘要划分缓存边界

缓存的目的只是减少下载与检出时间,不能成为依赖版本的来源。一个稳妥的缓存键应包含架构、Xcode 构建信息和解析文件摘要:

set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
输入变化 是否复用工作缓存 原因
仅业务源码变化 可以 依赖集合未变化
Package.resolved 变化 不可以 检出版本或修订已变化
Xcode 构建版本变化 不可以 工具链与产物格式可能变化
arm64 与其他架构切换 不可以 二进制产物不能混用

多节点执行时,不要让几台机器并发写同一个目录。可以保存按键归档的只读缓存,再由每台物理节点复制为本地工作目录。失败重试也应先清理当前工作副本,而不是删除所有历史归档。

让依赖漂移直接终止任务

构建前后各检查一次仓库状态,可以同时发现解析文件被改写和脚本生成物误入源码目录。正式任务应在干净检出中启动:

set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
  -clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
  -onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"

不要在失败后立即取消版本限制并重新解析,这会把配置错误伪装成成功。正确做法是保留解析日志、环境快照和缓存键,再判断是仓库缺少锁定结果、依赖修订失效,还是节点选错了 Xcode。

按顺序排查常见失败

解析阶段超时

先检查节点能否访问依赖源,再确认磁盘空间和缓存目录权限。网络恢复后使用同一解析文件重试,不要先升级依赖。若每次都在同一包失败,应单独验证该依赖声明与修订是否仍然匹配。

解析成功但编译失败

先比较两台节点的 xcode-select -pxcodebuild -versionuname -m。涉及二进制依赖时,还要确认其切片包含当前架构,并满足工程设置的最低系统版本。删除对应缓存键的本地副本后重建,可以区分缓存损坏与源码不兼容。

提交前检查清单

  1. 仓库中只有一份实际生效的 Package.resolved
  2. 构建命令启用了 -onlyUsePackageVersionsFromResolvedFile
  3. 缓存键包含架构、Xcode 构建信息和解析文件摘要。
  4. 每个节点使用独立可写工作目录。
  5. 构建前后解析文件摘要一致。
  6. 日志保留提交号、缓存键和工具链版本,不记录凭据。

常见问题

Package.resolved 是否应该提交到代码仓库?

应用与可部署服务通常应该提交,并在构建时启用只使用解析文件中的版本。可复用库是否提交可按团队策略决定,但发布验收必须在干净目录中重新解析并测试支持的版本范围。

多台云端 Mac 可以直接共享同一个 SwiftPM 缓存目录吗?

不建议让多台机器并发写入同一目录。应按处理器架构、Xcode 构建版本和 Package.resolved 摘要生成缓存键,每台节点使用独立工作副本,只共享只读归档。

依赖解析成功但编译失败时先检查什么?

先确认实际使用的 Xcode 路径与构建版本,再删除该缓存键对应的工作副本重新解析;随后核对二进制依赖架构、最低系统版本以及 Package.resolved 是否在构建前被改写。

独享物理节点

把可复现环境保留在同一台云端 Mac

选择固定机型、区域和租期,使用完整 macOS 图形界面与命令行完成开发或自动化任务。

立即租用云端 Mac