接續建置定義,此頁面將更詳細地說明 build.sbt
定義。
與其將 settings
視為鍵值對,不如將其視為任務的有向無環圖 (DAG),其中邊緣表示先發生。我們稱此為任務圖。
在我們深入探討之前,我們先複習一下關鍵術語。
.settings(...)
內的條目。SettingKey[A]
、TaskKey[A]
或 InputKey[A]
。SettingKey[A]
的設定運算式定義。該值在載入期間計算一次。TaskKey[A]
的任務運算式定義。該值在每次調用時計算。在 build.sbt
DSL 中,我們使用 .value
方法來表達對另一個任務或設定的相依性。value 方法很特別,只能在 :=
(或 +=
或 ++=
,我們稍後會看到)的參數中呼叫。
作為第一個範例,請考慮定義取決於 update
和 clean
任務的 scalacOptions
。以下是這些鍵的定義(來自 Keys)。
注意:以下計算的值對於 scalaOptions
來說沒有意義,它只是為了示範目的
val scalacOptions = taskKey[Seq[String]]("Options for the Scala compiler.")
val update = taskKey[UpdateReport]("Resolves and optionally retrieves dependencies, producing a report.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")
以下是我們如何重新佈線 scalacOptions
scalacOptions := {
val ur = update.value // update task happens-before scalacOptions
val x = clean.value // clean task happens-before scalacOptions
// ---- scalacOptions begins here ----
ur.allConfigurations.take(3)
}
update.value
和 clean.value
宣告任務相依性,而 ur.allConfigurations.take(3)
是任務的主體。
.value
不是一般的 Scala 方法呼叫。build.sbt
DSL 使用巨集將它們提升到任務主體之外。無論 update
和 clean
任務出現在主體中的哪一行,任務引擎都會在評估 scalacOptions
的開頭 {
時完成它們。
請參閱以下範例
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "Hello",
scalacOptions := {
val out = streams.value // streams task happens-before scalacOptions
val log = out.log
log.info("123")
val ur = update.value // update task happens-before scalacOptions
log.info("456")
ur.allConfigurations.take(3)
}
)
接下來,從 sbt shell 輸入 scalacOptions
> scalacOptions
[info] Updating {file:/xxx/}root...
[info] Resolving jline#jline;2.14.1 ...
[info] Done updating.
[info] 123
[info] 456
[success] Total time: 0 s, completed Jan 2, 2017 10:38:24 PM
即使 val ur = ...
出現在 log.info("123")
和 log.info("456")
之間,update
任務的評估也會在它們的任一個之前發生。
以下是另一個範例
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "Hello",
scalacOptions := {
val ur = update.value // update task happens-before scalacOptions
if (false) {
val x = clean.value // clean task happens-before scalacOptions
}
ur.allConfigurations.take(3)
}
)
接下來,從 sbt shell 輸入 run
,然後輸入 scalacOptions
> run
[info] Updating {file:/xxx/}root...
[info] Resolving jline#jline;2.14.1 ...
[info] Done updating.
[info] Compiling 1 Scala source to /Users/eugene/work/quick-test/task-graph/target/scala-2.12/classes...
[info] Running example.Hello
hello
[success] Total time: 0 s, completed Jan 2, 2017 10:45:19 PM
> scalacOptions
[info] Updating {file:/xxx/}root...
[info] Resolving jline#jline;2.14.1 ...
[info] Done updating.
[success] Total time: 0 s, completed Jan 2, 2017 10:45:23 PM
現在,如果您檢查 target/scala-2.12/classes/
,它將不存在,因為 clean
任務即使在 if (false)
內也已執行。
另一個需要注意的重要事項是,不保證 update
和 clean
任務的順序。它們可能會先執行 update
再執行 clean
、先執行 clean
再執行 update
,或兩者同時平行執行。
如上所述,.value
是一種特殊的方法,用於表達對其他任務和設定的相依性。在您熟悉 build.sbt 之前,我們建議您將所有 .value
呼叫放在任務主體的頂部。
但是,當您越來越熟悉時,您可能希望內嵌 .value
呼叫,因為它可以使任務/設定更簡潔,並且您不必想出變數名稱。
我們已經內嵌了一些範例
scalacOptions := {
val x = clean.value
update.value.allConfigurations.take(3)
}
請注意,無論 .value
呼叫是內嵌還是在任務主體中的任何位置,它們仍然會在進入任務主體之前進行評估。
在上面的範例中,scalacOptions
對 update
和 clean
任務有相依性。如果您將上述內容放在 build.sbt
中並執行 sbt 互動式主控台,然後輸入 inspect scalacOptions
,您應該會看到(一部分)
> inspect scalacOptions
[info] Task: scala.collection.Seq[java.lang.String]
[info] Description:
[info] Options for the Scala compiler.
....
[info] Dependencies:
[info] *:clean
[info] *:update
....
這就是 sbt 如何知道哪些任務相依於其他任務。
例如,如果您 inspect tree compile
,您會看到它相依於另一個鍵 incCompileSetup
,而該鍵又相依於其他鍵,例如 dependencyClasspath
。繼續追蹤相依性鏈,就會發生神奇的事。
> inspect tree compile
[info] compile:compile = Task[sbt.inc.Analysis]
[info] +-compile:incCompileSetup = Task[sbt.Compiler$IncSetup]
[info] | +-*/*:skip = Task[Boolean]
[info] | +-compile:compileAnalysisFilename = Task[java.lang.String]
[info] | | +-*/*:crossPaths = true
[info] | | +-{.}/*:scalaBinaryVersion = 2.12
[info] | |
[info] | +-*/*:compilerCache = Task[xsbti.compile.GlobalsCache]
[info] | +-*/*:definesClass = Task[scala.Function1[java.io.File, scala.Function1[java.lang.String, Boolean]]]
[info] | +-compile:dependencyClasspath = Task[scala.collection.Seq[sbt.Attributed[java.io.File]]]
[info] | | +-compile:dependencyClasspath::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | | +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | |
[info] | | +-compile:externalDependencyClasspath = Task[scala.collection.Seq[sbt.Attributed[java.io.File]]]
[info] | | | +-compile:externalDependencyClasspath::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | | | +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | | |
[info] | | | +-compile:managedClasspath = Task[scala.collection.Seq[sbt.Attributed[java.io.File]]]
[info] | | | | +-compile:classpathConfiguration = Task[sbt.Configuration]
[info] | | | | | +-compile:configuration = compile
[info] | | | | | +-*/*:internalConfigurationMap = <function1>
[info] | | | | | +-*:update = Task[sbt.UpdateReport]
[info] | | | | |
....
當您輸入 compile
時,例如,sbt 會自動執行 update
。它之所以能正常運作,是因為作為 compile
計算的輸入所需要的值需要 sbt 先執行 update
計算。
透過這種方式,sbt 中的所有建置相依性都是自動的,而不是明確宣告的。如果您在另一個計算中使用某個鍵的值,則該計算會相依於該鍵。
scalacOptions
是一個任務鍵。假設它已經設定為某些值,但您想為非 2.12 過濾掉 "-Xfatal-warnings"
和 "-deprecation"
。
lazy val root = (project in file("."))
.settings(
name := "Hello",
organization := "com.example",
scalaVersion := "2.12.18",
version := "0.1.0-SNAPSHOT",
scalacOptions := List("-encoding", "utf8", "-Xfatal-warnings", "-deprecation", "-unchecked"),
scalacOptions := {
val old = scalacOptions.value
scalaBinaryVersion.value match {
case "2.12" => old
case _ => old filterNot (Set("-Xfatal-warnings", "-deprecation").apply)
}
}
)
以下是它在 sbt shell 上的樣子
> show scalacOptions
[info] * -encoding
[info] * utf8
[info] * -Xfatal-warnings
[info] * -deprecation
[info] * -unchecked
[success] Total time: 0 s, completed Jan 2, 2017 11:44:44 PM
> ++2.11.8!
[info] Forcing Scala version to 2.11.8 on all projects.
[info] Reapplying settings...
[info] Set current project to Hello (in build file:/xxx/)
> show scalacOptions
[info] * -encoding
[info] * utf8
[info] * -unchecked
[success] Total time: 0 s, completed Jan 2, 2017 11:44:51 PM
接下來,採用這兩個鍵(來自 Keys)
val scalacOptions = taskKey[Seq[String]]("Options for the Scala compiler.")
val checksums = settingKey[Seq[String]]("The list of checksums to generate and to verify for dependencies.")
注意:scalacOptions
和 checksums
彼此無關。它們只是兩個具有相同值類型的鍵,其中一個是任務。
可以編譯一個將 scalacOptions
別名為 checksums
的 build.sbt
,但不能反之。例如,這是允許的
// The scalacOptions task may be defined in terms of the checksums setting
scalacOptions := checksums.value
沒有辦法往另一個方向走。也就是說,設定鍵不能相依於任務鍵。這是因為設定鍵只在專案載入時計算一次,因此任務不會每次都重新執行,而任務期望每次都重新執行。
// Bad example: The checksums setting cannot be defined in terms of the scalacOptions task!
checksums := scalacOptions.value
就執行時間而言,我們可以將設定視為在載入期間評估的特殊任務。
考慮將專案組織定義為與專案名稱相同。
// name our organization after our project (both are SettingKey[String])
organization := name.value
以下是一個實際的範例。只有當 scalaBinaryVersion
為 "2.11"
時,才會將 Compile / scalaSource
鍵重新佈線到不同的目錄。
Compile / scalaSource := {
val old = (Compile / scalaSource).value
scalaBinaryVersion.value match {
case "2.11" => baseDirectory.value / "src-2.11" / "main" / "scala"
case _ => old
}
}
我們使用 build.sbt
特定領域語言 (DSL) 來建構設定和任務的 DAG。設定運算式會編碼設定、任務及其之間的相依性。
此結構在 Make (1976)、Ant (2000) 和 Rake (2003) 中很常見。
基本的 Makefile 語法如下所示
target: dependencies
[tab] system command1
[tab] system command2
給定一個目標(預設目標名稱為 all
),
讓我們來看一個 Makefile
的例子
CC=g++
CFLAGS=-Wall
all: hello
hello: main.o hello.o
$(CC) main.o hello.o -o hello
%.o: %.cpp
$(CC) $(CFLAGS) -c $< -o $@
執行 make
時,預設會選擇名為 all
的目標。該目標將 hello
列為其相依性,而 hello
尚未建置,因此 Make 會建置 hello
。
接著,Make 會檢查 hello
目標的相依性是否已建置。hello
列出了兩個目標:main.o
和 hello.o
。一旦使用最後一個模式匹配規則建立這些目標後,才會執行系統命令將 main.o
和 hello.o
連結到 hello
。
如果您只是執行 make
,您可以專注於您想要的目標,而建置中間產品所需的確切時機和命令則由 Make 決定。我們可以將其視為以相依性為導向的程式設計或以流程為基礎的程式設計。Make 實際上被認為是一個混合系統,因為雖然 DSL 描述了任務相依性,但動作會委派給系統命令。
這種混合性延續到 Make 的後繼者,例如 Ant、Rake 和 sbt。看看 Rakefile 的基本語法
task name: [:prereq1, :prereq2] do |t|
# actions (may reference prereq as t.name etc)
end
Rake 的突破在於它使用程式語言來描述動作,而不是系統命令。
以這種方式組織建置有幾個動機。
首先是去重複化。使用流程基礎程式設計,即使一個任務被多個任務依賴,也只會執行一次。例如,即使任務圖中的多個任務都依賴 Compile / compile
,編譯也只會執行一次。
其次是平行處理。透過使用任務圖,任務引擎可以平行排程彼此不相依的任務。
第三是關注點分離和靈活性。任務圖讓建置使用者可以以不同的方式將任務連接在一起,而 sbt 和外掛程式可以提供各種功能,例如編譯和函式庫相依性管理,這些功能可以重複使用。
建置定義的核心資料結構是任務的 DAG (有向無環圖),其中邊表示先後關係。build.sbt
是一個 DSL,旨在表達以相依性為導向的程式設計或以流程為基礎的程式設計,類似於 Makefile
和 Rakefile
。
流程基礎程式設計的主要動機是去重複化、平行處理和可自訂性。