在雲端 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 建置版本及解析檔摘要分隔快取,每台節點使用自己的工作副本。

解析成功但編譯失敗時該先排查哪裡?

先核對 Xcode 路徑與版本,再重建目前快取鍵的工作副本,並檢查二進位依賴架構、最低系統版本及解析檔是否遭到改寫。

獨享實體節點

讓可重現的環境固定在同一台雲端 Mac 上

選擇固定的機型、地區與租用期間,透過完整的 macOS 圖形介面與命令列完成開發或自動化工作。

立即租用雲端 Mac