クラウドMacでApp Thinning端末別バリアントを検証する

クラウドMacでApp Thinning端末別バリアントを検証する

同じXcodeアーカイブを配布プロセスに渡しても、最終的に各端末へ配信される内容は完全には同一ではありません。App Thinningでは、端末の能力に応じて実行可能ファイルのスライスとリソースのバリアントが選択されます。通常のビルドでアーカイブの成功だけを確認していると、リソースタグの誤り、不要なアーキテクチャの混入、埋め込みフレームワークの変化、サイズレポートの未生成などを見落としがちです。より確実なのは、クラウドMac上で「アーカイブ」と「端末別バリアントの検証」を独立した2つの工程に分ける方法です。

この記事では、既存のプロジェクトと配布可能なアーカイブ設定を使用します。署名方式は変更せず、検証のたびに再コンパイルする必要もありません。1つの.xcarchiveからApp Thinningの結果を書き出し、構成一覧を生成して、明確な異常があればCIを非ゼロの終了コードで失敗させることが目的です。

検証対象の入力を固定する

検証を始める前に、Xcodeのパス、プロジェクトのバージョン、ビルド番号、アーカイブの場所を記録します。対話型Shellのデフォルトツールチェーンに依存させてはいけません。クラウドMacには複数のXcodeが共存している場合があるため、CIでは現在のパイプラインで承認済みのバージョンを明示的に選択します。

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_DIR="$PWD/output/thinning"
REPORT_DIR="$PWD/output/evidence"

xcodebuild -version
test -d "$ARCHIVE_PATH"
rm -rf "$EXPORT_DIR" "$REPORT_DIR"
mkdir -p "$EXPORT_DIR" "$REPORT_DIR"

続いてアーカイブ内のアプリ情報を読み取り、CIが前のジョブで残された成果物を誤って使用していないことを確認します。

INFO="$ARCHIVE_PATH/Info.plist"

/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleIdentifier" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$INFO"

作業ディレクトリはジョブ番号にひも付け、ジョブ開始時に空のディレクトリを作成することを推奨します。グローバルに固定したoutputディレクトリを複数の並列ジョブで共有しないでください。

App Thinning検証への入力は、通常のアーカイブ検査に合格した単一の.xcarchiveでなければなりません。入力自体が確定していなければ、その後で端末別バリアントをどれほど詳細に比較しても意味がありません。

互換性のあるすべての端末別バリアントを書き出す

専用のExportOptions.plistを用意します。methodは、チームが現在使用しているXcodeおよび既存の配布プロセスと一致させてください。以下の設定で重要なのは、thinningを、互換性のあるすべてのバリアントを書き出す値に設定することです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>method</key>
  <string>app-store-connect</string>
  <key>thinning</key>
  <string>&lt;thin-for-all-variants&gt;</string>
  <key>stripSwiftSymbols</key>
  <true/>
</dict>
</plist>

書き出しを実行するときは、結果とログの両方を現在のジョブディレクトリに保存します。

xcodebuild \
  -exportArchive \
  -archivePath "$ARCHIVE_PATH" \
  -exportPath "$EXPORT_DIR" \
  -exportOptionsPlist "$PWD/ExportOptions.plist" \
  2>&1 | tee "$REPORT_DIR/export.log"

find "$EXPORT_DIR" -maxdepth 3 -type f -print \
  | LC_ALL=C sort \
  > "$REPORT_DIR/export-files.txt"

既存のパイプラインで別の書き出し方式を使用している場合、例に合わせるためだけに変更してはいけません。従来のmethodを維持し、隔離したブランチでのみthinning設定を検証したうえで、使用したplistも一緒に保存します。

アーキテクチャと埋め込みコンテンツを確認する

書き出しの成功は、結果の正しさを意味しません。まずインストールパッケージを見つけて個別に展開し、メイン実行可能ファイルのアーキテクチャを確認します。スクリプトではアプリのディレクトリ名を固定と仮定せず、Payload内から.appを検出します。

mkdir -p "$REPORT_DIR/unpacked"

while IFS= read -r ipa; do
  name="$(basename "$ipa" .ipa)"
  dest="$REPORT_DIR/unpacked/$name"
  mkdir -p "$dest"
  ditto -x -k "$ipa" "$dest"

  app="$(find "$dest/Payload" -maxdepth 1 -name '*.app' -type d | head -n 1)"
  test -n "$app"

  executable="$(/usr/libexec/PlistBuddy \
    -c 'Print :CFBundleExecutable' "$app/Info.plist")"

  printf '%s: ' "$name"
  lipo -archs "$app/$executable"

  find "$app/Frameworks" -maxdepth 1 -type d -name '*.framework' 2>/dev/null \
    | sed "s#^$app/##" \
    | LC_ALL=C sort \
    > "$REPORT_DIR/$name-frameworks.txt"
done < <(find "$EXPORT_DIR" -type f -name '*.ipa' | LC_ALL=C sort)

