Livraison continue pour votre bibliothèque Kotlin Multiplatform

Le logo


Salut Je m'appelle Yuri Vlad, je suis développeur Android chez Badoo et participe à la création de la bibliothèque Reaktive - Reactive Extensions sur Kotlin pur.


Dans le processus, nous sommes confrontés au fait que dans le cas de l'intégration continue et de la livraison continue de Kotlin Multiplatform, une configuration supplémentaire est nécessaire. Il est nécessaire d'avoir plusieurs machines virtuelles sur différents systèmes d'exploitation afin d'assembler complètement la bibliothèque. Dans cet article, je vais vous montrer comment configurer la livraison continue pour votre bibliothèque Kotlin Multiplatform.


Intégration et livraison continues pour les bibliothèques open source


L'intégration continue et la livraison continue font depuis longtemps partie de la communauté open source grâce à divers services. Beaucoup d'entre eux fournissent gratuitement leurs services à des projets open source: Travis CI, JitPack, CircleCI, Microsoft Azure Pipelines, les actions GitHub récemment lancées.


Dans les projets open source de Badoo pour Android, nous utilisons Travis CI pour une intégration continue et JitPack pour une livraison continue.


Après avoir implémenté le support iOS dans notre bibliothèque multi-plateforme, j'ai constaté que nous ne pouvons pas construire la bibliothèque à l'aide de JitPack, car il ne fournit pas de machines virtuelles sur macOS (iOS ne peut être construit que sur macOS).


Par conséquent, pour une publication ultérieure de la bibliothèque, Bintray , plus familier à tout le monde, a été choisi . Il prend en charge un réglage plus fin des artefacts publiés, contrairement à JitPack, qui a simplement pris tous les résultats de l'appel publishToMavenLocal .


Pour la publication, il est recommandé d'utiliser le plugin Gradle Bintray, que j'ai ensuite personnalisé selon nos besoins. Et pour construire le projet, j'ai continué à utiliser Travis CI pour plusieurs raisons: premièrement, je le connaissais déjà et je l'ai utilisé dans presque tous mes projets pour animaux de compagnie; deuxièmement, il fournit les machines virtuelles macOS nécessaires pour construire sur iOS.


Assemblage parallèle d'une bibliothèque multi-plateforme


Si vous plongez dans les entrailles de la documentation Kotlin, vous pouvez trouver une section sur la publication de bibliothèques multi-plateformes.


Les développeurs de Kotlin Multiplatform sont conscients des problèmes d'assemblage multiplateforme (tout ne peut pas être construit sur n'importe quel système d'exploitation) et proposent de construire la bibliothèque séparément sur différents systèmes d'exploitation.


 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" } } } } } 

Comme vous pouvez le voir dans le code ci-dessus, selon la propriété isLinux passée à Gradle, nous isLinux publication de certaines cibles. Sous les objectifs à l'avenir, je parlerai de l'assemblage d'une plate-forme spécifique. Sous Windows, seule la cible Windows sera collectée, tandis que sur d'autres systèmes d'exploitation, les métadonnées et les autres cibles seront collectées.


Une solution très belle et concise qui ne fonctionne que pour publishToMavenLocal ou publish partir du plugin maven-publish , qui ne nous convient pas en raison de l'utilisation du plugin Gradle Bintray .


J'ai décidé d'utiliser la variable d'environnement pour sélectionner la cible, car ce code a été précédemment écrit en Groovy, se trouve dans un script Groovy Gradle distinct et l'accès aux variables d'environnement se fait à partir d'un contexte statique.


 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 } } } 

Dans le cadre de notre projet, j'ai identifié quatre groupes de cibles:


  1. ALL - toutes les cibles sont connectées et collectées, utilisées pour le développement et comme valeur par défaut.
  2. COMMUN - seules les cibles compatibles Linux sont connectées et collectées. Dans notre cas, il s'agit de JavaScript, JVM, JVM Android, Linux x64 et Linux ARM x32.
  3. IOS - seules les cibles iOS sont connectées et collectées; elles sont utilisées pour l'assemblage sur macOS.
  4. META - toutes les cibles sont connectées, mais seul le module contenant les méta-informations pour les métadonnées Gradle est assemblé.

Avec cet ensemble de groupes cibles, nous pouvons paralléliser l'assemblage du projet en trois machines virtuelles différentes (COMMUN - Linux, IOS - macOS, META - Linux).


