Gradle Kotlin DSL入門

Introduction

この記事はQiita版の転載です。
これはQiita Kotlin Advent Calendar 2019 7日目の記事です。6日目は @yt8492 さんのKotlin向けgRPC/protobufライブラリKroto+を使ってみよう、8日目は@nnao45 さんのkotlin製ORM Exposed小技集です。

どうも、株式会社 justInCaseの開発担当の子会社であるjustInCase TechnologiesでバイトとしてAWS CDKのラッパーライブラリを自動生成する奴を作ってるトリナーと申します。

Gradle Kotlin DSLはGradle 5.0で正式版になった、GradleのビルドスクリプトをKotlinで書ける機能です。
従来のビルドスクリプトであるbuild.gradlesettings.gradleはGroovyというJVM上で動作するスクリプト言語を用いていました。
主観的な話で申し訳ないですが、Gradleのビルドスクリプト用途以外でGroovyを用いているという話を聞いたことが無いため、「Gradleのビルドスクリプト専用言語」の様な形で使っている人が大半でしょう。そして、Groovyを勉強してGradleを使っている人はかなり少なかったのではないでしょうか?私のように「よくわからんけどコピペで動く」といった感じで使っていた人が多かったのではないでしょうか。
しかし、その様な時代はGradle Kotlin DSLの到来で終わりました。Kotlinという素晴らしい言語でGradleのビルドスクリプトが記述できるようになったのです。ビルドスクリプトとビルド対象のコードを両方Kotlinで書ける事により、ビルドスクリプトの読解と編集の障壁は圧倒的に下がりました。buildSrcとの併用で、より複雑なビルドスクリプトをより容易に記述する事ができるようになりました。
しかしながら、現在、Gradle Kotlin DSLについての日本語での入門記事はあまりありません。また、GradleのAPIとGradle Kotlin DSL特有のAPIが混在するため、学習に難易度がやや高いというのも事実です。そのため、入門記事を書いておこうと思います。

Requirement

バージョンは執筆時の物。

  • IntelliJ IDEA 2019.3
  • Gradle 6.0.1(IntelliJで自動生成されるものは5.2.1)
  • Kotlin 1.3.61
  • JDK 8以降
  • Gradleにある程度触れたことのある人

Setup

IntelliJ IDEAでFile > New > Projectを選択し、New ProjectのウインドウでGradleタブを選択します。ここで、Kotlin DSL build scripのチェックボックスにチェックを入れるとプロジェクトの作成時にbuild.gradle.ktsが生成されるようになります。

プロジェクトの作成が終わったら、IntelliJがIndexingとGradleとのSyncを完了するのを待ちます。終わったら、おもむろにTerminalを開いて gradlew wrapper --gradle-version=6.0.1を実行して完了するのを待ちましょう。Gradleのバージョンが6.0.1になります。

Basic of Gradle Kotlin DSL

自動生成されたスクリプトを読む

現バージョンでIntelliJにより自動生成されるKotlinのバージョンは以下のような感じになります。

plugins {  
    kotlin("jvm") version "1.3.61"  
}  

group = "org.example"  
version = "1.0-SNAPSHOT"  

repositories {  
    mavenCentral()  
}  

dependencies {  
    implementation(kotlin("stdlib-jdk8"))  
}  

tasks {  
    compileKotlin {  
        kotlinOptions.jvmTarget = "1.8"  
    }  
    compileTestKotlin {  
        kotlinOptions.jvmTarget = "1.8"  
    }  
}  

比較に、Groovy版のbuild.gradleを掲載します。

plugins {  
    id 'org.jetbrains.kotlin.jvm' version '1.3.61'  
}  

group 'org.example'  
version '1.0-SNAPSHOT'  

repositories {  
    mavenCentral()  
}  

dependencies {  
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"  
}  

compileKotlin {  
    kotlinOptions.jvmTarget = "1.8"  
}  
compileTestKotlin {  
    kotlinOptions.jvmTarget = "1.8"  
}  

