1. 构建定义

构建定义 

本页描述 sbt 构建定义,包括一些“理论”和 build.sbt 的语法。假设您已经安装了最新版本的 sbt,例如 sbt 1.9.8,知道如何 使用 sbt,并且已经阅读了入门指南中的前几页。

本页讨论 build.sbt 构建定义。

指定 sbt 版本 

作为构建定义的一部分,您将指定构建使用的 sbt 版本。这使具有不同 sbt 启动器版本的人能够使用一致的结果构建相同的项目。为此,创建一个名为 project/build.properties 的文件,其中指定 sbt 版本如下

sbt.version=1.9.8

如果本地没有所需的版本,sbt 启动器将为您下载。如果此文件不存在,sbt 启动器将选择任意版本,这并不鼓励,因为它会使您的构建不可移植。

什么是构建定义? 

构建定义build.sbt 中定义,它包含一组项目(类型为 Project)。由于术语项目可能含糊不清,因此在本指南中我们通常称其为子项目

例如,在 build.sbt 中,您可以像这样定义位于当前目录中的子项目

lazy val root = (project in file("."))
  .settings(
    name := "Hello",
    scalaVersion := "2.12.7"
  )

每个子项目都由键值对配置。

例如,一个键是 name,它映射到一个字符串值,即子项目的名称。键值对列在 .settings(...) 方法下,如下所示

lazy val root = (project in file("."))
  .settings(
    name := "Hello",
    scalaVersion := "2.12.7"
  )

如何使用 build.sbt 定义设置 

build.sbt 定义子项目,它使用build.sbt 领域特定语言 (DSL) 持有一系列称为设置表达式的键值对。

ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version      := "0.1.0-SNAPSHOT"

lazy val root = (project in file("."))
  .settings(
    name := "hello"
  )

让我们仔细看看 build.sbt DSL:setting expression

每个条目都称为设置表达式。其中一些也被称为任务表达式。我们将在本页的后面看到更多关于它们的区别。

设置表达式包含三个部分

  1. 左侧是
  2. 运算符,在本例中为 :=
  3. 右侧称为主体设置主体

在左侧,nameversionscalaVersion。键是 SettingKey[T]TaskKey[T]InputKey[T] 的实例,其中 T 是预期的值类型。键的种类将在下面解释。

因为键 name 的类型为 SettingKey[String],所以 name 上的 := 运算符也专门类型化为 String。如果您使用错误的值类型,构建定义将无法编译

lazy val root = (project in file("."))
  .settings(
    name := 42  // will not compile
  )

build.sbt 也可能与 vallazy valdef 交织在一起。不允许在 build.sbt 中使用顶层 objectclass。这些应该作为 Scala 源文件放在 project/ 目录中。

 

类型 

键有三种类型

  • SettingKey[T]:仅评估一次的值的键(该值在加载子项目时计算,并保留)。
  • TaskKey[T]:值的键,称为任务,每次引用它时都会评估(类似于 Scala 函数),可能会产生副作用。
  • InputKey[T]:具有命令行参数作为输入的任务的键。有关更多详细信息,请查看 输入任务

内置键 

内置键只是名为 Keys 的对象的字段。build.sbt 隐式包含 import sbt.Keys._,因此 sbt.Keys.name 可以称为 name

自定义键 

自定义键可以使用它们各自的创建方法定义:settingKeytaskKeyinputKey。每个方法都期望与键关联的值的类型以及描述。键的名称取自分配给键的 val。例如,要定义一个名为 hello 的新任务的键,

lazy val hello = taskKey[Unit]("An example task")

这里我们使用了这样一个事实,即 .sbt 文件除了设置之外还可以包含 valdef。所有此类定义都在设置之前评估,无论它们在文件中定义的位置如何。

注意: 通常,使用 lazy val 而不是 val 来避免初始化顺序问题。

任务键与设置键 

TaskKey[T] 被称为定义任务。任务是诸如 compilepackage 之类的操作。它们可能返回 UnitUnit 是 Scala 的 void),或者它们可能返回与任务相关的值,例如 package 是一个 TaskKey[File],它的值是它创建的 jar 文件。

每次您开始执行任务时,例如在交互式 sbt 提示符下键入 compile,sbt 将只执行一次涉及的任何任务。

