多人同時修改 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_IDENTIFIER、IPHONEOS_DEPLOYMENT_TARGET、SWIFT_VERSION、CODE_SIGN_STYLE 與 SUPPORTED_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。