タスクと設定は入門ガイドで紹介されています。まずそちらを読むことをお勧めします。このページでは、追加の詳細と背景を説明し、よりリファレンスとして使用することを目的としています。
設定とタスクはどちらも値を生成しますが、両者の間には2つの大きな違いがあります。
タスクシステムにはいくつかの機能があります。
valueを呼び出すことにより、タスクの値にアクセスできます。try/catch/finallyと同様に、タスクの失敗を処理する方法があります。これらの機能については、次のセクションで詳しく説明します。
build.sbt:
lazy val hello = taskKey[Unit]("Prints 'Hello World'")
hello := println("hello world!")
タスクを呼び出すには、コマンドラインから「sbt hello」を実行します。このタスクをリストに表示するには、「sbt tasks」を実行します。
新しいタスクを宣言するには、型 TaskKey の遅延 val を定義します。
lazy val sampleTask = taskKey[Int]("A sample task.")
valの名前は、Scalaコードおよびコマンドラインでタスクを参照するときに使用されます。taskKeyメソッドに渡される文字列は、タスクの説明です。taskKeyに渡される型パラメータ(ここではInt)は、タスクによって生成される値の型です。
例として、他のキーをいくつか定義します。
lazy val intTask = taskKey[Int]("An int task")
lazy val stringTask = taskKey[String]("A string task")
例自体は、build.sbtの有効なエントリであるか、Project.settingsへのシーケンスの一部として提供できます(.scala ビルド定義を参照)。
キーが定義されたら、タスクを実装するための主な部分は3つあります。
これらの部分は、設定の部分が結合されるのと同じように結合されます。
タスクは := を使用して定義されます。
intTask := 1 + 2
stringTask := System.getProperty("user.name")
sampleTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
導入で述べたように、タスクはオンデマンドで評価されます。たとえば、sampleTaskが呼び出されるたびに、合計が出力されます。実行間でユーザー名が変更された場合、stringTaskはそれらの個別の実行で異なる値を取ります。(実行内では、各タスクは最大で1回評価されます。)対照的に、設定はプロジェクトのロード時に1回評価され、次のリロードまで固定されます。
他のタスクまたは設定を入力として持つタスクも、:=を使用して定義されます。入力の値は、valueメソッドで参照されます。このメソッドは特別な構文であり、:=への引数など、タスクを定義するときにのみ呼び出すことができます。以下は、intTaskによって生成された値に1を加算し、結果を返すタスクを定義しています。
sampleTask := intTask.value + 1
複数の設定も同様に処理されます。
stringTask := "Sample: " + sampleTask.value + ", int: " + intTask.value
設定と同様に、タスクは特定のスコープで定義できます。たとえば、compileスコープとtestスコープには、個別のcompileタスクがあります。タスクのスコープは、設定の場合と同じように定義されます。次の例では、Test/sampleTaskはCompile/intTaskの結果を使用します。
Test / sampleTask := (Compile / intTask).value * 3
念のためですが、インフィックスメソッドの優先順位はメソッドの名前で決まり、ポストフィックスメソッドはインフィックスメソッドよりも優先順位が低くなります。
!=、<=、>=、および=で始まる名前を除いて、=で終わる名前のメソッドです。記号で始まり、含まれていない名前を持つメソッド
したがって、前の例は次と同等です。
(Test / sampleTask).:=( (Compile / intTask).value * 3 )
さらに、次のカッコは必須です。
helloTask := { "echo Hello" ! }
それらがないと、Scalaは行を、目的のhelloTask.:=( "echo Hello".! )の代わりに( helloTask.:=("echo Hello") ).!と解釈します。
タスクの実装は、バインディングから分離できます。たとえば、基本的な分離された定義は次のようになります。
// Define a new, standalone task implemention
lazy val intTaskImpl: Initialize[Task[Int]] =
Def.task { sampleTask.value - 3 }
// Bind the implementation to a specific key
intTask := intTaskImpl.value
.valueが使用される場合は常に、上記のDef.task内、または:=への引数としてなど、タスク定義内にある必要があります。
一般的なケースでは、以前のタスクを入力として宣言することにより、タスクを変更します。
// initial definition
intTask := 3
// overriding definition that references the previous definition
intTask := intTask.value + 1
以前のタスクを入力として宣言しないことで、タスクを完全にオーバーライドします。次の例の各定義は、前の定義を完全にオーバーライドします。つまり、intTaskを実行すると、#3のみが出力されます。
intTask := {
println("#1")
3
}
intTask := {
println("#2")
5
}
intTask := {
println("#3")
sampleTask.value - 3
}
複数のスコープから値を取得する式の一般的な形式は次のとおりです。
<setting-or-task>.all(<scope-filter>).value
注意!ScopeFilterをvalとして代入してください!これは、.allマクロの実装の詳細要件です。
allメソッドは、タスクと設定に暗黙的に追加されます。Scopesを選択するScopeFilterを受け入れます。結果の型はSeq[T]です。ここで、Tはキーの基になる型です。
一般的なシナリオとして、scaladocに渡すなど、すべてのサブプロジェクトのソースを一度に取得することがあります。値を取得したいタスクはsourcesであり、ルート以外のすべてのプロジェクトとCompile構成の値を取得したいとします。これは次のようになります。
lazy val core = project
lazy val util = project
val filter = ScopeFilter( inProjects(core, util), inConfigurations(Compile) )
lazy val root = project.settings(
sources := {
// each sources definition is of type Seq[File],
// giving us a Seq[Seq[File]] that we then flatten to Seq[File]
val allSources: Seq[Seq[File]] = sources.all(filter).value
allSources.flatten
}
)
次のセクションでは、ScopeFilterを構築するさまざまな方法について説明します。
基本的なScopeFilterは、ScopeFilter.applyメソッドによって構築されます。このメソッドは、Scopeの各部分(ProjectFilter、ConfigurationFilter、TaskFilter)のフィルターからScopeFilterを作成します。最も簡単なケースは、各部分の値を明示的に指定することです。
val filter: ScopeFilter =
ScopeFilter(
inProjects( core, util ),
inConfigurations( Compile, Test )
)
上記の例のようにタスクフィルターが指定されていない場合、デフォルトでは特定のタスク(グローバル)を持たないスコープが選択されます。同様に、構成フィルターが指定されていない場合は、グローバル構成のスコープが選択されます。プロジェクトフィルターは通常は明示的である必要がありますが、指定されていない場合は、現在のプロジェクトコンテキストが使用されます。
この例では、基本的なメソッドであるinProjectsとinConfigurationsを示しました。このセクションでは、ProjectFilter、ConfigurationFilter、またはTaskFilterを構築するためのすべてのメソッドについて説明します。これらのメソッドは、4つのグループに分類できます。
inProjects、inConfigurations、inTasks)inGlobalProject、inGlobalConfiguration、inGlobalTask)inAnyProject、inAnyConfiguration、inAnyTask)inAggregates、inDependencies)詳細については、APIドキュメントを参照してください。
ScopeFilterは、&&、||、--、および-メソッドで組み合わせることができます。
a && b aとbの両方に一致するスコープを選択します。a || b aまたはbのいずれかに一致するスコープを選択します。a -- b aには一致するがbには一致しないスコープを選択します。-b bに一致しないスコープを選択します。たとえば、以下はcoreプロジェクトのCompileおよびTest構成のスコープと、utilプロジェクトのグローバル構成のスコープを選択します。
val filter: ScopeFilter =
ScopeFilter( inProjects(core), inConfigurations(Compile, Test)) ||
ScopeFilter( inProjects(util), inGlobalConfiguration )
allメソッドは、設定(型Initialize[T]の値)とタスク(型Initialize[Task[T]]の値)の両方に適用されます。次の表に示すように、Seq[T]を提供する設定またはタスクを返します。
| ターゲット | 結果 |
|---|---|
| Initialize[T] | Initialize[Seq[T]] |
| Initialize[Task[T]] | Initialize[Task[Seq[T]]] |
これは、allメソッドをタスクと設定を構築するメソッドと組み合わせることができることを意味します。
一部のスコープでは、設定またはタスクが定義されていない場合があります。この場合、?と??メソッドが役立ちます。これらはどちらも設定とタスクで定義されており、キーが未定義の場合に何をすべきかを示します。
| ? | 基になる型Tの設定またはタスクでは、引数を受け取らず、型Option[T]の設定またはタスク(それぞれ)を返します。設定/タスクが未定義の場合はNoneになり、定義されている場合はSome[T]になります。 |
| ?? | 基になる型Tの設定またはタスクでは、型Tの引数を受け取り、設定/タスクが未定義の場合はこの引数を使用します。 |
次の作為的な例では、最大エラーを現在のプロジェクトのすべてのアグリゲートの最大値に設定します。
// select the transitive aggregates for this project, but not the project itself
val filter: ScopeFilter =
ScopeFilter( inAggregates(ThisProject, includeRoot=false) )
maxErrors := {
// get the configured maximum errors in each selected scope,
// using 0 if not defined in a scope
val allVersions: Seq[Int] =
(maxErrors ?? 0).all(filter).value
allVersions.max
}
allのターゲットは、匿名のものを含め、任意のタスクまたは設定です。これは、各スコープで新しいタスクまたは設定を定義せずに、一度に複数の値を取得できることを意味します。一般的なユースケースは、取得した各値を、それが由来するプロジェクト、構成、または完全なスコープとペアにすることです。
resolvedScoped:完全な外側のScopedKey(Scope + AttributeKey[_])を提供します。thisProject:このスコープに関連付けられたProjectを提供します(グローバルおよびビルドレベルでは未定義)。thisProjectRef:コンテキストのProjectRefを提供します(グローバルおよびビルドレベルでは未定義)。configuration:コンテキストのConfigurationを提供します(グローバル構成では未定義)。たとえば、以下は、sbtプラグインを定義するCompile以外の構成を出力するタスクを定義します。これは、誤って構成されたビルドを識別するために使用される場合があります(または、これはかなり作為的な例であるため、そうでない場合もあります)。
// Select all configurations in the current project except for Compile
lazy val filter: ScopeFilter = ScopeFilter(
inProjects(ThisProject),
inAnyConfiguration -- inConfigurations(Compile)
)
// Define a task that provides the name of the current configuration
// and the set of sbt plugins defined in the configuration
lazy val pluginsWithConfig: Initialize[Task[ (String, Set[String]) ]] =
Def.task {
( configuration.value.name, definedSbtPlugins.value )
}
checkPluginsTask := {
val oddPlugins: Seq[(String, Set[String])] =
pluginsWithConfig.all(filter).value
// Print each configuration that defines sbt plugins
for( (config, plugins) <- oddPlugins if plugins.nonEmpty )
println(s"$config defines sbt plugins: ${plugins.mkString(", ")}")
}
このセクションの例では、前のセクションで定義したタスクキーを使用します。
タスクごとのロガーは、ストリームと呼ばれるタスク固有のデータのためのより一般的なシステムの一部です。これにより、タスクのスタックトレースとロギングの冗長さを個別に制御したり、タスクの最後のロギングを呼び出したりできます。タスクは、独自の永続化されたバイナリまたはテキストデータにもアクセスできます。
ストリームを使用するには、streamsタスクの値を取得します。これは、定義タスクのTaskStreamsのインスタンスを提供する特別なタスクです。この型は、名前付きバイナリおよびテキストストリーム、名前付きロガー、およびデフォルトのロガーへのアクセスを提供します。最も一般的に使用される側面であるデフォルトのLoggerは、logメソッドで取得します。
myTask := {
val s: TaskStreams = streams.value
s.log.debug("Saying hi...")
s.log.info("Hello!")
}
ロギング設定を特定のタスクのスコープでスコープ指定できます。
myTask / logLevel := Level.Debug
myTask / traceLevel := 5
タスクからの最後のロギング出力を取得するには、lastコマンドを使用します。
$ last myTask
[debug] Saying hi...
[info] Hello!
ロギングが永続化される冗長性は、persistLogLevelおよびpersistTraceLevel設定を使用して制御されます。lastコマンドは、これらのレベルに従ってログに記録されたものを表示します。これらのレベルは、既にログに記録された情報には影響しません。
(sbt 1.4.0+が必要です)
Def.task { ... }がトップレベルのif式で構成されている場合、条件付きタスク(または選択タスク)が自動的に作成されます。
bar := {
if (number.value < 0) negAction.value
else if (number.value == 0) zeroAction.value
else posAction.value
}
通常の(Applicative)タスク構成とは異なり、条件付きタスクは、if式で自然に予期されるように、then-clauseとelse-clauseの評価を遅延させます。これは既にDef.taskDyn { ... }で可能ですが、動的タスクとは異なり、条件付きタスクはinspectコマンドで動作します。
Def.taskDynを使用した動的計算 タスクの結果を使用して、次に評価するタスクを決定すると便利な場合があります。これはDef.taskDynを使用して行われます。taskDynの結果は、実行時に依存関係を導入するため、動的タスクと呼ばれます。taskDynメソッドは、プレーンな値ではなくタスクを返す点を除いて、Def.taskおよび:=と同じ構文をサポートします。
例:
val dynamic = Def.taskDyn {
// decide what to evaluate based on the value of `stringTask`
if(stringTask.value == "dev")
// create the dev-mode task: this is only evaluated if the
// value of stringTask is "dev"
Def.task {
3
}
else
// create the production task: only evaluated if the value
// of the stringTask is not "dev"
Def.task {
intTask.value + 5
}
}
myTask := {
val num = dynamic.value
println(s"Number selected was $num")
}
myTaskの唯一の静的な依存関係はstringTaskです。intTaskへの依存関係は、非開発モードでのみ導入されます。
注意:動的タスクはそれ自体を参照することはできません。そうしないと、循環依存が発生します。上記の例では、taskDynに渡されたコードがmyTaskを参照した場合、循環依存が発生します。
sbt 0.13.8では、準シーケンシャルセマンティクスでタスクを実行するためのDef.sequential関数が追加されました。これは動的タスクと似ていますが、定義が簡単です。シーケンシャルタスクを示すために、scalastyle-sbt-pluginによって追加されたCompile / compileタスクとCompile / scalastyleタスクを実行するcompilecheckというカスタムタスクを作成しましょう。
lazy val compilecheck = taskKey[Unit]("compile and then scalastyle")
lazy val root = (project in file("."))
.settings(
Compile / compilecheck := Def.sequential(
Compile / compile,
(Compile / scalastyle).toTask("")
).value
)
シェルからcompilecheckでこのタスクタイプを呼び出します。コンパイルが失敗した場合、compilecheckは実行を停止します。
root> compilecheck
[info] Compiling 1 Scala source to /Users/x/proj/target/scala-2.10/classes...
[error] /Users/x/proj/src/main/scala/Foo.scala:3: Unmatched closing brace '}' ignored here
[error] }
[error] ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
このセクションでは、他のタスクの失敗を処理するために使用されるfailure、result、およびandFinallyメソッドについて説明します。
failure failureメソッドは、元のタスクが正常に完了しなかった場合にIncomplete値を返す新しいタスクを作成します。元のタスクが成功した場合、新しいタスクは失敗します。Incompleteは、失敗を引き起こしたタスクとタスク実行中にスローされた基になる例外に関する情報を含む例外です。
例:
intTask := sys.error("Failed.")
intTask := {
println("Ignoring failure: " + intTask.failure.value)
3
}
これにより、元の例外が出力され、定数3が返されるようにintTaskがオーバーライドされます。
failureは、ターゲットに依存する他のタスクが失敗するのを防ぎません。次の例を考えてみましょう。
intTask := if(shouldSucceed) 5 else sys.error("Failed.")
// Return 3 if intTask fails. If intTask succeeds, this task will fail.
aTask := intTask.failure.value - 2
// A new task that increments the result of intTask.
bTask := intTask.value + 1
cTask := aTask.value + bTask.value
次の表に、最初に呼び出されたタスクに応じた各タスクの結果を示します。
| 呼び出されたタスク | intTaskの結果 | aTaskの結果 | bTaskの結果 | cTaskの結果 | 全体の結果 |
|---|---|---|---|---|---|
| intTask | 失敗 | 実行されません | 実行されません | 実行されません | 失敗 |
| aTask | 失敗 | 成功 | 実行されません | 実行されません | 成功 |
| bTask | 失敗 | 実行されません | 失敗 | 実行されません | 失敗 |
| cTask | 失敗 | 成功 | 失敗 | 失敗 | 失敗 |
| intTask | 成功 | 実行されません | 実行されません | 実行されません | 成功 |
| aTask | 成功 | 失敗 | 実行されません | 実行されません | 失敗 |
| bTask | 成功 | 実行されません | 成功 | 実行されません | 成功 |
| cTask | 成功 | 失敗 | 成功 | 失敗 | 失敗 |
全体の結果は、常にルートタスク(直接呼び出されたタスク)と同じです。failureは成功を失敗に、失敗をIncompleteに変えます。通常のタスク定義は、入力のいずれかが失敗した場合に失敗し、それ以外の場合は値を計算します。
result resultメソッドは、元のタスクの完全なResult[T]値を返す新しいタスクを作成します。Resultは、タスク結果の型がTの場合、Either[Incomplete, T]と同じ構造を持っています。つまり、次の2つのサブタイプがあります。
IncompleteをラップするIncValue。したがって、resultによって作成されたタスクは、元のタスクが成功したか失敗したかに関係なく実行されます。
例:
intTask := sys.error("Failed.")
intTask := {
intTask.result.value match {
case Inc(inc: Incomplete) =>
println("Ignoring failure: " + inc)
3
case Value(v) =>
println("Using successful result: " + v)
v
}
}
これにより、元のintTaskの定義がオーバーライドされ、元のタスクが失敗した場合は例外が出力されて定数3が返されます。成功した場合は、値が出力されて返されます。
andFinallyメソッドは、元のタスクを実行し、元のタスクが成功したかどうかに関係なく副作用を評価する新しいタスクを定義します。タスクの結果は、元のタスクの結果です。例:
intTask := sys.error("I didn't succeed.")
lazy val intTaskImpl = intTask andFinally { println("andFinally") }
intTask := intTaskImpl.value
これにより、タスクが失敗した場合でも常に「andFinally」を出力するように元のintTaskが変更されます。
andFinallyは新しいタスクを構築することに注意してください。つまり、追加のブロックを実行するには、新しいタスクを呼び出す必要があります。これは、前の例のようにタスクをオーバーライドするのではなく、別のタスクでandFinallyを呼び出す場合に重要です。たとえば、次のコードを考えてみましょう。
intTask := sys.error("I didn't succeed.")
lazy val intTaskImpl = intTask andFinally { println("andFinally") }
otherIntTask := intTaskImpl.value
intTaskが直接実行された場合、otherIntTaskは実行に関与しません。このケースは、次のプレーンなScalaコードに似ています。
def intTask(): Int =
sys.error("I didn't succeed.")
def otherIntTask(): Int =
try { intTask() }
finally { println("finally") }
intTask()
ここでは、intTask()を呼び出しても「finally」が出力されないことは明らかです。