上から順に見ていきましょう。

plugin{}

Gradle Kotlin DSLではkotlin("jvm")という、Groovy版には無い専用関数でKotlinのPluginを指定しています。この"jvm"の部分は変更可能で、ターゲットのプラットフォームに応じて変更すれば動きます。
では、他のPluginはどの様に指定するのでしょうか?これは2通りあります。
1つは、applicationmaven-publish等のGradle公式のPlugin用の方法です。これらのPluginは、個別の特殊なプロパティが指定されているため、application`maven-publish`とだけ書けばこれらのPluginが適用されます。なお、Kotlinにおいて-はプロパティ名として使用できないため、-が入る場合は`で囲う必要があります。中身としては、拡張プロパティのgetterでPluginの適用処理を書いてるっぽいです。頭おかしい(褒め言葉)
2つ目は、サードパーティのPluginを適用する方法で上記の2つの方法が内部的に呼び出している方法です。id("plugin id")関数を用います。これは見たまんまで、Pluginのidを指定してPluginを適用します。
Pluginのバージョンを指定するには、versioninfix関数を用います。この方法はKotlin PluginとサードパーティのPluginで共通です。Gradle公式のPluginでも使えますが、不要です。ここらへんはGroovy版と共通ですね。

ここまでの内容をまとめたサンプルコードがこちら。

plugins {  
    kotlin("jvm") version "1.3.61"  
    application  
    `maven-publish`  
    id("com.jfrog.bintray") version "1.8.4"  
}  

repository{}

Maven CentralやJCenterはGroovy版と同様に関数を呼び出せば良いのですが、それ以外のMavenリポジトリを指定する方法が若干異なります。具体的には以下の様な感じです。

repositories {  
    mavenCentral()  
    maven(url = "https://dl.bintray.com/justincase/aws-cdk-kotlin-dsl")  
}  

maven()の引数にurlを指定する形ですね。

dependencies{}

Pluginと同様にKotlinの標準ライブラリが優遇されているだけで、それ以外はGroovyのコードがKotlinの関数呼び出しになっただけです。以下のようにも書けます。

dependencies {  
    // この2つは実質同じ  
    implementation(kotlin("stdlib-jdk8"))  
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61")  
}  

tasks{}

ここでは、タスクの設定を行います。このブロック内ではそのモジュール内に存在する全てのタスクに対応する名前の拡張プロパティが存在しており、ラムダ式を引数に取る関数として呼び出すことでタスクの設定を行えます。ちなみにこのスコープ内では、tasks.register()の様な、通常tasksに対して行う操作もtasksを省略して行うことが可能です。
ちなみに、IntelliJに生成されるビルドスクリプトがこの関数を使うようになったのは私が知る限りKotlin 1.3.60になって以降です。私もこの記事を書くために生成するまでこの関数の存在を知らなかったのでどのバージョンのGradle及びKotlinから利用可能かは不明です。タスクに関する操作をまとめれるので何気に神機能ですね。
ちなみに、以下の様に書いた場合、上のjar{}jarという名前のタスクにのみ適用され、下のwithType<org.gradle.jvm.tasks.Jar>{}Jarクラスを用いているタスク全てに適用されます。

tasks {  
    jar {  
        include("foo.class")  
    }  

    withType<org.gradle.jvm.tasks.Jar> {  
        include("bar.class")  
    }  
}  

EOF

以上で自動生成されるbuild.gradle.ktsについての解説は終わりです。自動生成部分だけでGradleのAPIとしてGoovy版と異なる部分はほぼ網羅されていて、残るはKotlinとGroovyの言語機能の違いなので、そこら辺はKotlinに慣れていたらある程度雰囲気でできそうな感じですね。
次は、自動生成されたコードには無いGradle Kotlin DSL特有のAPIについて紹介していこうと思います。

自動生成されたコードに無い諸々

関数名とタスク名と一致しない設定

GradleのPluginを各種導入していると、直接タスクの設定を弄っているわけでは無いがタスクに関連のある設定、というのがよく出てきます。これは、GradleのExtensionという、Pluginに対しての設定を行う機能です。私も詳しくは理解していないので詳細は自分で調べてください。このExtensionの例としては、applicationPluginのJavaApplicationや、maven-publishPluginのPublishingExtension等があります。Groovy版だとapplication{}publishing{}で呼び出している奴ですね。
こいつらの設定はGroovy版と同じです。タスクの設定は上記の通りtasks{}スコープ内でやる感じですが、Extensionの設定はGroovy版と同様にトップレベルに書く形ですね。
これはKotlin DSL特有のAPIでは無いですが、上記のtasks{}の補足として書きました。

Propertyの利用

みなさん、Gradleのプロパティを使っていますか?
私は依存ライブラリのバージョンの管理等に使っていますが。今の時代だとbuildSrc使えって言われるんですかね?正直、buildSrcにバージョンを記述する事にあまりメリットを見いだせないのですが(特に比較的小規模なProject)。むしろGradle 6.0の時代ならplatformを使えって感じですかね?それはともかく。
Groovy版ならプロパティは通常の変数みたいな感じでアクセスできてしまいますが、Kotlinではその方法は使えます。ここでは、Propertyの使い方について2通り紹介します。

ext

ext["propertyName"] as String
これでプロパティを取得できます。これはGroovy版と同じ仕様ですね。"propertyName"の部分を任意のプロパティの名前に変えて、好きな型にキャストしましょう。

Property Delegation

val foo: String by project
Kotlin DSLだとこんな風に書けます。
projectからのデリゲートでプロパティが取得できます。変数名はプロパティ名で、型はプロパティの型です。
こっちの方が簡潔でわかりやすいですね。

Taskの登録

タスクの登録自体は特筆すべき点はtasks{}のところで述べた以外にはありません。しかし、タスクを登録した後の取り回しに違いがあります。
tasks.register()TaskProvider<T>を返します。このTaskProvider<T>は名前の通りタスクを提供するクラスです。invoke()が定義されているのでfooTaskProvider {}の様にする事でタスクの設定ができます。また、fooTaskProvider.get()とする事でTaskを取得する事ができます。しかし、生のTaskだけが欲しい場合はこの仕様は少し煩雑です。そこで現れるのがKotlinのProperty delegation。
val barTask by register("bar")とすることで、barTaskには生のTaskが代入されます。ちなみにこのProperty Delegation自体はTaskProviderに対して定義されているので、あらゆるTaskProviderに適用が可能です。
まとめるとこうです。

// TaskProvider<T>  
val fooTask = register("foo")  
// Task  
val barTask by register("bar")  

こういう細かいところにまで気を利かせて便利な構文を用意してくれるあたり、Gradle Kotlin DSLは素晴らしいですね。

Conclusion

以上、Gradle Kotlin DSL入門でした。
Gradle Kotlin DSLは(補完が遅い事を除けば)本当に素晴らしい物です。Gradleのビルドスクリプトを構築する上では生産性を爆上げしてくれます。これがあればもうGradleは怖くない!
この記事がGradle Kotlin DSLの普及に役立てば幸いです。

これは私見なんですが、Gradleの入門で調べて出てくる記事って、最初に「タスクの作成」を行う物が多いですよね。しかし、大抵のユースケースの場合、タスクの作成を行う事は少なく「Pluginを設定して、依存ライブラリを指定して、ビルドの設定を行う」事ができれば十分な事が多いのではないでしょうか。この記事は「既存のGradleユーザーをKotlin DSLに誘う」事を目的としているので、タスク周りはGradleのAPIって事でサラッと飛ばしましたが。

如何せん、こんな入門記事を書くのは初めてなので、「こんな内容も追加して!」って事や「ここの説明がわかりにくい!」という事があればコメントかTwitterで言ってください。