工程文章

在雲端 Mac CI 稽核 iOS 動態函式庫依賴與匯出符號

在雲端 Mac CI 稽核 iOS 動態函式庫依賴與匯出符號

一個 iOS 專案在開發分支上或許能正常編譯,Archive 中卻可能悄悄多出動態 Framework、指向開發機目錄的絕對連結,或暴露原本僅供內部使用的全域符號。只審查專案檔案很難涵蓋這些變化,因為真正交付的是連結完成後的 Mach-O。更穩妥的做法,是讓雲端 Mac CI 直接檢查封存產物,並將依賴與符號變化轉換成必須審核的差異。

先定義要攔截的變化

這類門禁不應判斷某個函式庫「好或不好」,而應回答三個可驗證的問題:

  1. 主程式與內嵌 Framework 實際依賴了哪些項目?
  2. @rpath 指向的自帶函式庫是否確實存在於 App 套件內?
  3. 新增的全域符號是否屬於預期介面?

基準並非永久白名單,而是一次經過人工確認的產物快照。工具鏈升級、依賴更新或介面調整後可以更新基準,但必須與程式碼變更一併審查。

建議將檢查安排在 Release Archive 之後。Debug 建置可能包含診斷函式庫,最佳化結果也可能不同,因此無法代表最終交付邊界。首次導入時只產生報告,不要立即阻擋流程;待團隊確認基準內容後,再讓差異觸發失敗狀態。

從 Archive 擷取檢查對象

先建立獨立封存,避免重複使用開發者本機的歷史產物:

rm -rf out/App.xcarchive
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$PWD/out/App.xcarchive" \
  clean archive

如果簽章由後續的獨立工作處理,可依專案實際情況設定 CODE_SIGNING_ALLOWED=NO。不要機械式地將這個覆寫項加入所有專案;若專案包含特定能力或自訂建置階段,應先驗證未簽章的封存是否完整。

封存中的主程式通常位於 Products/Applications。檢查範圍至少應包含 App 的主要可執行檔,以及 Frameworks 目錄內每個 Framework 的可執行檔。擴充功能目標也應分別納入,因為它們各自擁有獨立的依賴邊界。

對象 主要檢查 常見異常
App 主程式 系統函式庫、自帶 Framework、全域符號 非系統絕對路徑
動態 Framework 次級依賴、安裝名稱、公開符號 依賴未隨套件交付
App Extension 獨立的依賴與符號基準 錯誤沿用主程式假設

產生依賴與符號清單

下列指令碼會讀取主程式與內嵌 Framework,分別輸出經穩定排序的依賴與全域符號。排序可避免工具輸出順序變動而產生無意義的差異。

set -euo pipefail

archive="${1:?archive path required}"
app=$(find "$archive/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)
test -n "$app"

mkdir -p audit/current
main=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Info.plist")
printf '%s
' "$app/$main" > audit/current/binaries.txt

if test -d "$app/Frameworks"; then
  while IFS= read -r framework; do
    executable=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$framework/Info.plist")
    printf '%s
' "$framework/$executable"
  done < <(find "$app/Frameworks" -maxdepth 1 -name '*.framework' -type d | sort)
fi >> audit/current/binaries.txt

: > audit/current/dependencies.txt
: > audit/current/symbols.txt

while IFS= read -r binary; do
  name=${binary#"$app/"}
  otool -L "$binary" |
    sed '1d' |
    awk -v n="$name" '{$1=$1; print n "	" $1}' \
    >> audit/current/dependencies.txt

  xcrun nm -gU "$binary" |
    awk -v n="$name" 'NF {print n "	" $NF}' \
    >> audit/current/symbols.txt
done < audit/current/binaries.txt

LC_ALL=C sort -u -o audit/current/dependencies.txt audit/current/dependencies.txt
LC_ALL=C sort -u -o audit/current/symbols.txt audit/current/symbols.txt

提交基準時只保留相對物件名稱,不要記錄雲端 Mac 的工作目錄。接著由管線執行:

diff -u audit/baseline/dependencies.txt audit/current/dependencies.txt
diff -u audit/baseline/symbols.txt audit/current/symbols.txt

將報告轉換成可靠門禁

只進行全文差異比對會較為敏感。更實用的策略是先執行硬性規則,再檢查基準變化。依賴通常可以合理地以 /System/Library/Frameworks//usr/lib/ 或受控的 @rpath/ 開頭;其他絕對路徑應直接判定失敗。對於 @rpath/Example.framework/Example,指令碼還應確認套件內確實存在對應的 Framework,不能只因前綴正確就放行。

符號變化則適合交由人工審核。Swift 名稱經過修飾後可能非常長,不要根據字串長度判斷風險。應重點檢查突然出現的大量全域符號、除錯輔助介面、測試進入點,以及原本應隱藏卻可跨模組存取的 C 或 Objective-C 符號。

降低誤報

固定 Xcode 版本與 Release 組態,並確保每次比較的都是同一個 scheme。若專案包含多個擴充功能,應為每個二進位檔保存獨立基準,而不是將所有結果混合成單一集合。工具鏈升級導致系統符號變化時,應在升級合併請求中更新基準,避免日後無法說明差異來源。

失敗後從產物反向追查

出現新依賴時,先使用 otool -L 確認是哪個二進位檔將其引入,再檢查目標的連結設定、條件式編譯與依賴嵌入階段。若 @rpath 目標缺失,應優先確認 Framework 是否只參與連結,卻未複製到套件內。出現非預期符號時,可使用 nm -gU 定位物件,再檢查可見性宣告、橋接程式碼與連結器匯出設定。

失敗後不要直接覆寫基準。正確順序是先確認變更來源、判斷是否影響交付邊界、修正或核准變化,最後才重新產生基準。以這種方式建立的門禁不會依賴某台雲端 Mac 的暫時狀態,也能為每次發布保留可供複核的二進位邊界紀錄。

常見問題

為什麼不能只檢查專案中的依賴宣告?

專案宣告不等於最終產物。條件編譯、連結參數與建置腳本都可能改變 Archive,因此門禁應直接檢查成品。

匯出符號改變時都必須阻擋發佈嗎?

不必一律阻擋。CI 應先要求審查;若變更來自已確認的工具鏈升級或公共介面調整,再更新並提交基準檔。

符號清單能取代敏感資料掃描嗎?

不能。符號清單主要揭露介面與連結邊界變化,敏感資料掃描仍須涵蓋原始碼、設定、資源與建置日誌。

獨享實體節點

將開發與建置任務交給獨享雲端 Mac

依任務選擇機型、節點區域與租用週期。每個實例都對應獨享實體機,採用非虛擬化環境。

選擇雲端 Mac 方案