Help us understand the problem. What is going on with this article?

Gradle を完全に理解した人が、何も分からなくなるための第一歩

Gradle を完全に理解した人が、何も分からなくなるための第一歩

by opengl-8080
1 / 62

自己紹介

  • opengl-8080
  • 主に Qiita で技術メモを書いたり
  • 関西の SIer 勤務

タイトルの元ネタ

※スライドモードでは画像が表示されないみたいなので、スライドモードを解除して記事本文を直接参照してください。


お話しすること

  • 「Gradle 完全に理解した」レベルの人が、「Gradle 何も分からん」レベルになるためのお話
  • 自分自身、最近 Gradle のことが何も分からなくなってきた
  • 完全に理解していた頃によく分からなかったことを解説することで、
    同じように完全に理解している人たちが何も分からなくなる一助になれば幸い

前提知識・対象者のイメージ


初心者の頃によく分からなかったこと

  • タスクの作り方がよく分からん:thinking:
  • ファイルの選択方法がよく分からん:thinking:

このあたりが分かってくると、初心者を脱出できる(たぶん)

タスクの作り方

  • ゼロからタスクを作ることができる
  • 既存のタスクをカスタマイズできる

ファイルの選択

  • ビルドタスクは、だいたい何らかのファイル操作が伴う
  • ファイル操作はビルドの基本であり中核(おそらく)

タスクの作り方がよく分からん:thinking:


タスクに関する疑問アレコレ

  • doFirst, doLast って2つあるけど、どっちを使うの?
  • タスクの順番ってどうやって制御するの?
  • ファイルコピーって結局どうやって実装するのがいいの?
    • Java の API
    • Groovy の API
    • Project の copy() メソッド
    • task(type: Copy)
      • type: Copy ってなんなん?

doFirst, doLast、どっち使えばいいの?

build.grdle
task hoge {
    doFirst { println "doFirst" }
    doLast { println "doLast" }
}
実行結果
$ gradle hoge
...
doFirst
doLast
  • どちらも、タスクの処理を記述するためのモノ
  • 使い分けは?

タスクの正体

gradle.JPG

  • org.gradle.api.Task のインスタンス
  • 内部に Action のリストを持つ
  • Action は、何らかの処理を持つ
  • Task は、 Action のリストを先頭から順番に実行していく

doFirst と doLast は Action を追加するメソッド

gradle.JPG

  • doFirstdoLast は、 Task に定義されたメソッド
  • 引数に渡したクロージャを Action にして Task に追加する
  • doFirst は、 Action リストの先頭に追加する
  • doLast は、 Action リストの末尾に追加する

どちらを使うのがいいの?

  • 自作タスクなら、どちらでもいい
  • 統一感が出るので、どちらかに揃えるくらいはしておいたほうがいい

既存タスクに処理を追加できる

build.gradle
plugins { id "java" }

compileJava {
    doFirst { println "Before compileJava" }
    doLast { println "After compileJava" }
}
実行結果
$ gradle compileJava
...
Before compileJava
After compileJava
  • doFirst, doLast の使い分けが重要になるのは、既存タスクを修正するような場面
  • 組み込みタスクに、任意の事前(事後)処理を追加できる

タスクの実行順序の制御


タスクAの後でタスクBを実行したい

build.gradle
task taskA {
    doFirst {
        println "Task A"
        tasks.taskB.execute() // 注:実際はできない
    }
}

task taskB {
    doFirst { println "Task B" }
}
  • 自分が初心者の頃は、指定したタスクを実行する方法を探していた

Gradle のタスクは依存関係で順序を制御する

考え方の向きを逆転させる

× 「~~したら、・・・する」(実行順序)
「・・・するためには、先に~~が必要」(依存関係)

gradle.jpg


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

build.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
  • TaskdependsOn で依存するタスクを指定する

とは言っても順序を指定したくなる場面はある

典型的な例は、 cleanbuild を同時に実行したとき

