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)用のライブラリ全体を配布する方法については以前ブログを書いたのでそちらを参照してください。
この記事のサンプルとして使っているのは、MPP用LoggingライブラリのNapierです☆
仕組みを理解する
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 Coroutines
やKtor
も同じようにNative用アーティファクトの.moduleファイルに全ての依存関係が記述されている形になっています。
- cf. Mavenディレクトリ
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.Serialization
やKtor
も同じ様にしてこの問題を回避しています。
ちなみにSQLDelight
はBintrayを使っていないので、このプラグインは使っていません。
こうして、Bintrayに.moduleファイルをアップロードすることで、napier-ios
という記述のみでArmやX64のような異なるCPUのアーティファクトでも1つの記述で指定できるようになりました。
iosMain {
dependencies {
implementation 'com.github.aakira:napier-ios:$napierVersion'
}
}
ちなみに、kotlinMultiplatform
のアーティファクトには依存関係のみが記述されているため、ライブラリの実体は入っていません。
ライブラリ利用者側はnapier-ios
だけの記述ですが、内部で依存解決して、napier-iosArm64
かnapier-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上にまとめていますので参考にしてみて下さい☆