現在のiOS端末向け配布物では、メインアプリにシミュレータ用アーキテクチャが含まれていた場合、その時点で失敗させるべきです。フレームワーク一覧は、リポジトリ内の承認済みベースラインとの比較に適しています。フレームワークの追加が必ずしも誤りとは限りませんが、デバッグ用コンポーネントや一時的な依存関係が配布パッケージに入るのを防ぐため、必ずコードレビューを通す必要があります。

インストールパッケージ全体のハッシュを比較しない

端末別バリアントには元から異なるリソースや実行可能ファイルのスライスが含まれる可能性があり、署名データによってもバイナリに差異が生じます。すべてのIPAに同じSHA-256を求めると、正常なApp Thinningまで失敗と誤判定してしまいます。

より有効な比較対象は次のとおりです。

  • メイン実行可能ファイルで許可されるアーキテクチャ。
  • FrameworksとPlugInsの相対パス一覧。
  • 必須のローカライズ、フォント、データファイル。
  • 製品版に含めてはならないデバッグ設定、テストリソース、ソースファイル。
  • 各バリアントの展開後サイズと、過去のベースラインに対する変化。

リソースとサイズを検証可能な証拠にする

最初に、書き出しプロセスでサイズレポートが実際に生成されたことを確認し、その後で各バリアントのファイル一覧を生成します。ファイルサイズはリソースの削減によって変化するため、ベースラインはバリアントごとに保存してください。

SIZE_REPORT="$(find "$EXPORT_DIR" -type f \
  -name 'App Thinning Size Report.txt' | head -n 1)"

test -n "$SIZE_REPORT"
cp "$SIZE_REPORT" "$REPORT_DIR/"

for app in "$REPORT_DIR"/unpacked/*/Payload/*.app; do
  variant="$(basename "$(dirname "$(dirname "$app")")")"
  (
    cd "$app"
    find . -type f -print0 \
      | xargs -0 stat -f '%z %N' \
      | LC_ALL=C sort
  ) > "$REPORT_DIR/$variant-files-and-sizes.txt"
done

サイズゲートには、変化しない単一の絶対値だけを設定しないでください。チームが承認したハードリミットと、直近の安定版ベースラインからの増加率という2種類の判定を併用することを推奨します。ハードリミットは明らかな肥大化を阻止し、相対ベースラインは1回のコミットによって生じた異常な増加を検出します。

リソースディレクトリについては、ビジネス上重要なファイルが存在することも確認する必要があります。たとえば、オフラインテンプレート、初期画面で使うフォント、特定言語のリソースが完全にそろっているかどうかは、「アプリが起動できる」だけでは証明できません。これらのパスをリポジトリ内のテキスト一覧として管理し、項目ごとにtest -eを実行する方が、スクリプト内に多数の条件を分散させるよりレビューしやすくなります。

CIで明確な失敗条件を設定する

最終的なスクリプトでは、少なくとも次の状況を阻止する必要があります。IPAが1つも書き出されていない、サイズレポートがない、メインアプリに許可されていないアーキテクチャが含まれる、重要なリソースが欠けている、未承認のフレームワーク構成変更がある、またはバリアントのサイズがチームの上限を超えている場合です。

同時に、次の証拠を保存します。

  1. XcodeのバージョンとDEVELOPER_DIR。
  2. アーカイブのバンドル識別子、バージョン、ビルド番号。
  3. 実際に使用したExportOptions.plist。
  4. 完全な書き出しログ。
  5. 各バリアントのアーキテクチャ、フレームワーク、ファイル一覧。
  6. App Thinningのサイズレポート。

失敗時に書き出しディレクトリを即座に削除してはいけません。先に証拠をパッケージ化してアップロードし、その後でジョブの終了フックからワークスペースをクリーンアップします。PingMiniのクラウドMac上で並列実行する場合は、ジョブごとに独立したディレクトリを使用し、コンソールで現在選択できる構成を確認してください。検証ロジック自体が特定の固定ノードに依存しないようにします。

App Thinningの価値はダウンロードサイズの削減だけではなく、最終的な配布物そのものが変わる点にもあります。端末別バリアントを正式なビルド成果物として検査することで、リソースタグ、アーキテクチャ、依存関係に変更が生じたとき、リリース前に再現可能で原因を特定できる失敗結果を得られます。

よくある質問

App Thinningの検証対象はアーカイブと書き出し後のパッケージのどちらですか?

両方です。アーカイブで共通入力を確認し、書き出し後のパッケージで端末別の実行アーキテクチャ、リソース、組み込み内容を確認します。

端末別バリアントのハッシュが異なると異常ですか?

異常とは限りません。実行ファイルのスライス、端末向けリソース、署名データは変化します。許可アーキテクチャ、ファイル一覧、必須リソースを比較してください。

CIを失敗させるべきApp Thinningの異常は何ですか?

サイズレポートの欠落、空の書き出し結果、未許可アーキテクチャ、必須リソースの欠落、フレームワーク構成の変化、設定した上限超過です。

次回のビルドに向けて専用の物理Macを準備

開発、継続的インテグレーション、ローカルモデルのテストに適した3種類の固定構成からクラウドMacを選び、タスク期間に合わせてセットアップできます。

クラウドMacを構成する