はじめに
またも半年ぶりの投稿になってしまいました。
ここ半年ほど、Kotlin Multiplatform(KMP)でネイティブとJVMの両方をターゲットとしたアプリケーションを作成しています。
KMPとは文字通り、Kotlinでマルチプラットフォームなプロジェクトを簡単に扱うためのフレームワークであり、プラットフォーム間でコードを可能な限り共通化し、API呼び出しなどプラットフォーム固有の機能だけを個別に書くようにすることで、プロジェクトのメンテナンスコスト等を下げることを目的としています1。
KMP自体はとても素晴らしい物なのですが、安定版になったのが去年の11月と、かなり新しい技術であるため、公式ドキュメント等が全体的に発展途上であり、日本語資料はおろか英語ですら資料が見つからないことも多いのが現状です。
そこで本記事では、KMP向けのビルドスクリプト(build.gradle.kts
) を書くにあたってハマった点と、その解決法をいくつかメモ程度に記しておきたいと思います。
KMPを使おうとする方々が同じ問題にハマってしまい、時間を浪費することを多少なりとも防げれば幸いです。
なお、KMP向けの build.gradle.kts
の書き方自体は公式リファレンスを読んで、トリッキーなこと2をせず、注意深く書けばそれほど難しくないかと思いますので割愛します。
KMPでfat JARを作成する
一般的に、KMPプロジェクトでJVMをターゲットとする場合、Kotlin Multiplatform Gradle pluginを plugins
ブロックに追加し、kotlin
ブロックに jvm
ブロックを追加するなど、公式リファレンス通りの手順を踏めばOKです。
しかし、Gradle Shadow PluginやKtor Gradle plugin3を使用してJVMのバイナリをfat JARとして配布したい場合、単に当該のプラグインを plugins
ブロックに追加するだけではうまくいきません4。
これを解決するためには、以下のコードのように、build.gradle
または build.gradle.kts
の plugins
ブロックに application
プラグインを追加し、 kotlin
の jvm
ブロック内でwithJava()
を呼び出す必要があります。
plugins {
alias(libs.plugins.kotlin.multiplatform)
application // required by shadowJar
alias(libs.plugins.shadowJar)
}
kotlin {
jvm {
withJava() // required by shadowJar
}
}
KMPを使用してマルチプラットフォームアプリケーションを作成する以上、Javaのコードを書くことはあまり多くないと思います。
それにも関わらず、Javaコードを含めるような書き方をしなければ期待した通りのJARが得られないため、この問題に引っかかってしまった場合は、解決に時間を要してしまう方が多いかと思います。
KMPでのテストにKotestを使用する
Kotlinでよく使われるテストフレームワークとして、Kotestというものがあります。
こちらは(おそらくJVMターゲットでのテスト時に)JUnit Platform Gradle pluginに依存しているため、KMPで使用する場合であっても以下のようなコードで JVM
ターゲットでのテストタスクの実行時に useJUnitPlatform()
が呼ばれるようにしてやる必要があります。
kotlin {
jvm {
testRuns.named("test") {
executionTask.configure {
useJUnitPlatform() // required by kotest
}
}
}
}
この点については一応Kotestの公式ページに載っていますが、コード例がJVM/Gradleの欄に載っているため、KMPでは不要だと思い込んで飛ばしてしまうかもしれません。
shadow
の minimize
の例外設定
先述のfat JAR作成プラグインである shadow は、生成するJARから不要なclassファイルを削ってくれる minimize
という機能を持っています。
しかし、使用する実装を実行時に動的に探すような一部ライブラリでは、本来必要なclassファイルをshadowが勝手に削ってしまい、実行時に一見不可解なエラーが出る、というケースに行き当たることがあります。
これはKMP固有の問題ではないため、通常のKotlin(Kotlin/JVM)などでも起こりますが、比較的苦戦してしまった問題であったため、こちらに載せておきます。
私がこの問題で引っかかったのは、
- slf4j全般
- KtorがJSONのデシリアライズを行うための
ktor-serialization-kotlinx-json
であったため、build.gradle.kts
の当該ブロックに以下のような項目を追加してあります。
tasks.shadowJar {
minimize {
exclude(dependency("org.slf4j:.*:.*"))
exclude(dependency("io.ktor:ktor-serialization-kotlinx-json:.*"))
}
}