EGradleを使いながらGradleに慣れる

2018年8月3日

build.gradleを変更しながら、徐々にgradleに慣れることにする。EGradleの機能を使ってbuild.gradleの変更結果を確認する。
その過程で必要なマニュアルも参照していく。

applyの様々な書き方

文字列引数のapply

Eclipseにて適当なJavaプロジェクトを作成し、プロジェクトトップに以下のbuild.gradleを置く。

// Javaプロジェクトには必須
apply plugin: 'java'  

// eclipseを使う場合には便利なので入れておく
apply plugin: 'eclipse'

これは以下のように書いてもよい。applyというメソッドの呼び出しと、その名前付パラメータであり、セミコロンは省略できる。

apply(plugin:'java');
apply(plugin:'eclipse');

二種類のapplyメソッド

ところで、apppyというメソッドは、build.gradle一つに一つ与えられるProjectというオブジェクトのメソッドらしい。

Projectのマニュアルの、Method detailsの中のapplyの項を見てみると、二つのメソッドがある。

void apply(Closure closure)
void apply(Map<String, ?> options)

クロージャ引数のapply

上記で使用したのは後者の方のメソッドらしい。では、前者はどう使うのかと言えば、クロージャを受け取るのだから、

apply{
}

あるいはクロージャを引数として与えるのだから、丁寧に書けば、

apply({
});

となるだろうが、このクロージャが「ObjectConfigurationActionを構成するため使われる」のだという。
ObjectConfigurationActionを見てみると、そこにはメソッドが4つあり、

ObjectConfigurationAction   from(Object script)
ObjectConfigurationAction   plugin(Class<? extends Plugin> pluginClass)
ObjectConfigurationAction   plugin(String pluginId)
ObjectConfigurationAction   to(Object... targets)

つまり、(他のメソッドの意味はわからないが)最初と同じことをやりたい場合には、3つ目のメソッドを呼び出せばよい。

apply{
  plugin('java')
}

二つ入れたい場合は、こうでもよい。

apply{
  plugin 'java'
  plugin 'eclipse'
}

これに対して、最初の形態では、以下は書けない。名前付き引数は唯一でなければならないようだ。

// ダメ
apply plugin: 'java', plugin: 'eclipse'
// ダメ
apply(plugin: 'java', plugin: 'eclipse')

しかし、以下はOK。

apply { plugin 'java'; plugin 'eclipse' }

以下でもOKになるのだが、セパレータが無くても引数の数で区切りがわかるのだろうか?

apply { plugin 'java' plugin 'eclipse' }

Eclipseのビルドパスを設定させてみる

applyメソッドはbuild.gradle一つに一つ与えられるProjectのメソッドなのだが、applyによってプラグインを持ち込むと、そのプラグインのプロパティやらメソッドやらがProjectに追加される、つまり、何の修飾もせずにそれらを使うことができるらしい。

さて、’java’と’eclipse’の二つのプラグインを入れた状態で、EGradleのボタンを押してみる。

このボタンは単に「gradle cleanEclipse eclipse」を呼び出すだけで、これは’eclipse’プラグインの機能として、build.gradleに指定されたとおりにeclipseプロジェクトのビルドパス等を変更するもの。

つまり、デフォルトで設定されていたものや、手で設定したものは削除されてしまい、build.gradleの設定状態となる。

しかし、eclipseのデフォルトのソースフォルダ「src」は、gradleのデフォルト「src/main/java」とは異なるため、結局ビルドパスからソースフォルダ指定が削除されてしまうことになる。

sourceSetsの操作

sourceSetsをEclipseのやり方に合わせる

sourceSetsをいじって、eclipseのやり方に合わせてみる。単純に以下のように書けばよいという。これはどこの本にも書いてある。
とりあえずソースのみで、リソースの方は無視する。

apply { plugin 'java'; plugin 'eclipse' }
sourceSets {
  main {
    java {
      srcDir 'src';
    }
  }
}

こうしてからEGradleを動かせば、きちんとEclipseのビルドパスを設定してくれる。

sourceSetsにはプロパティとクロージャ引数のメソッドがある

