開発プラクティス

Swift API Digesterで破壊的な変更をマージ前に検出する

Swift API Digesterで破壊的な変更をマージ前に検出する

複数のアプリから参照される Swift Framework では、公開メソッドを1つ削除したり、引数の型を制限したり、公開プロトコルに必須メンバーを追加したりするだけで、アップグレード後に依存先のプロジェクトがコンパイルできなくなる可能性があります。通常、単体テストは実装の振る舞いを検証しますが、「今回の変更によって公開 API が壊れた」ことまでは明確に示してくれません。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 はそのファイルを読み取るだけにします。コマンドがゼロ以外の終了ステータスを返した場合は、「互換性チェックに失敗しました」という一文だけを表示するのではなく、診断テキストをビルド成果物として保存してください。

重点的にレビューすべき変更

変更 原則的な判定 対応方法
公開型または公開メソッドの削除 破壊的 API を復元するか、明示的なメジャーバージョン変更として扱う
引数、戻り値、またはジェネリック制約の変更 破壊的 互換性のあるオーバーロードを用意し、非推奨期間を設定する
公開プロトコルへの必須メンバー追加 高リスク デフォルト実装の提供を検討する
公開メソッドまたは公開型の追加 通常は互換性あり 命名、可視性、プラットフォーム属性を確認する
内部実装のみの変更 検出されるべきではない アクセスレベルが意図せず拡大していないか確認する

public は、必ずしも意図的な互換性保証を意味しません。外部から呼び出されるべきでない宣言は、無視ルールを長期間維持するのではなく、アクセスレベルを制限することを優先してください。

診断結果を保守可能な互換性ゲートにする

処理は「ビルド」「スナップショット生成」「比較」「レポートのアップロード」の4段階に分けることを推奨します。スクリプトでは set -euo pipefail を有効にし、Derived Data はジョブごとの専用ディレクトリに保存して、並列ジョブが互いの成果物を読み込まないようにします。複数モジュールを含むリポジトリでは、モジュール一覧を管理して順番に処理し、直前のモジュールの PRODUCTS パスを使い回さないでください。

ゲートが失敗した場合、レビュアーには3つの情報が必要です。使用した Xcode と Swift のバージョン、ベースラインと候補版のスナップショット、そして完全な診断テキストです。変更がバージョン戦略に適合すると確認できた場合に限り、同じマージリクエスト内でベースラインを更新します。失敗したジョブ自身によるベースラインの書き換えを許可すると、チェックは常に通過するようになり、意味を失います。

誤検出を調査する順序

最初にツールチェーンを照合し、次に SDK とターゲットトリプルを確認します。その後でビルド引数と条件付きコンパイルフラグを比較し、最後に Digester の出力ノイズかどうかを判断します。次のチェックリストを使うと、調査時間を短縮できます。

  1. xcodebuild -version が完全に一致しているか。
  2. scheme、構成、destination が一致しているか。
  3. 両方で BUILD_LIBRARY_FOR_DISTRIBUTION が有効になっているか。
  4. モジュール検索パスが今回のジョブで生成した成果物を参照しているか。
  5. ベースラインが任意の過去コミットではなく、最後にリリースされたインターフェースから生成されているか。
  6. 生成ファイルに、作業ディレクトリなど変動しやすい絶対パスが混入していないか。

最後にバージョン戦略へ反映する

互換性ゲートの役割は変更を検出することであり、チームに代わってバージョン番号を決めることではありません。API の削除、公開型の変更、プロトコル要件の追加は、通常、不互換バージョンの計画に含める必要があります。API の追加であっても命名と利用可能性のレビューが必要です。非推奨にした API は合意済みの期間だけ維持し、その後のバージョンで削除します。

最も堅実な運用は、リリースブランチでベースラインを保存し、マージリクエストでは候補版のスナップショットだけを生成し、CI が機械的な診断結果を出力したうえで、メンテナーがバージョン戦略に基づいて判断することです。これにより、すべての API 変更を一律に禁止することなく、意図しない public の変更がそのまますべての依存先プロジェクトへ波及する事態も防げます。

よくある質問

Swift API Digesterは単体テストの代わりになりますか?

なりません。公開Swift APIの構造差分を検査するツールであり、実行時の挙動や業務ロジックは検証しません。単体テストや統合テストと併用します。

API基準ファイルはいつ更新すべきですか?

変更がバージョニング方針に合うとレビューで確認された時だけ更新します。通常のCI実行で自動上書きせず、コードと同じ変更として承認します。

同じコードなのに差分が変わるのはなぜですか?

Xcode、Swiftコンパイラ、SDK、対象アーキテクチャ、ビルド設定の差が主な原因です。基準生成時と比較時のツールチェーンを一致させてください。

専用物理Mac mini

次のビルドを専用の物理ノードで実行しましょう。

チップ、メモリ、ストレージ、ノード、利用期間を選択し、他のお客様とリソースを共有しないクラウドMacを利用できます。

構成を選んでレンタル