$ gradle build clean
...
> Task :build
> Task :clean
  • build してから clean してしまっている
  • dependsOn で依存関係として定義すると、 build したときは常に clean されてしまう
  • 依存関係(必ず実行するもの)ではないが、同時に実行されたら順序を制御したい

タスクの実行順序を定義する

build.gradle
plugins { id "java" }

build.mustRunAfter clean
実行結果
$ gradle build clean
...
> Task :clean
> Task :build
  • mustRunAfter を使うと、複数のタスクが同時に指定されたときに、実行する順序を制御できる
  • 依存関係はないけど同時に実行されたときは順序を制御したい、といったときに利用できる

ファイルコピーの実装方法


実現方法がいくつもある

  • Java の標準 API を使う
  • Groovy の API を使う
  • Projectcopy() メソッドを使う
  • Copy タスクを使う

Java の標準 API を使う

build.gradle
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 を使う

build.gradle
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() メソッドを使う

build.gradle
task copyFile {
    doFirst {
        copy {
            from "./foo.txt"
            into buildDir
        }
    }
}
  • Projectcopy() メソッドを使うことでファイルのコピーが可能

Copy タスク

build.gradle
task copyFile(type: Copy) {
    from "./foo.txt"
    into buildDir
}
  • Copy タスクを使うことで Project.copy() を使った場合と似たような記述でコピーを実現できる

どれを使うのがいいの?


より簡潔で宣言的な記述になる方法を選択するのがいいと思う


宣言的な記述

  • 何をするか(目的・やりたいこと・what)にフォーカスした記述
  • どうやって実現するか(手段・方法・how)は書かない

やりたいこと

ファイル foo.txtbuild ディレクトリの下にコピーしたい。


Groovy の例

build.gradle
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 の例

build.gradle
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() メソッド

build.gradle
task copyFile {
    doFirst {
        // 1. コピーする
        copy {
            // 2. foo.txt を
            from "./foo.txt"
            // 3. buildDir へ
            into buildDir
        }
    }
}
  • やりたいことだけが書かれている
    • 「ファイル foo.txtbuild ディレクトリの下にコピーしたい。」
  • 雑音もなくて簡潔

宣言的な記述のメリット

  • 目的にフォーカスすることで、やりたいこと(意図)が読み取りやすくなる
  • 詳細な手段を省くことで記述が簡潔になる

Copy と copy() の違いは?

Copy タスクと Project の copy() を使った場合の違いは?

build.gradle
task copyTask(type: Copy) {
    from "./foo.txt"
    into buildDir
}

task projectCopy {
    doFirst {
        copy {
            from "./foo.txt"
            into buildDir
        }
    }
}
  • 行数は Copy を使ったほうが短くなっているが、 frominto などの記述は同じ感じ
  • 何が違う?

タスクの型が違う

