March 28, 2019

Kotlin Multiplatform LibraryをBintray経由で配布

先日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

kotlin slack jake

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が作られます。

intellij

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.namenapier, 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の設定を変えれば上げることは可能です)

bintray1

1ヶ月程前からbintrayのUIが新しくなりました。 現在は移行期なので両方のUIが使えますが、後々古いUIは消えると思うので新しいUIの方で解説します。 トップのツールバーにあるボタンから切り替え可能です。

bintray2

実際に上げるライブラリの名前や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ディレクトリの中身をアップロードしてしまうので、関係ないファイルがあるとアップロードされてしまいます。

bintray3

アップロードが終わると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を選択しましょう。

bintray4

選択すると、詳細入力画面が出てきます。

bintray5

ここの詳細はそれほど重要ではない気がしますが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.propertiesbintrayUser, bintrayApiKeyを定義していますか?
sampleのpublish.gradlegradle.propertiesを見てください。

まとめ

Kotlin Multiplatform Libraryのライブラリを公開する手順を一通り書きました。 BintrayにアップロードしてjCenterに登録する一連の流れがわかったかと思います。 後半のBintrayの部分はmppプロジェクトに限らずAndroidのライブラリを公開するのにも役立つと思うので参考になればと思います。 しつこいですが、NapierにはJVM, Android, iOS, JS 全てのパターンのサンプルが作ってありますので、みなさんは先人の犠牲を無駄にせず便利なライブラリをたくさん作って下さい!

© AAkira 2019