Locked History Actions

sbt/dt_Tasks

タスク

https://github.com/harrah/xsbt/wiki/Tasksの訳(2011/10/30時点)

Tasks and settings are now introduced in the getting started guide, which you may wish to read first. This older page has some additional detail.

Wiki Maintenance Note: This page should have its overlap with the getting started guide cleaned up, and just have any advanced or additional notes. It should maybe also be consolidated with TaskInputs.

イントロダクション

sbt0.10+は、新たなsettingシステムに統合された新たなタスクシステムを持つ。 両者とも値を生成するのだが、二つには大きな違いがある。

Settingはプロジェクトロード時に評価されるが、タスクはオンデマンドで実行される。ユーザのコマンドへの応答のために行われることもある。 プロジェクトのロード時にはsettingとその依存は固定されるが、これに対してタスクはその実行中に新しいタスクを導入することもある(タスクはflatMapを持つが、settingは持たない)。

特徴

タスクシステムには、様々な特徴がある。

  • settingシステムと統合されていることにより、タスクはsettingのように簡単に追加、削除、変更することができる。
  • インプットタスク(メソッドタスクの後継)では、その引数の構文をパーサコンビネータを使って定義することができる。これにより、フレキシブルな構文を実現できるうえ、コマンドと同様のタブ補完が可能になる。
  • タスクは値を生成する。他のタスクからその値をmapあるいはflatMapメソッドでアクセスすることができる。
  • flatMapメソッドによって、動的にタスクグラフを変更することができる。他のタスクの結果に基づく実行グラフにタスクを注入することができる。
  • try/catch/finallyと同様の、タスクのエラーをハンドリングする仕組みがある。
  • Each task has access to its own Logger that by default persists the logging for that task at a more verbose level than is initially printed to the screen.

これらの特徴について各セクションで説明する。 以下のコードスニペットは.scalaでもbuild.sbtでも使用することができる。

新しいタスクの定義

Hello Worldサンプル

project/Build.scala

import sbt._
import Keys._

object HelloBuild extends Build {
  val hwsettings = Defaults.defaultSettings ++ Seq(
    organization := "hello",
    name         := "world",
    version      := "1.0-SNAPSHOT",
    scalaVersion := "2.9.0-1"
  )

  val hello = TaskKey[Unit]("hello", "Prints 'Hello World'")

  val helloTask = hello := {
    println("Hello World")
  }

  lazy val project = Project (
    "project",
    file ("."),
    settings = hwsettings ++ Seq(helloTask)
  )
}

コマンドラインから"sbt hello"と入力してタスクを起動してみよう。 次に"sbt tasks"と入力して、このタスクがリストされるのを見てみよう。

キーの定義

新しいタスクを宣言するには、フルコンフィギュレーション(訳注:.scalaファイル)中でTaskKeyを定義する。

val sampleTask = TaskKey[Int]("sample-task")

valの名称はscalaコード中でタスクを参照するときに使用される。 TaskKeyのメソッドに渡された文字列は、コマンドライン等の実行時に使用される。 コンベンションとして、Scalaの識別子はキャメルケースにし、実行時識別子はハイフンを使う。 TaskKeyの型パラメータ(ここではInt)はタスクによって生成される値のタイプだ。

例として、他のタスクを定義してみよう。

val intTask = TaskKey[Int]("int-task")
val stringTask = TaskKey[String]("string-task")

これらのエントリはbuild.sbtでも使えるし、Project.settingsのシーケンスの一部としても使用できる(Full COnigurationを参照のこと)。

タスクの実装

キーを定義したら、タスクの実装として三つのパートが必要になる。

  • そのタスクに必要なsettingや他タスクを決める。これらがタスクの入力になる。
  • これらの入力を受け取り、値を生成する関数を定義する。
  • タスクが実行されるべきスコープを決める。

settingのパートが組み合わされるのと同様に、これらのパートが組み合わされる。

入力無しのタスク

:=を使うことにより、引数無しのタスクを定義することができる。

intTask := 1 + 2

stringTask := System.getProperty("user.name")

sampleTask := {
  val sum = 1 + 2
  println("sum: " + sum)
  sum 
}

イントロダクションで言及したように、タスクはオンデマンドで評価される。 例えば、sample-taskが起動されると「和」を表示する。 各実行の間でusernameが変化すれば、string-taskは各実行において別の値を取得することになる (実行中には、各タスクは多くとも一度だけ評価される)。 これに対して、settingがプロジェクトロード時に一度だけ評価され、次のreloadまでは同じ阿智亜を保持する

入力のあるタスク

