某個 SSH 工作突然回報 Xcode 26 build database locked,而另一個建置工作仍在同一台遠端 Mac 上執行。
最快解法:先停止共用建置目錄的併行任務,替每個 Job 分配獨立的 DerivedData 路徑;確認沒有殘留 xcodebuild 後仍失敗,才清理受影響專案的建置資料庫,最後用單一任務 Archive 復驗。
這篇文章適合以下情況:
- 你透過 SSH 在遠端 Mac 執行
xcodebuild,斷線或重試後遇到資料庫鎖定。 - 你的小型團隊讓 CI 同時執行同一專案的 Build、Test 或 Archive。
- 你負責維護一台共用 Mac,需要控制工作目錄、程序與輸出產物。
失敗現場與證據基線
不要只複製錯誤紀錄最後一行。build database locked 只能說明目前建置程序無法取得資料庫使用權,不能直接證明是 Xcode 快取損壞。先保留完整命令、工作目錄、程序時間線和第一個有效錯誤。
例如,脫敏後的紀錄應至少包含以下資料:
[09:14:02] Job-A started from /Users/runner/work/app-a
xcodebuild -workspace Sample.xcworkspace \
-scheme Release \
-configuration Release \
-derivedDataPath /Users/runner/build/job-a/DerivedData \
-archivePath /Users/runner/artifacts/job-a/App.xcarchive \
archive
[09:14:05] Job-B started from /Users/runner/work/app-a
xcodebuild -workspace Sample.xcworkspace \
-scheme Release \
-configuration Release \
-derivedDataPath /Users/runner/build/shared/DerivedData \
-archivePath /Users/runner/artifacts/job-b/App.xcarchive \
archive
[09:14:08] error: database is locked
同時記下這次是 Build、Test 還是 Archive;由主 xcodebuild、Run Script、套件建置工具或其他子程序發起。Apple 將建置系統、Scheme 與 Target Dependencies 分開管理,因此不能把所有鎖定都歸咎於同一類快取問題。可先對照 Apple 的 Xcode Build System 說明 與 Xcode 26 Release Notes。
| 先確認的項目 | 證據入口 | 不能直接下的結論 |
|---|---|---|
完整 xcodebuild 命令 |
CI 紀錄、SSH shell 歷史、工作紀錄 | 不能只因錯誤含 database 就刪除全部 DerivedData |
| 工作目錄與輸出路徑 | -derivedDataPath、-archivePath、Build Settings |
命令列與圖形介面設定可能最後解析到相同位置 |
| 啟動程序與父子關係 | ps、pgrep、程序啟動時間 |
SSH 中斷不代表遠端程序已經結束 |
| 第一個有效錯誤 | 原始建置紀錄、.xcresult |
最後一行可能只是上游衝突的結果 |
注意: 專案名稱、Scheme、使用者名稱、主機位址、Bundle ID、Team ID 與路徑都應先脫敏。排障紀錄若包含簽署秘密或 App Store Connect 權杖,不應直接貼到團隊頻道。
併行程序:先找真正應該留下的工作
同一台遠端 Mac 上,常見的併行來源不只是在介面上重複按下建置:
- CI 重試已經失敗的工作,但原工作尚未取消。
- SSH 連線中斷,遠端的
xcodebuild仍繼續執行。 - 一個人執行 Archive,另一個人同時執行 Test。
- 父層
xcodebuild啟動了測試或腳本子程序,維護者誤以為只有一個工作。 - 不同 CI 工作使用相同的工作目錄或共享 Runner。
可以先在遠端 Mac 執行:
pgrep -alf 'xcodebuild|XCBBuildService|xctest'
ps -axo pid,ppid,lstart,stat,command | grep -E 'xcodebuild|XCBBuildService|xctest'
這些命令只用來建立證據,不是叫你無條件終止所有相關程序。你要把程序 PID、PPID、啟動時間、命令列和寫入中的輸出互相比對,判斷哪一個 Job 應保留。
優先順序通常是:先讓尚未失去進度的 Archive 或 Test 正常結束,再處理已重試且沒有新輸出的工作;若某個程序只是父程序,必須先確認它的子程序是否仍在產生測試結果或 xcresult。直接 kill 可能造成測試結果不完整、xcarchive 未封存完成、上傳工作中斷,甚至讓 CI 將一次可恢復的工作誤判為失敗。
| 處理選項 | 適合條件 | 代價與驗收 |
|---|---|---|
| 等待正常退出 | 程序仍有輸出,Archive 或 Test 正在推進 | 保留結果的機會最高;要持續觀察輸出與程序狀態 |
| 終止特定 Job | 已確認它是重試工作、沒有新輸出,且不再需要其結果 | 可能留下不完整的 xcresult;必須標記該 Job 已取消 |
| 終止整個程序樹 | 明確確認父子程序均屬同一個失控工作 | 可能連帶中斷測試、封存或上傳;不能作為第一個修復動作 |
| 立即清除全部快取 | 沒有合理證據支持此動作 | 會擴大診斷範圍,還可能刪除仍有價值的中間產物 |
DerivedData 與輸出路徑
兩個 Job 是否能共用 DerivedData,不能只看它們是否來自同一份原始碼。當兩個程序同時更新相同建置資料庫、模組快取或中間產物時,共用目錄就是高風險邊界;在併行 CI 中,應為每個 Job 建立獨立且可追蹤的路徑。
除了 -derivedDataPath,還要核對以下位置:
OBJROOT:物件檔與中間建置內容。SYMROOT:Build Products 和相關符號輸出。-archivePath:最終的xcarchive。-resultBundlePath:測試與建置結果,例如xcresult。- 腳本自行指定的輸出目錄。
- 圖形介面 Scheme 或工作區設定實際解析出的 Build Settings。
Apple 的 Build Settings Reference 可用來核對設定名稱與作用範圍;設定 Target Build Settings 的官方說明 則適合用來確認 Target 層級是否覆蓋了工作區或 CI 傳入值。
可採用這類命名方式:
JOB_ROOT="$PWD/.ci/jobs/${CI_JOB_ID}"
mkdir -p "$JOB_ROOT"
xcodebuild \
-workspace Sample.xcworkspace \
-scheme Release \
-configuration Release \
-derivedDataPath "$JOB_ROOT/DerivedData" \
-archivePath "$JOB_ROOT/Archive/App.xcarchive" \
-resultBundlePath "$JOB_ROOT/Results/App.xcresult" \
archive
實際 CI 變數名稱依你的系統而定,重點不是複製變數,而是讓每個 Job 的中間產物、封存檔與結果包都能透過同一個 Job 識別碼追蹤。兩個 Job 同時執行時,通過標準應是:兩者都沒有寫入相同的 DerivedData 和 Archive 路徑,並且各自產生可以獨立開啟與檢查的結果包。
最終 Artifacts 可以集中保存,但應在工作完成後才集中;不要為了共用輸出而讓建置中的中間產物共用目錄。既有 xcarchive、dSYM 和簽署資產的保存方式,也應參照 Apple 的簽署 Distribution Code 說明 重新核對。
| 路徑類型 | 併行 Job 是否應獨立 | 建議保存方式 |
|---|---|---|
| DerivedData | 是 | 以 Job 識別碼建立專屬目錄 |
| OBJROOT、SYMROOT | 是 | 不要讓不同 Job 指向同一個中間產物目錄 |
xcresult |
是 | 每個 Job 產生獨立結果包,再集中上傳 |
xcarchive |
是 | 使用唯一 Archive 路徑,驗證完成後保存 |
| dSYM 與發布 Artifacts | 建置期間獨立,完成後可集中 | 以 Commit、版本或 Job 識別碼歸檔 |
隱藏巢狀建置
如果程序清單沒有兩個明顯的頂層 xcodebuild,下一個方向是檢查主建置內是否又啟動了建置命令。常見位置包括 Run Script、跨專案 Target Dependencies、Swift Package 建置工具與發布腳本。
先搜尋腳本和 CI 設定:
grep -R "xcodebuild" Scripts .github ci 2>/dev/null
grep -R "archive\|build-for-testing\|test-without-building" Scripts .github ci 2>/dev/null
接著回答三個問題:
- 子建置是否真的需要獨立的 Scheme 或工作目錄?
- 父程序與子程序各自負責哪些輸出?
- 子建置完成後,父程序是否仍會重新寫入同一個 DerivedData?
不要把「關閉平行建置」當成預設修復。它可能只是讓競爭出現得較少,卻沒有消除路徑或腳本邊界問題。Apple 的 Run Script 建置文件 說明了腳本輸入與輸出宣告;Scheme 自訂文件 則可協助你確認 Build、Test、Profile 與 Archive 動作的實際組合。
修改腳本前,先把子任務的輸出改到專屬目錄,並保留原本的回退方式;修改後重新執行並檢查建置紀錄,確認同一個 Scheme 不再被父子任務重複啟動。若腳本只是為了產生一份可由父建置取得的檔案,優先改成明確的輸入輸出依賴,而不是在腳本內重新呼叫完整建置。
無併行程序時的局部清理
只有在確認相關 xcodebuild、測試程序、索引程序與腳本子程序都已退出後,才處理殘留或損壞狀態。清理要由小到大:
- 先備份原始建置紀錄、
.xcresult、xcarchive、dSYM 和 CI 輸出。 - 只移除發生鎖定的 Job 臨時目錄。
- 若仍失敗,再移除該專案專屬的 DerivedData。
- 重新建立工作區需要的輸出目錄。
- 使用完全相同的
xcodebuild命令做對照測試。 - 只有在專案專屬清理仍無法恢復時,才評估更廣泛的使用者快取。
刪除 DerivedData 不等於刪除既有 Archive 或簽署憑證,但風險在於你的腳本可能把 Archive、dSYM 或其他發布檔案放在 DerivedData 之下。因此,不能只根據目錄名稱判斷可刪除範圍。
提醒: 清理後若命令成功,仍不能立即宣布修復。你需要把「清理快取」與「隔離併行工作」分開驗證,否則下一次 CI 重試仍可能在相同共享路徑再次觸發鎖定。
復原驗收與 FAQ
驗收應從最小範圍逐步恢復,而不是清理後立刻把所有工作重新開啟:
- [ ] 單一 Job 執行 Build,確認新 DerivedData 路徑能完整建立。
- [ ] 單一 Job 執行 Test,確認
xcresult位置與內容正常。 - [ ] 兩個 Job 併行執行,確認 DerivedData、Archive 和結果包均不相同。
- [ ] 執行一次真實 Archive,確認
xcarchive與 dSYM 都被保存。 - [ ] 中斷 SSH 連線後重新登入,確認遠端工作狀態可查、輸出仍留存。
- [ ] 取消一個 Job,確認另一個 Job 不會被連帶終止。
- [ ] 重啟遠端 Mac 後,確認舊程序不會被錯誤重用,目錄可重新建立。
- [ ] 檢查 CI 是否把失敗重試限制在原 Job,而不是複製一個仍在執行的工作。
測試結果的判讀可參考 Apple 的測試與結果解讀文件;若專案含有 Swift Package,也應檢查 Apple 的持續整合建置說明,確認套件建置沒有另外使用共享目錄。
若問題只在共享 Runner 出現,優先修正 Job 路徑與取消邏輯;若每次 SSH 斷線後都留下無法控制的程序,應檢查工作啟動方式與遠端工作管理;若主機無法提供獨立工作目錄、完整程序控制或穩定常駐執行,再考慮拆分專用 Runner。
對於現有的遠端 Mac 環境,你可以先參考 遠端 Mac 使用情境,把「併行 Job、斷線重連、主機重啟」列為部署前驗收項目。若目前的主機必須由多人共用、工作目錄無法隔離,或重試機制經常讓舊程序留存,這些限制比單次快取損壞更值得處理;此時查看 KVMFLUX 的遠端 Mac 方案 會比反覆刪除快取更接近根因。
自購 Mac 適合長期穩定重負載、需要實體 USB 裝置或必須完全掌握硬體的人;但共用現有主機常見的缺點是路徑互相覆寫、程序權限難以分隔,以及 SSH 斷線後缺少可靠的恢復邏輯。若你只需要臨時建置、測試獨立的 iOS 打包流程,或希望快速取得可常駐的遠端 Mac,租用 KVMFLUX 能把獨立工作目錄與持續運行環境納入部署考量,而不必先為一台專用打包機承擔完整硬體成本。