September 24, 2019

MPPでKotlin/Nativeのアーティファクトをまとめて配布

Kotlin/Nativeを用いたiOSの開発では、シュミレータ用のx64とiPhone実機用のArm32, 64の2パターンのCPU用アーティファクトを用意するのが一般的です。 ただ普通の方法で配布すると、ライブラリ利用者側は このように3つのアーティファクトの依存関係の定義を別々に記述する必要があるため少し不便です。

iosArm32 {
  dependencies {
    implementation 'com.github.aakira:napier-iosArm32:$napierVersion'
  }
}
iosArm64 {
  dependencies {
    implementation 'com.github.aakira:napier-iosArm64:$napierVersion'
  }
}
iosX64 {
  dependencies {
    implementation 'com.github.aakira:napier-iosX64:$napierVersion'
  }
}

この記事では、それぞれのアーティファクトをまとめてライブラリとして配布する方法を説明します。
Kotlin Multiplatform Project(MPP)用のライブラリ全体を配布する方法については以前ブログを書いたのでそちらを参照してください。

Kotlin Multiplatform LibraryをBintray経由で配布 - AABrain

Kotlin1.3以上の構成で作ったMultiplatform Project用のライブラリをBintray経由でmaven, jCenterに配布するやり方を説明します

この記事のサンプルとして使っているのは、MPP用LoggingライブラリのNapierです☆

AAkira/Napier

Logging library for Kotlin Multiplatform. Contribute to AAkira/Napier development by creating an account on GitHub.

仕組みを理解する

MPPから生成されるアーティファクトには

  • Common(共通部分のコード)
  • Android
  • JVM
  • iOS
  • Native
  • JavaScript
  • WebAssembly

などがあります。
ライブラリによってはiOSではなくNativeとして大きな括りにしたり、Android固有の実装が無ければJVMとして配布されているものもあります。

通常Mavenを使って配布されているJVMのライブラリには

  • ライブラリのjarファイル
  • sourceコードが入ったjarファイル
  • バージョン情報や依存関係が記述されているpom.xml
  • (javadoc等のドキュメント)

等のファイルが含まれています。
しかし、これらのファイルだけではKotlin/Nativeのライブラリをまとめて配布することは出来ません。

MPPのライブラリには、これにプラスして.moduleファイルというものが存在します。
.moduleファイルには、pomと同じライブラリに関する名前やバージョン情報の他に アーティファクトごとに必要なプラットフォームの情報やリンクの情報が含まれます。 MPPではこの情報を元に依存関係を解決することが出来ます。

Napier全体の情報が含まれる.moduleファイルはこのようになっています。

{
  "formatVersion": "1.0",
  "component": {
    "group": "com.github.aakira",
    "module": "napier-ios",
    "version": "1.0.0",
    "attributes": {
      "org.gradle.status": "release"
    }
  },
  "createdBy": {
    "gradle": {
      "version": "5.4.1",
      "buildId": "hja3z3ikf5a37pu4d6wqnobphy"
    }
  },
  "variants": [
    {
      "name": "android-releaseApiElements",
      "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-api",
        "org.jetbrains.kotlin.platform.type": "androidJvm"
      },
      "available-at": {
        "url": "../../napier-android/1.0.0/napier-android-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-android",
        "version": "1.0.0"
      }
    },
    {
      "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": "../../napier-android/1.0.0/napier-android-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-android",
        "version": "1.0.0"
      }
    },
    {
      "name": "iosArm32-api",
      "attributes": {
        "org.gradle.usage": "kotlin-api",
        "org.jetbrains.kotlin.native.target": "ios_arm32",
        "org.jetbrains.kotlin.platform.type": "native"
      },
      "available-at": {
        "url": "../../napier-iosArm32/1.0.0/napier-iosArm32-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-iosArm32",
        "version": "1.0.0"
      }
    },
    {
      "name": "iosArm64-api",
      "attributes": {
        "org.gradle.usage": "kotlin-api",
        "org.jetbrains.kotlin.native.target": "ios_arm64",
        "org.jetbrains.kotlin.platform.type": "native"
      },
      "available-at": {
        "url": "../../napier-iosArm64/1.0.0/napier-iosArm64-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-iosArm64",
        "version": "1.0.0"
      }
    },
    {
      "name": "iosX64-api",
      "attributes": {
        "org.gradle.usage": "kotlin-api",
        "org.jetbrains.kotlin.native.target": "ios_x64",
        "org.jetbrains.kotlin.platform.type": "native"
      },
      "available-at": {
        "url": "../../napier-iosX64/1.0.0/napier-iosX64-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-iosX64",
        "version": "1.0.0"
      }
    },
    {
      "name": "js-api",
      "attributes": {
        "org.gradle.usage": "kotlin-api",
        "org.jetbrains.kotlin.platform.type": "js"
      },
      "available-at": {
        "url": "../../napier-js/1.0.0/napier-js-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-js",
        "version": "1.0.0"
      }
    },
    {
      "name": "js-runtime",
      "attributes": {
        "org.gradle.usage": "kotlin-runtime",
        "org.jetbrains.kotlin.platform.type": "js"
      },
      "available-at": {
        "url": "../../napier-js/1.0.0/napier-js-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-js",
        "version": "1.0.0"
      }
    },
    {
      "name": "jvm-api",
      "attributes": {
        "org.gradle.usage": "java-api-jars",
        "org.jetbrains.kotlin.platform.type": "jvm"
      },
      "available-at": {
        "url": "../../napier-jvm/1.0.0/napier-jvm-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-jvm",
        "version": "1.0.0"
      }
    },
    {
      "name": "jvm-runtime",
      "attributes": {
        "org.gradle.usage": "java-runtime-jars",
        "org.jetbrains.kotlin.platform.type": "jvm"
      },
      "available-at": {
        "url": "../../napier-jvm/1.0.0/napier-jvm-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier-jvm",
        "version": "1.0.0"
      }
    },
    {
      "name": "metadata-api",
      "attributes": {
        "org.gradle.usage": "kotlin-api",
        "org.jetbrains.kotlin.platform.type": "common"
      },
      "available-at": {
        "url": "../../napier/1.0.0/napier-1.0.0.module",
        "group": "com.github.aakira",
        "module": "napier",
        "version": "1.0.0"
      }
    }
  ]
}

