同一筆提交在本機可以順利通過,放到持續運作的雲端 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、.pcm 或 ModuleCache 的位置。
清除快取前,請先保存失敗日誌、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
接著檢查失敗命令中的 SDKROOT、ARCHS、DEVELOPER_DIR、CLANG_MODULE_CACHE_PATH 與 SWIFT_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 程序寫入同一個模組目錄,也不要將模組快取製作成可跨工具鏈還原的通用壓縮檔。
精準清理,而不是一鍵全部歸零
確認是模組快取問題後,先停止所有正在使用該目錄的建置程序,再依影響範圍由小到大處理:
- 刪除目前任務的
Modules/clang與Modules/swift。 - 保留
SourcePackages,重新執行相依套件解析與建置。 - 如果索引或中間產物仍引用舊模組,再刪除目前任務的 DerivedData。
- 如果只有某個第三方模組失敗,請核對其二進位產物所支援的架構與目前 SDK,不要反覆清除快取來掩蓋相容性問題。
清理操作必須限定在計算得出的任務目錄內。使用 rm -rf ~/Library/Developer/Xcode/DerivedData/* 會同時影響其他任務,很容易將一次局部問題擴大成整台機器的冷啟動。
避免誤刪仍在使用的目錄
回收舊快取時,應透過任務鎖定或執行記錄確認目錄未被占用,再依最後使用時間刪除。不要只根據目錄建立時間判斷,因為穩定分支可能會持續重複使用較早建立的快取。清理指令碼還應驗證目標路徑位於預期的根目錄下;如果變數為空或路徑計算失敗,應立即退出。
將快取一致性納入建置門禁
最終應讓流水線主動揭露環境漂移。每次建置都要保存 Xcode 建置編號、SDK 建置編號、架構、快取鍵與執行使用者;任務結束後,將失敗日誌連同這些資訊一併封存。升級工具鏈時應建立新的命名空間,確認新流程穩定後再回收舊快取。
還可以安排一個不還原模組快取的驗收任務。如果一般任務失敗而乾淨任務通過,問題便集中在快取層;如果兩者都失敗,則應回到原始碼、相依套件或建置設定繼續排查。如此既能避免將所有錯誤都歸因於快取,也不必靠反覆執行來碰運氣。
模組快取本質上是編譯器輸入的一部分。將版本、目錄所有權與並行邊界寫入指令碼後,PCM 失效便能從隨機故障轉變為可定位、可復原的工程事件。
常見問題
PCM 失效時需要刪除整個 DerivedData 嗎?
不必先做全面刪除。先保存日誌與建置設定,再清理目前任務的 ModuleCache.noindex;只有索引、產物與模組快取同時異常時,才重建該任務的 DerivedData。
不同 Xcode 版本能共用模組快取目錄嗎?
不建議。快取鍵至少要包含 Xcode 建置號、SDK、架構與任務識別,否則工具鏈切換或平行建置可能讀取不相容的 PCM。
在雲端 Mac 上繼續驗證這套工作流程
從三種 Apple Silicon 設定中選擇合適的記憶體、儲存空間、節點與租用週期。