197
169

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Gradle のタスク定義のあれこれ

Posted at

Gradle のタスクについて、基礎から応用までいろいろメモ。

前提知識

環境

Gradle

5.0

Java

openjdk 11.0.1

OS

Windows 10

タスクの実体

Gradle でタスクを定義するには、次のように記述する。

build.gradle
task foo

この foo タスクの定義は、 Projecttask(String) メソッドを呼び出している。

task() メソッドは、生成されたタスクを表すオブジェクトを返すので、次のようにして foo タスクのオブジェクトを取得できる。

build.gradle
def foo = task foo
println(foo)
println(foo.class)
実行結果
> gradle foo

task ':foo'
class org.gradle.api.DefaultTask_Decorated

Gradle のタスクの実体は Task のオブジェクトで、単純に task() で生成した場合は DefaultTask のオブジェクトが生成される。

Action

タスクは、内部に Action のリストを保持している。
この Action は、そのタスクで実行する具体的な処理を持っている。
そしてタスクを起動すると、リストの先頭から順番に Action が実行されていく。

Action は、 Task が持つ doFirst(), doLast() メソッドでリストの先頭と末尾のいずれかに追加できる。

build.gradle
def foo = task foo

foo.doFirst {
    println "two"
}
foo.doFirst {
    println "one"
}
foo.doLast {
    println "three"
}
foo.doLast {
    println "four"
}
実行結果
> gradle foo

one
two
three
four

doFirst() はリストの先頭に Action を追加し、 doLast() は末尾に Action を追加する。

上記例では doFirst() を二回実行している。
この場合、あとで実行した "one" を出力する Action のほうがリストの先頭に挿入されることになるので、 "one" -> "two" の順番で出力されている。

設定ブロックで定義する

build.gradle
task foo {
    println(delegate)
    println(delegate.class)

    doFirst {
        println("FOO!!")
    }
}
実行結果
> gradle foo

> Configure project :
task ':foo'
class org.gradle.api.DefaultTask_Decorated

> Task :foo
FOO!!

task() メソッドでタスクを定義するときに、第二引数にクロージャを渡すことができる
このクロージャの delegate は、作成されたタスクオブジェクトとなっている。
したがって、クロージャ内では作成したタスクのメソッドやプロパティに暗黙的にアクセスできるようになっている。

ビルドライフサイクル

Gradle の実行は、大きく3つのフェーズに分かれている。

フェーズ 内容
初期化フェーズ
(Initialization)
プロジェクトがシングルかマルチかなどを判定し、 Project のオブジェクトを生成する。
設定フェーズ
(Configuration)
ビルドスクリプトを実行し、 project オブジェクトを構築していく。
実行フェーズ
(Execution)
コマンドラインで指定されたタスクを実際に実行する。

具体的に build.gradle の中でいうと、タスクに設定している Action の中が実行フェーズで実行され、それ以外は設定フェーズで実行される。

build.gradle
println("設定フェーズ1")

task foo {
    println("設定フェーズ2")
    doFirst {
        println("実行フェーズ1")
    }
    
    println("設定フェーズ3")
    doLast {
        println("実行フェーズ2")
    }
}

task bar {
    println("設定フェーズ4")
}

println("設定フェーズ5")
実行結果
> gradle foo

> Configure project :
設定フェーズ1
設定フェーズ2
設定フェーズ3
設定フェーズ4
設定フェーズ5

> Task :foo
実行フェーズ1
実行フェーズ2

設定フェーズで実行される部分は、指定しているタスクにかかわらず常に実行される。
したがって、設定フェーズで実行される部分に間違ってタスクの処理を書いてしまうと、タスクを指定していないのに処理が実行されるという状態になってしまうので注意しなければならない。

タスクの参照

build.gradle
task foo {
    doFirst {
        println foo
        println project.foo
        println tasks.foo
        println tasks.getByName("foo")
        println tasks["foo"] // getAt() メソッドのシンタックスシュガー
    }
}
実行結果
task ':foo'
task ':foo'
task ':foo'
task ':foo'
task ':foo'

プロジェクトにタスクを定義すると、そのプロジェクト内ではタスク名だけでタスクを参照できるようになる。
これは、 Project オブジェクトのプロパティに、作成されたタスクオブジェクトが追加されることで実現している。

生成されたタスクのオブジェクトは Project が持つ TaskContainer に登録されている。
TaskContainerProjecttasks プロパティから参照でき、 TaskContainer からはプロパティアクセスや getByName(), getAt() メソッドを使ってタスクを参照できる。

まれに、タスク名と同じ名前の拡張オブジェクトを追加するプラグインが存在する。
例えば、 Eclipse Plugineclipse という名前でタスク拡張オブジェクトの2つを定義する。
この場合、 project.eclipse で参照したオブジェクトは拡張オブジェクトの方になる。
タスクを参照したい場合は、 tasks.eclipse のように TaskContainer を経由する必要がある。

なお、 getByName() のような文字列で指定するメソッドは、 :someSubProject:fooTask のようにして他のプロジェクトのタスクを参照することもできる。

タスクの依存関係を定義する

build.gradle
task foo {
    doFirst {
        println("foo")
    }
}

task bar {
    doFirst {
        println("bar")
    }
}

foo.dependsOn bar
実行結果
> gradle foo
bar
foo

