Gradle:スコープとプロパティ

Gradleのスコープの仕組みはとてもわかりにくい。「アクセスできるだろう」と思っても、なぜかアクセスできなかったり、できないと思ってもアクセスできたりする。

これは、Gradleでのアクセス可能性がプログラミング言語としてのスコープだけで決まるわけではなく、その場所でアクセス可能なドメインオブジェクトと、そのプロパティによるかららしい。

つまり、字面上の「スコープ」だけではなく、「その場所の文脈」にもよるのである。

しかし、初心者にとっては、「その場所がどんな文脈なのか」がわかりにくく、通常のプログラミング言語での「スコープ」の見方しかできないため、「なぜアクセスできないのか?」「なぜアクセスできるのか?」が全くわからなくなる。

apply fromの罠

この混乱は特にapply fromを使って、他のスクリプトの内容を利用したい場合に起こる。つまり、あちこちにbuild.gradleがあり、その共通処理を統一的な一つのスクリプトファイルにまとめ、これをそれらのbuild.gradleで利用したいのである。

ものの本には、「apply from: ‘…’」とすると、「あたかも’…’の内容がそこにペーストされたかのように動作する」とあるのだが、これは完全な間違いである

ペーストされたようには全く動作しない。以下で説明する。

※この稿では、apply fromについてだけ取り上げており、buildSrcについては一切考慮しない。これを「あらゆるプロジェクトに共通するもの」として自由な場所に設置する方法が無いからだ。必ずそのプロジェクトフォルダの直下に設置する必要がある。これは要望として出ているようだが、未だに解決されていないらしい。Customize location of buildSrc

なぜかtaskだけはアクセスできる例

次のような例を見てみる。単純にクラス、定数、関数、タスクを定義する。

sample.gradle

public class Class1 {
  public static void show() {
    println "class1 show"
  }
}

def const1 = "CONST1"

def func1() {
  println "func1"
}

task task1()  { doLast {
  println "task1 show"
}}

これをbuild.gradleから利用する。

build.gradle

func1()
println const1
Class1.show();

どれも失敗する。sample.gradleの中身は、このスクリプトファイルでローカルに定義されているらしく、build.gradleから参照することはできない。

しかし、task1は利用可能なのである。

gradle task1
...
:task1
task1 show

なぜタスクは実行可能なのか?

これはtaskというものが、言語上の「定義」ではなく、プロジェクトへの機能追加になっているかららしい。

あまり良くわかっていないのだが、こういうことだ。

  • Gradleはすべてのスクリプト実行前にプロジェクトオブジェクトを用意する。
  • build.gradleを読み込み、そこからsample.gradleを読み込む
  • sanple.gradleを読み込んだときにtaskというメソッドが実行され、プロジェクトにタスク機能を追加する。

これに対し、Class1、func1、const1は言語上の機能なので、特にプロジェクトに何か追加することはなく、またbuild.gradleとは別のスクリプトなので、参照することもできない。という具合のようだ。

実際に以下でタスクが追加されている様子がわかる。

build.gradle

apply from: 'sample.gradle'

tasks.each { println "-->" + it }

gradle task1とすると

-->task ':task1'
:task1
task1 show

他スクリプトファイルのクラス、定数、関数を実行するには?

以上から、タスクを除き、別スクリプトファイルにて定義された、クラス、定数、関数はそのファイル内でローカルにしか使えず、本体側からは見えないものになっている。これは言語仕様上の制限と思われる。

これを解消するには、先のClass1, const1, func1,という名前をプロジェクトのプロパティとして持ち込む必要がある(が、結局func1は持ち込めないのだが)。以下のようにする。

sample.gradle

public class Class1 {
  public static void show() {
    println "class1 show"
  }
}

def const1 = "CONST1"

def func1() {
  println "func1"
}

task task1()  { doLast {
  println "task1 show"
}}


ext.Class1 = Class1
ext.const1 = const1
//ext.func1 = func1 これはできないようだ。エラーになる

build.gradle

apply from: 'sample.gradle'

println const1
Class1.show()

実行結果は以下だ

CONST1
class1 show

このextというのは実際にはprojectのext、つまりproject.extなのだが、ここでプロパティを定義すれば、そのプロパティはプロパティ全体から見えるようになるということらしい。

つまり、奇妙なことに「ext.Class1=Class1」とすると、Class1としてアクセスできるのだが、明示的にextをつけてもよい。結果は全く同じだ。

build.gradle

apply from: 'sample.gradle'

println ext.const1
ext.Class1.show()

このインタフェースの定義は以下になる。

Interface ExtraPropertiesExtension

つまり、言語仕様的には、別スクリプトで定義されたものをアクセスする術はないのだが、実行時に共通して存在する「プロジェクト」オブジェクトに格納しておいてやれば、アクセスが可能になるということだ。

これを見ればわかるように、build.gradleにClass1を記述した場合、それはsample.gradleに記述されたClass1と言語仕様的に同じものではない。前者はドメインオブジェクト中のプロパティへの参照であり、もともとの後者は言語仕様としての識別子になる。したがって、言語仕様的に同じスコープを持つものではなくなる。

特にクラス内部からのアクセス

次に以下のような例を考えてみる

sample.gradle

public class Class1 {
  public static void show() {
    println "class1 show"
  }
}
ext.Class1 = Class1

build.gradle

apply from: 'sample.gradle'

class Class2 {
  static void show() {
  }
}

class Class3 {
  static void show() {
    //Class1.show() アクセス不可
    Class2.show()
  }
}

Class1.show()
Class3.show()

Class1をextに入れてアクセスできるようにしたのだが、これは言語仕様的にアクセスできるわけではなく、プロジェクトプロパティとしてアクセス可能にしただけだ。

したがって、プロジェクトプロパティにアクセスできる箇所であれば、Class1.show()を参照できるが、そうでない場所、Class3のメソッド内ではアクセスができない。

その一方で、Class3のメソッド内からは、同じスクリプトファイルにあるClass2に言語仕様的にアクセスが可能だ。

他スクリプトに存在するクラスを、どうやってこちらのクラスからアクセス可能にするのだろうか?

これが問題だ。sample.gradleのClass1はextに入れることによっておおよそアクセス可能になるのだが、クラスからはアクセスできない。

これをどうやれば良いのだろうか?

例えば、Getting access to class declared in a file applied with ‘apply from’ in gradleというQ&Aがあるが、同じことだ。

スクリプト自体からはアクセス可能だが、クラスからは不可能である。あるいはbuildSrcを使えということだが、buildSrcを使ってこの問題が解決するのかは不明だ。

結局のところ、クラスのコンストラクタなりメソッドなりの引数としてプロジェクトを渡してやる以外にはなさそうだ。

この問題の背景

この問題の背景としては、Gradle:gradle.propertiesの値をどこでも利用できるようにするに記述したことである。

つまり、gradle.propertiesの値を、分離したスクリプトでいったんGlobalsというクラスのstatic変数に移し替え、ext.Globals = Globalsとし、本体スクリプトで利用しようとしたのだが、移し替えを行ったスクリプトの他のクラスから利用可能ではあるものの、本体側のクラスからは参照できないのである。

結局のところ、どうやっても以下ができない。いくら探しまくっても何の答えも見つからない。

  • 別スクリプトファイルのクラスをこちらのクラスから(引数受け渡しなどなしに)アクセスする方法。
  • どこででもアクセス可能な、真の意味での完全にグローバルな識別子を定義する方法。

何かやり方があるのだろうか???