Gradleでタスク自動化

# はじめに

みなさん、自動化してますか？

定期的に発生する面倒な作業は、自動化したいものです。作業を自動化できれば、手作業で起こるイライラやミスをなくすことができます。また、本来必要な作業に集中することもできます。

本記事では、[Gradle](https://gradle.org/)のタスクを用いて、定型的な作業を自動化する手法について紹介します。まず、Gradleについて簡単に説明した後、自動化の例を紹介します。最後に、定義したタスクの整理方法について説明します。

今回紹介する自動化の例は、下記の通りです。

* フォルダの同期
* ZIPファイルの作成
* コマンドの実行

GradleはAndroidアプリの標準的なビルドツールとしても使われていますので、Androidアプリの開発者であれば、すぐにお手元で試すこともできます。

# 対象とする読者

本記事は、下記の方々を対象としています。

* ビルドツールを使用した作業の自動化に興味のある方
* Androidアプリの開発などでGradleを使用したことがあるものの、作業の自動化には使用したことがない方

Gradleに詳しくない方でも問題ありません。Gradleのコード例は出てきますが、数行の簡単なものばかりですので、雰囲気はつかめるかと思います。また、複雑なコードには、追加して解説を行っています。

なお、Gradleのタスクの実行方法など、Gradleの使い方そのものについては、本記事では説明しません。また、本記事で紹介するタスクについても、そのタスクで使用できる個々のプロパティやメソッドを事細かには説明しません。Gradleの使用方法については、[Gradle User Manual](https://docs.gradle.org/current/userguide/userguide.html)やお使いのIDEのマニュアルを、タスクの詳細は、[Gradle Build Language Reference](https://docs.gradle.org/current/dsl/)を参照してください。

# Gradleとは

Gradleは、主にJavaプラットフォーム向けに用いられているビルドツールです。Androidアプリのビルドツールとしても採用されています。最近は、C/C++プロジェクトのビルドに対応するなど、総合的なビルドツールへと改良が進んでいます。

Gradleでは、build.gradleというファイルをプロジェクトのフォルダに置きます。このファイルの中に、ビルドの設定やタスクを記述していきます。

通常は、アプリなどのビルドに使用されるGradleですが、実は、ビルド以外の作業にも便利に使用できます。下記のように便利なタスクが用意されているためです。

* フォルダの同期（Syncタスク）
* ZIPファイル作成（Zipタスク）
* コマンド実行（Execタスク）

単純なタスクは、たったの4行で書くこともできます。

作業の自動化にGradleを使用すると、以下の利点があります。

<dl>
<dt>作成したタスクがほぼマルチプラットフォームで動作する</dt>
<dd>特にJavaやAndroidのプロジェクトは、Windowsに限らず、UbuntuやmacOSなど、様々なOS上でも開発できますので、このような性質はうれしいものです。なお、「ほぼ」と書いたとおり、一部にはOS依存のタスクもあります。この部分については、後ほど説明します。</dd>
<dt>複数人での開発では、初めの1人以外はGradleを事前にダウンロードする必要がない</dt>
<dd>複数人で開発を進めようとすると、使用するツールや、そのバージョンを合わせることが場合によっては必要です。Gradleの場合は、初めにGradle Wrapperを配置する人以外はGradleをインストールする必要がありません。Gradle Wrapperを使用すると、タスクを初めて実行した際に、指定のバージョンのGradleが自動的にダウンロードされます。</dt>
</dl>

# Gradleでタスク自動化

本章では、Gradleを使用した作業の自動化の例を紹介します。具体的には、下記の3つです。

* フォルダの同期
* ZIPファイルの作成
* コマンドの実行

まず、その作業を自動化するメリットを説明します。その後、Gradleでどのように自動化するのか、サンプルコードと合わせて説明します。

## フォルダの同期（Syncタスク）

### 背景

開発作業では、プロジェクトが格納されているフォルダの外部から、ファイルの取り込みが必要なことがあります。逆に、プロジェクトのフォルダの内部から、外部へのコピーが必要な場合もあります。例えば、プロジェクト外のソースコードやリソースを取り込む場合や、成果物を外部に配置する場合です。

### 手作業の問題

そのようなファイルのコピー作業ですが、手作業で行う場合、問題がいくつか発生します。

<dl>
<dt>逆方向にコピーしてしまうリスクがある</dt>
<dd>フォルダAからBへコピーしたかったのに、間違ってBからAにコピーしてしまうことが起こりえます。この過ちを犯してしまうと、編集した成果を失うなど、壊滅的な結果となることがあります。</dd>
<dt>コピー先のフォルダが深い場合は手間がかかる</dt>
<dd>コマンドプロンプトなどのCUI操作の場合はキーをたくさん打たなければなりません。エクスプローラなどのGUI操作の場合は、クリックを何度も繰り返す必要があります。</dd>
<dt>余分なファイルのコピーを防ごうとすると手間がかかる</dt>
<dd>一時的に作成されるファイルや、個人設定が格納されているファイルのコピーを避けようとすると、コピー対象のファイルを毎回指定する必要があります。手間を省いて、フォルダをまるごとコピーする場合、ファイルが多いと時間がかかります。</dd>
<dt>削除したファイルの扱いは別途行う必要がある</dt>
<dd>コピー元で削除したファイルをコピー先から消す場合は、コピー先から、削除されたファイルを探して削除しなければなりません。上位のフォルダを削除してコピーし直す方法もありますが、その方法ではコピーに時間がかかることがあります。</dd>
</dl>

### Syncタスク

そこで、[Syncタスク](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Sync.html)の出番です。

Syncタスクを使用すると、指定のフォルダ同士の内容を同じにする、つまり同期することができます。例えば、otherフォルダの内容をsourceフォルダと同じにするには、下記のようにタスクを定義し、実行します。

```gradle:build.gradle
task syncToOther(type: Sync) {
    from 'source'
    into 'other'
}
```

### 同期するファイルの指定

同期するファイルを指定することもできます。例えば、source内にある、srcフォルダ配下のJavaソースファイルのみを同期するには、下記のように定義、実行します。

```gradle:build.gradle
task syncToOther(type: Sync) {
    from('source') {
        include 'src/**/*.java'
    }
    into 'other'
}
```

なお、「src/\*\*/\*.java」の\*\*は、0個以上の任意のフォルダを表し、\*は0文字以上のファイル名を表します。

### 問題がSyncタスクで解決

以上のように、Syncタスクを使用することで、手作業での問題が解決できます。

<dl>
<dt>逆方向にコピーしてしまうリスクがある</dt>
<dd>自動化していますので、一度成功すれば、逆方向へのコピーは起こりえません。</dd>
<dt>コピー先のフォルダが深い場合は手間がかかる</dt>
<dd>タスクを一度定義すれば、手間は増えません。</dd>
<dt>余分なファイルのコピーを防ごうとすると手間がかかる</dt>
<dd>Syncタスクでは、同期したいファイルを指定できます。</dd>
<dt>削除したファイルの扱いは別途行う必要がある</dt>
<dd>Syncタスクでは、同期元にないファイルは同期先から自動的に削除されます。</dd>
</dl>

## ZIPファイルの作成（Zipタスク）

### 背景

ビルド成果物やソースコードなどをリリースする際に、ZIPファイルにまとめることがあります。プロジェクトのフォルダをそのままZIPファイルにまとめることもあれば、ビルド成果物とソースコードなどを、それぞれ別のフォルダに配置してまとめることもあります。

### 手作業の問題

そのようなアーカイブ作業ですが、手作業で行うと問題がいくつか発生します。

<dl>
<dt>ファイルの配置が面倒</dt>
<dd>リリースのたびに、配置をやり直す必要があります。これは手間です。以前とは異なるフォルダに配置してしまったり、フォルダの名前を間違えてしまったりなど、配置ミスが起こるリスクがあります。</dd>
<dt>余分なファイルまで格納してしまう可能性がある</dt>
<dd>フォルダを丸ごとコピーして配置する場合、その中に余分なファイルが入っていると、そのファイルまで格納されてしまいます。いっぽうで、削除しようとすると手間がかかります。余分なファイルの例としては、ビルドの中間生成物やキャッシュファイル、個人用の設定ファイルなどがあります。</dd>
<dt>ビルドし忘れて古いバージョンのものをリリースしてしまうリスクがある</dt>
<dd>作業ブランチを切り替えながら作業していると、手元のビルド成果物がリリース対象のものではない可能性があります。ビルドをし忘れてリリースすると、想定とは異なるバージョンのものをリリースしてしまいます。</dd>
</dl>

### Zipタスク

そこで、[Zipタスク](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Zip.html)の出番です。

Zipタスクを使用すると、指定したフォルダの内容をZIPファイルにまとめることができます。例えば、abcフォルダの内容をrelease.zipというZIPファイルにまとめるには、下記のようなタスクを定義します。

```gradle:build.gradle
task release(type: Zip) {
    archiveName = 'release.zip'
    from 'abc'
}
```

### 余分なファイルを除く

特定のファイルを対象から除くこともできます。例えば、abcフォルダ以下にあるlocal.propertiesファイルすべてを対象外とするには、下記のようにします。

```gradle:build.gradle
task release(type: Zip) {
    archiveName = 'release.zip'
    from('abc') {
        exclude '**/local.properties'
    }
}
```

### ZIPの前にビルドする

[タスク間の依存関係](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:task_dependencies)を定義すれば、ZIPファイルを作成する前にビルドすることもできます。

例えば、Androidプロジェクトにおいて、ZIPファイルを作成する前にdebug用のapkをビルドできます。下記は、debug用apkをビルドし、application.apkに改名したうえで、binフォルダに配置してZIPファイルを作成する例です。

```gradle:build.gradle
task release(type: Zip, dependsOn: 'app:assembleDebug') {
    archiveName = 'release.zip'
    into('bin') {
        from 'app/build/outputs/apk/debug/app-debug.apk'
        rename '.+', 'application.apk'
    }
}
```

これまでよりも少し複雑なタスクのため、簡単に意味を解説します。

まず、dependsOnに指定された「app:assembleDebug」の意味です。これは、[Android Gradle Plugin](https://developer.android.com/studio/releases/gradle-plugin)に定義されている、debug用apkをビルドするためのタスクです。dependsOnを使用すると、定義したZipタスクを実行する前に、app:assembleDebugタスクを実行できます。こうすることで、定義したZipタスクが実行される前に、ビルドが実行されます。

debug用apkは、<プロジェクトのフォルダ>/app/build/outputs/apk/debug/app-debug.apkに出力されます。このため、そのファイルをapplication.apkに改名します。なお、debug用apkの出力先は、Android Gradle Pluginのバージョンによって異なることがあります。

ファイルの改名は、renameで指定しています。renameは第1引数の正規表現にマッチした文字列を第2引数の文字列に置き換える働きをします。第1引数の「.+」は、すべての文字にマッチする正規表現です。fromでファイルをひとつのみ指定しているため、ファイル名の「app-debug.apk」が正規表現にマッチし、「application.apk」に置き換わります。

### 問題がZipタスクで解決

以上のように、Zipタスクを使用することで、手作業での問題が解決できます。

<dl>
<dt>ファイルの配置が面倒</dt>
<dd>一度定義すれば、どれだけフォルダが深くても手間はかかりません。</dd>
<dt>余分なファイルまで格納してしまう可能性がある</dt>
<dd>格納対象のファイルを指定することができます。</dd>
<dt>ビルドし忘れて古いバージョンのものをリリースしてしまうリスクがある</dt>
<dd>Zipタスクを実行する前に、ビルド用のタスクを自動的に実行することができます。</dd>
</dl>

## コマンドの実行（Execタスク）

### 背景

自作のスクリプトや、OSにインストールされているコマンドなどを実行したい場合があります。例えば、データの生成・加工を行うスクリプトや、Androidの開発環境であればadbコマンドなどです。

### 手作業の問題

さて、そのようなコマンドの実行ですが、手作業で行うと問題がいくつか発生します。

<dl>
<dt>スクリプト言語ごとに起動方法が異なる</dt>
<dd>例えば、Pythonで書かれたスクリプトはpythonコマンドで、Rubyで書かれたスクリプトはrubyコマンドで起動しなければなりません。Unix系OSであれば、shebangを使用すれば、どのコマンドから起動するのか、スクリプト内に記述することができますが、WindowsではCygwinなどのシェル環境を導入しない限り、shebangは使用できません。</dd>
<dt>各スクリプトに共通する設定の扱い</dt>
<dd>例えば、プロジェクトのフォルダのパスなど、各スクリプトに共通する設定はどのように扱うべきでしょうか。コマンドライン引数に毎回渡すのは手間がかかります。環境変数に設定する方法もありますが、他のプロジェクトと名前が重複しないようにしなければなりません。</dd>
<dt>毎回同じオプションを指定するのは手間がかかる</dt>
<dd>定型的な処理をスクリプトやコマンドで行う場合、指定するオプションはいつも同じです。それを毎回指定するのは手間ですし、指定を誤ってしまうリスクもあります。そのために別のスクリプトを作成するという方法もありますが、管理するスクリプトがひとつ増えてしまいます。</dd>
</dl>

### Execタスク

そこで、[Execタスク](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Exec.html)の出番です。

例えば、WindowsでPythonスクリプトexample.pyを実行するには、次のようなタスクを定義します。

```gradle:build.gradle
task example(type: Exec) {
    commandLine 'cmd', '/c', 'python', 'example.py'
}
```

また、WindowsからAndroid端末にadb pushでファイルを転送するには、次のようなタスクを定義します。Android Gradle Pluginでは、adbの実行ファイルのパスが[adbExecutableプロパティ](http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.AppExtension.html#com.android.build.gradle.AppExtension:adbExecutable)に格納されています。そのため、簡単にadbを実行することができます。

```gradle:build.gradle
task pushFile(type: Exec) {
    commandLine 'cmd', '/c', ${android.adbExecutable}, 'push' 'file.txt', '/sdcard'
}
```

### ExecタスクはOS依存

ただし、Execタスクの定義には注意が必要です。commandLineに指定する内容は、OSによって異なります。そのOS専用のコマンドを使用する場合はもちろん、例えば、Pythonインタプリタを起動してexample.pyを実行する方法についても、WindowsとUnix系とで下記のように違いがあります。

```gradle
// Windows
commandLine 'cmd', '/c', 'python', 'example.py'

// Unix系（shebangなし）
commandLine 'python', 'example.py'

// Unix系（shebangを使用）
commandLine './example.py'
```

同じタスクを複数のOSで実行するには、どうすれば良いでしょうか。

解決案としては、下記のGradleプラグイン（[gradle-cross-platform-exec-plugin](https://github.com/jlouns/gradle-cross-platform-exec-plugin), [osdetector-gradle-plugin](https://github.com/google/osdetector-gradle-plugin)）を導入するという方法があります。これらのGradleプラグインを使用すると、OSの違いをGradleプラグイン側で吸収したり、実行時のOSを取得してcommandLineに指定する内容を分けることができます。

<dl>
<dt>gradle-cross-platform-exec-plugin</dt>
<dd>OSがWindowsの場合は、commandLineの先頭に「'cmd', '/c'」を、それ以外の場合は何も付けずにコマンドを実行するタスクCrossPlatformExecを追加するGradleプラグインです。使用方法は、typeに指定したExecをCrossPlatformExecに変更し、「'cmd', '/c'」を削除します。自作のスクリプトを実行する際に便利です。</dd>
<dt>osdetector-gradle-plugin</dt>
<dd>OSを判別できるプロパティを追加します。OSにインストールされているコマンドを実行する際に便利です。</dd>
</dl>

### 共通設定

複数のExecタスクで共通する設定を定義するには、[ローカル変数もしくはextra properties](https://docs.gradle.org/current/userguide/writing_build_scripts.html#sec:declaring_variables)を使用します。extra propertiesを使用すると、子プロジェクトにもその設定を適用できるという利点があります。extra propertiesを利用する場合の例は、次の通りです。

```gradle:build.gradle
ext {
    buildDirectory = file('build')
}

task example1(type: Exec) {
    commandLine 'cmd', '/c', 'python', 'example.py', ${buildDirectory}
}

task example2(type: Exec) {
    commandLine 'cmd', '/c', 'python', 'example2.py', ${buildDirectory}
}
```

### 問題がExecタスクで解決

以上のように、Execタスクを使用することで、手作業での問題が解決できます。

<dl>
<dt>スクリプト言語ごとに起動方法が異なる</dt>
<dd>Gradleを介して統一的な方法で実行できます。</dd>
<dt>各スクリプトに共通する設定の扱い</dt>
<dd>ローカル変数やextra propertiesに定義すれば、定義を1箇所にまとめることができます。</dd>
<dt>毎回同じオプションを設定するのは手間がかかる</dt>
<dd>タスク内で一度指定すれば、毎回同じオプションで実行されるので、手間はかかりません。</dd>
</dl>

# タスクの整理

## 最大の問題

以上で、タスクの紹介は終わりです。これまでに以下の自動化手法を紹介してきました。

* フォルダの同期（Syncタスク）
* ZIPファイルの作成（Zipタスク）
* コマンドの実行（Execタスク）

開発作業が進むにつれて、定義したタスクはどんどん増えていくと思います。しかし、ここで大きな問題に直面することになります。

<dl>
<dt>定義したタスクを忘れてしまう</dt>
<dd>たまにしか使わないタスクは、定義したことすら忘れがちです。英語でタスク名を付けていると、単数形だったか複数形だったかなど、文字単位で忘れることもあります。</dd>
<dt>他の人にプロジェクトを引き継ぐ際に手間がかかる</dt>
<dd>定義したタスクを引き継ぎ相手にひとつひとつ説明するのは大変です。引き継ぎ相手にタスクを読み解いてもらうにも時間がかかります。</dd>
</dl>

## タスクに説明を設定

そこで、定義したタスクに説明を設定しましょう。[groupとdescription](https://docs.gradle.org/current/userguide/authoring_maintainable_build_scripts.html#sec:improving_task_discoverability)を使用することで、タスクの分類と説明を定義することができます。例は以下の通りです。

```gradle:build.gradle
task example(type: Exec) {
    group = '例'
    description = 'Execタスクの例'

    commandLine 'cmd', '/c', 'python', 'example.py'
}

task syncToOther(type: Sync) {
    group = '例'
    description = 'Syncタスクの例'

    from 'source'
    into 'other'
}
```

例のとおり、groupとdescription内の記述は、日本語でも問題ありません。ただし、日本語を記述する場合は、ファイルの文字エンコーディングはUTF-8にする必要があります。

「gradle tasks」（Gradlew Wrapperを使用している場合は「gradlew tasks」）とコンソールから実行すると、次のような出力となります。

```
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'description'.
components - Displays the components produced by root project 'description'. [incubating]
dependencies - Displays all dependencies declared in root project 'description'.
dependencyInsight - Displays the insight into a specific dependency in root project 'description'.
dependentComponents - Displays the dependent components of components in root project 'description'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'description'. [incubating]
projects - Displays the sub-projects of root project 'description'.
properties - Displays the properties of root project 'description'.
tasks - Displays the tasks runnable from root project 'description'.

例 tasks
-------
example - Execタスクの例
syncToOther - Syncタスクの例

To see all tasks and more detail, run gradle tasks --all

To see more detail about a task, run gradle help --task <task>


BUILD SUCCESSFUL in 18s
1 actionable task: 1 executed
```

Gradle標準のタスクが出力されたあとに、定義したタスクが出力されています。同じgroupのタスクはひとつにまとめられて、それぞれのタスクの名前と、descriptionに指定した説明が出力されています。

## 問題をgroupとdescriptionで解決

このように、groupの定義でタスクのグループ分けを、descriptionの定義で各タスクに説明を設定できます。

「gradle tasks」（「gradlew tasks」）さえ覚えておけば、定義したタスクを忘れた場合でもすぐに確認できます。他の人にプロジェクトを引き継ぐ際にも、追加説明の分量を少なくできます。

# まとめ

本記事では、Gradleのタスクを用いて、定型的な作業を自動化する手法について説明しました。例として、フォルダの同期（Syncタスク）、ZIPファイルの作成（Zipタスク）、コマンドの実行（Execタスク）を取り上げました。数行の定義だけで、複雑なタスクも自動化できることがお分かりいただけたかと思います。

また、定義したタスクを整理する方法についても説明しました。タスクに説明を付けておくことで、「gradle tasks」を実行するだけで内容を確認できますので、個々のタスクを記憶する必要はありません。