エンジニアリング記事

クラウドMac CIでiOSの動的依存関係と公開シンボルを監査する

クラウドMac CIでiOSの動的依存関係と公開シンボルを監査する

開発ブランチでは問題なくビルドできるiOSプロジェクトでも、Archiveには動的Frameworkがいつの間にか追加されていたり、開発用Macのディレクトリを指す絶対リンクが含まれていたり、内部利用だけを想定したグローバルシンボルが公開されていたりすることがあります。実際に配布されるのはリンク後のMach-Oであるため、プロジェクトファイルのレビューだけではこうした変化を十分に検出できません。より確実なのは、クラウドMac CIでアーカイブ済み成果物を直接検査し、依存関係とシンボルの変化をレビュー対象の差分として扱う方法です。

まず阻止すべき変更を定義する

この種のゲートでは、特定のライブラリが「良いか悪いか」を判定するのではなく、検証可能な次の3点に答えられるようにします。

  1. メインプログラムと組み込みFrameworkは、実際に何へ依存しているか。
  2. @rpathが参照する同梱ライブラリは、Appバンドル内に本当に存在するか。
  3. 新たに追加されたグローバルシンボルは、意図したインターフェイスに含まれるか。

ベースラインは恒久的な許可リストではなく、人手で確認済みの成果物スナップショットです。ツールチェーンのアップグレード、依存関係の更新、インターフェイスの変更後には更新できますが、コード変更と一緒にレビューしなければなりません。

検査はRelease Archiveの作成後に実行するのが適切です。Debugビルドには診断用ライブラリが含まれることがあり、最適化結果も異なるため、最終的な配布境界を表すものではありません。導入当初はレポートの生成だけを行い、すぐにはブロックしないようにします。チームがベースラインの内容を確認した後で、差分があれば失敗する設定へ切り替えます。

Archiveから検査対象を抽出する

まず独立したアーカイブを生成し、開発者のMacに残っている過去の成果物が再利用されないようにします。

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を設定できます。ただし、この上書き設定をすべてのプロジェクトへ機械的に追加してはいけません。特定のCapabilityやカスタムビルドフェーズを含むプロジェクトでは、署名なしのアーカイブが完全であることを先に確認してください。

アーカイブ内のメインプログラムは通常、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を使用します。プロジェクトに複数の拡張が含まれる場合は、すべての結果を1つの集合へ混在させず、バイナリごとに独立したベースラインを保存してください。ツールチェーンのアップグレードによってシステムシンボルが変化する場合は、アップグレード用のマージリクエスト内でベースラインを更新し、後から差分の発生源を説明できなくなる事態を防ぎます。

失敗時は成果物から逆引きする

新しい依存関係が現れた場合は、まずotool -Lでどのバイナリが取り込んだかを確認し、その後で対象ターゲットのリンク設定、条件付きコンパイル、依存関係の埋め込みフェーズを調べます。@rpathの参照先が見つからない場合は、Frameworkがリンクされただけでバンドルへコピーされていない可能性を重点的に確認します。想定外のシンボルが現れた場合は、nm -gUで対象オブジェクトを特定し、可視性の宣言、ブリッジコード、リンカーのエクスポート設定を確認します。

失敗した直後にベースラインを上書きしてはいけません。正しい手順は、変更元を特定し、配布境界への影響を判断し、変更を修正または承認してから、最後にベースラインを再生成することです。この方法で構築したゲートは、特定のクラウドMacの一時的な状態に依存せず、リリースごとに検証可能なバイナリ境界の記録を残せます。

よくある質問

プロジェクトの依存関係だけを確認する方法では不十分ですか?

不十分です。条件付きビルド、リンカーフラグ、依存スクリプトによって最終Archiveが変わるため、生成物を直接確認する必要があります。

公開シンボルの変更は必ずリリース停止にするべきですか?

必ずしも停止し続ける必要はありません。意図したAPI変更やツールチェーン更新だと確認できた場合は、レビュー後に基準を更新します。

シンボル一覧で埋め込まれた秘密情報も検出できますか?

単独では検出できません。シンボル監査とは別に、ソース、設定、リソース、ビルドログを対象とした秘密情報検査が必要です。

専有物理ノード

開発とビルドのタスクを専有クラウドMacで実行

タスクに応じて、モデル、ノードのリージョン、レンタル期間を選択できます。各インスタンスは専有物理マシンで、仮想マシン環境ではありません。

クラウドMacのプランを選ぶ