SureVM 工程筆記

雲端 Mac CI 暫存目錄隔離:治理 TMPDIR、快取與結束清理

雲端 Mac CI 暫存目錄隔離:治理 TMPDIR、快取與結束清理

同一台雲端 Mac 連續執行多個 CI 任務時,最難重現的故障往往不是程式碼錯誤,而是上一個任務留下的暫存檔案。舊的通訊端、未釋放的鎖定、寫入不完整的快取,或名稱重複的匯出目錄,都可能讓下一次建置在相同提交上產生不同結果。獨享實體節點雖能避免其他帳戶干擾,卻不會自動隔離同一帳戶內的並行任務,因此仍須由流水線明確建立檔案系統邊界。

先找出會跨任務洩漏的目錄

不要把「工作區已清空」視為環境已完全還原。macOS 工具會將狀態分散到工作區之外,排查時至少應檢查以下四類位置:

先在一次正常建置前後分別執行 env | sort,記錄實際使用的 HOMETMPDIR 與工作區路徑。接著使用 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 與安全設定,較穩妥的方式是只隔離暫存目錄、建置目錄及可安全搬移的工具快取。

SureVM 雲端 Mac

選擇獨享實體 Mac 節點,滿足建置、開發與實驗需求

比較兩種 Mac mini M4 配置、四種租期與五個可訂購節點,依照實際工作流程做出合適選擇。

選擇租用方案