Entrega contínua para sua biblioteca Multiplataforma Kotlin

Logomarca


Oi Meu nome é Yuri Vlad, sou desenvolvedor Android do Badoo e participo da criação da biblioteca Reaktive - Extensões Reativas em Kotlin puro.


No processo, somos confrontados com o fato de que, no caso da Kotlin Multiplatform, a integração contínua e a entrega contínua exigem configuração adicional. É necessário ter várias máquinas virtuais em vários sistemas operacionais para montar a biblioteca completamente. Neste artigo, mostrarei como configurar a entrega contínua para sua biblioteca Kotlin Multiplatform.


Integração contínua e entrega contínua para bibliotecas de código aberto


A integração contínua e a entrega contínua fazem parte da comunidade de código aberto, graças a vários serviços. Muitos deles fornecem seus serviços para projetos de código aberto gratuitamente: Travis CI, JitPack, CircleCI, Pipelines do Microsoft Azure, as Ações GitHub lançadas recentemente.


Nos projetos de código aberto do Badoo para Android, usamos o Travis CI para integração contínua e o JitPack para entrega contínua.


Após implementar o suporte ao iOS em nossa biblioteca de várias plataformas, descobri que não podemos construir a biblioteca usando o JitPack, porque ele não fornece máquinas virtuais no macOS (o iOS só pode ser construído no macOS).


Portanto, para posterior publicação da biblioteca, a Bintray , mais familiar a todos, foi escolhida . Ele suporta o ajuste mais fino dos artefatos publicados, diferentemente do JitPack, que simplesmente obteve todos os resultados da chamada publishToMavenLocal .


Para publicação, recomenda-se o uso do plug-in Gradle Bintray, que mais tarde eu personalizei para nossas necessidades. E para construir o projeto, continuei usando o Travis CI por várias razões: primeiro, eu já estava familiarizado com ele e o usei em quase todos os meus projetos de animais de estimação; segundo, fornece as máquinas virtuais do macOS necessárias para criar no iOS.


Montagem paralela de uma biblioteca multiplataforma


Se você se aprofundar nas entranhas da documentação do Kotlin, poderá encontrar uma seção sobre a publicação de bibliotecas multiplataforma.


Os desenvolvedores da Kotlin Multiplatform estão cientes dos problemas da montagem em várias plataformas (nem tudo pode ser montado em qualquer sistema operacional) e oferecem a montagem da biblioteca separadamente em diferentes sistemas operacionais.


 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 você pode ver no código acima, dependendo da propriedade isLinux passada para Gradle, permitimos isLinux publicação de determinados destinos. Sob as metas no futuro, vou me referir à montagem de uma plataforma específica. No Windows, apenas o destino do Windows será coletado, enquanto em outros sistemas operacionais os metadados e outros destinos serão coletados.


Uma solução muito bonita e concisa que funciona apenas para publishToMavenLocal ou publish no plug maven-publish in maven-publish , o que não é adequado para nós devido ao uso do plug-in Gradle Bintray .


Decidi usar a variável de ambiente para selecionar o destino, já que esse código foi escrito anteriormente no Groovy, em um script separado do Groovy Gradle e o acesso às variáveis ​​de ambiente é de um 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 do nosso projeto, identifiquei quatro grupos de metas:


  1. TUDO - todos os destinos são conectados e coletados, usados ​​para desenvolvimento e como valor padrão.
  2. COMUM - apenas destinos compatíveis com Linux são conectados e coletados. No nosso caso, são JavaScript, JVM, Android JVM, Linux x64 e Linux ARM x32.
  3. IOS - apenas destinos iOS são conectados e coletados; é usado para montagem no macOS.
  4. META - todos os destinos estão conectados, mas apenas o módulo com meta-informação para Metadados Gradle é montado.

Com esse conjunto de grupos-alvo, podemos paralelizar a montagem do projeto em três máquinas virtuais diferentes (COMMON - Linux, IOS - macOS, META - Linux).


No momento, você pode criar tudo no macOS, mas minha solução tem duas vantagens. Primeiro, se decidirmos implementar o suporte para o Windows, basta adicionar um novo grupo de destino e uma nova máquina virtual no Windows para construí-lo. Em segundo lugar, não há necessidade de gastar recursos da máquina virtual no macOS no que você pode criar no Linux. O tempo de CPU nessas máquinas virtuais geralmente é duas vezes mais caro.


Metadados Gradle


O que são Metadados Gradle e para que servem?


Atualmente, o Maven usa o POM (Modelo de Objeto de Projeto) para resolver dependências.


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

O arquivo POM contém informações sobre a versão da biblioteca, seu criador e as dependências necessárias.


Mas e se quisermos ter duas versões da biblioteca para JDKs diferentes? Por exemplo, o kotlin-stdlib possui duas versões: kotlin-stdlib-jdk8 e kotlin-stdlib-jdk7 . Os usuários precisam conectar a versão desejada.


Ao atualizar a versão do JDK, é fácil esquecer dependências externas. Foi para resolver esse problema que os Metadados Gradle foram criados, o que permite adicionar condições adicionais para conectar uma biblioteca específica.


Um dos atributos de Metadados Gradle suportados é org.gradle.jvm.version , que indica a versão do JDK. Portanto, para o kotlin-stdlib uma forma simplificada de um arquivo de metadados pode ser assim:


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

