同一筆提交在開發機上能正常封存,到了 HexVM 雲端 Mac 卻變更了部署目標,或突然少編譯一個架構,問題往往不在原始碼,而是出在 Xcode 最終解析出的建置設定。專案檔、.xcconfig、環境變數與命令列參數會逐層覆寫,只檢查其中一個檔案,很難確認目標實際採用了哪些設定。
這類設定漂移不一定會立即導致失敗。它可能先改變產出內容,直到發佈階段才浮現。更穩妥的做法,是保存一份經過清理的有效設定基準,並在每次合併及正式封存前重新產生並比對。
先界定需要稽核的設定範圍
不要直接保存 xcodebuild 的完整輸出。絕對路徑、暫存目錄與建置編號會產生大量無意義的差異,因此應先將設定分成三類。
| 類型 | 典型鍵 | 處理方式 |
|---|---|---|
| 產出邊界 | PRODUCT_BUNDLE_IDENTIFIER、SUPPORTED_PLATFORMS、ARCHS |
有變更即審查 |
| 編譯行為 | SWIFT_VERSION、SWIFT_OPTIMIZATION_LEVEL、GCC_PREPROCESSOR_DEFINITIONS |
有變更即審查 |
| 節點雜訊 | BUILD_DIR、TEMP_DIR、PROJECT_TEMP_DIR |
刪除或正規化 |
簽署相關設定也應納入基準,但不要將憑證檔案、私鑰內容或臨時憑證寫入儲存庫。稽核的對象是設定名稱、簽署模式與權限邊界,而不是複製敏感資料。
基準不是「永遠不變」的設定檔。它是一份需要經過程式碼審查的預期狀態:允許變更,但變更必須可見、可解釋,也能回復。
匯出目標實際生效的設定
先固定專案、Scheme、建置組態與目標平台。若使用工作區專案,將 -project 換成 -workspace,其餘參數維持不變。
mkdir -p .ci/build-settings
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-showBuildSettings \
> .ci/build-settings/raw.txt
執行前,先用 xcodebuild -list -project App.xcodeproj 確認 Scheme 是否已共享。若流水線使用額外參數,例如功能開關或自訂 SYMROOT,產生基準與執行建置時必須傳入同一組參數,否則比對的是兩個不同的執行環境。
含有多個 Target 的專案會輸出多組同名鍵。此時不要直接去重,應保留 Target 標題,或為每個 Scheme 分別建立檔案。應用程式、擴充功能與測試目標的部署範圍原本就可能不同。
清理路徑雜訊並產生穩定基準
以下指令碼會擷取 KEY = VALUE 行、移除暫存目錄相關鍵,並將工作目錄與使用者目錄替換為穩定標記。指令碼不會解析金鑰,也不會輸出完整的環境變數清單。
from pathlib import Path
import os
source = Path(".ci/build-settings/raw.txt")
target = Path(".ci/build-settings/current.txt")
ignored = {
"BUILD_DIR",
"BUILD_ROOT",
"CONFIGURATION_BUILD_DIR",
"DERIVED_FILES_DIR",
"PROJECT_TEMP_DIR",
"TARGET_TEMP_DIR",
"TEMP_DIR"
}
root = str(Path.cwd())
home = str(Path.home())
rows = []
for line in source.read_text().splitlines():
stripped = line.strip()
if " = " not in stripped:
continue
key, value = stripped.split(" = ", 1)
if key in ignored:
continue
value = value.replace(root, "<ROOT>").replace(home, "<HOME>")
rows.append(f"{key}={value}")
target.write_text("
".join(sorted(rows)) + "
")
首次確認結果後,將 current.txt 複製成依用途命名的基準,例如 release-ios.txt,再提交至儲存庫。
python3 .ci/normalize_build_settings.py
cp .ci/build-settings/current.txt \
.ci/build-settings/release-ios.txt
如果相依套件路徑仍頻繁變動,應先判斷它是否會影響編譯輸入。不要為了得到「零差異」而過濾 HEADER_SEARCH_PATHS、FRAMEWORK_SEARCH_PATHS 或 OTHER_SWIFT_FLAGS;這些鍵的變更往往正是需要被偵測的問題。
將差異設為流水線門禁
檢查工作應在解析相依套件之後、正式封存之前執行。這樣既能取得完整的搜尋路徑,也能在成本較高的封存步驟開始前攔截錯誤設定。
set -euo pipefail
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-showBuildSettings \
> .ci/build-settings/raw.txt
python3 .ci/normalize_build_settings.py
diff -u \
.ci/build-settings/release-ios.txt \
.ci/build-settings/current.txt
當 diff 傳回非零狀態時,流水線應停止並保留差異內容。不要自動以新檔案覆寫基準,否則門禁將退化成單純的記錄器。
設定允許清單
團隊可以為確定無害的鍵維護明確的忽略清單,但清單必須精簡,並註明原因。建議持續阻擋以下變更:
IPHONEOS_DEPLOYMENT_TARGET與SUPPORTED_PLATFORMSARCHS、EXCLUDED_ARCHS與ONLY_ACTIVE_ARCHSWIFT_VERSION與最佳化層級CODE_SIGN_STYLE、簽署身分選擇方式PRODUCT_BUNDLE_IDENTIFIER與權限檔案路徑DEBUG_INFORMATION_FORMAT與連結器參數
排查常見誤報與實際漂移
若每次執行都出現大量路徑差異,先檢查根目錄替換是否涵蓋符號連結解析後的路徑。可以在工作開始時記錄 pwd -P,並讓指令碼同時正規化邏輯路徑與實際路徑。
若本機沒有差異,但雲端持續出現差異,請依序確認:是否使用相同的 Scheme、組態名稱是否一致、命令列是否附加了設定、相依套件是否已完成解析,以及環境變數是否參與 .xcconfig 展開。不要一開始就修改基準來遷就結果。
遇到實際變更時,應先找出來源,再更新基準:
- 使用
xcodebuild -showBuildSettings確認變更屬於哪一個 Target。 - 在專案檔、
.xcconfig與流水線參數中搜尋對應的鍵。 - 說明變更目的,以及對 Debug、Release 的影響。
- 完成一次全新建置與封存驗證。
- 將設定修改與基準更新放在同一次審查中。
建立可持續的稽核節奏
設定基準應依 Scheme、建置組態與平台拆分,不要用單一檔案涵蓋所有情境。日常提交可檢查主要應用程式的 Release 基準;涉及擴充功能、測試套件或發佈流程時,再執行相對應的檢查。
每次調整 Xcode 版本策略後,都應重新產生設定並逐項審查,因為預設值可能有所變動。審查重點不在差異數量,而是哪些差異會改變編譯輸入、產出結構、簽署邊界與執行目標。保留這條證據鏈後,建置失敗時就不必再從整台機器的環境開始猜測,設定變更也能像原始碼變更一樣被追蹤。
常見問題
為什麼不能只比較專案檔或 xcconfig?
專案檔與 xcconfig 只代表輸入。xcodebuild 輸出還會解析繼承、條件式設定與命令列覆寫,因此更接近目標實際使用的設定。
哪些設定變更應該直接阻止流水線?
部署目標、支援架構、Swift 版本、最佳化層級、程式碼簽署方式與產品識別碼變更通常應阻止流水線;暫存路徑則應先正規化。
為下一次建置選擇獨享雲端 Mac
依工作負載選擇 Mac mini 機型、租期與區域。每筆訂單都對應獨立實體節點,實際可用性以控制台即時回傳結果為準。