17. 使用sbt构建项目

虽然你可以使用Ant、Maven和Gradle等工具来构建你的Scala项目,但sbt —— 原名为 Simple Build Tool —— 是Scala应用事实上的构建工具。sbt使基本的构建和依赖管理任务变得简单,并让你使用Scala语言本身来克服更困难的任务。

sbt使用与Maven相同的目录结构,和Maven一样,它采用了“惯例大于配置”的方法,使基本项目的构建过程变得异常简单。因为它提供了一个众所周知的标准构建过程,如果你在一个用sbt构建的Scala项目上工作,你可以很容易转移到另一个也用sbt的项目。项目的目录结构将是相同的,而且你会知道你应该看看 build.sbt 文件和可选的 project/*.sbt 文件,观察构建过程是如何配置的。

从1.3.0版本开始,sbt开始使用Coursier( https://get-coursier.io )进行类库管理,Coursier网站将这项任务称为 工件获取(artifact fetching)。在1.3.0之前,sbt使用Apache Ivy来完成这项任务,但Coursier旨在成为一个更快的替代品。当你在 build.sbt 文件中指定 托管的依赖关系(managed dependencies) 时,Coursier是一个为你检索JAR文件的工具。

除了处理托管的依赖关系外,你还可以将 非托管的依赖关系(unmanaged dependencies) —— 普通的旧JAR文件放在项目的lib文件夹中,sbt会自动找到它们。

能够支持这个项目是我的荣幸。由于所有的这些特性,你只需付出很少的努力,sbt就可以让你建立包含Scala和Java代码、单元测试以及托管和非托管依赖的项目。

sbt的功能

简而言之,sbt的主要功能是:

  • 它使用Maven的标准目录结构,因此很容易构建标准的Scala项目,也很容易在不同的sbt项目之间移动。

  • 小项目只需要很少的配置。

  • 构建定义文件使用Scala DSL,所以你能使用Scala代码来构建Scala项目。

  • sbt支持在同一个项目中编译Scala和Java源代码文件。

  • 它同时支持托管和非托管的依赖关系。

  • 你可以使用多种测试框架,包括ScalaTest( https://www.scalatest.org )、ScalaCheck( https://www.scalacheck.org )和MUnit( https://scala meta.org/munit ),JUnit也有一个插件支持。

  • 源代码可以在交互式或批量模式下进行编译。

  • 支持连续编译和测试。

  • 支持增量编译和测试(只有改变的源代码文件被重新编译)。

  • 支持多个子项目。

  • 能够打包和发布JAR文件。

  • 生成和打包项目文档。

  • 与IntelliJ IDEA和VS Code简单集成。

  • 你可以在sbt中启动Scala REPL,所有项目的类和依赖关系都会自动在classpath上可用。

  • 并行任务和测试执行。

理解sbt的理念

要使用sbt,就必须了解它的关键概念。首先要知道的是,sbt是一个构建工具 —— 它是用来构建Scala项目的。你可以使用Ant、Maven、Gradle和Mill( https://oreil.ly/8sLBz )等其他工具来构建Scala项目,但sbt是第一个Scala构建工具,而且它仍然被广泛使用。

目录结构

第二件要知道的事是,sbt使用的目录结构与Maven相同,所以一个有一个非托管依赖(JAR文件)、一个源代码文件和一个测试文件的简单项目就有这样的目录结构:

    .
    |-- build.sbt
    |-- lib
        |-- my-library.jar
    |-- project
    |   `-- build.properties
    `-- src
        |-- main
        |   `-- scala
        |       `-- example
        |           `-- Hello.scala
        `-- test
            `-- scala
                `-- example
                    `-- HelloTest.scala

如图所示,Scala源代码文件放在 src/main/scala 目录下,测试文件放在 src/test/scala 目录下。如果你想在项目中包含Java源代码文件,它们将被放在 src/main/javasrc/test/java 目录下。如前所述,lib 目录下的JAR文件将在编译、测试和构建项目时自动作为依赖使用。

配置文件不是必须的,但是... -- TODO 鸽子栏

严格来说,对于一个极其简单的项目,build.properties 甚至 build.sbt 文件都不是必要的,但作为一个实际问题,你会在每个严肃的项目中看到它们。

build.sbt

接下来要知道的是,项目的大部分配置信息都在一个名为 build.sbt 的文件中,它属于项目的根目录。关于 build.sbt 需要知道的事情包括:

  • 它由键/值对形式的设置(name := MyProject )和用sbt的自定义DSL编写的Scala代码组成。

  • 大多数项目开始时至少有三个设置:项目名称、项目版本和用于编译项目的Scala版本。这些都是用名为 nameversionscalaVersion 的键指定的。

  • 小项目可能只包括一些设置,而大项目可能包括几十行的设置和Scala代码。

  • 托管的依赖关系也在这个文件中,用 libraryDependencies 键指定。

作为最后一点的预览,libraryDependencies 设置看起来像这样:

    libraryDependencies ++= Seq(
        "org.typelevel" %% "cats-core" % "2.6.0",
        "org.typelevel" %% "cats-effect" % "3.1.0"
    )

请注意,这只是正常的Scala代码。

其他注意事项

关于sbt,需要了解的其他几件事是:

  • 你可以在一个sbt项目中包含多个项目。我在博文“How to Create an sbt Project with Subprojects”( https://oreil.ly/VR334 )中展示了这一点。

  • 你可以在 build.sbt 文件中添加你自己的导入语句,以便在构建中使用你自己的类。这些包是默认导入的:

      — sbt.*
      — sbt.Keys.* 
      — Process.*

作为最后的说明,本章中所有的示例都是用sbt1.5.1版本测试的。

17.1 为sbt创建一个项目目录结构

问题

你要创建一个新的Scala/sbt项目所需的初始文件和目录。

解决方案

使用shell脚本或 sbt new 命令来创建新项目。这里展示了这两种方法。

方法1:使用一个shell脚本

sbt使用与Maven相同的目录结构,所以如果使用的是Unix系统,可以用shell脚本生成一个兼容的结构。例如,下面的shell脚本可以为大多数项目创建初始的文件和目录集:

    #!/bin/sh
    mkdir -p src/{main,test}/{java,resources,scala}
    mkdir project

    # create an initial build.sbt file
    echo 'name := "MyProject"
    version := "0.1"
    scalaVersion := "3.0.0"

    // libraryDependencies ++= Seq(
    //     "org.scalatest" %% "scalatest" % "3.2.3" % "test"
    // )
    ' > build.sbt

    # create a project/build.properties file with the desired sbt version
    echo 'sbt.version=1.5.1' > project/build.properties

只要在Unix系统上把这段代码保存为shell脚本,使其可执行,并在一个新的项目目录下运行它,以创建所有sbt需要的子目录和文件。例如,假设这个脚本在你的路径上,并被命名为 mkdirs4sbt,这个过程看起来像这样:

    /Users/Al/Projects> mkdir MyNewProject 
    /Users/Al/Projects> cd MyNewProject 
    /Users/Al/Projects/MyNewProject> mkdirs4sbt

如果你在系统中安装了tree命令,并在当前目录下运行它,你会看到这些命令会创建这些文件和目录:

    $ tree .
    .
    ├── build.sbt
    ├── project
    │   └── build.properties 
    └── src
        ├── main
        |   ├── java
        │   ├── resources
        │   └── scala
        └── test
            ├── java
            ├── resources
            └── scala

正如shell脚本所暗示的,build.sbt 文件有这些内容:

    name := "MyProject"
    version := "0.1"
    scalaVersion := "3.0.0"
    // libraryDependencies ++= Seq(
    //     "org.scalatest" %% "scalatest" % "3.2.3" % "test"
    // )

前三行设置了键/值对,你在每个sbt项目中都会用到:

  • name 声明你的项目的名称。

  • version 设置项目的版本级别。

  • scalaVersion 设置用于编译的Scala版本。

在这之后,libraryDependencies 一行声明了项目的所有依赖。因为我在大多数项目中都使用ScalaTest,所以我把它放在这里。

我还将 libraryDependencies 声明为 Seq ,因为我的项目中通常有不止一个依赖。如果你只添加一个依赖,你可以像这样声明这一行:

    libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.3" % "test"

注意,在第一个例子中我使用了 ++=,在第二个例子中我使用了 +=。在这两种情况下,这是因为我把这个依赖添加到之前可能定义的其他依赖中。与此相反,前三个参数是用 := 设置的,在这几行中我只设置了一个值,但 libraryDependencies 允许你添加多个依赖关系。

通过这个shell脚本,project/build.properties 文件被创建,其中有这些内容:

    sbt.version=1.5.1

这告诉sbt launcher,我想在这个项目上使用1.5.1版本的sbt。

这只是一个简单的启动脚本,我首先展示它,以证明创建一个sbt项目是多么容易。关于更完整的shell脚本 sbtmkdirs,请参阅我的博文“sbtmkdirs: A Shell Script to Create a Scala SBT Project Directory Structure”( https:// oreil.ly/o5uYO )。

控制scalac -- TODO 耗子栏

在我的项目中,我通常会添加一系列的选项来控制 scalac 编译器如何与sbt一起工作。这些是我在Scala 3中使用的几个选项:

    scalacOptions ++= Seq(
        "-deprecation",
        "-explain",
        "-explain-types",
        "-new-syntax",
        "-unchecked",
        "-Xfatal-warnings",
        "-Xmigration"
    )

你可以把这些选项添加到 build.sbt 文件的末尾,或者添加到你在后面的 sbt new 部分看到的 settings 方法里面。

方法2:使用sbt new

虽然该脚本展示了如何简单地建立一个初始的sbt项目,但你也可以使用 sbt new 命令从预先构建的模板中创建新项目。这些模板是开源的,由其他sbt用户创建,它们被用来创建预先配置好的sbt项目,以使用一个或多个Scala工具,如ScalaTest、Akka和其他。我发现这些模板还使用了不同的编码风格,当你想看到不同的sbt配置功能时,这对你很有帮助。

为了展示它是如何工作的,这个 sbt new 命令大致相当于我刚才展示的shell脚本:

    $ sbt new scala/scala3.g8

这是从你的操作系统命令行中看到的过程:

    $ sbt new scala/scala3.g8
    // some initial output here ...

    A template to demonstrate a minimal Scala 3 application

    name [Scala 3 Project Template]: My New Project

    Template applied in ./my-new-project

my-new-project 目录现在包含一个 build.sbt 文件以及其他目录和文件,因此你可以将其用于一个新的Scala/sbt项目。

讨论

sbt new 的方法与使用shell脚本有很大不同,所以值得进一步讨论。首先,下面是关于 sbt new 如何工作的一些说明:

  • 可以使用不同的模板构造项目,但 sbt new scala/scala3.g8 命令使用一个名为Giter8( http://www.foundweekends.org/giter8 )的工具寻找并运行一个名为 scala3.g8 的模板。

  • 在这个例子中,scala3.g8 模板可以在这个GitHub页面找到( https://github.com/scala/scala3.g8 )。

  • 根据Giter8的网站,“Giter8是一个命令行工具,可以从GitHub或任何其他Git仓库发布的模板中生成文件和目录。”

  • 因为这个命令是从GitHub上调取模板,所以运行可能需要花一些时间。

  • 该命令将项目名称“My New Project”转换为名为 my-new-project 的目录。

由于采用了模板方法,这个命令创建的目录结构和文件可能会随着时间的推移而改变,但在写这篇文章时,从 scala3.g8 模板创建的结构看起来是这样的:

    $ tree .
    .
    ├── README.md
    ├── build.sbt
    ├── project
    │   └── build.properties 
    └── src
        ├── main
        │   └── scala
        │       └── Main.scala
        └── test
            └── scala
                └── Test1.scala

模板创建的文件

模板的 build.sbt 文件看起来是这样的:

    val scala3Version = "3.0.0"
    lazy val root = project
      .in(file("."))
      .settings(
        name := "scala3-simple",
        version := "0.1.0",
        scalaVersion := scala3Version,
        libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test"
    )

像往常一样,project/build.properties 文件包含最新的sbt版本:

    sbt.version=1.5.1

build.sbt 的语法与我在shell脚本中使用的不同。虽然我发现在第一次学习sbt的时候,shell脚本中的方法更容易阅读,但当项目越来越大时,这第二种方法更受欢迎,部分原因是它看起来更像Scala代码,所以它更像是用Scala代码控制你的Scala项目配置。

随着需求的增长,你会看到额外的 build.sbt 变量。例如,如果你要发布一个类库到一个公共仓库,并想控制像 pom.xml 文件中的内容那样,你会想指定与组织有关的参数:

    organization := "com.alvinalexander"
    organizationName := "Alvin Alexander"
    organizationHomepage := Some(url("https://alvinalexander.com"))

你可能想为这个用例配置的其他参数显示在sbt项目的元数据页面( https://oreil.ly/2BAc4 ),包括:

    homepage := Some(url("https://www.scala-sbt.org"))
    startYear := Some(2008)
    description := "A build tool for Scala."
    licenses += "GPLv2" -> url("https://www.gnu.org/licenses/gpl-2.0.html")

综上所述,使用 sbt new 命令的主要好处是:

  • 已经创建了模板来帮助你开始使用ScalaTest、Akka、Play Framework、Lagom、Scala Native等等。

  • 这些模板目前生成的 build.sbt 文件都有些不同,所以你可以看到不同的配置方式,也就是其他用户喜欢的方式。

你可以在sbt网站( https://oreil.ly/XoVSg )上找到与 sbt new 一起使用的模板列表。

.gitignore中的文件和目录 -- TODO 耗子栏

假设你将代码保存在Git仓库中,你还需要创建一个 .gitignore 文件来告诉Git应该忽略哪些sbt文件和目录。这是两个你想告诉Git忽略的初始目录:

    target/
    project/target/

我的 sbtmkdirs 脚本( https://oreil.ly/o5uYO )增加了许多其他条目,以说明IntelliJ IDEA、VS Code、Bloop( https://scalacenter.github.io/bloop )和Metals( https://scalameta.org/metals )等工具。

17.2 使用sbt命令构建项目

问题

你需要看看如何用sbt命令来编译、测试和运行你的项目。

解决方案

使用sbt命令来构建、编译、测试和打包你的项目。例如,这个命令可以编译你的项目:

    $ sbt compile

如果你在项目中配置了ScalaTest( https://www.scalatest.org )这样的测试框架的话,这个命令就可以运行你的项目测试:

    $ sbt test

而这个命令是在你的项目中运行 main 方法:

    $ sbt run

在17.4和17.11小节中详细介绍了从一个sbt项目创建一个JAR文件,但作为一个快速介绍,你可以使用 package 命令将一个简单的项目打包成一个JAR文件:

    $ sbt package

多个@main方法 -- TODO 鸽子栏

如果你的项目有多个 @main 方法,请参阅17.10小节以了解在使用 runpackage 命令时如何处理它们。

讨论

需要理解的一点是,系统中的 sbt 命令只是一个 启动器(launcher)。它启动了整个sbt进程,但由于sbt能够使用不同版本的Scala和sbt,所以 sbt 命令只是启动了这个进程。当它启动时,它会下载任何它需要的资源,包括你想在项目中使用的Scala和sbt的版本。(事实上,当你运行sbt时,你实际上是在Unix系统上运行一个Bash脚本或在Windows上运行一个批处理文件)。

正因为如此,建议你在你的项目的 project/build.properties 文件中加入这样的设置:

    sbt.version=1.5.1

这告诉sbt启动器,在这个项目上运行命令时,你想使用sbt1.5.1版本。这样做是为了确保在团队环境中,每个人在进行构建时都使用的是相同版本的sbt。如果你不设置这个值,sbt会在第一次运行时将其设置为最新版本。

批量和交互式模式

关于sbt命令另一个需要注意的是,它可以在批处理模式下运行,也可以交互式模式下运行。我之前展示的命令是 批处理模式(batch mode) 的命令:

    $ sbt compile $ sbt test
    $ sbt run
    $ sbt package

它们从你的操作系统命令行运行。它们首先启动 sbt 启动器,然后运行你指定的任何命令。因为它们是从操作系统的命令行运行的,所以sbt需要花一点时间来启动,因此这不是运行sbt的首选方式,除非你从一个脚本中运行它,比如Unix的cron进程。(Unix中的cron系统是一种调度工作的方式,可以在特定的日期和时间运行。)

首选的方法是在 交互式(interactive) 模式下运行sbt。在这种模式下,你从操作系统的命令行启动一次sbt:

    $ sbt

然后你在sbt shell里面运行命令:

    > compile > test
    > run
    > package

这些命令的运行速度明显加快,因为sbt已经经过预热,并处于运行的状态。如17.6小节所示,你也可以连续运行 compiletest 命令。在这种用法中,每当你改变项目中的一个文件时,这些命令就会运行。

sbt只需要用到Java -- TODO 鸽子栏

你不需要安装Scala来运行sbt。它只要求你安装了Java JDK。

17.3 理解build.sbt语法风格

问题

你需要以更复杂的风格来编写 build.sbt 文件,以便利用更强大的sbt功能。

解决方案

在17.1小节中,我展示了你可以在一个简单的项目中使用这种语法:

    name := "MyProject"
    version := "0.1"
    scalaVersion := "3.0.0"

同样重要的是要知道,由于sbt的强大和灵活,你也可以用更多的Scala风格来写这个配置,比如这样:

    // 定义位于当前目录下的 "subproject"。
    lazy val root = (project in file("."))
        .settings(
            name := "MyProject",
            version := "0.1",
            scalaVersion := "3.0.0"
        )

或者这种光秃秃的风格:

    ThisBuild / scalaVersion := "3.0.0"
    ThisBuild / version      := "0.1"
    ThisBuild / name         := "MyProject"

这些风格是优先考虑的 -- TODO 鸽子栏

本文在2021年年中撰写,这些风格是目前比较推荐使用的。多了解一下它们也很重要,当你在使用sbt新模板时将会看到它们,并且当构建变得越来越复杂的时候,你会希望自己已经熟习了这些风格。

你也可以把这些风格结合起来。这里是上两个例子的组合,同时增加了依赖:

    ThisBuild / scalaVersion := "3.0.0"
    ThisBuild / version      := "0.1"

    val catsCore = "org.typelevel" %% "cats-core" % "2.6.0",
    val catsEffect = "org.typelevel" %% "cats-effect" % "3.1.0"

    lazy val root = (project in file("."))
        .settings(
            name := "MyProject",
            libraryDependencies ++= Seq(
              catsCore, 
              catsEffect 
            )
        )

当你的项目变得越来越复杂时,你可以试试用sbt DSL和普通Scala代码的组合来构建它们,你会感觉很不错的。

讨论

注意事项:比如,在这些例子中的这个语法:

    (project in file("."))

其是,是创建了一个 sbt Project 的实例。因此,它后面的 .settings 代码是你在 Project 对象上调用的一个方法。

另一件重要的事情是,文件中的 project in file 是指在当前目录下可以找到一个sbt项目,其中 . 的语法指的是当前目录。参阅我的博文“How to Create an sbt Project with Subprojects”( https://oreil.ly/VR334 ),以了解关于这种语法的更多细节。

17.4 编译、运行和打包一个Scala项目

问题

你想用sbt来编译和运行一个Scala项目,并将该项目打包成一个JAR文件。

解决方案

创建一个sbt目录结构,添加你的代码,然后执行 sbt compile 命令编译项目,执行 sbt run 命令运行项目,执行 sbt package 命令将项目打包成JAR文件。这些命令可以在批处理模式或交互式模式下使用。

为了证明这一点,如17.1小节所示,创建一个新的sbt项目,然后在 src/main/scala 目录下创建一个名为 Hello.scala 的文件,内容如下:

    package foo.bar.baz
    @main def main = println("Hello, world")

如上面的示例所示,在Scala中,文件的包名不一定要和它所在的目录名称相同。事实上,对于像这样的简单测试,如果你愿意,甚至可以把这个文件放在你的sbt项目的根目录下。

在项目的根目录执行命令,下面是编译项目的方法:

    $ sbt compile

运行项目:

    $ sbt run

并打包项目:

    $ sbt package

这些命令显示了sbt的批处理模式。一般来说,当你在一个项目上工作时,在交互式模式下从sbt shell里面运行同样的命令会更快:

    $ sbt
    sbt> compile 
    sbt> run 
    sbt> package

注意,package 命令并不显示它所创建的输出JAR文件的名称。要看到这个名字,可以运行 show package 来代替:

    sbt> show package
    // the output file name and location is shown here

讨论

在你第一次运行sbt时,它可能需要一段时间来下载它所需要的一切,但在第一次运行后,它只在需要时下载新的依赖。

当你用 package 命令创建一个JAR文件时,它会创建一个普通的JAR文件,你可以用 jar tvf 命令显示其内容:

    $ jar tvf ./target/scala-3.0.0/my-project_3.0.0-0.1.0.jar 
       288 Thu Jan 01 00:00:00 MST 1970 META-INF/MANIFEST.MF 
       969 Thu Jan 01 00:00:00 MST 1970 Main$.class
       350 Thu Jan 01 00:00:00 MST 1970 Main.class
       649 Thu Jan 01 00:00:00 MST 1970 Main.tasty

交互式模式

首选在交互式模式下运行sbt,因为它更快;JVM已经被加载到内存中,所以没有初始启动的滞后时间。下面是在sbt解释器(interpreter)中运行 cleancompilerun 命令的输出:

    $ sbt

    sbt> clean
    [success] Total time: 0 s

    sbt> compile
    [info] compiling 1 Scala source ... [success] Total time: 2 s

    sbt> run
    [info] Running foo.bar.baz.main Hello, world
    [success] Total time: 1 s

你也可以在sbt中一个命令接着一个命令运行。这就是你在 compile 命令之前运行 clean 命令的方法:

    sbt> clean; compile

在命令行中向sbt传递参数

虽然我几乎总是在交互式模式下运行sbt,但当你从操作系统的命令行中运行sbt并需要向你的应用程序传递参数时,你需要把sbt的 run 命令和参数一起放在引号里。例如,给定的这个小程序会打印其命令行参数:

    @main def hello(args: String*) =
        print(s"Hello, ")
        for a <- args do print(s"$a ")

当用sbt运行这个应用程序时,要向它传递命令行参数,请将 run 命令和参数用引号括起来,像这样:

    $ sbt "run Charles Carmichael" 
    // omitted sbt output here ... 
    // Hello, Charles Carmichael

无论你是用这种技术在sbt中运行一个应用程序,还是启动sbt以交互式模式运行它,你都可以将JVM选项(options)传递给sbt。例如,本例子展示了如何使用sbt的 -J-D 选项,在运行同一个应用程序的同时向JVM传递参数:

    $ sbt -v -J-Xmx2048m -Duser.timezone=America/Denver "run Charles Carmichael" 
    [process_args] java_version = 11
    # Executing command line:
    java
    -Dfile.encoding=UTF-8
    -Xmx2048m
    -Duser.timezone=America/Denver
    -jar
    /Users/al/bin/sbt/bin/sbt-launch.jar
    "run Charles Carmichael"
    // omitted sbt output here ...
    Hello, Charles Carmichael

-v 选项代表 verbose,所以除了在最后看到我的应用程序的输出外,前面的 verbose 输出显示了 -J-D 参数被成功地传入了sbt:

在不同的JVM中运行 -- TODO 耗子栏

根据sbt关于分叉的页面( https://oreil.ly/AZs6i ),“run 任务与sbt在同一个JVM中运行”。我发现有时 —— 例如在创建JavaFX或其他多线程应用程序时 —— 这可能是一个问题。要在sbt提示符下运行你的应用程序并叉开一个新的JVM,可以在你的 build.sbt 文件中添加这样一行:

    fork := true

还有其他几个选项可以让你控制fork过程。更多细节请参阅该文档页面。

17.5 理解其他sbt命令

问题

你需要知道还有哪些常用的sbt命令可用,包括如何列出所有可用的命令和任务。

解决方案

除了 cleancompilerunpackage 命令外,还有 许多 sbt命令可供你使用。你至少可以通过三种方式列出可用的命令:

  • help 打印一个高级命令的列表。

  • tasks 显示为当前项目定义的任务列表(包括 cleancompilerun)。

  • 你也可以在sbt提示符下按两次Tab键,它将显示可以运行的所有命令 —— 在sbt 1.5.1中超过300个命令。

你可以通过输入 help inspect 。比如说:

  • help package 提供了关于 package 命令的帮助。

  • inspect package 提供了关于 package 如何运行的深入细节。

表17-1包含了最常见的sbt命令的描述。

表17-1. 常见的sbt命令

命令
描述

clean

删除由构建产生的文件,包括编译的类、任务缓存和生成的源文件。

compile

编译 src/main/scalasrc/main/java 以及项目根目录下的源代码文件。

~compile

当你在交互式模式下运行sbt时,自动重新编译源代码文件。

console

编译项目中的源代码文件,将其放在classpath上,并启动Scala解释器(REPL)。

consoleQuick

使用classpath上的项目依赖启动Scala REPL,但不编译项目源代码文件。

doc

从你的Scala源代码生成API文档。

help [arg]

不携带参数执行时,它列出了当前可用的普通命令。当给定一个参数时,它提供该任务或键的描述。

inspect [arg]

显示有关给定设置或任务如何工作的详细信息(如 inspect package)。

package

创建一个JAR文件,其中包含 src/main/scalasrc/main/java 中的文件,以及 src/main/resources 中的资源。

packageDoc

创建一个JAR文件,包含从你的Scala源代码生成的API文档。

publish

将你的项目发布到一个远程仓库。见17.12小节。

publishLocal

将项目工件发布到本地的Ivy仓库。

reload

重新加载构建定义文件。如果你改变了这些文件中的任何一个,就需要在交互模式下进行。

run

编译你的代码,并运行你项目中的main class。如果你的项目有多个main方法,你会被提示需要选择一个来运行。

settings [arg]

显示为当前项目定义的设置。-v 显示一些的设置,--v 显示更多的设置,-V 显示所有设置。

show [setting]

显示一个设置的值,比如 show sbtVersion

show [task]

执行任务并显示其返回的值。

test

编译并运行所有测试。

testQuick [test*]

运行尚未运行的测试,包括上次运行失败的测试,或自上次成功运行后有任何传递依赖关系被重新编译的测试。

~test

当项目源代码文件改变时,自动重新编译和重新运行测试。

test:console

编译项目中的源代码文件,将其放在classpath上,并在测试模式下启动Scala REPL(因此你可以使用ScalaTest、MUnit、ScalaCheck等)。

除了内置的命令外,当你使用插件时,它们也可以使用自己的任务。例如,Scala.js插件给sbt增加了一个 fastLinkJS 命令。

讨论

作为这些命令的一个例子,console 命令从你的sbt命令提示符中启动一个Scala REPL会话:

    sbt:MyProject> console
    [info] Compiling 10 Scala sources to target/scala-3.0.0/classes ... 
    [info] Done compiling.
    [info] Starting scala interpreter...

    scala> _

在这一点上,你可以像普通的Scala REPL一样使用它,而且还有一个额外的好处,就是当前项目中的所有的类都可以直接使用。

另见

  • sbt命令行参考( https://oreil.ly/W6wGH )提供了更多的例子和对可用命令的讨论。

17.6 持续的编译和测试

问题

你想让sbt在你对应用的源代码进行修改时,不断地编译和测试它。

解决方案

通过在sbt的交互式模式(即在sbt shell内)运行以下命令,可以 持续地 编译和测试代码:

  • ~compile

  • ~test

  • ~testQuick

当你运行这些命令时,sbt会监视你的源代码文件,并在看到文件变化时自动重新编译。如表17-2所述,~compile 命令只是在检测到文件变化时重新编译你的代码,而 test 命令则额外地运行你的测试。

为了证明这一点,从一个sbt项目的根目录启动sbt shell:

    $ sbt

然后发出 ~compile 命令:

    sbt:Packaging> ~ compile
    [success] Total time: 0 s
    [info] 1. Monitoring source files for root/compile ... 
    [info] Press <enter> to interrupt or ? for more options.

现在,任何时候你改变并保存一个源代码文件,sbt都会自动重新编译它。当sbt重新编译代码时,你会看到像这样的新输出行:

    [info] Build triggered by src/main/scala/Foo.scala. Running 'compile'.
    [info] compiling 1 Scala source to target/scala-3.0.0/classes ...
    [success] Total time: 0 s
    [info] 2. Monitoring source files for root/compile ...
    [info]    Press <enter> to interrupt or '?' for more options

同样,只要有变化,你可以使用这些sbt命令来自动运行项目的测试:

    ~ test
    ~ testQuick

注意,在所有这些命令中,~ 字符后面的空格是可选的。

表17-2提供了这些命令的描述。

表17-2. sbt的持续compile/test命令

命令
描述

~compile

当你在交互式模式下运行sbt时,自动重新编译源代码文件

~test

当项目源代码文件改变时,自动重新编译并重新运行测试。

~testQuick

自动重新编译并重新运行尚未运行的测试、上次运行失败的测试或自上次成功运行后有任何传递依赖关系被重新编译的测试

使用这些命令有点像在你的本地系统上使用一个持续集成服务器,尽管使用的是你自己的代码。

17.7 用sbt管理依赖

问题

你想在你的Scala/sbt项目中使用一个或多个外部类库(依赖)。

解决方案

如以下小节所述,你可以在sbt项目中使用托管的和非托管的依赖。

非托管依赖

如果你有自己某些特定的JAR文件—— 无人管理的依赖,或者更准确地说,是 自己管理的依赖 —— 想在你的项目中使用,只要把这些文件放在sbt项目根目录下的 lib 文件夹中,sbt就会自动找到它们。(如果你已经在运行一个sbt会话,则需要运行 reload 命令。)如果这些JARs依赖于其他的JAR文件,你必须手动下载这些被依赖的其他JAR文件并复制到 lib 目录中。

托管依赖

如果你有一个托管依赖想在项目中使用,例如Cats核心库( https://oreil.ly/fg5pO ),在 build.sbt 文件中添加一个 libraryDependencies 行,像这样:

    libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.0"

一个简单却完整的有一个依赖关系的 build.sbt 文件看起来像这样:

    name := "MyCatsProject"
    version := "0.1"
    scalaVersion := "3.0.0"
    libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.0"

要在你的项目中添加多个托管的依赖,请在 build.sbt 文件中使用 Seq 添加它们:

    libraryDependencies ++= Seq(
        "org.typelevel" %% "cats-core" % "2.6.0",
        "org.typelevel" %% "cats-effect" % "3.1.0"
    )

在Scala 3构建中使用Scala 2.13的依赖

当你想在Scala 3 build.sbt 文件中使用Scala 2.13的依赖时,使用这个 cross(CrossVersion.for3Use2_13) 语法:

    libraryDependencies ++= Seq(
      ("org.scala-js" %%% "scalajs-dom" % "1.1.0").cross(CrossVersion.for3Use2_13),
      ("org.querki" %%% "jquery-facade" % "2.0").cross(CrossVersion.for3Use2_13)
    )

这项技术的展示在21.2小节“用Scala.js响应事件”和21.3小节“使用Scala.js构建单个页面的应用程序”,在这两个示例中,我将Scala 2.13的依赖关系纳入了Scala 3的sbt构建文件。

你可以在Scala 3构建中使用Scala 2.13依赖,除非这些依赖使用Scala 2宏。有关的最新整合细节,请参阅Scala 3迁移指南 ( https://oreil.ly/ex7FZ ) 。

讨论

托管依赖 是指由你的构建工具管理的依赖,这里是指sbt。在这种情况下,如果库 a.jar 依赖于 b.jar,而该库又依赖于 c.jar,并且这些JAR文件与这些关系信息一起保存在Maven仓库中,那么你只需在 build.sbt 文件中添加这样一行:

    libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.0"

通过sbt、Coursier和生态系统中其他工具的魔力,其他JAR文件将被下载并自动包含在你的项目中。

当使用一个独立的JAR文件作为 非托管依赖 时,你必须自己管理这个。考虑一个与上一段所描述的相同情况,如果你想在项目中使用库 a.jar,你必须手动下载 a.jar,然后你还必须知道对 b.jar 的依赖和对 c.jar传递依赖,然后自己下载所有这些文件并将它们放在项目的 lib 目录中。

大多数项目使用托管依赖 -- TODO 鸽子栏

手动管理 lib 目录下的JAR文件对于非常小的项目来说是可行的,但当传递依赖逐渐出现时,这会很快变得更加困难。

正如第17章所提到的,在内部,sbt使用Coursier进行类库管理。因此,sbt可以让你在Scala项目中轻松使用多年来创建的大量Java库。

libraryDependencies语法

build.sbt 文件中添加托管的依赖有两种常规形式。在第一种形式中,你指定 groupIDartifactIDrevision

    libraryDependencies += groupID % artifactID % revision

在第二个形式中,你添加了一个可选的 configuration 参数:

    libraryDependencies += groupID % artifactID % revision % configuration

groupIDartifactIDrevision 字符串对应于Coursier和Apache Ivy这样的工具来检索所需的库:

  • groupID 通常是一个Java/Scala风格的包名,比如 “org.typelevel”。

  • artifactID 说明你想要的具体产品,例如 “cats-core”。

  • revision 声明你想要的工件(模块)的版本(Version)/修订版(Revision),如 “2.6.0”。

通常情况下,模块的开发者会在其文档中说明这些信息。例如,在写这篇文章的时候,MUnit的GitHub页面( https://oreil.ly/6KQv1 )上有一个 maven-central 的链接,点击后会进入一个 index.scala-lang.org 页面( https://oreil.ly/Gmwjf )。该页面上的版本矩阵显示,当使用Scala 3.0.0时,最新的MUnit版本是0.7.25,它为你提供了这个 libraryDependencies 字符串:

    libraryDependencies += "org.scalameta" %% "munit" % "0.7.25"

为了显示该字符串与该工件的Maven信息之间的关系,你也可以使用同一个网页,点击Maven标签,就能看到这些信息:

    <dependency>
        <groupId>org.scalameta</groupId>
        <artifactId>munit_3.0.0</artifactId>
        <version>0.7.25</version>
    </dependency>

这显示了sbt配置字符串与Maven XML文件中的参数名称之间的对应关系。

用于构建libraryDependencies的方法

build.sbt 中使用的符号 +=%%% 是 sbt DSL 的一部分。它们在表17-3中有描述。

表17-3. 用于构建libraryDependencies字符串的方法

方法
描述

+=

附加到键的值上。build.sbt 文件可以处理定义为键/值对的设置。上述所示的例子中,libraryDependencies 是一个键,并且它显示了几个不同的值。

%

一个用于从你提供的字符串中构建Apache Ivy“Module ID”的方法。

%%

当在 groupID 之后使用时,它会自动将你项目的Scala版本添加到工件名称的最后。

%%%

在使用Scala.js和Scala Native的工件时,请用这个代替%%。

你可以在 groupID 后面指定%,%%,或者%%%。这个Scala 2.13的例子展示了%方法:

    libraryDependencies += "org.scalatest" % "scalatest_2.13" % "3.0.5" % "test"

我总是用 scalaVersion 参数来声明我在项目中使用的Scala版本 —— 2.13,所以在这里重新声明它可能会出错。因此,几乎总是使用%%的方法:

    libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test"

当你的Scala版本是2.13.x时,这两个例子是等价的。%%方法将你项目的Scala主版本号添加到工件名称的末尾。将Scala主版本号添加到 artifactID 中是必须的,因为模块可能是为不同的Scala版本编译的。

Scala.js和Scala Native的依赖

当指定专门为Scala.js和Scala Native打包的依赖时,在 groupIDartifactID 字段之间使用三个百分点的符号,如下所示:

    libraryDependencies += "org.querki" %%% "jquery-facade" % "1.2"
                                        ---

正如前面 “org.scalatest”的例子所示,你可以用%%来包含为Scala编译的库,但是当一个库为Scala.js或Scala Native编译时,就用%%%来代替。如“Simple Command Line Tools with Scala Native” ( https://oreil.ly/T2yb4 )中所解释的那样,正如两个百分点的符号告诉sbt使用正确的依赖版本,三个百分点的符号告诉sbt使用正确的目标环境,目前是Scala.js或者Scala Native。

configuration字段

请注意,在一些例子中,在版本号的后面添加了值 Test 或字符串 "test"

    libraryDependencies += "com.typesafe.akka" %% "akka-testkit" % "2.5.19" % Test
    libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test"

这些例子指定了添加依赖的configuration字段:

    libraryDependencies += groupID % artifactID % revision % configuration

这个configuration参数通常用于测试依赖,如上所示,你用 "test"Test 来声明它们。像这样声明的依赖只会被添加到用于sbt的 测试 配置的classpath中。这对于添加ScalaTest、MUnit和Mockito等依赖很有用,这些依赖在测试应用程序时使用,但在编译和运行应用程序时不使用。

依赖在哪里?

你可能想知道项目的JAR文件在下载后的位置。根据Coursier缓存文档( https://oreil.ly/Ha0zn ),它的缓存位置与平台有关,它下载的文件都保存在这些位置:

  • 在macOS上: ~/Library/Caches/Coursier

  • 在Linux上, ~/.cache/coursier/v1 (这也适用于基于Linux的CI环境。FreeBSD也是如此)

  • 在Windows上: %LOCALAPPDATA%\Coursier\Cache\v1,对于用户Alex来说,这通常对应于 C:\Users\Alex\AppData\Local\Coursier\Cache\v1

比如当使用macOS系统,并且使用Scala 2.13在build.sbt文件中有如下依赖:

    libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.3" % "test"

那么该JAR文件会在 /Users/al/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.3/scala-library-2.13.3.jar 目录中找到。

在好奇的时候使用Debug模式 -- TODO 耗子栏

当你对sbt shell内部的工作情况感到好奇时,可以在运行其他命令之前发出它的 debug 命令:

    sbt> debug

现在,当你像这样运行sbt命令时,你会看到几十行的行的输出,也许是几百行:

    sbt> clean; compile

在你看到想看的东西之后,你可以切换回其他日志模式,如 errorwarninginfo

储存库

sbt默认使用标准的Maven中央仓库,所以当你在 build.sbt 文件中添加 libraryDependencies 一行时,它可以找到大多数库。在这种情况下,你不需要告诉sbt去哪里找文件。然而,当一个库 不在 标准仓库中时,你可以告诉sbt如何在互联网上找到这个文件。这个过程被称为添加一个 解析器(resolver) ,你可以在sbt解析器文档( https://oreil.ly/zcUlK )中了解更多的信息。

17.8 控制使用哪个版本的托管依赖

问题

在Scala/sbt项目中,你希望确保项目总是拥有托管依赖的所需版本,包括最新的集成版本、里程碑版本,或其他版本。

解决方案

libraryDependencies 设置中的 revision 字段并不局限于指定一个固定的版本。根据Apache Ivy的依赖文档( https://oreil.ly/zYPGE ),你可以指定字符串,如 latest.integrationlatest.milestone,以及其他项。

一个作为这种灵活性的例子,与其像这样指定一个 foobar 模块的1.8版本:

    libraryDependencies += "org.foobar" %% "foobar" % "1.8"

不如像这样请求 latest.integration 版本:

    libraryDependencies += "org.foobar" %% "foobar" % "latest.integration"

根据你的期望,模块开发者的文档通常会告诉你有哪些版本可以使用,以及应该使用哪些版本。你也可以在Maven中央仓库找到模块的所有版本,比如MvnRepository的所有ScalaTest版本( https:// oreil.ly/gF2z0 )。

Revision字段选项

一旦你知道了想要的版本,你就可以指定标签(tags)来控制将被下载和使用的模块的版本。尽管从1.3版本开始,sbt现在使用Coursier来获取工件(而不是Ivy),但sbt仍然支持Ivy语法,而且Ivy依赖文档指出,可以使用以下标签:

  • latest.integration

  • latest.[any status],例如 latest.milestone

  • 你可以用一个+字符来结束修订版。这将选择依赖模块的最新子修订版。例如,如果依赖模块存1.0.3、1.0.7和1.1.2三个版本,指定 1.0.+ 将导致1.0.7被选中并应用。

  • 你可以使用版本范围,如下面的例子所示:

    • —— [1.0,2.0] 匹配所有大于或等于1.0和小于或等于2.0的版本

    • —— [1.0,2.0[ 匹配所有大于或等于1.0且小于2.0的版本

    • —— ]1.0,2.0] 匹配所有大于1.0且小于或等于2.0的版本

    • —— ]1.0,2.0[ 匹配所有大于1.0且小于2.0的版本

    • —— [1.0,)匹配所有大于或等于1.0的版本

    • —— 1.0,)匹配所有大于1.0的版本

    • —— (,2.0] 匹配所有低于或等于2.0的版本

    • —— (,2.0[ 匹配所有低于2.0的版本

这些配置的例子是由Apache Ivy依赖文档( https://oreil.ly/RSrqC )提供的。

讨论

为了演示这些修订版字段的几个选项,本例展示了与ScalaTest一起使用的 latest.integration 标签:

    libraryDependencies += "org.scalatest" %% "scalatest" % "latest.integration" ↵
                           % "test"

在写这篇文章的时候,该配置检索了15个ScalaTest JAR文件,其名称是这样的:

    scalatest_2.13-3.3.0-SNAP3.jar
    scalatest-core_2.13-3.3.0-SNAP3.jar
    scalatest-funspec_2.13-3.3.0-SNAP3.jar

你可以在 build.sbt 文件中放入该配置行,如果你在sbt交互式模式下运行 reload,然后运行 updatecompile 等命令,就可以看到该输出。

作为第二个例子,像这样使用 + 标签:

    libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test"

当我在添加该配置行后运行 update 命令时,我看到它检索到了这些JAR文件:

    scalactic_2.13-3.0.9.jar
    scalatest_2.13-3.0.9.jar

另见

关于管理sbt依赖版本的更多信息,请参见 scala-sbt.org 的这些页面:

  • 类库依赖 ( https://oreil.ly/t5wkB

  • 类库管理 ( https://oreil.ly/gfFTe

17.9 生成项目的API文档

问题

在一个sbt项目中,你已经用Scaladoc注释标记Scala源代码,并想为项目生成API文档。

解决方案

根据你的文档需求,使用表17-4中列出的任何一条sbt命令。

表17-4. 与项目文档相关的sbt命令

sbt命令
说明

doc

从位于 src/main/scala 的Scala源代码文件中创建Scaladoc API文档

Test / doc

从位于 src/test/scala 的Scala源代码文件中创建Scaladoc API文档

packageDoc

创建一个JAR文件,其中包含从位于 src/main/scala 的Scala源代码文件中创建的API文档

Test / packageDoc

创建一个JAR文件,其中包含从位于 src/test/scala 的Scala源代码文件中创建的API文档

publish

将工件发布到由 publish-to setting定义的存储库

publishLocal

将工件发布到本地Ivy资源库

例如,要生成API文档,使用 doc 命令:

    $ sbt doc

当你运行这个命令时,它的输出被写在 target/scala-3.0.0/api 目录下,其中 3.0.0 部分代表你在项目中使用的Scala版本,在这里是Scala 3.0.0。同样地,Test/doc 的输出被写在 target/scala-3.0.0/test-api/ 子目录下,而 publishLocal 的输出被写在你系统中的 $HOME/.ivy2/local/ProjectName 目录下,其中 ProjectName 是你sbt项目的名字。其他命令也显示了它们的输出被写在哪里。

讨论

有些sbt命令不显示它们产生的输出文件,所以如果你想看到这些文件名,可以在 docpackage 等命令前面加上 show,像这样:

    sbt> show doc 
    target/scala-3.0.0/api

    sbt> show Test/doc
    [info] target/scala-3.0.0/test-api

    sbt> show package
    target/scala-3.0.0/stringutils_3.0.0-1.0.jar

这些命令照常运行,完成后会显示其输出文件。

另见

  • sbt命令行reference( https://oreil.ly/W6wGH )有关于这些命令的更多信息。

  • Scaladoc标签( @see、@param 等)列在这个针对类库作者的Scaladoc页面上( https://oreil.ly/E7tsc )。

  • 参阅17.12小节,了解使用 publishpublishLocal 的例子。

17.10 指定一个使用sbt运行的主类

问题

你在一个Scala/sbt项目中有多个main方法,你想指定(a)当你输入 sbt run 时应该运行哪个main方法,或者(b)当你的项目被打包成JAR文件时应该调用的main方法。

解决方案

它有一些稍微不同的解决方案,这取决于你是希望使用 sbt run 运行main方法,还是将应用程序打包为JAR文件。

为sbt run指定一个main方法

如果你的项目中有多个main方法,并且想在输入 sbt run 时指定运行哪个main方法,可以在 build.sbt 文件中添加这样一行:

    // set the main class for the 'sbt run' task
    Compile / run / mainClass := Some("com.alvinalexander.Main1")

在这个例子中,Main1com.alvinalexander 包中的一个 @main 方法。

另一个选择是使用sbt的 run-main 命令来指定要运行的类。以下是它在操作系统命令行中的工作方式:

    $ sbt "runMain com.alvinalexander.Main1" 
    [info] Running com.alvinalexander.Main1 
    hello
    [success] Total time: 1 s

这是它在sbt shell内的工作方式:

    $ sbt

    sbt> runMain com.alvinalexander.Main1 
    [info] Running com.alvinalexander.Main1 
    hello
    [success] Total time: 1 s

为打包的JAR文件指定一个main方法

要指定应用程序打包成JAR文件时将被添加到manifest中的类,请在 build.sbt 文件中添加这样一行:

    Compile / packageBin / mainClass := Some("com.alvinalexander.Main2")

现在,当你使用 packageshow package 的命令为你的应用程序创建一个JAR文件时:

    sbt> show package
    [info] target/scala-3.0.0/myapp_3.0.0-1.0.jar

创建的JAR文件中的 META-INF/MANIFEST.MF 文件包含了你指定的main类:

    Main-Class: com.alvinalexander.Main2

讨论

如果你的应用程序中只有一个 @main 方法,sbt会自动运行该方法。在这种情况下,这些配置行是没有必要的。

如果你的项目中有多个 @main 方法,并且没有使用解决方案中显示的任何方法,当你执行 sbt run 时,sbt会提示你它发现的 @main 方法的列表:

    Multiple main classes detected, select one to run:

    [1] com.alvinalexander.myproject.Foo
    [2] com.alvinalexander.myproject.Bar

下面的代码显示了具有两个 mainClass 设置的 build.sbt 文件是什么样子:

    name := "MyProject"
    version := "0.1"
    scalaVersion := "3.0.0"

    // set the main class for the 'sbt run' task
    Compile / run / mainClass := Some("com.alvinalexander.myproject.Foo")

    // set the main class for the 'sbt package' task
    Compile / packageBin / mainClass := Some("com.alvinalexander.myproject.Foo")

这些 mainClass 设置是针对包 com.alvinalexander.myproject 中一个名为 Foo@main 方法。

另见

关于 @main 方法和对象中的 main 方法的更多细节,请参阅1.4小节,“使用scalac编译,使用scala运行”。

17.11 部署单个可执行的JAR文件

问题

你正在用sbt构建一个有多个依赖的Scala应用程序,并想部署一个可执行的JAR文件。

解决方案

sbt package 命令创建了一个JAR文件,其中包括它从你的源代码中编译的类文件,以及你项目中 src/main/resources 中的资源,但它并不包括项目中的依赖或Scala发行版中的库,这些库是用java命令执行JAR文件所需要的。因此,使用sbt-assembly插件( https://oreil.ly/3LVap )来创建一个包含 所有 这些资源的单一JAR文件。

sbt-assembly

sbt-assembly的安装说明可能会改变,但在写这篇文章时,你所要做的就是在项目根目录下创建一个包含这下面一行的 project/assembly.sbt 文件:

    addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

之后,只要重新加载你的sbt项目,sbt-assembly就可以使用了。

现在 —— 假设你的项目只有一个 @main 方法 —— 运行sbt assembly 命令来创建一个可执行的JAR文件:

    sbt> assembly

要在运行 assembly 时看到输出JAR文件的位置,请在该命令前加上show:

    sbt> show assembly
    [info] compiling 1 Scala source to target/scala-3.0.0/classes ... 
    [info] target/scala-3.0.0/MyApp.jar

作为一个更具体的例子,我用Scala和JavaFX编写了一个“Knowledge Browser”,当我用sbt-assembly构建它时,我可以看到它所包含的所有依赖关系,以及最终JAR文件的位置:

    [info] Including from cache: rome-1.8.1.jar
    [info] Including from cache: scala-xml_2.12-1.0.6.jar
    // more ooutput here ...
    [info] Including from cache: jsoup-1.11.2.jar
    [info] Including from cache: jfxrt.jar
    [info] Packaging target/scala-2.12/Knol-assembly-1.0.jar ...
    [info] Done packaging.

sbt-assembly插件的工作原理是将源代码中的类文件、依赖关系和Scala库复制到一个单一的可以用java解释器执行的JAR文件中。

讨论

你可以通过多种方式定制sbt-assembly的工作方式。首先,如果你的项目有多个 @main 方法,在 build.sbt 文件中添加这个设置,为你组装的JAR文件指定 @main 方法:

    assembly / mainClass := Some("com.alvinalexander.myproject.Foo")

该设置是针对包 com.alvinalexander.myproject 中一个名为 Foo@main 方法。

要设置生成的JAR文件的名称,可以使用这个设置:

    assembly / assemblyJarName := "MyApp.jar"

要在装配过程中跳过运行测试,请使用此设置:

    assembly / test := {}

当你运行 show assembly 命令时,你可以验证 assemblyJarName 设置是否有效。 为了验证 mainClass 的设置,cd 进入JAR文件所在的目录,从该JAR文件中提取 META-INF/MANIFEST.MF 文件,然后检查其内容:

    $ cd target/scala-3.0.0

    $ jar xvf MyApp.jar META-INF/MANIFEST.MF
    inflated: META-INF/MANIFEST.MF

    $ cat META-INF/MANIFEST.MF 
    Manifest-Version: 1.0
    Main-Class: com.alvinalexander.myproject.Foo 
    // more output ...

如上示例所示,该文件中的 Main-Class 设置与 build.sbt 文件中的 mainClass 设置一致。

另见

  • sbt-assembly项目( https://oreil.ly/3LVap )。

  • 目前,最容易看到最新的assembly版本和其他信息的是 index.scala-lang.org 页面( https://oreil.ly/11fcj )。

17.12 发布你的类库

问题

你用sbt创建了一个Scala项目或类库,想与其他用户分享,并为此创建了Maven/Ivy仓库所需的所有文件。

解决方案

这个方案只有几个步骤:

  1. 定义你的存储库信息。

  2. sbt publishsbt publishLocal 发布你的项目。

对于我的StringUtils库( https://oreil.ly/nDNzy ),我创建了一个 build.sbt 文件,其中包含常规的 project nameversionscalaVersion 设置。然后我添加了一个 publishTo 配置行,用于配置 publish 任务应该发送其输出的位置:

    lazy val root = (project in file("."))
        .settings(
            name := "StringUtils",
            version := "1.0",
            scalaVersion := "3.0.0"
        )

    // 为 "publish "任务设置目标;告诉sbt将其输出写到 "out "子目录
    publishTo := Some(Resolver.file("file", new File("./out")))            

现在,当我运行 sbt publish 时,我看到sbt输出看起来像这样:

    sbt> publish
        [info] published sutils_3.0.0 to
               out/sutils/sutils_3.0.0/1.0.part/sutils_3.0.0-1.0.pom
        [info] published sutils_3.0.0 to
               out/sutils/sutils_3.0.0/1.0.part/sutils_3.0.0-1.0.jar
        [info] published sutils_3.0.0 to
               out/sutils/sutils_3.0.0/1.0.part/sutils_3.0.0-1.0-sources.jar
        [info] published sutils_3.0.0 to
               out/sutils/sutils_3.0.0/1.0.part/sutils_3.0.0-1.0-javadoc.jar

在该输出中,我把实际的 out/stringutils 输出目录名改为 out/sutils ,以便适配显示端宽度的限制(也就是说,实际的目录名是 out/stringutils )。

接下来,在没有做任何事情来定义本地Ivy仓库的情况下,运行 publishLocal 任务时得到以下结果:

    sbt> publishLocal
    [info] Main Scala API documentation successful.
    [info] :: delivering :: sutils#sutils_3.0.0;1.0 :: 1.0 ... 
    [info] delivering ivy file to target/scala-3.0.0/ivy-1.0.xml 
    [info] published sutils_3.0.0 to
            ~/.ivy2/local/sutils/sutils_3.0.0/1.0/poms/sutils_3.0.0.pom
    [info]  published sutils_3.0.0 to
            ~/.ivy2/local/sutils/sutils_3.0.0/1.0/jars/sutils_3.0.0.jar
    [info]  published sutils_3.0.0 to
            ~/.ivy2/local/sutils/sutils_3.0.0/1.0/srcs/sutils_3.0.0-sources.jar
    [info]  published sutils_3.0.0 to
            ~/.ivy2/local/sutils/sutils_3.0.0/1.0/out/sutils_3.0.0-javadoc.jar
    [info]  published ivy to ~/.ivy2/local/sutils/sutils_3.0.0/1.0/ivys/ivy.xml

同样,在那个输出中,我把类库的名字从 stringutils 改成了 sutils,这样输出就不会被截断了。

讨论

sbt发布文档( https://oreil.ly/76jgk )说,发布“包括将描述符(如Ivy文件或Maven POM文件)和工件(如jar或war)上传到仓库,以便其他项目可以依赖并使用你的项目”。

  • publish 动作用于将你的项目发布到一个远程仓库。要使用发布,你需要指定要发布的版本库和要使用的凭证。一旦这些都设置好了,你就可以运行 publish

  • publishLocal 动作用于将你的项目发布到本地的Ivy仓库。然后你可以从同一台机器上的其他项目中使用这个项目。

在我的例子中,我使用 publish 来发布我的项目到一个本地目录。sbt publishing页面( https://oreil.ly/76jgk )讨论了将你的工件发布到远程仓库的必要步骤。

另见

  • sbt工件页面( https://oreil.ly/DSXoH )描述了如何控制在构建过程中应该创建哪些工件。

  • sbt交叉构建页面( https://oreil.ly/xLW7v )展示了如何“针对多个版本的Scala构建和发布你的项目”。

最后更新于

这有帮助吗?