sourceSetsについては第23章 Javaプラグインに記述があるのだが、「23.7. ソースセットの利用」にこうある。「プロジェクトのソースセットには、sourceSetsプロパティを使ってアクセスすることができます。 これはプロジェクトのソースセットのコンテナで、SourceSetContainer型です。 また、sourceSets()というメソッドもあり、クロージャを渡してソースセットコンテナを設定することができます。 ソースセットコンテナは、tasksのような他のコンテナと同じように働きます。 」

applyという名前には、クロージャ引数のメソッドと、それ以外のメソッドがあったのだが、sourceSetsという名前のものには、プロパティとメソッドと二通りがある。メソッドの方はクロージャを受取る。上記で書いているのはそちらの方である。

しかし、このページからは、クロージャの中で何が書けるのかの記述はない。プロパティの方しか説明されていないからだ。これではマニュアルになっていないと思うのだが。。。

sourceSetsのクロージャ引数の中で何が書けるのか?

これに対し、Projectの方を見てみると、こうある。

sourceSets { }

Configures the source sets of this project.
The given closure is executed to configure the SourceSetContainer. 
The SourceSetContainer is passed to the closure as its delegate.
See the example below how SourceSet 'main' is accessed and 
how the SourceDirectorySet 'java' is configured to exclude some package from compilation.

apply plugin: 'java'
sourceSets {
  main {
    java {
      exclude 'some/unwanted/package/**'
    }
  }
}

Delegates to:
    SourceSetContainer from sourceSets

つまり、このクロージャの中では、delegateとしてSourceSetContainerが与えられているので、そのメンバーであるmainにアクセスできるということらしい。delegateについてはクロージャが暗黙的に持つ delegate 変数が詳しい。

sourceSetsのmainメソッドは、どこで説明されているのか?

そこで、インタフェース SourceSetContainerを見てみるのだが、どこにもmainなどという言葉は無い。

そして、mainにはクロージャが与えられているので、これもまたメソッドのはずである(もちろん、同時にプロパティであってもよい)。

このQ&Aがあった、当然の疑問だろう。Where is the ‘main’ method of ‘sourceSets’ defined?、こういう答え、

sourceSetsは名前付ソースセットのコンテナだよ。javaプラグインがmainという名前(もう一つはtest)という名前をコンテナに入れるんだ。
だから、mainという名前の物理的なメソッドやプロパティは無い。sourceSets.main { … } は、sourceSets.getByName(“main”) { … }とも書けるよ。

ということはつまり、本当は、

sourceSets {
  main {
    java {
      srcDir 'src';
    }
  }
}

ではなくて、

sourceSets {
  'main' {
    java {
      srcDir 'src';
    }
  }
}

ということなのだが、こう書いてしまっても通ってしまう。正直訳がわからない。

本来mainというのは、メソッド名称でなくてはいけないはずなのに、ここでは単に文字列なのである。

新たなソースセットの定義

さらに、よくgradleの本やマニュアルで説明されているのは、mainやtest以外の新たなソースセットの定義方法である。単純にその名前を書くだけなのだ。

sourceSets {
  sample {
    java {
      srcDir 'src';
    }
  }
}

これももちろん、以下のように書けてしまう。

sourceSets {
  'sample' {
    java {
      srcDir 'src';
    }
  }
}

つまり、ここで行われていることは、こういうことだ。

  • 識別子でも文字列でもいい局面がある。どちらでも同じで、文字列として解釈される。
  • その文字列が既に登録されていれば、それに紐付いたオブジェクトのメソッドにクロージャが引数として与えられる。
  • その文字列が未登録であれば、新規作成して登録して、上記を行う。

このことは、マニュアルを見ても、市販書籍を見ても何も記述がなく、普通のプログラマは混乱してしまうことになる。

task名に未定義の変数が使える

task名の不可解現象

前述の不可解現象は他でも現れる。例えば、以下を書く

apply { plugin 'java' plugin 'eclipse' }

sourceSets {
  main {
    java {
      srcDir 'src';
    }
  }
}

task sample {
  println "hello, world"
}

EGradleのクイックラウンチを使って実行する。つまり、Ctrl+Shift+Alt+Endの後でsample+ENTERとする。
すると、「hello, world」が表示される。これは単に「gradle sample」を実行するのと同じである。