Task の [dependsOn()] (https://docs.gradle.org/current/dsl/org.gradle.api.Task.html#org.gradle.api.Task:dependsOn(java.lang.Object[])) メソッドを使用することで、タスクの依存関係を定義できる。

ここでは、 foo タスクが bar タスクに依存するように定義している。
これにより、 foo タスクを実行すると必ず bar タスクが先に実行されるようになる。

Gradle のタスクの実行順序は、基本はこの依存関係の定義を利用して制御する(依存関係以外の制御方法も用意されている)。

タスクは一度だけ実行される

Gradle は、コマンドラインで指定されたタスクを実行する前に、依存するタスクを調査する。
そして、依存されている側から順番にタスクを実行していく。

このとき、たとえ複数のタスクから依存されているタスクが存在しても、全てのタスクは必ず一度だけしか実行されないように制御されている。

build.gradle
task foo {
    doFirst { println("foo") }
}

task bar {
    doFirst { println("bar") }
}

task hoge {
    doFirst { println("hoge") }
}

foo.dependsOn bar
hoge.dependsOn foo
hoge.dependsOn bar
実行結果
> gradle hoge
bar
foo
hoge

タスク hoge は、 foo, bar の両方に依存している。
foobar に依存しているので、単純にタスクの依存関係を図にすると次のようになる。

依存関係を図で表現
hoge -> foo -> bar
 |              ^
 +--------------+

単純に依存するタスクを全て実行していくと、 bar が二回実行されてしまう。
しかし、 Gradle は前述したように複数のタスクに依存されていても一度だけしか実行されないように制御している。

dependsOn を書ける場所と注意点

dependsOnTask のメソッドなので、次のように設定ブロックの中で書くこともできる。

build.gradle
task foo {
    dependsOn "bar"
    doFirst {
        println("foo")
    }
}

task bar {
    doFirst {
        println("bar")
    }
}

ここでは、 bar タスクの指定を文字列にしている。
もしこれを、文字列ではなくプロパティ参照にすると、次のようなエラーになる。

build.gradle
task foo {
    dependsOn bar // ★プロパティ参照に置き換え
    doFirst {
        println("foo")
    }
}

task bar {
    doFirst {
        println("bar")
    }
}
実行結果
> gradle foo
...
> Could not get unknown property 'bar' for task ':foo' of type org.gradle.api.DefaultTask.
...

BUILD FAILED in 4s

bar というプロパティが見つからずにエラーになる。

dependsOn bar を書いている部分は、ビルドライフサイクルにおける「設定フェーズ」で実行される部分になる。
設定フェーズでは、ビルドスクリプトが上から順番に実行されていく。
dependsOn bar が実行される時点では、まだ task bar が実行されていないので、タスク bar は未定義ということになる。
したがって、 bar タスクの定義より上にある foo タスクの設定ブロック内では、 bar タスクをプロパティ参照でアクセスすることはできない。

この問題は、次のいずれかの方法で回避できる。

  • 前述のように、文字列指定で定義する
  • bar タスクの定義を foo タスクの定義より上に持っていく

記述の順序に依存すると変更に脆くなるので、個人的には文字列指定が良い気がする。

task() メソッドの引数で指定する

build.gradle
task foo(dependsOn: "bar") {
    doFirst {
        println("foo")
    }
}

task bar {
    doFirst {
        println("bar")
    }
}

Map を受け取る task() メソッドを使えば、引数の Map で依存するタスクを指定することもできる。

実行順序を強制する

build.gradle
apply plugin: "java"

clean.doFirst { println "clean" }
build.doFirst { println "build" }

task cleanBuild(dependsOn: [clean, build])
実行結果
> gradle cleanBuild
build
clean

cleanBuild タスクの依存タスクに [clean, build] と指定している。
しかし、 cleanBuild タスクを実行すると build -> clean の順番でタスクが実行されている。

このように、単純に dependsOn に指定しただけでは、タスクの実行順序は定まらない(名前順?)。

clean -> build の順で実行されるように制御するにはどうすればいいか?
まず単純に思いつくのは、 dependsOnbuildclean に依存するように定義する方法。

build.gradle
apply plugin: "java"

clean.doFirst { println "clean" }
build.doFirst { println "build" }
build.dependsOn clean

task cleanBuild(dependsOn: [clean, build])
実行結果
> gradle cleanBuild
clean
build

これで、 clean -> build の順序でタスクが実行されるようになった。

しかし、この方法だと build だけを実行することができなくなる(常に clean されてしまう)。

buildだけ指定して実行
> gradle build
clean
build

できれば、 build 単独で実行したときは、 clean は実行しないようにしたい。

このような、同時に指定された場合は順序を限定したいが、単独でも実行できるようにしておきたい場合は、 mustRunAfter(Object...) を使用する。

build.gradle
apply plugin: "java"

clean.doFirst { println "clean" }
build.doFirst { println "build" }
build.mustRunAfter clean

task cleanBuild(dependsOn: [clean, build])
実行結果
> gradle cleanBuild
clean
build

> gradle build
build

taskA.mustRunAfter(taskB) とすることで、 taskAtaskB が同時に実行されたときの順序を taskB -> taskA となるように強制できる。

後処理のタスクを指定する

build.gradle
task foo {
    doFirst { println "foo" }
    finalizedBy "finalizer"
}

task bar {
    doFirst { 
        println "bar"
        throw new Exception("test")
    }
    finalizedBy "finalizer"
}

task finalizer {
    doFirst { println "finalizer" }
}
実行結果
> gradle foo
foo
finalizer

> gradle bar
bar
finalizer

...

Execution failed for task ':bar'.
> java.lang.Exception: test

...

BUILD FAILED in 3s

finalizedBy() で設定したタスクは、そのタスクが正常終了したか異常終了したかにかかわらず、必ず実行される。

リソースの開放のような、必ず実行しないといけない処理がある場合に指定する。

実行する条件を指定する

build.gradle
task foo {
    doFirst { println "foo" }
    onlyIf { project.hasProperty("hoge") }
}
実行結果
> gradle foo
【何も出力されない】

> gradle foo -Phoge
foo

onlyIf() を使うと、そのタスクのアクションを実行する条件を指定できる。

引数で渡すクロージャが true を返したときにだけ、そのタスクのアクションが実行される。
クロージャが false を返した場合は、そのタスクのアクションはスキップされる。

ただし、スキップされるのはあくまでそのタスクのみで、依存するタスクや finalizedBy で指定したタスクは実行される。

build.gradle
task foo {
    doFirst { println "foo" }
    onlyIf { project.hasProperty("hoge") }
    dependsOn "bar"
    finalizedBy "finalizer"
}

task bar {
    doFirst { println "bar" }
}

task finalizer {
    doFirst { println "finalizer" }
}
実行結果
> gradle foo
bar
finalizer

foo の実行はスキップされているが、依存する bar タスクやファイナライザに指定している finalizer タスクは実行されている。

ファイルの選択

Gradle にはファイルやディレクトリを選択するための柔軟な API が Project クラスに用意されている。
タスクの設定やアクションを記述するときは、この API をうまく利用することでファイルの操作が簡潔に書けるようになる。

file(Object)

build.gradle
import java.nio.file.Path

task foo {
    doFirst {
        File file1 = file("foo.txt")
        println(file1)

        File file2 = file(new File("foo.txt"))
        println(file2)

        File file3 = file(Path.of("foo.txt"))
        println(file3)
    }
}
実行結果
> gradle foo
F:\etc\...\foo.txt
F:\etc\...\foo.txt
F:\etc\...\foo.txt

※パスは省略して記載しているだけで、実際はフルパスが出力されている。

file() メソッドは、引数で受け取った値を解析して File オブジェクトを返す。

文字列のパスから FilePath など、様々な入力に対応している(詳しくは API ドキュメントを参照)。

Gradle で任意の入力を良しなに File オブジェクトに変換したい場合は、この file() メソッドを使うとだいたいうまくいく。

files(Object...)

build.gradle
task foo {
    doFirst {
        FileCollection files = files("foo.txt", "bar.txt")
        files.each { println it }
    }
}
実行結果
> gradle foo
F:\etc\...\foo.txt
F:\etc\...\bar.txt

files() メソッドは、複数の File をまとめたコレクションである FileCollection を生成する。
引数は可変長引数になっており、 file() で指定できる値は同様に指定できる。
それ以外にも、 IterableCollection などを渡すこともできる。

とにかく、こちらもたいていの値を良しなに解析して FileCollection にしてくれる。

FileCollection には次のようなメソッドが用意されている。

getAsPath()

build.gradle
        FileCollection files = files("foo.txt", "bar.txt")
        println "asPath = ${files.asPath}"
実行結果
asPath = F:\etc\...\foo.txt;F:\etc\...\bar.txt

各パスをプラットフォームごとの区切り文字で連結した文字列を取得する。
区切り文字は、例えば Linux なら : で、 Windows なら ; になる。

クラスパスやモジュールパスのような、ファイルを複数指定するオプションなどで利用できる。

filter(Closure)

build.gradle
        FileCollection files = files("foo.txt", "bar.txt", "fizz.txt", "buzz.txt")
        FileCollection filtered = files.filter { it.name.startsWith("b") }
        filtered.each { println it }
実行結果
F:\etc\...\bar.txt
F:\etc\...\buzz.txt

filter() に渡したクロージャが true を返した File のみに要素が絞られた FileCollection を返す。

fileTree(Object)

build.gradle
task foo {
    doFirst {
        FileTree tree = fileTree("fromDir")
        tree.each { println it }
    }
}
実行結果
> gradle foo
F:\etc\...\fromDir\hoge.txt
F:\etc\...\fromDir\hoge.xml
F:\etc\...\fromDir\sub\foo.txt
F:\etc\...\fromDir\sub\foo.xml

fileTree() メソッドを使うと、指定したディレクトリ以下のファイルを再帰的に保持した FileTree を取得できる。
FileTreeFileCollection を継承しているので、 FileCollection と同様の操作ができる。

fileTree(Object, Closure)

build.gradle
task foo {
    doFirst {
        FileTree tree = fileTree("fromDir") {
            include "**/*.xml"
        }
        tree.each { println it }
    }
}
実行結果
> gradle foo
F:\etc\...\fromDir\hoge.xml
F:\etc\...\fromDir\sub\foo.xml

fileTree() は、第二引数にクロージャを渡すことができる。

クロージャの delegate は、第一引数で指定したディレクトリの ConfigurableFileTree オブジェクトになっている。
ConfigurableFileTree は、名前の通り設定可能な FileTree で、 include()exclude() といったファイルの絞り込みができるようになっている。

絞り込みのパターンは Ant 形式1で指定できる。

fileTree(Map)

build.gradle
task foo {
    doFirst {
        FileTree tree = fileTree(dir: "fromDir", include: "**/*.xml")
        tree.each { println it }
    }
}
実行結果
> gradle foo
F:\etc\...\fromDir\hoge.xml
F:\etc\...\fromDir\sub\foo.xml

引数に Map を受け取る fileTree() では、 ConfigurableFileTree のプロパティに設定する値を Map で指定できる。

ファイル関係のクラス図

あらためて、ファイル操作まわりのクラスの関係を確認する。

gradle.png

FileCollectionFile のコレクション。
FileTree は、特定のディレクトリ以下の File を再帰的に保持したコレクション。

それぞれを継承した、 ConfigurableFileCollectionConfigurableFileTree があり、対象の File を絞り込むような設定ができるようになっている。

タスクの入出力

build.gradle
task foo {
    doFirst {
        println "foo"
        file("foo.txt").text = project.hoge
    }
}

タスク foo は、 projecthoge プロパティに設定された値を foo.txt に出力するタスクとなっている。
このタスクを何度か実行してみる。

実行結果
> gradle foo -Phoge=HOGE
> Task :foo
foo

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

> gradle foo -Phoge=HOGE
> Task :foo
foo

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

当然のことながら、タスクを実行するとタスクのアクションが実行され、ファイル出力が行われる。

しかし、入力が変わっていない以上、このタスクは何度実行しても同じ結果しか出力しない。
出力が変わらないことがわかっているのに毎回実行するのは無駄が大きい。

もしこのタスクが時間のかかるタスクだった場合、ビルド時間に大きな影響を与える可能性がある。

Gradle では、タスクの結果が変わらないのであれば、そのタスクの実行をスキップする仕組みが用意されている。
この仕組みを利用するためには、タスクの入出力を定義する必要がある。

build.gradle
task foo {
    def outputFile = file("foo.txt")

    inputs.property "hoge", project.hoge
    outputs.file outputFile

    doFirst {
        println "foo"
        outputFile.text = hoge
    }
}
実行結果
> gradle foo -Phoge=HOGE
> Task :foo
foo

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

> gradle foo -Phoge=HOGE
> Task :foo UP-TO-DATE

BUILD SUCCESSFUL in 3s
1 actionable task: 1 up-to-date

> gradle foo -Phoge=HOGEHOGE
> Task :foo
foo

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

hoge の値を一回目と同じ値にして実行した時、 foo タスクの表示の右に UP-TO-DATE (最新) と出力され、タスクの実行がスキップされている。
hoge の値を変更すると、再び foo タスクは実行された。

build.gradle
task foo {
    ...
    inputs.property "hoge", project.hoge
    outputs.file outputFile
    ...
}

タスクの入出力を定義するには、まず Task に定義されている次の2つのプロパティを使って入出力を定義するためのオブジェクトにアクセスする。

それぞれのプロパティは、 TaskInputsTaskOutputs を返す。
これらのクラスに用意されているメソッドを使って、具体的な入出力を定義していく。

Gradle は、タスクが実行されたときに入出力の内容を記録している。
そして、次に同じタスクが実行されたときに、入出力の内容が変化していないかをチェックする2
もし入出力が変化していない場合、 Gradle はそのタスクの実行をスキップする。
入出力に指定した値が1つでも変化している場合は、タスクは通常どおり実行される。

入出力は、両方が定義されていなければならない。
片方だけ(入力だけ、出力だけ)の指定では、このスキップの仕組みは動かない。

入力の定義

入力には、大きく次の3つを利用できる。

プロパティは、前述の例で示したように、任意のキーと値を指定できる。
ただし、値はシリアライズ可能で、かつ equals() で比較できなければならない。

ファイルを指定した場合は、ファイルの内容が変更されたかどうかがチェックされる。

ディレクトリを指定した場合は、そのディレクトリ以下の状態がチェックされる。
ディレクトリの中にファイルやディレクトリが作られたり、ファイルの内容が変更されると、入力が変化したと判断される。
この判定は、サブディレクトリ以下も再帰的に対象になる。

build.gradle
task foo {
    ...
    inputs.file file("input.txt") // 入力ファイルの指定
    inputs.dir file("inputDir")   // 入力ディレクトリの指定
    ...
}

出力の定義

出力は、次の2つを利用できる。

build.gradle
task foo {
    ...
    outputs.file file("output.txt") // 出力ファイルの指定
    outputs.dir file("outputDir")   // 出力ディレクトリの指定
    ...
}

タスクルール

build.gradle
tasks.addRule("Pattern: echo<MESSAGE>") { taskName ->
    if (taskName.startsWith("echo")) {
        task(taskName) {
            doFirst { println(taskName - "echo") }
        }
    }
}
実行結果
> gradle echoHoge
Hoge

> gradle tasks
...

Rules
-----
Pattern: echo<MESSAGE>

TaskContaineraddRule() メソッドを使うと、タスクルールというものを定義できる。

addRule() は、第一引数にタスクルールの説明を渡し、第二引数にクロージャを渡す。
クロージャには、参照しようとしたタスクの名前が渡ってくる(ここでは、コマンドラインで指定した "echoHoge" という文字列が渡ってくる)。

このクロージャの中で受け取った名前でタスクを定義すれば、その名前のタスクが事前に定義されていなくてもエラーにはならず、動的に定義されたタスクが採用される。
つまり、あらかじめ静的にタスク名を定義できないようなタスクでも、タスク名にパターン(ルール)を設けることで、実行時に動的にタスクを定義できる。

このタスクルールで定義されるルールは、 dependsOn などでも参照できる。

build.gradle
tasks.addRule("Pattern: echo<MESSAGE>") { taskName ->
    if (taskName.startsWith("echo")) {
        task(taskName) {
            doFirst { println(taskName - "echo") }
        }
    }
}

task foo(dependsOn: echoBar) {
    doFirst { println "foo" }
}
実行結果
> gradle foo
Bar
foo

ここでは、 foo タスクの dependsOnechoBar と指定している。
ここでもタスクルールが適用され、 echoBar タスクが動的に生成されている。

標準で用意されている clean ルール

build.gradle
apply plugin: "base"
実行結果
> gradle tasks
...

Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.

base プラグインを読み込むと、 clean ルールが適用されるようになる。
(このプラグインは、 Java プラグインなどを適用すれば一緒に読み込まれるので、明示的に読み込むことは基本的にない)

このルールは、任意のタスクの出力ファイルを削除するタスクを生成する。

build.gradle
apply plugin: "base"

task foo {
    def outputFile = file("foo.txt")
    outputs.file outputFile

    doFirst {
        outputFile.text = "foo!!"
    }
}

ここでは foo というタスクを定義している。
このタスクは、 foo.txt というファイルを出力ファイルとして定義している。

実行結果
> type foo.txt
指定されたファイルが見つかりません。

> gradle foo
...
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

> type foo.txt
foo!!

> gradle cleanFoo
...
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

> type foo.txt
指定されたファイルが見つかりません。

cleanFoo というタスクは clean ルールによって動的に生成され、 foo タスクの出力ファイルを削除するように定義されている。

カスタムタスク

task() で生成するタスクは、そのプロジェクトでしか利用できない。
また、同じ処理をパラメータを変えて複数の場所で再利用する、といったこともできない。

複数のプロジェクトでパラメータだけを変えて同じタスクを再利用したいような場合は、カスタムのタスクを作成する。

build.gradle
class FooTask extends DefaultTask {
    @TaskAction
    def foo() {
        println("FOO!!")
    }
}

task foo(type: FooTask)
実行結果
> gradle foo
FOO!!

カスタムタスクは、 DefaultTask を継承して作成する。
任意のメソッドを定義して、 @TaskAction でアノテートすると、そのメソッドがタスクのアクションとして実行される。

作成したカスタムタスクを利用するには、 Map を受け取る task() を使用する。
この引数の Map で、 type に作成したいタスクの型を指定する。
すると、指定した型でタスクのオブジェクトが生成されるようになる。

ここでは説明を簡単にするために同じビルドスクリプト内でカスタムタスクを定義している。
しかし、実際は複数のプロジェクトから参照できる外部の場所(後述する buildSrc や、外部の jar ファイルなど)に定義することになる。

カスタムタスクの設定

build.gradle
class FooTask extends DefaultTask {

    String message

    @TaskAction
    def foo() {
        println("FOO!! message=${message}")
    }
}

task foo(type: FooTask) {
    message = "Hello World"
}
実行結果
> gradle foo
FOO!! message=Hello World

type で型を指定して生成されたタスクは、その型のオブジェクトになっている。
つまり、カスタムタスクに設定用のプロパティやメソッドを定義しておけば、生成されたタスクオブジェクトにアクセスできる任意の場所でタスクオブジェクトの設定を指定できるようになる。

ここでは、設定ブロックの中で message プロパティに値を設定している。

ファイルの設定値は Object で定義しておく

build.gradle
class FooTask extends DefaultTask {

    List inputFiles = []
    Object output

    @TaskAction
    def foo() {
        def out = project.file(this.output)
        out.text = ""
        project.files(this.inputFiles)
               .each { out.append("${it.name}\n") }
    }
}

task foo (type: FooTask) {
    inputFiles = ["fromDir/hoge.txt", "build.gradle", file(".gitignore")]
    output = file("output/test.txt")
}

タスクの入出力にファイルやディレクトリを定義する場合、型は ObjectList のような何でも入れられる型にしておく。
そして、実際に設定値を利用するときに Projectfile()files() を使って変換する。

こうすることで、文字列や File オブジェクトなど、 Project#file() がサポートする様々な形式でファイルを指定できるようになるので、設定の柔軟性が高まる。

入出力の指定

build.gradle
class FooTask extends DefaultTask {
    @Input
    String message
    @OutputFile
    Object output

    @TaskAction
    def foo() {
        project.file(output).text = message
    }
}

task foo(type: FooTask) {
    message = "Hello World!!"
    output = "foo.txt"
}
実行結果
> gradle foo
...
BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

> type foo.txt
Hello World!!

> gradle foo
...
BUILD SUCCESSFUL in 4s
1 actionable task: 1 up-to-date

カスタムタスクでは、アノテーションでタスクの入出力を定義する方法が用意されている。

次のアノテーションでカスタムタスクのフィールドや Getter メソッドを注釈することで、タスクの入出力を定義できる。

標準で用意されているタスク

よく利用しそうな処理については、標準でタスククラスが用意されている。
例えば、次のようなタスクがある。

Copy

build.gradle
task foo(type: Copy) {
    from "fromDir"
    into "toDir"
}
実行結果
> tree /f fromDir
...
│  hoge.txt
│
└─sub
        foo.txt

> gradle foo
...

> tree /f toDir
│  hoge.txt
│
└─sub
        foo.txt

Copy タスクは、ファイルやディレクトリのコピーを実行する。

コピーの方法の設定には、 Copy が実装している CopySpec が提供している API を使用する。
CopySpec にはコピー対象の絞り込みやコピー先を指定するための様々なメソッドが用意されており、かなり柔軟な設定ができるようになっている。

from(Object...)

build.gradle
task foo(type: Copy) {
    from "fooDir", "barFile"
    from file("fizzDir"), file("buzzFile")
    ...
}

コピー元のファイルやディレクトリを指定する。
引数で複数指定することも、 from() 自体を複数回呼ぶこともできる。

引数で渡した値は最終的に Projectfiles(Object...) メソッドに渡される。
したがって、文字列のパスや File オブジェクトなど、様々な値を渡すことができる。

from(Object, Closure)

build.gradle
task foo(type: Copy) {
    from("fromDir") {
        include "**/*.txt"
    }
    ...
}

from() の第二引数にクロージャを渡すことができる。
このクロージャの delegateCopySpec になっており、 from() で指定したディレクトリ以下だけを対象にさらにコピーの条件を絞ることができるようになっている。
ここでは、 include() でコピーするファイルを *.txt だけに絞っている。

into(Object)

build.gradle
task foo(type: Copy) {
    into "toDir"
    ...
}

コピー先のディレクトリを指定する。

include(String...)

build.gradle
task foo(type: Copy) {
    include "*.txt", "**/*.xml"
    ...
}

Ant 形式のパターン指定でコピー対象に含める条件を指定できる。

exclude(String...)

build.gradle
task foo(type: Copy) {
    exclude "**/*.class", "**/*.bk"
    ...
}

こちらは、同じく Ant 形式のパターンでコピーから除外する条件を指定できる。

rename(Closure)

build.gradle
task foo(type: Copy) {
    from "fromDir"
    into "toDir"
    rename { name ->
        name.toUpperCase()
    }
}

rename() を使用すると、コピーするときにファイルの名前を変更できる。
クロージャを受け取る rename() の場合は、クロージャの引数にコピー前のファイル名(String)が渡される。
そして、クロージャが返した値がコピー後のファイル名として利用される。

上記実装の場合、元ファイル名を全て大文字にしたファイル名がコピー先のファイル名に利用される。

rename(String, String)

build.gradle
task foo(type: Copy) {
    from "fromDir"
    into "toDir"
    rename(/(.*)\.([a-z]+)/, '$1_copy.$2')
}

引数に String を2つ受け取る rename() は、第一引数に正規表現を、第二引数にリネーム後の式を渡す。

第一引数の正規表現でグループとして定義した部分(() で括った部分)は、第二引数の式では $n で参照できる(n は 1 からの連番)。

上の例の場合、1つ目のグループが (.*) で、拡張子より前のファイルのベース名を指している。
2つ目のグループは ([a-z]+) の部分で、拡張子の部分を指している。

そして、 $1_copy.$2 とリネーム後の式で指定することで、ファイルのベース名の末尾に _copy をつけた名前にリネームしてコピーしている。

実際に動かすと、次のようになる。

実行結果
> tree /f fromDir
...
│  hoge.txt
│  hoge.xml
│
└─sub
        foo.txt
        foo.xml

> gradle foo
...

> tree /f toDir
...
│  hoge_copy.txt
│  hoge_copy.xml
│
└─sub
        foo_copy.txt
        foo_copy.xml

Project の copy()

Project クラスには、 copy(Closure) というメソッドが用意されている。

引数のクロージャの delegateCopySpec インターフェースを実装しており、 Copy タスクと同じ要領でコピーの対象を指定できるようになっている。

build.gradle
task foo {
    doFirst {
        copy {
            from "fromDir"
            into "toDir"
            include "**/*.txt"
        }
    }
}

単一のタスクの中でいくつかのコピー処理を実行したい場合は、この Projectcopy() メソッドを使う方法もある。

Delete

build.gradle
task foo(type: Delete) {
    delete "targetDir/aaa.txt", "targetDir/hoge"
}
実行結果
> tree /f targetDir
│  aaa.txt
│  bbb.txt
│
└─hoge
    │  ccc.txt
    │
    └─fuga
            ddd.txt

> gradle foo
...
BUILD SUCCESSFUL in 5s

> tree /f targetDir
    bbb.txt

Delete タスクを使うと、ファイルやフォルダの削除ができる。

Delete クラスは DeleteSpec インターフェースを実装している。
このインターフェースで定義されている delete(Object...) メソッドで削除対象を指定する。
引数で指定した値は、 Project#files(Object...) メソッドに渡されるので、文字列や File など様々な型で指定できる。

Delete 自体には、「特定のディレクトリの、特定のパターンのファイル」を指定できるような API は用意されていない。
しかし、前述のように delete() に渡す値は Project#files() に渡されるので、 FileCollection を渡すこともできる。
つまり、次のような感じで指定することもできる。

build.gradle
task foo(type: Delete) {
    delete fileTree(dir: "targetDir", include: "hoge/**/*.txt")
}
実行結果
> tree /f targetDir
│  aaa.txt
│  bbb.txt
│
└─hoge
    │  ccc.txt
    │
    └─fuga
            ddd.txt

> gradle foo
...
BUILD SUCCESSFUL in 5s

> tree /f targetDir
│  aaa.txt
│  bbb.txt
│
└─hoge
    └─fuga

Project の delete() メソッド

copy() 同様、delete(Action) というメソッドが Project に用意されている。

クロージャの delegateDeleteSpec を実装しているので、 Delete タスクと同じ要領で削除対象を指定できる。

build.gradle
task foo {
    doFirst {
        project.delete { delete "targetDir" }
    }
}

ここで注意なのが、 delete() メソッドを呼ぶときは必ず project.delete() というふうに project を前につけないといけない点。

build.gradle
task foo {
    doFirst {
        project.delete {
            println "project.delete.delegate=${delegate.class}"
        }

        delete {
            println "delete.delegate=${delegate.class}"
        }
    }
}
実行結果
> gradle foo
project.delete.delegate=class org.gradle.api.internal.file.delete.DefaultDeleteSpec
delete.delegate=class build_32bm3o8iprxruz9mv43mbtt86$_run_closure1$_closure2

project.delete() の方は、 delegateDeleteSpec になっているが、 delete() だけの方は DeleteSpec になっていない。

ちなみに、 copy() の方は project をつけなくても delegateCopySpec になるので問題ない。
どうも、引数に Action を受け取るメソッドしかないと、 project が必要になるもよう(copy()copy(Closure)copy(Action) があるが、 delete()delete(Action) しかない)。

なんでこういう動きになるのかは、正直よくわかってない。

Sync

build.gradle
task foo(type: Sync) {
    from "fromDir"
    into "toDir"
    include "**/*.txt"
}
実行結果
> tree /f fromDir
│  aaa.txt
│  bbb.txt
│  eee.xml
│
└─hoge
    │  ccc.txt
    │  fff.xml
    │
    └─fuga
            ddd.txt
            ggg.xml

> tree /f toDir
    hoge.txt

> gradle foo
...
BUILD SUCCESSFUL in 5s

> tree /f toDir
│  aaa.txt
│  bbb.txt
│
└─hoge
    │  ccc.txt
    │
    └─fuga
            ddd.txt

Sync タスクは、 into で指定したディレクトリを from で指定したディレクトリと同じ状態に更新する。
SyncCopySpec を実装しているので、 Copy タスクと同じ要領でコピーの方法を制御できる。

タスク実行前に toDir に存在した hoge.txt が削除されている。
Sync による同期は、デフォルトではコピー先のファイルやフォルダを全て一旦削除してから行われる

同期先にだけ存在するファイルやフォルダで削除してほしくないモノがある場合は、 preserve を指定する。

build.gradle
task foo(type: Sync) {
    from "fromDir"
    into "toDir"
    include "**/*.txt"
    preserve {
        include "hoge.txt"
    }
}
実行結果
> tree /f toDir
    fuga.txt
    hoge.txt
    piyo.txt

> gradle foo
...
BUILD SUCCESSFUL in 5s

> tree /f toDir
│  aaa.txt
│  bbb.txt
│  hoge.txt
│
└─hoge
    │  ccc.txt
    │
    └─fuga
            ddd.txt

preserve 内の include で指定した対象は削除されなくなる(preserve は「保存」という意味)。

exclude を指定することも可能で、 exclude "hoge.txt" と指定した場合は hoge.txt だけが削除される(保存対象ではなくなる)。

includeexclude を同時に指定した場合は、 include の設定が優先されるもよう。

Project の sync() メソッド

想像通り、 sync(Action) メソッドも Project に用意されている。

ただし、このクロージャの delegate が実装しているのは CopySpec インターフェースになる。
つまり、 preserve は指定できない(リファレンスでは preserve が指定可能と書かれているが、実際に書くとエラーになる)。

build.gradle
task foo {
    doFirst {
        project.sync {
            from "fromDir"
            into "toDir"
            include "**/*.txt"
        }
    }
}

sync() も、 sync(Action) しかないので、 delete() 同様 project の修飾をつけないとエラーになる。

Exec

build.gradle
task foo(type: Exec) {
    workingDir "workDir"
    environment MESSAGE: "Hello World!!"
    commandLine "cmd", "/c", "echo", "%MESSAGE%", ">", "hello.txt"
}
実行結果
> gradle foo
...
BUILD SUCCESSFUL in 5s

> type workDir\hello.txt
Hello World!!

Exec タスクを使うと、任意のコマンドを実行できる。
ExecExecSpec インターフェースを実装しており、そこに設定用のメソッドが定義されている。

commandLine(Object...) で、実行したいコマンドを指定する。
Linux の場合は直接実行したいコマンドから始めればいいが、 Windows の場合は cmd /c <実際に実行したいコマンド> と続ける必要がある。

workingDir(Object) で実行時のワーキングディレクトリを指定できる。

environment(Map) で、コマンドを実行しているサブプロセス内での環境変数を設定できる。

Project の exec() メソッド

build.gradle
task foo {
    doFirst {
        exec {
            commandLine "cmd", "/c", "echo", "Hello World!!"
        }
    }
}

Projectexec(Closure) メソッドを使えば、 Exec と同じ要領でコマンドを実行できる。

クロージャの delegateExecSpec となっている。

Zip

build.gradle
task foo(type: Zip) {
    baseName = "foo"
    from "fromDir"
    destinationDir = file("toDir")
    include "**/*.txt"
}
実行結果
> tree /f fromDir
│  aaa.txt
│  bbb.txt
│  eee.xml
│
└─hoge
    │  ccc.txt
    │  fff.xml
    │
    └─fuga
            ddd.txt
            ggg.xml

> gradle foo
...
BUILD SUCCESSFUL in 5s

> dir /b toDir
foo.zip

Zip タスクを使うと、任意のディレクトリを zip で圧縮できる。

ZipCopySpec インターフェースを実装しているので、 Copy タスクと同じように対象を絞り込むことができる。

ただし、出力先は destinationDir プロパティで指定する。
このプロパティは Object ではなく File なので注意。
なお、 base プラグインを適用している場合、 destinationDir はデフォルトで build/distributions になる。
base プラグインが適用されていない場合(基本ありえないが。。。)、 destinationDir の指定は必須になる。

zip ファイルの名前は、いくつかのプロパティに設定された値が連結されてつくられる。

build.gradle
task foo(type: Zip) {
    baseName = "baseName"
    appendix = "appendix"
    version = "version"
    classifier = "classifier"
    extension = "extension"

    from "fromDir"
    destinationDir = file("toDir")
    include "**/*.txt"
}
実行結果
> gradle foo
...
BUILD SUCCESSFUL in 5s

> dir /b toDir
baseName-appendix-version-classifier.extension

ファイル名全体は、次のようにして各プロパティをハイフンでつなげて作られる。
(設定されていない分のプロパティは無視される(extension が未設定の場合は "zip" になる))

baseName-appendix-version-classifier.extension

ファイル名全体そのものを一気に指定したい場合は、 archiveName プロパティを指定する。

なお、 Zip タスクによって生成される zip ファイルのパスは archivePath プロパティで確認できる。

Project の zipTree() メソッド

残念ながら? Projectzip() というメソッドは存在しない。

代わりではないが、逆に zip の内容を抽出するための zipTree(Object) メソッドが用意されている。
引数で対象の zip ファイルのパスを指定すると、その zip の内容の情報を保持した FileTree が返される。

build.gradle
task foo {
    doFirst {
        def zip = zipTree("toDir/foo.zip")
        zip.each { println it }
    }
}
実行結果
> gradle foo
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\aaa.txt
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\bbb.txt
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\eee.xml
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\hoge\ccc.txt
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\hoge\fff.xml
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\hoge\fuga\ddd.txt
F:\etc\...\build\tmp\expandedArchives\foo.zip_d658f891babbd7031de320615856ccb1\hoge\fuga\ggg.xml

zipTree() をした直後は、まだ zip ファイルは解凍されていない。
ZipTree 内のファイルの情報にアクセスしようとした時点で実際に zip ファイルがテンポラリディレクトリに解凍され、ファイルの情報にアクセスできるようになる。

この zipTree()copy() を組み合わせることで、 zip の解凍を再現できる。

build.gradle
task foo {
    doFirst {
        copy {
            from zipTree("toDir/foo.zip")
            into "build/unzip"
        }
    }
}

これで、 zip の内容が build/unzip ディレクトリの下に解凍される。

buildSrc

Gradle には、ビルドスクリプトで使用するクラスを配置するための専用のプロジェクトが用意されている。

フォルダ構成
|-build.gradle
`-buildSrc/
  `-src/main/groovy/
    `-sample/
      `-HogeTask.groovy

ルートの buildSrc ディレクトリが、専用のプロジェクトになる。
このプロジェクトの src/main/groovy ディレクトリの下に、自作のタスクやプラグインのコードを配置できる。

HogeTask.groovy
package sample

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class HogeTask extends DefaultTask {

    @TaskAction
    def hoge() {
        println("Hello Hoge.")
    }
}
build.gradle
task foo(type: sample.HogeTask)
実行結果
> gradle foo
Hello Hoge.

buildSrc にソースコードが存在すると、タスクを実行するときに自動的にコンパイルが行われ、ビルドスクリプトで利用できるようになる。

プラグイン

カスタムタスクを使うと、タスクのロジックをあらかじめ定義しておき、複数の箇所で設定値だけを切り替えて使い回すことができるようになる。
しかし、カスタムタスクはあくまで単一のタスクであり、タスクの生成や細かい設定はすべて利用側で指定しなければならない。

複数のタスクの生成や、タスク間の依存関係などの定義など、より広範囲な処理を共通化して使い回しできるようにするためには、プラグインという仕組みを利用する。

フォルダ構成
|-build.gradle
`-buildSrc/
  `-src/main/groovy/
    `-sample/
      `-FooPlugin.groovy
FooPlugin.groovy
package sample

import org.gradle.api.Project
import org.gradle.api.Plugin

class FooPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        project.task("hello") {
            doFirst { println("FOO!!") }
        }
    }
}
build.gradle
apply plugin: sample.FooPlugin
実行結果
> gradle hello
FOO!!

