技術ランブック

クラウドMac CIで最終Info.plistを監査する

クラウドMac CIで最終Info.plistを監査する

同じiOSプロジェクトでも、ローカルでは正常にアーカイブできる一方、クラウドMacのReleaseパイプラインでは誤ったBundle IDやデバッグ用URL Schemeが含まれたり、カメラ権限の説明が欠落したりすることがあります。多くの場合、問題はリポジトリ内の Info.plist ではなく、Xcodeがビルド設定、生成された構成、ターゲット属性をマージしてAppに書き込んだ最終結果にあります。信頼できるCIでは、ソースファイルをテキストとして確認するだけでなく、実際のビルド成果物を監査する必要があります。

実際に配布される構成を確認する

最新のXcodeプロジェクトでは、Info.plistの自動生成が有効になっている場合があります。また、target、configuration、.xcconfig ごとに異なる値が指定されることもあります。INFOPLIST_FILE が指すファイルは入力の一つにすぎず、PRODUCT_BUNDLE_IDENTIFIERMARKETING_VERSIONCURRENT_PROJECT_VERSION などの設定もビルド時にマージされます。

まず、workspace、scheme、configuration、出力ディレクトリを明示してビルドを実行します。

set -euo pipefail

ROOT="$(pwd)"
DERIVED_DATA="$ROOT/.ci/DerivedData"

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath "$DERIVED_DATA" \
  CODE_SIGNING_ALLOWED=NO \
  build

ここで署名を無効にするのは、構成を素早く監査する場合に限られます。リリース用アーカイブでも署名を無効にすべきという意味ではありません。ビルド完了後はAppのパスを推測せず、ビルド設定から製品ディレクトリと製品名を取得します。

SETTINGS="$(mktemp)"
xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath "$DERIVED_DATA" \
  -showBuildSettings > "$SETTINGS"

BUILD_DIR="$(awk -F ' = ' '/ TARGET_BUILD_DIR = /{print $2; exit}' "$SETTINGS")"
WRAPPER_NAME="$(awk -F ' = ' '/ WRAPPER_NAME = /{print $2; exit}' "$SETTINGS")"
APP_PATH="$BUILD_DIR/$WRAPPER_NAME"
PLIST_PATH="$APP_PATH/Info.plist"

test -d "$APP_PATH"
test -f "$PLIST_PATH"
plutil -lint "$PLIST_PATH"

監査対象は、現在のジョブで生成されたAppでなければなりません。前回のビルドで残った固定パスを再利用すると、ゲートが古い成果物を基に誤った判定を下す可能性があります。

期待値をバージョン管理に含める

多数の期待値をCIプラットフォームの画面にハードコードしないでください。レビューしやすくするには、リリース環境ごとに小さなJSONファイルを用意します。たとえば ci/plist-release.json は次のようになります。

{
  "bundleIdentifier": "com.example.product",
  "urlSchemes": ["example"],
  "requiredUsageKeys": [
    "NSCameraUsageDescription",
    "NSPhotoLibraryUsageDescription"
  ],
  "allowedBackgroundModes": ["remote-notification"]
}

ここには、公開リポジトリに保存しても問題のないルールだけを記録し、トークン、秘密鍵、署名用パスワードは含めません。バージョン番号とビルド番号は通常、パイプラインのパラメータから生成されます。スクリプトでは特定の番号を長期間固定するのではなく、形式とビルド入力との整合性を検証します。

App target、Extension、テストホストごとに個別のルールを用意してください。メインAppのBundle IDルールでWidgetを誤判定したり、カメラ権限が不要なExtensionに対してカメラ説明を含むチェックリストを流用したりしてはいけません。

失敗可能な監査スクリプトを作成する

plutil -extract は辞書や配列の読み取りに適しており、PlistBuddy は単純なスカラー値の取得に適しています。次のスクリプトは最小構成の例です。

#!/bin/bash
set -euo pipefail

PLIST_PATH="${1:?missing Info.plist path}"
EXPECTED_BUNDLE_ID="${EXPECTED_BUNDLE_ID:?missing bundle id}"
EXPECTED_VERSION="${EXPECTED_VERSION:?missing version}"
EXPECTED_BUILD="${EXPECTED_BUILD:?missing build number}"

read_key() {
  /usr/libexec/PlistBuddy -c "Print :$1" "$PLIST_PATH" 2>/dev/null
}

require_key() {
  local key="$1"
  local value
  value="$(read_key "$key" || true)"
  if [[ -z "$value" ]]; then
    printf 'Missing required key: %s
' "$key" >&2
    exit 1
  fi
}

[[ "$(read_key CFBundleIdentifier)" == "$EXPECTED_BUNDLE_ID" ]]
[[ "$(read_key CFBundleShortVersionString)" == "$EXPECTED_VERSION" ]]
[[ "$(read_key CFBundleVersion)" == "$EXPECTED_BUILD" ]]

