同一筆提交在開發者電腦上封存成功,改到雲端 Mac 或夜間流水線後也顯示成功,但兩份產物的雜湊值卻不相同。先不要把問題歸因於 Xcode 快取。簽章、壓縮時間、產生的原始碼、指令碼輸出順序與絕對路徑,都可能寫入產物。更有效的做法,是在同一個 ArmVMS 節點上建立兩套完全隔離的建置目錄,連續執行兩次未簽署封存,再由外而內比對差異。
先定義「可重現」的檢查邊界
已簽署的 IPA 很難直接做到位元組完全一致,因為簽章資料、描述檔與封裝中繼資料都可能發生變化。工程檢查應拆成兩個層級:
- 內容層:使用相同的提交、工具鏈、相依套件鎖定檔與建置參數,產生相同的應用程式資源與可執行檔。
- 交付層:匯出方式、簽署設定、權限宣告與套件內檔案符合發布要求,但不強求整份 IPA 的雜湊值相同。
先證明未簽署的應用程式內容穩定,再檢查簽署與匯出。直接比較兩份 IPA 的整體雜湊值,只能確認「兩者不同」,無法指出差異來自哪裡。
開始前,請記錄提交雜湊、Xcode 路徑、SDK、Scheme、Configuration,以及 Package.resolved、Podfile.lock 等鎖定檔。工作區必須保持乾淨,尚未提交的產生檔案也要納入檢查。
使用隔離目錄執行兩次封存
兩次建置不能共用 DerivedData,否則第二次建置可能會讀取第一次留下的中間產物。下列指令碼會固定語言環境與時區,為每次封存配置獨立目錄,並停用程式碼簽署:
#!/bin/zsh
set -euo pipefail
export LC_ALL=C
export LANG=C
export TZ=UTC
ROOT="$PWD"
SCHEME="ExampleApp"
for RUN in A B; do
rm -rf "$ROOT/.repro/$RUN"
mkdir -p "$ROOT/.repro/$RUN"
xcodebuild archive \
-workspace "$ROOT/ExampleApp.xcworkspace" \
-scheme "$SCHEME" \
-configuration Release \
-destination "generic/platform=iOS" \
-derivedDataPath "$ROOT/.repro/$RUN/DerivedData" \
-archivePath "$ROOT/.repro/$RUN/App.xcarchive" \
CODE_SIGNING_ALLOWED=NO \
COMPILER_INDEX_STORE_ENABLE=NO
done
如果專案必須在指令碼階段讀取環境變數,應明確傳入固定值,不要繼承互動式 Shell 中的暫時設定。兩次封存也應使用相同的 DEVELOPER_DIR,避免預設 Xcode 路徑遭到切換。
從檔案清單縮小差異範圍
不要一開始就對二進位檔進行十六進位比較。先確認兩份 .app 是否包含相同的路徑:
APP_A=".repro/A/App.xcarchive/Products/Applications/ExampleApp.app"
APP_B=".repro/B/App.xcarchive/Products/Applications/ExampleApp.app"
find "$APP_A" -type f | sed "s#^$APP_A/##" | LC_ALL=C sort > /tmp/files-a.txt
find "$APP_B" -type f | sed "s#^$APP_B/##" | LC_ALL=C sort > /tmp/files-b.txt
diff -u /tmp/files-a.txt /tmp/files-b.txt
如果檔案數量不同,通常表示 Run Script 階段、資源產生器或複製規則中存在不穩定的輸入。確認路徑一致後,再逐一計算檔案雜湊,並排除已知會發生變化的簽章目錄:
(
cd "$APP_A"
find . -type f ! -path "./_CodeSignature/*" -print0 |
sort -z | xargs -0 shasum -a 256
) > /tmp/hash-a.txt
(
cd "$APP_B"
find . -type f ! -path "./_CodeSignature/*" -print0 |
sort -z | xargs -0 shasum -a 256
) > /tmp/hash-b.txt
diff -u /tmp/hash-a.txt /tmp/hash-b.txt
| 第一個差異位置 | 優先檢查項目 |
|---|---|
| Info.plist | 自動寫入的日期、主機名稱、路徑或流水線編號 |
| Assets.car | 資源輸入、工具鏈版本與資源目錄順序 |
| 可執行檔 | 編譯參數、產生的原始碼、物件檔與連結順序 |
| Framework | 相依套件鎖定檔、二進位相依套件版本與嵌入指令碼 |
| 文字或 JSON | 未排序的集合、隨機識別碼與地區格式 |
深入檢查設定與 Mach-O
屬性列表可能只因鍵值順序不同而產生位元組差異。複製檔案後,使用 plutil -convert xml1 進行正規化,再執行文字比較。不要直接修改封存原檔,否則會污染後續證據。
可執行檔不相同時,先查看 UUID:
dwarfdump --uuid "$APP_A/ExampleApp"
dwarfdump --uuid "$APP_B/ExampleApp"
xcodebuild \
-workspace ExampleApp.xcworkspace \
-scheme ExampleApp \
-configuration Release \
-showBuildSettings > /tmp/build-settings.txt
UUID 不同只代表連結結果不同,並不能直接證明業務程式碼有所變更。接著檢查 OTHER_SWIFT_FLAGS、OTHER_LDFLAGS、搜尋路徑、部署目標與實際工具路徑。如果產生的程式碼寫入目前日期、暫存目錄或未排序字典的走訪結果,也會導致後續編譯結果全面漂移。
請特別留意 #filePath、偵錯對應資訊,以及指令碼輸出中的工作區絕對路徑。CI 工作目錄變更時,可以統一簽出路徑,或適當設定偵錯前綴對應,以減少路徑差異;但不要為了讓雜湊值一致而刪除偵錯所需的資訊。
將檢查轉為可維護的門禁機制
門禁機制不應只規定「兩份 IPA 的雜湊值必須相同」。更穩妥的策略是保留差異報告,並依層級進行判定:
必須阻擋的變更
同一筆提交若出現檔案缺失、資源內容變更、權限宣告變更、嵌入的 Framework 版本變更,或主可執行檔在相同輸入下結果不穩定,應立即阻擋,並保留兩份封存。
可獨立審查的變更
簽章目錄、匯出記錄,以及明確標示的封裝時間,均屬於交付層差異,可以進入獨立檢查;但必須限制允許變更的路徑,不能採用「忽略所有 plist」這類過於寬泛的規則。
修正後,應重新建立兩套空白目錄進行複驗,而不是重複使用舊的 DerivedData。大型專案不必在每筆提交上都執行兩次封存,可以在 Xcode 版本、相依套件鎖定檔、產生器、Run Script 或發布設定變更時觸發。如此得到的不是一條脆弱的雜湊規則,而是一套能夠指出差異發生在哪個層級的建置證據鏈。
常見問題
為什麼不能只比較兩份已簽章 IPA 的雜湊?
簽章資料、描述檔與封裝時間可能出現預期差異。應先比較未簽章的應用程式內容,再分開驗證匯出階段。
Mach-O UUID 不同是否代表原始碼已改變?
不一定。物件檔順序、產生的原始碼、編譯參數、連結輸入與工具路徑也可能改變 UUID。
每次提交都需要執行可重現性檢查嗎?
小型專案可定期完整執行;大型專案適合在工具鏈、鎖定檔、建置腳本或發佈設定變更時執行。
為下一次建置選擇專屬 Apple Silicon 實體節點
按日、週、月或季租用 ArmVMS M4 與 ArmVMS M4 Pro。建立訂單前,可逐項確認設定、節點與附加項目。