プラグインは、 Plugin インターフェースを実装して作成する(型引数は Project にしておく)。

apply(T) メソッドを実装し、その中にプラグインの処理を記述していく。
記述する内容は、普通に build.gradle で記述していた内容と同じような感じで問題ない。
ただし、 build.gradle の場合は暗黙的に Project への委譲が行われるようになっていたので task() などを直接呼べていたが、ここでは暗黙的な委譲は行われないので、明示的に Project のメソッドを呼び出す形で記述する必要がある。

作成したプラグインは、他のプラグインと同じように apply で読み込むことができる。

プラグインの設定値

プラグインに設定値を渡したい場合は、拡張オブジェクトの仕組みを利用する。

FooPlugin.groovy
package sample

import org.gradle.api.Project
import org.gradle.api.Plugin

class FooPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def foo = project.extensions.create("foo", FooPluginExtension)

        project.task("hello") {
            doFirst { println(foo.message) }
        }
    }
}

class FooPluginExtension {
    String message
}

拡張オブジェクトを生成するには、 ExtensionContainercreate() メソッドを使用する。
ExtensionContainer は、 Projectextensions プロパティから取得できる。

create() メソッドは、第一引数に拡張オブジェクトの名前を、第二引数に拡張オブジェクトの型を渡す。
ここでは、名前に "foo" を指定し、型に FooPluginExtension を指定している。

