同一筆提交在開發機上順利通過,換到另一台雲端 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 resolve 與 swift 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 -p、xcodebuild -version 與 uname -m。若涉及二進位依賴,還要確認其中的切片包含目前架構,並符合專案設定的最低系統版本。刪除對應快取鍵的本機副本後重新建置,即可區分快取損壞與原始碼不相容。
提交前檢查清單
- 儲存庫中只有一份實際生效的
Package.resolved。 - 建置命令已啟用
-onlyUsePackageVersionsFromResolvedFile。 - 快取鍵包含架構、Xcode 建置資訊與解析檔摘要。
- 每個節點都使用各自獨立且可寫入的工作目錄。
- 建置前後的解析檔摘要一致。
- 記錄中保留提交編號、快取鍵與工具鏈版本,但不記錄憑證。
常見問題
Package.resolved 應該提交到版本庫嗎?
應用程式與可部署服務通常應提交,建置時也應限制只能使用解析檔內的版本。函式庫可依發布策略決定,但仍需在乾淨環境驗證宣告的版本範圍。
多台雲端 Mac 能共用同一個 SwiftPM 快取目錄嗎?
不應讓多台節點同時寫入同一目錄。請以處理器架構、Xcode 建置版本及解析檔摘要分隔快取,每台節點使用自己的工作副本。
解析成功但編譯失敗時該先排查哪裡?
先核對 Xcode 路徑與版本,再重建目前快取鍵的工作副本,並檢查二進位依賴架構、最低系統版本及解析檔是否遭到改寫。
讓可重現的環境固定在同一台雲端 Mac 上
選擇固定的機型、地區與租用期間,透過完整的 macOS 圖形介面與命令列完成開發或自動化工作。