工程實務

雲端 Mac 的 Xcode 專案檔一致性門禁實作

雲端 Mac 的 Xcode 專案檔一致性門禁實作

多人同時修改 Xcode 專案時,最危險的問題往往不是編譯錯誤,而是 project.pbxproj 已經損壞,卻仍能通過文字合併。檔案參照遭到刪除、Scheme 未共享,或關鍵建置設定被介面操作悄悄改寫,都可能直到歸檔階段才暴露。更穩妥的做法,是在雲端 Mac 上將專案檔視為獨立的建置輸入,先執行一輪成本較低的一致性檢查,再開始解析相依套件、測試與歸檔。

門禁應檢查哪些項目

一套實用的門禁至少要涵蓋四個層級,而且順序不能顛倒:先檢查文字殘留,再驗證檔案格式,接著讓 Xcode 實際解析專案,最後比較關鍵設定。前一層失敗時應立即結束,不必繼續耗費建置時間。

層級 檢查對象 失敗代表什麼
文字 衝突標記、空白檔案 合併尚未完成
結構 project.pbxproj plist 結構無法解析
專案 Project、Target、Scheme Xcode 無法建立專案模型
設定 SDK、部署版本、簽署方式 設定出現非預期漂移

git diff 沒有衝突,不代表 Xcode 專案有效。版本控制工具只能確認文字合併已完成,並不知道物件參照、Target 或 Scheme 是否仍能由 Xcode 識別。

檢查腳本應固定執行目錄與 Xcode 路徑,不要依賴互動式 Shell 的目前狀態。若專案同時包含 .xcodeproj.xcworkspace,日常建置應以實際使用的進入點為準,但底層的 project.pbxproj 仍須個別檢查。

先攔截合併殘留與格式錯誤

可以將以下腳本儲存為 ci/check_xcode_project.sh。範例假設專案名稱為 App.xcodeproj;實際使用時請透過環境變數覆寫,避免將專案名稱分散在多個 CI 設定中。

#!/bin/bash
set -euo pipefail

PROJECT_PATH="${PROJECT_PATH:-App.xcodeproj}"
PBXPROJ="${PROJECT_PATH}/project.pbxproj"

test -s "$PBXPROJ" || {
  echo "project.pbxproj is missing or empty"
  exit 1
}

if grep -nE '^(<<<<<<<|=======|>>>>>>>)' "$PBXPROJ"; then
  echo "merge conflict markers found"
  exit 1
fi

plutil -lint "$PBXPROJ"
xcodebuild -list -json -project "$PROJECT_PATH" > /tmp/xcode-project-list.json
plutil -lint /tmp/xcode-project-list.json

衝突標記採用行首比對,避免業務檔名或註解中偶然出現一連串等號而造成誤報。plutil 通過後,再呼叫 xcodebuild -list,因為只有後者會建立 Xcode 專案模型。命令以非零狀態結束時,應保留標準錯誤輸出,而不是自動執行「修復」或重新產生專案檔。

Workspace 專案的處理方式

使用 Workspace 時,還要增加一次進入點驗證:

WORKSPACE_PATH="${WORKSPACE_PATH:-App.xcworkspace}"
xcodebuild -list -json -workspace "$WORKSPACE_PATH" \
  > /tmp/xcode-workspace-list.json
plutil -lint /tmp/xcode-workspace-list.json

不要只驗證 Workspace 而略過底層專案。Workspace 能被識別,並不保證其中每個專案檔都沒有合併殘留。

驗證共享 Scheme 與目標集合

CI 所需的 Scheme 必須納入版本控制。可以先檢查共享 Scheme 檔案,再從 xcodebuild -list 的 JSON 輸出確認名稱。系統內建的 Python 已足以完成解析,不需要額外安裝相依套件。

SCHEME_NAME="${SCHEME_NAME:-App}"
SCHEME_FILE="${PROJECT_PATH}/xcshareddata/xcschemes/${SCHEME_NAME}.xcscheme"

test -s "$SCHEME_FILE" || {
  echo "shared scheme is missing: $SCHEME_NAME"
  exit 1
}

python3 - "$SCHEME_NAME" /tmp/xcode-project-list.json <<'PY'
import json
import sys

expected = sys.argv[1]
path = sys.argv[2]

with open(path, encoding="utf-8") as handle:
    payload = json.load(handle)

schemes = payload.get("project", {}).get("schemes", [])
if expected not in schemes:
    raise SystemExit(f"expected scheme not found: {expected}")