sbt 描述子项目的键值对可以保留一个设置(例如名称)的固定字符串值,但它必须保留一个任务(例如 compile)的一些可执行代码,即使该可执行代码最终返回一个字符串,它也必须每次都重新运行。

给定键始终引用任务或普通设置。 也就是说,“任务性”(是否每次都重新运行)是键的属性,而不是值的属性。

列出所有可用的设置键和任务键 

可以通过在 sbt 提示符下键入 settingssettings -v 来获取当前构建定义中存在的设置键列表。

同样,可以通过键入 taskstasks -v 来获取当前定义的任务键列表。您还可以查看 命令行参考,了解在 sbt 提示符下常用的内置任务的讨论。
如果键满足以下条件,它将打印在生成的列表中

  • 它是内置的 sbt(例如上面的示例中的 namescalaVersion
  • 您将其创建为自定义键
  • 您导入了一个将它引入构建定义的插件。

您也可以在 sbt 提示符下键入 help <key> 以获取更多信息。

定义任务和设置 

使用 :=,您可以为设置分配一个值,为任务分配一个计算。对于设置,该值将在项目加载时计算一次。对于任务,每次执行任务时都会重新运行该计算。

例如,要实现上一节中的 hello 任务

lazy val hello = taskKey[Unit]("An example task")

lazy val root = (project in file("."))
  .settings(
    hello := { println("Hello!") }
  )

我们已经看到定义设置的示例,当我们定义项目的名称时,

lazy val root = (project in file("."))
  .settings(
    name := "hello"
  )

任务和设置的类型 

从类型系统的角度来看,从任务键创建的 Setting 与从设置键创建的 Setting 略有不同。taskKey := 42 会生成一个 Setting[Task[T]],而 settingKey := 42 会生成一个 Setting[T]。在大多数情况下,这没有任何区别;任务键在任务执行时仍然会创建一个类型为 T 的值。

TTask[T] 类型差异具有以下含义:设置不能依赖于任务,因为设置仅在项目加载时评估一次,不会重新运行。有关更多信息,请参阅 任务图

sbt shell 中的键 

在 sbt shell 中,您可以键入任何任务的名称来执行该任务。这就是为什么键入 compile 会运行 compile 任务的原因。compile 是一个任务键。

如果你输入的是设置键的名称而不是任务键的名称,将显示设置键的值。输入任务键的名称会执行任务,但不会显示结果值;要查看任务的结果,请使用 `show <任务名称>` 而不是简单的 `<任务名称>`。键名称的约定是使用 `camelCase`,这样命令行名称和 Scala 标识符就相同。

要了解更多关于任何键的信息,请在 sbt 交互式提示符中键入 `inspect <键名>`。有些 `inspect` 显示的信息现在可能没有意义,但在顶部会显示设置的值类型和简要描述。

build.sbt 中的导入 

你可以在 `build.sbt` 的顶部放置导入语句;它们不需要用空白行分隔。

有一些隐含的默认导入,如下所示

import sbt._
import Keys._

(此外,如果你有自动插件,则 `autoImport` 下标记的名称将被导入。)

裸 .sbt 构建定义 

可以将设置直接写入 `build.sbt` 文件,而不是将它们放在 `settings(...)` 调用中。我们称之为“裸风格”。

ThisBuild / version := "1.0"
ThisBuild / scalaVersion := "2.12.18"

这种语法推荐用于 `ThisBuild` 范围的设置和添加插件。请参阅后面关于范围和插件的部分。

添加库依赖项 

要依赖第三方库,有两种选择。第一种是在 `lib/` 中放置 jar 文件(非托管依赖项),另一种是添加托管依赖项,它将在 `build.sbt` 中如下所示

val derby = "org.apache.derby" % "derby" % "10.4.1.3"

ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version      := "0.1.0-SNAPSHOT"

lazy val root = (project in file("."))
  .settings(
    name := "Hello",
    libraryDependencies += derby
  )

这是如何在 `build.sbt` 中添加对 Apache Derby 库(版本 10.4.1.3)的托管依赖项的方法。

`libraryDependencies` 键包含两个复杂之处: `+=` 而不是 `:=`,以及 `%` 方法。 `+=` 将附加到键的旧值而不是替换它,这在 任务图 中解释。 `%` 方法用于从字符串构造 Ivy 模块 ID,在 库依赖项 中解释。

我们将跳过库依赖项的细节,直到后面在入门指南中介绍。在后面的 整页 中会覆盖它。