Kotlin/Nativeのライブラリ配布には、この.moduleファイルをうまく利用する必要があります。

Kotlin/Nativeのアーティファクトをまとめて配布する

.moduleファイルを配布する前にまずは、MPP用ライブラリのアーティファクトを作成します。
MPP用に提供されているGradleのkotlin-multiplatformプラグインでは、デフォルトでこれらのアーティファクトが生成されます

  • kotlinMultiplatform
  • metadata
  • Gradleで設定した各Targetの読み込み名

Napierの例だとkotlinMultiplatform, metadataに加えて

  • androidRelease
  • iosArm32
  • iosArm64
  • iosX64
  • js
  • jvm

が生成されています。

前の記事でも説明しましたが、Commonモジュールの成果物はmetadataとして生成されます。
Kotlin CoroutinesやKotlin Serialization等のMPP用ライブラリを使ったことがある方はご存知だと思いますが、共通モジュールのアーティファクトは一般的にmetadataという名前では配布されていません。 ライブラリのプロジェクト名か、プロジェクト名のSuffixに-commonが付いた形が多いです。 NapierではSuffixは付けずに、プロジェクト名で配布をしています。

今回の記事のポイントは、生成されるアーティファクトのkotlinMultiplatformの部分になります。
Kotlin1.3.0より前ではkotlin-multiplatformのプラグインではなく、Kotlin/Native, Kotlin/JSのプラグインがそれぞれ別れていて、別々にapplyする必要があったためモジュール(ディレクトリ)が複数あったのですが、現在は1ディレクトリでMPPのライブラリを生成しています。
そのため、生成されるkotlinMultiplatformというアーティファクトには、このMPP用ライブラリの情報が全て.moduleに記述されています。
このファイルは全ての依存関係が参照出来てしまうので本来良くは無いのですが、現状iOSのみの依存関係が記述された.moduleファイルは生成されないので、このファイルを用いてiOSのCPU別ファイルを参照させます。
現段階では、Kotlin CoroutinesKtorも同じようにNative用アーティファクトの.moduleファイルに全ての依存関係が記述されている形になっています。

CoroutinesやKtorのように、アーティファクトをRenameして配布するためにはGradleファイルで少し加工する必要があります。
NapierではPublish用にGradleファイルを分割していますが、1つのファイルにまとめて記述しても構いません。

afterEvaluate {
    publishing {
        def projectName = project.name

        publications.all {
            // rename artifacts
            groupId = BINTRAY_PACKAGE

            if (name == 'kotlinMultiplatform') { 
                artifactId = "$projectName-ios"   // A
                artifact sourcesJar
            } else if (name == 'metadata') { 
                artifactId = "$projectName"       // B
            } else {
                artifactId = "$projectName-$name" // C
            }

            // D
            if (!it.name.contains('ios') && it.name != 'kotlinMultiplatform') {
                moduleDescriptorGenerator = null
            }
        }
    }
}

A. モジュール全体の参照が入っている部分になります。
そのままではkotlinMultiplatformとしてアーティファクトが生成されてしまうので、napier-iosにRenameします。

B. metadataは共通モジュールの事を指しているため、プロジェクト名(Napier)にRenameしています。

C. その他のアーティファクトは、プロジェクト名-targetにRenameしています。

