Im Team-Chat kam eine Beschwerde auf: „Der Android-Build läuft in der CI in drei Minuten durch, aber der iOS-Build hat lokal fünfzehn Minuten gebraucht, und ich musste zwischendurch noch manuell die Xcode-Version wechseln." Das ist Alltag für viele Flutter-Teams — eine Codebasis, zwei Build-Pfade, aber jeweils eine eigene Umgebung, die gepflegt werden muss. Verlegt man die Release-Builds für beide Plattformen auf denselben Cloud-Mac-mini, fixiert die Flutter-Version mit fvm und bringt Gradle- und CocoaPods-Caching in Ordnung, teilen sich beide Pfade den größten Teil der Aufwärmkosten. Im Folgenden eine Konfiguration, die in der Praxis funktioniert.
Warum beide Build-Pfade auf einem Mac laufen sollten
Android-Builds allein könnten auf einer günstigen Linux-Maschine laufen, aber der eigentliche Schmerzpunkt bei Flutter-Projekten ist, dass iOS und Android dieselbe pubspec.yaml-Abhängigkeitsliste und dieselbe Flutter-SDK-Version teilen. Trennt man die beiden Umgebungen, muss das Team zwei separate Versionsabgleiche pflegen, und jedes Flutter-Upgrade muss auf zwei Maschinen einzeln verifiziert werden. Legt man alles auf einen einzigen dedizierten physischen Mac-mini, lassen sich ~/fvm, ~/.pub-cache sowie die Gradle- und CocoaPods-Cache-Verzeichnisse von beiden Plattformen wiederverwenden. Was man dabei einspart, ist nicht CPU-Zeit beim Build selbst, sondern wiederholtes Herunterladen und erneutes Auflösen von Abhängigkeiten.
Der Vorteil einer dedizierten physischen Maschine liegt nicht nur in der Rohleistung — es geht um Cache-Kontinuität. Caches auf einer geteilten VM können jederzeit durch den Build eines Nachbarn verdrängt werden, in einer dedizierten Umgebung passiert das nicht.
Version mit fvm fixieren, damit das ganze Team gegen dieselbe SDK baut
Version im Projekt-Root festlegen:
brew install fvm
fvm install 3.24.3
fvm use 3.24.3 --force
echo "3.24.3" > .fvmrc
Ab jetzt laufen alle Build-Befehle über fvm flutter statt über das globale flutter, damit die lokale Version bei den Teammitgliedern nicht auseinanderdriftet:
fvm flutter pub get
fvm flutter build ios --release --no-codesign
fvm flutter build appbundle --release
Im CI-Skript eine Versionsprüfung ergänzen, um Fälle abzufangen, in denen jemand die globale Flutter-Installation geändert, aber vergessen hat, .fvmrc anzupassen:
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-Seite: SDK, NDK und Gradle-Cache
Ein Cloud-Mac-mini bringt standardmäßig kein Android SDK mit. Beim erstmaligen Einrichten in dieser Reihenfolge vorgehen:
brew install --cask android-commandlinetoolssdkmanager --install "platforms;android-34" "build-tools;34.0.0" "ndk;26.3.11579264"- In
~/.gradle/gradle.propertiesParallelisierung und Daemon aktivieren:
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m
Der Gradle-Daemon beendet sich standardmäßig zehn Minuten nach Abschluss eines Builds, wenn er im Leerlauf bleibt. Bei dichtem Build-Takt empfiehlt es sich, --max-workers explizit auf die Kernanzahl minus eins zu setzen, damit Gradle nicht mit xcodebuild auf der iOS-Seite um CPU-Zeit konkurriert.
Der erste Aufruf von flutter build appbundle lädt sämtliche Gradle-Abhängigkeiten komplett neu herunter — die Dauer notieren, um später den Unterschied vor und nach dem Cache-Aufwärmen klar vergleichen zu können.
iOS-Seite: Zertifikate, Pod-Cache und flutter build ipa
Das CocoaPods-Caching besteht aus zwei Schichten: dem über das CDN geladenen Pod-Spec-Index und dem eigentlichen Quellcode der jeweiligen Pods:
pod cache list | head
export CP_HOME_DIR="$HOME/.cocoapods-cache"
cd ios && pod install --repo-update
Für Zertifikate und Provisioning Profiles empfiehlt sich eine zentrale Verwaltung im Stil von match, statt sie bei jedem Build manuell zu importieren. Ist das einmal eingerichtet, reicht für den Build ein einziger Befehl:
fvm flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
Neun von zehn Signierungsfehlern lassen sich auf ein Provisioning Profile zurückführen, das nicht zur Bundle-ID passt. Vor dem Build mit security find-identity -v -p codesigning prüfen, ob die Signing-Identität im Schlüsselbund noch gültig ist — das geht deutlich schneller, als dem Fehler erst nach einem gescheiterten Build nachzugehen.
Build-Pipeline und Zeitvergleich
Auf einer einzelnen M4-Cloud-Instanz unterscheiden sich die Build-Zeiten vor und nach dem Cache-Aufwärmen deutlich:
| Phase | Erster Durchlauf (kalter Cache) | Folgedurchlauf (warmer Cache) |
|---|---|---|
pub get-Abhängigkeitsauflösung |
ca. 40 Sekunden | ca. 8 Sekunden |
| Android-Gradle-Build | ca. 6 Minuten | ca. 1 Min. 40 Sek. |
| iOS Pod install | ca. 2 Minuten | ca. 20 Sekunden |
| iOS-xcodebuild-Paketierung | ca. 5 Minuten | ca. 3 Minuten |
Ein warmer Cache setzt voraus, dass ~/.gradle, ~/.pub-cache, ios/Pods und ~/fvm nicht geleert wurden. Für die Teamarbeit empfiehlt es sich, diese vier Verzeichnisse auf Maschinenebene zu sichern, statt dass jedes Teammitglied seinen eigenen separaten Cache pflegt.
Häufige Stolperfallen
- NDK-Version passt nicht zur Gradle-Plugin-Version:
ndk;26.3.11579264muss exakt mit der inandroid/app/build.gradledeklariertenndkVersionübereinstimmen. Andernfalls zeigt sich der Fehler erst beim Build und kostet eine ganze Runde Zeit. - CocoaPods und Xcode-Version nicht synchron: Nach einem Xcode-Upgrade führt das Auslassen von
pod deintegrate && pod installgelegentlich zu Signierungsfehlern, die fälschlich als Zertifikatsproblem gedeutet werden. - fvm-Cache-Verzeichnis versehentlich gelöscht: Beim Aufräumen von Speicherplatz wird
~/fvm/versionsleicht für verwaiste Dateien gehalten und gelöscht — am besten explizit auf die Ausschlussliste setzen. - Gradle-Daemon gibt Speicher nicht frei: Auf Maschinen, die dauerhaft Builds fahren, regelmäßig
./gradlew --stopausführen, um Speicher für den anschließenden iOS-Build freizugeben.
Häufig gestellte Fragen
Brauche ich wirklich einen Mac mini nur für den Android-Build?
Nicht zwingend, aber da ein Flutter-Projekt Code und Abhängigkeiten für beide Plattformen teilt, spart es Aufwand, den Android-Build auf demselben dedizierten Mac mini laufen zu lassen, der bereits die fvm-fixierte SDK, den Pub-Cache und den Gradle-Cache bereithält, statt einen separaten Linux-Runner zu pflegen.
Muss ein neues Teammitglied nach dem Fixieren mit fvm die komplette SDK erneut herunterladen?
Nur beim ersten Mal. fvm liest die Version aus .fvmrc und lädt sie einmal nach ~/fvm/versions, danach greift jeder auf der gleichen dedizierten Maschine auf denselben Cache zu, wodurch ab dem zweiten Build kaum noch Wartezeit entsteht.
Gehen Gradle- und CocoaPods-Caches verloren, wenn die Mietzeit abläuft?
Während der Mietdauer gehört die dedizierte Maschine dir allein und nichts wird automatisch gelöscht, aber vor Ablauf solltest du ~/.gradle, ~/.pub-cache und den Pods-Ordner sichern, damit eine erneute Anmietung sie wiederherstellen kann statt jeden Cache neu aufzubauen.
portal://order
Jetzt gleich eine Maschine mieten?
Dedizierter physischer Mac mini, Mietbeginn ab einem Tag – in rund 4 Minuten von der Zahlung bis zur Nutzung.