mike-neckのブログ

Java or Groovy or Swift or Golang

あなたの知らない Gradle の kotlin-dsl プラグイン

やんくさんに頼まれたので、誰かが書くだろうと思って待ってたけど誰も書かない Gradle の kotlin-dsl プラグインについて書きます。


まず、先日(2020/12/11)開催された JJUG ナイトセミナーにて、時間がないのでほぼ飛ばしてしまった、こちらの話題について解説します。

f:id:mike_neck:20201212182915p:plain

build.gradle(build.gradle) の中で if 文(Kotlin だと式ですね)とか for 文(こっちは式だったっけ?)を書くと、宣言的なコード(例えば依存ライブラリーの宣言など)と命令的なコードが混在してしまいます。そのため、ビルドスクリプトを読むために頭の切り替えが必要になるのでメンテナンス性が下がるというのが理由です。したがって、命令的なコードを書いている部分はすべてプラグインにして、 buildSrc プロジェクトに移動するのがよいと公式のドキュメントに書いてあります。

docs.gradle.org

しかし、これは素直におすすめできないところがあって、プラグインを書くためには少しだけプラグインの仕組みの学習コストがかかります(まあ、大したコストではありませんが…) 。そこで今持っている build.gradle(build.gradle.kts) の 知識でプラグインを無理なく書ける仕組みを使うのがよいでしょう。

プリコンパイルド・スクリプトプラグイン(Precompiled script plugins) というのがその仕組です。

docs.gradle.org

具体的には以前このブログで紹介した groovy-gradle-plugin が Groovy の場合のプラグインです。ここでは詳しく解説しないので、以下のエントリーを読んでください。

mike-neck.hatenadiary.com

これの Kotlin 版が kotlin-dsl プラグインです。(この言い方は語弊があって、先に kotlin-dsl があって、その後に Groovy 版ができたらしい)

なお、ググラビリティが悪いので、このプラグインの名前はどうにかしてほしい


kotlin-dsl の書き方

buildSrc/build.gradle.kts ファイルに以下のように書きます。 repositories {} ブロックを忘れると kotlin-dsl をビルドするための Kotlin をダウンロードできなくなってエラーが発生します

plugins {
  `kotlin-dsl`
}

repositories {
  jcenter()
}

buildSrc/src/main/kotlin の下に任意の名前で .gradle.kts ファイルを作成します。ここではサンプルのため、 example.gradle.kts というファイル名にします。なお、内容的には先の Java toolchains のエントリーの build.gradle と同じ様な内容にしています

plugins {
  `java`
}

repositories {
  mavenCentral()
}

dependencies {
  compileOnly("org.jetbrains:annotations:19.0.0")
  implementation("org.slf4j:slf4j-api:1.7.30")
  testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
  testImplementation("org.assertj:assertj-core:3.18.1")
}

internal val ss = sourceSets
internal val testSourceSets = ss.getByName("test")
@Suppress("UnstableApiUsage")
internal val javaToolChains = extensions.getByType<JavaToolchainService>()
@Suppress("UnstableApiUsage")
internal val javaPluginExt = extensions.getByType<JavaPluginExtension>()

javaPluginExt.toolchain {
  @Suppress("UnstableApiUsage")
  languageVersion.convention(JavaLanguageVersion.of(8))
  @Suppress("UnstableApiUsage")
  vendor.convention(JvmVendorSpec.AZUL)
  @Suppress("UnstableApiUsage")
  implementation.convention(JvmImplementation.VENDOR_SPECIFIC)
}

open class ProjectTestConfig {
  @Suppress("UnstableApiUsage")
  // 指定されたバージョンの Java でテストを実行するタスクを登録します
  fun testOnJavaVersion(vararg javaVersions: Int): Map<Int, TaskProvider<Test>> =
    javaVersions.map { javaVer ->
      javaVer to tasks.register<Test>("testJava${javaVer}") {
        group = "verification"
        description = "runs test on Java $javaVer"
        classpath = testSourceSets.runtimeClasspath
        testClassesDirs = testSourceSets.output.classesDirs
        useJUnitPlatform()
        javaLauncher.convention(javaToolChains.launcherFor {
          languageVersion.convention(JavaLanguageVersion.of(javaVer))
        })
      }
    }.onEach { pair ->
      tasks.named("check").configure { dependsOn(pair.second) }
    }.fold(mapOf()) { acc, pair ->
      acc + pair
    }
}

extensions.add<ProjectTestConfig>("testConfig", ProjectTestConfig())

なお、このスクリプトですが、書いている途中でコンパイルエラーが発生すると補完が効かなくなるので、気合で乗り越えていく必要があります。 これが僕が Kotlin DSL をあまり好まない理由の一つです。

f:id:mike_neck:20201212194334p:plain

f:id:mike_neck:20201212194356p:plain
tasks くらい取れるだろ

f:id:mike_neck:20201212194446p:plain
`launcherFor` だったっけな? `languageVersion` だったっけな?補完してくれなきゃわからん

最後に、ルートディレクトリーつまりメインプロジェクトの build.gradle.kts です。ここで、先程の .gradle.kts ファイルの拡張子を抜いた部分(ここまでの例だと example.gradle.ktsexample) をプラグインとして指定します。

plugins {
    `example`
}

// テストは Java 11 と 15 でも実行する
testConfig.testOnJavaVersion(11, 15)

かなりシンプル(というより、なにもない)になりました。

では、 Java 8 以外で実行すると落ちるようなテストを書いて実行してみます

class Version {

    @Test
    void show() {
        String javaHome = System.getProperty("java.home");
        assertThat(javaHome).contains("jre");
    }
}

f:id:mike_neck:20201212195301p:plain
check タスクから起動。途中のテストで落ちるので continue オプション付き

f:id:mike_neck:20201212195430p:plain
Java 11 と 15 でめでたくテストが落ちたようです

というわけで、 kotlin-dsl を使うとビルドスクリプトがシンプルになったと思います。また、 groovy での記事でも書いたように複数のサブプロジェクトに対してプロジェクト横断的に同一な記述が必要になるケース(例えば、同じ Kotlin のバージョンと Linter と フォーマッターを使いたい)で、 kotlin-dsl で作ったプラグインをサブプロジェクトで利用できます。


kotlin-dsl のおさらい

  1. プロジェクト(プロダクト)用の build.gradle.kts から命令的なコードを取り除いて、 buildSrc プロジェクトに命令的なコードを記述する
  2. buildSrcプラグインを書く場合に kotlin-dsl を使うと .gradle.kts ファイルを作成する
    1. で作成した .gradle.kts ファイルのピリオドよりも前の部分をメインのスクリプトプラグインに適用する

以上のような形でビルドスクリプトをシンプルにできます。