HexVM 工程實務

雲端 Mac 的 Xcode 建置設定漂移稽核實戰

雲端 Mac 的 Xcode 建置設定漂移稽核實戰

同一筆提交在開發機上能正常封存,到了 HexVM 雲端 Mac 卻變更了部署目標,或突然少編譯一個架構,問題往往不在原始碼,而是出在 Xcode 最終解析出的建置設定。專案檔、.xcconfig、環境變數與命令列參數會逐層覆寫,只檢查其中一個檔案,很難確認目標實際採用了哪些設定。

這類設定漂移不一定會立即導致失敗。它可能先改變產出內容,直到發佈階段才浮現。更穩妥的做法,是保存一份經過清理的有效設定基準,並在每次合併及正式封存前重新產生並比對。

先界定需要稽核的設定範圍

不要直接保存 xcodebuild 的完整輸出。絕對路徑、暫存目錄與建置編號會產生大量無意義的差異,因此應先將設定分成三類。

類型 典型鍵 處理方式
產出邊界 PRODUCT_BUNDLE_IDENTIFIERSUPPORTED_PLATFORMSARCHS 有變更即審查
編譯行為 SWIFT_VERSIONSWIFT_OPTIMIZATION_LEVELGCC_PREPROCESSOR_DEFINITIONS 有變更即審查
節點雜訊 BUILD_DIRTEMP_DIRPROJECT_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_PATHSFRAMEWORK_SEARCH_PATHSOTHER_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_TARGETSUPPORTED_PLATFORMS
  • ARCHSEXCLUDED_ARCHSONLY_ACTIVE_ARCH
  • SWIFT_VERSION 與最佳化層級
  • CODE_SIGN_STYLE、簽署身分選擇方式
  • PRODUCT_BUNDLE_IDENTIFIER 與權限檔案路徑
  • DEBUG_INFORMATION_FORMAT 與連結器參數

排查常見誤報與實際漂移

若每次執行都出現大量路徑差異,先檢查根目錄替換是否涵蓋符號連結解析後的路徑。可以在工作開始時記錄 pwd -P,並讓指令碼同時正規化邏輯路徑與實際路徑。

若本機沒有差異,但雲端持續出現差異,請依序確認:是否使用相同的 Scheme、組態名稱是否一致、命令列是否附加了設定、相依套件是否已完成解析,以及環境變數是否參與 .xcconfig 展開。不要一開始就修改基準來遷就結果。

遇到實際變更時,應先找出來源,再更新基準:

  1. 使用 xcodebuild -showBuildSettings 確認變更屬於哪一個 Target。
  2. 在專案檔、.xcconfig 與流水線參數中搜尋對應的鍵。
  3. 說明變更目的,以及對 Debug、Release 的影響。
  4. 完成一次全新建置與封存驗證。
  5. 將設定修改與基準更新放在同一次審查中。

建立可持續的稽核節奏

設定基準應依 Scheme、建置組態與平台拆分,不要用單一檔案涵蓋所有情境。日常提交可檢查主要應用程式的 Release 基準;涉及擴充功能、測試套件或發佈流程時,再執行相對應的檢查。

每次調整 Xcode 版本策略後,都應重新產生設定並逐項審查,因為預設值可能有所變動。審查重點不在差異數量,而是哪些差異會改變編譯輸入、產出結構、簽署邊界與執行目標。保留這條證據鏈後,建置失敗時就不必再從整台機器的環境開始猜測,設定變更也能像原始碼變更一樣被追蹤。

常見問題

為什麼不能只比較專案檔或 xcconfig?

專案檔與 xcconfig 只代表輸入。xcodebuild 輸出還會解析繼承、條件式設定與命令列覆寫,因此更接近目標實際使用的設定。

哪些設定變更應該直接阻止流水線?

部署目標、支援架構、Swift 版本、最佳化層級、程式碼簽署方式與產品識別碼變更通常應阻止流水線;暫存路徑則應先正規化。

獨享 Apple Silicon

為下一次建置選擇獨享雲端 Mac

依工作負載選擇 Mac mini 機型、租期與區域。每筆訂單都對應獨立實體節點,實際可用性以控制台即時回傳結果為準。

選擇雲端 Mac 方案