アカウンティング・サース・ジャパン Advent Calendar の22日目!
はじめに
自分がsbtを使い始める上で、まず公式のGetting Startedを読んでみたのですが、なかなか理解しにくい部分や、今後のためにまとめておきたい部分などがあったので、一部についてまとめ直しや補足をしています。これからsbtを始める方は公式のGetting Startedの補足に見ていただけばと思います。前回はキーの概念をまとめました。今回はタスクの実装の部分をまとめていきます。
公式の Getting Started
http://www.scala-sbt.org/0.13/docs/Getting-Started.html
タスクを実装する
前回まででキーの概念がわかったので、今回は独自でタスクを実装してみます。
既存のタスクの前後に処理を行う
まずsbtを使っていてやりたくなることとしては、既存のタスクを実行する前後で、何か自前の処理を入れることではないでしょうか。例えばcompileの後でログを出してみましょう。
lazy val root = (project in file(".")).
settings(
...
(compile in Compile) := {
val result = (compile in Compile).value
println("Finish compiling !")
result
}
)
実行してみましょう。
> compile
[info] Updating {file:/Users/mtn/study/scala/sbt-tutorial/}root...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 2 Scala sources to /Users/mtn/study/scala/sbt-tutorial/target/scala-2.11/classes...
Finish compiling !
いい感じですね。このようにタスクの定義は、TaskKey[T]のキーに対して、T型の値を返すコードブロックを設定することで行います。自前のタスクの中で既存のタスクを実行してその結果を得るには、TaskKey#value を使います。今回の例は一見無限の再帰にも見えますが、sbtがオリジナルのcompileタスクを中で呼び出してくれています。
それでは今度は、コンパイル前にもログを入れてみます。
lazy val root = (project in file(".")).
settings(
...
(compile in Compile) := {
println("Start compiling !")
val result = (compile in Compile).value
println("Finish compiling !")
result
}
)
実行。
> compile
[info] Updating {file:/Users/mtn/study/scala/sbt-tutorial/}root...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 2 Scala sources to /Users/mtn/study/scala/sbt-tutorial/target/scala-2.11/classes...
Start compiling !
Finish compiling !
おっと、compile前に出るはずのログがcompile後に出てしましました。この動作はsbtで癖がある部分だと感じますが、タスク定義の中で別のタスクを呼び出す(.value)記述をすると、そのタスクの実行時に、sbtは依存関係を整理して、依存するタスクをはじめに実行してくれるのです。ドキュメントを読むとよりイメージが湧きやすいと思いますが、sbtはタスクの依存グラフを作って、タスクの並列実行および、同じタスクが1度だけ実行されるように調整したりします。タスクの依存関係といえば前回 inspectコマンド打った時にも出てきていました。
では、既存のタスクの前に何かを行いたい場合はどうすれば良いのでしょうか。1つの方法としては、TaskKey#dependsOnを利用することができます(これは最新バージョンのドキュメントに記述を見つけられなかったので、もしかして使わないほうがいいいのかも?)。今回の例の場合は、
lazy val root = (project in file(".")).
settings(
...
(compile in Compile) := {
val result = (compile in Compile).dependsOn(Def.task {
println("Start compiling !")
}).value
println("Finish compiling !")
result
}
)
実行
> compile
Start Compiling !
[info] Updating {file:/Users/mtn/study/scala/sbt-tutorial/}root...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 2 Scala sources to /Users/mtn/study/scala/sbt-tutorial/target/scala-2.11/classes...
Finish Compiling !
いい感じですね。ここで、Def.task {...} という書き方が出てきましたが、これはタスクをtaskKey := {...} 以外でも定義できる書き方になっています。
オリジナルのタスクを作成する
独自のタスクを定義するには、1.キーの定義、2.キーに対するタスクの実装が必要になります。
例として、ファイルを生成するタスクを定義してみます。
lazy val genFile = taskKey[Unit]("generate files sample")
...
lazy val root = (project in file(".")).
settings(
...
genFile := {
IO.write((scalaSource in Compile).value / "Foo.scala", "object Foo {}")
}
)
これを実行すると、src/main/scala にFoo.scalaファイルが生成されます。1行目はTaskKeyを定義していて、今回はファイルを出力するのでタスクの戻り値はなしで(Unit型)にしています。ファイルの生成についての詳細はこちら。
次にこれを少し改良して、ファイルを生成した後にソースファイルのリストを出力できるようにしてみます。
lazy val genFile = taskKey[Unit]("generate files sample")
...
lazy val root = (project in file(".")).
settings(
...
genFile := {
IO.write((scalaSource in Compile).value / "Foo.scala", "object Foo {}")
(unmanagedSources in Compile).value.foreach(println)
}
)
ここで、unmanagedSourcesはTaskKey[Seq[File]]で、ソースディレクトリにあるソースのリストを取得するタスクのキーです。実行してみます。
> genFile
/Users/mtn/study/scala/sbt-tutorial/src/main/scala/Hoge.scala
/Users/mtn/study/scala/sbt-tutorial/hw.scala
[success] Total time: 0 s, completed 2016/12/20 21:09:01
あれ、Foo.scalaが出てないですね。これは先ほど出てきた、依存タスクの実行順序によるものですね。unmanagedSources.valueがgenTask実行前に実行されています。一連のタスクを実行するときに、順序通りに実行できるとよいのですが...。実はこれにはsbtの0.13.8から入っているDef.sequentialを利用することできます。今回の例では、以下の様に定義できます。
lazy val genFile = taskKey[Unit]("generate files sample")
...
lazy val root = (project in file(".")).
settings(
...
genFile := {
Def.sequential(
Def.task { IO.write((scalaSource in Compile).value / "Foo.scala", "object Foo {}") },
Def.task { (unmanagedSources in Compile).value.foreach(println) }
).value
}
)
実行
> genFile
/Users/mtn/study/scala/sbt-tutorial/src/main/scala/Foo.scala
/Users/mtn/study/scala/sbt-tutorial/src/main/scala/Hoge.scala
/Users/mtn/study/scala/sbt-tutorial/hw.scala
[success] Total time: 0 s, completed 2016/12/20 21:18:13
うまくFoo.scalaが出てますね。Def.sequentialに複数のタスクを並べるとそれを順序通りに実行してくれます。
おわりに
今回はsbtのタスクの実装についてまとめました。sbtはタスクの依存関係の解決と引き換えに、ビルド定義の記述から見ると直感的ではない動きをするので慣れが必要ですね。
タスクが実装できるようになったら、便利なビルドタスクを作っていろんなプロジェクトで共有したいですね。ということで、次回はプラグインについてまとめてみる予定です。(Advent Calendarには収まらなそうなので、来年あたりに書きます。)