1. タスク

タスク 

タスクと設定は入門ガイドで紹介されています。まずそちらを読むことをお勧めします。このページでは、追加の詳細と背景を説明し、よりリファレンスとして使用することを目的としています。

はじめに 

設定とタスクはどちらも値を生成しますが、両者の間には2つの大きな違いがあります。

  1. 設定はプロジェクトのロード時に評価されます。タスクは、ユーザーからのコマンドに応答して、オンデマンドで実行されます。
  2. プロジェクトのロード開始時に、設定とその依存関係は固定されます。ただし、タスクは実行中に新しいタスクを導入できます。

機能 

タスクシステムにはいくつかの機能があります。

  1. 設定システムと統合することにより、タスクは設定と同じように簡単かつ柔軟に追加、削除、および変更できます。
  2. インプットタスクは、パーサーコンビネータを使用して、引数の構文を定義します。これにより、コマンドと同じように、柔軟な構文とタブ補完が可能になります。
  3. タスクは値を生成します。他のタスクは、タスク定義内でvalueを呼び出すことにより、タスクの値にアクセスできます。
  4. タスクグラフの構造を動的に変更できます。タスクは、別のタスクの結果に基づいて実行グラフに注入できます。
  5. try/catch/finallyと同様に、タスクの失敗を処理する方法があります。
  6. 各タスクには、デフォルトで画面に最初に表示されるよりも詳細なレベルでそのタスクのロギングを永続化する独自のロガーへのアクセス権があります。

これらの機能については、次のセクションで詳しく説明します。

タスクの定義 

Hello World の例 (sbt) 

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つあります。

  1. タスクに必要な設定と他のタスクを決定します。それらはタスクの入力です。
  2. これらの入力に基づいてタスクを実装するコードを定義します。
  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/sampleTaskCompile/intTaskの結果を使用します。

Test / sampleTask := (Compile / intTask).value * 3
優先順位について 

念のためですが、インフィックスメソッドの優先順位はメソッドの名前で決まり、ポストフィックスメソッドはインフィックスメソッドよりも優先順位が低くなります。

  1. 代入メソッドは最も低い優先順位を持ちます。これらは!=<=>=、および=で始まる名前を除いて、=で終わる名前のメソッドです。
  2. 文字で始まるメソッドは、次に高い優先順位を持ちます。
  3. 記号で始まり、含まれていない名前を持つメソッド

    1. 最も高い優先順位を持ちます。(このカテゴリは、開始する特定の文字に応じてさらに分割されます。詳細については、Scalaの仕様を参照してください。)

したがって、前の例は次と同等です。

(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

注意!ScopeFiltervalとして代入してください!これは、.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は、ScopeFilter.applyメソッドによって構築されます。このメソッドは、Scopeの各部分(ProjectFilterConfigurationFilterTaskFilter)のフィルターからScopeFilterを作成します。最も簡単なケースは、各部分の値を明示的に指定することです。

val filter: ScopeFilter =
   ScopeFilter(
      inProjects( core, util ),
      inConfigurations( Compile, Test )
   )
未指定のフィルター 

上記の例のようにタスクフィルターが指定されていない場合、デフォルトでは特定のタスク(グローバル)を持たないスコープが選択されます。同様に、構成フィルターが指定されていない場合は、グローバル構成のスコープが選択されます。プロジェクトフィルターは通常は明示的である必要がありますが、指定されていない場合は、現在のプロジェクトコンテキストが使用されます。

フィルターの構築について 

この例では、基本的なメソッドであるinProjectsinConfigurationsを示しました。このセクションでは、ProjectFilterConfigurationFilter、またはTaskFilterを構築するためのすべてのメソッドについて説明します。これらのメソッドは、4つのグループに分類できます。

  • 明示的なメンバーリスト(inProjectsinConfigurationsinTasks
  • グローバル値(inGlobalProjectinGlobalConfigurationinGlobalTask
  • デフォルトフィルター(inAnyProjectinAnyConfigurationinAnyTask
  • プロジェクトの関係(inAggregatesinDependencies

詳細については、APIドキュメントを参照してください。

ScopeFilterの組み合わせ 

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を参照した場合、循環依存が発生します。

Def.sequentialの使用 

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

失敗の処理 

このセクションでは、他のタスクの失敗を処理するために使用されるfailureresult、および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をラップするInc
  • 成功の場合にタスクの結果をラップするValue

したがって、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 

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」が出力されないことは明らかです。