D. Gradle5.3以下では、settings.gradleに以下の記述をする必要がありました。

enableFeaturePreview('GRADLE_METADATA') 

この記述をした場合は、全てのアーティファクトのディレクトリに.moduleファイルが追加されてしまうので、moduleDescriptorGenerator=nullにすることで、不要なアーティファクトディレクトリではmoduleファイルを生成しないようにしています。

しかし、Gradle5.4以降では enableFeaturePreview('GRADLE_METADATA')の記述は不要なため、この記述を消すことも可能です。
その場合は、kotlinMultiplatformのアーティファクトディレクトリにのみ、.moduleファイルが生成されますのでD部分の記述が不要になります。

ちなみに、SQLDelightはios-driverというiOS用のModule(ディレクトリ)を別で作成して、そもそもiOS以外のアーティファクトの参照が作られないような仕組みになっています。

Bintrayにアップロード

Napierは、gradle-bintray-pluginというプラグインを使ってBintrayにライブラリをアップロードしています。
しかし、2019年9月現在の最新バージョン1.8.4ではこのプラグイン経由で.moduleファイルをbintrayにアップロードすることが出来ません。(#229)
私はこれに気付けず、解決に時間がかかってしまいました。

解決策としては、Issueの様に記述することでGradleからアップロードする事も出来ますが、JetBrainsが.moduleファイルをアップ出来るように修正したバージョン(1.8.4-jetbrains-5)を使うと.moduleファイルを自動でアップロードしてくれます。
Kotlinx.SerializationKtorも同じ様にしてこの問題を回避しています。
ちなみにSQLDelightはBintrayを使っていないので、このプラグインは使っていません。

こうして、Bintrayに.moduleファイルをアップロードすることで、napier-iosという記述のみでArmやX64のような異なるCPUのアーティファクトでも1つの記述で指定できるようになりました。

iosMain {
  dependencies {
    implementation 'com.github.aakira:napier-ios:$napierVersion'
  }
}

ちなみに、kotlinMultiplatformのアーティファクトには依存関係のみが記述されているため、ライブラリの実体は入っていません。
ライブラリ利用者側はnapier-iosだけの記述ですが、内部で依存解決して、napier-iosArm64napier-iosX64を自動で読み込んでくれています。 そのため、以前と同じように直接アーティファクトの指定をすることも可能です。
当然、参照が出来ても必要なアーティファクト自体が配布されていなければ読み込むことは出来ないため、結局配布する側は今まで通り必要な全てのアーティファクトを公開しなければなりません。

iosArm64 {
  dependencies {
    implementation 'com.github.aakira:napier-iosArm64:$napierVersion'
  }
}
iosX64 {
  dependencies {
    implementation 'com.github.aakira:napier-iosX64:$napierVersion'
  }
}

おまけ

依存関係がkotlinMultiplatformのアーティファクトに全て記述されているということは、

commonMain {
  dependencies {
    implementation 'com.github.aakira:napier:$napierVersion'
  }
}
iosMain {
  dependencies {
    implementation 'com.github.aakira:napier-ios:$napierVersion'
  }
}
jvmMain {
  dependencies {
    implementation 'com.github.aakira:napier-jvm:$napierVersion'
  }
}

の様に、それぞれのアーティファクトを指定せずに1つのアーティファクトに全て依存情報を含めて配布して、

commonMain {
  dependencies {
    implementation 'com.github.aakira:napier-ios:$napierVersion'
  }
}
iosMain {
  dependencies {
    implementation 'com.github.aakira:napier-ios:$napierVersion'
  }
}
jvmMain {
  dependencies {
    implementation 'com.github.aakira:napier-ios:$napierVersion'
  }
}

利用者側は全て同じものを参照すれば良いのでは?と考えると思います。

結論から言うと、kotlinMultiplatformで生成される依存関係が全て記述されたアーティファクトを指定すれば全てのTargetで読み込み可能です。(Napierの場合はnapier-ios)
しかし、現状JetBrains等が出しているメジャーなMPP用ライブラリは1つのアーティファクト指定(大抵はprojectName-native)で参照出来るにも関わらずそのようにはなっていません。 確かに、Target毎に指定方法が別れている方が影響範囲が少なくなる方が自然ではあるので、将来的にはアーティファクト毎に分離する予定があるのかもしれません。
MPP用ライブラリは1つの名前で配布する事が出来ますが、私は各ターゲット毎に分割していた方が良いのではと思います。(もちろん将来的には1つになる可能性もあると思います)


その他の、MPP用のライブラリもGitHub上にまとめていますので参考にしてみて下さい☆

AAkira/Kotlin-Multiplatform-Libraries

Kotlin Multiplatform Libraries. Welcome PR if you find or create new Kotlin Multiplatform Library. - AAkira/Kotlin-Multiplatform-Libraries

© AAkira 2018