一個被多個 App 引用的 Swift Framework,只要刪除一個公開方法、限縮某個參數型別,或替公開協定新增必要成員,就可能導致下游專案升級後無法編譯。單元測試通常著重於實作行為,卻不會明確指出「這次修改破壞了公開介面」。在 OnceMac 的雲端 Mac 建置節點上,可以把 Swift API Digester 納入合併檢查:先保存已發布版本的 API 快照,再用完全相同的工具鏈產生候選快照,最後比較兩者。
先固定比較條件
API 快照並不只由原始碼決定。Xcode 版本、Swift 編譯器、SDK、目標架構、建置設定與條件編譯旗標都會影響結果。第一步不是執行比較,而是把這些輸入寫入記錄並固定下來。
set -euo pipefail
xcodebuild -version
xcrun swift --version
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
echo "SDK_PATH=$SDK_PATH"
CI 應明確選擇 Xcode,並讓基準與候選分支使用相同映像或相同節點範本。目標三元組也必須保持一致,例如統一使用 arm64-apple-ios17.0-simulator。不要一邊產生實機介面,另一邊產生模擬器介面,否則平台條件差異會被誤判為程式碼變更。
API 基準是發布契約,不是編譯快取。它應納入版本控制並接受審查,一般建置工作不得自動覆寫。
建置可供分析的 Framework
以下範例假設 scheme 名稱與 module 名稱皆為 MyKit。先清除獨立的 Derived Data,再以 Release 設定進行建置。BUILD_LIBRARY_FOR_DISTRIBUTION=YES 會產生模組穩定性所需的介面檔案,也能讓檢查條件更貼近二進位發布情境。
DERIVED_DATA="$PWD/.build/api-dd"
PRODUCTS="$DERIVED_DATA/Build/Products/Release-iphonesimulator"
rm -rf "$DERIVED_DATA"
xcodebuild build \
-scheme MyKit \
-configuration Release \
-destination "generic/platform=iOS Simulator" \
-derivedDataPath "$DERIVED_DATA" \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
SKIP_INSTALL=NO \
CODE_SIGNING_ALLOWED=NO
建置完成後,先確認模組確實存在,避免 Digester 因搜尋路徑錯誤而產生難以理解的「module not found」。
test -d "$PRODUCTS/MyKit.framework"
find "$PRODUCTS/MyKit.framework/Modules" -maxdepth 3 -type f -print
如果專案透過工作區管理相依性,需要補上 -workspace;如果 scheme 未共享,CI 將無法找到它。此時應先在專案設定中共享 scheme,而不是讓指令碼猜測路徑。
產生並比較 API 快照
分別為目前的發布基準與候選程式碼建置一次,再輸出 JSON。建議將基準檔案放在儲存庫的 api-baselines/ 目錄中,並在檔名中記錄平台,無須寫入機器路徑或建置編號。
mkdir -p api-baselines .build/api-report
xcrun swift-api-digester \
-dump-sdk \
-module MyKit \
-sdk "$SDK_PATH" \
-target arm64-apple-ios17.0-simulator \
-F "$PRODUCTS" \
-o ".build/api-report/current-ios-simulator.json"
xcrun swift-api-digester \
-diagnose-sdk \
-input-paths "api-baselines/MyKit-ios-simulator.json" \
-input-paths ".build/api-report/current-ios-simulator.json" \
> ".build/api-report/diagnostics.txt"
首次導入時,將確認過的 current-ios-simulator.json 複製為基準並提交。此後 CI 只讀取該檔案。若命令傳回非零狀態,應保留診斷文字作為建置附件,而不是只顯示一句「相容性檢查失敗」。
應重點審查哪些變更
| 變更 | 預設判斷 | 處理方式 |
|---|---|---|
| 刪除公開型別或方法 | 破壞性 | 恢復介面,或納入明確的主版本變更 |
| 修改參數、回傳值或泛型約束 | 破壞性 | 提供相容的多載,並安排棄用週期 |
| 替公開協定新增必要成員 | 高風險 | 考慮提供預設實作 |
| 新增公開方法或型別 | 通常相容 | 檢查命名、可見性與平台標記 |
| 僅調整內部實作 | 不應出現 | 檢查存取層級是否意外擴大 |
public 不一定代表有意作出的承諾。如果某個宣告不應由外部呼叫,應優先限縮存取層級,而不是長期維護忽略規則。
將診斷結果轉為可維護的門禁
建議把流程拆成「建置」「產生快照」「比較」「上傳報告」四個步驟。指令碼應啟用 set -euo pipefail,並將 Derived Data 放進工作專用目錄,避免平行作業互相讀取產物。對於多模組儲存庫,可以維護一份模組清單並逐一執行,不要重複使用上一個模組的 PRODUCTS 路徑。
門禁失敗後,審查者需要看到三項資訊:使用的 Xcode 與 Swift 版本、基準和候選快照,以及完整的診斷文字。只有在確認變更符合版本策略時,才可在同一個合併請求中更新基準。如果允許失敗的工作自行重寫基準,檢查將永遠通過,也就失去意義。
處理誤報的順序
先核對工具鏈,再檢查 SDK 與目標三元組,接著比較建置參數和條件編譯旗標,最後才判斷是否屬於 Digester 的輸出雜訊。可利用以下清單縮短排查時間:
xcodebuild -version是否完全一致;- scheme、設定與 destination 是否一致;
- 兩邊是否都啟用了
BUILD_LIBRARY_FOR_DISTRIBUTION; - 模組搜尋路徑是否指向本次工作的產物;
- 基準是否來自最後一個已發布介面,而不是任意的歷史提交;
- 產生的檔案中是否混入工作目錄等容易變動的絕對路徑。
最後以版本策略收尾
相容性門禁只負責找出變更,不能替團隊決定版本號。刪除介面、修改公開型別或新增協定要求,通常需要納入不相容版本計畫;新增介面仍須經過命名與可用性審查;已棄用的介面則應保留約定的週期,再由後續版本移除。
最穩妥的流程是:由發布分支保存基準,合併請求只產生候選快照,CI 輸出機器診斷,維護者再依版本策略作出決定。如此既不會一律禁止所有 API 變更,也不會讓一次無意的 public 修改直接影響所有下游專案。
常見問題
Swift API Digester 可以取代單元測試嗎?
不可以。它檢查公開 Swift 介面的結構變化,不驗證業務行為、執行期語意或 Objective-C 介面,應與單元測試和整合測試並行執行。
應該在什麼時候更新 API 基準?
只有團隊確認介面變更符合版本策略後才更新。基準檔應與程式碼一起審查,不能由一般 CI 工作自動覆寫。
為什麼相同程式碼會得到不同的比較結果?
常見原因是 Xcode、Swift 編譯器、SDK、目標架構或建置設定不同。產生基準與執行比較時必須固定這些條件。
把下一次建置放到獨立實體節點上。
選擇晶片、記憶體、儲存空間、節點與租期,取得不與其他客戶共用資源的雲端 Mac。