Pour le moment, vous pouvez tout construire sur macOS, mais ma solution présente deux avantages. Premièrement, si nous décidons de mettre en œuvre la prise en charge de Windows, nous avons juste besoin d'ajouter un nouveau groupe cible et une nouvelle machine virtuelle sur Windows pour le construire. Deuxièmement, il n'est pas nécessaire de dépenser des ressources de machine virtuelle sur macOS sur ce que vous pouvez construire sur Linux. Le temps CPU sur ces machines virtuelles est généralement deux fois plus cher.


Métadonnées Gradle


Que sont les métadonnées Gradle et à quoi servent-elles?


Maven utilise actuellement POM (Project Object Model) pour résoudre les dépendances.


 <?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> 

Le fichier POM contient des informations sur la version de la bibliothèque, son créateur et les dépendances nécessaires.


Mais que se passe-t-il si nous voulons avoir deux versions de la bibliothèque pour différents JDK? Par exemple, kotlin-stdlib a deux versions: kotlin-stdlib-jdk8 et kotlin-stdlib-jdk7 . Les utilisateurs doivent connecter la version souhaitée.


Lors de la mise à niveau de la version JDK, il est facile d'oublier les dépendances externes. C'est pour résoudre ce problème que Gradle Metadata a été créé, ce qui vous permet d'ajouter des conditions supplémentaires pour connecter une bibliothèque particulière.


Un des attributs de métadonnées Gradle pris en charge est org.gradle.jvm.version , qui indique la version du JDK. Par conséquent, pour kotlin-stdlib une forme simplifiée d'un fichier de métadonnées pourrait ressembler à ceci:


 { "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" } } ] } 

Plus précisément, dans notre cas, reaktive-1.0.0-rc1.module sous une forme simplifiée ressemble à ceci:


 { "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" } }, ] } 

Grâce aux attributs org.jetbrains.kotlin Gradle comprend dans quel cas quelle dépendance tirer dans l'ensemble source souhaité.


Vous pouvez activer les métadonnées en utilisant:


 enableFeaturePreview("GRADLE_METADATA") 

Vous pouvez trouver des informations détaillées dans la documentation .


Paramètre de publication


Après avoir déterminé les cibles et la parallélisation de l'assemblage, nous devons configurer quoi exactement et comment nous publierons.


Pour la publication, nous utilisons le plugin Gradle Bintray, donc la première chose à faire est de se tourner vers son fichier README et de configurer les informations sur notre référentiel et les informations d'identification pour la publication.


Nous effectuerons la configuration complète dans notre propre plugin dans le dossier buildSrc .
L'utilisation de buildSrc offre plusieurs avantages, y compris une saisie semi-automatique toujours fonctionnelle (dans le cas des scripts Kotlin, elle ne fonctionne toujours pas toujours et nécessite souvent un appel pour appliquer les dépendances), la possibilité de réutiliser les classes déclarées et d'y accéder à partir des scripts Groovy et Kotlin. Vous pouvez voir un exemple d'utilisation de buildSrc partir des dernières E / S de Google (section Gradle).


 private fun setupBintrayPublishingInformation(target: Project) { //  Bintray Plugin   target.plugins.apply(BintrayPlugin::class) //    target.extensions.getByType(BintrayExtension::class).apply { user = target.findProperty("bintray_user")?.toString() key = target.findProperty("bintray_key")?.toString() pkg.apply { repo = "maven" name = "reaktive" userOrg = "badoo" vcsUrl = "https://github.com/badoo/Reaktive.git" setLicenses("Apache-2.0") version.name = target.property("reaktive_version")?.toString() } } } 

J'utilise trois propriétés dynamiques du projet: bintray_user et bintray_key , qui peuvent être obtenues à partir des paramètres de profil personnels sur Bintray , et reaktive_version , qui est défini dans le fichier racine build.gradle .


Pour chaque cible, le plugin Kotlin Multiplatform crée une MavenPublication , qui est disponible dans PublishingExtension .


En utilisant l'exemple de code de la documentation Kotlin que j'ai fourni ci-dessus, nous pouvons créer cette configuration:


 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 ) } 

Dans cette carte simple, nous décrivons quelles publications doivent être publiées sur une machine virtuelle particulière. Le nom de la publication est le nom de la cible. Cette configuration est parfaitement cohérente avec la description des groupes cibles que j'ai citée ci-dessus.


