
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.
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:
- TUDO - todos os destinos são conectados e coletados, usados para desenvolvimento e como valor padrão.
- 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.
- IOS - apenas destinos iOS são conectados e coletados; é usado para montagem no macOS.
- 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.
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) {
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 {
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!