PY

若儲存庫包含多個應用程式或擴充功能,應維護明確的 Scheme 清單,而不是接受「至少存在一個 Scheme」。同時,不要將個人目錄下的 Scheme 當作 CI 輸入;切換至另一個實體節點後,這些未共享的檔案不會存在。

為關鍵建置設定建立快照

專案即使能成功解析,設定仍可能遭到誤改。建議只挑選會改變產出內容意義的欄位建立快照,例如 PRODUCT_BUNDLE_IDENTIFIERIPHONEOS_DEPLOYMENT_TARGETSWIFT_VERSIONCODE_SIGN_STYLESUPPORTED_PLATFORMS。不要儲存完整的 -showBuildSettings 輸出,因為其中包含路徑與暫存目錄,跨節點比較時會產生大量雜訊。

xcodebuild -project "$PROJECT_PATH" \
  -scheme "$SCHEME_NAME" \
  -configuration Release \
  -showBuildSettings |
awk -F ' = ' '
  /PRODUCT_BUNDLE_IDENTIFIER =/ ||
  /IPHONEOS_DEPLOYMENT_TARGET =/ ||
  /SWIFT_VERSION =/ ||
  /CODE_SIGN_STYLE =/ ||
  /SUPPORTED_PLATFORMS =/ {
    gsub(/^[ 	]+/, "", $1)
    print $1 " = " $2
  }
' | LC_ALL=C sort > /tmp/build-settings.current

diff -u ci/build-settings.release /tmp/build-settings.current

首次建立 ci/build-settings.release 後,應將其納入版本控制。有意調整部署版本或簽署方式時,先審查設定差異,再於同一項變更中更新快照。禁止讓 CI 自動覆寫基準,否則任何漂移都會被視為新的標準。

接入管線並處理誤報

將一致性門禁放在下載相依套件與完整建置之前,並讓腳本的失敗階段清楚可辨。建議順序為:簽出程式碼、選擇固定的 Xcode、執行專案門禁、解析相依套件、編譯、測試、歸檔。腳本在本機與雲端 Mac 上應使用同一個進入點,例如:

DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" \
PROJECT_PATH="App.xcodeproj" \
SCHEME_NAME="App" \
bash ci/check_xcode_project.sh

常見誤報來自三個地方。第一,開發者修改了 Scheme 卻未共享;解決方式是提交 xcshareddata/xcschemes,而不是在節點上手動建立。第二,快照包含絕對路徑;應縮小欄位集合。第三,不同任務使用了不同的 Xcode 路徑;應在門禁開始前輸出並核對 xcodebuild -version,同時固定 DEVELOPER_DIR

檢查失敗時,工單或建置紀錄至少要保留提交識別碼、Xcode 版本、失敗命令、標準錯誤與專案進入點。不要上傳包含敏感值的完整環境變數。若需要更換實體節點,應在控制台確認目前可選的設定,並讓新節點從同一個儲存庫重新執行門禁,而不是複製舊節點的暫時專案狀態。

合併前檢查清單

提交門禁前,請逐項確認:腳本已啟用 set -euo pipefail;專案路徑與 Scheme 可透過環境變數覆寫;衝突標記檢查只掃描專案檔;Project 與 Workspace 依實際進入點分別解析;共享 Scheme 已納入版本控制;設定快照只保留穩定欄位;任何基準更新都經過人工審查。

這套檢查無法取代編譯與測試,但能將一類原本要到歸檔階段才會出現的問題,提前到數秒即可完成的步驟。當專案檔成為明確、可審查且可重複執行的輸入後,團隊便不必再依賴「某位開發者在本機仍能開啟專案」來判斷主分支是否健康。

常見問題

只執行 plutil 就能確認 Xcode 專案正常嗎?

不能。plutil 主要檢查 project.pbxproj 的結構與語法,還要執行 xcodebuild -list,確認 Xcode 能解析專案且預期的 Scheme 可見。

一致性門禁應該安排在完整建置之前嗎?

是。它應先在每個合併請求執行,再於共享分支重跑,以較低成本阻止損壞的專案檔進入後續建置。

團隊刻意修改建置設定時該怎麼處理?

先審查差異,再明確更新納入版本控制的設定快照。不要讓 CI 自動覆寫基準,否則無法辨認非預期漂移。

獨享實體 Mac mini

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

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

選擇配置並租用