エンジニアリング記事

クラウドMacでKotlin MultiplatformのiOS Frameworkを構築する

クラウドMacでKotlin MultiplatformのiOS Frameworkを構築する

Kotlin Multiplatformのリポジトリがローカルでビルドできても、クラウドMacへ移せば同じ結果がそのまま得られるとは限りません。よくある差異はビジネスロジックではなく、Java、Gradle Wrapper、Kotlin/Native、CocoaPods、Xcodeの組み合わせにあります。また、実機向けとシミュレータ向けの成果物を誤って混在させるケースも少なくありません。確実な方法は、まずツールチェーンを固定し、共有モジュール、Framework、最終的なXcode workspaceを個別に検証することです。

まずビルド入力を固定する

ノードへ初めて接続した直後に、パイプライン全体を実行しないでください。チームのドキュメントに記載された推奨バージョンを見るだけでなく、ビルドで実際に使用されるツールを最初に記録します。

sw_vers
uname -m
xcodebuild -version
xcode-select -p
java -version
./gradlew --version
bundle exec pod --version

リポジトリには、gradlewgradle/wrapper/gradle-wrapper.properties、依存関係のロックファイル、Podfile.lock、Ruby依存関係のロックファイルをコミットします。CIでは、マシンにたまたまインストールされているグローバルGradleを呼び出さず、./gradlewを使用してください。CocoaPodsもbundle exec pod経由で実行し、ジョブごとに異なるgemバージョンが読み込まれないようにします。

Xcodeを切り替える必要がある場合は、ジョブの冒頭で明示的に設定し、すぐに確認します。

sudo xcode-select -s /Applications/Xcode.app
xcodebuild -version

ツールが「インストール済み」であることは、再現性の条件ではありません。バージョン、パス、ロックファイル、実行方法のすべてをログで確認できることが、再現可能なビルドの条件です。

実機向けとシミュレータ向けのターゲットを分ける

Apple Silicon Mac上のiOSシミュレータも通常はarm64ですが、iPhone実機と同じターゲットプラットフォームではありません。CPUアーキテクチャだけを確認していると、リンク段階で原因の分かりにくいエラーが発生します。

共有モジュールには、次のように2つのターゲットを明示しておけます。

kotlin {
    iosArm64()
    iosSimulatorArm64()

    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
        .configureEach {
            binaries.framework {
                baseName = "SharedKit"
                isStatic = true
            }
        }
}

まず共有モジュールだけを個別にビルドし、障害の切り分け範囲を短くします。

./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 \
  --no-daemon --stacktrace

./gradlew :shared:linkReleaseFrameworkIosArm64 \
  --no-daemon --stacktrace

両方のタスクが成功してから、CocoaPodsとXcodeのレイヤーへ進みます。タスク名が存在しない場合は、既存の成果物をコピーして設定上の問題を回避するのではなく、モジュールパス、ターゲット宣言、Framework名を確認してください。

CocoaPodsとXcodeではロック済みの結果だけを使う

CIでは、ビルドのたびにPodを自動更新しないでください。まずpodspecを生成または確認し、Podfile.lockに厳密に従ってインストールします。

./gradlew :shared:podspec
bundle check
bundle exec pod install --project-directory=iosApp

続いて、プロジェクトファイルではなくworkspaceをビルドします。

xcodebuild \
  -workspace iosApp/App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.build/DerivedData" \
  CODE_SIGNING_ALLOWED=NO \
  build

CODE_SIGNING_ALLOWED=NOはシミュレータのビルド検証には適していますが、実機向けアーカイブに必要な署名設定の代わりにはなりません。プロジェクトでカスタム構成を使用している場合は、対応する.xcconfigのFramework検索パスにローカルの絶対パスが指定されていないことも確認してください。

レイヤー別の検査で問題範囲を絞り込む

