雲端 Mac CI 如何可靠解析系統與 Xcode 輸出

CI/CD 實踐 ·約 9 分鐘閱讀

雲端 Mac CI 如何可靠解析系統與 Xcode 輸出

同一套 CI 指令碼在互動式終端機中執行正常,換到無人值守的雲端 Mac 後卻選錯 Scheme,常見原因通常不在 Xcode 本身,而是指令碼把「供人閱讀的文字」當成了介面。系統語言、工具版本、警告插入的位置,甚至只是一個額外空格,都可能導致 grep、cut 與固定行號失效。更穩健的做法是優先讀取 JSON 或 plist,並分別保存結束代碼、原始輸出與解析結果。

先找出不穩定的文字解析

先搜尋流水線中的高風險組合:

grep -RInE 'xcodebuild|simctl|system_profiler|sw_vers' ci scripts \
  | grep -E 'grep|awk|sed|cut|tail|head'

並非所有管線都不可靠。真正危險的是指令碼依賴自然語言,例如搜尋「Build Settings」、擷取冒號後的第二欄,或假設目標裝置一定位於第三行。LC_ALL=C 可以減少語言差異,卻無法固定欄位順序、警告內容或未來版本的輸出格式。

如果命令輸出主要供人閱讀,就不要預設它同時也是穩定的機器介面。

稽核時,應為每個讀取動作明確回答三個問題:需要哪個欄位、命令是否提供結構化格式,以及欄位缺失時應該失敗還是採用明確的預設值。不要讓空字串繼續流入後續的封存步驟。

將建置設定轉換為 JSON 契約

xcodebuild -showBuildSettings -json 可以直接傳回目標的建置設定。應先檢查命令結束代碼,再解析檔案,不能把標準錯誤混入 JSON。

set -euo pipefail

mkdir -p artifacts/environment

if ! xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings \
  -json \
  > artifacts/environment/build-settings.json \
  2> artifacts/environment/build-settings.stderr; then
  cat artifacts/environment/build-settings.stderr >&2
  exit 1
fi

python3 - <<'PY'
import json
from pathlib import Path

path = Path("artifacts/environment/build-settings.json")
if not path.exists() or path.stat().st_size == 0:
    raise SystemExit("build settings output is empty")

items = json.loads(path.read_text(encoding="utf-8"))
if len(items) != 1:
    raise SystemExit(f"expected one target, got {len(items)}")

settings = items[0].get("buildSettings", {})
required = ["PRODUCT_BUNDLE_IDENTIFIER", "SDKROOT", "CONFIGURATION_BUILD_DIR"]
missing = [key for key in required if not settings.get(key)]
if missing:
    raise SystemExit("missing build settings: " + ", ".join(missing))

print(settings["PRODUCT_BUNDLE_IDENTIFIER"])
PY

如果工作區包含多個目標,不要強制要求陣列長度為一。應根據 target 欄位選取目標,並在零個符合項目或出現多個符合項目時終止。如此一來,新增擴充功能目標後,指令碼就不會在沒有警告的情況下讀取錯誤設定。

使用 simctl JSON 固定裝置選擇

模擬器清單尤其不適合逐行解析。裝置名稱可能包含空格,同名裝置也可能分布在不同的執行階段中。先保存完整結果,再依執行階段、名稱與可用狀態進行篩選:

xcrun simctl list -j devices available \
  > artifacts/environment/simulators.json

python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(
    Path("artifacts/environment/simulators.json").read_text(encoding="utf-8")
)
matches = []
for runtime, devices in data.get("devices", {}).items():
    if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"):
        continue
    for device in devices:
        if device.get("isAvailable") and device.get("name") == "iPhone 16":
            matches.append((runtime, device["udid"]))

if len(matches) != 1:
    raise SystemExit(f"expected one matching simulator, got {len(matches)}")

print(matches[0][1])
PY

正式環境的指令碼還應將預期的執行階段版本寫入儲存庫設定,而不是自動選擇「最新」版本。自動選取最新版會讓系統元件更新直接改變測試基準。最終執行測試時傳入 UDID,比只指定裝置名稱更不容易產生歧義。

不要忽略空集合

「找不到符合的裝置」應明確視為失敗。在暫時建立模擬器之前,先確認所需的執行階段確實存在;工作結束後,只刪除本次工作所建立的裝置,不要清理共用裝置集。平行工作最好分別設定 -derivedDataPath,並使用彼此獨立的 CoreSimulator 裝置集。

從 plist 讀取主機資訊

macOS 版本資訊可以直接從系統 plist 讀取,避免解析 sw_vers 的標籤文字:

SYSTEM_PLIST=/System/Library/CoreServices/SystemVersion.plist

product_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductVersion' "$SYSTEM_PLIST"
)
build_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductBuildVersion' "$SYSTEM_PLIST"
)

printf '%s\n' "$product_version" > artifacts/environment/macos-version.txt
printf '%s\n' "$build_version" > artifacts/environment/macos-build.txt
xcodebuild -version > artifacts/environment/xcode-version.txt

讀取專案 plist 時,應優先使用 plutil -extract KEY raw -o - FILE。如果欄位可能不存在,先用 plutil -type KEY FILE 檢查,再由指令碼決定是否允許缺失。不要使用正規表示式處理 XML;plist 可能採用二進位格式,字串也可能經過逸出處理。

建立可診斷的資料收集邊界

採用結構化格式並不代表命令必然成功。可靠的收集步驟至少應保留四類證據:命令結束代碼、標準錯誤、未修改的原始檔案,以及解析後的精簡結果。原始檔案用於回顧工具行為,精簡結果則供後續工作使用。

建議讓每個收集器只負責一個物件,並將輸出寫入暫存檔案。只有在語法與所有必填欄位均通過檢查後,才使用 mv 以不可分割的方式取代正式檔案。這樣即使命令中斷,也不會留下不完整的 JSON。

提交前檢查項目

合併流水線變更前,至少確認以下事項:

  • 指令碼沒有依照固定行號或自然語言標籤擷取欄位。
  • 標準輸出與標準錯誤寫入不同檔案。
  • JSON 與 plist 在讀取前均經過語法檢查。
  • 陣列篩選對零個符合項目與多個符合項目都有明確處理。
  • Xcode、macOS 建置編號與選定裝置的 UDID 都能隨工作封存。
  • 日誌只記錄環境資訊,不輸出權杖、存取憑證或完整環境變數。
  • 更新工具版本後,先執行資料收集驗收,再開放正式建置工作。

最終目標不是把所有命令都改成 JSON,而是建立清楚的邊界:機器使用結構化資料,人員閱讀診斷日誌。確實沒有結構化介面時,也應固定命令版本、隔離語言環境、檢查欄位數量,並將解析失敗視為失敗,而不是繼續使用看似合理的空值。

常見問題

設定 LC_ALL=C 後還需要結構化輸出嗎?

需要。固定語言只能減少部分差異,無法保證欄位順序、空白、提示訊息與後續版本不變;JSON 或 plist 才適合自動化。

結構化命令失敗時應保留哪些檔案?

至少保留結束碼、標準錯誤、原始 JSON 或 plist、Xcode 版本及 macOS 建置編號,解析前也要檢查檔案非空且語法有效。

獨享實體 Mac

將下一項開發任務放到雲端 Mac 上

選擇 Berry M4 或 Berry M4 Pro,確認五個節點、租用期間與附加選項後提交訂單。

選擇設定並訂購