Especificamente, no nosso caso, o reaktive-1.0.0-rc1.module de uma forma simplificada se parece com isso:


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

Graças aos atributos org.jetbrains.kotlin Gradle entende nesse caso qual dependência extrair no conjunto de fontes desejado.


Você pode ativar os metadados usando:


 enableFeaturePreview("GRADLE_METADATA") 

Você pode encontrar informações detalhadas na documentação .


Configuração de publicação


Depois de descobrirmos os alvos e a paralelização da montagem, precisamos configurar o que exatamente e como publicaremos.


Para publicação, usamos o Gradle Bintray Plugin, portanto, a primeira coisa a fazer é acessar o README e configurar informações sobre o repositório e as credenciais para publicação.


Executaremos toda a configuração em nosso próprio plug-in na pasta buildSrc .
O uso do buildSrc oferece várias vantagens, incluindo um preenchimento automático sempre funcionando (no caso de scripts Kotlin, ele ainda nem sempre funciona e geralmente requer uma chamada para aplicar dependências), a capacidade de reutilizar as classes declaradas nele e acessá-las nos scripts Groovy e Kotlin. Você pode ver um exemplo do uso do buildSrc na última E / S do Google (seção 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() } } } 

Uso três propriedades dinâmicas do projeto: bintray_user e bintray_key , que podem ser obtidas nas configurações de perfil pessoal no Bintray , e reaktive_version , que é definido no arquivo build.gradle raiz.


Para cada destino, o plug-in Kotlin Multiplatform cria um MavenPublication , disponível em PublishingExtension .


Usando o código de exemplo da documentação do Kotlin que forneci acima, podemos criar esta configuração:


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

Neste mapa simples, descrevemos quais publicações devem ser liberadas em uma máquina virtual específica. O nome da publicação é o nome do destino. Essa configuração é totalmente consistente com a descrição dos grupos-alvo que citei acima.


Vamos configurar a publicação no Bintray. O plug-in Bintray cria um BintrayUploadTask , que personalizaremos de acordo com nossas necessidades.


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

Todo mundo que começa a trabalhar com o plug-in Bintray descobre rapidamente que seu repositório está coberto de musgo por um longo tempo (a última atualização ocorreu há cerca de seis meses) e que todos os problemas são resolvidos por todos os tipos de hacks e muletas na guia Issues. O suporte a uma tecnologia tão nova como os Metadados Gradle não foi implementado, mas no problema correspondente você pode encontrar uma solução 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" }) } } 

Usando esse código, adicionamos o arquivo module.json à lista de artefatos para publicação, devido aos quais os Metadados Gradle funcionam.


Mas nossos problemas não param por aí. Quando você tenta executar o bintrayPublish nada acontece.


No caso de bibliotecas Java e Kotlin regulares, o Bintray automaticamente pega as publicações disponíveis e as publica. No entanto, no caso da Kotlin Multiplatform, ao puxar automaticamente as publicações, o plug-in simplesmente trava com um erro. E sim, também há um problema no GitHub para isso. E usaremos novamente a solução a partir daí, apenas filtrando as publicações de que precisamos.


 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) 

Mas esse código também não funciona!


Isso ocorre porque o bintrayUpload não possui uma tarefa nas dependências que bintrayUpload o projeto e criariam os arquivos necessários para publicação. A solução mais óbvia é definir publishToMavenLocal como uma dependência publishToMavenLocal , mas não tão simples.


Ao coletar metadados, conectamos todos os destinos ao projeto, o que significa que publishToMavenLocal compilará todos os destinos, já que as dependências para esta tarefa incluem publishToMavenLocalAndroidDebug , publishToMavenLocalAndroiRelase , publishToMavenLocalJvm , etc.


Portanto, criaremos uma tarefa de proxy separada, dependendo da qual colocaremos apenas os publishToMavenLocalX necessários, e colocaremos essa tarefa na dependência 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 }) } } 

Resta apenas coletar todo o código e aplicar o plug-in resultante a um projeto no qual a publicação é necessária.


 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 

O código completo do PublishPlugin pode ser encontrado em nosso repositório aqui .


Configurar o Travis CI


A parte mais difícil acabou. Resta configurar o Travis CI para que ele paralelize a montagem e publique artefatos no Bintray quando uma nova versão for lançada.


Designaremos o lançamento da nova versão criando uma tag no 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 

Se, por algum motivo, a montagem em uma das máquinas virtuais falhar, os metadados e outros destinos ainda serão carregados no servidor Bintray. É por isso que não adicionamos um bloco com liberação automática da biblioteca no Bintray por meio de sua API.


Ao liberar a versão, você precisa garantir que tudo esteja em ordem e basta clicar no botão publicar para a nova versão no site, pois todos os artefatos já foram carregados.


Conclusão


Assim, configuramos integração e entrega contínuas em nosso projeto Kotlin Multiplatform.


Tendo paralelizado as tarefas de montagem, execução de testes e publicação de artefatos, usamos efetivamente os recursos fornecidos a nós gratuitamente.


E se você usa Linux (como Arkady Ivanov arkivanov , autor da biblioteca Reaktive), não precisa mais pedir à pessoa que usa o macOS (eu) para publicar a biblioteca cada vez que uma nova versão é lançada.


Espero que, após o lançamento deste artigo, mais projetos comecem a usar essa abordagem para automatizar atividades de rotina.


Obrigado pela atenção!

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


All Articles