Android
Kotlin
gradle

kotlinとkotlin-dslでAndorid用GradlePluginを作成する

kotlinとkotlin-dslでAndorid用GradlePluginを作成する

kotlinとkotlin-dslを利用し、Android向けのGradle Pluginを作成したときに調査した内容をメモです。

Gradle Pluginの作成がはじめての場合、下記ドキュメントを読むと全体像が把握できます。
Writing Custom Plugins - Gradle User Manual
DroidKaigi 2018でGradleプラグインを作って開発効率を改善しようって発表をした話 – Yuki Fujisaki / tnj – Medium

kotlinでの実装はkotlin-dslリポジトリにGradlePluginのサンプルがあったので、こちらを参考にしながら進めていきました。
kotlin-dsl/samples/gradle-plugin at master · gradle/kotlin-dsl · GitHub

kotlin-dslを使用できるようにする

build.gradle.ktsへのマイグレーション

下記を参考にしながら、既存のbuild.gradleをbuild.gradle.ktsにマイグレーションをしました。
Migrating build logic from Groovy to Kotlin

最終的にすべて書き換えましたが、最初はkotlin-dslにあったgroovy-interopサンプルを参考に、既存のbuild.gradleと併用しつつ徐々に書き換えていきました。
kotlin-dsl/samples/groovy-interop at master · gradle/kotlin-dsl · GitHub

build.gradle.kts
apply(from = "build.gradle")

また、settings.gradleに下記を追加することで、settings.gradle.ktsへの置き換えも後回しにできます。

settings.gradle
rootProject.buildFileName = 'build.gradle.kts'

Pluginの追加

build.gradle.ktsではいくつかpluginのエイリアスを用意しており、id指定なしで使用することができます。

build.gradle.kts
plugins {
    `kotlin-dsl`
    `java-gradle-plugin`
    `maven-publish`
}

kotlin-dslを追加すると、build.gradle.kts以外のkotlinのコード上でもorg.gradle.kotlin.dslパッケージを使用することができるようになります。

Pluginの実装

Pluginクラスの作成

org.gradle.api.Pluginを継承したクラスを作成します。

class MyPlugin : Plugin<Project> {

    override fun apply(project: Project) {
    }
}

タスクの追加

タスクの作成は下記のように実装することができます。

class MyPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        project.run {
            tasks.create("myTask").doLast {
                println("Hello, myTask")
            }
        }
    }
}

他にも数パターン定義の仕方があり、kotlin-dslリポジトリのtask-dependenciesサンプルのように定義することもできます。
kotlin-dsl/build.gradle.kts at master · gradle/kotlin-dsl · GitHub

import org.gradle.kotlin.dsl.invoke

class MyPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        project.run {
            tasks {
                "myTask" {
                    doLast { println("Hello, myTask") }
                }
            }
        }
    }
}

tasksはorg.gradle.kotlin.dsl.invokeをimportすることで使えるようになります。

Androidのタスクにアクションを追加

例としてassemble後にアクションを実行するplugin作成します。
groovyでは下記のように実装することができます。

class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.android.applicationVariants.all { variant ->
            variant.assemble.doLast {
              //do something
            }
        }
    }
}

kotlinでは下記のように記述しました。groovyで書くよりも少々冗長になってしまいました。
また、groovyとの差を少なくするためにextentionを作成しています。

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val android = project.extensions["android"] as AppExtension

        android.applicationVariants.all { variant ->
            variant.assemble.doLast {
                //do something
            }
        }
    }
}

fun <T> DomainObjectCollection<T>.all(action: T.(T) -> Unit) {
    all {
        action(this)
    }
}

詳細を見ていきます。

AppExtensionの取得

groovyコードproject.androidで取得できる値の実体はcom.android.build.gradle.AppExtensionです。
applicationVariantsをはじめ、Androidプロジェクトのbuild.gradleで使用するプロパティが定義されています。

kotlinではproject.extensions["extensionの名前"]で取得することができます。
AppExtensionの名前はandroidです。

また、AppExtensionのプロパティにアクセスできるようにas AppExtensionでキャストをします。

さらにAppExtensionはcom.android.build.gradleパッケージにあるため、dependenciesに追加してkotlinコードから参照できるようにします。

build.gradle.kts
dependencies {
    implementation("com.android.tools.build", "gradle", "X.X.X")
}

Variantの取得

Variant取得部分
android.applicationVariants.all { variant ->
    variant.assemble.doLast {
        //dosomething
    } 
  }
}  
extentionのみ抜粋
fun <T> DomainObjectCollection<T>.all(action: T.(T) -> Unit) {
    all {
        action(this)
    }
}

各Variantの取得にはapplicationVariants.allを使用します。
AppExtension#applicationVariants - Android Plugin DSL Reference

allはorg.gradle.api.DomainObjectCollectionに定義されており、引数にorg.gradle.api.Actionを受け取ります。

しかし、kotlinでは引数のActionがApplicationVariant.() -> Unitとして解釈され、lambdaで引数をもらうことができませんでした。
Kotlin not able to convert gradle's Action class to a lambda - Stack Overflow

allで完結する場合はthisが使用できますが、その後のdoLastの引数もActionなのでthisを区別する必要があります。
そのためApplicationVariant.(ApplicationVariant) -> Unitを受け取るallをextentionとして定義し、メソッドの中でもともと用意されていたallを呼び出しています。

上記のようにgroovyのコードをベースに実装を進めていくと、ところどころ修正が必要な箇所がでてきます。

リリース

java-gradle-pluginとmaven-publish pluginの設定をします。

java-gradle-pluginの設定

java_gradle_pluginを参考に設定する。

build.gradle.kts
gradlePlugin {
    (plugins) {
        "MyPlugin" {
            id = "my.plugin" //Plugin使用側でapply時に指定する値
            implementationClass = "my.plugin.MyPlugin" //Plugin実装クラス
        }
    }
}

maven-publish pluginの設定

Maven Publish Pluginを参考に設定します。
今回は自分でたてたMaven Repositoryにリリースしたかったため以下のように記述。

build.gradle.kts
publishing {
    repositories {
        maven(url = "http://my/repo") {
            credentials {
                username = "my_maven_user"
                password = "my_maven_pass"
            }
        }
    }
}

設定後はkotlin-dslを使用していないときと同様、 publishpublishToMavenLocalでリリースすることができます。
また、使用する側もkotlin-dslで作成されたpluginかを区別することなく使用することができます。

参考資料

gradle pluginの作り方ガイド
Writing Custom Plugins - Gradle User Manual

android用gradle pluginについて説明があり参考になりました。
DroidKaigi 2018でGradleプラグインを作って開発効率を改善しようって発表をした話 – Yuki Fujisaki / tnj – Medium

kotlin-dslのサンプル集。特にGradle Pluginのサンプルが参考になりました。
kotlin-dsl/samples at master · gradle/kotlin-dsl · GitHub

DroidconでのGradle Pluginについての発表資料。本記事では記述していないkotlinでExtensionやTaskクラスを定義する手順についても記載されています。
GitHub - StefMa/Gloc: A Gradle plugin which I used as an example at my talk at the Droidcon Italy 18