検査レイヤー 推奨コマンド 失敗時に優先して確認する項目
Kotlinソースコード ./gradlew :shared:compileKotlinIosSimulatorArm64 expect/actual、依存関係の可視性
Frameworkのリンク linkDebugFrameworkIosSimulatorArm64 ターゲットプラットフォーム、ネイティブ依存関係
Pods統合 bundle exec pod install ロックファイル、Ruby環境、podspec
Xcodeビルド xcodebuild -workspace Scheme、検索パス、ビルド構成

ダウンロードはキャッシュし、書き込み可能な作業領域は共有しない

~/.gradle/caches~/.konanは再ダウンロードのコストが高いため、通常はキャッシュする価値があります。一方、PodsとDerivedDataは、より慎重に扱う必要があります。並列ジョブで書き込み可能なDerivedDataを共有すると、あるジョブが別のブランチの中間ファイルを読み込む可能性があります。最も安全なのは、ジョブごとに独立したディレクトリを作成し、終了後に削除する方法です。

キャッシュキーには、少なくとも次の情報を含めます。

  • gradle-wrapper.propertiesと依存関係ロックファイルのハッシュ
  • libs.versions.tomlPodfile.lock、Ruby依存関係ロックファイルのハッシュ
  • xcodebuild -versionの出力
  • JavaのメジャーバージョンとKotlinプラグインのバージョン
  • ターゲット構成がDebugかReleaseか

ユーザーディレクトリ全体を単一の長期キャッシュとしてまとめないでください。無効化が難しくなるだけでなく、認証情報、ログ、無関係な状態まで混入します。キャッシュを復元した後は、軽量なコンパイルを一度実行します。失敗した場合は、ノード上の開発ディレクトリをすべて削除するのではなく、まず現在のジョブのキャッシュを消去してください。

エラーの形から原因を特定し、証拠を残す

building for iOS Simulator, but linking in object file built for iOSが発生した場合は、CocoaPodsを再インストールする前に、Frameworkがどのターゲット向けに生成されたものかを確認してください。framework not foundが発生した場合は、Build Settingsで展開された検索パス、Frameworkの実際の配置場所、スクリプトフェーズがコンパイル前に実行されているかを確認します。

主要な診断出力は、ジョブの添付ファイルとして保存できます。

xcodebuild -showBuildSettings \
  -workspace iosApp/App.xcworkspace \
  -scheme App > .build/build-settings.txt

find shared -type d -name '*.framework' -print \
  > .build/framework-inventory.txt

ローカルでは成功し、リモートでは失敗する場合は、xcodebuild -versionjava -version、ロックファイルのハッシュ、環境変数、workspaceの生成時刻を比較してください。最初からすべてのキャッシュを削除してはいけません。現場の情報が失われるうえ、問題が一時的に消えても原因を説明できなくなります。

マージ前に必要な最小限の確認項目

マージリクエストでは、少なくとも共有モジュールのシミュレータ向けビルド、Xcode workspaceのビルド、Release実機向けFrameworkのリンクを検証します。並列ジョブには独立したDerivedDataを使用し、ログにはツールのバージョンと失敗した完全なコマンドを残し、ビルド成果物はプラットフォーム別に命名してください。これらを徹底すれば、Kotlinレイヤー、Podsレイヤー、Xcodeレイヤーの責任範囲が十分に明確になり、多くの障害を1回のパイプライン内で特定できます。

よくある質問

実機向けarm64 FrameworkをApple Siliconシミュレータで使えますか?

使えません。同じarm64でも対象プラットフォームが異なるため、iosArm64とiosSimulatorArm64を別々にビルドする必要があります。

Kotlin MultiplatformのCIで何をキャッシュすべきですか?

GradleのダウンロードキャッシュとKotlin/Nativeツールチェーンを優先し、ロックファイル、Gradle Wrapper、Xcode、Javaの各バージョンをキャッシュキーに含めます。

専有物理ノード

ビルドタスクを専有クラウドMacで実行

VMOrbit M4またはVMOrbit M4 Proを選び、チームの所在地に合わせてノードを選択します。実際の利用可否と提供情報は、コンソールにリアルタイムで表示される内容をご確認ください。

クラウドMacを選ぶ