同時包含 iOS App 與 watchOS App 的封存檔,即使主應用程式編譯成功,仍可能無法交付:手錶端未正確嵌入、組建編號落後一版、延伸功能仍指向舊的 Bundle ID,或某個內層元件未正確簽章。這類問題不應等到匯出或提交階段才被發現。更穩妥的做法,是讓雲端 Mac 在每次產生 xcarchive 後直接檢查最終套件結構,並將結果設為流水線門禁。
為什麼要檢查最終封存檔
專案檔案描述的是預期結果,xcarchive 才是實際交付物。不同的設定檔、組建指令碼和環境變數都可能覆寫版本號或產品識別碼。只讀取專案設定,無法證明 watchOS App 已經出現在主應用程式的 Watch 目錄中,也無法證明延伸功能與外層應用程式之間的關係正確。
建議將驗收對象固定為 Release 封存檔,而不是 DerivedData 中的中間目錄。先使用與正式流水線相同的工作區、Scheme 和設定產生建置產物:
set -euo pipefail
ROOT="$PWD"
OUT="$ROOT/out"
ARCHIVE="$OUT/Client.xcarchive"
rm -rf "$ARCHIVE"
mkdir -p "$OUT"
xcodebuild \
-workspace Client.xcworkspace \
-scheme Client \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
clean archive
clean archive 適合用於基準驗收;日常流水線可依快取策略調整,但驗收指令碼必須始終讀取本次工作產生的封存檔,不能退回使用某個「最近產生」的目錄。
門禁應回答「這次準備交付的套件裡有什麼」,而不是「專案理論上應該產生什麼」。
從套件結構建立元件清單
典型路徑為 Products/Applications/*.app,其中的 Watch 目錄包含 watchOS App,而手錶應用程式的 PlugIns 目錄再包含延伸功能。不要在指令碼中寫死產品名稱;Target 重新命名後,固定路徑很容易失效。
IOS_APP=$(find "$ARCHIVE/Products/Applications" \
-maxdepth 1 -type d -name '*.app' -print -quit)
test -n "${IOS_APP:-}" || {
printf '%s
' "iOS app not found"
exit 1
}
WATCH_APP=$(find "$IOS_APP/Watch" \
-maxdepth 1 -type d -name '*.app' -print -quit)
test -n "${WATCH_APP:-}" || {
printf '%s
' "watchOS app not found"
exit 1
}
WATCH_EXTENSION=$(find "$WATCH_APP/PlugIns" \
-maxdepth 1 -type d -name '*.appex' -print -quit)
printf 'ios=%s
watch=%s
extension=%s
' \
"$IOS_APP" "$WATCH_APP" "${WATCH_EXTENSION:-embedded}"
部分較新的專案沒有獨立的 .appex 層,因此找不到延伸功能時不能直接判定失敗。門禁應依據儲存庫中宣告的產品結構作出判斷:舊式結構要求三層元件,較新的單一 Target 結構只要求 iOS App 與 watchOS App。可以在儲存庫中保存一份簡短的預期清單,而不是根據目錄結構猜測專案類型。
驗證版本與識別碼關係
先對每個 Info.plist 執行 plutil -lint,再讀取 CFBundleIdentifier、CFBundleShortVersionString 和 CFBundleVersion。同一次發佈通常應讓 iOS 與 watchOS 使用完全相同的行銷版本與組建編號。如果團隊採用各自獨立的組建編號,也應在指令碼中明確定義允許的規則。
| 檢查項目 | 建議規則 | 失敗風險 |
|---|---|---|
| 行銷版本 | iOS 與 watchOS 相同 | 商店版本關係不一致 |
| 組建編號 | 同一發佈工作中相同 | 手錶端被識別為舊組建 |
| 伴隨識別碼 | 指向 iOS 主應用程式 ID | 安裝後無法建立伴隨關係 |
| 延伸功能歸屬 | 指向目前的 watchOS App ID | 延伸功能與外層應用程式不相符 |
read_plist() {
/usr/libexec/PlistBuddy -c "Print :$2" "$1"
}
IOS_PLIST="$IOS_APP/Info.plist"
WATCH_PLIST="$WATCH_APP/Info.plist"
plutil -lint "$IOS_PLIST" "$WATCH_PLIST"
IOS_ID=$(read_plist "$IOS_PLIST" CFBundleIdentifier)
WATCH_ID=$(read_plist "$WATCH_PLIST" CFBundleIdentifier)
IOS_VERSION=$(read_plist "$IOS_PLIST" CFBundleShortVersionString)
WATCH_VERSION=$(read_plist "$WATCH_PLIST" CFBundleShortVersionString)
IOS_BUILD=$(read_plist "$IOS_PLIST" CFBundleVersion)
WATCH_BUILD=$(read_plist "$WATCH_PLIST" CFBundleVersion)
COMPANION_ID=$(read_plist "$WATCH_PLIST" WKCompanionAppBundleIdentifier)
test "$IOS_VERSION" = "$WATCH_VERSION"
test "$IOS_BUILD" = "$WATCH_BUILD"
test "$COMPANION_ID" = "$IOS_ID"
不要以「watchOS Bundle ID 必須以 iOS Bundle ID 開頭」取代精確比對。團隊可能使用明確指定的識別碼,前綴相似並不能證明綁定關係正確。應從發佈設定讀取預期值,並驗證 WKCompanionAppBundleIdentifier 的完整內容。
相容包含延伸功能的結構
如果封存檔中存在 .appex,請繼續讀取其中的 WKAppBundleIdentifier,並要求該值等於目前 watchOS App 的 Bundle ID。同時記錄延伸功能本身的版本與組建編號,避免某個 Target 漏掉統一版本指令碼。
檢查簽章與可執行檔架構
外層應用程式成功簽章,不代表每個巢狀元件都能通過驗證。與其只對主應用程式執行遞迴檢查,不如逐一列舉實際元件,讓日誌明確指出驗證失敗的對象:
verify_component() {
local item="$1"
codesign --verify --strict --verbose=2 "$item"
codesign -d --entitlements :- "$item" >/dev/null
}
verify_component "$IOS_APP"
verify_component "$WATCH_APP"
if test -n "${WATCH_EXTENSION:-}"; then
verify_component "$WATCH_EXTENSION"
fi
接著根據各自的 CFBundleExecutable 找出二進位檔,並使用 file 或 lipo -archs 記錄架構。門禁不必永久寫死某一組架構,因為工具鏈和部署目標都可能改變;更可靠的規則是拒絕空白的可執行檔、拒絕非預期的模擬器產物,並保存目前的架構清單以供檢閱。
此外,也要檢查巢狀元件中是否有符號連結越界、重複的 Bundle ID,或額外且未宣告的延伸功能。這些問題通常源自複製指令碼使用了範圍過大的萬用字元。
將驗收結果轉化為穩定門禁
指令碼的結束碼只負責阻擋流程,報告則負責說明原因。每次工作都應保存元件的相對路徑、Bundle ID、版本、組建編號、架構、簽章驗證狀態和封存檔校驗和。報告中不要收集憑證私鑰、完整的環境變數內容或完整的使用者目錄路徑。
建議將流程拆分為四個步驟:
- 產生唯一的封存目錄,禁止重複使用上一次的產物。
- 探索元件,並與儲存庫中的預期結構比較。
- 驗證中繼資料、關聯欄位、簽章和可執行檔架構。
- 輸出文字或 JSON 報告,再執行匯出工作。
首次啟用門禁時,可以先執行數次僅記錄、不阻擋的檢查,以確認新舊專案結構都已涵蓋;規則穩定後,再將元件缺失、版本不相符和簽章失敗設為硬性錯誤。如果同一台雲端 Mac 同時執行多個封存工作,必須為每個工作分配獨立的 archivePath 與報告目錄,避免其中一個工作讀取到另一個工作的結果。
最終目標不是增加一輪形式化檢查,而是讓流水線能夠證明:主應用程式、手錶應用程式及其延伸功能確實來自同一次發佈情境,彼此關係明確,而且最終封存檔可以繼續進入匯出階段。
常見問題
只核對 Xcode 專案設定,能取代 xcarchive 驗收嗎?
不能。專案設定只代表預期值,實際封存產物還會受到建置腳本、設定覆寫與簽章流程影響。
iOS App 與 watchOS App 的版本是否必須一致?
同一次發佈通常應讓行銷版本與建置編號一致;若採獨立策略,CI 必須明確定義並驗證允許的對應關係。
在雲端持續執行你的 Mac mini 工作流程
選擇 RunAMac M4、租用週期與部署節點,將開發、建置或實驗工作放到一台獨享實體主機上。