require_key NSCameraUsageDescription
require_key NSPhotoLibraryUsageDescription

SCHEMES_JSON="$(plutil -extract CFBundleURLTypes json -o - "$PLIST_PATH")"
printf '%s' "$SCHEMES_JSON" | grep -q '"example"'

MODES="$(plutil -extract UIBackgroundModes raw -o - "$PLIST_PATH" 2>/dev/null || true)"
if [[ "$MODES" == *"audio"* ]]; then
  printf 'Unexpected background mode: audio
' >&2
  exit 1
fi

本番用スクリプトでは、キー名、期待値、実際の値も出力する必要があります。ただし、plist全体を出力してはいけません。権限の説明文、クエリ対象のScheme、サードパーティー設定には内部識別子が含まれる可能性があり、そのままログへアップロードすると情報の露出範囲が広がります。

肯定ルールと否定ルールを併用する

「必須」という条件だけでは設定漏れは検出できますが、デバッグ項目の混入は検出できません。Release成果物へのテストサーバー識別子、デバッグ用URL Scheme、ファイル共有スイッチ、未承認のバックグラウンドモードなどを禁止するリストも併せて管理することを推奨します。

配列フィールドの最終的な検証に、単純な文字列包含チェックを使用しないでください。より堅牢な方法は、plutil -extract ... json でJSONを出力し、Ruby、Python、またはプロジェクト既存のスクリプトで要素ごとに比較することです。その際、フィールドが存在しない場合、型が誤っている場合、値が重複している場合の3つを明示的に処理します。

App、Extension、Archiveの両段階を監査する

一つのArchiveには、メインApp、Widget、通知サービスExtension、その他の .appex が含まれる場合があります。メインAppだけを確認すると、Extensionの識別子、バージョン番号、権限設定を見落とします。アーカイブ完了後、すべてのbundleを次のように走査できます。

find "$ARCHIVE_PATH/Products/Applications" \
  \( -name "*.app" -o -name "*.appex" \) -print0 |
while IFS= read -r -d '' bundle; do
  plist="$bundle/Info.plist"
  plutil -lint "$plist"
  printf 'Auditing %s
' "$bundle"
done

ゲートは2段階にすることを推奨します。通常のRelease build後に簡易チェックを行って早期にフィードバックし、archive 完了後にArchive内の成果物を再度確認してリリース可否を判定します。Archive処理では異なるconfiguration、エクスポートパラメータ、パイプライン変数が使用される可能性があるため、2段階目は省略できません。

検査対象 重要なフィールド 失敗時の処理
メインApp 識別子、バージョン、権限説明、URL Scheme Archiveをブロック
App Extension 識別子のプレフィックス、バージョン、Extensionの種類 Archiveをブロック
最終Archive すべてのbundleと本番環境で禁止されている項目 リリースをブロック

よくある誤検知を防ぎ、証跡を残す

よくある誤検知の原因は、空文字列、ブール値の型、配列の順序です。権限説明はキーの存在だけで判断せず、前後の空白を取り除いた後に空でないことを確認します。ブール値はplistの型に従って読み取り、文字列の "false" をブール値として扱わないでください。URL Schemeとバックグラウンドモードは通常、集合として比較し、配列の順序には依存させません。

スクリプトが失敗した場合は、マスキング済みの監査レポートを保存します。レポートにはscheme、configuration、SDK、Appのパス、失敗したキー名、ハッシュを含めます。ビルド環境全体を保存したり、すべての環境変数をログに出力したりしてはいけません。同じクラウドMacで複数のジョブを実行する場合は、ジョブごとに独立した DerivedData を使用し、監査前に成果物の更新日時が現在のジョブに対応していることを確認します。

最後に、監査スクリプトもコードとして管理してください。ルール変更はマージレビューを通し、「キーの欠落、型の不一致、禁止値、Extensionの検査漏れ」について、それぞれテストケースを用意します。これにより、Info.plistの問題はアーカイブ時や提出時に人手で推測するものではなく、パイプライン内で明確かつ再現可能な失敗として検出されます。

よくある質問

ソースのInfo.plistだけでは不十分なのはなぜですか?

Xcodeがビルド設定、生成値、構成別ファイルを統合するためです。実際に配布される値はApp内の最終Info.plistにあります。

最初に監査すべきキーは何ですか?

CFBundleIdentifier、バージョン、ビルド番号、権限説明、URL Scheme、UIBackgroundModesから始め、欠落値と本番禁止値を失敗にします。

監査はarchiveの前後どちらで実行しますか?

通常ビルド後に実行して早く検出し、archive完了後にアーカイブ内のAppを再監査します。リリース判定には後者を使います。

MacVPSGo クラウドMac

独占利用できるApple Siliconビルドマシンが必要ですか?

Xcodeビルド、iOS CI、リモート開発、自動化タスク向けに、独占利用の物理ノードを日単位、週単位、月単位、または四半期単位でレンタルできます。

構成を選んで注文する