工程文章

雲端 Mac CI 中治理 Swift 程式碼生成漂移

雲端 Mac CI 中治理 Swift 程式碼生成漂移

同一個提交在開發者電腦上沒有任何變更,進入雲端 Mac CI 後,卻讓 Generated/ 出現數十行差異;更棘手的是,專案仍然可以編譯,因此舊常數、遺漏的資源鍵或過期的 mock 也會被帶進產物。處理這類問題時,不能只臨時追加一次生成命令,而必須把生成器版本、輸入集合、輸出目錄與驗收規則一併納入建置契約。

先確認漂移來自哪裡

SwiftGen、Sourcery 這類工具的輸出由四個部分共同決定:執行檔版本、設定與範本、輸入檔案,以及執行環境。只鎖定設定檔並不足夠。開發機 PATH 中的工具可能比 CI 更新,檔案走訪順序可能隨目錄狀態改變,地區設定也可能影響排序或日期格式。

先在本機與 CI 各記錄一次最小現場資訊:

set -euo pipefail

sw_vers
xcodebuild -version
swiftgen --version
sourcery --version
printf 'LANG=%s
' "${LANG:-unset}"
printf 'PWD=%s
' "$PWD"
git status --short

這裡的目的不是蒐集越多資訊越好,而是確認生成流程實際讀取了哪些內容。如果範本會寫入目前時間、絕對路徑或隨機識別碼,應先移除這些非確定性輸入,否則每次執行都會產生看似合理、實際上毫無意義的差異。

「可以再次生成」不等於「可以生成相同結果」。門禁要判斷的是:輸入相同時,產物是否能逐位元組保持一致。

建立可重現的生成基準

生成目錄應完全由工具管理,不要混入人工維護的 Swift 檔案。每次執行前先刪除舊目錄,才能發現已從輸入中移除、卻因增量生成而殘留的型別。設定、範本與輸入目錄都應使用以儲存庫根目錄為基準的確定路徑,避免依賴呼叫者目前所在的位置。

建議依照下表整理各項責任:

項目 固定方式 CI 驗收
生成器 鎖定版本並輸出版本資訊 與儲存庫宣告一致
設定與範本 納入版本控制 工作區沒有暫時修改
輸入 明確指定目錄與副檔名 排序後的集合保持穩定
輸出 使用專屬目錄 先清空再生成
環境 固定地區設定 不寫入時間與絕對路徑

工具的安裝方式可以依團隊需求調整,但版本宣告必須只有一個來源。不要讓腳本指定一個版本、開發文件記載另一個版本,最後又由雲端 Mac 使用 PATH 中的第三個版本。

在編譯前執行差異檢查

把生成步驟安排在編譯之前,可以更快取得失敗回饋,也能避免編譯錯誤掩蓋真正原因。以下腳本假設所有生成檔案都位於 Generated/,而且這些檔案已提交至儲存庫:

set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

export LANG=C
export LC_ALL=C

rm -rf Generated
mkdir -p Generated build

swiftgen config run --config swiftgen.yml
sourcery --config sourcery.yml

find Generated -type f -print |
  LC_ALL=C sort |
  while IFS= read -r file; do
    shasum -a 256 "$file"
  done > build/generated.sha256

git diff --exit-code -- Generated
git status --short --untracked-files=all Generated

git diff 只能看見已追蹤檔案的變更,因此還必須檢查未追蹤檔案。實際門禁應在 git status 有任何輸出時明確判定失敗,並將差異修補檔與 generated.sha256 保存為建置附件。雜湊清單不能取代差異檢查,但可以回答:「這次 CI 究竟生成了哪一組檔案?」

區分三種失敗類型

如果只有格式改變,應檢查範本換行、工具版本與格式化工具的執行順序;如果有新增或刪除檔案,應檢查輸入集合與清空邏輯;如果內容值發生變化,應檢查資源、介面描述或範本參數。完成分類後再修正根本原因,不要直接在 CI 中覆寫產物並繼續編譯。

避開 Xcode 建置階段的並行陷阱

多個 target 共用同一個生成目錄時,如果把生成命令分別放進每個 Run Script Phase,就可能同時寫入同一個檔案。結果有時只是重複執行,有時則會產生遭截斷的檔案,或讓檔案短暫消失。更穩妥的做法,是將生成設為 CI 中獨立的前置步驟,完成後再啟動所有 xcodebuild 工作。

如果生成流程必須放在 Xcode 建置階段,就應只保留一個負責者,並完整宣告輸入與輸出檔案清單。生成腳本本身不應修改專案檔,也不要在其他程序讀取生成目錄時將其清空。若平行建置需要彼此獨立的輸出,可依工作識別碼建立暫存目錄,驗收通過後再透過一次原子替換發布到固定位置。

另一個常見誤區,是在生成後立即執行格式化工具,但本機的提交前 hook 卻採用相反順序。生成、格式化與差異檢查必須維持相同順序,並由同一個入口腳本執行。

合併前與故障時的檢查清單

提交生成器或輸入變更時,程式碼審查至少應核對以下項目:

  1. 生成器版本宣告是否同步更新。
  2. 設定、範本與輸入變更是否出現在同一個提交中。
  3. 刪除輸入後,對應的生成檔案是否確實消失。
  4. 產物中是否混入時間、使用者名稱或絕對路徑。
  5. 在本機連續執行兩次後,第二次是否維持零差異。
  6. CI 是否在編譯前檢查已追蹤與未追蹤檔案。
  7. 平行工作是否寫入彼此隔離的目錄。
  8. 發生失敗時,是否保留版本資訊、差異修補檔與雜湊清單。

疑難排解時,不要先執行自動修正並覆蓋現場。應先保存 git status、工具版本與差異,再從乾淨的檢出環境重新執行生成入口。如果乾淨檢出後仍不一致,問題就來自環境或尚未鎖定的輸入;如果只有重複使用的工作區會失敗,則應優先檢查舊檔案、快取與並行寫入。最終標準很簡單:相同提交在任何雲端 Mac 工作中連續生成兩次,第二次都不應出現任何可觀察的差異。

常見問題

生成的 Swift 檔案應該提交到版本庫嗎?

若本機開發、程式碼審查或下游目標需要直接讀取生成檔案,就應提交,並由 CI 重新生成後檢查差異;若不提交,生成器、範本與輸入都必須納入建置相依。

為什麼本機生成正常,雲端 Mac CI 仍出現差異?

通常是生成器版本、區域設定、輸入排序、換行格式或工作目錄不同。應在編譯前明確固定這些條件,而不是只檢查最終能否建置。

VMMini M4

在專屬實體節點上執行下一項任務

選擇節點與計費週期,使用固定的 M4、16GB RAM 和 256GB SSD 設定開始部署。實際可用性以控制台即時回傳為準。

立即租用雲端 Mac