Builds Flutter iOS et Android sur un Mac mini cloud dédié

DevOps et CI/CD ·~5 min de lecture

Builds Flutter iOS et Android sur un Mac mini cloud dédié

Dans le chat d'équipe, quelqu'un s'est plaint : « Le build Android tourne en trois minutes sur la CI, mais le build iOS prend quinze minutes en local, et il faut encore basculer manuellement la version de Xcode. » C'est le lot quotidien de beaucoup d'équipes Flutter : une seule base de code, deux chaînes de build, chacune avec son propre environnement à entretenir. En regroupant les builds des deux plateformes sur un même Mac mini cloud, en verrouillant la version de Flutter avec fvm et en organisant proprement les caches Gradle et CocoaPods, les deux chaînes peuvent en réalité partager la majeure partie du coût de préchauffage. Voici une configuration qui fonctionne en pratique.

Pourquoi regrouper les builds iOS et Android sur un seul Mac

Le build Android seul pourrait très bien tourner sur une machine Linux bon marché, mais le vrai problème dans un projet Flutter, c'est que iOS et Android partagent le même pubspec.yaml et la même version du SDK Flutter. Si les deux environnements sont séparés, l'équipe doit maintenir deux registres d'alignement de version, et chaque montée de version de Flutter doit être validée sur deux machines distinctes. En plaçant tout sur un Mac mini physique dédié, les répertoires ~/fvm, ~/.pub-cache, ainsi que les caches Gradle et CocoaPods, deviennent réutilisables par les deux plateformes. Le gain porte sur le temps évité en téléchargements et résolutions de dépendances redondants, pas sur le temps CPU du build lui-même.

Une machine physique dédiée n'apporte pas seulement de la performance, elle garantit surtout la continuité du cache — sur une VM partagée, le cache peut être évincé à tout moment par le build d'un voisin ; sur un environnement dédié, ce risque disparaît.

Verrouiller la version avec fvm pour aligner les builds d'équipe

Fixer la version à la racine du projet :

brew install fvm
fvm install 3.24.3
fvm use 3.24.3 --force
echo "3.24.3" > .fvmrc

Ensuite, toutes les commandes de build doivent passer par fvm flutter plutôt que par le flutter global, pour éviter que les versions locales des membres de l'équipe ne divergent :

fvm flutter pub get
fvm flutter build ios --release --no-codesign
fvm flutter build appbundle --release

Ajoutez dans le script CI une vérification de version, pour éviter qu'une modification manuelle du Flutter global ne soit oubliée de synchronisation avec .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

Côté Android : SDK, NDK et cache Gradle

Un Mac mini cloud ne vient pas avec le SDK Android préinstallé ; pour la première configuration, suivez cet ordre :

  1. brew install --cask android-commandlinetools
  2. sdkmanager --install "platforms;android-34" "build-tools;34.0.0" "ndk;26.3.11579264"
  3. Activez le parallélisme et le daemon dans ~/.gradle/gradle.properties :
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m

Par défaut, le daemon Gradle se ferme après dix minutes d'inactivité après un build. Si le rythme des builds est soutenu, il est préférable de fixer explicitement --max-workers au nombre de cœurs de la machine moins un, pour éviter qu'il ne se dispute le CPU avec xcodebuild côté iOS.

Le premier flutter build appbundle télécharge l'intégralité des dépendances Gradle : notez ce temps de référence, cela rend plus visible la différence une fois le cache actif.

Côté iOS : certificats, cache Pod et flutter build ipa

Le cache de CocoaPods se compose de deux couches : l'index des pod specs récupéré via CDN, et le code source de chaque pod lui-même :

pod cache list | head
export CP_HOME_DIR="$HOME/.cocoapods-cache"
cd ios && pod install --repo-update

Pour les certificats et profils de provisioning, privilégiez une gestion centralisée façon match plutôt qu'un import manuel à chaque fois ; une fois configuré, le build ne tient qu'en une commande :

fvm flutter build ipa --release \
  --export-options-plist=ios/ExportOptions.plist

Neuf erreurs de signature sur dix viennent d'une incompatibilité entre le Provisioning Profile et le Bundle ID. Avant de lancer le build, vérifiez avec security find-identity -v -p codesigning que l'identité de signature est toujours valide dans le trousseau — c'est bien plus rapide que de diagnostiquer après coup.

Pipeline double plateforme et comparatif des temps

Sur une même instance cloud M4, l'écart de durée entre un build avant et après préchauffage du cache est net :

Étape Premier build (cache froid) Builds suivants (cache chaud)
Résolution des dépendances pub get ~40 secondes ~8 secondes
Build Gradle Android ~6 minutes ~1 min 40
pod install iOS ~2 minutes ~20 secondes
Génération de l'IPA via xcodebuild ~5 minutes ~3 minutes

Un cache chaud suppose que les quatre répertoires ~/.gradle, ~/.pub-cache, ios/Pods et ~/fvm n'aient pas été vidés. Pour le travail en équipe, il est conseillé d'inclure ces quatre répertoires dans une sauvegarde au niveau machine, plutôt que de laisser chacun maintenir son propre cache.

Liste des pièges courants

  • Incompatibilité entre la version du NDK et celle du plugin Gradle : ndk;26.3.11579264 doit correspondre exactement au ndkVersion déclaré dans android/app/build.gradle, sinon l'erreur ne survient qu'au moment du build, gaspillant tout un cycle.
  • Lien entre CocoaPods et la version de Xcode : après une mise à jour de Xcode, si vous n'exécutez pas pod deintegrate && pod install, un échec de signature sporadique sera facilement pris à tort pour un problème de certificat.
  • Suppression accidentelle du répertoire de cache fvm : lors du nettoyage de l'espace disque, ~/fvm/versions est souvent confondu avec des fichiers orphelins et supprimé par erreur ; mieux vaut l'ajouter explicitement à une liste d'exclusion.
  • Le daemon Gradle qui consomme la mémoire sans la libérer : sur une machine qui build en continu, exécutez régulièrement ./gradlew --stop pour libérer la mémoire au profit du build iOS suivant.

Questions fréquentes

Ai-je vraiment besoin d'un Mac mini juste pour le build Android ?

Pas strictement, mais comme un projet Flutter partage code et dépendances entre les deux plateformes, garder le build Android sur le même Mac mini dédié permet de réutiliser le SDK verrouillé par fvm, le cache pub et le cache Gradle déjà chauds côté iOS, ce qui coûte moins que de maintenir un runner Linux séparé.

Après un verrouillage avec fvm, un nouveau collègue doit-il retélécharger tout le SDK ?

Seulement la première fois. fvm lit la version dans .fvmrc et la télécharge une fois dans ~/fvm/versions ; ensuite, toute personne sur la même machine dédiée réutilise ce cache, donc dès le second build l'attente devient quasi nulle sur un Mac mini cloud partagé.

Les caches Gradle et CocoaPods disparaissent-ils à la fin de la location ?

Pendant la période de location, la machine dédiée reste entièrement à toi et rien n'est effacé automatiquement, mais avant l'échéance il vaut mieux archiver ~/.gradle, ~/.pub-cache et le dossier Pods afin qu'une location renouvelée puisse les restaurer plutôt que tout reconstruire.

portal://order

Besoin d'une machine dès maintenant ?

Mac mini physique dédié, location à la journée dès le départ, disponible en environ 4 minutes après le paiement.

Démarrer →