create() の戻り値は第二引数で指定したクラスのオブジェクトになっており、このオブジェクトを介してビルドスクリプトで指定された設定値にアクセスできる。

生成された拡張オブジェクトは Project にプロパティとして追加されており、 create() のときに指定した名前で参照できるようになっている。

build.gradle
apply plugin: sample.FooPlugin

foo.message = "Hello Foo!!"
実行結果
> gradle hello
Hello Foo!!

拡張オブジェクトは設定ブロックが利用できるようになっているので、次のように記述することもできる。

build.gradle
apply plugin: sample.FooPlugin

foo {
    message = "Hello Foo!!"
}

設定値が複数存在する場合は、設定ブロックを利用して記述を簡潔にすることができる。

拡張オブジェクトのコンストラクタ引数

拡張オブジェクトのコンストラクタで引数を受け取るようにしたい場合は、次のように記述する。

FooPlugin.groovy
package sample

import org.gradle.api.Project
import org.gradle.api.Plugin

class FooPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def foo = project.extensions.create("foo", FooPluginExtension, project)

        project.task("hello") {
            doFirst { println(foo.outputDir) }
        }
    }
}

class FooPluginExtension {
    Object outputDir

    FooPluginExtension(Project project) {
        this.outputDir = project.file("${project.buildDir}/foo")
    }
}

