Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

Gradle Plugin実装の基本

Gradleは動きが早いのでTIPSを文書化する意味があまりないのですが、全体像をざっくりつかめるだけでも変化を追うには有用のはず。 そのうち体裁まとめてZennに置いたほうがいい気がしてますが、一旦走り書きということで。

Gradleの考え方

設定と実行の分離

キャッシュを効かせるためにも、Configuration Cacheとかの文脈で、設定フェーズと実行フェーズは分けて考えることを推奨する動きがあるっぽい。 実行フェーズに設定を変えるなとか、afterEvaluate { ... } に過度に依存した設定を書くなとかを気にすればいい。

設定の遅延

設定より規約

能力と慣習の分離

登場人物

プラグイン実装に必要な登場人物は、だいたい3つほど頭に入れれば良いと思われる。

  1. Plugin
  2. Extension
  3. Task

他には依存を扱うConfigurationとかProjectにメソッドやプロパティを追加するためのConventionなどもある。ConventionはExtensionで代替すべきという情報もCommunity Forumにはあったので置いといて良いかも。

Plugin

プラグインを適用した際、真っ先にインスタンスが作成されるクラス。このPluginによってまず必要な初期化処理を行う。Gradleのバージョン確認もここ。 基本的にはファイル作成やタスク作成といった重い処理は行うべきではない。適用しただけで重くなるようなプラグインになってしまうので。

また"能力と慣習の分離"の観点から、機能を提供する部分をbase pluginとして慣習・規約を扱う部分から独立して定義することが望ましい。 BasePlugin, ReportingBasePluginなど標準で用意されたbase pluginもあるので、適宜 project.pluginManager.apply(BasePlugin) などとして利用できる。

Extension

Property-like fieldsを持つPOJO。 ExtensionもPlugin同様、インスタンス生成コストは抑える方が良い。これはプラグインの提供する機能(タスク)を使わない場合でもExtensionは作成されることが多いため。

Extensionではプロジェクト内で実行されるTaskすべてに共通の設定を行うことになるはず。

Task

主にDefaultTaskを拡張Property-like fieldsを持つ。入力と出力を宣言することで、キャッシュ可能にできる。

実行のコストは重くして良い。不要なTaskは生成されない・実行されないようにビルドスクリプトを書くことがユーザには推奨されている。 同じプロジェクトに属するTaskは同時実行されない=後続Taskをブロックするので、重い処理はWorker APIに逃がすことも検討する。

lazy config をどう実現するか

PropertyFileCollection に代表されるProperty-likeなクラスを活用する。Extensionのプロパティのconventionをデフォルト値に、TaskのプロパティのconventionをExtensionのプロパティにする。ちょっと古いプラグインだとconvention mappingというのを使っているがinternal APIなので無視で良い。

// ExtensionはPOJOにする。生成コストを低く保つ。
// 抽象クラスにすることでProperty-likeフィールドの生成処理を省略できる(ドキュメント未確認)
abstract class MyExtension {
  abstract Property<String> getFoo()
}

// TaskもProperty-likeフィールドを持つ。@Inputや@Output,@Internalなどの修飾子を使ってキャッシュ可能にする
// やはり抽象クラスにすることでProperty-likeフィールドの生成処理を省略できる
@CacheableTask
abstract class MyTask extends DefaultTask {
  @Input
  abstract Property<String> getFoo()

  @TaskAction
  void run() {
    print this.foo.map { "The value of foo property is ${it}" } .get()
  }
}

// base pluginはextensionの登録やGradleのバージョン確認など、機能実行の前準備のみ実行。規約(慣習)は扱わない。
class MyBasePlugin implements Plugin<Project> {
  @Override
  public void apply(Project project) {
    def extension = project.extensions.create("myExtension", MyExtension)
    project.tasks.withType(MyTask).configureEach {
      // 規約に関係ないデフォルト値はここで設定して良いはず
      it.foo.convention(extension.foo)
    }
  }
}

// こちらのpluginでは規約(慣習)を扱う。これはsourceSetごとにTaskを作る例
class MyPlugin implements Plugin<Project> {
  @Override
  public void apply(Project project) {
    project.pluginManager.apply(MyBasePlugin)

    project.plugins.withId('java-base') {
      def convention = project.convention.getPlugin(JavaPluginConvention)
      convention.sourceSets.all {
        def name = it.getTaskName("foo", null)
        project.tasks.register(name, MyTask) {
          // 必要な設定を行う
        }
      }
    }
  }
}

VerificationTask のようなインタフェースもあるので適宜実装すると、他プラグインとの整合性を取りやすい。

ドキュメントに目を通しても迷ったら