工程紀錄

雲端 Mac 診斷 Xcode 模組快取污染與 PCM 失效

雲端 Mac 診斷 Xcode 模組快取污染與 PCM 失效

同一筆提交在本機可以順利通過,放到持續運作的雲端 Mac 後,卻間歇出現 module file was created by a different version of the compiler,有時重新執行又會恢復正常。這類問題通常不是 Swift 原始碼隨機變動,而是不同工具鏈、SDK 或平行任務讀取了同一份預編譯模組快取。直接刪除全部 DerivedData 雖然能暫時緩解問題,卻也會抹除最關鍵的現場資訊,並讓下一次污染繼續發生。

先確認問題是否來自模組快取

Clang 會將系統標頭檔與 Objective-C 模組編譯成 PCM,Swift 編譯器也會維護自己的模組快取。只要建立快取時使用的編譯器、SDK、架構或編譯參數與目前任務不一致,載入階段就可能失敗。

先在完整日誌中搜尋下列特徵:

日誌特徵 較可能的原因 第一個檢查點
different version of the compiler 曾切換 Xcode xcodebuild -version
PCM file is out of date SDK 或標頭檔時間戳記變更 目前的 SDK 路徑
could not build module 上游模組損壞 第一個失敗的模組
同一任務重新執行後通過 平行寫入或共用快取 快取目錄的歸屬

不要只擷取最後一條錯誤。模組載入失敗通常會引發數十條連鎖錯誤,應往前找到第一個出現 module.pcmModuleCache 的位置。

清除快取前,請先保存失敗日誌、Xcode 建置編號與實際建置設定。缺少這些資訊,即使重新建置成功,也無法判斷原因究竟是工具鏈漂移、並行污染,還是快取本身損壞。

為失敗任務保存可供比較的現場資訊

在 VMKeep 的雲端 Mac 上,應先記錄命令實際解析到的開發者目錄,而不是只查看指令碼預期使用的版本。

set -o pipefail

mkdir -p artifacts/diagnostics
xcode-select -p | tee artifacts/diagnostics/developer-dir.txt
xcodebuild -version | tee artifacts/diagnostics/xcode-version.txt
xcrun --sdk iphoneos --show-sdk-path | tee artifacts/diagnostics/sdk-path.txt
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings \
  > artifacts/diagnostics/build-settings.txt

接著檢查失敗命令中的 SDKROOTARCHSDEVELOPER_DIRCLANG_MODULE_CACHE_PATHSWIFT_MODULE_CACHE_PATH。如果任務是透過 sudo、遠端工作階段或排程程序啟動,還必須比較執行使用者與 HOME;同一份指令碼改由不同使用者執行時,預設快取位置也會跟著改變。

固定工具鏈入口

建置指令碼應明確設定 DEVELOPER_DIR,並在實際編譯前執行一次 xcodebuild -version。版本不符合預期時應立即退出,不要讓任務使用錯誤的工具鏈寫入共用快取。升級 Xcode 後,應產生新的快取鍵,而不是繼續沿用舊目錄。

將快取邊界縮小至單一任務

可靠的目錄鍵至少應包含 Xcode 建置編號、SDK、架構與任務識別碼。分支名稱不能直接作為路徑,必須先轉換特殊字元。以下做法可讓每個任務擁有獨立的 DerivedData 與模組快取,同時將相依套件下載目錄保留在另一個獨立位置。

set -euo pipefail

JOB_KEY="$(printf '%s' "${CI_JOB_ID:-local}" | tr -cs 'A-Za-z0-9._-' '-')"
XCODE_BUILD="$(xcodebuild -version | awk '/Build version/{print $3}')"
SDK_BUILD="$(xcrun --sdk iphoneos --show-sdk-build-version)"
CACHE_KEY="${XCODE_BUILD}-${SDK_BUILD}-arm64-${JOB_KEY}"

ROOT="$HOME/BuildCaches/$CACHE_KEY"
DERIVED="$ROOT/DerivedData"
MODULES="$ROOT/Modules"
PACKAGES="$HOME/BuildCaches/SourcePackages"

mkdir -p "$DERIVED" "$MODULES/clang" "$MODULES/swift" "$PACKAGES"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -derivedDataPath "$DERIVED" \
  -clonedSourcePackagesDirPath "$PACKAGES" \
  CLANG_MODULE_CACHE_PATH="$MODULES/clang" \
  SWIFT_MODULE_CACHE_PATH="$MODULES/swift" \
  build

長期分支可以重複使用各自的穩定鍵;臨時合併請求則應依任務隔離。不要讓兩個仍在執行的 xcodebuild 程序寫入同一個模組目錄,也不要將模組快取製作成可跨工具鏈還原的通用壓縮檔。

精準清理,而不是一鍵全部歸零

確認是模組快取問題後,先停止所有正在使用該目錄的建置程序,再依影響範圍由小到大處理:

  1. 刪除目前任務的 Modules/clangModules/swift
  2. 保留 SourcePackages,重新執行相依套件解析與建置。
  3. 如果索引或中間產物仍引用舊模組,再刪除目前任務的 DerivedData。
  4. 如果只有某個第三方模組失敗,請核對其二進位產物所支援的架構與目前 SDK,不要反覆清除快取來掩蓋相容性問題。

清理操作必須限定在計算得出的任務目錄內。使用 rm -rf ~/Library/Developer/Xcode/DerivedData/* 會同時影響其他任務,很容易將一次局部問題擴大成整台機器的冷啟動。

避免誤刪仍在使用的目錄

回收舊快取時,應透過任務鎖定或執行記錄確認目錄未被占用,再依最後使用時間刪除。不要只根據目錄建立時間判斷,因為穩定分支可能會持續重複使用較早建立的快取。清理指令碼還應驗證目標路徑位於預期的根目錄下;如果變數為空或路徑計算失敗,應立即退出。

將快取一致性納入建置門禁

最終應讓流水線主動揭露環境漂移。每次建置都要保存 Xcode 建置編號、SDK 建置編號、架構、快取鍵與執行使用者;任務結束後,將失敗日誌連同這些資訊一併封存。升級工具鏈時應建立新的命名空間,確認新流程穩定後再回收舊快取。

還可以安排一個不還原模組快取的驗收任務。如果一般任務失敗而乾淨任務通過,問題便集中在快取層;如果兩者都失敗,則應回到原始碼、相依套件或建置設定繼續排查。如此既能避免將所有錯誤都歸因於快取,也不必靠反覆執行來碰運氣。

模組快取本質上是編譯器輸入的一部分。將版本、目錄所有權與並行邊界寫入指令碼後,PCM 失效便能從隨機故障轉變為可定位、可復原的工程事件。

常見問題

PCM 失效時需要刪除整個 DerivedData 嗎?

不必先做全面刪除。先保存日誌與建置設定,再清理目前任務的 ModuleCache.noindex;只有索引、產物與模組快取同時異常時,才重建該任務的 DerivedData。

不同 Xcode 版本能共用模組快取目錄嗎?

不建議。快取鍵至少要包含 Xcode 建置號、SDK、架構與任務識別,否則工具鏈切換或平行建置可能讀取不相容的 PCM。

獨享實體節點

在雲端 Mac 上繼續驗證這套工作流程

從三種 Apple Silicon 設定中選擇合適的記憶體、儲存空間、節點與租用週期。

選擇設定並下單