Configurons la publication dans Bintray. Le plugin Bintray crée une BintrayUploadTask , que nous personnaliserons selon nos besoins.


 private fun setupBintrayPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.tasks.named(BintrayUploadTask.getTASK_NAME(), BintrayUploadTask::class) { doFirst { //   } } } 

Tous ceux qui commencent à travailler avec le plugin Bintray découvrent rapidement que son référentiel est recouvert de mousse depuis longtemps (la dernière mise à jour remonte à environ six mois) et que tous les problèmes sont résolus par toutes sortes de hacks et de béquilles dans l'onglet Problèmes. La prise en charge d'une nouvelle technologie telle que Gradle Metadata n'a pas été implémentée, mais dans le problème correspondant , vous pouvez trouver une solution que nous utilisons.


 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" }) } } 

À l'aide de ce code, nous ajoutons le fichier module.json à la liste des artefacts à publier, grâce auquel Gradle Metadata fonctionne.


Mais nos problèmes ne s'arrêtent pas là. Lorsque vous essayez d'exécuter bintrayPublish rien ne se produit.


Dans le cas des bibliothèques Java et Kotlin classiques, Bintray extrait automatiquement les publications disponibles et les publie. Cependant, dans le cas de Kotlin Multiplatform, lors de l'extraction automatique de publications, le plugin se bloque simplement avec une erreur. Et oui, il y a aussi un problème sur GitHub pour cela. Et nous utiliserons à nouveau la solution à partir de là, uniquement en filtrant les publications dont nous avons besoin.


 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) 

Mais ce code ne fonctionne pas non plus!


En effet, bintrayUpload n'a pas de tâche dans les dépendances qui assemblerait le projet et créerait les fichiers nécessaires à la publication. La solution la plus évidente est de définir publishToMavenLocal comme une dépendance publishToMavenLocal , mais pas si simple.


Lors de la collecte des métadonnées, nous connectons toutes les cibles au projet, ce qui signifie que publishToMavenLocal compilera toutes les cibles, car les dépendances pour cette tâche incluent publishToMavenLocalAndroidDebug , publishToMavenLocalAndroiRelase , publishToMavenLocalJvm , etc.


Par conséquent, nous allons créer une tâche proxy distincte, en fonction de laquelle nous mettons uniquement les publishToMavenLocalX dont nous avons besoin, et nous mettrons cette tâche elle-même dans la dépendance bintrayPublish .


 private fun setupLocalPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.project.tasks.withType(AbstractPublishToMaven::class).configureEach { val configuration = publication?.name ?: run { // Android-       PublishToMaven,        val configuration = taskConfigurationMap.keys.find { name.contains(it, ignoreCase = true) } logger.warn("Found $configuration for $name") configuration } //          enabled = taskConfigurationMap[configuration] == true } } private fun createFilteredPublishToMavenLocalTask(target: Project) { //  -         publishToMavenLocal target.tasks.register(TASK_FILTERED_PUBLISH_TO_MAVEN_LOCAL) { dependsOn(project.tasks.matching { it is AbstractPublishToMaven && it.enabled }) } } 

Il ne reste plus qu'à rassembler tout le code et à appliquer le plug-in résultant à un projet dans lequel la publication est requise.


 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 

Le code complet de PublishPlugin peut être trouvé dans notre référentiel ici .


Configurer Travis CI


La partie la plus difficile est terminée. Il reste à configurer Travis CI afin qu'il parallélise l'assembly et publie des artefacts sur Bintray lorsqu'une nouvelle version est publiée.


Nous désignerons la sortie de la nouvelle version en créant un tag sur le commit.


 #    ( ) 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 

Si, pour une raison quelconque, l'assembly sur l'une des machines virtuelles a échoué, les métadonnées et autres cibles seront toujours téléchargées sur le serveur Bintray. C'est pourquoi nous n'ajoutons pas de bloc avec libération automatique de la bibliothèque sur Bintray via leur API.


Lors de la publication de la version, vous devez vous assurer que tout est en ordre et cliquer sur le bouton pour publier la nouvelle version sur le site, car tous les artefacts sont déjà téléchargés.


Conclusion


Nous avons donc mis en place une intégration et une livraison continues dans notre projet Kotlin Multiplatform.


Ayant parallélisé les tâches d'assemblage, d'exécution de tests et de publication d'artefacts, nous utilisons efficacement les ressources qui nous sont fournies gratuitement.


Et si vous utilisez Linux (comme Arkady Ivanov arkivanov , auteur de la bibliothèque Reaktive), vous n'avez plus besoin de demander à la personne utilisant macOS (moi) de publier la bibliothèque chaque fois qu'une nouvelle version est publiée.


J'espère qu'après la publication de cet article, d'autres projets commenceront à utiliser cette approche pour automatiser les activités de routine.


Merci de votre attention!

Source: https://habr.com/ru/post/fr468841/


All Articles