
Hallo! Mein Name ist Yuri Vlad, ich bin ein Android-Entwickler bei Badoo und beteilige mich an der Erstellung der Reaktive Bibliothek - Reactive Extensions auf reinem Kotlin.
Dabei sehen wir uns mit der Tatsache konfrontiert, dass im Fall von Kotlin Multiplatform eine kontinuierliche Integration und kontinuierliche Lieferung eine zusätzliche Konfiguration erfordern. Es ist erforderlich, mehrere virtuelle Maschinen auf verschiedenen Betriebssystemen zu haben, um die Bibliothek vollständig zusammenzustellen. In diesem Artikel werde ich zeigen, wie Sie die kontinuierliche Bereitstellung für Ihre Kotlin Multiplatform-Bibliothek konfigurieren.
Kontinuierliche Integration und kontinuierliche Bereitstellung für Open-Source-Bibliotheken
Kontinuierliche Integration und kontinuierliche Bereitstellung sind dank verschiedener Dienste seit langem Teil der Open-Source-Community. Viele von ihnen bieten ihre Dienste kostenlos für Open-Source-Projekte an: Travis CI, JitPack, CircleCI, Microsoft Azure Pipelines und die kürzlich gestarteten GitHub-Aktionen.
In Badoos Open-Source-Projekten für Android verwenden wir Travis CI für die kontinuierliche Integration und JitPack für die kontinuierliche Bereitstellung.
Nach der Implementierung der iOS-Unterstützung in unserer plattformübergreifenden Bibliothek stellte ich fest, dass wir die Bibliothek nicht mit JitPack erstellen können, da unter macOS keine virtuellen Maschinen bereitgestellt werden (iOS kann nur unter macOS erstellt werden).
Daher wurde für die weitere Veröffentlichung der Bibliothek Bintray ausgewählt , der allen besser bekannt ist. Im Gegensatz zu JitPack, bei dem lediglich alle Ergebnisse des publishToMavenLocal
Aufrufs erfasst wurden, wird die Feinabstimmung veröffentlichter Artefakte unterstützt.
Für die Veröffentlichung wird empfohlen, das Gradle Bintray Plugin zu verwenden, das ich später an unsere Bedürfnisse angepasst habe. Und um das Projekt zu erstellen, habe ich Travis CI aus mehreren Gründen weiter verwendet: Erstens war ich bereits damit vertraut und habe es in fast allen meiner Lieblingsprojekte verwendet. Zweitens werden die virtuellen macOS-Maschinen bereitgestellt, die zum Erstellen auf iOS erforderlich sind.
Wenn Sie sich mit den Grundlagen der Kotlin-Dokumentation befassen, finden Sie einen Abschnitt zum Veröffentlichen von Bibliotheken mit mehreren Plattformen.
Entwickler von Kotlin Multiplatform sind sich der Probleme der Montage auf mehreren Plattformen bewusst (nicht alles kann auf jedem Betriebssystem zusammengestellt werden) und bieten an, die Bibliothek auf verschiedenen Betriebssystemen separat zusammenzustellen.
kotlin { jvm() js() mingwX64() linuxX64() // Note that the Kotlin metadata is here, too. // The mingwx64() target is automatically skipped as incompatible in Linux builds. configure([targets["metadata"], jvm(), js()]) { mavenPublication { targetPublication -> tasks.withType(AbstractPublishToMaven) .matching { it.publication == targetPublication } .all { onlyIf { findProperty("isLinux") == "true" } } } } }
Wie Sie dem obigen Code isLinux
, isLinux
wir abhängig von der an Gradle übergebenen isLinux
Eigenschaft isLinux
Veröffentlichung bestimmter Ziele. Unter den Zielen in der Zukunft meine ich die Montage für eine bestimmte Plattform. Unter Windows wird nur das Windows-Ziel erfasst, während unter anderen Betriebssystemen Metadaten und andere Ziele erfasst werden.
Eine sehr schöne und prägnante Lösung, die nur für publishToMavenLocal
oder für die publish
über das maven-publish
Plugin funktioniert, das aufgrund der Verwendung des Gradle Bintray maven-publish
Plugins für uns nicht geeignet ist.
Ich habe mich entschieden, die Umgebungsvariable zur Auswahl des Ziels zu verwenden, da dieser Code zuvor in Groovy geschrieben wurde, in einem separaten Groovy Gradle-Skript lag und der Zugriff auf die Umgebungsvariablen über einen statischen Kontext erfolgt.
enum class Target { ALL, COMMON, IOS, META; val common: Boolean @JvmName("isCommon") get() = this == ALL || this == COMMON val ios: Boolean @JvmName("isIos") get() = this == ALL || this == IOS val meta: Boolean @JvmName("isMeta") get() = this == ALL || this == META companion object { @JvmStatic fun currentTarget(): Target { val value = System.getProperty("MP_TARGET") return values().find { it.name.equals(value, ignoreCase = true) } ?: ALL } } }
Im Rahmen unseres Projekts habe ich vier Zielgruppen identifiziert:
- ALL - Alle Ziele werden verbunden und gesammelt, für die Entwicklung und als Standardwert verwendet.
- GEMEINSAM - nur Linux-kompatible Ziele werden verbunden und gesammelt. In unserem Fall sind dies JavaScript, JVM, Android JVM, Linux x64 und Linux ARM x32.
- IOS - Nur iOS-Ziele werden verbunden und gesammelt. Sie werden für die Montage unter macOS verwendet.
- META - Alle Ziele sind verbunden, aber nur das Modul mit Metainformationen für Gradle-Metadaten wird zusammengestellt.
Mit diesen Zielgruppen können wir die Zusammenstellung des Projekts in drei verschiedene virtuelle Maschinen (COMMON - Linux, IOS - macOS, META - Linux) parallelisieren.
Im Moment können Sie alles auf macOS bauen, aber meine Lösung hat zwei Vorteile. Wenn wir die Unterstützung für Windows implementieren möchten, müssen wir zunächst nur eine neue Zielgruppe und eine neue virtuelle Maschine unter Windows hinzufügen, um sie zu erstellen. Zweitens müssen Sie keine Ressourcen für virtuelle Maschinen unter macOS für das ausgeben, was Sie unter Linux erstellen können. Die CPU-Zeit auf solchen virtuellen Maschinen ist normalerweise doppelt so teuer.
Was sind Gradle-Metadaten und wofür?
Maven verwendet derzeit POM (Project Object Model), um Abhängigkeiten aufzulösen.
<?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>com.jakewharton.rxbinding2</groupId> <artifactId>rxbinding-leanback-v17-kotlin</artifactId> <version>2.2.0</version> <packaging>aar</packaging> <name>RxBinding Kotlin (leanback-v17)</name> <description>RxJava binding APIs for Android's UI widgets.</description> <url>https://github.com/JakeWharton/RxBinding/</url> <licenses> <license> <name>The Apache Software License, Version 2.0</name> <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> <distribution>repo</distribution> </license> </licenses> <developers> <developer> <id>jakewharton</id> <name>Jake Wharton</name> </developer> </developers> <scm> <connection>scm:git:git://github.com/JakeWharton/RxBinding.git</connection> <developerConnection>scm:git:ssh://git@github.com/JakeWharton/RxBinding.git</developerConnection> <url>https://github.com/JakeWharton/RxBinding/</url> </scm> <dependencies> <dependency> <groupId>com.android.support</groupId> <artifactId>support-annotations</artifactId> <version>28.0.0</version> <scope>compile</scope> </dependency> </dependencies> </project>
Die POM-Datei enthält Informationen zur Bibliotheksversion, ihrem Ersteller und den erforderlichen Abhängigkeiten.
Aber was ist, wenn wir zwei Versionen der Bibliothek für verschiedene JDKs haben möchten? Zum Beispiel hat kotlin-stdlib
zwei Versionen: kotlin-stdlib-jdk8
und kotlin-stdlib-jdk7
. Benutzer müssen die gewünschte Version verbinden.
Beim Upgrade der JDK-Version können externe Abhängigkeiten leicht vergessen werden. Um dieses Problem zu lösen, wurden Gradle-Metadaten erstellt, mit denen Sie zusätzliche Bedingungen für die Verbindung einer bestimmten Bibliothek hinzufügen können.
Eines der unterstützten Gradle-Metadatenattribute ist org.gradle.jvm.version
, das die Version des JDK angibt. Daher könnte für kotlin-stdlib
eine vereinfachte Form einer Metadatendatei folgendermaßen aussehen:
{ "formatVersion": "1.0", "component": { "group": "org.jetbrains.kotlin", "module": "kotlin-stdlib", "version": "1.3.0" }, "variants": [ { "name": "apiElements", "attributes": { "org.gradle.jvm.version": 8 }, "available-at": { "url": "../../kotlin-stdlib-jdk8/1.3.0/kotlin-stdlib-jdk8.module", "group": "org.jetbrains.kotlin", "module": "kotlin-stdlib-jdk8", "version": "1.3.0" } }, { "name": "apiElements", "attributes": { "org.gradle.jvm.version": 7 }, "available-at": { "url": "../../kotlin-stdlib-jdk7/1.3.0/kotlin-stdlib-jdk7.module", "group": "org.jetbrains.kotlin", "module": "kotlin-stdlib-jdk7", "version": "1.3.0" } } ] }
In unserem Fall reaktive-1.0.0-rc1.module
in vereinfachter Form folgendermaßen aus:
{ "formatVersion": "1.0", "component": { "group": "com.badoo.reaktive", "module": "reaktive", "version": "1.0.0-rc1", "attributes": { "org.gradle.status": "release" } }, "createdBy": { "gradle": { "version": "5.4.1", "buildId": "tv44qntk2zhitm23bbnqdngjam" } }, "variants": [ { "name": "android-releaseRuntimeElements", "attributes": { "com.android.build.api.attributes.BuildTypeAttr": "release", "com.android.build.api.attributes.VariantAttr": "release", "com.android.build.gradle.internal.dependency.AndroidTypeAttr": "Aar", "org.gradle.usage": "java-runtime", "org.jetbrains.kotlin.platform.type": "androidJvm" }, "available-at": { "url": "../../reaktive-android/1.0.0-rc1/reaktive-android-1.0.0-rc1.module", "group": "com.badoo.reaktive", "module": "reaktive-android", "version": "1.0.0-rc1" } }, { "name": "ios64-api", "attributes": { "org.gradle.usage": "kotlin-api", "org.jetbrains.kotlin.native.target": "ios_arm64", "org.jetbrains.kotlin.platform.type": "native" }, "available-at": { "url": "../../reaktive-ios64/1.0.0-rc1/reaktive-ios64-1.0.0-rc1.module", "group": "com.badoo.reaktive", "module": "reaktive-ios64", "version": "1.0.0-rc1" } }, { "name": "linuxX64-api", "attributes": { "org.gradle.usage": "kotlin-api", "org.jetbrains.kotlin.native.target": "linux_x64", "org.jetbrains.kotlin.platform.type": "native" }, "available-at": { "url": "../../reaktive-linuxx64/1.0.0-rc1/reaktive-linuxx64-1.0.0-rc1.module", "group": "com.badoo.reaktive", "module": "reaktive-linuxx64", "version": "1.0.0-rc1" } }, ] }
Dank der Attribute org.jetbrains.kotlin
Gradle, in welchem Fall welche Abhängigkeit in den gewünschten Quellensatz zu ziehen ist.
Sie können Metadaten aktivieren, indem Sie:
enableFeaturePreview("GRADLE_METADATA")
Detaillierte Informationen finden Sie in der Dokumentation .
Einstellung veröffentlichen
Nachdem wir die Ziele und die Parallelisierung der Assembly herausgefunden haben, müssen wir konfigurieren, was genau und wie wir veröffentlichen werden.
Für die Veröffentlichung verwenden wir das Gradle Bintray-Plugin. Schauen Sie sich zunächst die README- Datei an und richten Sie Informationen zu unserem Repository und den Anmeldeinformationen für die Veröffentlichung ein.
Wir werden die gesamte Konfiguration in unserem eigenen Plugin im Ordner buildSrc
.
Die Verwendung von buildSrc
bietet mehrere Vorteile, darunter eine stets funktionierende automatische Vervollständigung (bei Kotlin-Skripten funktioniert sie immer noch nicht immer und erfordert häufig einen Aufruf zum Anwenden von Abhängigkeiten) sowie die Möglichkeit, darin deklarierte Klassen wiederzuverwenden und über Groovy- und Kotlin-Skripte darauf zuzugreifen. Sie können ein Beispiel für die Verwendung von buildSrc
aus der neuesten Google-E / A (Abschnitt Gradle) sehen.
private fun setupBintrayPublishingInformation(target: Project) {
Ich verwende drei dynamische Eigenschaften des Projekts: bintray_user
und bintray_key
, die aus den persönlichen Profileinstellungen in Bintray abgerufen werden können , und reaktive_version
, die in der Root-Datei build.gradle
ist.
Für jedes Ziel erstellt das Kotlin Multiplatform Plugin eine MavenPublication , die in PublishingExtension verfügbar ist.
Mit dem Beispielcode aus der Kotlin-Dokumentation, die ich oben bereitgestellt habe, können wir diese Konfiguration erstellen:
private fun createConfigurationMap(): Map<String, Boolean> { val mppTarget = Target.currentTarget() return mapOf( "kotlinMultiplatform" to mppTarget.meta, KotlinMultiplatformPlugin.METADATA_TARGET_NAME to mppTarget.meta, "jvm" to mppTarget.common, "js" to mppTarget.common, "androidDebug" to mppTarget.common, "androidRelease" to mppTarget.common, "linuxX64" to mppTarget.common, "linuxArm32Hfp" to mppTarget.common, "iosArm32" to mppTarget.ios, "iosArm64" to mppTarget.ios, "iosX64" to mppTarget.ios ) }
In dieser einfachen Karte beschreiben wir, welche Veröffentlichungen auf einer bestimmten virtuellen Maschine veröffentlicht werden sollen. Der Veröffentlichungsname ist der Name des Ziels. Diese Konfiguration stimmt voll und ganz mit der oben zitierten Beschreibung der Zielgruppen überein.
Lassen Sie uns die Veröffentlichung in Bintray einrichten. Das Bintray-Plugin erstellt eine BintrayUploadTask
, die wir an unsere Bedürfnisse anpassen.
private fun setupBintrayPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.tasks.named(BintrayUploadTask.getTASK_NAME(), BintrayUploadTask::class) { doFirst {
Jeder, der mit dem Bintray-Plugin arbeitet, stellt schnell fest, dass sein Repository schon lange mit Moos bedeckt ist (das letzte Update war vor etwa sechs Monaten) und dass alle Probleme durch alle Arten von Hacks und Krücken auf der Registerkarte Probleme gelöst werden. Die Unterstützung für eine so neue Technologie wie Gradle Metadata wurde nicht implementiert, aber in der entsprechenden Ausgabe finden Sie eine Lösung, die wir verwenden.
val publishing = project.extensions.getByType(PublishingExtension::class) publishing.publications .filterIsInstance<MavenPublication>() .forEach { publication -> val moduleFile = project.buildDir.resolve("publications/${publication.name}/module.json") if (moduleFile.exists()) { publication.artifact(object : FileBasedMavenArtifact(moduleFile) { override fun getDefaultExtension() = "module" }) } }
Mit diesem Code fügen wir die Datei module.json
zur Liste der zu veröffentlichenden Artefakte hinzu, aufgrund derer Gradle-Metadaten funktionieren.
Aber unsere Probleme enden nicht dort. Wenn Sie versuchen, bintrayPublish
geschieht nichts.
Bei regulären Java- und Kotlin-Bibliotheken ruft Bintray verfügbare Veröffentlichungen automatisch auf und veröffentlicht sie. Im Fall von Kotlin Multiplatform stürzt das Plugin beim automatischen Abrufen von Veröffentlichungen einfach mit einem Fehler ab. Und ja, dafür gibt es auch ein Problem bei GitHub . Und wir werden die Lösung von dort wieder verwenden, nur indem wir die Veröffentlichungen filtern, die wir benötigen.
val publications = publishing.publications .filterIsInstance<MavenPublication>() .filter { val res = taskConfigurationMap[it.name] == true logger.warn("Artifact '${it.groupId}:${it.artifactId}:${it.version}' from publication '${it.name}' should be published: $res") res } .map { logger.warn("Uploading artifact '${it.groupId}:${it.artifactId}:${it.version}' from publication '${it.name}'") it.name } .toTypedArray() setPublications(*publications)
Aber dieser Code funktioniert auch nicht!
Dies liegt daran, dass bintrayUpload
in den Abhängigkeiten, die das Projekt zusammenstellen und die für die Veröffentlichung erforderlichen Dateien erstellen würden, keine Aufgabe hat. Die naheliegendste Lösung besteht darin, publishToMavenLocal
als publishToMavenLocal
Abhängigkeit publishToMavenLocal
, jedoch nicht so einfach.
Beim Sammeln von Metadaten verbinden wir alle Ziele mit dem Projekt. publishToMavenLocal
bedeutet, dass publishToMavenLocal
alle Ziele kompiliert, da die Abhängigkeiten für diese Aufgabe publishToMavenLocalAndroidDebug
, publishToMavenLocalAndroiRelase
, publishToMavenLocalJvm
usw. umfassen.
Daher werden wir eine separate Proxy-Aufgabe erstellen, je nachdem, welche nur wir veröffentlichenToMavenLocalX, die wir benötigen, und wir werden diese Aufgabe selbst in bintrayPublish
Abhängigkeiten setzen.
private fun setupLocalPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.project.tasks.withType(AbstractPublishToMaven::class).configureEach { val configuration = publication?.name ?: run {
Es bleibt nur, den gesamten Code zusammen zu sammeln und das resultierende Plug-In auf ein Projekt anzuwenden, in dem eine Veröffentlichung erforderlich ist.
abstract class PublishPlugin : Plugin<Project> { override fun apply(target: Project) { val taskConfigurationMap = createConfigurationMap() createFilteredPublishToMavenLocalTask(target) setupLocalPublishing(target, taskConfigurationMap) setupBintrayPublishingInformation(target) setupBintrayPublishing(target, taskConfigurationMap) }
apply plugin: PublishPlugin
Den vollständigen PublishPlugin
Code finden Sie in unserem Repository hier .
Konfigurieren Sie Travis CI
Der schwierigste Teil ist vorbei. Travis CI muss weiterhin so konfiguriert werden, dass es die Assembly parallelisiert und Artefakte in Bintray veröffentlicht, wenn eine neue Version veröffentlicht wird.
Wir werden die Veröffentlichung der neuen Version festlegen, indem wir ein Tag für das Commit erstellen.
# ( ) matrix: include: # Linux Android Chrome JS, JVM, Android JVM Linux - os: linux dist: trusty addons: chrome: stable language: android android: components: - build-tools-28.0.3 - android-28 # MP_TARGET, env: MP_TARGET=COMMON # install — Gradle install: true # JVM RxJava2 script: ./gradlew reaktive:check reaktive-test:check rxjava2-interop:check -DMP_TARGET=$MP_TARGET # macOS iOS- - os: osx osx_image: xcode10.2 language: java env: MP_TARGET=IOS install: true script: ./gradlew reaktive:check reaktive-test:check -DMP_TARGET=$MP_TARGET # Linux - os: linux language: android android: components: - build-tools-28.0.3 - android-28 env: MP_TARGET=META # - install: true script: true # Gradle ( ) before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ # , Kotlin/Native - $HOME/.konan/ # Bintray , deploy: skip_cleanup: true provider: script script: ./gradlew bintrayUpload -DMP_TARGET=$MP_TARGET -Pbintray_user=$BINTRAY_USER -Pbintray_key=$BINTRAY_KEY on: tags: true
Wenn die Assembly auf einer der virtuellen Maschinen aus irgendeinem Grund fehlgeschlagen ist, werden die Metadaten und andere Ziele weiterhin auf den Bintray-Server hochgeladen. Aus diesem Grund fügen wir keinen Block mit automatischer Freigabe der Bibliothek auf Bintray über deren API hinzu.
Wenn Sie die Version veröffentlichen, müssen Sie sicherstellen, dass alles in Ordnung ist, und einfach auf die Schaltfläche "Veröffentlichen" für die neue Version auf der Site klicken, da alle Artefakte bereits hochgeladen wurden.
Fazit
Deshalb haben wir in unserem Kotlin Multiplatform-Projekt eine kontinuierliche Integration und Bereitstellung eingerichtet.
Nachdem wir die Aufgaben zum Zusammenstellen, Ausführen von Tests und Veröffentlichen von Artefakten parallelisiert haben, nutzen wir die uns zur Verfügung gestellten Ressourcen effektiv und kostenlos.
Und wenn Sie Linux verwenden (wie Arkady Ivanov arkivanov , Autor der Reaktive Bibliothek), müssen Sie die Person, die macOS (mich) verwendet, nicht mehr jedes Mal, wenn eine neue Version veröffentlicht wird, bitten, die Bibliothek zu veröffentlichen.
Ich hoffe, dass nach der Veröffentlichung dieses Artikels weitere Projekte diesen Ansatz verwenden werden, um Routinetätigkeiten zu automatisieren.
Vielen Dank für Ihre Aufmerksamkeit!