Gradle pluginを自作する

公式ドキュメントを試してGradleと仲良くなろうシリーズ、今日はこちら。

docs.gradle.org

 

Gradleのバージョンは7.1.0を利用している。

 

 

[シンプルなGradleプラグイン]

シンプルなGradleプラグインは、build.gradle.ktsファイルに直接実装することができる。

 

まずはいつものようにgradle initコマンドでGradleプロジェクトを作成する。

$ gradle init
Starting a Gradle Daemon (subsequent builds will be faster)

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 4

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Kotlin) [1..2] 2

Project name (default: gradle-simple-plugin): 

Source package (default: gradle.simple.plugin): 


> Task :init
Get more help with your project: https://docs.gradle.org/7.1/samples/sample_building_kotlin_applications.html

BUILD SUCCESSFUL in 30s
2 actionable tasks: 2 executed

 

./app/build.gradle.ktsファイルが作成されたので、ここにGradleプラグインを実装してみる。Gradleプラグインorg.gradle.api.Plugin<T>インターフェースを実装したクラスとなる。

プラグインの処理はapply()関数に実装する。

 

Pluginインターフェースを実装したクラスのボディでaとだけタイプすると、apply()関数がサジェストされた。便利。

f:id:kidani_a:20210709170245p:plain

 

サジェストされたapply()関数を選択すると、下の画像のような仮実装がされた。

f:id:kidani_a:20210709170321p:plain

 

TODO()関数は、常にエラーを発生させるらしい。恐ろしい。

f:id:kidani_a:20210711151217p:plain

 

公式ドキュメントに従ってTODOを消化する。

f:id:kidani_a:20210711151337p:plain

 

この時点で、helloタスクを実行しようとすると次のようにエラーが発生する。

$ ./gradlew -q hello

FAILURE: Build failed with an exception.

* What went wrong:
Task 'hello' not found in root project 'gradle-simple-plugin'. Some candidates are: 'help'.

* Try:
Run gradlew tasks to get a list of available tasks. Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 2s

 

プラグインを実装しただけではダメで、プラグインを適用する設定が必要らしい。

適当にサジェストに従って、実装を進めていく。自作プラグインもちゃんとサジェストされる。賢いぞGradle(むしろIntelliJ IDEAのほうか?)。

f:id:kidani_a:20210709170441p:plain

 

applyは関数なので関数呼び出しが必要。

f:id:kidani_a:20210709170602p:plain

 

完成。

f:id:kidani_a:20210709170625p:plain

 

再度実行してみると、今度はうまくいった!

$ ./gradlew -q hello
Hello from GreetingPlugin!!

 

ところで、ドキュメントで指示された「-q」オプションについて知らなかったので、gradle --helpコマンドを実行して、オプションをリストアップすると、以下のように書かれていた。

-q, --quiet Log errors only.

 

つぎは、コードを若干修正して実行してみる。出力されるメッセージに!!を追加した。

f:id:kidani_a:20210709171400p:plain

 

タスクを実行してみると、すぐに修正が反映された!🙌

$ ./gradlew hello

> Task :app:hello
Hello from GreetingPlugin!!

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

 

今回はPlugin<Project>とProjectを型引数に指定したが、他にもSettings型やGradle型を指定できるらしい。

Settings型の場合settings script(多分settings.gradleまたはsettings.gradle.ktsのこと?)で利用でき、Gradle型の場合初期化スクリプトで利用できるらしい。初期化スクリプトってなんだろう……。

 

Hello Worldレベルの出力ではあまり意味はないが、これを発展させると、パッケージング、静的解析、コード署名など様々なプラグインを実装できる(はずだ)。

docs.gradle.org

実装は相当難しそうだけど……。

github.com

 

[Gradleプラグインの設定]

プラグインを利用する際の設定を保持することもできる。

同じbuild.gradle.ktsファイルにGreetingPlugin2を実装していく。

 

まずは、プラグインの拡張設定クラスを公式ドキュメントに従って実装する。 

f:id:kidani_a:20210709172953p:plain

 

Property.convention()ってなんだろう、とおもったら設定されない場合のデフォルト値を提供するものらしい。なるほど。

f:id:kidani_a:20210709172918p:plain

 

プラグイン本体の実装と、プラグインの適用、拡張設定を行うとコードの全体は次のようになる。 

f:id:kidani_a:20210709173441p:plain

 

