チームのチャットでこんな愚痴が流れてきた。「Android のビルドは CI で3分で終わるのに、iOS のビルドはローカルで15分もかかって、その上わざわざ Xcode のバージョンを手動で切り替えないといけない」。これは多くの Flutter チームの日常だ——コードは1つなのに、ビルドの経路は2つあり、それぞれ別の環境を維持しなければならない。両プラットフォームのビルドを1台のクラウド Mac mini に集約し、fvm で Flutter のバージョンを固定し、Gradle と CocoaPods のキャッシュをきちんと整理すれば、2つの経路は実はウォームアップコストの大部分を共有できる。以下は実際に動作する設定の記録だ。
なぜ両プラットフォームのビルドを1台の Mac にまとめるのか
Android 単体のビルドなら安価な Linux マシンに置いても構わないが、Flutter プロジェクトの本当の課題は、iOS と Android が同じ pubspec.yaml の依存関係と同じ Flutter SDK バージョンを共有していることにある。両端の環境が分断されていると、チームは2つのバージョン整合表を維持しなければならず、Flutter を一度アップグレードするたびに2台のマシンでそれぞれ検証が必要になる。専有の物理 Mac mini 1台にまとめれば、~/fvm、~/.pub-cache、Gradle と CocoaPods のキャッシュディレクトリはいずれも両端で再利用できる。節約できるのは重複するダウンロードや依存解析の時間であり、ビルド自体の CPU 時間ではない。
専有物理マシンの利点は性能だけではなく、キャッシュの継続性にもある——共有仮想マシン上のキャッシュは、いつ隣のビルドに押し出されるか分からないが、専有環境ではその心配がない。
fvm でバージョンを固定し、チームのビルドを揃える
プロジェクトのルートでバージョンを固定する:
brew install fvm
fvm install 3.24.3
fvm use 3.24.3 --force
echo "3.24.3" > .fvmrc
以降のすべてのビルドコマンドはグローバルの flutter ではなく fvm flutter を使い、チームメンバーのローカルバージョンがずれるのを防ぐ:
fvm flutter pub get
fvm flutter build ios --release --no-codesign
fvm flutter build appbundle --release
CI スクリプトにバージョン検証を追加し、誰かがグローバルの Flutter を手動で変更して .fvmrc の同期を忘れる事態を防ぐ:
LOCKED=$(cat .fvmrc)
CURRENT=$(fvm flutter --version | head -1 | awk '{print $2}')
if [ "$LOCKED" != "$CURRENT" ]; then
echo "flutter version mismatch: locked=$LOCKED current=$CURRENT" >&2
exit 1
fi
Android 側:SDK、NDK と Gradle キャッシュ
クラウド Mac mini には初期状態で Android SDK が入っていないため、初回セットアップは次の順で行う:
brew install --cask android-commandlinetoolssdkmanager --install "platforms;android-34" "build-tools;34.0.0" "ndk;26.3.11579264"~/.gradle/gradle.propertiesで並列処理とデーモンを有効化する:
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m
Gradle デーモンはデフォルトでビルド終了後10分間アイドルになると終了する。ビルドの頻度が高い場合は、--max-workers をマシンのコア数マイナス1に明示的に設定し、iOS 側の xcodebuild と CPU を奪い合わないようにするとよい。
初回の flutter build appbundle はすべての Gradle 依存を取得するので、この時の所要時間を記録しておくと、キャッシュが効いた後との差がより分かりやすくなる。
iOS 側:証明書、Pod キャッシュと flutter build ipa
CocoaPods のキャッシュは2層に分かれる。1つは CDN 経由の pod spec インデックス、もう1つは各 pod のソースコード本体だ:
pod cache list | head
export CP_HOME_DIR="$HOME/.cocoapods-cache"
cd ios && pod install --repo-update
証明書とプロビジョニングプロファイルは、毎回手動でインポートするより match 方式の集中管理にしておくのがよい。設定しておけば、ビルドは1つのコマンドで済む:
fvm flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
署名エラーの9割は Provisioning Profile と Bundle ID の不一致に集中している。ビルド前に security find-identity -v -p codesigning でキーチェーン内の署名 ID がまだ有効か確認しておく方が、エラー発生後に調査するよりずっと速い。
両プラットフォームのパイプラインと所要時間の比較
同一の M4 クラウドインスタンス上で、キャッシュをウォームアップする前後のビルド時間差は明確だ:
| フェーズ | 初回(コールドキャッシュ) | 以降(ウォームキャッシュ) |
|---|---|---|
pub get 依存解決 |
約40秒 | 約8秒 |
| Android Gradle ビルド | 約6分 | 約1分40秒 |
| iOS Pod install | 約2分 | 約20秒 |
| iOS xcodebuild パッケージング | 約5分 | 約3分 |
ウォームキャッシュの前提は、~/.gradle、~/.pub-cache、ios/Pods、~/fvm の4つのディレクトリがクリアされていないことだ。チームで運用する場合は、これら4つのディレクトリをマシン単位のバックアップに含め、各メンバーが個別にキャッシュを持つのではなく共有する方がよい。
よくある落とし穴チェックリスト
- NDK バージョンと Gradle プラグインバージョンの不一致:
ndk;26.3.11579264はandroid/app/build.gradleに記載されたndkVersionと完全に一致させる必要がある。そうでないとビルド時に初めてエラーが出て、1サイクル分の時間を無駄にする。 - CocoaPods と Xcode バージョンの連動:Xcode をアップグレードした後に
pod deintegrate && pod installを実行しないと、たまに発生する署名失敗を証明書の問題と誤診しやすい。 - fvm のキャッシュディレクトリの誤削除:ディスク容量を整理する際、
~/fvm/versionsを不要なファイルと勘違いして削除しやすい。除外リストに個別で追加しておくとよい。 - Gradle デーモンがメモリを占有したまま解放しない:長期間ビルドを回すマシンでは、定期的に
./gradlew --stopを実行し、その後の iOS ビルド用にメモリを解放しておくとよい。
よくある質問
Android ビルドのためだけに Mac mini が必要ですか?
厳密には不要ですが、Flutter プロジェクトは iOS と Android で同じコードと依存関係を共有するため、同じ専有 Mac mini 上で Android ビルドを行えば fvm で固定した SDK や pub キャッシュ、Gradle キャッシュをそのまま再利用でき、別に Linux ランナーを維持するより短期プロジェクトでは合理的です。
fvm でバージョンを固定した後、新しいメンバーは SDK を再度全部ダウンロードする必要がありますか?
最初の一回だけです。fvm は .fvmrc に書かれたバージョンを読み取り ~/fvm/versions に一度だけダウンロードし、以降は同じ専有マシンを使う全員がそのキャッシュを共有するため、2回目以降のビルドではほぼ待ち時間が発生しません。
Gradle や CocoaPods のキャッシュは、レンタル期間が終わると消えてしまいますか?
レンタル期間中は専有物理機が完全にあなたの管理下にあり自動で消去されることはありませんが、期限が近づいたら ~/.gradle、~/.pub-cache、Pods フォルダをまとめてバックアップしておくと、再度レンタルした際にキャッシュを復元でき、ゼロから再構築する必要がなくなります。
コンソール://order
今すぐ使ってみたい方へ
専有物理Mac miniを、1日単位でレンタル。決済完了から約4分でご利用開始できます。