一份在本機模擬器上正常執行的 iOS 專案,移至 HopVM 雲端 Mac 完成 Archive 後,仍可能直到安裝階段才暴露問題:某個預編譯 Framework 混入了 x86_64 切片、擴充功能連結到錯誤的平台,或巢狀動態函式庫悄悄提高了最低系統版本。與其等到交付後才排查,不如直接掃描匯出的 .app,把二進位檔的實際狀態轉化為流水線門禁。
為什麼要檢查最終匯出套件
Xcode 專案設定只能說明「準備如何建置」,最終套件才能說明「實際交付了什麼」。主程式、App Extension、Framework 與 .dylib 都可能擁有各自獨立的 Mach-O 標頭;相依套件管理工具下載的二進位套件,也未必會繼承主專案的設定。
門禁至少必須回答三個問題:
- 面向實體 iOS 裝置的二進位檔是否只包含允許的架構。
- 每個 Mach-O 的平台是否為 iOS,而不是模擬器或 macOS。
- 巢狀二進位檔的最低系統版本是否高於主 App 的宣告。
不要把「Archive 成功」視為交付驗收。連結器只會驗證目前目標能否組成建置產物,不會代替團隊判斷所有巢狀檔案是否符合發布基準。
應檢查透過 xcodebuild -exportArchive 產生的最終套件,而不是只掃描 DerivedData。匯出過程會重新組織 Framework、擴充功能與簽章內容,這才是最接近實際交付狀態的輸入。
固定輸入與驗收基準
先固定封存檔、匯出目錄與報告目錄,避免腳本掃描到前一次工作的殘留檔案。雲端 Mac 由多條流水線依序使用時,工作目錄的隔離尤其重要。
set -euo pipefail
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_PATH="$PWD/output/export"
REPORT_PATH="$PWD/output/macho-report.tsv"
rm -rf "$EXPORT_PATH"
mkdir -p "$EXPORT_PATH"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$PWD/ci/ExportOptions.plist"
APP_PATH="$(find "$EXPORT_PATH" -maxdepth 2 -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
產品基準應納入版本庫,而不是散落在 CI 設定介面中。例如,可使用 ci/macho-policy.env 儲存允許的架構與最低版本:
EXPECTED_ARCHS="arm64"
EXPECTED_PLATFORM="IOS"
DECLARED_MIN_IOS="17.0"
DECLARED_MIN_IOS 必須符合專案實際支援的範圍。此處版本僅為腳本範例,不能複製後直接當成產品決策。
遞迴列舉所有 Mach-O
不能只依副檔名搜尋,因為 Framework 的主二進位檔通常沒有副檔名。更穩妥的方式是逐一遍歷檔案,再由 file 判斷是否屬於 Mach-O。
: > "$REPORT_PATH"
failure=0
while IFS= read -r -d '' candidate; do
if ! file -b "$candidate" | grep -q 'Mach-O'; then
continue
fi
archs="$(lipo -archs "$candidate" 2>/dev/null || true)"
build="$(vtool -show-build "$candidate" 2>/dev/null || true)"
platform="$(awk '/platform/{print $2; exit}' <<<"$build")"
minos="$(awk '/minos/{print $2; exit}' <<<"$build")"
printf '%s %s %s %s
' \
"${candidate#"$APP_PATH"/}" "$archs" "$platform" "$minos" \
>> "$REPORT_PATH"
if [[ "$archs" != "$EXPECTED_ARCHS" ]]; then
printf 'architecture mismatch: %s (%s)
' "$candidate" "$archs" >&2
failure=1
fi
if [[ "$platform" != "$EXPECTED_PLATFORM" ]]; then
printf 'platform mismatch: %s (%s)
' "$candidate" "$platform" >&2
failure=1
fi
done < <(find "$APP_PATH" -type f -print0)
exit "$failure"
報告採用定位字元分隔,既可作為建置附件保留,也方便後續轉換成表格。失敗訊息必須包含檔案路徑與實際值;若只輸出「架構檢查失敗」,排障人員就不得不重新執行完整工作。
不要在門禁中直接修改套件
發現 x86_64 後直接執行 lipo -remove 看似省事,卻會變更已簽章的內容,也會掩蓋相依套件生產流程中的問題。正確做法是確認該檔案來自原始碼編譯、二進位相依套件,還是複製腳本,接著修正上游並重新 Archive。
比對最低系統版本
先讀取主 App 的產品宣告:
PLIST="$APP_PATH/Info.plist"
APP_MIN_IOS="$(/usr/libexec/PlistBuddy \
-c 'Print :MinimumOSVersion' "$PLIST")"
printf 'declared minimum iOS: %s
' "$APP_MIN_IOS"
接著從每個 Mach-O 的 LC_BUILD_VERSION 讀取 minos。判斷時應使用語意化版本比較,不能採用一般字串比較,否則 17.10 可能會被錯誤地排在 17.9 前面。
| 檢查對象 | 讀取位置 | 失敗條件 |
|---|---|---|
| 主 App 宣告 | Info.plist 的 MinimumOSVersion |
與版本庫基準不一致 |
| 主程式 | LC_BUILD_VERSION 的 minos |
高於產品宣告 |
| Framework 與動態函式庫 | 各自的 LC_BUILD_VERSION |
高於產品宣告 |
| App Extension | 擴充功能 Info.plist 與 Mach-O | 宣告值或實際值超出基準 |
可使用系統內建的版本排序功能完成比較:
version_gt() {
[[ "$1" != "$2" ]] &&
[[ "$(printf '%s
%s
' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
}
if version_gt "$minos" "$APP_MIN_IOS"; then
printf 'minimum iOS mismatch: %s requires %s
' \
"$candidate" "$minos" >&2
failure=1
fi
如果 vtool 沒有傳回平台或版本,不應靜默略過。應先使用 otool -l 儲存完整的載入命令,再確認它究竟是舊格式二進位檔、異常檔案,還是腳本誤判。
接入 CI 並處理常見誤報
將檢查安排在 Archive 與匯出之後、上傳之前,並且一律上傳 macho-report.tsv。即使工作失敗,也應透過 CI 的失敗後附件機制保留報告。
常見誤報主要分為三類:
- 掃描到了符號檔或封存目錄,而不是最終
.app。 - 不同工具輸出平台名稱時使用了不同的大小寫,腳本卻直接進行字串等值比較。
- 將 macOS 輔助工具的策略套用到 iOS App,導致合法的通用二進位檔被錯誤攔截。
因此,腳本應依產品類型分別維護策略,不要讓同一套規則同時涵蓋 iOS、模擬器測試套件與 macOS 工具。若流水線同時產生多種建置產物,應針對每個匯出目錄分別執行,並在報告第一欄寫入產物類型。
最後再執行一次簽章結構驗證:
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
這不能取代 Mach-O 檢查,但能發現腳本誤改二進位檔、巢狀簽章失效等後續問題。完整流程應依序為匯出、掃描、版本比對、簽章驗證與保存報告,完成後才允許交付。如此一來,每次失敗都能定位到具體檔案、具體欄位與具體修正入口,而不是等到安裝失敗後才猜測原因。
常見問題
為什麼不能只檢查主 App 的架構?
Framework、App Extension 與動態程式庫各自包含 Mach-O。任一巢狀檔案若帶入模擬器切片、錯誤平台或過高的最低系統版本,都可能造成安裝或啟動失敗。
在 iOS 交付檔看到 x86_64 時該怎麼辦?
先找出檔案來源,不要直接刪除切片後交付。應修正依賴建置或封存流程,再重新匯出只包含實機平台與架構的套件。
最低系統版本要讀 Info.plist 還是 Mach-O?
兩者都要讀。Info.plist 代表產品宣告,LC_BUILD_VERSION 代表實際編譯結果;所有巢狀二進位檔的 minos 都不應高於產品宣告。
用一台可依專案啟停的雲端 Mac 重現工作流程
從三種 Apple Silicon 配置中選擇合適的資源,按日、週、月或季租用。設備為獨享物理機,並非虛擬機器;實際可用狀態以控制台即時回傳的資訊為準。