エンジニアリング記事

クラウドMacでibtoolによるStoryboardとXIB事前検査

クラウドMacでibtoolによるStoryboardとXIB事前検査

開発用Macでは問題なく開けるStoryboardでも、変更をマージした後、リモートのアーカイブ処理が途中で失敗することがあります。この場合、依存関係の再ダウンロード、シミュレータの起動、DerivedDataの削除を行っても解決しません。実際の原因は、無効になったoutlet、カスタムクラスの誤ったモジュール指定、または現在のデプロイメントターゲットではサポートされていないXIB属性かもしれないためです。クラウドMac上の継続的インテグレーションでは、まずibtoolでUIリソースを個別にコンパイルし、その結果を見て完全なビルドへ進むかどうかを判断する方が効率的です。

UIリソースの検査をビルド前に移動する

ibtoolはXcodeに同梱されているため、スクリプトにツールのパスをハードコードするのではなく、xcrun経由で呼び出します。StoryboardとXIBを読み込み、errors、warnings、noticesを出力できるほか、ソースファイルをstoryboardcまたはnibにコンパイルできます。この段階ではシミュレータを起動する必要がないため、依存関係の解決後、xcodebuild archiveの前に配置するのが適しています。

まず、実行ノードで実際に使用されているDeveloperディレクトリを確認します。

set -euo pipefail

xcode-select -p
xcrun --find ibtool
xcodebuild -version
xcrun ibtool --version

これらの情報はジョブログに記録します。同じクラウドMacに複数のXcodeを保持している場合、パイプラインではDEVELOPER_DIRを明示的に設定し、ジョブ終了後にその環境変数を削除する必要があります。そうしないと、後続ジョブが誤ったツールチェーンを引き継ぐおそれがあります。

UIファイルをグラフィカルエディタで開けるからといって、指定したXcode、デプロイメントターゲット、モジュールコンテキストで正常にコンパイルできるとは限りません。CI検査の目的は、特定の開発者のデスクトップ環境ではなく、リリース用ツールチェーンを再現することです。

StoryboardとXIBを走査して個別にコンパイルする

次のスクリプトは、リポジトリ内のUIファイルを走査し、入力ファイルごとに個別の出力ディレクトリを作成します。ビルドディレクトリと依存関係ディレクトリを除外しないと、生成物やサードパーティ製リソースを重複して検査する可能性があります。

#!/usr/bin/env bash
set -euo pipefail

root="${1:-$PWD}"
target="${IPHONEOS_DEPLOYMENT_TARGET:-16.0}"
work="${TMPDIR:-/tmp}/ibtool-preflight"
log="$work/ibtool.log"

rm -rf "$work"
mkdir -p "$work/out"
: > "$log"

find "$root" \
  \( -path "*/DerivedData/*" -o -path "*/build/*" -o -path "*/Pods/*" \) -prune \
  -o \( -name "*.storyboard" -o -name "*.xib" \) -print0 |
while IFS= read -r -d '' source; do
  relative="${source#"$root"/}"
  safe_name="$(printf '%s' "$relative" | tr '/ ' '__')"

  case "$source" in
    *.storyboard)
      output="$work/out/${safe_name%.storyboard}.storyboardc"
      ;;
    *.xib)
      output="$work/out/${safe_name%.xib}.nib"
      ;;
  esac

  printf 'Checking %s
' "$relative" | tee -a "$log"
  xcrun ibtool \
    --errors \
    --warnings \
    --notices \
    --minimum-deployment-target "$target" \
    --compile "$output" "$source" 2>&1 | tee -a "$log"
done

このスクリプトは一時ディレクトリを使用するため、リポジトリを汚しません。失敗時にはibtool.logを保存し、成功時にはコンパイル結果を削除してログの要約だけをアップロードできます。一時出力をバージョン管理へコミットしたり、複数の並列ジョブで固定ディレクトリを共有したりしないでください。

個別事前検査とプロジェクトビルドの境界を理解する

ibtoolを個別に実行すると、XMLの破損、一部の接続異常、互換性のない属性、コンパイルエラーを検出できます。ただし、完全なtargetに適用されるすべてのビルド設定を把握しているわけではありません。カスタムビューコントローラがどのモジュールに属するか、リソースがCopy Bundle Resourcesに追加されているか、条件付きコンパイル後にクラスが存在するかといった点は、引き続きxcodebuildで検証する必要があります。

検査は次の2段階に分けることを推奨します。

レベル 入力 主に検出できる問題 失敗時の処理
高速事前検査 Storyboard、XIB ファイル破損、属性の非互換、基本的な接続エラー 直ちに停止
プロジェクトビルド workspace、project、Scheme モジュール解決、リソースの所属、リンクと署名のコンテキスト 完全なビルドログを保存

