
你好 我叫Yuri Vlad,我是Badoo的一名Android开发人员,参与了创建Reaktive库-纯Kotlin上的Reactive Extensions。
在此过程中,我们面临这样一个事实,就Kotlin Multiplatform而言,持续集成和持续交付需要额外的配置。 为了完全组装该库,必须在各种操作系统上具有多个虚拟机。 在本文中,我将展示如何为Kotlin Multiplatform库配置连续交付。
开源库的持续集成和持续交付
长期以来,由于各种服务,持续集成和持续交付一直是开源社区的一部分。 他们中的许多人免费为开源项目提供服务:Travis CI,JitPack,CircleCI,Microsoft Azure Pipelines,最近启动的GitHub Actions。
在Badoo的Android开源项目中,我们使用Travis CI进行持续集成,并使用JitPack进行持续交付。
在我们的多平台库中实现iOS支持后,我发现我们无法使用JitPack构建该库,因为它没有在macOS上提供虚拟机(iOS只能在macOS上构建)。
因此,为了进一步出版该图书馆,选择了更熟悉的Bintray 。 与JitPack不同,它支持对发布的工件进行更好的调整,而JitPack只是获取publishToMavenLocal
调用的所有结果。
为了出版,建议使用Gradle Bintray插件,我后来根据我们的需要对其进行了自定义。 为了构建该项目,出于以下几个原因,我继续使用Travis CI:首先,我已经熟悉它并在几乎所有宠物项目中使用了它。 其次,它提供了在iOS上构建所需的macOS虚拟机。
如果您深入研究Kotlin文档的内容,则可以找到有关发布多平台库的部分。
Kotlin Multiplatform的开发人员意识到多平台组装的问题(并非所有内容都可以在任何操作系统上构建),并愿意在不同的操作系统上分别构建库。
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" } } } } }
从上面的代码中可以看到,根据传递给Gradle的isLinux
属性,我们isLinux
某些目标isLinux
发布。 在未来的目标下,我将指特定平台的组装。 在Windows上,仅收集Windows目标,而在其他操作系统上,将收集元数据和其他目标。
一个非常漂亮简洁的解决方案,仅适用于publishToMavenLocal
或从maven-publish
插件maven-publish
,由于使用了Gradle Bintray插件,因此不适合我们。
我决定使用环境变量来选择目标,因为该代码以前是用Groovy编写的,位于单独的Groovy Gradle脚本中,并且可以从静态上下文访问环境变量。
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 } } }
作为我们项目的一部分,我确定了四组目标:
- ALL-连接并收集所有目标,用于开发并用作默认值。
- COMMON-仅连接和收集兼容Linux的目标。 在我们的例子中,这些是JavaScript,JVM,Android JVM,Linux x64和Linux ARM x32。
- IOS-仅连接和收集iOS目标;用于在macOS上进行组装。
- META-所有目标均已连接,但仅组装了具有Gradle元数据的元信息的模块。
通过这组目标组,我们可以将项目的组装并行化为三个不同的虚拟机(COMMON-Linux,IOS-macOS,META-Linux)。
目前,您可以在macOS上构建所有内容,但是我的解决方案有两个优点。 首先,如果我们决定实现对Windows的支持,我们只需要在Windows上添加一个新的目标组和一个新的虚拟机来构建它即可。 其次,无需在macOS上花费虚拟机资源即可在Linux上构建。 这种虚拟机上的CPU时间通常是昂贵的两倍。
什么是Gradle元数据,其用途是什么?
Maven当前使用POM(项目对象模型)来解决依赖关系。
<?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>
POM文件包含有关库版本,其创建者和必要依赖项的信息。
但是,如果我们想为不同的JDK提供两个版本的库怎么办? 例如, kotlin-stdlib
有两个版本: kotlin-stdlib-jdk8
和kotlin-stdlib-jdk7
。 用户需要连接所需的版本。
升级JDK版本时,很容易忘记外部依赖项。 为了解决此问题,创建了Gradle元数据,使您可以添加其他条件来连接特定的库。
支持的Gradle元数据属性之一是org.gradle.jvm.version
,它指示JDK的版本。 因此,对于kotlin-stdlib
元数据文件的简化形式可能如下所示:
{ "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" } } ] }
具体来说,在我们的情况下,简化形式的reaktive-1.0.0-rc1.module
看起来像这样:
{ "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" } }, ] }
org.jetbrains.kotlin
属性org.jetbrains.kotlin
Gradle可以了解在哪种情况下可以将哪个依赖项放入所需的源集中。
您可以使用以下方式启用元数据:
enableFeaturePreview("GRADLE_METADATA")
您可以在文档中找到详细信息。
发布设置
在确定了目标和程序集的并行化之后,我们需要配置确切的内容以及发布的方式。
对于发布,我们使用Gradle Bintray插件,因此首先要做的是转向其README,并设置有关我们的存储库和凭据的信息以供发布。
我们将在buildSrc
文件夹中的自己的插件中执行整个配置。
使用buildSrc
多个优点,包括始终有效的自动完成功能(对于Kotlin脚本,它仍然不能始终有效,并且经常需要调用以应用依赖项),能够重用其中声明的类并从Groovy和Kotlin脚本访问它们的能力。 您可以从最新的Google I / O中看到使用 buildSrc
的示例 (Gradle部分)。
private fun setupBintrayPublishingInformation(target: Project) {
我使用了该项目的三个动态属性: bintray_user
和bintray_key
(可以从Bintray上的个人配置文件设置中获得 )以及reaktive_version
(在根build.gradle
文件中设置)。
对于每个目标,Kotlin Multiplatform插件都会创建一个MavenPublication ,可在PublishingExtension中使用它 。
使用我上面提供的Kotlin文档中的示例代码,我们可以创建此配置:
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 ) }
在此简单图中,我们描述了应在特定虚拟机上发布哪些出版物。 发布名称是目标的名称。 此配置与我上面引用的目标组的描述完全一致。
让我们在Bintray中设置发布。 Bintray插件创建一个BintrayUploadTask
,我们将根据需要对其进行自定义。
private fun setupBintrayPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.tasks.named(BintrayUploadTask.getTASK_NAME(), BintrayUploadTask::class) { doFirst {
每个开始使用Bintray插件的人都会很快发现自己的存储库已经被苔藓覆盖了很长时间了(最近一次更新大约是六个月前),并且所有问题都可以通过“问题”标签中的各种hack和拐杖解决。 没有实现对Gradle Metadata这样的新技术的支持,但是在相应的问题中,您可以找到我们使用的解决方案。
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" }) } }
使用此代码,由于Gradle Metadata可以工作,我们将module.json
文件添加到要发布的工件列表中。
但是,我们的问题并不止于此。 当您尝试运行bintrayPublish
没有任何反应。
对于常规Java和Kotlin库,Bintray会自动提取可用的出版物并进行发布。 但是,对于Kotlin Multiplatform,当自动提取出版物时,该插件会因错误而崩溃。 是的, 在GitHub上还有一个问题 。 而且,我们将仅从筛选所需出版物中再次使用该解决方案。
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)
但是此代码也不起作用!
这是因为bintrayUpload
在依赖bintrayUpload
中没有任务,该任务将组装项目并创建发布所需的文件。 最明显的解决方案是将publishToMavenLocal
设置为publishToMavenLocal
依赖项,但并不是那么简单。
收集元数据时,我们将所有目标连接到项目,这意味着publishToMavenLocal
将编译所有目标,因为此任务的依赖项包括publishToMavenLocalAndroidDebug
, publishToMavenLocalAndroiRelase
, publishToMavenLocalJvm
等。
因此,我们将创建一个单独的代理任务,具体取决于我们仅将所需的那些publishToMavenLocalX
放在其中,并将此任务本身放入bintrayPublish
依赖项中。
private fun setupLocalPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.project.tasks.withType(AbstractPublishToMaven::class).configureEach { val configuration = publication?.name ?: run {
仅剩下将所有代码收集在一起并将所得的插件应用到需要发布的项目中。
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
完整的PublishPlugin
代码可以在我们的存储库中找到 。
配置Travis CI
最困难的部分结束了。 仍然需要配置Travis CI,以便在发布新版本时将程序集并行化并将工件发布到Bintray。
我们将通过在提交上创建标签来指定新版本的发布。
# ( ) 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
如果出于某种原因,其中一个虚拟机上的程序集失败,则元数据和其他目标仍将上载到Bintray服务器。 这就是为什么我们不添加通过其API在Bintray上自动释放库的块的原因。
发布版本时,您需要确保一切都井井有条,并且只需单击站点上新版本的发布按钮,因为所有工件都已上传。
结论
因此,我们在Kotlin Multiplatform项目中设置了持续集成和持续交付。
并行完成了组装,运行测试和发布工件的任务后,我们有效地免费使用了提供给我们的资源。
而且,如果您使用Linux(例如Reaktive库的作者Arkady Ivanov arkivanov ),则您不再需要让使用macOS(me)的人每次发布新版本时都发布该库。
我希望在本文发布之后,更多的项目将开始使用这种方法来自动化日常活动。
感谢您的关注!