
تحية! اسمي Yuri Vlad ، أنا مطور للأندرويد على Badoo وأشارك في إنشاء مكتبة Reaktive - Reactive Extensions على Kotlin الخالصة.
في هذه العملية ، نواجه حقيقة أن التكامل المستمر والتسليم المستمر في حالة 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 ، والتي أخذت ببساطة كل نتائج مكالمة publishToMavenLocal
.
للنشر ، يوصى باستخدام البرنامج المساعد Gradle Bintray ، الذي قمت بتخصيصه فيما بعد حسب احتياجاتنا. لبناء المشروع ، واصلت استخدام Travis CI لعدة أسباب: أولاً ، كنت على دراية به بالفعل واستخدمته في جميع مشاريعي للحيوانات الأليفة تقريبًا ؛ ثانياً ، توفر الأجهزة الظاهرية لنظام التشغيل MacOS اللازمة للبناء على نظام التشغيل iOS.
إذا بحثت في أحشاء وثائق 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" } } } } }
كما ترى من الكود أعلاه ، بناءً على خاصية isLinux
تم تمريرها إلى Gradle ، يمكننا isLinux
نشر بعض الأهداف. تحت الأهداف في المستقبل ، أعني التجمع لمنصة محددة. على Windows ، سيتم جمع هدف Windows فقط ، بينما سيتم جمع البيانات التعريفية لأنظمة التشغيل الأخرى والأهداف الأخرى.
حل جميل وموجز يعمل فقط من أجل publishToMavenLocal
أو publish
من المكون الإضافي publishToMavenLocal
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 } } }
كجزء من مشروعنا ، حددت أربع مجموعات من الأهداف:
- الكل - جميع الأهداف متصلة ومجمعة ، وتستخدم للتطوير وكقيمة افتراضية.
- COMMON - يتم توصيل الأهداف المتوافقة مع Linux فقط وجمعها. في حالتنا ، هذه هي JavaScript و JVM و Android JVM و Linux x64 و Linux ARM x32.
- IOS - يتم توصيل وتجميع أهداف iOS فقط ؛ يتم استخدامه للتجميع على ماك.
- META - جميع الأهداف متصلة ، ولكن يتم تجميع الوحدة النمطية التي تحتوي على معلومات التعريف الخاصة بـ Gradle Metadata فقط.
من خلال هذه المجموعة من المجموعات المستهدفة ، يمكننا موازنة تجميع المشروع في ثلاثة أجهزة افتراضية مختلفة (COMMON - Linux ، IOS - macOS ، META - Linux).
في الوقت الحالي ، يمكنك بناء كل شيء على نظام التشغيل macOS ، لكن الحل الخاص بي له ميزتان. أولاً ، إذا قررنا تنفيذ الدعم لنظام Windows ، فسنحتاج فقط إلى إضافة مجموعة مستهدفة جديدة وجهاز ظاهري جديد على Windows لإنشاءها. ثانياً ، ليست هناك حاجة إلى إنفاق موارد الجهاز الظاهري على macOS على ما يمكنك البناء عليه على Linux. وقت وحدة المعالجة المركزية على هذه الأجهزة الافتراضية عادة ما يكون ضعف تكلفة.
ما هو Gradle Metadata وما هو؟
يستخدم 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 Metadata لحل هذه المشكلة ، مما يتيح لك إضافة شروط إضافية لتوصيل مكتبة معينة.
إحدى سمات Gradle Metadata المدعومة هي 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
يفهم Gradle في هذه الحالة التبعية التي يمكن سحبها إلى مجموعة المصادر المطلوبة.
يمكنك تمكين البيانات الوصفية باستخدام:
enableFeaturePreview("GRADLE_METADATA")
يمكنك العثور على معلومات مفصلة في الوثائق .
نشر الإعداد
بعد أن توصلنا إلى أهداف التجميع وموازنته ، نحتاج إلى تكوين ما هو بالضبط وكيف سننشر.
للنشر ، نستخدم المكوّن الإضافي Gradle Bintray ، لذا فإن أول ما يجب فعله هو الرجوع إلى README وإعداد معلومات حول مستودعنا وبيانات اعتمادنا للنشر.
سنقوم بإجراء التكوين بالكامل في البرنامج المساعد الخاص بنا في مجلد buildSrc
.
يوفر استخدام buildSrc
العديد من المزايا ، بما في ذلك الإكمال التلقائي الذي يعمل دائمًا (في حالة البرامج النصية لـ Kotlin ، لا يزال لا يعمل دائمًا ويتطلب غالبًا استدعاء لتطبيق التبعيات) ، والقدرة على إعادة استخدام الفئات المعلنة فيه والوصول إليها من البرامج النصية Groovy و Kotlin. يمكنك مشاهدة مثال على استخدام buildSrc
من أحدث إصدار من I / O من Google (قسم Gradle).
private fun setupBintrayPublishingInformation(target: Project) {
يمكنني استخدام ثلاث خصائص ديناميكية للمشروع: bintray_user
و bintray_key
، والتي يمكن الحصول عليها من إعدادات ملف التعريف الشخصي على Bintray ، و reaktive_version
، والتي تم تعيينها في ملف build.gradle
الجذر.
لكل هدف ، يقوم Kotlin Multiplatform Plugin بإنشاء 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 بسرعة أن مستودعه قد تمت تغطيته بالطحالب لفترة طويلة (كان آخر تحديث منذ حوالي ستة أشهر) ، وأن جميع المشكلات يتم حلها بواسطة جميع أنواع الاختراقات والعكازات في علامة التبويب "المشكلات". لم يتم تطبيق دعم هذه التقنية الجديدة مثل 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" }) } }
باستخدام هذا الرمز ، نضيف ملف module.json
إلى قائمة الأعمال الفنية للنشر ، بسبب عمل Gradle Metadata.
لكن مشاكلنا لا تنتهي عند هذا الحد. عند محاولة تشغيل 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
ليس لديه مهمة في التبعيات التي من شأنها تجميع المشروع وإنشاء الملفات اللازمة للنشر. الحل الأكثر وضوحا هو تعيين 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
الكامل في 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. هذا هو السبب في أننا لا نضيف كتلة مع الإصدار التلقائي للمكتبة على Bintray من خلال API الخاصة بهم.
عند تحرير الإصدار ، يجب عليك التأكد من أن كل شيء في حالة جيدة ، وانقر فقط على الزر لنشر الإصدار الجديد على الموقع ، نظرًا لأن جميع القطع الأثرية قد تم تحميلها بالفعل.
استنتاج
لذلك أنشأنا التكامل المستمر والتسليم المستمر في مشروعنا Kotlin Multiplatform.
بعد موازاة مهام التجميع وإجراء الاختبارات ونشر الأعمال الفنية ، نستخدم بفعالية الموارد المتاحة لنا على أساس حر.
وإذا كنت تستخدم Linux (مثل Arkady Ivanov arkivanov ، مؤلف مكتبة Reaktive) ، فلن تحتاج بعد ذلك إلى مطالبة الشخص الذي يستخدم macOS (أنا) بنشر المكتبة في كل مرة يتم فيها إصدار إصدار جديد.
آمل أنه بعد إصدار هذا المقال ، سوف تبدأ المزيد من المشروعات في استخدام هذا النهج لأتمتة الأنشطة الروتينية.
شكرا لاهتمامكم!