theってなんだろう、typoかな?と思ったらちゃんとそういう関数があるらしい。プラグインの設定を取得するための関数だそうな。

f:id:kidani_a:20210709173406p:plain

ただしドキュメントによると、Gradle 8でconventionの方は廃止になるらしい。Extensionの方は使い続けても大丈夫ってことなのかな?Gradle 7の公式ドキュメントでもExtensionの説明をしてくれてるわけだから、多分大丈夫なんだろう。

 

拡張設定を追加したhello2タスクを実行してみる……と、以下のようにエラーとなった。

$ ./gradlew hello2

FAILURE: Build failed with an exception.

* Where:
Build file '/(中略)/kotlin-sandbox/gradle-simple-plugin/app/build.gradle.kts' line: 82

* What went wrong:
Failed to apply plugin class 'Build_gradle$GreetingPlugin2'.
> Could not create plugin of type 'GreetingPlugin2'.
   > The constructor for type Build_gradle.GreetingPlugin2 should be annotated with @Inject.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s

 

GreetingPlugin2クラスのコンストラクタに@Injectアノテーションが必要らしい。よくわからない。

適当にググって見つけた記事に従って、コンストラクタに@Injectアノテーションを追加してみた。

f:id:kidani_a:20210711153255p:plain

 

しかし、やはりエラーとなる。

$ ./gradlew -q hello2

FAILURE: Build failed with an exception.

* Where:
Build file '/(中略)/kotlin-sandbox/gradle-simple-plugin/app/build.gradle.kts' line: 141

* What went wrong:
Failed to apply plugin class 'Build_gradle$GreetingPlugin2'.
> Could not create plugin of type 'GreetingPlugin2'.
   > 0

(略)

 

0ってなんだろうと思い、スタックトレースを出力してみると、ArrayIndexOutOfBoundsExceptionが発生しているようだった。

$ ./gradlew -q hello2 --stacktrace

FAILURE: Build failed with an exception.

(略)

* Exception is:
org.gradle.api.internal.plugins.PluginApplicationException: Failed to apply plugin class 'Build_gradle$GreetingPlugin2'.
        at org.gradle.api.internal.plugins.DefaultPluginManager.doApply(DefaultPluginManager.java:173)
(略)
        at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
Caused by: org.gradle.api.plugins.PluginInstantiationException: Could not create plugin of type 'GreetingPlugin2'.
        at org.gradle.api.internal.plugins.DefaultPluginManager.instantiatePlugin(DefaultPluginManager.java:83)
(略)
        at org.gradle.api.internal.plugins.DefaultPluginManager.doApply(DefaultPluginManager.java:166)
        ... 156 more
Caused by: java.lang.ArrayIndexOutOfBoundsException: 0
        at org.gradle.internal.instantiation.generator.DependencyInjectingInstantiator.addServicesToParameters(DependencyInjectingInstantiator.java:167)
        at org.gradle.internal.instantiation.generator.DependencyInjectingInstantiator.convertParameters(DependencyInjectingInstantiator.java:121)
        at org.gradle.internal.instantiation.generator.DependencyInjectingInstantiator.doCreate(DependencyInjectingInstantiator.java:62)
        at org.gradle.internal.instantiation.generator.DependencyInjectingInstantiator.newInstance(DependencyInjectingInstantiator.java:55)
        at org.gradle.api.internal.plugins.DefaultPluginManager.instantiatePlugin(DefaultPluginManager.java:81)
        ... 173 more

* Get more help at https://help.gradle.org

BUILD FAILED in 710ms

 

ググると、以下のIssueが見つかった。1年以上前のIssueが放置されているようだ。公式ドキュメントの例が動かないんだから早いところ直して欲しいものだ……。

github.com

 

GradleもOSSなので、これくらい直せるかな?と思ったらDependencyInjectingInstantiator#addServicesToParameters()がプライベートメソッドでテストが書きづらそうな感じだったので泣く泣く断念……。

github.com

そもそもこのあたりのテストがGroovyで実装されててGroovyわからん……となったのもある。

https://github.com/gradle/gradle/blob/v7.1.0/subprojects/model-core/src/test/groovy/org/gradle/internal/instantiation/generator/DependencyInjectingInstantiatorTest.groovy

 

Twitterにも流してみたが、特に反応がなかったのでみんな困ってないのかしら……。

 

[まとめ]

github.com