エンジニアリングガイド

iOSとMac Catalystの二重ビルドゲートを構築する

iOSとMac Catalystの二重ビルドゲートを構築する

もともとiPhoneとiPadだけを対象としていたプロジェクトでMac Catalystを有効にすると、「iOS向けにコンパイルできるなら、Mac向けも問題なく通るはず」と判断しがちです。しかし、クラウドMac上の自動化ジョブでは、依存関係の対応プラットフォーム宣言、リソースの所属先、条件付きコンパイル、キャッシュの共用に問題が集中します。既存のコマンドにdestinationを1つ追加するだけでは不十分です。2つのプラットフォームを、ソースコードは共有しつつも個別に検証するビルド成果物として扱う必要があります。

プロジェクトが本当に二重ターゲットへ対応しているか確認する

まずXcodeのターゲット設定でMac Catalystを有効にし、対応するプロジェクトファイルの変更がコミットされていることを確認します。GUI上のチェック状態だけで判断してはいけません。自動化ジョブでは、ビルド設定から最終的な値を読み取ります。

xcodebuild \
  -project DemoApp.xcodeproj \
  -scheme DemoApp \
  -showBuildSettings |
grep -E 'SUPPORTS_MACCATALYST|PRODUCT_BUNDLE_IDENTIFIER|SDKROOT'

SUPPORTS_MACCATALYSTYESでなければなりません。ワークスペースを使用している場合は、-project-workspaceに置き換えます。続いてxcodebuild -showdestinationsを実行し、共有schemeでiOS SimulatorとMac Catalystの両方のdestinationが公開されていることを確認します。コマンドラインからschemeが見つからない場合は、destination文字列を何度も変更するのではなく、まずschemeがSharedに設定されているか確認してください。

Swift Package、バイナリ依存関係、内部モジュールについても、対応プラットフォームの範囲を1つずつ確認します。あるパッケージをiOS向けにコンパイルできても、Mac Catalystへの対応が宣言されているとは限りません。クローズドソースのバイナリ依存関係では、XCFrameworkにCatalyst用のスライスが含まれているか確認します。必要なスライスがなければ、リンク段階で初めて失敗します。

キャッシュと結果ディレクトリを分離する

iOSとCatalystでは、異なるモジュールキャッシュ、中間オブジェクト、リンク成果物が生成されます。継続的インテグレーションで同じDerivedDataを共用すると、ジョブの実行順によって失敗したり、クリーンアップ後に一時的に問題が消えたりすることがあります。プラットフォームごとに固定のディレクトリを設定し、結果バンドルも併せてアーカイブすることを推奨します。

set -euo pipefail

xcodebuild \
  -workspace DemoApp.xcworkspace \
  -scheme DemoApp \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath build/derived-ios \
  -resultBundlePath build/results-ios.xcresult \
  CODE_SIGNING_ALLOWED=NO \
  build-for-testing

xcodebuild \
  -workspace DemoApp.xcworkspace \
  -scheme DemoApp \
  -destination 'platform=macOS,variant=Mac Catalyst,arch=arm64' \
  -derivedDataPath build/derived-catalyst \
  -resultBundlePath build/results-catalyst.xcresult \
  CODE_SIGNING_ALLOWED=NO \
  build-for-testing

ここで署名を無効にしているのは、ゲートでソースコード、依存関係、リンクの成立だけを検証するためです。アーカイブと配布用署名は後続ジョブで実行し、プラットフォーム互換性のエラーと署名設定のエラーが同じログに混在しないようにします。

~/Library/Developer/Xcode/DerivedData全体の削除を標準的な解決策にしないでください。現在のジョブのディレクトリだけを正確に削除すれば、同じマシン上のほかのジョブへ影響を与えず、失敗時の状態も保持できます。

プラットフォーム差分を分散させず集約する

Catalystでは、UIライフサイクル、メニュー、ウインドウの挙動、一部のシステム機能がiOSと異なります。条件付きコンパイルはアダプテーション層に集約し、targetEnvironment(macCatalyst)をビジネスロジックの各所に散在させないようにします。

enum PlatformLayout {
    static var usesDesktopNavigation: Bool {
        #if targetEnvironment(macCatalyst)
        return true
        #else
        return false
        #endif
    }
}

プラットフォーム固有APIには、可用性チェックも必要です。条件付きコンパイルで分かるのは現在のコンパイルターゲットだけであり、実行先のOSバージョンでそのAPIが提供されているとは限りません。共通プロトコルには先に安定したインターフェースを定義し、iOS用とCatalyst用の実装をそれぞれ用意すると、ユニットテストから両プラットフォームの挙動を直接検証できます。

