先日Napier
というKotlin Multiplatform Project(以後mpp)におけるログライブラリを作成したのですが、
jCenterにアップロードするまでに大変苦労しました。
先人の知恵を書き記しておくので、この記事を参考にしてみなさんもKotlin mpp libraryを作ってみて下さい。
Kotlin mppに限らずgradleのプロジェクトをbintray経由でjCenter, Mavenに上げる際にも役立つと思うので、参考になれば。
とある勉強会で発表したスライドはこちらです。
構成
まずはKotlin Multiplatformの構成を考えます。色々なライブラリやサンプルを見ていると
.
├── android
│ ├── src
│ └── build.gradle (apply plugin: 'kotlin-platform-android')
├── common
│ ├── src
│ │ └── main
│ └── build.gradle (apply plugin: 'kotlin-platform-common')
├── ios
│ ├── src
│ │ └── main
│ └── build.gradle (apply plugin: 'org.jetbrains.kotlin.platform.native')
└─── js
├── src
│ └── main
└── build.gradle (apply plugin: 'kotlin-platform-js')
各actual用のmodule毎にgradleがある、このような構成になっている場合があります。
間違ってはいないのですが、この構成はKotlin1.3より前 Kotlin1.2時代のmpp構成になっています。
もちろん現バージョンKotlin1.3.xでも動作はするのですが、以前Kotlin Slackでこの構成は非推奨になるの?という問にJakeが力強くyes と答えているのでいずれはこの構成ではなくなるでしょう。
https://kotlinlang.slack.com/archives/C3PQML5NU/p1541489100244700
Kotlin1.3時代のmpp構成はどうするかというと
.
└── library
├── android.gradle (apply plugin: 'com.android.library')
├── build.gradle (apply plugin: 'kotlin-multiplatform')
└── src
├── androidMain
├── androidTest
├── commonMain
├── commonTest
├── iosMain
├── iosTest
├── jsMain
├── jsTest
├── jvmMain
└── jvmTest
1つのmoduleを作成して、gradleは基本的に1つになり各プラットフォームの名前を付けたディレクトリが作成されるパターンになります。
実際の構成ではandorid.gradle
として別ファイルにしているがlibrary配下のbuild.gradle
で読み込んでいるだけなので、結局は1つのgradleファイルしか無いのと同じです。
Android Studioだと選択出来ないのですが、IntelliJ IDEAだとデフォルトでKotlin Multiplatform Libraryが選択出来ます。 androidは特殊なのでデフォルトでは含まれませんがmacos, jvm, jsが作られます。
gradle
Libraryのgradle
それでは早速作っていきましょう。 こちらのサンプルライブラリ を元に解説していくので、実際のライブラリ構成が気になる方は比べながら見て下さい。
Library moduleのgradleはこのようになっています。 (testの部分は一応書いておきますが一旦無視しても大丈夫です)
apply plugin: 'kotlin-multiplatform'
apply from: 'android.gradle' // [A]
apply from: rootProject.file('gradle/publish.gradle') // [B]
kotlin {
android {
publishLibraryVariants("release") // [C]
}
// [D]
iosX64('ios')
iosArm32('iosArm32')
iosArm64('iosArm64')
js()
jvm()
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
}
}
commonTest {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutine_version"
implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version"
}
}
androidMain {
dependencies {
}
}
androidTest {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
}
}
iosMain {
dependencies {
}
}
iosTest {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutine_version"
}
}
iosArm32Main.dependsOn iosMain
iosArm32Test.dependsOn iosTest
iosArm64Main.dependsOn iosMain
iosArm64Test.dependsOn iosTest
jsMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutine_version"
}
}
jsTest {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version"<Paste>
}
}
jvmMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
}
jvmTest {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
}
}
}
}
// 以下略
A. AndroidはminSDKの設定等が必要になるので、build.gradleとは分離しています。サンプルはこちら
B. こちらもAndorid同様 gradleの設定とは関係ないので、build.gradleとは分離しています。
C. ここはポイントです。 Androidの成果物に関してはここで、release buildを指定してあげる必要があります。これを指定しないとdebug buildの成果物が作られてしまうので気をつけて下さい。
D. 基本的にはKotlin mppと構成は同じです。 Kotlin1.2からの変更点としては、targetの指定が
kotlin {
targets {
fromPreset(presets.android, 'android')
fromPreset(presets.iosArm64, 'ios')
fromPreset(presets.js, 'js')
fromPreset(presets.jvm, 'jvm')
}
}
の形から
kotlin {
android()
iosX64('ios')
iosArm32('iosArm32')
iosArm64('iosArm64')
js()
jvm()
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
}
}
...
}
}
targertなしの指定に変わっています。
今後上の書き方はDeprecatedになっていくと思われますので、今からMultiplatformプロジェクトを作る場合は下の1.3~
の書き方にしましょう。
Publishのgradle
Maven Publish Plugin
まず、MavenプロジェクトとしてBuildの成果物を作るためにMaven Publish Plugin
を使います。
前章でも書きましたが、サンプルプロジェクト
では、管理しやすい用にライブラリのディレクトリにあるbuild.gradle
とは別にpublish.gradle
というファイルを用意しています。
まずは、pluginを読み込みます。
apply plugin: 'maven-publish'
自分で定義した変数pomConfigをPOMに設定します。 また、gradle内の大文字スネークケースの変数はgradle.propertiesに定義しています。 サンプルのライブラリでは、ここ に書いてあるので参考にしてみてください。
def pomConfig = {
licenses {
license {
name POM_LICENSE_NAME
url POM_LICENSE_URL
distribution POM_LICENSE_DIST
}
}
developers {
developer {
id POM_DEVELOPER_ID
name POM_DEVELOPER_NAME
organization POM_ORGANIZATION_NAME
organizationUrl POM_ORGANIZATION_URL
}
}
scm {
url SITE_URL
}
}
afterEvaluate {
project.publishing.publications.all { // [A]
pom.withXml { // [B]
def root = asNode()
root.appendNode('name', project.name)
root.appendNode('description', POM_DESCRIPTION)
root.appendNode('url', SITE_URL)
root.children().last() + pomConfig
}
// rename artifacts
groupId = BINTRAY_PACKAGE
if (it.name.contains('metadata')) { // [C]
artifactId = "${project.name}"
} else {
artifactId = "${project.name}-$name"
}
}
}
A. 生成されるproject全てに対してPOMを設定していきます。
B. pom.withXml
に値を設定するとPOMのxmlファイルには以下のような同じ構造の内容が出力されます。
<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>aakira</id>
<name>aakira</name>
<organization>aakira</organization>
<organizationUrl>https://github.com/aakira</organizationUrl>
</developer>
</developers>
<scm>
<url>https://github.com/aakira/Napier</url>
</scm>
C. ここがポイントになります。
Kotlin Multiplatform ProjectのCommon部分はmetadata
という名前で出力されます。
私は最初ここでかなりハマりました。むしろmppライブラリを作る時はこれだけ知ってれば良いかも知れません。
名前がmetadataになってしまうので、artifactIdをproject nameで上書きします。
この例の場合 project.name
はnapier
, name
はtarget名(android, ios,etc…)になります。
今回はmavenに配布するライブラリの名前を
common用は[project name]
, android用は[project name]-android
としたいので、このように書いています。
以上の設定を終えると指定したtargetの数だけpublishに関するgradle taskが作られます。
- publish
- publishAndroidReleasePublicationToMavenLocal
- publishIosArm32PublicationToMavenLocal
- publishIosArm64PublicationToMavenLocal
- publishIosPublicationToMavenLocal
- publishJsPublicationToMavenLocal
- publishJvmPublicationToMavenLocal
- publishKotlinMultiplatformPublicationToMavenLocal
- publishMetadataPublicationToMavenLocal
- publishToMavenLocal
これらのtaskを実行すると、
[Library Directory]/build/publiscations
にそれぞれの
module.json
, pom-default.xml
が作成されます。
$ ./gradlew publishToMavenLocal
build
└── publications
├── androidRelease
│ ├── module.json
│ └── pom-default.xml
├── ios
│ ├── module.json
│ └── pom-default.xml
├── iosArm32
│ ├── module.json
│ └── pom-default.xml
├── iosArm64
│ ├── module.json
│ └── pom-default.xml
├── js
│ ├── module.json
│ └── pom-default.xml
├── jvm
│ ├── module.json
│ └── pom-default.xml
├── kotlinMultiplatform
│ ├── module.json
│ └── pom-default.xml
└── metadata
├── module.json
└── pom-default.xml
Bintray Upload plugin
Mavenの成果物を作れるようになったら、次にBintrayにbuildした成果物をアップロードしていきます。
Uploadには、Bintray Upload Plugin
を使います。(もちろん手動でもアップロードすることは可能です)
rootのgradleにversionの定義をします。
buildscript {
repositories {
...
}
dependencies {
...
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4"
...
}
}
maven pluginと同じようにgradleファイルでapply pluginします。
apply plugin: 'com.jfrog.bintray'
mavenの時と同様に大文字スネークケースの変数はgradle.propertiesに定義しています。
def getBintrayUserProperty() {
return hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER')
}
def getBintrayApiKeyProperty() {
return hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY')
}
bintray {
user = getBintrayUserProperty() // [A]
key = getBintrayApiKeyProperty() // [B]
publish = false // [C]
pkg {
repo = BINTRAY_REPOSITORY
name = BINTRAY_NAME
userOrg = GROUP
licenses = ['Apache-2.0']
vcsUrl = VCS_URL
websiteUrl = SITE_URL
issueTrackerUrl = ISSUE_URL
version {
name = rootProject.ext.LIBRARY_VERSION_NAME
vcsTag = rootProject.ext.LIBRARY_VERSION_NAME
released = new Date()
}
}
}
// [D]
bintrayUpload.doFirst {
publications = publishing.publications.collect {
it.name
}.findAll {
it != "kotlinMultiplatform"
}
}
bintrayUpload.dependsOn publishToMavenLocal // [E]
[A][B]. Aにbintrayのユーザ名, BにBintrayのAPI Keyを読み込んでいます。
API Keyはgitに含みたくないので、私はローカル上の ~/.gradle/gradle.properties
に書いています。
gradleのpropertyファイルは、ホームディレクトリのgradle > プロジェクトディレクトリのgradleの順に読み込まれるので、ホームディレクトリのpropertyに書いておけば、他のプロジェクトにも使えるので覚えておくと良いでしょう。
[C]. publishをtrueにすると、bintrayにuploadした瞬間新しいバージョンが公開されます。 falseにするとbintrayのweb上でpublishのボタンを押さないと公開されません。間違えて最新版を公開してしまう事故を防げるのでfalseにしておくのがオススメです。
[D]. これはbintrayUploadのgradle taskを行う前にbintrayにuploadするpublicationsを指定しています。
1.3以降の1モジュールのmpp構成ではこのように明示的にpublicationsに成果物を指定してあげないとuploadされません。
findAllの部分は無くても良いのですが、[project name]-kotlinMultiplatform
というpomファイルだけが入ったディレクトリもアップロードされてしまうので、除外した方が良いでしょう。
[E]. bintrayUploadのtaskに前項で設定したpublishToMavenLocalを依存させてます。これにより、bintrayUplaodのtaskを実行する際にmaven用の成果物を作れます。
Bintrayの設定
前章までの設定でBintrayにUploadする準備は整いました。
次にBintrayのサイト側の設定をしていきます。
今回はMavenとしてbuildしているので、左上のrepositoryの中からMavenを選択します。 (ここはrepository名なので、実際は何を選んでもgradleの設定を変えれば上げることは可能です)
1ヶ月程前からbintrayのUIが新しくなりました。 現在は移行期なので両方のUIが使えますが、後々古いUIは消えると思うので新しいUIの方で解説します。 トップのツールバーにあるボタンから切り替え可能です。
実際に上げるライブラリの名前やgitのurlをきちんと入力しましょう。
きちんと入力していないと、後々jCenterに上げる際に申請が通りません。
bintray上でrepositoryが作成出来たらgradleコマンドを使ってbuildしたものをアップロードしてみましょう。
ちなみに、publish.gradle
で変数になっているBINTRAY_REPOSITORY
が今登録したmaven、BINTRAY_NAME
がbintray上のプロジェクト名となっています。
この2つはBintray上の値と一致している必要があります。BINTRAY_PACKAGE
はBintray上の任意のファイルのパス指定になります。
$ ./gradlew clean
$ ./gradlew bintrayUpload
必ずcleanしてからuploadしましょう。 buildディレクトリの中身をアップロードしてしまうので、関係ないファイルがあるとアップロードされてしまいます。
アップロードが終わるとbintrayのwebページに公開するの?という表示が出ます。
ここで、Publish all
を選択すると無事bintrayに公開されます。
jCenterへの登録
このままでは自分のbintray上のrepositoryにのみ公開されているので、gradleにこのようにrepositoryを指定する必要があります。
buildscript {
repositories {
...
maven { url "http://dl.bintray.com/[bintray repository]/[bintray name]" }
...
}
}
これでも良いのですが、jCenterにも紐付けてあげると使う側がわざわざrepositoryを指定せずに済みます。 新しいUIになってから場所がわかりづらくなりましたが、右上のActionsからAdd to jCenterを選択しましょう。
選択すると、詳細入力画面が出てきます。
ここの詳細はそれほど重要ではない気がしますが2,3行の説明を英語で書きましょう。 おそらく人間が見ていて1~3日ぐらいで返信が来ます。 不足しているとメッセージに何が駄目なのか理由が書かれているはずなので、対応してまた同じように申請します。 無事申請が通ると、これだけで利用可能になります。 ライブラリ利用者が物凄く楽になるので、多くの人に使ってもらうライブラリを作った際はjCenterのヒモ付は必ず行うようにしましょう。
buildscript {
repositories {
jcenter()
}
}
トラブルシューティング
変更したのに変わらない
gradleやmaven周りを触っていると、きっとlocal cacheで苦しむ時が来ます。
あれ?ここ変更したのに変わってない🤔と悩んでるうちに時間だけが溶けていきます。
そういう時は、local cacheを一度消してみるのが良いです。
mavenのcacheは ~/.m2/repository
にあります。
gradleのcacheは ~/.gradle/caches/modules-2/files-2.1/
にあります。
bintrayにupload出来ない
~/.gradle/gradle.properties
にbintrayUser
, bintrayApiKey
を定義していますか?
sampleのpublish.gradle
とgradle.properties
を見てください。
まとめ
Kotlin Multiplatform Libraryのライブラリを公開する手順を一通り書きました。 BintrayにアップロードしてjCenterに登録する一連の流れがわかったかと思います。 後半のBintrayの部分はmppプロジェクトに限らずAndroidのライブラリを公開するのにも役立つと思うので参考になればと思います。 しつこいですが、Napier にはJVM, Android, iOS, JS 全てのパターンのサンプルが作ってありますので、みなさんは先人の犠牲を無駄にせず便利なライブラリをたくさん作って下さい!