維護多年的 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.log、warnings.current 與 warnings.new 一併封存。結束代碼 42 只是團隊內部約定,重點是將「編譯失敗」與「新增警告」顯示為不同原因。開發者看到路徑與警告內文後,才能在一次回饋循環內完成修正。
若多個任務平行建置不同 Target,應分別產生結果,最後再合併、排序與去除重複項目。不要讓多個程序同時寫入同一個檔案,否則截斷與交錯寫入會造成偶發誤判。
持續縮減基準,而不是永久凍結
門禁上線後,每次修正既有警告,都要刪除對應的基準行。也可以增加一項不會阻止建置的檢查,找出「存在於基準中、但目前已消失」的項目,提醒提交者順手清理。如此一來,基準只會單向縮減,不會變成無人理解的歷史清單。
在穩定運作前,還應檢查四個項目:CI 與本機是否使用相同的 Xcode 版本;Scheme 是否包含預期的 Target;地區設定是否影響工具輸出;儲存庫路徑篩選是否涵蓋所有自行維護的原始碼。OnceMac 節點上的執行目錄也應固定由任務建立,避免重複使用上一次建置留下的日誌與 DerivedData。
當基準降至零時,就可以移除差異比較,並啟用編譯器將警告視為錯誤的策略。在此之前,基準門禁提供的是一條務實的過渡路徑:既不掩蓋舊債,也不允許新債持續增加。
常見問題
為什麼不直接把所有警告視為錯誤?
歷史警告較多時,直接啟用會讓所有建置立即失敗。基準門禁先凍結現況,只阻止新增警告,待存量逐步歸零後再切換較安全。
警告行號改變是否會造成誤報?
可能會,因此簽名應移除行號與欄號,只保留專案內相對路徑及警告內容。Xcode 升級造成的文字變動則應在驗收後獨立更新基準。
相依套件的警告需要納入基準嗎?
通常不需要。先依專案根目錄篩選團隊維護的程式碼;必須追蹤的內部套件再透過明確路徑白名單加入。
把下一次建置放到獨立實體節點上。
選擇晶片、記憶體、儲存空間、節點與租期,取得不與其他客戶共用資源的雲端 Mac。