リソースも個別に確認する必要があります。フォント、プライバシー記述ファイル、ローカライズリソース、データモデルは、Target Membershipの選択漏れによって片方の成果物にしか含まれないことがあります。実行時に空白画面が表示されて初めて気付くのではなく、ビルド後に成果物ディレクトリを調べ、重要なファイルが存在することを確認してください。

確認項目 iOS Mac Catalyst
依存関係のリンク Simulator用スライスが利用可能 Catalyst用スライスが利用可能
リソースの所属 Appバンドル内に存在 Catalyst Appバンドル内に存在
条件付きコンパイル モバイル向け分岐 デスクトップ適応向け分岐
結果の証跡 個別のxcresult 個別のxcresult

失敗を切り分け可能な段階に分割する

二重ターゲットのジョブを、単一の「ビルド失敗」だけで終わらせてはいけません。依存関係の解決、署名なしコンパイル、ユニットテスト、アーカイブ、配布受け入れ検証の順に分割することを推奨します。前の段階が成功してから次へ進み、ログもプラットフォーム別に命名します。

コンパイル失敗時に最初に確認する4種類のシグナル

No such moduleが発生した場合は、まず依存関係のプラットフォーム宣言とビルドターゲットを確認します。アーキテクチャが一致しない場合は、グローバルな除外アーキテクチャを安易に追加せず、バイナリスライスを確認してください。unavailable APIのエラーが出た場合は、アダプテーション層に戻り、条件付きコンパイルと可用性チェックを追加します。リソースの重複や不足がある場合は、Copy Bundle ResourcesとTarget Membershipを確認します。

ビルドスクリプトにも、特定のプラットフォームを前提とした処理が含まれている場合があります。たとえばSDKをiphoneosに固定しているスクリプトでは、Catalystジョブが誤ったパスを参照します。スクリプトではXcodeから渡されるPLATFORM_NAMESDKROOTTARGET_BUILD_DIRを使用し、未知のプラットフォームを検出したら直ちに終了するようにします。

マージ前チェックリストを整備する

最小限のゲートでは、両方のターゲットについて、クリーンなディレクトリから依存関係の解決と署名なしコンパイルを完了でき、それぞれの.xcresultが保存されることを保証します。コアモジュールのユニットテストでは、プラットフォーム適応インターフェースをカバーする必要があります。ウインドウ、メニュー、ドラッグ&ドロップに関する挙動には、追加でCatalyst専用テストを用意します。

コミット前に、次の順序で確認できます。

  • schemeが共有され、コマンドラインから2つのdestinationを列挙できる。
  • iOSとCatalystで個別のDerivedDataを使用している。
  • 依存関係が両方のプラットフォームへの対応を明示し、必要なバイナリスライスがすべて揃っている。
  • プラットフォーム判定がアダプテーション層に集約され、共通ロジックが重複実装されていない。
  • 重要なリソースが両方の成果物に含まれている。
  • 失敗ログ、結果バンドル、ビルド設定のスナップショットがプラットフォーム別にアーカイブされている。
  • コンパイルゲートとアーカイブ署名ジョブが分離されている。

G-Miniやその他のリモートビルド環境へ初めて導入するときは、まず2回連続で実行し、その後で2つのターゲットの実行順を入れ替えます。結果が順序によって変わる場合は、共有キャッシュ、スクリプトの一時ディレクトリ、削除されていない生成ファイルを優先的に調査してください。二重ターゲットのフローが安定している指標は、両方が偶然同時に成功することではありません。クリーンなワークスペースから、どの実行順でも同じ結果を得られることです。

よくある質問

iOSとMac CatalystでDerivedDataを共有できますか?

短いローカル確認では可能ですが、CIでは分離を推奨します。中間生成物の混在を防ぎ、失敗した側だけを安全に削除して再実行できます。

最初から全テストを実行する必要がありますか?

必要ありません。まず署名なしビルドと主要な単体テストを両方で通し、その後にUIテスト、アーカイブ、署名検証を追加します。

Catalystビルド失敗時の最初の確認点は何ですか?

ターゲットのCatalyst対応、依存パッケージの対応範囲、条件付きコンパイル、リソースのTarget Membership、固有のBuild Phaseを確認します。

専有物理ノード

次のビルドキューに備えるクラウドMac

2種類のM4構成と5つのノードを比較し、実際の作業サイクルに合わせて日単位、週単位、月単位、または四半期単位でレンタルできます。

クラウドMacプランを選ぶ