1.次に、以下のように書き換えてみる。

task 'sample' {
  println "hello, world"
}

2.もちろん、以下でもよい。最後の引数が

task 'sample', {
  println "hello, world"
}

3.あるいは、何をしているかを明確にするためには、以下の方がわかりやすい。

task ('sample', {
  println "hello, world"
})

最後の引数がクロージャの場合は直前のカンマが不要なために、1.のように書けてしまう

これらでも普通に実行できてしまうのである。つまり、taskというメソッドは、第一引数にタスク名、第二引数(とは限らないのだが)がクロージャなのだが、第一引数は文字列でなく、識別子でも良いのである。意味上は文字列であることが正しいのだが、それでは書きづらいので識別子を書けるだけなのである。

task定義の謎

つまり、巷に流布している例というのは、本来の意味を想像もできないような形に変更されてしまった後の形であり、まともなプログラマであればあるほどわかりにくい形になってしまっているのだ。

こちらの人もそれに疑問を持ったようだ、【おまけ】タスク名に未定義の変数?が使用されている点について。この議論を追っかけてみると、以下を見つけた。

これはtask定義におけるgradleの構文を理解するとして翻訳した。

Gradleの構文はGroovy構文ではない

つまり、Gradleの構文はGroovyの構文ではないのである。
おそらくだが、Groovyの構文でも「文字列でも識別子でもどちらでも良い」などという構文は存在しないだろう(使ったことが無いのでわからないが)。しかしGradleでは簡単な記述のために、このような構文が多用されているのだ。悪く言えばご都合主義なのである。

これが、初学者の敷居が高くなる理由である。通常のプログラミング言語の概念とは異なるのだ(おそらくは。他に見たことが無いので)。

sourceSets再訪

さて、sourceSetsの謎は解けた(ように思える)。次は、mainとして現れるSourceSetである。

apply { plugin 'java' plugin 'eclipse' }

sourceSets {
  main {
    java {
      srcDir 'src';
    }
  }
}

このコードを見てみると、SourceSetのjavaというメソッドを呼び出し、そこに引数 { srcDir ‘src’; }を指定している。
SourceSetを見てみると、たしかにクロージャを引数とするjavaというメソッドがある。

SourceSet java(Closure configureClosure)
  Configures the Java source for this set.

このクロージャにdelegateされるものに、srcDirがあるに違いない。しかし、
SourceSetには記述が無い。
このような場合には、何がdelegateされているかを表示させてしまう。

apply { plugin 'java' plugin 'eclipse' }

sourceSets {
  main {
    java {
      println 'delegate ' + delegate.class
      srcDir 'src';
    }
  }
}

すると、org.gradle.api.internal.file.DefaultSourceDirectorySetが表示される。しかし、これはSourceDirectorySetの実装で、SourceDirectorySetを調べてみると、第23章 Javaプラグインの中の、「23.7.1. ソースセットプロパティ」のjavaが見つかる。

なんのことはない、メソッドjavaの引数クロージャに与えられるdelegateは、プロパティjavaそのものなのである。

そして、SourceDirectorySetを見てみると、たしかにsrcDirというオブジェクトを引数とするメソッドがあり、こうある。

SourceDirectorySet srcDir(Object srcPath)
Adds the given source directory to this set.
パラメータ:
    srcPath - The source directory. This is evaluated as per Project.file(Object)
戻り値:
    this 

つまり、文字列にかぎらず、file()メソッドで解釈できるものであれば何でもよいということ。

まとめ

つまり、Gradle言語の「理解上」の問題点は大きく二つある。

  • 誰も正確にその構文を説明できていない。
  • マニュアルもめちゃくちゃで、必要な情報を探し当てるのに苦労する。

書籍や普通にウェブサイトを探しても、ここまでのことはほぼ書いていない。
プログラマであれば、ごく普通に疑問を持つようなことなのだが、誰も疑問に思わないのだろうか?
基本的なことなのに、全く放置されているのは何か理由があるのだろうか?
これは何とかならないのだろうか?これではわかりにくくて仕方がない。