拡張オブジェクトの FooPluginExtensionProject オブジェクトを受け取り、 outputDir のデフォルト値を Project から生成した値にしている。

このコンストラクタ引数に値を渡すには、 create() メソッドの第三引数以降に対応する値を渡すように実装する。
create() の第三引数は可変長引数になっており、ここで指定した値はそのまま拡張オブジェクトのコンストラクタに渡されるようになっている。

拡張オブジェクトで設定した値をタスクの設定値に利用する

FooPlugin.groovy
package sample

import org.gradle.api.Project
import org.gradle.api.Plugin

class FooPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def foo = project.extensions.create("foo", FooPluginExtension)

        project.task("copyFile") {
            inputs.file foo.inputFile
            outputs.dir foo.outputDir

            doFirst {
                project.copy {
                    from foo.inputFile
                    into foo.outputDir
                }
            }
        }
    }
}

class FooPluginExtension {
    Object inputFile
    Object outputDir
}

copyFile というタスクを作成し、拡張オブジェクトの inputFileoutputDir にコピーするように実装している。
また、 copyFile タスクの入出力には、それぞれ inputFileoutputDir を設定している。

build.gradle
apply plugin: sample.FooPlugin

foo {
    inputFile = "fromDir/aaa.txt"
    outputDir = "build"
}

