任務和設定已在入門指南中介紹,您可能希望先閱讀它。此頁面有其他詳細資訊和背景,更像是作為參考。
設定和任務都會產生值,但它們之間有兩個主要差異
任務系統有幾個功能
value
來存取任務的值。try/catch/finally
。這些功能將在以下章節中詳細討論。
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 建置定義)。
一旦定義其金鑰後,實作任務有三個主要部分
然後,這些部分會像組合設定的部分一樣組合在一起。
任務使用 :=
定義
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
與設定一樣,任務可以在特定作用域中定義。例如,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
的各個部分的篩選器建立 ScopeFilter
:ProjectFilter
、ConfigurationFilter
和 TaskFilter
。最簡單的情況是明確指定各部分的值:
val filter: ScopeFilter =
ScopeFilter(
inProjects( core, util ),
inConfigurations( Compile, Test )
)
如果未指定任務篩選器(如上面的範例所示),則預設會選取沒有特定任務(全域)的 scope。同樣地,未指定的組態篩選器將選取全域組態中的 scope。專案篩選器通常應該是明確的,但如果未指定,則會使用目前的專案內容。
範例顯示了基本方法 inProjects
和 inConfigurations
。本節將描述建構 ProjectFilter
、ConfigurationFilter
或 TaskFilter
的所有方法。這些方法可以組織成四個群組:
inProjects
, inConfigurations
, inTasks
)inGlobalProject
, inGlobalConfiguration
, inGlobalTask
)inAnyProject
, inAnyConfiguration
, inAnyTask
)inAggregates
, inDependencies
)有關詳細資訊,請參閱 API 文件。
可以使用 &&
、||
、--
和 -
方法組合 ScopeFilters
。
a && b
選取同時符合 a 和 b 的 scope。a || b
選取符合 a 或 b 的 scope。a -- b
選取符合 a 但不符合 b 的 scope。-b
選取不符合 b 的 scope。例如,以下選取 core
專案的 Compile
和 Test
組態的 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
}
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!
記錄持續的詳細程度是使用 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)任務組合不同,條件任務會延遲 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,則會產生循環相依性。
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
本節討論 failure
、result
和 andFinally
方法,這些方法用於處理其他任務的失敗。
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
方法定義一個新任務,該任務會執行原始任務,並評估副作用,無論原始任務是否成功。任務的結果是原始任務的結果。例如:
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」。