1. 核心原则

核心原则 

本文档详细介绍了 sbt 设计和代码风格的总体核心原则。sbt 的核心原则可以简单地概括为

  1. 一切事物都应该有 Type,尽可能地强制执行。
  2. 依赖项应该 明确
  3. 一旦学习,一个概念应该贯穿 sbt 的 所有 部分。
  4. 并行是默认设置。

牢记这些原则,让我们一起了解 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]
}

这些键存储一个 labelstring)和一些运行时类型信息(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[_] 的值的方式。设置由两部分组成

  1. AttributeKey[T],设置的值应该分配到该位置。
  2. 一个 Initialize[T] 对象,它能够为该设置构造值。

sbt 的初始化时间基本上只是获取这些 Setting[_] 对象的序列,运行它们的初始化对象,然后将值存储到 AttributeMap 中。这意味着在键上覆盖现有值就像在序列末尾追加一个 Setting[_] 一样简单。

有趣的地方在于,Initialize[T] 可以依赖于构建状态中的其他 AttributeKey[_]。每个 Initialize[_] 都可以从构建状态的 AttributeMap 中的任何 AttributeKey[_] 获取值,以计算其值。在处理 Initialize[_] 依赖项时,sbt 会确保一些事项

  1. 不能存在循环依赖项
  2. 如果一个 Initialize[_] 依赖于另一个 Initialize[_] 键,那么

    所有 与该键相关的 Initialize[_] 块都必须在加载值之前运行。

让我们看看为设置存储的内容

normalizedName := normalize(name.value)

image

在这里,构造了一个 Setting[_],它了解它依赖于 name AttributeKey 中的值。它的初始化块首先获取 name 键的值,然后在其上运行函数 normalize 来计算其值。

这代表了构建 sbt 构建状态的核心机制。从概念上讲,在某个时刻,我们有一个依赖项和初始化函数的图,我们可以用它来构造第一个构建状态。完成此操作后,我们就可以开始处理用户请求了。

任务架构 

sbt 的下一层是关于这些用户请求或任务。当用户配置构建时,他们正在定义一组可重复的任务,他们可以在项目上运行这些任务。例如,compiletest。这些任务具有依赖项图,例如,test 任务需要在 compile 运行后才能成功执行。

sbt 定义了一个类 Task[T]T 类型参数代表任务返回的数据类型。还记得 sbt 的原则吗?“所有事物都有类型”和“依赖项是明确的”都适用于任务。sbt 推崇一种更接近函数式编程的任务依赖风格:为你的用户返回数据,而不是使用共享的易变状态。

大多数构建工具通过文件系统进行通信,并且 sbt 也确实需要这样做。但是,为了实现稳定的并行化,最好将任务在文件系统上保持隔离,并直接通过类型进行通信。

Setting[_] 存储依赖项和初始化函数类似,Task[_] 存储其 Task[_] 依赖项和行为(一个函数)。

待办事项 - 关于 Task[_] 的更多内容

待办事项 - 过渡到 InputTask[_],重新讨论命令

待办事项 - 过渡到范围。