おでーぶでおでーぶ

いろいろ書く。いろいろ。

Kotlin DSL を考慮した Gradle Plugin を記述するために必要だったこと

とある Gradle Plugin を 2.0.0 に移行する際、v1 から Kotlin DSL を使っていた人の環境でちょっと問題が発生したというツイートを見たので、Kotlin DSL がどうやって DSL Marker なしに lambda で書けるようにしてるのかちょっと調べてみた。ここで記述している問題は 2.0.1 では修正されていて、また Kotlin DSL での移行ステップも README に追記しておきました。

github.com

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 が定義する拡張関数によって実現されていた。

github.com

ということで差異は 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 DSLapks { ... } と書く用途

として動かせることがわかったので、修正した *2

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)

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 DSLDSL として動くように拡張関数が定義されており、かなり楽しい。 うーん、Configurable#invoke 生やしてくれないかなー

github.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 のファイルっていつの間にか増えるよね」で増やした張本人は僕です。

speakerdeck.com

www.youtube.com

*1:悲しい?嬉しい? ことに IDE では型推論が動いてくれる

*2:ちなみに deployments は型を指定していた。なんでか覚えてないけど・・・