工程紀錄

雲端 Mac 平行 Xcode 建置的 DerivedData 鎖定衝突治理

雲端 Mac 平行 Xcode 建置的 DerivedData 鎖定衝突治理

雲端 Mac 平行 Xcode 建置的 DerivedData 鎖定衝突治理

同一台雲端 Mac 同時執行兩條 Xcode 流水線時,偶爾出現的 database is locked、索引資料庫損壞,或「本機無法重現」的連結錯誤,通常不是程式碼變更所致。首先應確認兩個任務是否共用了預設的 ~/Library/Developer/Xcode/DerivedData。這個目錄不只是快取,還包含建置資料庫、索引、模組快取,以及任務持續改寫的中間產物;若將它視為可供多個任務同時讀寫的共用快取,發生失敗只是時間問題。

先確認衝突來自哪裡

不要一看到失敗就立刻刪除所有快取。應先保存失敗任務的完整 xcodebuild 命令、開始時間、工作目錄與程序清單,再搜尋日誌中的 lockeddatabaseunable to attachmalformed,以及重複產物相關訊息。

pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log

如果兩個任務的 -derivedDataPath 相同,或兩者都沒有明確設定此參數,就已經找到最主要的風險。也要檢查流水線逾時後,是否只終止了外層指令碼,卻留下 xcodebuild 或編譯子程序繼續寫入目錄。

重試成功並不能證明問題已經解決。競爭時間窗縮短後,第二次建置可能剛好通過,但共用寫入的關係依然存在。

區分鎖定衝突與一般編譯失敗

原始碼編譯錯誤通常會穩定地出現在同一個檔案與行號;鎖定衝突則更常隨平行執行的時序而變化,並隨機出現在解析相依套件、產生模組或連結階段。若停用平行任務後可連續成功,而恢復平行執行後又失敗,應優先稽核目錄的所有權,而不是修改業務程式碼。

為每個任務配置獨立工作區

可靠的做法,是讓儲存庫簽出目錄、DerivedData、結果套件與暫存目錄都包含任務的唯一識別碼。此識別碼可取自流水線任務編號;在本機驗證時,則可退回使用程序編號。

set -euo pipefail

JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"

mkdir -p "$DERIVED_DATA"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -resultBundlePath "$RESULT_BUNDLE" \
  build-for-testing

test -d "$RESULT_BUNDLE"

路徑必須由任務建立,也只能由該任務刪除。不要只用專案名稱作為唯一目錄名稱,因為同一專案的兩個分支仍可能發生碰撞。清理指令碼也不應使用模糊比對,例如刪除所有名稱中包含 App 的 DerivedData。

將相依套件解析與編譯分開

隔離 DerivedData 後,如果多個任務仍同時修改同一個相依套件簽出目錄,問題就會轉移到套件解析階段。應先執行相依套件解析,再於正式建置時禁止自動變更相依版本。儲存庫應提交經過審核的鎖定檔,流水線不得在未明確告知的情況下更新它。

PACKAGES="$ROOT/SourcePackages"

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

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "$PACKAGES" \
  -disableAutomaticPackageResolution \
  build

若需要快取,應快取下載結果或經過驗證的不可變快照,而不是讓多個任務寫入同一個使用中的目錄。快取鍵至少應包含鎖定檔摘要、Xcode 版本與目標架構。命中快取後,仍須驗證鎖定檔沒有發生變化。

建立可明確判定的驗收表

不能只憑一次成功結果就判定修復完成。應使用同一筆提交同時啟動多輪任務,並核對目錄、程序與產物的邊界。

| 檢查項目 | 通過條件 | 失敗時的處理方式 | |---|---|---| | DerivedData 路徑 | 每個平行任務均為唯一 | 修正任務識別碼與目錄拼接方式 | | 相依套件鎖定檔 | 建置前後摘要一致 | 禁止自動解析並檢查指令碼 | | 殘留程序 | 任務結束後沒有隸屬於該任務的編譯程序 | 修正逾時與結束訊號的處理方式 | | 結果套件 | 每個任務均獨立產生且可讀取 | 分離 `resultBundlePath` | | 清理範圍 | 僅刪除目前任務的根目錄 | 移除萬用字元與共用目錄刪除操作 |

建議至少同時執行兩條完全相同的建置,再執行一組來自不同分支的建置。前者用來驗證寫入隔離,後者則用來找出工作目錄、模組快取或產物路徑是否仍依專案名稱共用。

保留證據並安全回收

失敗的任務不應立刻銷毀現場。應先封存建置日誌、結果套件、Xcode 版本、鎖定檔摘要、磁碟剩餘空間與任務路徑;原始碼、環境變數及日誌中的權杖必須先經過遮蔽處理。成功的任務則可在確認產物已複製到獨立的發佈目錄後進行清理。

清理作業最好綁定任務根目錄,並加入路徑前綴驗證。若任務遭到強制終止,後續回收程式應依任務識別碼處理遺留目錄,而不是清空整台機器的開發者目錄。在 NUMACS 上執行多條流水線時,同樣應先在控制台確認目前可選的配置,再依平行任務數量設定任務上限;提高平行度無法取代寫入隔離。

最終目標不是讓錯誤「較少發生」,而是建立清楚的不變條件:每個平行任務只有一個寫入者、建置期間的相依版本保持不變、失敗現場可以追蹤,而且清理操作不會影響其他任務。滿足這四點後,DerivedData 才能從隨機故障來源恢復為可管理的建置資料。

常見問題

每個平行工作都需要獨立 DerivedData 嗎?

需要。只要工作可能同時寫入,就應使用 -derivedDataPath 指向獨立目錄;只有完全依序執行的工作才能安全重用路徑。

隔離 DerivedData 後還能保留快取效益嗎?

可以。依賴下載與驗證過的唯讀產物可分層重用,會被 Xcode 持續改寫的建置資料則維持工作級隔離。

遇到 database is locked 時直接重試可以嗎?

不建議。重試不會消除競爭條件,應先移除共用寫入路徑,並確認沒有殘留的 xcodebuild 程序。

NUMACS 雲端 Mac

將建置工作交給獨享實體工作站

兩種 Apple Silicon 配置皆為獨享實體機,並非虛擬機器,可按日、週、月或季租用,涵蓋新加坡、東京、首爾與香港節點。

選擇裝置並下單