エンジニアリング実践

iOS再現可能アーカイブと2回のビルド差分調査

iOS再現可能アーカイブと2回のビルド差分調査

iOS再現可能アーカイブと2回のビルド差分調査

同じコミットを開発者のMacでは正常にアーカイブでき、クラウドMacや夜間パイプラインでも成功と表示されるのに、2つの成果物のハッシュが一致しないことがあります。すぐにXcodeのキャッシュが原因だと決めつけてはいけません。署名、圧縮時刻、生成ソースコード、スクリプトの出力順、絶対パスはいずれも成果物に入り込む可能性があります。より効果的なのは、同じArmVMSノード上に完全に隔離された2つのビルドディレクトリを作成し、署名なしのアーカイブを連続して2回実行したうえで、外側から内側へ差分を比較する方法です。

まず「再現可能」の検査範囲を定義する

署名済みIPAは、署名データ、プロビジョニングプロファイル、パッケージ化メタデータが変化し得るため、バイト単位で完全に一致させるのは困難です。プロジェクトの検査は次の2つのレイヤーに分ける必要があります。

  1. コンテンツレイヤー:同一のコミット、ツールチェーン、依存関係のロックファイル、ビルドパラメータから、同一のアプリリソースと実行ファイルを生成します。
  2. デリバリーレイヤー:エクスポート方式、署名構成、権限宣言、パッケージ内のファイルがリリース要件を満たすことを確認します。ただし、IPA全体のハッシュ一致までは求めません。

まず署名なしのアプリコンテンツが安定していることを証明し、その後で署名とエクスポートを検査します。2つのIPAの全体ハッシュを直接比較しても、「異なる」ことしか確認できず、差分がどこから生じたのかは分かりません。

開始前に、コミットハッシュ、Xcodeのパス、SDK、Scheme、Configurationに加え、Package.resolvedPodfile.lockなどのロックファイルを記録します。ワークスペースはクリーンな状態にし、コミットされていない生成ファイルも検査対象に含める必要があります。

隔離したディレクトリでアーカイブを2回実行する

2回のビルドでDerivedDataを共有してはいけません。共有すると、2回目のビルドが1回目に残された中間成果物を読み込む可能性があります。次のスクリプトではロケールとタイムゾーンを固定し、アーカイブごとに独立したディレクトリを割り当て、コード署名を無効にします。

#!/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の一時的な設定を引き継がないようにします。また、デフォルトのXcodeパスが切り替わるのを防ぐため、2回のアーカイブで同じDEVELOPER_DIRを使用する必要があります。

ファイル一覧から差分範囲を絞り込む

最初からバイナリを16進数で比較してはいけません。まず、2つの.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_FLAGSOTHER_LDFLAGS、検索パス、デプロイメントターゲット、実際に使われたツールのパスを照合します。生成コードに現在の日付、一時ディレクトリ、順序が保証されていない辞書の走査結果が書き込まれている場合も、それ以降のコンパイル結果全体が変動します。

特に、#filePath、デバッグマッピング、スクリプト出力に含まれるワークスペースの絶対パスに注意してください。CIの作業ディレクトリが変わる場合は、チェックアウト先のパスを統一するか、デバッグプレフィックスマッピングを適切に設定することでパス差分を減らせます。ただし、ハッシュを一致させるためにデバッグに必要な情報を削除してはいけません。

検査を保守可能なゲートにする

ゲートでは、単純に「2つのIPAのハッシュが必ず一致すること」と規定すべきではありません。より堅牢なのは、差分レポートを保存し、レイヤーごとに判定する方法です。

必ずブロックすべき変更

同一コミットでファイルの欠落、リソース内容の変更、権限宣言の変更、埋め込まれたFrameworkのバージョン変更が発生した場合、または同一入力に対してメイン実行ファイルが安定しない場合は、直ちにブロックして2つのアーカイブを両方とも保存する必要があります。

個別にレビューできる変更

署名ディレクトリ、エクスポートログ、明示的に記録されたパッケージ化時刻はデリバリーレイヤーの差分として個別に検査できます。ただし、変更を許可するパスを限定する必要があり、「すべてのplistを無視する」といった広範なルールを使用してはいけません。

修正後は古いDerivedDataを再利用せず、空のディレクトリを2つ作り直して再検証します。大規模なプロジェクトでは、すべてのコミットでアーカイブを2回実行する必要はありません。Xcodeのバージョン、依存関係のロックファイル、ジェネレーター、Run Script、リリース構成が変わったときに実行するよう設定できます。これにより、壊れやすい単一のハッシュルールではなく、差分がどのレイヤーで発生したかを示せるビルド証拠チェーンを構築できます。

よくある質問

署名済みIPAのハッシュだけでは判定できませんか?

署名データやパッケージ作成時刻には想定内の差分が入ります。まず未署名アーカイブ内のアプリ本体を比較してください。

Mach-O UUIDが違えばコードも違うのでしょうか?

必ずしもそうではありません。リンク入力順、生成コード、コンパイル引数、ツールのパスもUUIDに影響します。

再現性検査は毎回のコミットで必要ですか?

大規模なプロジェクトでは、ツールチェーン、依存ロック、ビルドスクリプト、配布設定の変更時に実行する方法が現実的です。

ArmVMS クラウドMac

次のビルドに専用のApple Silicon物理ノードを選択

ArmVMS M4とArmVMS M4 Proを、日単位、週単位、月単位、または四半期単位でレンタルできます。注文を作成する前に、構成、ノード、追加項目を一つずつ確認できます。

構成を選択して注文を作成