build.gradle
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 のインスタンス
build.gradle
task copyFile(type: Copy) { ...

// Java 風に書くと
task(Map.of("type", Copy.class), "copyFile");
  • type: は、名前付き引数
  • Copy は、 Java のコードになおすと Copy.class になる
  • つまり、 Copy クラスの Class オブジェクトを type という名前付き引数で渡していることになる

型が違うだけ?
使い方に差はある?


どちらも CopySpec を使用している

build.gradle
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

コピーの内容を定義するためのインターフェース

CopySpec
public interface CopySpec extends ... {
    ...
    CopySpec from(Object... sourcePaths);
    ...
    CopySpec into(Object destPath);
    ...
}
  • CopySpec は、その名の通りコピーの仕様を定義するメソッドを定義している
  • コピーの内容を定義する方法が CopySpec で統一されている
  • つまり、使い方(書き方)は同じということになる

書き方が CopySpec で統一されているということは、
結局両者に違いは無いということ?


複数回実行すると違いが見えてくる

Copyを利用したタスクを複数回実行した場合
# 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 でタスクが実行されなかった。

Project.copy()を利用したタスクを複数回実行した場合
# 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 タスクは、 frominto で指定した内容がタスクの入出力になる
  • Project.copy() は、純粋にファイルをコピーするだけ
    • タスクの入出力は、別途自力で設定する必要がある

Project.copy() と Copy の違い

Project.copy()

  • 純粋にコピーだけを行う
  • 「コピー=タスクの処理内容の一部」の場合に便利

Copy

  • コピーだけでなく、 Gradle タスクとして便利な設定が施されている
  • 「コピー=タスクの処理内容そのもの」の場合に便利

ところで ~~Spec はいろいろある(ちょっと脱線)

同じ ~~Spec を使用している部分は、同じ書き方ができる。


CopySpec はところどころに出てくる(ちょっと脱線)

CopySpec の書き方を覚えておくと、いろいろなところで応用できる。

delegate

build.gradle
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 のインスタンス
  • TaskAction のリストを持ち、先頭から順番に実行していく
  • doFirst, doLast は、 Action のリストの先頭か末尾に Action を追加する
  • 自作タスクならどちらでもいい
  • 既存タスクに処理を追加するときに意識する

タスクの順番ってどうやって制御するの?

  • タスクの順序は依存関係で定義する
  • 依存関係は無いが順番を制御したいときは mustRunAfter を使う

ファイルコピーって結局どうやって実装するのがいいの?

  • 目的を達成できる方法が複数ある場合は、より宣言的で簡潔になる方法を選ぶ
  • Project.copy() は、純粋なコピー処理のみ
  • Copy は、タスクの入出力まで定義する
  • 使い方を統一するために ~~Spec というインターフェースが存在する

ファイルの選択方法がよく分からん:thinking:


ファイル選択に関する疑問アレコレ

  • file() とか files() とか fileTree() の使い方がよくわからない
  • file() は、まぁなんとか分かる
  • files(), fileTree() になってくると違いが分からなくなってくる

file()

build.gradle
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 オブジェクトを返す
  • 文字列だけでなく Filejava.nio.file.Path, java.net.URL, java.net.URI などを渡しても、よしなに判断してくれる

files()

build.gradle
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()

build.gradle
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 と同じ操作が可能

階層構造内のファイルを絞り込む

build.gradle
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
  • include()exclude() で、階層構造内のファイル(ディレクトリ)を絞り込むことができる
  • 引数の文字列には、Ant スタイルのパターン を指定する

Ant スタイルのパターン

  • Apache Ant で使用されていたファイルやディレクトリを絞り込むときのパターン
  • *:任意の文字列
    • foo/*1
    • *.java2
    • Test*3
  • **:任意のサブディレクトリ
    • **/foo4
    • **/bar/*5
    • fizz/**/*.java6

fileTree() には書き方がいくつかある

build.gradle
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() の引数に指定できる値

build.gradle
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")
        );
    }
}

file(), files() は様々なところで利用されている

  • 例えば CopySpecfrom() メソッドは、引数の値を files() に渡している

The given paths are evaluated as per Project.files(Object...).

  • Gradle の API でファイルやファイルのコレクションを受け取るところは、だいたい file() または files() を使っている
    • API ドキュメントを見ると書いてある

files() を使っている = FileTree を渡せる
これが分かると応用の幅が広がる


zip を解凍する方法

build.gradle
task foo {
    doFirst {
        copy {
            from zipTree("foo.zip")
            into "${buildDir}/foo"
        }
    }
}
  • zipTree() は、指定された zip ファイルの内容を FileTree にして取得するメソッド
  • zip 内のファイルをコピーする = zip を解凍する
  • コピーだけでなく、 files() を使ってファイル情報を受取る API に対して zipTree() の結果を渡すことができる

自作タスクの設定で利用する

build.gradle
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


  1. foo ディレクトリ直下の全ファイル 

  2. 拡張子が java のファイル 

  3. Test で始まるファイル 

  4. 任意のサブディレクトリ内の foo ディレクトリ(ファイル) 

  5. 任意のサブディレクトリ内の bar ディレクトリ直下の全ファイル 

  6. fizz ディレクトリ以下の任意のサブディレクトリにある拡張子が java のファイル 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away