これを実行すると、次のようにビルドは失敗する。

実行結果
> gradle copyFile
...
> Task :copyFile FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Some problems were found with the configuration of task ':copyFile'.
> No value has been specified for property '$1'.
...

これだけだと、ちょっと何を言っているかわからないが、次のようなデバッグ用の出力を追加してみると何が起こっているかわかってくる。

FooPlugin.groovy
package sample

import org.gradle.api.Project
import org.gradle.api.Plugin

class FooPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println "FooPlugin.apply() 開始"
        def foo = project.extensions.create("foo", FooPluginExtension)

        project.task("copyFile") {
            inputs.file foo.inputFile
            outputs.dir foo.outputDir
            
            println "foo.inputFile=${foo.inputFile}"
            println "foo.outputDir=${foo.outputDir}"

            doFirst {
                project.copy {
                    from foo.inputFile
                    into foo.outputDir
                }
            }
        }
        println "FooPlugin.apply() 終了"
    }
}

class FooPluginExtension {
    Object inputFile
    Object outputDir
}
build.gradle
println "FooPlugin 適用前"
apply plugin: sample.FooPlugin
println "FooPlugin 適用後"

println "foo 拡張オブジェクトに設定値を指定"
foo {
    inputFile = "fromDir/aaa.txt"
    outputDir = "build"
}
実行結果
> gradle createFile
...
FooPlugin 適用前
FooPlugin.apply() 開始
foo.inputFile=null
foo.outputDir=null
FooPlugin.apply() 終了
FooPlugin 適用後
foo 拡張オブジェクトに設定値を指定

