自己紹介
- opengl-8080
- 主に Qiita で技術メモを書いたり
- 関西の SIer 勤務
タイトルの元ネタ
エンジニア用語の「完全に理解した」「何も分からない」「チョットデキル」は「ダニング・クルーガー効果」で簡単に説明ができます。これは一種の認知バイアスで能力の低い段階では自分の能力の低さを認識できないためです(過大評価しがち)。その反面で能力が高くなると過少評価しがちです。 pic.twitter.com/LGaJ4E5hWo
— おちゃめ (@ochame_nako) April 8, 2019
※スライドモードでは画像が表示されないみたいなので、スライドモードを解除して記事本文を直接参照してください。
お話しすること
- 「Gradle 完全に理解した」レベルの人が、「Gradle 何も分からん」レベルになるためのお話
- 自分自身、最近 Gradle のことが何も分からなくなってきた
- 完全に理解していた頃によく分からなかったことを解説することで、
同じように完全に理解している人たちが何も分からなくなる一助になれば幸い
前提知識・対象者のイメージ
- ユーザマニュアル通りなら
build.gradle
を書ける - でも、自力でゼロからタスクを作るのは無理
-
build.gradle
を読むための最低限の知識はある- Groovyを知らない人のためのbuild.gradle読み書き入門 にかかれている知識を有している程度
初心者の頃によく分からなかったこと
- タスクの作り方がよく分からん
- ファイルの選択方法がよく分からん
このあたりが分かってくると、初心者を脱出できる(たぶん)
タスクの作り方
- ゼロからタスクを作ることができる
- 既存のタスクをカスタマイズできる
ファイルの選択
- ビルドタスクは、だいたい何らかのファイル操作が伴う
- ファイル操作はビルドの基本であり中核(おそらく)
タスクの作り方がよく分からん
タスクに関する疑問アレコレ
-
doFirst
,doLast
って2つあるけど、どっちを使うの? - タスクの順番ってどうやって制御するの?
- ファイルコピーって結局どうやって実装するのがいいの?
- Java の API
- Groovy の API
- Project の copy() メソッド
-
task(type: Copy)
-
type: Copy
ってなんなん?
-
doFirst, doLast、どっち使えばいいの?
task hoge {
doFirst { println "doFirst" }
doLast { println "doLast" }
}
$ gradle hoge
...
doFirst
doLast
- どちらも、タスクの処理を記述するためのモノ
- 使い分けは?
タスクの正体
- org.gradle.api.Task のインスタンス
- 内部に Action のリストを持つ
-
Action
は、何らかの処理を持つ -
Task
は、Action
のリストを先頭から順番に実行していく
doFirst と doLast は Action を追加するメソッド
-
doFirst と doLast は、
Task
に定義されたメソッド - 引数に渡したクロージャを
Action
にしてTask
に追加する -
doFirst
は、Action
リストの先頭に追加する -
doLast
は、Action
リストの末尾に追加する
どちらを使うのがいいの?
- 自作タスクなら、どちらでもいい
- 統一感が出るので、どちらかに揃えるくらいはしておいたほうがいい
既存タスクに処理を追加できる
plugins { id "java" }
compileJava {
doFirst { println "Before compileJava" }
doLast { println "After compileJava" }
}
$ gradle compileJava
...
Before compileJava
After compileJava
-
doFirst
,doLast
の使い分けが重要になるのは、既存タスクを修正するような場面 - 組み込みタスクに、任意の事前(事後)処理を追加できる
タスクの実行順序の制御
タスクAの後でタスクBを実行したい
task taskA {
doFirst {
println "Task A"
tasks.taskB.execute() // 注:実際はできない
}
}
task taskB {
doFirst { println "Task B" }
}
- 自分が初心者の頃は、指定したタスクを実行する方法を探していた
Gradle のタスクは依存関係で順序を制御する
考え方の向きを逆転させる
× 「~~したら、・・・する」(実行順序)
○ 「・・・するためには、先に~~が必要」(依存関係)
タスクの依存関係を定義する
task taskA {
doFirst { println "Task A" }
}
task taskB {
// ★ taskB が taskA に依存していることを定義する
dependsOn "taskA"
doFirst { println "Task B" }
}
$ gradle taskB
...
> Task :taskA
Task A
> Task :taskB
Task B
-
Task
のdependsOn
で依存するタスクを指定する
とは言っても順序を指定したくなる場面はある
典型的な例は、 clean
と build
を同時に実行したとき
$ gradle build clean
...
> Task :build
> Task :clean
-
build
してからclean
してしまっている -
dependsOn
で依存関係として定義すると、build
したときは常にclean
されてしまう - 依存関係(必ず実行するもの)ではないが、同時に実行されたら順序を制御したい
タスクの実行順序を定義する
plugins { id "java" }
build.mustRunAfter clean
$ gradle build clean
...
> Task :clean
> Task :build
- mustRunAfter を使うと、複数のタスクが同時に指定されたときに、実行する順序を制御できる
- 依存関係はないけど同時に実行されたときは順序を制御したい、といったときに利用できる
ファイルコピーの実装方法
実現方法がいくつもある
Java の標準 API を使う
import java.nio.file.*
task copyFile {
doFirst {
Path from = Paths.get("${projectDir}/foo.txt")
Path to = Paths.get("${buildDir}/foo.txt")
Files.copy(from, to)
}
}
-
build.gradle
は実質 Groovy のスクリプトファイルなので、 Java のプログラムを書くことができる - つまり、 Java の標準 API (Files.copy(Path, Path, CopyOption...))を使用できる
Groovy の API を使う
task copyFile {
doFirst {
def from = new File("${projectDir}/foo.txt")
def to = new File("${buildDir}/build/foo.txt")
to.bytes = from.bytes
}
}
- Groovy でのみ利用できる書き方も可能
- ※この書き方はメモリに一旦全データを読み込んでいるので注意(あくまで例)
Project の copy() メソッドを使う
task copyFile {
doFirst {
copy {
from "./foo.txt"
into buildDir
}
}
}
Copy タスク
task copyFile(type: Copy) {
from "./foo.txt"
into buildDir
}
-
Copy タスクを使うことで
Project.copy()
を使った場合と似たような記述でコピーを実現できる
どれを使うのがいいの?
より簡潔で宣言的な記述になる方法を選択するのがいいと思う
宣言的な記述
- 何をするか(目的・やりたいこと・what)にフォーカスした記述
- どうやって実現するか(手段・方法・how)は書かない
やりたいこと
ファイル foo.txt
を build
ディレクトリの下にコピーしたい。
Groovy の例
task copyFile {
doFirst {
// 1. foo.txt の File オブジェクトを構築して、 from 変数に代入
def from = new File("${projectDir}/foo.txt")
// 2. コピー先となる File オブジェクトを構築して、 to 変数に代入
def to = new File("${buildDir}/foo.txt")
// 3. from の内容をバイトで読み込んで、 to にそのまま書き込む
to.bytes = from.bytes
}
}
- 実現方法が書かれている
-
File
オブジェクトの準備 - バイトを読み込んで、書き込む
-
Java の例
import java.nio.file.*
task copyFile {
doFirst {
// 1. foo.txt の Path オブジェクトを構築して、 from 変数に代入
Path from = Paths.get("${projectDir}/foo.txt")
// 2. コピー先となる Path オブジェクトを構築して、 to 変数に代入
Path to = Paths.get("${buildDir}/foo.txt")
// 3. from から to にコピー
Files.copy(from, to)
}
}
-
Files.copy()
のおかげで少し宣言的になった - でも雑音が多い
-
Path
の構築 -
Files
というユーティリティクラスの存在
-
Project の copy() メソッド
task copyFile {
doFirst {
// 1. コピーする
copy {
// 2. foo.txt を
from "./foo.txt"
// 3. buildDir へ
into buildDir
}
}
}
- やりたいことだけが書かれている
- 「ファイル
foo.txt
をbuild
ディレクトリの下にコピーしたい。」
- 「ファイル
- 雑音もなくて簡潔
宣言的な記述のメリット
- 目的にフォーカスすることで、やりたいこと(意図)が読み取りやすくなる
- 詳細な手段を省くことで記述が簡潔になる
Copy と copy() の違いは?
Copy タスクと Project の copy() を使った場合の違いは?
task copyTask(type: Copy) {
from "./foo.txt"
into buildDir
}
task projectCopy {
doFirst {
copy {
from "./foo.txt"
into buildDir
}
}
}
- 行数は
Copy
を使ったほうが短くなっているが、from
やinto
などの記述は同じ感じ - 何が違う?
タスクの型が違う
task copyTask(type: Copy) {
from "./foo.txt"
into buildDir
}
task projectCopy {
doFirst {
copy {
from "./foo.txt"
into buildDir
}
}
}
println "copyTask.class = ${copyTask.class}"
println "projectCopy.class = ${projectCopy.class}"
$ gradle
...
copyTask.class = class org.gradle.api.tasks.Copy_Decorated
projectCopy.class = class org.gradle.api.DefaultTask_Decorated
補足
-
type
を指定してタスクを定義した場合、タスクのインスタンスはtype
で指定したクラスから生成される-
copyTask
タスクはCopy
のインスタンス -
projectCopy
タスクはDefaultTask
のインスタンス
-
task copyFile(type: Copy) { ...
// Java 風に書くと
task(Map.of("type", Copy.class), "copyFile");
-
type:
は、名前付き引数 -
Copy
は、 Java のコードになおすとCopy.class
になる - つまり、
Copy
クラスのClass
オブジェクトをtype
という名前付き引数で渡していることになる
型が違うだけ?
使い方に差はある?
どちらも CopySpec を使用している
task copyTask(type: Copy) {
from "./foo.txt"
into buildDir
println "copyTask > ${delegate instanceof CopySpec}"
}
task projectCopy {
doFirst {
copy {
from "./foo.txt"
into buildDir
println "projectCopy > ${delegate instanceof CopySpec}"
}
}
}
$ gradle projectCopy
...
copyTask > true
projectCopy > true
- Copy タスクは CopySpec を実装している
-
Project.copy(Closure) メソッドは、引数のクロージャの
delegate
がCopySpec
になっている
コピーの内容を定義するためのインターフェース
public interface CopySpec extends ... {
...
CopySpec from(Object... sourcePaths);
...
CopySpec into(Object destPath);
...
}
- CopySpec は、その名の通りコピーの仕様を定義するメソッドを定義している
- コピーの内容を定義する方法が
CopySpec
で統一されている - つまり、使い方(書き方)は同じということになる
書き方が CopySpec で統一されているということは、
結局両者に違いは無いということ?
複数回実行すると違いが見えてくる
# 1回目
$ gradle copyTask
> Task :copyTask
BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed
# 2回目
$ gradle copyTask
> Task :copyTask UP-TO-DATE
BUILD SUCCESSFUL in 3s
1 actionable task: 1 up-to-date
2回目は UP-TO-DATE
でタスクが実行されなかった。
# 1回目
$ gradle projectCopy
> Task :projectCopy
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
# 2回目
$ gradle projectCopy
> Task :projectCopy
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
2回目も実行された。
不必要なタスクの再実行を防ぐ仕組み
- Gradle には、タスクの不必要な再実行を防ぐ仕組みがある
- タスクの入出力を定義する
- タスクが実行されると、入出力の内容が記録される
- 入出力が前回と同じ場合、タスクは実行されない(
UP-TO-DATE
)
-
Copy
タスクは、from
やinto
で指定した内容がタスクの入出力になる -
Project.copy()
は、純粋にファイルをコピーするだけ- タスクの入出力は、別途自力で設定する必要がある
Project.copy() と Copy の違い
Project.copy()
- 純粋にコピーだけを行う
- 「コピー=タスクの処理内容の一部」の場合に便利
Copy
- コピーだけでなく、 Gradle タスクとして便利な設定が施されている
- 「コピー=タスクの処理内容そのもの」の場合に便利
ところで ~~Spec はいろいろある(ちょっと脱線)
- CopySpec :コピーの仕様
- DeleteSpec :削除の仕様
- ExecSpec :コマンド実行の仕様
- JavaExecSpec:Javaコマンド実行の仕様
同じ ~~Spec
を使用している部分は、同じ書き方ができる。
CopySpec はところどころに出てくる(ちょっと脱線)
CopySpec
の書き方を覚えておくと、いろいろなところで応用できる。
delegate
task syncSrc {
doFirst {
project.sync {
// delegate が CopySpec
from "src"
into "${buildDir}/sync"
}
}
}
タスク
task zipTask(type: Zip) {
// Zip が CopySpec を実装している
from "src"
destinationDirectory = buildDir
archiveBaseName = "zipTask"
}
タスクの作り方・まとめ
doFirst, doLast って2つあるけど、どっちを使うの?
- タスクの実体は
Task
のインスタンス -
Task
はAction
のリストを持ち、先頭から順番に実行していく -
doFirst
,doLast
は、Action
のリストの先頭か末尾にAction
を追加する - 自作タスクならどちらでもいい
- 既存タスクに処理を追加するときに意識する
タスクの順番ってどうやって制御するの?
- タスクの順序は依存関係で定義する
- 依存関係は無いが順番を制御したいときは
mustRunAfter
を使う
ファイルコピーって結局どうやって実装するのがいいの?
- 目的を達成できる方法が複数ある場合は、より宣言的で簡潔になる方法を選ぶ
-
Project.copy()
は、純粋なコピー処理のみ -
Copy
は、タスクの入出力まで定義する - 使い方を統一するために
~~Spec
というインターフェースが存在する
ファイルの選択方法がよく分からん
ファイル選択に関する疑問アレコレ
-
file()
とかfiles()
とかfileTree()
の使い方がよくわからない -
file()
は、まぁなんとか分かる -
files()
,fileTree()
になってくると違いが分からなくなってくる
file()
task foo {
doFirst {
File file = file("./foo.txt")
file.text = "Hello Foo!!"
println file.text
}
}
$ gradle foo
> Task :foo
Hello Foo!!
-
file() は、引数で指定したパスを表す
java.io.File
オブジェクトを返す - 文字列だけでなく
File
やjava.nio.file.Path
,java.net.URL
,java.net.URI
などを渡しても、よしなに判断してくれる
files()
task foo {
doFirst {
ConfigurableFileCollection files
= files("hoge.txt", "fuga.txt", "piyo.txt")
files
.findAll { it.name.contains("g") }
.each { println it }
println "files.asPath = ${files.asPath}"
}
}
C:\work\hoge.txt
C:\work\fuga.txt
files.asPath = C:\work\hoge.txt;C:\work\fuga.txt;C:\work\piyo.txt
- files() の戻り値の型は ConfigurableFileCollection
-
FileCollection を継承している
- 名前の通り
File
のコレクション
- 名前の通り
-
Iterable<File>
を継承しているので、findAll()
やeach()
などの Groovy のコレクション用メソッドを使用できる - また、 getAsPath() のような便利メソッドも定義されている
- 各パスを環境ごとのパス区切り文字で連結した文字列を返す
- Windows なら
;
、 Linux 系の環境なら:
で連結した文字列になる - CUI ツールを起動するときのオプション指定で活用できる
fileTree()
task foo {
doFirst {
ConfigurableFileTree tree = fileTree("src")
tree.each { println it }
println "tree.asPath = ${tree.asPath}"
}
}
$ gradle foo
C:\work\src\main\java\fuga\Fuga.java
C:\work\src\main\java\hoge\Hoge.java
C:\work\src\main\java\Main.java
C:\work\src\main\resources\fuga\fuga.xml
C:\work\src\main\resources\hoge\hoge.xml
C:\work\src\main\resources\main.xml
tree.asPath = C:\work\src\main\java\fuga\Fuga.java;C:\work\src\main\java\hoge\Hoge.java;C:\work\src\main\java\Main.java;C:\work\src\main\resources\fuga\fuga.xml;C:\work\src\main\resources\hoge\hoge.xml;C:\work\src\main\resources\main.xml
- fileTree(Object) の戻り値の型は ConfigurableFileTree
-
FileTree を継承している
- 名前の通り、
File
の木構造(階層構造)を表している - 引数で指定したディレクトリ以下の階層を取得できる
- 名前の通り、
-
FileCollection
を継承しているので、FileCollection
と同じ操作が可能
階層構造内のファイルを絞り込む
task foo {
doFirst {
ConfigurableFileTree tree = fileTree("src")
tree.include "**/hoge/**"
tree.each { println it }
}
}
$ gradle foo
C:\work\src\main\java\hoge\Hoge.java
C:\work\src\main\resources\hoge\hoge.xml
Ant スタイルのパターン
- Apache Ant で使用されていたファイルやディレクトリを絞り込むときのパターン
-
*
:任意の文字列 -
**
:任意のサブディレクトリ
fileTree() には書き方がいくつかある
task foo {
doFirst {
// 1. トップのディレクトリを指定するだけ
ConfigurableFileTree tree = fileTree("src")
tree.include "**/hoge/**"
// 2. 第二引数にクロージャを渡して ConfigurableFileTree の設定を記述できる
fileTree("src") {
include "**/hoge/**"
}
// 3. ConfigurableFileTree のプロパティに設定したい値を名前付き引数で指定する
fileTree(dir: "src", include: "**/hoge/**")
}
}
- fileTree(Object) は、階層構造のトップとなるディレクトリを指定するのみ
-
fileTree(Object, Closure) は、第二引数にクロージャを渡す
- クロージャの
delegate
は対象ディレクトリのConfigurableFileTree
なので、ファイルの絞り込みなどを記述できる
- クロージャの
-
fileTree(Map) は
ConfigurableFileTree
のプロパティにセットする値を名前付き引数で指定できる
file(), files(), fileTree() の違いを整理
-
file()
は、単一のファイル(ディレクトリ)を指定して、File
を作る -
files()
は、複数のファイル(ディレクトリ)を指定して、File
のコレクションを作る -
fileTree()
は、ディレクトリを指定して階層構造で取得する
files() の引数に指定できる値
import java.nio.file.Paths
task foo {
doFirst {
def files = files(
"foo.txt",
new File("foo.txt"),
file("file.txt"),
Paths.get("file.txt"),
["hoge.txt", file("fuga.txt"), Paths.get("piyo.txt")],
files("foo.txt", "bar.txt"),
fileTree(dir: "src", include: "**/*.java")
);
}
}
- ファイルっぽいものなら、だいたい渡せる
- CharSequence, String, GString, File, Path, URI, URL, Directory, RegularFile
- Collection や Iterable を渡した場合は中身を再帰的に解析してくれる
-
FileCollection も OK なので、他の
files()
やfileTree()
で取得した結果を渡すことも可能
file(), files() は様々なところで利用されている
- 例えば
CopySpec
の from() メソッドは、引数の値をfiles()
に渡している
The given paths are evaluated as per Project.files(Object...).
- Gradle の API でファイルやファイルのコレクションを受け取るところは、だいたい
file()
またはfiles()
を使っている- API ドキュメントを見ると書いてある
files() を使っている = FileTree を渡せる
これが分かると応用の幅が広がる
zip を解凍する方法
task foo {
doFirst {
copy {
from zipTree("foo.zip")
into "${buildDir}/foo"
}
}
}
-
zipTree() は、指定された zip ファイルの内容を
FileTree
にして取得するメソッド - zip 内のファイルをコピーする = zip を解凍する
- コピーだけでなく、
files()
を使ってファイル情報を受取る API に対してzipTree()
の結果を渡すことができる
自作タスクの設定で利用する
task foo(type: MyTask) {
inputFiles "foo.txt", file("bar.txt"), Paths.get("fizz.txt")
}
class MyTask extends DefaultTask {
Object inputFiles
@TaskAction
def execute() {
println "<<inputFiles>>"
project.files(this.inputFiles).each { println it }
}
void inputFiles(Object... args) {
this.inputFiles = args
}
}
<<inputFiles>>
C:\work\foo.txt
C:\work\bar.txt
- 自作タスクのオプションでファイルの情報を受け取る場合は、内部で
file()
,files()
を使って解析する - こうすることで、柔軟なファイル指定が可能なタスクを作ることができる
ファイルの選択・まとめ
ファイルを選択するための API
-
file()
は、単一のファイルを指定してFile
を取得する -
files()
は、複数のファイルを指定してFileCollection
を取得する -
fileTree()
は、ディレクトリを指定してFileTree
を取得する- Ant 形式のパターンで柔軟な絞り込みが可能
file(), files() の柔軟性
-
file()
,files()
は、ファイルっぽいものなら何でも渡せる -
FileCollection
も渡すことができるので、fileTree()
の結果を渡すこともできる - Gradle の API の多くは
file()
,files()
を使って引数を解析している -
fileTree()
の結果を渡すことで柔軟な組み合わせが可能となる - 自作タスクも
file()
,files()
を使うことで、柔軟性が増す
さらに何も分からなくなるために
- タスクの入出力
- タスクルール
- プラグイン
- 拡張プロパティ
- Configuration
- Property
- etc...
今日の話も含め、もうちょっと詳しい話が知りたい場合は↓を参照
Gradle のタスク定義のあれこれ - Qiita