エンジニアリング記事

クラウド Mac で iOS App Extension の埋め込みを検証する

クラウド Mac で iOS App Extension の埋め込みを検証する

通知サービス、共有 Extension、Widget Extension を含む iOS プロジェクトでは、メインアプリのビルドとアーカイブには成功したのに、エクスポート、インストール、またはリリース検証の段階で Extension が無効と判定されることがあります。原因は通常、ソースコードではありません。多くの場合、.app.appex の間でバージョン番号、デプロイメントターゲット、埋め込み先、または署名にずれが生じています。クラウド Mac のアーカイブジョブ直後に検証を実行すれば、成果物がビルドノードを離れる前に明確な判定を出せます。

アーカイブの受け入れ基準を先に定義する

App Extension は単独で配布する成果物ではありません。メインアプリの PlugIns ディレクトリに配置され、ホストアプリとの埋め込み関係を検証できる必要があります。CI では少なくとも次の項目を確認します。

検査項目 メインアプリ App Extension 推奨ルール
CFBundleVersion 必須 必須 完全一致
CFBundleShortVersionString 必須 必須 完全一致
MinimumOSVersion 必須 必須 チームで統一している場合は一致させる
NSExtension 不要 必須 Dictionary が存在し、読み取り可能
コード署名 有効 有効 strict 検証に合格
埋め込み先 Applications PlugIns/*.appex 別の場所にあるコピーは認めない

最低 OS バージョンは、すべてのプロジェクトで一致させる必要があるわけではありません。ただし、チームが意図的に異なるデプロイメントターゲットを管理していないのであれば、完全一致を要件にしたほうが設定のずれを検出しやすくなります。意図した差異がある場合は、スクリプトで黙って無視するのではなく、許容範囲をリポジトリ内に明記します。

「ビルド成功」が示すのは、各 Target が成果物を生成できたことだけです。「有効なアーカイブ」であるためには、それらの成果物が正しい関係で組み合わされ、配布可能なアプリを構成していることまで証明する必要があります。

検証対象の xcarchive を一つだけ生成する

ゲートでは DerivedData の中間ディレクトリではなく、アーカイブを検査します。DerivedData には前回のビルドで生成された .appex が残っている可能性があり、最終的な埋め込み構造を表しているとは限りません。まず固定のアーカイブパスを削除し、Release アーカイブを新規に実行します。

set -euo pipefail

ARCHIVE_PATH="$PWD/build/App.xcarchive"
rm -rf "$ARCHIVE_PATH"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$ARCHIVE_PATH" \
  clean archive

異なるブランチで同じアーカイブディレクトリを共有しないでください。並列ジョブではコミットハッシュや CI ジョブ番号を使って個別のパスを作成できますが、後続のスクリプトには特定の .xcarchive を一つだけ渡します。

まず埋め込み結果を確認する

アーカイブ後、メインアプリは通常 Products/Applications にあり、Extension はメインアプリバンドル内の PlugIns に配置されます。まず find で実際の出力を確認します。

find build/App.xcarchive/Products/Applications \
  \( -name '*.app' -o -name '*.appex' \) \
  -print

プロジェクトに Extension の Target が定義されているのに .appex が見つからない場合は、メインアプリ Target の Embed App Extensions ビルドフェーズと、現在の Scheme でその Extension がビルド対象になっているかを優先して確認します。別のディレクトリから Extension をコピーしてアーカイブを「補完」してはいけません。

スクリプトでメタデータと署名を検証する

次の Bash スクリプトは、macOS に標準搭載されているツールだけで動作します。メインアプリを特定し、各 Extension の Info.plist を読み取り、バージョンと最低 OS バージョンを比較したうえで、それぞれの署名を検証します。

#!/bin/bash
set -euo pipefail

ARCHIVE="${1:?usage: validate-extensions.sh App.xcarchive}"
APP=$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)

if [ -z "$APP" ]; then
  printf '%s
' "main app not found"
  exit 1
fi

read_plist() {
  /usr/libexec/PlistBuddy -c "Print :$2" "$1/Info.plist"
}

host_build=$(read_plist "$APP" CFBundleVersion)
host_version=$(read_plist "$APP" CFBundleShortVersionString)
host_minimum=$(read_plist "$APP" MinimumOSVersion)
count=0
failed=0

while IFS= read -r extension; do
  count=$((count + 1))
  ext_build=$(read_plist "$extension" CFBundleVersion)
  ext_version=$(read_plist "$extension" CFBundleShortVersionString)
  ext_minimum=$(read_plist "$extension" MinimumOSVersion)

  [ "$ext_build" = "$host_build" ] || failed=1
  [ "$ext_version" = "$host_version" ] || failed=1
  [ "$ext_minimum" = "$host_minimum" ] || failed=1

  /usr/libexec/PlistBuddy \
    -c "Print :NSExtension" \
    "$extension/Info.plist" >/dev/null

  codesign --verify --strict --verbose=2 "$extension"
done < <(find "$APP/PlugIns" -maxdepth 1 -name '*.appex' -print)

[ "$count" -gt 0 ] || {
  printf '%s
' "no app extensions found"
  exit 1
}

codesign --verify --deep --strict --verbose=2 "$APP"
[ "$failed" -eq 0 ] || {
  printf '%s
' "extension metadata mismatch"
  exit 1
}

スクリプトを ci/validate-extensions.sh として保存し、chmod +x を実行してから、アーカイブ処理の直後に呼び出します。リポジトリで「Extension あり」と「Extension なし」の二つの Scheme を生成する場合は、想定する Extension 数を引数で明示し、Extension がゼロでも一律に成功とは判定しないようにします。

バージョン情報をビルド設定に集約する

設定のずれで最も多い原因は、複数の Info.plist を手作業で管理することです。メインアプリの Build Number を更新しても、Extension に古い値が残ることがありますが、それが原因でソースコードのコンパイルが失敗することはありません。より確実なのは、すべての Target で同じビルド変数を使用する方法です。

MARKETING_VERSION = 4.8.0
CURRENT_PROJECT_VERSION = 8204
IPHONEOS_DEPLOYMENT_TARGET = 17.0

対応する plist の値には、$(MARKETING_VERSION)$(CURRENT_PROJECT_VERSION)$(IPHONEOS_DEPLOYMENT_TARGET) を使用します。設定を .xcconfig で管理している場合は、メインアプリと Extension の両方から共通のベースファイルを読み込み、本当に異なる値が必要なキーだけを少数の Target 専用ファイルで上書きします。

アーカイブ後の再署名で問題を隠さない

codesign --deep はバンドル全体の検証には適していますが、汎用的な修復コマンドとして使うべきではありません。アーカイブ後に再帰的な再署名を行うと、元の埋め込み関係が変わる可能性があり、CI の成果物とプロジェクト設定も一致しなくなります。署名検証に失敗した場合は、Target の署名設定、Build Phases、エクスポート設定を修正し、完全なアーカイブを作り直します。

失敗時は順序どおりに証拠を保存する

ゲートが失敗しても、すぐに作業ディレクトリを削除しないでください。まず次の情報を保存します。

  1. .xcarchive 内にあるすべての .app.appex の相対パス;
  2. 各バンドルのバージョン番号、最低 OS バージョン、Bundle Identifier;
  3. codesign --verify --strict --verbose=2 の完全な出力;
  4. 今回使用した Scheme、Configuration、および展開後の実際のビルド設定;
  5. アーカイブジョブで使用したコミット識別子と Xcode のバージョン。

次のコマンドでビルド設定を出力すると、メインアプリと Extension に実際に渡された変数を比較できます。

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings > build/build-settings.txt

最終的なゲートでは、二つの要件を満たす必要があります。失敗メッセージから、どの .appex のどのキーが一致していないかを直接特定できること。そして、成果物の生成後ではなく、プロジェクト設定で問題を修正することです。これにより、使用するクラウド Mac ノードが変わっても受け入れ基準はリポジトリに維持され、アーカイブ結果を繰り返し検証できます。

よくある質問

Xcode のアーカイブ成功後に App Extension が失敗するのはなぜですか?

アーカイブは主に各ターゲットのビルド可否を確認します。書き出しとインストールでは、バージョン、配置、最低 OS、拡張宣言、署名も検証されます。

アプリと App Extension のビルド番号は同じにするべきですか?

CFBundleVersion は一致させます。CFBundleShortVersionString も共通のビルド設定から生成すると、リリース追跡が明確になります。

アーカイブ後の再署名で修正してもよいですか?

推奨しません。ビルド設定、埋め込みフェーズ、署名設定を直し、クリーンな作業環境からアーカイブを作り直してください。

専有物理ノード

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

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

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