HexVMのエンジニアリング実践

クラウドMacでXcodeビルド設定の差分を監査する

クラウドMacでXcodeビルド設定の差分を監査する

同じコミットでも、開発マシンでは正常にアーカイブできるのに、HexVMのクラウドMacではデプロイメントターゲットが変わったり、特定のアーキテクチャが突然ビルド対象から外れたりすることがあります。こうした問題の原因は、ソースコードではなく、Xcodeが最終的に解決したビルド設定にある場合が少なくありません。プロジェクトファイル、.xcconfig、環境変数、コマンドライン引数は順番に設定を上書きするため、いずれか1つのファイルだけを確認しても、ターゲットに実際に適用された設定を把握するのは困難です。

この種の設定ドリフトは、必ずしもすぐにビルドエラーを引き起こすとは限りません。先に成果物だけが変化し、リリース段階になって初めて問題が表面化することもあります。より確実な方法は、不要な差分要因を除去した有効設定のベースラインを保存し、マージ前と正式なアーカイブ前に毎回生成し直して比較することです。

監査対象となる設定範囲を定義する

xcodebuildの出力全体をそのまま保存してはいけません。絶対パス、一時ディレクトリ、ビルド番号によって大量の無意味な差分が発生するため、まず設定を3種類に分類します。

種類 代表的なキー 処理方法
成果物の境界 PRODUCT_BUNDLE_IDENTIFIERSUPPORTED_PLATFORMSARCHS 変更時に必ずレビュー
コンパイル動作 SWIFT_VERSIONSWIFT_OPTIMIZATION_LEVELGCC_PREPROCESSOR_DEFINITIONS 変更時に必ずレビュー
ノード固有のノイズ BUILD_DIRTEMP_DIRPROJECT_TEMP_DIR 削除または正規化

署名関連の設定もベースラインに含めます。ただし、証明書ファイル、秘密鍵の内容、一時的な認証情報をリポジトリに保存してはいけません。監査の対象は設定名、署名方式、権限の境界であり、機密情報そのものではありません。

ベースラインは「永久に変更しない」設定ファイルではありません。コードレビューの対象となる期待状態です。変更は許容されますが、その内容が可視化され、説明可能で、ロールバックできる必要があります。

ターゲットで実際に有効な設定を出力する

最初に、プロジェクト、Scheme、ビルド構成、対象プラットフォームを固定します。ワークスペースを使用するプロジェクトでは、-project-workspaceに置き換え、それ以外の引数は同じにします。

mkdir -p .ci/build-settings

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -showBuildSettings \
  > .ci/build-settings/raw.txt

実行前にxcodebuild -list -project App.xcodeprojを使い、Schemeが共有されていることを確認します。パイプラインで機能フラグやカスタムSYMROOTなどの追加引数を使用している場合は、ベースラインの生成時と実際のビルド時に同じ引数一式を渡さなければなりません。そうしないと、異なる2つのコンテキストを比較することになります。

複数のTargetを持つプロジェクトでは、同名のキーが複数組出力されます。この場合、単純に重複を削除してはいけません。Targetの見出しを残すか、Schemeごとに個別のファイルを作成してください。アプリ、拡張機能、テストターゲットでは、デプロイ対象の範囲が異なっていても不自然ではありません。

パス由来のノイズを除去して安定したベースラインを作る

次のスクリプトは、KEY = VALUE形式の行を抽出し、一時ディレクトリに関するキーを削除したうえで、作業ディレクトリとユーザーディレクトリを安定したマーカーに置き換えます。秘密鍵を解析することも、環境変数全体を出力することもありません。

from pathlib import Path
import os

source = Path(".ci/build-settings/raw.txt")
target = Path(".ci/build-settings/current.txt")

ignored = {
    "BUILD_DIR",
    "BUILD_ROOT",
    "CONFIGURATION_BUILD_DIR",
    "DERIVED_FILES_DIR",
    "PROJECT_TEMP_DIR",
    "TARGET_TEMP_DIR",
    "TEMP_DIR"
}

root = str(Path.cwd())
home = str(Path.home())
rows = []

for line in source.read_text().splitlines():
    stripped = line.strip()
    if " = " not in stripped:
        continue
    key, value = stripped.split(" = ", 1)
    if key in ignored:
        continue
    value = value.replace(root, "<ROOT>").replace(home, "<HOME>")
    rows.append(f"{key}={value}")

