工程實務

在雲端 Mac 建立 Xcode 編譯警告基準門禁

在雲端 Mac 建立 Xcode 編譯警告基準門禁

維護多年的 Xcode 專案,往往已累積數十條編譯警告。此時若直接啟用 SWIFT_TREAT_WARNINGS_AS_ERRORS,主分支會立刻變得無法建置;但若繼續放任不管,新的警告又會混入既有雜訊。更可行的做法,是在雲端 Mac 上固定一份經過審查的警告基準,讓 CI 只拒絕本次變更新增的項目。

先定義門禁的邊界

警告門禁不能只是統計日誌中的 warning: 數量。相依套件下載、建置指令碼與系統工具都可能輸出相同字樣,直接計數不但不穩定,也無法告訴開發者應該修改哪個檔案。

建議將每條警告正規化為「儲存庫相對路徑 + 警告內文」,並移除行號、欄號、暫存目錄與時間戳記。如此一來,程式碼上下移動時不會產生新的簽名;檔案重新命名或警告內容改變時,則仍會觸發審查。

內容 是否納入簽名 原因
儲存庫相對路徑 識別負責的檔案
警告內文 區分問題類型
行號與欄號 修改程式碼後容易偏移
DerivedData 路徑 每次任務都可能不同
外部相依套件警告 預設否 團隊通常無法直接修正

基準代表「目前已知且被接受的技術債」,不代表這些警告是正確的。每刪除一條既有警告,也應同步縮減基準。

產生可重現的警告簽名

首先固定 Workspace、Scheme、組態與 SDK。若開發環境使用 Debug、CI 使用 Release,條件式編譯會走不同路徑,產生的基準也就沒有比較意義。以下指令碼要求呼叫端明確傳入 Workspace 與 Scheme,並保存完整輸出。

#!/bin/bash
set -u

WORKSPACE="${WORKSPACE:?set WORKSPACE}"
SCHEME="${SCHEME:?set SCHEME}"
ROOT="$(git rev-parse --show-toplevel)"
OUT="$ROOT/.ci-artifacts"
DERIVED="$OUT/DerivedData"

mkdir -p "$OUT"
rm -rf "$DERIVED"

set +e
xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath "$DERIVED" \
  CODE_SIGNING_ALLOWED=NO \
  build 2>&1 | tee "$OUT/xcodebuild.log"
BUILD_STATUS=${PIPESTATUS[0]}
set -e

grep -F "$ROOT/" "$OUT/xcodebuild.log" \
  | grep " warning: " \
  | sed "s#${ROOT}/##" \
  | sed -E 's#:[0-9]+:[0-9]+: warning: # | #' \
  | LC_ALL=C sort -u \
  > "$OUT/warnings.current"

exit "$BUILD_STATUS"

CODE_SIGNING_ALLOWED=NO 適合只驗證編譯的任務;需要封存或驗證簽章的管線不應直接照搬。建置失敗與警告回歸也必須分開處理:如果 xcodebuild 本身失敗,應優先回傳其結束代碼,而不是用空白警告檔案掩蓋真正的故障。

處理指令碼與多行診斷

部分 Run Script 階段會輸出自訂警告,其格式未必包含原始碼座標。若這些指令碼由團隊維護,可以為它們約定固定前綴,再建立第二套解析規則。不要使用過於寬泛的正規表示式擷取所有輸出,否則網路提示與工具升級通知也會進入基準。

Swift 的補充說明行通常不含 warning:,不適合當作獨立簽名,但仍應保留在完整日誌中。門禁負責快速判斷,完整日誌負責還原脈絡,兩者不能互相取代。

審查並提交第一版基準

首次產生的 warnings.current 不應由自動化任務直接複製為基準。應先依路徑分配負責人、刪除重複項目,並確認未納入衍生檔案、相依套件原始碼或暫存目錄。完成審查後再執行:

mkdir -p .ci
LC_ALL=C sort -u .ci-artifacts/warnings.current > .ci/xcode-warnings.baseline
git add .ci/xcode-warnings.baseline
git commit -m "Add reviewed Xcode warning baseline"

基準必須與程式碼一起納入版本控制。任何擴大基準的提交,都應列出新增簽名及其原因。不能讓失敗的任務自動「學習」新警告,否則門禁會在最需要攔截時自行放行。

升級 Xcode 時,編譯器可能會調整診斷訊息的措辭。正確流程是在獨立分支執行完整建置,區分真正新增的問題與純粹的文字變更,再一次更新基準並記錄工具鏈版本,而不是在比較指令碼中加入無限寬鬆的模糊比對。

在 CI 中只阻止新增警告

將目前結果與基準排序後,即可使用 comm 找出只存在於目前建置結果中的簽名:

BASELINE=".ci/xcode-warnings.baseline"
CURRENT=".ci-artifacts/warnings.current"
NEW=".ci-artifacts/warnings.new"

test -f "$BASELINE"
LC_ALL=C sort -u "$BASELINE" -o "$BASELINE"
LC_ALL=C sort -u "$CURRENT" -o "$CURRENT"
LC_ALL=C comm -13 "$BASELINE" "$CURRENT" > "$NEW"

if test -s "$NEW"; then
  printf '%s
' "New Xcode warnings detected:"
  cat "$NEW"
  exit 42
fi

應將 xcodebuild.logwarnings.currentwarnings.new 一併封存。結束代碼 42 只是團隊內部約定,重點是將「編譯失敗」與「新增警告」顯示為不同原因。開發者看到路徑與警告內文後,才能在一次回饋循環內完成修正。

若多個任務平行建置不同 Target,應分別產生結果,最後再合併、排序與去除重複項目。不要讓多個程序同時寫入同一個檔案,否則截斷與交錯寫入會造成偶發誤判。

持續縮減基準,而不是永久凍結

門禁上線後,每次修正既有警告,都要刪除對應的基準行。也可以增加一項不會阻止建置的檢查,找出「存在於基準中、但目前已消失」的項目,提醒提交者順手清理。如此一來,基準只會單向縮減,不會變成無人理解的歷史清單。

在穩定運作前,還應檢查四個項目:CI 與本機是否使用相同的 Xcode 版本;Scheme 是否包含預期的 Target;地區設定是否影響工具輸出;儲存庫路徑篩選是否涵蓋所有自行維護的原始碼。OnceMac 節點上的執行目錄也應固定由任務建立,避免重複使用上一次建置留下的日誌與 DerivedData。

當基準降至零時,就可以移除差異比較,並啟用編譯器將警告視為錯誤的策略。在此之前,基準門禁提供的是一條務實的過渡路徑:既不掩蓋舊債,也不允許新債持續增加。

常見問題

為什麼不直接把所有警告視為錯誤?

歷史警告較多時,直接啟用會讓所有建置立即失敗。基準門禁先凍結現況,只阻止新增警告,待存量逐步歸零後再切換較安全。

警告行號改變是否會造成誤報?

可能會,因此簽名應移除行號與欄號,只保留專案內相對路徑及警告內容。Xcode 升級造成的文字變動則應在驗收後獨立更新基準。

相依套件的警告需要納入基準嗎?

通常不需要。先依專案根目錄篩選團隊維護的程式碼;必須追蹤的內部套件再透過明確路徑白名單加入。

獨享實體 Mac mini

把下一次建置放到獨立實體節點上。

選擇晶片、記憶體、儲存空間、節點與租期,取得不與其他客戶共用資源的雲端 Mac。

選擇配置並租用