> Task :copyFile FAILED
...

createFile タスクの入出力を設定している段階では、まだ foo 拡張オブジェクトに値が設定されていない。
このため、入出力の指定が null になってしまっている。

プラグインの適用はビルドライフサイクルの設定フェーズで行われており、基本的に上から順番に設定処理が実行されていく。
したがって、カスタムプラグインの apply() メソッドの中では、まだ foo 拡張オブジェクトが設定されていないので値を参照できないことになる。

要するに、プラグインの適用後に設定される予定の値を、プラグイン適用中に参照しようとしているのでおかしな状態になっているということになる。
もし拡張オブジェクトに設定している値が、全て実行フェーズで参照されるのであれば、このような問題はおこならない。

しかし、入出力の設定は設定フェーズで指定しなければならないので、このままだと入出力の設定が不可能ということになってしまう。

Property 使ってプロパティを定義する

この問題は、 Property を利用すると回避できる。

FooPlugin.groovy
package sample

import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.provider.Property

class FooPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def foo = project.extensions.create("foo", FooPluginExtension, project)

        project.task("copyFile") {
            inputs.file foo.inputFile
            outputs.dir foo.outputDir

            doFirst {
                project.copy {
                    from foo.inputFile
                    into foo.outputDir
                }
            }
        }
    }
}

