Engineering-Artikel

Kotlin-Multiplatform-iOS-Frameworks auf einem Cloud-Mac bauen

Kotlin-Multiplatform-iOS-Frameworks auf einem Cloud-Mac bauen

Dass sich ein Kotlin-Multiplatform-Repository lokal kompilieren lässt, bedeutet nicht, dass ein Cloud-Mac automatisch dasselbe Ergebnis liefert. Die häufigsten Abweichungen liegen nicht im Anwendungscode, sondern im Zusammenspiel von Java, Gradle Wrapper, Kotlin/Native, CocoaPods und Xcode. Eine weitere typische Fehlerquelle ist die Verwechslung von Artefakten für physische Geräte und Simulatoren. Für reproduzierbare Builds wird daher zuerst die Toolchain festgeschrieben. Anschließend werden das gemeinsame Modul, das Framework und der endgültige Xcode-Arbeitsbereich getrennt geprüft.

Build-Eingaben zuerst festschreiben

Nach der ersten Verbindung mit dem Knoten sollte nicht sofort die vollständige Pipeline gestartet werden. Zunächst müssen die tatsächlich vom Build verwendeten Werkzeuge protokolliert werden. Die in der Teamdokumentation empfohlenen Versionen allein reichen dafür nicht aus.

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

Das Repository sollte gradlew, gradle/wrapper/gradle-wrapper.properties, Sperrdateien für Abhängigkeiten, Podfile.lock und die Sperrdatei für Ruby-Abhängigkeiten enthalten. In der CI ist ./gradlew zu verwenden und nicht eine zufällig auf dem Rechner installierte globale Gradle-Version. Auch CocoaPods sollte über bundle exec pod ausgeführt werden, damit verschiedene Jobs nicht unterschiedliche gem-Versionen laden.

Wenn die Xcode-Version gewechselt werden muss, ist sie zu Beginn des Jobs explizit festzulegen und sofort zu prüfen:

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

Die Aussage, ein Werkzeug sei „installiert“, ist keine Voraussetzung für reproduzierbare Builds. Reproduzierbarkeit setzt voraus, dass Version, Pfad, Sperrdateien und Aufrufweg in den Protokollen nachvollziehbar sind.

Ziele für Geräte und Simulatoren getrennt definieren

Der iOS-Simulator auf einem Apple-Silicon-Mac verwendet üblicherweise ebenfalls arm64. Trotzdem handelt es sich nicht um dieselbe Zielplattform wie bei einem physischen iPhone. Wer nur die CPU-Architektur prüft, erhält in der Link-Phase möglicherweise schwer verständliche Fehler.

Für das gemeinsame Modul können zwei eindeutig getrennte Ziele definiert werden:

