程式碼沒有變更,雲端 Mac 上的第二次 Xcode 建置卻仍編譯大量目標;同一個提交換到新的工作區後,又恢復正常。這類問題不一定源自 DerivedData,也可能是原始碼、產生的檔案或快取產物帶有錯誤的修改時間。僅清除快取可以暫時消除症狀,但如果同步與還原流程沒有改變,時間戳漂移仍會再次進入工作區。
先確認是否為時間戳問題
先固定提交、Scheme、Xcode 路徑與建置參數,連續執行兩次相同的建置。第一次允許產生完整的中間產物,第二次則觀察哪些任務仍在執行。若同一批 Swift、資源或指令碼階段反覆執行,再比較乾淨工作區與重複使用工作區的結果。
排查時不要只看建置總時間。應重點記錄重複執行的輸入路徑、指令碼輸出與產生目錄,並檢查建置開始前是否有工具改寫檔案。常見跡象包括:
- 檔案修改時間晚於目前系統時間;
- 快取解壓縮後,輸出比原始碼更新或更舊;
- 產生指令碼每次執行都覆寫內容相同的檔案;
- 同步工具保留了另一台機器上的異常時間;
- 工作區所在磁碟區的時間正常,但封存檔內的中繼資料不一致。
增量建置依賴的不只是檔案內容。輸入與輸出的時間關係一旦失真,建置系統就可能持續將已完成的任務判定為需要重做。
掃描未來時間與異常目錄
先檢查系統時間與時區,再掃描修改時間比目前時間晚五分鐘以上的檔案。五分鐘的容許範圍可以避開極短暫的擷取誤差,同時足以找出明顯的時間漂移。
#!/bin/zsh
set -euo pipefail
root="${1:-$PWD}"
limit=$(( $(date +%s) + 300 ))
find "$root" -type f -print0 |
while IFS= read -r -d '' file; do
modified=$(stat -f '%m' "$file")
if (( modified > limit )); then
printf '%s\t%s\n' \
"$(date -r "$modified" '+%Y-%m-%d %H:%M:%S %z')" \
"$file"
fi
done
掃描範圍至少應涵蓋原始碼、專案檔案、指令碼、資源、程式碼產生目錄,以及還原後的快取。不要一開始就掃描整個使用者目錄,否則套件管理器快取、日誌與系統檔案會產生大量雜訊。對掃描命中的檔案繼續執行 stat -x 檔案路徑,核對修改時間、變更時間與所屬磁碟區。
如果異常集中在單一目錄,通常可以直接將範圍縮小到某個下載、解壓縮、同步或程式碼產生步驟;如果異常散布在整個儲存庫,則應檢查工作區的複製方式,以及任務開始前執行的初始化指令碼。
依來源判斷修復方式
不同來源需要採取不同的處理方式,不能一律使用 touch 掩蓋問題。
| 現象 | 常見來源 | 建議處理 |
|---|---|---|
| 少量原始碼時間位於未來 | 壓縮檔或同步來源異常 | 重新取得檔案,並核對來源機器的時間 |
| 產生的檔案每輪都更新時間 | 產生器無條件覆寫 | 僅在內容變更後進行原子替換 |
| 快取目錄的時間關係混亂 | 還原時保留了錯誤的中繼資料 | 捨棄該批快取並重建快取鍵 |
| 整個工作區的時間接近複製時刻 | 複製參數改變了中繼資料 | 統一取出方式,不要混用多種同步策略 |
| 輸出早於輸入且長期不變 | 快取封裝內部時間錯誤 | 重新產生並驗證快取清單 |
對於由 Git 管理的原始碼,最穩妥的修復方式通常是先確認沒有未提交的變更,再重新取出受影響的目錄,而不是重寫整個儲存庫的時間。對於產生的檔案,應讓產生器先寫入暫存檔,比較內容後再替換:
generate_config > Config.generated.swift.tmp
if ! cmp -s Config.generated.swift.tmp Config.generated.swift; then
mv Config.generated.swift.tmp Config.generated.swift
else
rm Config.generated.swift.tmp
fi
如此一來,內容未變更時,原始檔案的修改時間也不會改變,依賴該檔案的編譯任務便不會被無意義地喚起。
檢查快取與同步邊界
還原快取前,應先明確定義哪些目錄可以跨任務重複使用。建置產物、模組快取與套件管理器快取的生命週期不同,不應封裝成一個無法追蹤的大型檔案。快取清單至少應記錄快取鍵、Xcode 版本、架構、產生命令與檔案數量;還原後先執行時間戳預檢,再允許建置進入主要階段。
同步工具也必須明確設定是否保留修改時間。保留時間有利於增量判斷,但來源機器的時間若有錯誤,也會原樣傳播;不保留時間則可能讓所有檔案看起來都像剛被修改。關鍵不在於固定選擇某個參數,而是讓團隊只採用一套經過驗證的策略,並將該策略寫入任務指令碼。
若程式碼透過壓縮檔交接,可先解壓縮至隔離目錄,完成未來時間掃描、檔案數量核對與 Git 狀態檢查後,再以原子方式切換至正式工作區。不要直接覆寫仍含有舊中間產物的目錄。
將時間基準納入任務入口
最終應將檢查安排在相依套件解析與 Xcode 建置之前。發現帶有未來時間的檔案時應立即失敗,並輸出有限數量的路徑;不要自動修改後繼續建置,因為自動修正會抹除故障證據。
建議為每次任務保留下列記錄:
date與systemsetup -gettimezone的輸出;- 目前提交、Xcode 路徑與 SDK 資訊;
- 帶有未來時間的檔案數量及前幾筆路徑;
- 快取鍵與還原結果;
- 第一次建置與第二次建置的任務差異。
修復後,使用同一個提交連續執行三輪:第一輪建立快取,第二輪驗證增量效果,第三輪確認結果並非偶然。若第二、三輪仍重複執行相同任務,就沿著該任務的輸入檔案繼續追查,而不是再次進行全量清理。時間戳治理的目標不是讓所有檔案擁有相同時間,而是讓原始碼、產生物與快取之間維持可解釋、可複核的先後關係。
常見問題
找到未來時間檔案後,可以直接對整個儲存庫執行 touch 嗎?
不建議。這會同時改寫所有檔案的修改時間,通常會觸發大範圍重新編譯。應先確認來源,再只修復異常檔案或重新取出受影響目錄。
為什麼乾淨建置正常,增量建置卻持續變慢?
增量建置會比較既有輸出與相依狀態。未來時間輸入、系統時間回撥或帶有錯誤中繼資料的快取,都可能讓相依項目每次被判定為過期。
為下一項開發任務設定雲端 Mac
選擇 Oak M4、Oak M4 Plus 或 Oak M4 Pro,並依團隊所在地從五個實體節點中設定租用方案。