このページでは、独自のセッティングやタスクの作成を紹介する。
このページを理解するには、このガイドの前のページ、 特に build.sbt と タスク・グラフ を読んである必要がある。
Keys は、キーをどのように定義するかを示すサンプル例が満載だ。 多くのキーは、Defaults で実装されている。
キーには 3 つの型がある。
SettingKey
と TaskKey
は .sbt ビルド定義で説明した。
InputKey
に関しては Input Tasks を見てほしい。
以下に Keys からの具体例を示す:
val scalaVersion = settingKey[String]("The version of Scala used for building.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")
キーのコンストラクタは、二つの文字列のパラメータを取る。
キー名("scala-version"
)と解説文("The version of scala used for building."
)だ。
.sbt ビルド定義でみた通り、SettingKey[T]
内の型パラメータ T
は、セッティングの値の型を表す。
TaskKey[T]
内の T
は、タスクの結果の型を表す。
また、.sbt ビルド定義でみた通り、セッティングはプロジェクトが再読み込みされるまでは固定値を持ち、 タスクは「タスク実行」の度(sbt のインタラクティブモードかバッチモードでコマンドが入力される度)に再計算される。
キーは .sbt ファイル、.scala ファイル、または auto plugin 内で定義する事が出来る。
有効化された auto plugin の autoImport
オブジェクト内で定義された val
は全て .sbt
ファイルに自動的にインポートされる。
タスクで使えるキーを定義したら、次はそのキーをタスク定義の中で使ってみよう。
自前のタスクを定義しようとしているかもしれないし、既存のタスクを再定義してようと考えているかもしれないが、
いずれにせよ、やることは同じだ。:=
を使ってタスクのキーになんらかのコードを関連付けよう:
val sampleStringTask = taskKey[String]("A sample string task.")
val sampleIntTask = taskKey[Int]("A sample int task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val library = (project in file("library"))
.settings(
sampleStringTask := System.getProperty("user.home"),
sampleIntTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
)
もしタスクに依存してるものがあれば、[タスク・グラフ][More-About-Settings]で説明したとおり value
を使ってその値を参照すればよい。
タスクを実装する上で一番難しい点は、多くの場合 sbt 固有の問題ではない。なぜならタスクはただの Scala コードだからだ。 難しいのはそのタスクが実行したいことの「本体」部分を書くことだ。
例えば HTML を整形したいとすると、今度は HTML のライブラリを利用したくなるかもしれない (おそらくビルド定義にライブラリ依存性を追加して、その HTML ライブラリに基づいたコードを書けばよいだろう)。
sbt には、いくつかのユーティリティライブラリや便利な関数があり、特にファイルやディレクトリの取り扱いには Scaladocs-IO にある API がしばしば重宝するだろう。
カスタムタスクから value
を使って他のタスクに依存するとき、
タスクの実行意味論 (execution semantics) に注意する必要がある。
ここでいう実行意味論とは、実際どの時点でタスクが評価されるかを決定するものとする。
sampleIntTask
を例に取ると、タスク本文の各行は一行ずつ正格評価 (strict evaluation) されているはずだ。
これは逐次実行の意味論だ:
sampleIntTask := {
val sum = 1 + 2 // first
println("sum: " + sum) // second
sum // third
}
実際には JVM は sum
を 3
とインライン化したりするかもしれないが、観測可能なタスクの作用は、各行ずつ逐次実行したものと同一のものとなる。
次に、startServer
と stopServer
という 2つのカスタムタスクを定義して、sampleIntTask
を以下のように書き換えたとする:
val startServer = taskKey[Unit]("start server")
val stopServer = taskKey[Unit]("stop server")
val sampleIntTask = taskKey[Int]("A sample int task.")
val sampleStringTask = taskKey[String]("A sample string task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
stopServer := {
println("stopping...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
stopServer.value // THIS WON'T WORK
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
}
)
sampleIntTask
を sbt のインタラクティブ・プロンプトから実行すると以下の結果となる:
> sampleIntTask
stopping...
starting...
sum: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:00:00 PM
何が起こったのかを考察するために、sampleIntTask
を視覚化してみよう:
素の Scala のメソッド呼び出しと違って、タスクの value
メソッドの呼び出しは正格評価されない。
代わりに、sampleIntTask
が startServer
タスクと stopServer
タスクに依存するということを表すマークとして機能する。
sampleIntTask
がユーザによって呼び出されると、sbt のタスクエンジンは以下を行う:
sampleIntTask
を評価する前にタスク依存性を評価する。(半順序)
非重複化を説明するために、sbt インタラクティブ・プロンプトから sampleStringTask
を実行する。
> sampleStringTask
stopping...
starting...
sum: 3
s: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:30:00 PM
sampleStringTask
は startServer
と sampleIntTask
の両方に依存し、
sampleIntTask
もまた startServer
タスクに依存するため、startServer
はタスク依存性として 2 度現れる。
しかし、value
はタスク依存性を表記するだけなので、評価は 1 回だけ行われる。
以下は sampleStringTask
の評価を視覚化したものだ:
もしタスク依存性を非重複化しなければ、Test / test
のタスク依存性として Test / compile
が何度も現れるため、テストのソースコードを何度もコンパイルすることになる。
stopServer
タスクはどう実装するべきだろうか?
タスクは依存性を保持するものなので、終了処理タスクという考えはタスクの実行モデルにそぐわないものだ。
最後の処理そのものもタスクになるべきで、そのタスクが他の中間タスクに依存すればいい。
例えば、stopServer
が sampleStringTask
に依存するべきだが、
その時点で stopServer
は sampleStringTask
と呼ばれるべきだろう。
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
},
sampleStringTask := {
val old = sampleStringTask.value
println("stopping...")
Thread.sleep(500)
old
}
)
これが動作することを調べるために、インタラクティブ・プロンプトから sampleStringTask
を実行してみよう:
> sampleStringTask
starting...
sum: 3
s: 3
stopping...
[success] Total time: 1 s, completed Dec 22, 2014 6:00:00 PM
何かが起こったその後に別の何かが起こることを保証するもう一つの方法は Scala を使うことだ。
例えば project/ServerUtil.scala
に簡単な関数を書いたとすると、タスクは以下のように書ける:
sampleIntTask := {
ServerUtil.startServer
try {
val sum = 1 + 2
println("sum: " + sum)
} finally {
ServerUtil.stopServer
}
sum
}
素のメソッド呼び出しは逐次実行の意味論に従うので、全ては順序どおりに実行される。 非重複化もされなくなるので、それは気をつける必要がある。
.scala
ファイルに大量のカスタムコードがあることに気づいたら、
プラグインを作って複数のプロジェクト間で再利用できないか考えてみよう。
以前にちょっと触れたし、詳しい解説はここにあるが、 プラグインを作るのはとても簡単だ。
このページは簡単な味見だけで、カスタムタスクに関しては Tasksページで詳細に解説されている。