他のタスクやsettingを入力としてとるタスクは<<=を使って定義される。 ほとんどの場合、右辺としては、他のsettingやタスクについてmapやflatMapを呼び出すことになる。 mapやflatMapに対する関数引数がタスクの本体だ。 int-taskで生成された値に1を加えてそれを返すタスクを、以下では二つの方法で定義してみる。

sampleTask <<= intTask map { (count: Int) => count + 1 }

sampleTask <<= intTask map { _ + 1 }

複数入力のハンドリングの仕方はsettingと同じようにする。 mapやflatMapは入力のタプルにも使用可能だ。

stringTask <<= (sampleTask, intTask) map { (sample: Int, intValue: Int) =>
        "Sample: " + sample + ", int: " + intValue
}

タスクのスコープ

settingと同様に、タスクの定義ではスコープを指定できる。 例えば、compileとtestのスコープで異なるcompileタスクが存在する。 タスクのスコープはsettingと同様の方法で定義できる。 以下の例では、test:sample-taskはcompile:int-taskの結果を利用している。

sampleTask.in(Test) <<= intTask.in(Compile).map { (intValue: Int) => 
        intValue * 3
}

// more succinctly:
sampleTask in Test <<= intTask in Compile map { _ * 3 }

インラインタスクキー

一般には推奨されないが、タスクキーをインラインで指定することが可能だ。

TaskKey[Int]("sample-task") in Test <<= TaskKey[Int]("int-task") in Compile map { _ * 3 }

The type argument to TaskKey must be explicitly specified because of SI-4653. It is not recommended because:

  • Tasks are no longer referenced by Scala identifiers (like sampleTask), but by Strings (like "sample-task") The type information must be repeated. Keys should come with a description, which would need to be repeated as well.

On precedence

As a reminder, method precedence is by the name of the method.

  • Assignment methods have the lowest precedence. These are methods with names ending in =, except for !=, <=, >=, and names that start with =. Methods starting with a letter have the next highest precedence. Methods with names that start with a symbol and aren't included in 1. have the highest precedence. (This category is divided further according to the specific character it starts with. See the Scala specification for details.)

Therefore, the second variant in the previous example is equivalent to the following:

(sampleTask in Test) <<= (intTask in Compile map { _ * 3 })

既存のタスクを変更する

このセクションでは、以下のキー定義を使用しているが、 これらはフルコンフィギュレーション中のBuildオブジェクトに記述されなければならない。 そうではなく、上述したように、キーはインラインで指定されてもよい。

val unitTask = TaskKey[Unit]("unit-task")
val intTask = TaskKey[Int]("int-task")
val stringTask = TaskKey[String]("string-task")

例それ自体はbuild.sbtファイル中でも有効だし、Project.settingsへ提供されるシーケンスの一部としても有効だ。

一般的なケースでは、以前のタスクを入力と宣言することにより、タスクを変更する。

// 初期定義
intTask := 3

// 以前の定義を参照して定義をオーバライドする
intTask <<= intTask map { (value: Int) => value + 1 }

以前のタスクを入力として宣言しなければ、タスクを完全にオーバライドすることになる。 以下の例ではこれを行なっている。つまり、int-taskを実行すると、単に#3を表示するだけだ。

intTask := {
        println("#1")
        3
}

intTask := {
        println("#2")
        5
}

intTask <<= sampleTask map { (value: Int) => 
        println("#3")
        value - 3
}

To apply a transformation to a single task, without using additional tasks as inputs, use ~=. This accepts the function to apply to the task's result:

intTask := 3

// increment the value returned by intTask
intTask ~= { (x: Int) => x + 1 }

タスク操作

The previous sections used the map method to define a task in terms of the results of other tasks. This is the most common method, but there are several others. The examples in this section use the task keys defined in the previous section. Dependencies

To depend on the side effect of some tasks without using their values and without doing additional work, use dependOn on a sequence of tasks. The defining task key (the part on the left side of <<=) must be of type Unit, since no value is returned.

unitTask <<= Seq(stringTask, sampleTask).dependOn

To add dependencies to an existing task without using their values, call dependsOn on the task and provide the tasks to depend on. For example, the second task definition here modifies the original to require that string-task and sample-task run first:

intTask := 4

intTask <<= intTask.dependsOn(stringTask, sampleTask)

Streams: Per-task logging

New in sbt 0.10+ are per-task loggers, which are part of a more general system for task-specific data called Streams. This allows controlling the verbosity of stack traces and logging individually for tasks as well as recalling the last logging for a task. Tasks also have access to their own persisted binary or text data.