class FooPluginExtension {
    Property<Object> inputFile
    Property<Object> outputDir

    FooPluginExtension(Project project) {
        this.inputFile = project.objects.property(Object)
        this.outputDir = project.objects.property(Object)
    }
}
build.gradle
apply plugin: sample.FooPlugin

foo {
    inputFile = "fromDir/aaa.txt"
    outputDir = "build"
}
実行結果
> gradle copyFile
...
BUILD SUCCESSFUL in 6s

inputFile, outputDir プロパティの型に Property という型を使用している。

Property のインスタンスは ObjectFactoryproperty(Class) メソッドで生成できる。
ObjectFactory は、 ProjectgetObjects() メソッドから取得できる。

Projectfile() などは、この Property 型をサポートしているのでそのまま渡せているが、中身の値が必要な場合は get() メソッドを使えば取得できる。
また、 set() メソッドで値を変更することもできる。

ところで、上の例では Property 型であるはずのプロパティに対して、代入演算子で文字列(String 型)を設定している。
普通であれば、型が違うためにこのような記述はエラーになる。

これは、 Gradle が Property 型のプロパティに対してセッターメソッドを自動的に生成してくれているために実現できている。

build.gradle
apply plugin: sample.FooPlugin

foo {
    inputFile = "fromDir/aaa.txt"
    outputDir = "build"

    println delegate.class.methods.find { it.name == "setInputFile" }
}
自動生成されているPropertyのセッターメソッド
> gradle copyFile
...
public void sample.FooPluginExtension_Decorated.setInputFile(java.lang.Object)
...

この代入演算子は、自動生成された setInpputFile(Object) メソッド呼び出しのシンタックスシュガーとなっている。

この Property 型はカスタムタスクのプロパティでも利用でき、セッターの自動生成もサポートされている。

CustomCopyTask.groovy
package sample

import org.gradle.api.provider.Property
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory

class CustomCopyTask extends DefaultTask {
    @InputFile
    Property<Object> inputFile = project.objects.property(Object)
    @OutputDirectory
    Property<Object> outputDir = project.objects.property(Object)

    @TaskAction
    def copy() {
        project.copy {
            from this.inputFile
            into this.outputDir
        }
    }
}
build.gradle
task foo(type: sample.CustomCopyTask) {
    inputFile = "fromDir/aaa.txt"
    outputDir = "build"
}

プロパティをコレクションで宣言したい場合は、 ListPropertySetProperty といったサブタイプが用意されている。
いずれも ObjectFactory にファクトリメソッドが用意されているので、そこからオブジェクトを生成できる。

ライフサイクルタスク

Gradle にはライフサイクルタスクというものが用意されている。

ライフサイクルタスクは、それ自身は処理を行わない。
代わりに、何らかの概念を表現し、他のタスクに依存することで役割を果たす。

たとえば、 Base プラグイン には check というタスクが定義されている。
この check タスク自体は何もしないが、「プロジェクトの検証処理を行う」という概念を表現している。

あるプラグインが検証処理を行うタスクを追加する場合、 check の依存関係にその検証タスクを追加しておく。
すべてのプラグインがこのように実装されていれば、 check を実行するだけですべての検証処理が動くようになる。

例えば、 Java プラグイン は、 test タスクを check の依存関係に追加している。
これにより、 check を実行すれば test も実行されることになる。

参考

  1. Ant形式については「ant パターン 形式」とかで検索すればいろいろ情報が出てくるのでそちらを参照

  2. 出力ファイルが削除されていたりしたら再実行が必要なので、出力の状態もチェックする必要がある

197
169
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
197
169

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?