1. 范围

范围 

此页面介绍了范围。假设您已经阅读并理解了前面的页面,构建定义任务图

关于键的完整故事 

我们先前假定像 name 这样的键对应于 sbt 的键值对映射中的一个条目。这是一种简化。

实际上,每个键可以在多个称为范围的上下文中具有关联的值。

一些具体示例

  • 如果您在构建定义中有多个项目(也称为子项目),则键可以在每个项目中具有不同的值。
  • compile 键可能对于您的主源代码和测试源代码具有不同的值,如果您想以不同的方式编译它们。
  • packageOptions 键(包含用于创建 jar 包的选项)在打包类文件 (packageBin) 或打包源代码 (packageSrc) 时可能具有不同的值。

给定键 name 没有单一值,因为值可能因范围而异。

但是,给定范围键有一个单一值。

如果您考虑 sbt 处理设置列表以生成描述项目的键值映射,如之前讨论过,该键值映射中的键是范围键。构建定义(例如在 build.sbt 中)中定义的每个设置也适用于范围键。

范围通常是隐式的或具有默认值,但如果默认值不正确,则需要在 build.sbt 中提及所需的范围。

范围轴 

范围轴是一个类似于 Option[A] 的类型构造器,用于在范围内形成一个组件。

有三个范围轴

  • 子项目轴
  • 依赖项配置轴
  • 任务轴

如果您不熟悉的概念,我们可以考虑 RGB 颜色立方体为例

color cube

在 RGB 颜色模型中,所有颜色都由立方体中的一个点表示,该点的轴对应于由数字编码的红色、绿色和蓝色分量。类似地,sbt 中的完整范围由子项目、配置和任务值的元组形成

projA / Compile / console / scalacOptions

这是在 sbt 1.1 中引入的用于以下内容的斜杠语法:

scalacOptions in (
  Select(projA: Reference),
  Select(Compile: ConfigKey),
  Select(console.key)
)

通过子项目轴进行范围限定 

如果您将多个项目放入单个构建中,则每个项目都需要自己的设置。也就是说,键可以根据项目进行范围限定。

项目轴也可以设置为 ThisBuild,这意味着“整个构建”,因此设置适用于整个构建,而不是单个项目。构建级设置通常用作项目未定义项目特定设置时的回退。我们将在本页面的后面部分讨论更多关于构建级设置的内容。

通过配置轴进行范围限定 

依赖项配置(简称“配置”)定义了一个库依赖项图,它可能具有自己的类路径、源代码、生成的包等。依赖项配置概念来自 Ivy,sbt 以前使用它来管理依赖项 库依赖项,以及来自 MavenScopes

您将在 sbt 中看到的一些配置

  • Compile 定义了主构建 (src/main/scala)。
  • Test 定义了如何构建测试 (src/test/scala)。
  • Runtime 定义了 run 任务的类路径。

默认情况下,与编译、打包和运行相关的所有键都范围限定到配置,因此它们在每个配置中的工作方式可能不同。最明显的例子是 compilepackagerun 任务键;但所有影响这些键的键(例如 sourceDirectoriesscalacOptionsfullClasspath)也范围限定到配置。

关于配置的另一件事是它可以扩展其他配置。下图显示了最常见配置之间的扩展关系。

dependency configurations

TestIntegrationTest 扩展 RuntimeRuntime 扩展 CompileCompileInternal 扩展 CompileOptionalProvided

通过任务轴进行范围限定 

设置会影响任务的工作方式。例如,packageSrc 任务受 packageOptions 设置影响。

为了支持这一点,任务键(例如 packageSrc)可以作为另一个键(例如 packageOptions)的范围。

构建包的各种任务 (packageSrcpackageBinpackageDoc) 可以共享与打包相关的键,例如 artifactNamepackageOptions。这些键可以对每个打包任务具有不同的值。

零范围组件 

每个范围轴都可以用轴类型的一个实例(类似于 Some(_))填充,或者用特殊值 Zero 填充。因此,我们可以将 Zero 视为 None

Zero 是所有范围轴的通用回退,但在大多数情况下,其直接使用应保留给 sbt 和插件作者。

