當同一台雲端 Mac 連續執行相依套件升級、專案遷移與破壞性指令碼時,最棘手的往往不是建置本身,而是如何可靠地回復到乾淨的工作區。重新簽出大型儲存庫會反覆讀取大量小型檔案,直接沿用舊目錄則可能帶回未追蹤檔案、產生的成品與錯誤的權限。如果工作目錄位於 APFS 卷宗上,可以先維護一份通過驗收的基線,再透過寫入時複製,為每項任務建立可修改的複本。
APFS 複製解決了什麼問題
APFS 複製一開始會讓兩個檔案共用底層資料區塊;只有其中一方發生寫入時,檔案系統才會為變更的部分配置新區塊。因此,包含原始碼、相依套件與靜態工具的基線,可以快速複製成獨立目錄,而後續修改不會寫回基線。
這與硬連結不同:複製後的檔案擁有各自獨立的目錄項目,修改複本不會改變原始檔案。這也不是備份,因為基線與複本仍位於同一個卷宗。若卷宗損毀、基線遭到誤刪,或整台機器的資料遺失,複製無法提供異地復原能力。
請將 APFS 複製視為「以低成本建立工作複本」的機制,而不是快取命中保證、快照系統或資料保護方案。
du -sh 顯示的是目錄的邏輯大小,無法直接反映複製實際新增了多少實體空間。觀察容量變化時,應同時檢查 APFS 卷宗的剩餘空間,並在任務持續寫入時設定清理門檻。
先建立一份可驗證的基線
基線不應只是某次建置結束後隨手留下的目錄。請先簽出固定的提交、還原相依套件,再清除所有僅屬於單次任務的狀態。至少應確認工作樹乾淨、子模組位置正確,並記錄基線提交。
set -euo pipefail
ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
mkdir -p "$ROOT"
git -C "$BASE" status --porcelain
git -C "$BASE" submodule status --recursive
git -C "$BASE" rev-parse HEAD > "$BASE/.baseline-revision"
test -z "$(git -C "$BASE" status --porcelain)"
.baseline-revision 本身會成為未追蹤檔案,因此應將它存放在儲存庫外,或預先加入團隊認可的忽略規則。更穩妥的配置是由 $ROOT/metadata 儲存提交編號,讓 $BASE 始終能通過乾淨工作樹檢查。
哪些內容不應放入基線
不要將 DerivedData、測試結果套件、封存檔案、暫時性鑰匙圈、執行記錄與使用中的通訊端放入基線。這些內容通常包含絕對路徑、程序狀態或任務專屬憑證。是否將相依套件目錄納入基線,則取決於其內容能否由鎖定檔案確定,以及安裝指令碼是否會寫入機器層級的路徑。
基線也應只允許更新任務寫入。一般建置帳戶可以讀取基線,但不應在其中執行建置命令,否則一次誤操作就可能污染之後的所有複本。
為每項任務建立獨立複本
請先確認根目錄確實位於 APFS,再使用任務編號產生唯一目錄。目標路徑必須不存在,任務編號也必須限制為安全字元,避免將外部輸入直接拼接到刪除路徑中。
set -euo pipefail
ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
JOB_ID="${BUILD_ID:?BUILD_ID is required}"
case "$JOB_ID" in
*[!A-Za-z0-9._-]*) exit 64 ;;
esac
test "$(stat -f %T "$ROOT")" = "apfs"
WORK="$ROOT/jobs/$JOB_ID"
DERIVED="$ROOT/derived/$JOB_ID"
test ! -e "$WORK"
mkdir -p "$ROOT/jobs" "$DERIVED"
cp -cR "$BASE" "$WORK"
xcodebuild \
-workspace "$WORK/App.xcworkspace" \
-scheme App \
-derivedDataPath "$DERIVED" \
build
cp -cR 會要求以複製方式拷貝,但實際行為仍取決於來源路徑、目標路徑與檔案系統。來源目錄與目標目錄必須位於同一個 APFS 卷宗;如果跨卷宗執行,就不應將它視為寫入時複製流程。部署前,可以使用包含大型檔案的測試目錄,觀察複製耗時與卷宗容量的變化。
隔離並行任務與可變狀態
工作區彼此獨立,不代表所有狀態都已隔離。建置工具仍可能讀寫使用者目錄下的快取、暫存目錄與設定檔。至少應為每項任務配置獨立的 DerivedData 與結果目錄,並避免多項任務共用同一組模擬器裝置或相同的輸出路徑。
為並行數量設定容量預算
複本剛建立時新增的空間用量較少,但編譯物件、相依套件重寫與封存作業會迅速產生新區塊。並行上限應依最壞情況下的可寫入增量決定,而不是依基線的邏輯大小決定。可以在啟動任務前檢查卷宗空間,並在建置結束後記錄變化:
df -h "$ROOT"
du -sh "$DERIVED" "$WORK"
如果任務會重寫大量相依套件檔案,複製所帶來的空間優勢會逐漸降低。此時應將不可變的相依套件保留在基線中,並把頻繁變動的產生目錄移至任務專屬路徑,而不是為了維持複製比例而共用可寫入快取。
安全回收並定期重建基線
清理指令碼的首要目標不是「刪得快」,而是絕不超出任務根目錄。刪除前應同時驗證路徑前綴、目錄是否存在及任務編號;不要對可能為空的變數直接執行遞迴刪除。
set -euo pipefail
ROOT="$HOME/ci-workspaces"
WORK="$ROOT/jobs/${BUILD_ID:?BUILD_ID is required}"
DERIVED="$ROOT/derived/${BUILD_ID:?BUILD_ID is required}"
case "$WORK" in
"$ROOT"/jobs/*) rm -rf -- "$WORK" ;;
*) exit 64 ;;
esac
case "$DERIVED" in
"$ROOT"/derived/*) rm -rf -- "$DERIVED" ;;
*) exit 64 ;;
esac
更新基線時應使用新目錄完成:簽出目標提交、安裝具確定性的相依套件、執行驗收,再取代目前的基線。不要直接在現有基線上原地拉取更新後繼續使用,因為更新一旦中斷,就會留下無法判斷的新舊混合狀態。
最終檢查包含四項:基線提交可追溯,工作樹保持乾淨;每項任務都有唯一的工作區與產生目錄;卷宗剩餘空間足以涵蓋最大並行寫入量;清理邏輯只接受受控路徑。滿足這些條件後,APFS 複製才真正成為可重複執行的工程步驟,而不只是一項看似快速的複製技巧。
常見問題
APFS 複製能取代建置快取或備份嗎?
不能。它只適合在同一 APFS 卷內快速建立可修改副本;檔案被改寫後仍會增加實體用量,重要資料也必須另行備份。
為什麼基線不應包含 DerivedData?
DerivedData 會保存絕對路徑、索引與任務狀態,跨任務重用可能造成不穩定結果。每次建置應使用獨立的 DerivedData 目錄。
在雲端 Mac 上繼續驗證這套工作流程
從三種 Apple Silicon 設定中選擇合適的記憶體、儲存空間、節點與租用週期。