本文档详细介绍了 sbt 设计和代码风格的总体核心原则。sbt 的核心原则可以简单地概括为
Type
,尽可能地强制执行。牢记这些原则,让我们一起了解 sbt 的核心设计。
这是你在启动 sbt 时遇到的第一个部分。sbt 的命令引擎是它使用构建状态处理用户请求的方式。命令引擎本质上是一种在构建状态上应用 状态转换 的方式,以执行用户请求。
在 sbt 中,命令是接受当前构建状态(sbt.State
)并产生下一个状态的函数。换句话说,它们本质上是 sbt.State => sbt.State
的函数。然而,实际上,命令实际上是字符串处理器,它们接受一些字符串输入并对其进行操作,返回下一个构建状态。
因此,整个 sbt 都是由 sbt.State
类驱动的。由于此类需要在面对自定义代码和插件时保持弹性,因此它需要一种机制来存储任何潜在客户端的状态。在动态语言中,这可以直接在对象上完成。
在 Scala 中,一种幼稚的方法是使用 Map<String,Any>
。但是,这违反了原则 #1:“一切事物都应该有 Type
”。因此,sbt 定义了一种名为 AttributeMap
的新类型的映射。AttributeMap
是一种键值存储机制,其中键既是字符串,也是其值的预期 Type
。
以下是类型安全的 AttributeKey
键的示例
sealed trait AttributeKey[T] {
/** The label is the identifier for the key and is camelCase by convention. */
def label: String
/** The runtime evidence for ``T`` */
def manifest: Manifest[T]
}
这些键存储一个 label
(string
)和一些运行时类型信息(manifest
)。要在 AttributeMap
上放置或获取内容,我们首先需要构造其中一个键。让我们看看 AttributeMap
的基本定义
trait AttributeMap {
/** Gets the value of type ``T`` associated with the key ``k`` or ``None`` if no value is associated.
* If a key with the same label but a different type is defined, this method will return ``None``. */
def get[T](k: AttributeKey[T]): Option[T]
/** Adds the mapping ``k -> value`` to this map, replacing any existing mapping for ``k``.
* Any mappings for keys with the same label but different types are unaffected. */
def put[T](k: AttributeKey[T], value: T): AttributeMap
}
现在已经定义了构建状态是什么,就需要一种动态构建它的方法。在 sbt 中,这是通过 Setting[_]
序列完成的。
Setting 代表在构建状态的 AttributeMap
中构造一个特定 AttributeKey[_]
的值的方式。设置由两部分组成
AttributeKey[T]
,设置的值应该分配到该位置。Initialize[T]
对象,它能够为该设置构造值。sbt 的初始化时间基本上只是获取这些 Setting[_]
对象的序列,运行它们的初始化对象,然后将值存储到 AttributeMap
中。这意味着在键上覆盖现有值就像在序列末尾追加一个 Setting[_]
一样简单。
有趣的地方在于,Initialize[T]
可以依赖于构建状态中的其他 AttributeKey[_]
。每个 Initialize[_]
都可以从构建状态的 AttributeMap
中的任何 AttributeKey[_]
获取值,以计算其值。在处理 Initialize[_]
依赖项时,sbt 会确保一些事项
如果一个 Initialize[_]
依赖于另一个 Initialize[_]
键,那么
所有 与该键相关的 Initialize[_]
块都必须在加载值之前运行。
让我们看看为设置存储的内容
normalizedName := normalize(name.value)
在这里,构造了一个 Setting[_]
,它了解它依赖于 name
AttributeKey
中的值。它的初始化块首先获取 name
键的值,然后在其上运行函数 normalize
来计算其值。
这代表了构建 sbt 构建状态的核心机制。从概念上讲,在某个时刻,我们有一个依赖项和初始化函数的图,我们可以用它来构造第一个构建状态。完成此操作后,我们就可以开始处理用户请求了。
sbt 的下一层是关于这些用户请求或任务。当用户配置构建时,他们正在定义一组可重复的任务,他们可以在项目上运行这些任务。例如,compile
或 test
。这些任务也具有依赖项图,例如,test
任务需要在 compile
运行后才能成功执行。
sbt 定义了一个类 Task[T]
。T
类型参数代表任务返回的数据类型。还记得 sbt 的原则吗?“所有事物都有类型”和“依赖项是明确的”都适用于任务。sbt 推崇一种更接近函数式编程的任务依赖风格:为你的用户返回数据,而不是使用共享的易变状态。
大多数构建工具通过文件系统进行通信,并且 sbt 也确实需要这样做。但是,为了实现稳定的并行化,最好将任务在文件系统上保持隔离,并直接通过类型进行通信。
与 Setting[_]
存储依赖项和初始化函数类似,Task[_]
存储其 Task[_]
依赖项和行为(一个函数)。
待办事项 - 关于 Task[_]
的更多内容
待办事项 - 过渡到 InputTask[_]
,重新讨论命令
待办事项 - 过渡到范围。