Global 是一个范围,它将所有轴设置为 ZeroZero / Zero / Zero。换句话说,Global / someKeyZero / Zero / Zero / someKey 的简写形式。

在构建定义中引用范围 

如果您在 build.sbt 中使用裸键创建设置,它将范围限定到(当前子项目 / 配置 Zero / 任务 Zero

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

运行 sbt 并输入 inspect name 以查看它是通过 ProjectRef(uri("file:/private/tmp/hello/"), "root") / name 提供的,即项目是 ProjectRef(uri("file:/Users/xxx/hello/"), "root"),并且没有显示配置或任务范围(这意味着 Zero)。

右侧的裸键也范围限定到(当前子项目 / 配置 Zero / 任务 Zero

organization := name.value

任何范围轴的类型都已进行方法富化,具有 / 运算符。/ 的参数可以是键或另一个范围轴。因此,例如,虽然没有很好的理由这样做,但您可以将 name 键的一个实例范围限定到 Compile 配置

Compile / name := "hello"

或者您可以将范围限定到 packageBin 任务的名称(毫无意义!只是一个示例)

packageBin / name := "hello"

或者您可以使用多个范围轴设置 name,例如在 Compile 配置的 packageBin 任务中

Compile / packageBin / name := "hello"

或者您可以使用 Global

// same as Zero / Zero / Zero / concurrentRestrictions
Global / concurrentRestrictions := Seq(
  Tags.limitAll(1)
)

(Global / concurrentRestrictions 隐式转换为 Zero / Zero / Zero / concurrentRestrictions,将所有轴设置为 Zero 范围组件;任务和配置默认情况下已经是 Zero,因此这里的效果是使项目 Zero,也就是说,定义 Zero / Zero / Zero / concurrentRestrictions 而不是 ProjectRef(uri("file:/tmp/hello/"), "root") / Zero / Zero / concurrentRestrictions)

从 sbt shell 引用范围键 

在命令行和 sbt shell 中,sbt 以这种方式显示(并解析)范围键

ref / Config / intask / key
  • ref标识子项目轴。它可以是<project-id>ProjectRef(uri("file:..."), "id")或表示“整个构建”范围的ThisBuild
  • Config使用大写的Scala标识符来标识配置轴。
  • intask标识任务轴。
  • key标识要作用域的键。

Zero可以出现在每个轴上。

如果您省略作用域键的一部分,它将按如下方式推断:

  • 如果您省略项目,将使用当前项目。
  • 如果您省略配置或任务,将自动检测与键相关的配置。

有关更多详细信息,请参阅与配置系统交互

sbt shell中作用域键表示法的示例 

  • fullClasspath仅指定一个键,因此使用默认作用域:当前项目、与键相关的配置和Zero任务作用域。
  • Test / fullClasspath指定配置,因此这是Test配置中的fullClasspath,其他两个作用域轴使用默认值。
  • root / fullClasspath指定项目root,其中项目由项目 ID 标识。
  • root / Zero / fullClasspath指定项目root,并为配置指定Zero,而不是默认配置。
  • doc / fullClasspath指定作用域到doc任务的fullClasspath键,项目和配置轴使用默认值。
  • ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath指定项目ProjectRef(uri("file:/tmp/hello/"), "root")。还指定配置 Test,保留默认的任务轴。
  • ThisBuild / version 将子项目轴设置为“整个构建”,其中构建为ThisBuild,使用默认配置。
  • Zero / fullClasspath 将子项目轴设置为Zero,使用默认配置。
  • root / Compile / doc / fullClasspath 设置所有三个作用域轴。

检查作用域 

在 sbt shell 中,您可以使用inspect命令来理解键及其作用域。尝试 inspect Test/fullClasspath

$ sbt
sbt:Hello> inspect Test / fullClasspath
[info] Task: scala.collection.Seq[sbt.internal.util.Attributed[java.io.File]]
[info] Description:
[info]  The exported classpath, consisting of build products and unmanaged and managed, internal and external dependencies.
[info] Provided by:
[info]  ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath
[info] Defined at:
[info]  (sbt.Classpaths.classpaths) Defaults.scala:1639
[info] Dependencies:
[info]  Test / dependencyClasspath
[info]  Test / exportedProducts
[info]  Test / fullClasspath / streams
[info] Reverse dependencies:
[info]  Test / testLoader
[info] Delegates:
[info]  Test / fullClasspath
[info]  Runtime / fullClasspath
[info]  Compile / fullClasspath
[info]  fullClasspath
[info]  ThisBuild / Test / fullClasspath
[info]  ThisBuild / Runtime / fullClasspath
[info]  ThisBuild / Compile / fullClasspath
[info]  ThisBuild / fullClasspath
[info]  Zero / Test / fullClasspath
[info]  Zero / Runtime / fullClasspath
[info]  Zero / Compile / fullClasspath
[info]  Global / fullClasspath
[info] Related:
[info]  Compile / fullClasspath
[info]  Runtime / fullClasspath

在第一行,您可以看到这是一个任务(与设置相反,如.sbt 构建定义中所述)。任务产生的值将具有类型scala.collection.Seq[sbt.Attributed[java.io.File]]

“由...提供”指向定义值的范围键,在本例中为ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath(它是作用域到Test配置和ProjectRef(uri("file:/tmp/hello/"), "root")项目的fullClasspath键)。

“依赖项”在上一页中进行了详细讨论。

我们将在后面讨论“委托”。

尝试 inspect fullClasspath(与上面的示例不同,检查 Test / fullClasspath)以了解差异。由于省略了配置,因此它被自动检测为Compile。因此,inspect Compile / fullClasspath应该与inspect fullClasspath看起来相同。

尝试 inspect ThisBuild / Zero / fullClasspath 以进行另一种对比。默认情况下,fullClasspath 未在Zero配置作用域中定义。

同样,有关更多详细信息,请参阅与配置系统交互

何时指定作用域 

如果所讨论的键通常有作用域,则需要指定作用域。例如,默认情况下,compile 任务的作用域为CompileTest 配置,并且在这些作用域之外不存在。

要更改与compile键关联的值,您需要编写Compile / compileTest / compile。使用普通的compile将定义一个作用域到当前项目的新编译任务,而不是覆盖作用域到配置的标准编译任务。

如果您收到类似于“引用未定义设置”的错误,通常是因为您没有指定作用域,或者您指定了错误的作用域。您正在使用的键可能在其他作用域中定义。sbt 将尝试在错误消息中建议您的本意;寻找“您是说 Compile / compile 吗?”

一种思考方式是,名称仅仅是键的一部分。实际上,所有键都包含名称和作用域(其中作用域具有三个轴)。完整的表达式Compile / packageBin / packageOptions 是一个键名,换句话说。仅仅packageOptions也是一个键名,但它是一个不同的键(对于没有斜杠的键,隐式地假设一个作用域:当前项目、Zero 配置、Zero 任务)。

构建级设置 

在子项目之间提取通用设置的先进技术是将设置定义为作用域到ThisBuild

如果在特定子项目中作用域的键未找到,sbt 将在ThisBuild中查找它作为后备。使用这种机制,我们可以为versionscalaVersionorganization等常用键定义构建级默认设置。

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

lazy val root = (project in file("."))
  .settings(
    name := "Hello",
    publish / skip := true
  )

lazy val core = (project in file("core"))
  .settings(
    // other settings
  )

lazy val util = (project in file("util"))
  .settings(
    // other settings
  )

为了方便起见,有一个inThisBuild(...)函数将把键和设置表达式的正文都作用域到ThisBuild。将设置表达式放在那里等效于在可能的情况下在前面加上ThisBuild /

由于作用域委托的性质(我们将在后面讨论),构建级设置应该只设置为纯值或来自GlobalThisBuild作用域的设置。

作用域委托 

如果在作用域中没有与之关联的值,作用域键可能未定义。

对于每个作用域轴,sbt 都具有由其他作用域值组成的后备搜索路径。通常,如果键在更具体的范围中没有关联的值,sbt 将尝试从更一般的范围(例如ThisBuild范围)获取值。

此功能允许您在更一般的范围内设置一次值,从而允许多个更具体的范围继承该值。我们将在后面详细讨论作用域委托