1. 任務

任務 

任務和設定已在入門指南中介紹,您可能希望先閱讀它。此頁面有其他詳細資訊和背景,更像是作為參考。

簡介 

設定和任務都會產生值,但它們之間有兩個主要差異

  1. 設定會在專案載入時評估。任務會按需執行,通常是回應使用者的命令。
  2. 在專案載入開始時,設定及其相依性是固定的。但是,任務可以在執行期間引入新任務。

功能 

任務系統有幾個功能

  1. 透過與設定系統整合,可以像設定一樣輕鬆且彈性地新增、移除和修改任務。
  2. 輸入任務使用剖析器組合器來定義其引數的語法。這允許靈活的語法和 Tab 鍵自動完成,與命令的方式相同。
  3. 任務會產生值。其他任務可以透過在任務定義中對其呼叫 value 來存取任務的值。
  4. 可以動態變更任務圖的結構。可以根據另一個任務的結果將任務注入執行圖中。
  5. 有方法可以處理任務失敗,類似於 try/catch/finally
  6. 每個任務都可以存取自己的 Logger,預設會以比最初列印到螢幕上更詳細的層級保留該任務的記錄。

這些功能將在以下章節中詳細討論。

定義任務 

Hello World 範例 (sbt) 

build.sbt:

lazy val hello = taskKey[Unit]("Prints 'Hello World'")

hello := println("hello world!")

從命令列執行 "sbt hello" 以叫用任務。執行 "sbt tasks" 以查看列出的此任務。

定義金鑰 

若要宣告新任務,請定義 TaskKey 類型的 lazy val

lazy val sampleTask = taskKey[Int]("A sample task.")

當在 Scala 程式碼和命令列中參照任務時,會使用 val 的名稱。傳遞給 taskKey 方法的字串是任務的描述。傳遞給 taskKey 的類型參數(此處為 Int)是任務產生的值類型。

我們將為範例定義其他幾個金鑰

lazy val intTask = taskKey[Int]("An int task")
lazy val stringTask = taskKey[String]("A string task")

這些範例本身是 build.sbt 中的有效項目,或者可以作為序列的一部分提供給 Project.settings(請參閱.scala 建置定義)。

實作任務 

一旦定義其金鑰後,實作任務有三個主要部分

  1. 判斷任務所需的設定和其他任務。它們是任務的輸入。
  2. 根據這些輸入定義實作任務的程式碼。
  3. 判斷任務將在哪個作用域中。

然後,這些部分會像組合設定的部分一樣組合在一起。

定義基本任務 

任務使用 := 定義

intTask := 1 + 2

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

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

如簡介中所述,任務會按需評估。例如,每次叫用 sampleTask 時,它都會列印總和。如果使用者名稱在執行之間變更,stringTask 在這些個別執行中會採用不同的值。(在執行中,每個任務最多評估一次。)相反地,設定會在專案載入時評估一次,並且在下次重新載入之前是固定的。

具有輸入的任務 

具有其他任務或設定作為輸入的任務也使用 := 定義。輸入的值會由 value 方法參照。此方法是特殊語法,只能在定義任務時呼叫,例如在 := 的引數中。以下定義一個任務,將 intTask 產生的值加 1 並傳回結果。

sampleTask := intTask.value + 1

多個設定的處理方式類似

stringTask := "Sample: " + sampleTask.value + ", int: " + intTask.value
任務作用域 

與設定一樣,任務可以在特定作用域中定義。例如,compiletest 作用域有不同的 compile 任務。任務的作用域定義與設定相同。在以下範例中,Test/sampleTask 使用 Compile/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

注意! 請務必將 ScopeFilter 指派為 val!這是 .all 巨集的實作細節要求。

all 方法會隱式新增至任務和設定。它會接受一個將選取 ScopesScopeFilter。結果具有類型 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 的各個部分的篩選器建立 ScopeFilterProjectFilterConfigurationFilterTaskFilter。最簡單的情況是明確指定各部分的值:

val filter: ScopeFilter =
   ScopeFilter(
      inProjects( core, util ),
      inConfigurations( Compile, Test )
   )
未指定的篩選器 

如果未指定任務篩選器(如上面的範例所示),則預設會選取沒有特定任務(全域)的 scope。同樣地,未指定的組態篩選器將選取全域組態中的 scope。專案篩選器通常應該是明確的,但如果未指定,則會使用目前的專案內容。

更多關於篩選器建構的資訊 

