
Hai Nama saya Yuri Vlad, saya adalah pengembang Android di Badoo dan ikut serta dalam membuat perpustakaan Reaktive - Ekstensi Reaktif di Kotlin murni.
Dalam prosesnya, kita dihadapkan dengan fakta bahwa dalam kasus integrasi berkelanjutan Multiplatform Kotlin dan pengiriman kontinu memerlukan konfigurasi tambahan. Hal ini diperlukan untuk memiliki beberapa mesin virtual pada berbagai sistem operasi untuk merakit perpustakaan sepenuhnya. Pada artikel ini saya akan menunjukkan cara mengkonfigurasi pengiriman kontinu untuk perpustakaan Multiplatform Kotlin Anda.
Integrasi dan pengiriman berkelanjutan untuk perpustakaan open-source
Integrasi berkelanjutan dan pengiriman berkelanjutan telah lama menjadi bagian dari komunitas open-source berkat berbagai layanan. Banyak dari mereka menyediakan layanan mereka untuk proyek-proyek sumber terbuka secara gratis: Travis CI, JitPack, CircleCI, Microsoft Azure Pipelines, Tindakan GitHub yang baru diluncurkan.
Dalam proyek sumber terbuka Badoo untuk Android, kami menggunakan Travis CI untuk integrasi berkelanjutan dan JitPack untuk pengiriman berkelanjutan.
Setelah menerapkan dukungan iOS di perpustakaan multi-platform kami, saya menemukan bahwa kami tidak dapat membangun perpustakaan menggunakan JitPack, karena tidak menyediakan mesin virtual pada macOS (iOS hanya dapat dibangun di atas macOS).
Oleh karena itu, untuk publikasi perpustakaan lebih lanjut, Bintray yang lebih akrab dipilih. Ini mendukung penyempurnaan yang lebih baik dari artefak yang diterbitkan, tidak seperti JitPack, yang hanya mengambil semua hasil panggilan publishToMavenLocal
.
Untuk publikasi, disarankan untuk menggunakan Plugin Gradle Bintray, yang kemudian saya sesuaikan dengan kebutuhan kita. Dan untuk membangun proyek, saya terus menggunakan Travis CI karena beberapa alasan: pertama, saya sudah terbiasa dengannya dan menggunakannya di hampir semua proyek hewan peliharaan saya; kedua, ia menyediakan mesin virtual macOS yang dibutuhkan untuk membangun di iOS.
Jika Anda mempelajari isi dokumentasi Kotlin, Anda dapat menemukan bagian tentang penerbitan perpustakaan multi-platform.
Pengembang Kotlin Multiplatform menyadari masalah perakitan multi-platform (tidak semuanya dapat dibangun pada sistem operasi apa pun) dan menawarkan untuk membangun perpustakaan secara terpisah pada sistem operasi yang berbeda.
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" } } } } }
Seperti yang Anda lihat dari kode di atas, tergantung pada properti isLinux
diteruskan ke Gradle, kami isLinux
publikasi target tertentu. Di bawah target di masa depan saya akan berarti perakitan untuk platform tertentu. Di Windows, hanya target Windows yang akan dikumpulkan, sedangkan pada sistem operasi lain, metadata dan target lainnya akan dikumpulkan.
Solusi yang sangat indah dan ringkas yang hanya berfungsi untuk publishToMavenLocal
atau publish
dari plugin maven-publish
, yang tidak cocok untuk kami karena penggunaan Plugin Gradle Bintray .
Saya memutuskan untuk menggunakan variabel lingkungan untuk memilih target, karena kode ini sebelumnya ditulis dalam Groovy, terletak di skrip Groovy Gradle yang terpisah dan akses ke variabel lingkungan dari konteks statis.
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 } } }
Sebagai bagian dari proyek kami, saya mengidentifikasi empat kelompok target:
- ALL - semua target terhubung dan dikumpulkan, digunakan untuk pengembangan dan sebagai nilai default.
- COMMON - hanya target yang kompatibel dengan Linux yang terhubung dan dikumpulkan. Dalam kasus kami, ini adalah JavaScript, JVM, Android JVM, Linux x64, dan Linux ARM x32.
- IOS - hanya target iOS yang terhubung dan dikumpulkan, digunakan untuk perakitan di macOS.
- META - semua target terhubung, tetapi hanya modul dengan meta-informasi untuk Gradle Metadata yang dirakit.
Dengan kelompok target ini, kita dapat memparalelkan perakitan proyek menjadi tiga mesin virtual yang berbeda (COMMON - Linux, IOS - macOS, META - Linux).
Saat ini, Anda dapat membangun semuanya di macOS, tetapi solusi saya memiliki dua keunggulan. Pertama, jika kita memutuskan untuk mengimplementasikan dukungan untuk Windows, kita hanya perlu menambahkan grup target baru dan mesin virtual baru di Windows untuk membangunnya. Kedua, tidak perlu menghabiskan sumber daya mesin virtual pada macOS pada apa yang dapat Anda bangun di Linux. Waktu CPU pada mesin virtual seperti itu biasanya dua kali lebih mahal.
Untuk apa Gradle Metadata dan untuk apa?
Maven saat ini menggunakan POM (Project Object Model) untuk menyelesaikan dependensi.
<?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>
File POM berisi informasi tentang versi pustaka, pembuatnya dan dependensi yang diperlukan.
Tetapi bagaimana jika kita ingin memiliki dua versi perpustakaan untuk JDK yang berbeda? Sebagai contoh, kotlin-stdlib
memiliki dua versi: kotlin-stdlib-jdk8
dan kotlin-stdlib-jdk7
. Pengguna perlu menghubungkan versi yang diinginkan.
Saat memutakhirkan versi JDK, mudah untuk melupakan dependensi eksternal. Untuk mengatasi masalah ini, Gradle Metadata dibuat, yang memungkinkan Anda menambahkan kondisi tambahan untuk menghubungkan pustaka tertentu.
Salah satu atribut Gradle Metadata yang didukung adalah org.gradle.jvm.version
, yang menunjukkan versi JDK. Oleh karena itu, untuk kotlin-stdlib
bentuk sederhana dari file metadata mungkin terlihat seperti ini:
{ "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" } } ] }
Khususnya, dalam kasus kami, reaktive-1.0.0-rc1.module
dalam bentuk yang disederhanakan terlihat seperti ini:
{ "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" } }, ] }
Berkat atribut org.jetbrains.kotlin
Gradle memahami dalam hal ini ketergantungan untuk menarik ke set sumber yang diinginkan.
Anda dapat mengaktifkan metadata menggunakan:
enableFeaturePreview("GRADLE_METADATA")
Anda dapat menemukan informasi terperinci dalam dokumentasi .
Publikasikan Pengaturan
Setelah kami mengetahui target dan paralelisasi majelis, kami perlu mengonfigurasi apa tepatnya dan bagaimana kami akan mempublikasikan.
Untuk penerbitan, kami menggunakan Plugin Gradle Bintray, jadi hal pertama yang harus dilakukan adalah beralih ke README -nya dan mengatur informasi tentang repositori dan kredensial kami untuk publikasi.
Kami akan melakukan seluruh konfigurasi dalam plugin kami sendiri di folder buildSrc
.
Menggunakan buildSrc
memberikan beberapa keuntungan, termasuk autocomplete yang selalu berfungsi (dalam hal skrip Kotlin, masih tidak selalu berfungsi dan sering membutuhkan panggilan untuk menerapkan dependensi), kemampuan untuk menggunakan kembali kelas yang dideklarasikan di dalamnya dan mengaksesnya dari skrip Groovy dan Kotlin. Anda dapat melihat contoh menggunakan buildSrc
dari Google I / O terbaru (bagian Gradle).
private fun setupBintrayPublishingInformation(target: Project) {
Saya menggunakan tiga sifat dinamis proyek: bintray_user
dan bintray_key
, yang dapat diperoleh dari pengaturan profil pribadi di Bintray , dan reaktive_version
, yang diatur dalam file root build.gradle
.
Untuk setiap target, Plugin Multiplatform Kotlin menciptakan MavenPublication , yang tersedia di PublishingExtension .
Menggunakan kode contoh dari dokumentasi Kotlin yang saya berikan di atas, kita dapat membuat konfigurasi ini:
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 ) }
Dalam peta sederhana ini kami menjelaskan publikasi mana yang harus dirilis pada mesin virtual tertentu. Nama publikasi adalah nama target. Konfigurasi ini sepenuhnya konsisten dengan deskripsi grup target yang saya kutip di atas.
Mari mengatur penerbitan di Bintray. Plugin Bintray membuat BintrayUploadTask
, yang akan kami sesuaikan dengan kebutuhan kami.
private fun setupBintrayPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.tasks.named(BintrayUploadTask.getTASK_NAME(), BintrayUploadTask::class) { doFirst {
Setiap orang yang mulai bekerja dengan plugin Bintray dengan cepat menemukan bahwa repositori-nya telah lama ditutup dengan lumut (pembaruan terakhir adalah sekitar enam bulan yang lalu), dan bahwa semua masalah diselesaikan dengan segala macam peretasan dan penopang pada tab Masalah. Dukungan untuk teknologi baru seperti Gradle Metadata tidak diterapkan, tetapi dalam masalah terkait Anda dapat menemukan solusi yang kami gunakan.
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" }) } }
Menggunakan kode ini, kami menambahkan file module.json
ke daftar artefak untuk publikasi, karena Gradle Metadata berfungsi.
Tapi masalah kita tidak berakhir di situ. Ketika Anda mencoba menjalankan bintrayPublish
tidak ada yang terjadi.
Dalam kasus perpustakaan Jawa dan Kotlin biasa, Bintray secara otomatis menarik publikasi yang tersedia dan menerbitkannya. Namun, dalam kasus Kotlin Multiplatform, saat menarik publikasi secara otomatis, plugin hanya mengalami gangguan karena kesalahan. Dan ya, ada juga masalah pada GitHub untuk ini. Dan kami akan kembali menggunakan solusi dari sana, hanya dengan memfilter publikasi yang kami butuhkan.
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)
Tetapi kode ini juga tidak berfungsi!
Ini karena bintrayUpload
tidak memiliki tugas dalam dependensi yang akan merakit proyek dan membuat file yang diperlukan untuk publikasi. Solusi yang paling jelas adalah mengatur publishToMavenLocal
sebagai ketergantungan publishToMavenLocal
, tetapi tidak sesederhana itu.
Saat mengumpulkan metadata, kami menghubungkan semua target ke proyek, yang berarti publishToMavenLocal
akan mengkompilasi semua target, karena dependensi untuk tugas ini termasuk publishToMavenLocalAndroidDebug
, publishToMavenLocalAndroiRelase
, publishToMavenLocalJvm
, dll.
Oleh karena itu, kami akan membuat tugas proksi terpisah, tergantung pada yang kami taruh hanya publishToMavenLocalX
yang kami butuhkan, dan kami akan menempatkan tugas ini sendiri dalam ketergantungan bintrayPublish
.
private fun setupLocalPublishing( target: Project, taskConfigurationMap: Map<String, Boolean> ) { target.project.tasks.withType(AbstractPublishToMaven::class).configureEach { val configuration = publication?.name ?: run {
Tinggal mengumpulkan semua kode bersama-sama dan menerapkan plug-in yang dihasilkan untuk proyek di mana publikasi diperlukan.
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
Kode PublishPlugin
lengkap dapat ditemukan di repositori kami di sini .
Konfigurasikan Travis CI
Bagian tersulit sudah berakhir. Masih mengkonfigurasi Travis CI sehingga membuatnya paralel dengan perakitan dan menerbitkan artefak ke Bintray ketika versi baru dirilis.
Kami akan menunjuk rilis versi baru dengan membuat tag pada komit.
# ( ) 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
Jika karena alasan tertentu perakitan di salah satu mesin virtual gagal, maka metadata dan target lainnya masih akan diunggah ke server Bintray. Itu sebabnya kami tidak menambahkan blok dengan rilis otomatis perpustakaan di Bintray melalui API mereka.
Saat merilis versi, Anda harus memastikan semuanya sudah beres, dan cukup klik tombol untuk menerbitkan versi baru di situs, karena semua artefak sudah diunggah.
Kesimpulan
Jadi kami mengatur integrasi berkelanjutan dan pengiriman berkelanjutan di proyek Multiplatform Kotlin kami.
Setelah memparalelkan tugas perakitan, menjalankan pengujian, dan menerbitkan artefak, kami secara efektif menggunakan sumber daya yang disediakan untuk kami secara gratis.
Dan jika Anda menggunakan Linux (seperti Arkady Ivanov arkivanov , penulis Reaktive library), maka Anda tidak perlu lagi meminta orang tersebut menggunakan macOS (saya) untuk menerbitkan perpustakaan setiap kali versi baru dirilis.
Saya berharap setelah rilis artikel ini, lebih banyak proyek akan mulai menggunakan pendekatan ini untuk mengotomatiskan kegiatan rutin.
Terima kasih atas perhatian anda!