target.write_text("
".join(sorted(rows)) + "
")

初回の結果を確認したら、current.txtを用途が分かる名前のベースラインへコピーします。たとえばrelease-ios.txtとし、リポジトリにコミットします。

python3 .ci/normalize_build_settings.py
cp .ci/build-settings/current.txt \
   .ci/build-settings/release-ios.txt

依存関係のパスが引き続き頻繁に変わる場合は、まずそのパスがコンパイル入力に影響するかを判断してください。「差分ゼロ」にするためだけにHEADER_SEARCH_PATHSFRAMEWORK_SEARCH_PATHSOTHER_SWIFT_FLAGSを除外してはいけません。これらのキーの変更こそ、検出すべき問題であることがよくあります。

設定差分をパイプラインのゲートにする

チェックジョブは、依存関係の解決後、正式なアーカイブの前に実行します。これにより、完全な検索パスを取得しつつ、コストの高いアーカイブ処理が始まる前に誤った設定を検出して停止できます。

set -euo pipefail

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -showBuildSettings \
  > .ci/build-settings/raw.txt

python3 .ci/normalize_build_settings.py

diff -u \
  .ci/build-settings/release-ios.txt \
  .ci/build-settings/current.txt

diffがゼロ以外の終了ステータスを返した場合、パイプラインを停止し、差分テキストを保存します。新しいファイルでベースラインを自動的に上書きしてはいけません。それではゲートが単なる記録処理になってしまいます。

設定の許可リストを定義する

無害であることが確認できているキーについては、チームで明示的な除外リストを管理できます。ただし、リストは短く保ち、除外理由を記載する必要があります。次の変更は引き続きブロックすることを推奨します。

  • IPHONEOS_DEPLOYMENT_TARGETSUPPORTED_PLATFORMS
  • ARCHSEXCLUDED_ARCHSONLY_ACTIVE_ARCH
  • SWIFT_VERSIONと最適化レベル
  • CODE_SIGN_STYLEと署名IDの選択方法
  • PRODUCT_BUNDLE_IDENTIFIERとエンタイトルメントファイルのパス
  • DEBUG_INFORMATION_FORMATとリンカー引数

よくある誤検知と実際の設定ドリフトを切り分ける

実行するたびに大量のパス差分が出る場合は、まずルートディレクトリの置換処理が、シンボリックリンク解決後のパスにも対応しているか確認します。ジョブ開始時にpwd -Pを記録し、論理パスと実パスの両方をスクリプトで正規化する方法もあります。

ローカルでは差分がないのにクラウド環境で差分が出続ける場合は、同じSchemeを使用しているか、構成名が一致しているか、コマンドラインで設定が追加されていないか、依存関係の解決が完了しているか、環境変数が.xcconfigの展開に使われていないか、の順に確認します。結果に合わせるために、先にベースラインを変更してはいけません。

実際の変更が見つかった場合は、先に発生源を特定してからベースラインを更新します。

  1. xcodebuild -showBuildSettingsを使い、どのTargetに属する変更かを確認します。
  2. プロジェクトファイル、.xcconfig、パイプライン引数から該当するキーを検索します。
  3. 変更の目的と、DebugおよびReleaseへの影響を説明します。
  4. クリーンビルドとアーカイブを1回実行して検証します。
  5. 設定変更とベースライン更新を同じレビューに含めます。

継続可能な監査サイクルを構築する

設定ベースラインは、Scheme、ビルド構成、プラットフォームごとに分割します。1つのファイルですべてのケースを扱ってはいけません。通常のコミットではメインアプリのReleaseベースラインを確認し、拡張機能、テストバンドル、リリースフローに関係する変更では、対応するチェックも実行します。

Xcodeのバージョン方針を変更するたびに設定を再生成し、差分を1項目ずつレビューしてください。デフォルト値が変わる可能性があるためです。重要なのは差分の数ではなく、コンパイル入力、成果物の構造、署名の境界、実行ターゲットを変える差分がどれかという点です。この証跡を維持すれば、ビルド失敗の原因をマシン全体から推測する必要がなくなり、設定変更もソースコードの変更と同様に追跡できるようになります。

よくある質問

プロジェクトファイルやxcconfigの比較だけでは不十分ですか?

それらは入力値です。xcodebuildの出力には継承、条件付き設定、コマンドライン上書きも反映されるため、実際のビルド設定により近い結果を確認できます。

どの設定変更でパイプラインを停止すべきですか?

デプロイ対象、アーキテクチャ、Swiftバージョン、最適化、署名方式、製品識別子の変更は原則として停止対象です。一時パスは比較前に正規化します。

専用Apple Silicon

次のビルドには専用クラウドMacを選択

ワークロードに合わせてMac miniの機種、利用期間、リージョンを選択できます。各注文には専用の物理ノードが割り当てられ、実際の利用可否はコンソールからリアルタイムで確認できます。

クラウドMacプランを選択