Gradle:プラグインを作り、既存プラグインの動作を変更する

多数のプロジェクトで同じようなbuild.gradleがあるとき、その処理を共通化したい。これを行うには、既存のプラグインの動作を変更しなければならない。例えば、Gradleのコンベンションに従わない以下のような状況があるとする。

apply plugin: 'java'
sourceSets {
  main {
    java {
      srcDir 'src_a'
      srcDir 'src_b'
      srcDir 'src_c'
    }
    resources {
      srcDir 'src_a'
      srcDir 'src_b'
      srcDir 'src_c'
    }
  }
}

これではいかにも面倒だ。ここでは最終的に次のように記述できることを見ていく。

apply from 'sample.gradle'
apply plugin: JavaExPlugin
sourceSetsEx {
  main 'src_a', 'src_b', 'src_c'
}

ここで、apply fromされるsample.gradleはすべてのbuild.gradleの共通処理を入れるものとする。

プラグインの作成

まず初めにはプラグインを作成してみる。これは簡単だ。

sample.gradle

ext.JavaExPlugin = JavaExPlugin
class JavaExPlugin implements Plugin<Project> {
  def void apply(Project project) {
    project.apply(plugin:'java')
  }
}

build.gradle

apply from: 'sample.gradle'
apply plugin: JavaExPlugin

sourceSets {
  main {
    java {
      srcDir 'src_a'
      srcDir 'src_b'
      srcDir 'src_c'
    }
    resources {
      srcDir 'src_a'
      srcDir 'src_b'
      srcDir 'src_c'
    }
  }
}

sample.gradleの中でJavaExPluginを定義し、そこで’java’プラグインを呼びだす。だから、build.gradleの方では、sample.gradleの取り込みと、JavaExPluginの呼び出しだけでよい。’java’プラグインが呼び出されるので、普通にsourceSetsを使うことができる。

※特に注意「ext.JavaExPlugin = JavaExPlugin」がないとbuild.gradleからJavaExPluginを参照できなくなる。これはスコープの問題だ。

プロジェクトを拡張する

やりたいことは、以下のように簡単に記述することだ。

sourceSetsEx {
  main 'src_a', 'src_b', 'src_c'
}

このためには、プロジェクトに拡張を行ってやる。javaExPluginを以下に変更する。

class JavaExPlugin implements Plugin<Project> {
  def void apply(Project project) {
    project.apply(plugin:'java')
    // プロジェクトに"sourceSetsEx"という名前の拡張を追加する
    project.extensions.create("sourceSetsEx", SourceSetsEx, project)
  }
}

その上でSourceSetsExというクラスを記述してやるのだが、最終的なsample.gradleは次のようになる。

ext.JavaExPlugin = JavaExPlugin
class JavaExPlugin implements Plugin<Project> {
  def void apply(Project project) {
    project.apply(plugin:'java')
    project.extensions.create("sourceSetsEx", SourceSetsEx, project)
  }
}

class SourceSetsEx {
  Project project
  SourceSetsEx(Project project) {
    this.project = project
  }
  def methodMissing(String name, args) {
    String s = "x.sourceSets { ${name}  {"
    s += "java {";
    args.each {
      s += "srcDir('${it}');"
    }
    s += "}"
    s += "resources{"    
    args.each {
      s += "srcDir('${it}');"  
    }
    s += "}" + "}}"
    println s
    Eval.x(project, s)
  }
}

build.gradleは以下だ

apply from: 'sample.gradle'
apply plugin: JavaExPlugin

sourceSetsEx {
  main 'src_a', 'src_b', 'src_c'
}

SourceSetsExにおいてmethodMissingで処理している理由は、元々のsourceSetsに記述されるものがmainだけではなく、任意の名称がかけてしまうからだ。つまり、

sourceSetsEx {
  server 'src_a'
  terminal 'src_b', 'src_c'
}

などといきなり書けてしまったりする。これに対処するため、すべてを「不明なメソッド」として扱うようにしている。

また、Eval.xによって作成した文字列をコードとして評価するのだが、ここでは実行する文字列の他にprojectオブジェクトを与えなければならないので、Eval.meではなく、Eval.xを使用している。

mainだけのサポートの場合

mainしかサポートしない場合は簡単になる。

sample.gradle

ext.JavaExPlugin = JavaExPlugin
class JavaExPlugin implements Plugin<Project> {
  def void apply(Project project) {
    project.apply(plugin:'java')
    project.extensions.create("sourceSetsEx", SourceSetsEx, project)
  }
}

class SourceSetsEx {
  Project project
  SourceSetsEx(Project project) {
    this.project = project
  }
  def main(String...args) {
    args.each{
      project.sourceSets.main.java.srcDir(it)
      project.sourceSets.main.resources.srcDir(it)
    }
  }
}