Entrega continua para su biblioteca multiplataforma Kotlin

Logotipo


Hola Mi nombre es Yuri Vlad, soy desarrollador de Android en Badoo y participo en la creación de la biblioteca Reaktive - Extensiones reactivas en Kotlin puro.


En el proceso, nos enfrentamos al hecho de que, en el caso de Kotlin Multiplatform, la integración continua y la entrega continua requieren una configuración adicional. Es necesario tener varias máquinas virtuales en varios sistemas operativos para ensamblar la biblioteca por completo. En este artículo mostraré cómo configurar la entrega continua para su biblioteca multiplataforma Kotlin.


Integración continua y entrega continua para bibliotecas de código abierto.


La integración continua y la entrega continua han sido durante mucho tiempo parte de la comunidad de código abierto gracias a varios servicios. Muchos de ellos proporcionan sus servicios a proyectos de código abierto de forma gratuita: Travis CI, JitPack, CircleCI, Microsoft Azure Pipelines, las acciones de GitHub recientemente lanzadas.


En los proyectos de código abierto de Badoo para Android, utilizamos Travis CI para la integración continua y JitPack para la entrega continua.


Después de implementar el soporte de iOS en nuestra biblioteca multiplataforma, descubrí que no podemos construir la biblioteca usando JitPack, porque no proporciona máquinas virtuales en macOS (iOS solo se puede construir en macOS).


Por lo tanto, para una mayor publicación de la biblioteca, se eligió Bintray , más familiar para todos. Admite un ajuste más preciso de los artefactos publicados, a diferencia de JitPack, que simplemente tomó todos los resultados de la llamada publishToMavenLocal a publishToMavenLocal .


Para su publicación, se recomienda utilizar el complemento Gradle Bintray, que luego personalicé según nuestras necesidades. Y para construir el proyecto, continué usando Travis CI por varias razones: en primer lugar, ya estaba familiarizado con él y lo usé en casi todos mis proyectos favoritos; en segundo lugar, proporciona las máquinas virtuales macOS necesarias para construir en iOS.


Montaje en paralelo de una biblioteca multiplataforma


Si profundiza en las entrañas de la documentación de Kotlin, puede encontrar una sección sobre la publicación de bibliotecas multiplataforma.


Los desarrolladores de Kotlin Multiplatform son conscientes de los problemas del ensamblaje multiplataforma (no todo se puede construir en cualquier sistema operativo) y ofrecen construir la biblioteca por separado en diferentes sistemas operativos.


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

Como puede ver en el código anterior, dependiendo de la propiedad isLinux pasada a Gradle, permitimos isLinux publicación de ciertos objetivos. Bajo los objetivos en el futuro, me referiré al ensamblaje para una plataforma específica. En Windows, solo se recopilará el objetivo de Windows, mientras que en otros sistemas operativos se recopilarán metadatos y otros objetivos.


Una solución muy hermosa y concisa que funciona solo para publishToMavenLocal o publish desde el complemento de maven-publish , que no es adecuado para nosotros debido al uso del complemento Gradle Bintray .


Decidí usar la variable de entorno para seleccionar el objetivo, ya que este código se escribió anteriormente en Groovy, se encontraba en un script de Groovy Gradle separado y el acceso a las variables de entorno es desde un contexto estático.


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

Como parte de nuestro proyecto, identifiqué cuatro grupos de objetivos:


  1. TODOS: todos los objetivos están conectados y recopilados, se utilizan para el desarrollo y como valor predeterminado.
  2. COMÚN: solo los objetivos compatibles con Linux están conectados y recopilados. En nuestro caso, estos son JavaScript, JVM, Android JVM, Linux x64 y Linux ARM x32.
  3. IOS: solo los objetivos iOS están conectados y recopilados; se usa para ensamblar en macOS.
  4. META: todos los objetivos están conectados, pero solo se ensambla el módulo con metainformación para los metadatos de Gradle.

Con este conjunto de grupos objetivo, podemos paralelizar el ensamblaje del proyecto en tres máquinas virtuales diferentes (COMÚN - Linux, IOS - macOS, META - Linux).


Por el momento, puedes construir todo en macOS, pero mi solución tiene dos ventajas. Primero, si decidimos implementar el soporte para Windows, solo necesitamos agregar un nuevo grupo objetivo y una nueva máquina virtual en Windows para construirlo. En segundo lugar, no es necesario gastar recursos de máquinas virtuales en macOS en lo que puede construir en Linux. El tiempo de CPU en tales máquinas virtuales suele ser el doble de caro.


Metadatos de Gradle


¿Qué es Gradle Metadata y para qué sirve?


Maven actualmente usa POM (Modelo de objetos del proyecto) para resolver dependencias.


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

El archivo POM contiene información sobre la versión de la biblioteca, su creador y las dependencias necesarias.


Pero, ¿qué pasa si queremos tener dos versiones de la biblioteca para diferentes JDK? Por ejemplo, kotlin-stdlib tiene dos versiones: kotlin-stdlib-jdk8 y kotlin-stdlib-jdk7 . Los usuarios necesitan conectar la versión deseada.


Al actualizar la versión JDK, es fácil olvidarse de las dependencias externas. Fue para resolver este problema que se creó Gradle Metadata, que le permite agregar condiciones adicionales para conectar una biblioteca en particular.


Uno de los atributos de metadatos Gradle compatibles es org.gradle.jvm.version , que indica la versión del JDK. Por lo tanto, para kotlin-stdlib una forma simplificada de un archivo de metadatos podría verse así:


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

Específicamente, en nuestro caso, reaktive-1.0.0-rc1.module en una forma simplificada se ve así:


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