To use Streams, map or flatMap the streams task. This is a special task that provides an instance of TaskStreams for the defining task. This type provides access to named binary and text streams, named loggers, and a default logger. The default Logger, which is the most commonly used aspect, is obtained by the log method:

myTask <<= streams map { (s: TaskStreams) =>
  s.log.debug("Saying hi...")
  s.log.info("Hello!")
}

You can scope logging settings by the specific task's scope:

logLevel in myTask := Level.Debug

traceLevel in myTask := 5

To obtain the last logging output from a task, use the last command:

$ last my-task
[debug] Saying hi...
[info] Hello!

The verbosity with which logging is persisted is controlled using the persist-log-level and persist-trace-level settings. The last command displays what was logged according to these levels. The levels do not affect already logged information.

Handling Failure

This section discusses the andFinally, mapFailure, and mapR methods, which are used to handle failure of other tasks. andFinally

The andFinally method defines a new task that runs the original task and evaluates a side effect regardless of whether the original task succeeded. The result of the task is the result of the original task. For example:

intTask := error("I didn't succeed.")

intTask <<= intTask andFinally { println("andFinally") }

This modifies the original intTask to always print "andFinally" even if the task fails.

Note that andFinally constructs a new task. This means that the new task has to be invoked in order for the extra block to run. This is important when calling andFinally on another task instead of overriding a task like in the previous example. For example, consider this code:

intTask := error("I didn't succeed.")

otherIntTask <<= intTask andFinally { println("andFinally") }

If int-task is run directly, other-int-task is never involved in execution. This case is similar to the following plain Scala code:

def intTask: Int =
  error("I didn't succeed.")

def otherIntTask: Int =
  try { intTask }
  finally { println("finally") }

intTask()

It is obvious here that calling intTask() will never result in "finally" being printed.

mapFailure

mapFailure accepts a function of type Incomplete => T, where T is a type parameter. In the case of multiple inputs, the function has type Seq[Incomplete] => T. Incomplete is an exception with information about any tasks that caused the failure and any underlying exceptions thrown during task execution. The resulting task defined by mapFailure fails if its input succeeds and evaluates the provided function if it fails.

For example:

intTask := error("Failed.")

intTask <<= intTask mapFailure { (inc: Incomplete) =>
        println("Ignoring failure: " + inc)
        3
}

This overrides the int-task so that the original exception is printed and the constant 3 is returned.

mapFailure does not prevent other tasks that depend on the target from failing. Consider the following example:

intTask := if(shouldSucceed) 5 else error("Failed.")

// return 3 if int-task fails.  if it succeeds, this task will fail
aTask <<= intTask mapFailure { (inc: Incomplete) => 3 }

// a new task that increments the result of int-task
bTask <<= intTask map { _ + 1 }

cTask <<= (aTask, bTask) map { (a,b) => a + b }

The following table lists the results of each task depending on the initially invoked task:

invoked task int-task result a-task result b-task result c-task result overall result int-task failure not run not run not run failure a-task failure success not run not run success b-task failure not run failure not run failure c-task failure success failure failure failure int-task success not run not run not run success a-task success failure not run not run failure b-task success not run success not run success c-task success failure success failure failure

The overall result is always the same as the root task (the directly invoked task). A mapFailure turns a success into a failure, and a failure into whatever the result of evaluating the supplied function is. A map fails when the input fails and applies the supplied function to a successfully completed input.

In the case of more than one input, mapFailure fails if all inputs succeed. If at least one input fails, the supplied function is provided with the list of Incompletes. For example:

cTask <<= (aTask, bTask) mapFailure { (incs: Seq[Incomplete]) => 3 }

The following table lists the results of invoking c-task, depending on the success of aTask and bTask:

a-task result

b-task result

c-task result

failure

failure

success

failure

success

success

success

failure

success

success

success

failure

mapR

mapR accepts a function of type Result[S] => T, where S is the type of the task being mapped and T is a type parameter. In the case of multiple inputs, the function has type (Result[A], Result[B], ...) => T. Result has the same structure as Either[Incomplete, S] for a task result of type S. That is, it has two subtypes:

  • Inc, which wraps Incomplete in case of failure Value, which wraps a task's result in case of success.

Thus, mapR is always invoked whether or not the original task succeeds or fails.

For example:

intTask := error("Failed.")

intTask <<= intTask mapR {
        case Inc(inc: Incomplete) =>
                println("Ignoring failure: " + inc)
                3
        case Value(v) =>
                println("Using successful result: " + v)
                v
}

This overrides the original int-task definition so that if the original task fails, the exception is printed and the constant 3 is returned. If it succeeds, the value is printed and returned.