同一台雲端 Mac 連續執行多個 CI 任務時,最難重現的故障往往不是程式碼錯誤,而是上一個任務留下的暫存檔案。舊的通訊端、未釋放的鎖定、寫入不完整的快取,或名稱重複的匯出目錄,都可能讓下一次建置在相同提交上產生不同結果。獨享實體節點雖能避免其他帳戶干擾,卻不會自動隔離同一帳戶內的並行任務,因此仍須由流水線明確建立檔案系統邊界。
先找出會跨任務洩漏的目錄
不要把「工作區已清空」視為環境已完全還原。macOS 工具會將狀態分散到工作區之外,排查時至少應檢查以下四類位置:
TMPDIR下的暫存檔案、通訊端與下載片段;- Xcode 的 DerivedData、索引與建置資料庫;
- Clang 模組快取及 SwiftPM 的任務級快取;
- 指令碼自行建立的匯出、日誌與測試附件目錄。
先在一次正常建置前後分別執行 env | sort,記錄實際使用的 HOME、TMPDIR 與工作區路徑。接著使用 du -sh 比較相關目錄的大小,而不是一開始就遞迴刪除。若故障只在並行執行時發生,應優先搜尋固定檔名、固定連接埠與固定輸出路徑。
隔離的目的不是每次刪除所有快取,而是讓每個任務明確擁有自己的可寫入目錄,並且只清理由自己建立的內容。
為每個任務建立唯一根目錄
任務識別碼應取自 CI 系統提供的唯一編號;若沒有可靠的編號,可由 mktemp 產生。不要只使用分支名稱,因為同一分支可能同時觸發多次建置。
#!/bin/bash
set -euo pipefail
job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"
export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"
mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"
cleanup() {
local status=$?
rm -rf "$job_root"
exit "$status"
}
trap cleanup EXIT INT TERM
mktemp 建立的目錄預設不會與其他任務重名。清理函式會先保存結束狀態碼,再刪除目錄並以原始狀態結束,避免測試失敗遭清理命令掩蓋。路徑變數必須加上引號,以免工作區名稱包含空格時被拆成多個參數。
不要盲目覆寫 HOME
將 HOME 指向任務目錄看似能徹底隔離,卻可能中斷目前使用者對 Keychain、工具偏好設定與已完成系統初始化的存取。一般編譯可針對特定套件管理工具指定獨立快取;涉及簽署時,通常應保留 HOME,只隔離建置產物與確定可移轉的快取。
明確接入 Xcode 的寫入路徑
僅設定 TMPDIR 並不會自動遷移 DerivedData。建置命令應將關鍵路徑以參數傳入,確保日誌能直接顯示本次任務使用的目錄。
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
COMPILER_INDEX_STORE_ENABLE=NO \
CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
test \
-resultBundlePath "$RESULT_BUNDLE"
若 CI 不需要程式碼索引,可停用索引儲存以減少無關寫入。測試結果應放在任務根目錄內;但若失敗後需要上傳附件,必須先完成封存,再觸發清理。更穩妥的流程是「執行測試—將診斷檔案複製到流水線產物區—驗證複製結果—結束」。
共享快取唯讀,任務快取可寫入
確實需要重複使用相依套件時,可將通過驗證的共享快取視為種子:任務開始時將其複製或還原到自己的目錄,建置期間只寫入任務副本,成功後再由獨立步驟更新共享版本。不要讓兩個 xcodebuild 程序同時寫入同一個模組快取或建置資料庫。
讓異常結束也能留下證據
直接使用 trap 'rm -rf ...' EXIT,會在失敗發生的瞬間刪除現場。實務上應先產生一份精簡的診斷清單,包括結束狀態碼、磁碟剩餘空間、目錄大小,以及尚未結束的相關程序,再決定要保留哪些檔案。不要在日誌中輸出完整的環境變數,因為其中可能包含權杖或簽署相關值。
可在清理前執行:
{
echo "exit_status=$status"
df -h "$job_root"
du -sh "$job_root"/* 2>/dev/null || true
find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"
ARTIFACT_DIR 應位於任務根目錄之外,並由流水線負責上傳與定期清理。若需要保留失敗現場,可先壓縮診斷目錄,再刪除原始目錄;同時必須對日誌與路徑進行去識別化處理。
透過殘留稽核驗證隔離是否生效
改造完成後,連續執行兩次相同提交,並在第二次開始前確認不存在與上一個任務識別碼對應的目錄。並行執行時,還應檢查兩個任務的 TMPDIR、DerivedData 與結果套件路徑是否完全不同。
驗收清單可固定為五項:每個任務都有唯一根目錄;所有 Xcode 路徑均明確指定;共享快取沒有並行寫入;成功與失敗時都會執行結束鉤子;診斷產物先複製、再清理。若磁碟用量仍持續增加,可根據任務識別碼反查尚未納入邊界的工具目錄,而不是擴大刪除範圍。
在 SureVM 的獨享實體 Mac 上,這套方法尤其適合長期運作的建置節點:保留機器層級的工具鏈,同時將每次任務的可變狀態限制在可追蹤、可刪除的目錄中。最終判斷標準很簡單——無論前一個任務成功、失敗或遭到中止,都不應改變下一個任務的輸入條件。
常見問題
為什麼不能讓所有 CI 任務共用系統暫存目錄?
系統暫存目錄可以作為上層目錄,但每個任務都應建立獨立子目錄。共用固定路徑容易造成並行任務互相覆寫,也可能讓失敗任務留下的鎖定檔與中間產物影響下一次建置。
隔離 TMPDIR 後還需要另外設定 DerivedData 嗎?
需要。Xcode 的 DerivedData、模組快取與暫存檔案不一定使用相同位置,必須透過參數或環境變數逐一指定,才能建立完整的任務邊界。
簽章任務應該覆寫 HOME 嗎?
通常不建議。簽章流程可能依賴目前使用者的 Keychain 與安全設定,較穩妥的方式是只隔離暫存目錄、建置目錄及可安全搬移的工具快取。
選擇獨享實體 Mac 節點,滿足建置、開發與實驗需求
比較兩種 Mac mini M4 配置、四種租期與五個可訂購節點,依照實際工作流程做出合適選擇。