範例顯示了基本方法 inProjectsinConfigurations。本節將描述建構 ProjectFilterConfigurationFilterTaskFilter 的所有方法。這些方法可以組織成四個群組:

  • 明確的成員列表 (inProjects, inConfigurations, inTasks)
  • 全域值 (inGlobalProject, inGlobalConfiguration, inGlobalTask)
  • 預設篩選器 (inAnyProject, inAnyConfiguration, inAnyTask)
  • 專案關係 (inAggregates, inDependencies)

有關詳細資訊,請參閱 API 文件

組合 ScopeFilter 

可以使用 &&||--- 方法組合 ScopeFilters

  • a && b 選取同時符合 a 和 b 的 scope。
  • a || b 選取符合 a 或 b 的 scope。
  • a -- b 選取符合 a 但不符合 b 的 scope。
  • -b 選取不符合 b 的 scope。

例如,以下選取 core 專案的 CompileTest 組態的 scope,以及 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 方法可以與建構任務和設定的方法結合使用。

遺失的值 

某些 scope 可能未定義設定或任務。在這種情況下,??? 方法可以提供協助。它們都在設定和任務上定義,並指出當 key 未定義時該如何處理。

? 在基礎類型為 T 的設定或任務上,此方法不接受任何引數,並傳回類型為 Option[T] 的設定或任務(分別)。如果設定/任務未定義,則結果為 None;如果已定義,則結果為 Some[T],其中包含該值。
?? 在基礎類型為 T 的設定或任務上,此方法接受類型為 T 的引數,如果設定/任務未定義,則使用此引數。

以下虛構範例會將最大錯誤數設定為目前專案所有 aggregate 的最大值。

// 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
}
來自多個 scope 的多個值 

all 的目標可以是任何任務或設定,包括匿名的。這表示可以在每個 scope 中取得多個值,而無需定義新的任務或設定。一個常見的使用案例是將取得的每個值與其來源的專案、組態或完整 scope 配對。

  • resolvedScoped:提供完整的封閉 ScopedKey(它是 Scope + AttributeKey[_])。
  • thisProject:提供與此 scope 關聯的專案(在全域和建置層級未定義)。
  • thisProjectRef:提供內容的 ProjectRef(在全域和建置層級未定義)。
  • 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!")
}

您可以使用特定任務的 scope 來範圍化記錄設定。

myTask / logLevel := Level.Debug

myTask / traceLevel := 5

若要取得任務的最後記錄輸出,請使用 last 命令。

$ last myTask
[debug] Saying hi...
[info] Hello!

記錄持續的詳細程度是使用 persistLogLevelpersistTraceLevel 設定來控制的。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)任務組合不同,條件任務會延遲 then 子句和 else 子句的評估,正如 if 運算式自然預期的那樣。這已經可以使用 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 函數,以在半循序語意下執行任務。這類似於動態任務,但更容易定義。為了示範循序任務,讓我們建立一個名為 compilecheck 的自訂任務,該任務會執行 Compile / compile,然後執行 scalastyle-sbt-plugin 新增的 Compile / scalastyle 任務。

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
  )

若要從 shell 中呼叫 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

處理失敗 

本節討論 failureresultandFinally 方法,這些方法用於處理其他任務的失敗。

failure 

當原始任務無法正常完成時,failure 方法會建立一個新任務,該任務會傳回 Incomplete 值。如果原始任務成功,則新任務會失敗。Incomplete 是一個例外,其中包含有關導致失敗的任何任務以及任務執行期間擲回的任何基礎例外的資訊。

例如,

intTask := sys.error("Failed.")

intTask := {
   println("Ignoring failure: " + intTask.failure.value)
   3
}

這會覆寫 intTask,以便列印原始例外,並傳回常數 3

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 failure 未執行 未執行 未執行 failure
aTask failure 成功 未執行 未執行 成功
bTask failure 未執行 failure 未執行 failure
cTask failure 成功 failure failure failure
intTask 成功 未執行 未執行 未執行 成功
aTask 成功 failure 未執行 未執行 failure
bTask 成功 未執行 成功 未執行 成功
cTask 成功 failure 成功 failure failure

整體結果始終與根任務(直接叫用的任務)相同。failure 會將成功變成失敗,並將失敗變成 Incomplete。當任何輸入失敗時,正常的任務定義會失敗,否則會計算其值。

result 

result 方法會建立一個新任務,該任務會傳回原始任務的完整 Result[T] 值。Result 的結構與類型為 T 的任務結果的 Either[Incomplete, T] 相同。也就是說,它有兩個子類型:

  • Inc,在失敗時會包裝 Incomplete
  • 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

這會修改原始的 intTask,即使任務失敗,也始終列印「andFinally」。

請注意,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」。