Kotlin DSL を考慮した Gradle Plugin を記述するために必要だったこと
とある Gradle Plugin を 2.0.0 に移行する際、v1 から Kotlin DSL を使っていた人の環境でちょっと問題が発生したというツイートを見たので、Kotlin DSL がどうやって DSL Marker なしに lambda で書けるようにしてるのかちょっと調べてみた。ここで記述している問題は 2.0.1 では修正されていて、また Kotlin DSL での移行ステップも README に追記しておきました。
TL;DR
- 外に見せる境界で def を使うのは避けておいた方が無難
- Kotlin DSL は拡張関数で delegate してて、見るべきメソッドが違うかもしれないから気をつけよう
- Kotlin と Gradle の言語仕様の違いに気をつけよう
kotlin-dsl
を apply して開発しないと Groovy と Kotlin DSL で同じような Closure (Lambda) の記法に出来ない(多分) (2/25 追記: DomainObjectでない場合)
lambda と Closure の型変換
v1 では動いていた以下の記述が 2.0.0 だと動かず、 2.0.0 から導入された deployments
であれば動くという問題が発生していた。
deploygate { apks { ... } }
これは下記の通りに書き換えると apks 記述でも動く。
deploygate { apks(closureOf<NamedDomainObjectContainer<NamedDeployment>> { ... } }
def apks(Closure) を呼び出そうとするが、渡している lambda と Closure の間で型変換が不可能であるので発生していた。これはclosureOf
を使い、型情報も明示的に提供することで解決できる。が、普通に lambda で書きたいですよね。
lambda での Closure と見かけ上同等の構文の実現
じゃあそもそも v1 では def apks(Closure)
と書いてなかったのか?というと、ちゃんと(?) def apks(Closure) としていた。実は Kotlin DSL で closureOf などを使わずに lambda で書けている場合、この Closure 関数が直接呼ばれているわけではない。
Groovy では apks { ... }
は def apks(Closure)
で動くし、この Closure を引数に取る関数が実際に呼ばれているんだけど、Kotlin DSL において apks { ... }
は getApks().invoke(...)
の糖衣構文となっており、これは Kotlin DSL が定義する拡張関数によって実現されていた。
ということで差異は getApks().invoke(...)
の糖衣構文であるところに由来していた。v1 では getApks()
に NamedDomainObjectContainer がちゃんと指定されていたんだけど、2.0.0 ではリファクタリングの過程で def
を使って動的解決に変更してしまったことが原因だった。 そうすると Object 型として認識されてしまう。そして最終的に Kotlin は「def apks(Closure)
を呼ぼうとしてるけど型が一致してないよ」とエラーを吐くという感じ*1。 ref: v1 / v2
ということで、
apks(Closure)
は Groovy でapks { ... }
と書く用途getApks(): <concrete type>
は Kotlin DSL でapks { ... }
と書く用途
setter property アクセス
Kotlin でも Groovy でも Property アクセスがサポートされているので、configuration ではメソッド呼び出しではなく代入文の書き方で表現できる
propName = "..."
という感じなんだけど、 Kotlin と Groovy の言語仕様の関係で、以下の記述が Kotlin DSL だと v2 で動いてなかった。
deploygate { userName = "userName" }
Kotlin では Setter の可視性は Getter よりも広くなければならない。つまり private getter + public setter のような property は存在せず、この場合は setFoo(...)
という呼び出しを強いられる。
Groovy はそんな制約はなく、private getter + public setter のような property でも foo = ...
と書ける。
今回 2.0.0 では deprecated とした設定値を全て public setter で定義し、新しい設定値の property に delegate している。その結果、v1 ではただの public property だったものが Kotlin 上では property 扱いとならなくなってしまった。なお Groovy では動くので、Groovy 用の後方互換テストケースでは気づけなかった。
これは getter を定義するだけで修正できる。
lambda や Closure を受け取りたい block (not DomainObject)
2022/10/25 追記。実行側のGradle バージョン依存の動作差異(Kotlin 1.3 compatibility)でした
2.0.0 では配布ページ(英名: Distribution) の設定について、distribution
ブロックを用意して切り出した。
deployments { create("debug") { distribution { ... } } }
これは Kotlin DSL だと最初の項 lambda と Closure の型変換
と同じ理由で、そのままでは動かない。この distribution は NamedDomainObjectContainer ではないので、invoke による補助が使えない。下記のように closureOf + 型指定でも動くけど、型を明示的に書いてもらうと今後のアップデートに支障が出るので正直避けたい。
deployments { create("debug") { distribution(closureOf<Distribution> { ... }) } }
さて、Kotlin DSL で推奨されている org.gradle.api.Action
にすれば動くんじゃない?って思ったけど今度は Groovy 側で落ちるようになった。caller (owner) が Distribution lambda の this になってしまって見つからないらしい。
- Marks a SAM interface as a target for lambda expressions / closures
- where the single parameter is passed as the implicit receiver of the
- invocation ({@code this} in Kotlin, {@code delegate} in Groovy) as if
- the lambda expression was an extension method of the parameter type.
ドキュメントを見た感じ、delgate に差し込んでくれるんじゃないの?と思って悩んでたんだけど、そもそも Gradle Plugin 開発時に kotlin-dsl
plugin (というか Kotlin + SAML w/ receiver compiler plugin) を当てないとここらへんの機能提供ができないっぽい。なんとなく Kotlin で Gradle 設定を書く方をKotlin DSLと呼ぶ印象があって、Plugin 開発側では何も考えていなかった。
試そうと思って Kotlin DSL を入れたら Kotlin コードから Groovy のコードが見れず、凄い長い戦いになりそうだったのでやめました。詳しい人教えてください。
蛇足
ここに Kotlin DSL が DSL として動くように拡張関数が定義されており、かなり楽しい。 うーん、Configurable#invoke 生やしてくれないかなー
https://github.com/gradle/kotlin-dsl/tree/master/subprojects/provider/src/main/kotlin/org/gradle/kotlin/dslgithub.com
Action がなんで this を解決できるかについては SAM-with-receiver Kotlin compiler plugin
を調べればOK。
kotlin/build.gradle.kts at master · JetBrains/kotlin · GitHub
そういえばGroovy -> Kotlin DSL への書き換えについては DroidKaigi 2019 で tnj という人が書き換えについて発表をしています。
スライド中で触れられている「Gradle のファイルっていつの間にか増えるよね」で増やした張本人は僕です。