Kotlin Multiplatformのbuild.gradle.ktsを書く上でハマったこと

## はじめに
またも半年ぶりの投稿になってしまいました。

ここ半年ほど、Kotlin Multiplatform（KMP）でネイティブとJVMの両方をターゲットとしたアプリケーションを作成しています。

KMPとは文字通り、Kotlinでマルチプラットフォームなプロジェクトを簡単に扱うためのフレームワークであり、プラットフォーム間でコードを可能な限り共通化し、API呼び出しなどプラットフォーム固有の機能だけを個別に書くようにすることで、プロジェクトのメンテナンスコスト等を下げることを目的としています[^kmp]。

[^kmp]: https://kotlinlang.org/docs/multiplatform.html

KMP自体はとても素晴らしい物なのですが、安定版になったのが[去年の11月](https://blog.jetbrains.com/ja/kotlin/2023/11/kotlin-multiplatform-stable/)と、かなり新しい技術であるため、公式ドキュメント等が全体的に発展途上であり、日本語資料はおろか英語ですら資料が見つからないことも多いのが現状です。

そこで本記事では、KMP向けのビルドスクリプト（`build.gradle.kts`） を書くにあたってハマった点と、その解決法をいくつかメモ程度に記しておきたいと思います。

KMPを使おうとする方々が同じ問題にハマってしまい、時間を浪費することを多少なりとも防げれば幸いです。

なお、KMP向けの `build.gradle.kts` の書き方自体は[公式リファレンス](https://kotlinlang.org/docs/multiplatform-dsl-reference.html)を読んで、トリッキーなこと[^tricky]をせず、注意深く書けばそれほど難しくないかと思いますので割愛します。

[^tricky]: 同一のプラットフォームに複数のターゲットを作るなど

## KMPでfat JARを作成する

一般的に、KMPプロジェクトでJVMをターゲットとする場合、Kotlin Multiplatform Gradle pluginを `plugins` ブロックに追加し、`kotlin` ブロックに `jvm` ブロックを追加するなど、[公式リファレンス](https://kotlinlang.org/docs/multiplatform-dsl-reference.html)通りの手順を踏めばOKです。

しかし、[Gradle Shadow Plugin](https://github.com/johnrengelman/shadow)や[Ktor Gradle plugin](https://github.com/ktorio/ktor-build-plugins)[^ktor]を使用してJVMのバイナリをfat JARとして配布したい場合、単に当該のプラグインを `plugins` ブロックに追加するだけではうまくいきません[^fatJAR]。

[^fatJAR]: JAR自体は生成できるのですが、実行しようとすると「メインクラスを検出およびロードできない」というようなエラーが出ます
[^ktor]: 有名なWebサーバ・クライアントライブラリのKtorのGradle pluginにもfat JARを作成する機能がありますが、この機能は（コードを読む限り）shadowを使用しているらしく、shadowでの場合と同じ問題が発生します

これを解決するためには、以下のコードのように、`build.gradle` または `build.gradle.kts` の `plugins` ブロックに [`application`](https://docs.gradle.org/current/userguide/application_plugin.html) プラグインを追加し、 `kotlin` の `jvm`ブロック内で`withJava()`を呼び出す必要があります。


```kotlin
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](https://kotest.io/)というものがあります。

こちらは（おそらくJVMターゲットでのテスト時に）[JUnit Platform Gradle plugin](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-gradle)に依存しているため、KMPで使用する場合であっても以下のようなコードで `JVM` ターゲットでのテストタスクの実行時に `useJUnitPlatform()` が呼ばれるようにしてやる必要があります。 

```kotlin
kotlin {
    jvm {
        testRuns.named("test") {
            executionTask.configure {
                useJUnitPlatform() // required by kotest
            }
        }
    }
}
```

この点については一応Kotestの公式ページに[載っています](https://kotest.io/docs/quickstart)が、コード例が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`の当該ブロックに以下のような項目を追加してあります。

```kotlin
tasks.shadowJar {
    minimize {
        exclude(dependency("org.slf4j:.*:.*"))
        exclude(dependency("io.ktor:ktor-serialization-kotlinx-json:.*"))
    }
}
```