kotlin {
    iosArm64()
    iosSimulatorArm64()

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

Zunächst wird nur das gemeinsame Modul gebaut, um den möglichen Fehlerpfad zu verkürzen:

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

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

Erst wenn beide Tasks erfolgreich abgeschlossen sind, folgen die CocoaPods- und Xcode-Ebenen. Existiert ein Taskname nicht, müssen Modulpfad, Zieldefinition und Framework-Name geprüft werden. Das Kopieren vorhandener Artefakte ist kein geeigneter Weg, um Konfigurationsprobleme zu umgehen.

CocoaPods und Xcode nur gesperrte Ergebnisse verwenden lassen

Die CI sollte Pods nicht bei jedem Build eigenständig aktualisieren. Zunächst wird die podspec erzeugt oder geprüft. Danach erfolgt die Installation strikt anhand von Podfile.lock:

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

Anschließend muss der Workspace und nicht die Projektdatei gebaut werden:

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 eignet sich zur Build-Prüfung im Simulator, ersetzt aber nicht die Signaturkonfiguration für Archive physischer Geräte. Verwendet das Projekt benutzerdefinierte Konfigurationen, muss außerdem sichergestellt werden, dass in der zugehörigen .xcconfig kein lokaler absoluter Pfad für die Framework-Suche eingetragen ist.

Fehlerbereich durch Prüfungen auf mehreren Ebenen eingrenzen

Prüfebene Empfohlener Befehl Bei Fehlern zuerst prüfen
Kotlin-Quellcode ./gradlew :shared:compileKotlinIosSimulatorArm64 expect/actual, Sichtbarkeit von Abhängigkeiten
Framework-Linking linkDebugFrameworkIosSimulatorArm64 Zielplattform, native Abhängigkeiten
Pods-Integration bundle exec pod install Sperrdateien, Ruby-Umgebung, podspec
Xcode-Build xcodebuild -workspace Scheme, Suchpfade, Build-Konfiguration

Downloads cachen, beschreibbare Arbeitsbereiche nicht teilen

~/.gradle/caches und ~/.konan sollten in der Regel gecacht werden, da erneute Downloads vergleichsweise teuer sind. Bei Pods und DerivedData ist dagegen größere Vorsicht erforderlich. Nutzen parallele Jobs dasselbe beschreibbare DerivedData-Verzeichnis, kann ein Job Zwischendateien aus einem anderen Branch einlesen. Am zuverlässigsten ist es, für jeden Job ein separates Verzeichnis anzulegen und dieses anschließend zu löschen.

Der Cache-Schlüssel sollte mindestens Folgendes enthalten:

  • Prüfsummen von gradle-wrapper.properties und den Sperrdateien für Abhängigkeiten
  • Prüfsummen von libs.versions.toml, Podfile.lock und der Sperrdatei für Ruby-Abhängigkeiten
  • Ausgabe von xcodebuild -version
  • Java-Hauptversion und Version des Kotlin-Plugins
  • Zielkonfiguration Debug oder Release

Das gesamte Benutzerverzeichnis sollte nicht als langfristiger Cache gepackt werden. Ein solcher Cache lässt sich nicht nur schwer korrekt invalidieren, sondern enthält möglicherweise auch Zugangsdaten, Protokolle und irrelevante Zustände. Nach dem Wiederherstellen des Caches sollte ein kleiner Kompiliertest ausgeführt werden. Schlägt er fehl, ist zunächst der Cache des aktuellen Jobs zu löschen, nicht sämtliche Entwicklungsverzeichnisse auf dem Knoten.

Fehler anhand ihres Musters lokalisieren und Belege sichern

Bei building for iOS Simulator, but linking in object file built for iOS sollte zuerst geprüft werden, für welches Ziel das Framework gebaut wurde. Eine Neuinstallation von CocoaPods ist hier nicht der erste Schritt. Bei framework not found sind die aufgelösten Suchpfade in den Build Settings, der tatsächliche Speicherort des Frameworks und die Ausführungsreihenfolge der Skriptphasen zu prüfen.

Wichtige Diagnoseausgaben können als Job-Artefakte gespeichert werden:

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

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

Wenn der Build lokal erfolgreich ist, remote jedoch fehlschlägt, sollten xcodebuild -version, java -version, die Prüfsummen der Sperrdateien, die Umgebungsvariablen und der Erstellungszeitpunkt des Workspace verglichen werden. Nicht sofort sämtliche Caches löschen: Dadurch gehen die Spuren des Fehlers verloren, während das Problem möglicherweise nur vorübergehend verschwindet und weiterhin unerklärt bleibt.

Mindestprüfungen vor dem Zusammenführen

Ein Merge Request sollte mindestens den Simulator-Build des gemeinsamen Moduls, den Build des Xcode-Workspace und das Linking des Release-Frameworks für physische Geräte prüfen. Parallele Jobs verwenden jeweils eigenes DerivedData. Die Protokolle enthalten die Werkzeugversionen sowie den vollständigen fehlgeschlagenen Befehl, und Build-Artefakte werden nach Zielplattform benannt. Damit sind die Zuständigkeiten zwischen Kotlin-, Pods- und Xcode-Ebene klar genug abgegrenzt, um die meisten Fehler innerhalb eines einzigen Pipeline-Durchlaufs zu lokalisieren.

Häufig gestellte Fragen

Kann ein arm64-Geräte-Framework im Apple-Silicon-Simulator laufen?

Nein. Trotz gleicher CPU-Architektur unterscheiden sich die Plattformkennungen. iosArm64 und iosSimulatorArm64 müssen separat gebaut werden.

Welche Verzeichnisse sollten in Kotlin-Multiplatform-Builds gecacht werden?

Sinnvoll sind vor allem Gradle-Downloads und die Kotlin/Native-Toolchain. Der Cache-Schlüssel muss Lockdateien sowie Gradle-, Xcode- und Java-Versionen berücksichtigen.

Dedizierter physischer Knoten

Build-Aufgaben auf einen dedizierten Cloud-Mac verlagern

Wählen Sie VMOrbit M4 oder VMOrbit M4 Pro und wählen Sie den Knoten passend zum Standort Ihres Teams. Die tatsächliche Verfügbarkeit und Bereitstellungsinformation richtet sich nach den Live-Angaben in der Konsole.

Cloud-Mac auswählen