Gracias a los atributos org.jetbrains.kotlin Gradle comprende en qué caso qué dependencia extraer en el conjunto de origen deseado.


Puede habilitar metadatos usando:


 enableFeaturePreview("GRADLE_METADATA") 

Puede encontrar información detallada en la documentación .


Configuración de publicación


Después de descubrir los objetivos y la paralelización del ensamblaje, necesitamos configurar qué exactamente y cómo publicaremos.


Para la publicación, utilizamos el complemento Gradle Bintray, por lo que lo primero que debe hacer es recurrir a su archivo README y configurar la información sobre nuestro repositorio y las credenciales para su publicación.


Realizaremos la configuración completa en nuestro propio complemento en la carpeta buildSrc .
El uso de buildSrc proporciona varias ventajas, incluido un autocompletado que siempre funciona (en el caso de los scripts de Kotlin, todavía no siempre funciona y a menudo requiere una llamada para aplicar dependencias), la capacidad de reutilizar las clases declaradas en él y acceder a ellos desde los scripts de Groovy y Kotlin. Puede ver un ejemplo del uso de buildSrc de la última E / S de Google (sección 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() } } } 

Utilizo tres propiedades dinámicas del proyecto: bintray_user y bintray_key , que se pueden obtener de la configuración de perfil personal en Bintray , y reaktive_version , que se establece en el archivo raíz build.gradle .


Para cada objetivo, el complemento multiplataforma Kotlin crea una publicación Maven , que está disponible en PublishingExtension .


Usando el código de ejemplo de la documentación de Kotlin que proporcioné anteriormente, podemos crear esta configuración:


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

En este mapa simple describimos qué publicaciones deberían publicarse en una máquina virtual en particular. El nombre de la publicación es el nombre del objetivo. Esta configuración es totalmente coherente con la descripción de los grupos objetivo que cité anteriormente.


Configuremos la publicación en Bintray. El complemento Bintray crea un BintrayUploadTask , que personalizaremos según nuestras necesidades.


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

Todos los que comienzan a trabajar con el complemento Bintray descubren rápidamente que su repositorio ha estado cubierto de musgo durante mucho tiempo (la última actualización fue hace unos seis meses), y que todos los problemas se resuelven con todo tipo de trucos y muletas en la pestaña Problemas. No se implementó el soporte para una tecnología tan nueva como Gradle Metadata, pero en el problema correspondiente puede encontrar una solución que usamos.


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

Con este código, agregamos el archivo module.json a la lista de artefactos para publicación, debido a que Gradle Metadata funciona.


Pero nuestros problemas no terminan ahí. Cuando intenta ejecutar bintrayPublish no sucede nada.


En el caso de las bibliotecas regulares de Java y Kotlin, Bintray extrae automáticamente las publicaciones disponibles y las publica. Sin embargo, en el caso de Kotlin Multiplatform, cuando se abren automáticamente las publicaciones, el complemento simplemente se bloquea con un error. Y sí, también hay un problema en GitHub para esto. Y de nuevo usaremos la solución desde allí, solo filtrando las publicaciones que necesitamos.


 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) 

¡Pero este código tampoco funciona!


Esto se debe a que bintrayUpload no tiene una tarea en las dependencias que ensamblaría el proyecto y crearía los archivos necesarios para su publicación. La solución más obvia es establecer publishToMavenLocal como una dependencia publishToMavenLocal , pero no tan simple.


Al recopilar metadatos, conectamos todos los objetivos al proyecto, lo que significa que publishToMavenLocal compilará todos los objetivos, ya que las dependencias para esta tarea incluyen publishToMavenLocalAndroidDebug , publishToMavenLocalAndroiRelase , publishToMavenLocalJvm , etc.


Por lo tanto, crearemos una tarea de proxy separada, dependiendo de la cual colocamos solo aquellos publishToMavenLocalX que necesitamos, y bintrayPublish esta tarea en la dependencia 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 }) } } 

Solo queda recopilar todo el código y aplicar el complemento resultante a un proyecto en el que se requiere publicación.


 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 

El código completo de PublishPlugin se puede encontrar en nuestro repositorio aquí .


Configurar Travis CI


La parte más difícil ha terminado. Queda por configurar Travis CI para que paralelice el ensamblaje y publique artefactos en Bintray cuando se lance una nueva versión.


Designaremos el lanzamiento de la nueva versión creando una etiqueta en el 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 por alguna razón el ensamblaje en una de las máquinas virtuales falla, entonces los metadatos y otros objetivos aún se cargarán en el servidor Bintray. Es por eso que no agregamos un bloque con el lanzamiento automático de la biblioteca en Bintray a través de su API.


Al lanzar la versión, debe asegurarse de que todo esté en orden, y simplemente haga clic en el botón de publicación para la nueva versión en el sitio, ya que todos los artefactos ya se han cargado.


Conclusión


Por lo tanto, configuramos la integración continua y la entrega continua en nuestro proyecto Kotlin Multiplatform.


Después de haber paralelizado las tareas de ensamblaje, ejecución de pruebas y publicación de artefactos, utilizamos efectivamente los recursos que se nos proporcionan de forma gratuita.


Y si usa Linux (como Arkady Ivanov arkivanov , autor de la biblioteca Reaktive), ya no necesita pedirle a la persona que usa macOS (yo) que publique la biblioteca cada vez que se lanza una nueva versión.


Espero que después del lanzamiento de este artículo, más proyectos comiencen a utilizar este enfoque para automatizar las actividades de rutina.


Gracias por su atencion!

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


All Articles