個別事前検査には成功したもののプロジェクトビルドが失敗した場合は、Target Membership、Moduleフィールド、クラス名のリファクタリング履歴、デプロイメントターゲットの継承関係を優先して確認します。逆に、事前検査の時点で失敗しているなら、さらに時間のかかるアーカイブ処理を続ける必要はありません。

ローカライズファイルも同じゲートで検査する

Storyboardのローカライズには、言語ごとに独立したリソースを管理する方法と、Base Internationalizationと.stringsを組み合わせる方法があります。前者ではオブジェクトIDのずれが発生しやすく、後者では削除済みのキーが残りやすくなります。まずplutilを使って.stringsの基本形式を検査します。

find . -name "*.strings" -print0 |
while IFS= read -r -d '' file; do
  plutil -lint "$file"
done

Base Storyboardでは、現在のキーセットを生成し、リポジトリ内のローカライズファイルと比較できます。

mkdir -p "${TMPDIR:-/tmp}/ibtool-strings"
xcrun ibtool \
  --generate-strings-file "${TMPDIR:-/tmp}/ibtool-strings/Main.strings" \
  "App/Base.lproj/Main.storyboard"
plutil -lint "${TMPDIR:-/tmp}/ibtool-strings/Main.strings"

生成したファイルは比較専用であり、既存の翻訳を直接上書きしてはいけません。より安全なのは、キー名を抽出して並べ替え、差分を確認する方法です。追加されたキーは翻訳待ちリストへ入れ、削除されたキーはクリーンアップ対象として通知し、既存キーのテキストは引き続きローカライズ工程で管理します。これにより不足を検出しつつ、既存の翻訳がBaseのテキストで誤って置き換えられるのを防げます。

警告ベースラインと一般的な誤検知を管理する

すべての出力を一律に失敗として扱う方法は、短期的には厳格に見えますが、実際には赤いエラー表示を無視する習慣をチームに定着させてしまいます。より実用的なルールは、errorsなら直ちに失敗させ、warningsはリポジトリ内のレビュー済みベースラインと比較して新規項目だけをブロックし、noticesはビルド成果物に保存して定期的に整理することです。

一般的な調査手順は次のとおりです。

  1. DEVELOPER_DIRが完全なビルドと一致していることを確認します。
  2. IPHONEOS_DEPLOYMENT_TARGETがプロジェクトの実際の設定から取得されていることを確認します。
  3. ファイルパスの大文字と小文字を確認します。特に、リネーム時に文字の大小だけを変更したリソースには注意が必要です。
  4. 無効になったoutlet、action、カスタムクラス名を検索します。
  5. 一時ディレクトリがジョブ単位で分離され、並列コンパイルによって出力が上書きされないことを確認します。
  6. 同じSchemeで署名なしビルドをもう一度実行し、モジュールコンテキストを検証します。

警告ベースラインには、一時的な絶対パスではなく、安定した識別子を保存します。Xcodeをアップグレードするときは、まず別ブランチで事前検査を実行し、新しい診断をレビューしてからベースラインを更新します。アップグレードのコミットで新しい警告をすべて直接除外してはいけません。

ロールバック可能な導入手順を整える

初回導入時は、マージをブロックせずに1週間ログだけを収集できます。誤検知の原因を確認した後、結果が確定的なerrorsを失敗条件に設定し、その後で新規warningのゲートを段階的に有効化します。スクリプト自体はリポジトリに固定してローカルMacとクラウドMacで共用し、CI設定の中に別実装を隠さないようにします。

最終的な確認項目は、ツールチェーンが固定されていること、走査対象ディレクトリに明確な除外設定があること、各ジョブが独立した一時ディレクトリを使用すること、デプロイメントターゲットがプロジェクト設定から取得されること、失敗時にログが添付ファイルとして保存されること、ローカライズキーは比較だけを行い上書きしないこと、完全なSchemeビルドも引き続き実行されることです。これにより、UIリソースの問題をアーカイブ終盤まで待つことなく、数十秒程度の事前検査で検出でき、再現困難なエラーだけが残る事態を避けられます。

よくある質問

ibtoolの検査だけでXcodeビルドを省略できますか?

省略できません。UIリソースの早期検査には有効ですが、最終的には実際のSchemeでビルドとテストを実行する必要があります。

ローカルで開けるStoryboardがCIで失敗するのはなぜですか?

Xcodeの選択、配置ターゲット、パスの大文字小文字、カスタムクラスを含むモジュールの解決条件が異なる可能性があります。

ibtoolのnoticeもすべて失敗扱いにするべきですか?

最初から失敗扱いにはしません。errorsのみを阻止し、warningsは基準との差分を確認し、noticesはログとして保存する方法が実用的です。

VMMini M4

専有物理ノードで次のタスクを実行

ノードと課金期間を選択し、固定構成のM4、16GB RAM、256GB SSDでデプロイを開始できます。実際の利用可否はコンソールのリアルタイム表示をご確認ください。

クラウドMacを今すぐレンタル