CIパイプラインでAndroidとiOSアプリをビルドする

ローカルでは正常にコンパイルできるモバイルアプリがあるとしよう。コードをリポジトリにプッシュし、CIパイプラインが起動する。10分後、今まで見たことのないエラーで失敗する。SDKのバージョンが違う。依存関係が少し新しいリリースに解決された。署名証明書がない。これがCIでのモバイルビルドの現実だ。ローカルで動くものが、パイプラインではしばしば壊れる。

モバイルCIパイプラインで最初に正しく設定すべきは、ビルドプロセスそのものだ。単にコンパイラを実行するだけでなく、一貫性があり、テスト可能で、配布準備が整ったアーティファクトを生成すること。AndroidとiOSではこのプロセスが異なり、それぞれに固有の落とし穴がある。

Gradleを使ったAndroidビルド

AndroidビルドはGradleを介して実行され、build.gradleファイルから設定を読み取る。ここで重要な3つのSDK設定は、compileSdkminSdktargetSdkだ。これらは混同しやすいが、それぞれ明確な目的がある。

compileSdkはコンパイル時に利用可能なAPIレベルを制御する。これを34に設定すると、Android 14で導入されたAPIを使用できる。minSdkはアプリがサポートする最小のAndroidバージョンだ。これを26に設定すると、Android 8.0以前のデバイスにはアプリをインストールできない。targetSdkは、どのバージョンに対してテストを行ったかをAndroidに伝える。新しいSDKをターゲットにすると、Androidはアプリに影響を与える可能性のある動作変更を適用することがある。

パイプラインはローカル開発環境と同じSDKバージョンを使用しなければならない。バージョンが一致しないと、CIでのみ発生するコンパイルエラーの原因になる。これらのバージョンはbuild.gradleファイルに明示的に固定し、パイプライン設定でも確認すること。

依存関係もよくあるトラブルの原因だ。GradleはMaven Central、Google Maven、または内部リポジトリからライブラリを取得する。キャッシュがないと、パイプラインはビルドのたびにすべての依存関係を最初からダウンロードする。20の依存関係があるプロジェクトでは、ダウンロードだけで10〜15分かかることもある。ビルド間でGradleの依存関係ディレクトリをキャッシュすること。ほとんどのCIプラットフォームは簡単な設定変更でこれをサポートしており、ビルド時間を大幅に短縮できる。

Androidビルドの出力はAPKまたはAABのいずれかだ。APKは従来の形式で、デバイスに直接インストールできる。AABは新しい形式で、Google Playにアップロードすると、各デバイス設定に最適化されたAPKが生成される。内部テストや手動配布にはAPKを使用する。Play Storeを通じた正式リリースにはAABを使用する。パイプラインは両方をサポートすべきだが、どちらを選ぶかはアーティファクトの送り先による。

以下は、AndroidアプリをビルドしてAPKをアーティファクトとして保存する最小限のGitHub Actionsジョブの例だ。

name: Android Build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
      - name: Cache Gradle dependencies
        uses: actions/cache@v4
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - name: Build release APK
        run: ./gradlew assembleRelease
      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: app-release
          path: app/build/outputs/apk/release/*.apk

この例では、JDKのセットアップ、Gradleキャッシュの有効化、リリースビルドの実行、生成されたAPKのアップロードを行っている。JavaバージョンとGradleラッパーのパスはプロジェクトに合わせて調整すること。

Xcodeを使ったiOSビルド

iOSビルドはXcodeとそのビルドシステムを使用する。パイプラインはxcodebuildを実行し、スキーム、設定(DebugまたはRelease)、出力先(シミュレータまたは実機)をパラメータで指定する。プロジェクトファイルは.xcodeprojまたは.xcworkspaceだ。プロジェクトがCocoaPodsを使用している場合は、ワークスペースファイルが必要になる。

依存関係の管理は、iOSビルドがよく壊れるポイントだ。多くのiOSプロジェクトはCocoaPods、Swift Package Manager、またはCarthageを使用している。パイプラインはビルド前にpod installまたはswift package resolveを実行する必要がある。CIで解決されるバージョンがチームのローカル環境と異なると、デバッグが困難なエラーが発生する。依存関係のバージョンは固定すること。CocoaPodsはPodfile.lockファイルを生成する。これをリポジトリにコミットし、パイプラインがそれを使用するようにする。

iOSビルドの出力はIPAファイルだ。APKとは異なり、IPAは署名済みアプリケーションを含むバンドルである。パイプラインは通常、開発用証明書を使った内部テスト用の開発IPAと、App Store用の配布IPAの2種類を生成する。どちらも適切な署名が必要だが、これについては別途説明する。

ビルドアーティファクトの保存

AndroidとiOSの両方のビルドで、保存が必要なアーティファクトが生成される。最も簡単な方法は、CIプラットフォームに組み込まれているアーティファクトストレージを使用することだ。大規模なチームやより複雑なワークフローの場合は、S3やGoogle Cloud Storageなどの共有ストレージにアーティファクトをアップロードする。

次の図は、AndroidとiOSのビルドパイプラインが並行して実行され、アーティファクトストレージで合流する様子を示している。

flowchart TD A[コードのチェックアウト] --> B[Android: Gradleビルド] A --> C[iOS: Xcodeビルド] B --> D[APK / AAB] C --> E[IPA] D --> F[アーティファクトストレージ] E --> F F --> G[バージョンとビルド番号]

すべてのアーティファクトには明確な識別子が必要だ。ファイル名またはメタデータにバージョン番号とビルド番号を含めること。どのアーティファクトも、それを生成した正確なコミットまで追跡できなければならない。これがないと、本番環境の問題のデバッグは推測作業になる。

実践的なチェックリスト

モバイルビルドパイプラインの準備ができたと宣言する前に、次の短いチェックリストを確認すること。

  • build.gradleのSDKバージョンがローカル環境とCI環境で一致している
  • Gradleの依存関係キャッシュが有効になっている
  • Podfile.lockまたは同等のファイルがコミットされ、CIで使用されている
  • Xcodeのスキームと設定がパイプラインで明示的に設定されている
  • アーティファクトのファイル名にバージョン番号とビルド番号が含まれている
  • ビルドアーティファクトが保存され、パイプライン完了後にアクセス可能である

まとめ

モバイルビルドパイプラインは、単にコンパイラを実行することではない。毎回同じ環境、同じ依存関係、同じ設定を再現することだ。ローカルビルドは成功するのにパイプラインが失敗する場合、問題はほぼ常にこれら3つの要素のいずれかの違いにある。それらを固定し、積極的にキャッシュし、アーティファクトに明確な名前を付けること。パイプラインの残りの部分は、この最初のステップが正しく機能することに依存している。