工程技術文章

雲端 Mac 定位 Swift 編譯器崩潰:保存現場與最小重現

雲端 Mac 定位 Swift 編譯器崩潰:保存現場與最小重現

同一筆提交在開發機上可以順利通過,到了雲端 Mac 的持續建置環境,卻可能讓 swift-frontend 異常退出。重新執行偶爾會成功,清除快取後又改成另一個檔案失敗。此時最危險的做法,是連續修改原始碼、升級相依套件並刪除所有快取:現場條件同時遭到改變,真正觸發編譯器崩潰的因素也會隨之消失。更穩妥的方式,是先確認故障類型,再依照單一變因原則逐步縮小範圍。

先確認是否真的是編譯器崩潰

CompileSwiftSources 失敗並不等於編譯器崩潰。語法錯誤、型別推斷失敗或模組缺失,也會在這個階段傳回非零狀態。真正適合進入本文排查流程的跡象包括:

  • 日誌明確指出 swift-frontend 異常退出,或出現 signal 11
  • 輸出包含 Stack dump、內部斷言失敗或編譯器呼叫堆疊。
  • ~/Library/Logs/DiagnosticReports/ 中出現時間相符的 swift-frontend 報告。
  • 相同的原始碼與命令可以多次觸發,而不是僅發生一次的遠端工作階段中斷。

先記錄工具鏈與主機資訊,不要只截取日誌最後十行:

mkdir -p diagnostics
{
  date -u
  sw_vers
  uname -m
  xcode-select -p
  xcodebuild -version
  xcrun swiftc --version
} | tee diagnostics/environment.txt

find "$HOME/Library/Logs/DiagnosticReports" \
  -maxdepth 1 -type f -name 'swift-frontend*' \
  -exec cp {} diagnostics/ \;

崩潰報告、完整建置命令與觸發問題的原始碼,必須來自同一次重現。把不同執行輪次的資料拼在一起,往往只會產生無法驗證的錯誤線索。

以隔離建置保存第一現場

為重現作業準備獨立的 DerivedData,並完整保存終端機輸出。不要一開始就刪除全域快取,因為快取是否參與觸發,本身就是需要驗證的變因。

set -o pipefail
rm -rf "$PWD/.diagnostics-derived-data"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.diagnostics-derived-data" \
  -jobs 1 \
  build 2>&1 | tee diagnostics/build-single-job.log

此處將並行度降為一,只是為了判斷崩潰是否與平行編譯有關,不代表應長期採用這項設定。如果單一工作仍會穩定崩潰,請保留該份日誌;如果故障消失,則在原始碼與 DerivedData 固定不變的情況下,分別測試 -jobs 2 與日常使用的並行值。每組至少重複三次,並記錄結果是成功、一般編譯錯誤,還是編譯器崩潰,不能只寫「失敗」。

在 OVPS 雲端 Mac 上重現時,也應記錄實際選用的 Xcode 路徑。只比較介面顯示的版本名稱並不足夠;即使主版本相同,建置編號與 Swift 工具鏈仍可能不同。

建立單一變因對照矩陣

排查應從成本最低、破壞性最小的變因開始。每次只修改一項,並另外保存一份新日誌。

對照項目 基準值 變更值 要回答的問題
並行度 -jobs 1 日常並行值 是否只在平行編譯時觸發
DerivedData 隔離目錄 新建空目錄 是否依賴既有的中間產物
編譯模式 目前專案值 暫時對照值 是否與增量編譯或整體模組編譯路徑有關
最佳化層級 Debug 設定 專案允許的對照設定 是否只在最佳化階段觸發
工具鏈 目前鎖定版本 另一個已驗證版本 是否屬於特定工具鏈的回歸問題

不要同時切換 Xcode、更新相依套件並清除快取。若切換工具鏈後問題消失,只能說明故障與該工具鏈組合有關,不能直接證明新版本已經修復問題。還需要把失敗檔案、編譯模式與觸發次數記錄在同一張表中。

識別快取造成的假象

只有在空白 DerivedData 可以通過,而舊目錄會穩定失敗時,才進一步檢查模組快取與建置資料庫。先封存有問題的目錄,再進行定點清理。若直接大範圍刪除,就會失去比較舊產物與新產物的機會。

從失敗檔案縮減為最小重現案例

在完整日誌中找出失敗的 Swift 編譯工作。若問題可以脫離原專案重現,請將相關宣告複製到 Repro.swift,先以型別檢查進行驗證:

xcrun swiftc -typecheck Repro.swift 2>&1 | tee diagnostics/repro.log

縮減時應依相依關係反向處理:先刪除無關方法,再移除協定實作、泛型約束與屬性包裝器;每刪除一部分,就執行一次判定指令稿。不要只憑肉眼猜測某段語法「看起來很複雜」,編譯器崩潰經常是由兩個普通特性的組合所觸發。

#!/bin/zsh
set -o pipefail

output="$(mktemp)"
xcrun swiftc -typecheck Repro.swift >"$output" 2>&1

if grep -Eiq 'signal 11|segmentation fault|stack dump|swift-frontend.*failed' "$output"; then
  cp "$output" diagnostics/latest-crash.log
  rm -f "$output"
  exit 0
fi

rm -f "$output"
exit 1

這個指令稿將「仍能觸發目標崩潰」定義為成功,方便搭配人工二分法或原始碼縮減工具。每次縮減後,還要確認錯誤特徵保持一致;如果內部斷言變成一般型別錯誤,就表示關鍵條件已經被刪除。

若單一檔案無法重現,就保留最小的模組邊界:一個精簡專案、必要的建置設定、鎖定的相依套件版本,以及一條執行命令。不要夾帶業務資料、存取憑證或無關資源。

建立可複查的交付套件

最終資料應讓另一台雲端 Mac 在沒有口頭說明的情況下完成驗證。建議目錄只包含:

  • README.md:預期現象、執行命令、重複次數與實際結果。
  • environment.txt:系統架構、Xcode 建置編號與 Swift 版本。
  • Repro.swift 或精簡專案:只保留必要的原始碼。
  • build.log:未經截斷的標準輸出與錯誤輸出。
  • 崩潰報告:與該次執行時間相符的診斷檔案。
  • check.sh:透過退出狀態明確表示是否重現目標崩潰。

交付前,請在新目錄中重新執行一次,確認指令稿不依賴原專案的絕對路徑、使用者目錄或殘留的模組快取。若問題只會在特定並行度下出現,應清楚寫明並行參數與重現機率;若只在某個工具鏈建置編號中出現,則同時記錄可以順利通過的對照版本。這樣留下的就不再只是一句「Swift 偶爾崩潰」,而是一組可以重複、比較並繼續用於修復的工程證據。

常見問題

CompileSwiftSources 失敗是否代表編譯器已經崩潰?

不代表。語法、型別與相依套件錯誤也會在同一階段失敗。只有 swift-frontend 異常結束、出現 signal、Stack dump 或對應診斷報告時,才應視為編譯器崩潰。

最小重現一定要包含完整 Xcode 專案嗎?

不一定。若單一 Swift 檔案配合 swiftc -typecheck 即可穩定觸發,提交該檔案與命令更清楚;依賴模組邊界或建置設定時,才需要精簡專案。

OVPS CLOUD MAC

將下一次建置部署至獨享物理節點

從三種 Apple Silicon 配置與六個在售節點中選擇,實際可用狀態以控制台即時回傳為準。

選擇配置並下單