【Scala系列】你还在用Gradle? 听说Scala开发者都在用sbt

1,676 阅读4分钟

What is sbt?


A build tool for Scala (and Java)

  • Mark Harrah创造sbt的时候, 它被称为"Simple Build Tool"
  • 但它第一次公开宣布的时候,名字sbt不代表任何东西,它只是 "sbt",它应该这样写[1]

Other options of Scala build tool

  • Java build tools, like ant, maven, gradle
  • Mill
  • Fury
  • seed
  • CBT
  • ...

Why sbt?


The most used scala build tool

  • Scala官方推荐
  • 93.6% Scala开发者的选择[2]
  • 使用Scala作为DSL定义build文件
  • 市面上几乎所有的Scala指南都是基于sbt
  • 可以混合构建Java和Scala项目
  • 增量编译(优秀的增量编译设计)
  • 通过触发执行(trigger execution)特性支持持续的编译与测试(file changes watch)
  • 支持并行任务执行
  • 支持大量的插件
  • 可以重用Maven或者ivy的repository进行依赖管理

The sbt project structure

  ├── build.sbt              ------ build definition file
  ├── project                ------ build support files
  │   ├── build.properties
  │   ├── plugins.sbt
  │   └── project
  ├── src
  │   ├── main
  │   │   ├── resources
  │   │   └── scala
  │   └── test
  │       ├── resources
  │       └── scala
  └── target                  ------ build products
  

Build Definition


Settings Expression


Introduction

{key} := {body}

配置表达式包含如下三部分:

  • 操作符左侧被称为 key
  • 操作符用 := 表示
  • 操作符右侧被称为 body

Keys

  • sbt 预置的keys都被定义在 sbt.Keys 里, 预置的Keys会被全部导入到 sbt.Keys 3

  • 可以通过如下方式自定义keys

    lazy val customKey = settingKey[String]("The custom key")
    

Adding project description

name := "scala-demo-project"
organization := "com.shuailli.demo"
version := "0.1.0-SNAPSHOT"
scalaVersion := "2.13.5"

Adding library dependencies

// define libs version
val http4sVersion = "0.21.29"
val specs2Version = "4.11.0"
libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
  "org.http4s" %% "http4s-blaze-client" % http4sVersion,
  "org.http4s" %% "http4s-circe" % http4sVersion,
  "org.specs2" %% "specs2-core" % specs2Version % "test",
)

Tips

  • Scala主次版本的特性差异较大,因此很多lib都会被编译给多个Scala版本 可见示例

  • 通过 %% 方法获取相应Scala版本的lib,确保lib对项目是二进制兼容的

    如果你用的是 groupID %% artifactID % revision 而不是 groupID % artifactID % revision(区别在于 groupID 后面是 %%),sbt 会在 lib名称中加上项目的 Scala 版本号。 这只是一种快捷方法。你可以这样写不用 %%:

libraryDependencies += "org.scala-tools" % "scala-stm_2.11" % "0.3"

假设这个构建的 scalaVersion 是 2.11.1,下面这种方式是等效的(注意 "org.scala-tools" 后面是 %%):

libraryDependencies += "org.scala-tools" %% "scala-stm" % "0.3"

Compiler Options

scalacOptions ++= Seq(
  "-encoding", "utf8", // Option and arguments on same line
  "-Xfatal-warnings",  // New lines for each options
  "-deprecation",
  "-unchecked",
  "-language:implicitConversions",
  "-language:higherKinds",
  "-language:existentials",
  "-language:postfixOps"
)
  • 官方完整的compiler options可见: 链接

  • 一份推荐的compiler options配置可见: 链接


Command Alias

addCommandAlias("checkAndTest", ";clean;scalafmtCheck;coverage;test;coverageReport;coverageOff")

Adding plugins

// define lib version
val kindProjectorVersion = "0.13.0"

addCompilerPlugin("org.typelevel" %% "kind-projector" % kindProjectorVersion cross CrossVersion.full)

Build Support Files

目录 project 下任何的文件都可以被用在构建过程中.


Specifying the sbt version

  • 使用 project/build.properties 文件指定 sbt 版本
sbt.version = 1.4.7
  • 如果指定的 sbt 版本在本地不存在, sbt launcher 将会在构建开始前自动为你下载.
  • 这个特性会保证本地预装了不同版本 sbt 的开发者对同一项目的构建生成一致的构建产物, 因为它将最终使用指定版本的sbt

Organize plugins

  • 使用 project/plugins.sbt 文件管理 sbt 插件
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")

How to run sbt?


Installation

  • 安装 JDK (推荐安装JDK8或者JDK11)

  • 安装 sbt

// In MacOS 
$ brew install sbt
  • 现在你可以开始Scala项目的开发

  • Tips:

    • Scala不同于其他语言,Scala主次版本之间的特性差异较大,因此通常都是为每个项目安装特定版本(build.sbt)的Scala,而不是全局安装,_sbt_就可以用来管理每个Scala项目特定的Scala版本.

    • 安装 sbt 后,默认情况下, sbt 会使用和自身版本相应版本的Scala来构建项目


sbt Shell and common commands

  • 进入 sbt 交互模式
$ sbt
  • 常用命令:
> new
> clean
> compile
> test
> testOnly
> package
> console
> run

Batch mode

你也可以用批处理模式来运行 sbt,可以以空格为分隔符指定参数。对于接受参数的 sbt 命令,将命令和参数用引号引起来一起传给 sbt。例如:

$ sbt clean compile "testOnly TestA TestB"

在这个例子中,testOnly 有两个参数 TestA 和 TestB。这个命令会按顺序执行(clean, compile, 然后 testOnly).

批处理模式下,构建将运行的非常慢.


Continuous Build and Test

为了加快编辑-编译-测试循环,你可以让 sbt 在你保存源文件时自动重新编译或者跑测试。 在命令前面加上前缀 ~ 后,每当有一个或多个源文件发生变化时就会自动运行该命令.

  • 交互模式下尝试
> ~ compile
> ~ run
> ~ testQuick

交互模式下按回车键停止监视变化,当然了交互模式或者批处理模式下均可使用 ~ 前缀.

  • 批处理模式下尝试
$ sbt '~ run'

为了区分类Unix系统下和 sbt 下的 ~ 前缀(Unix系统下代表当前用户根目录),在批处理模式下需要使用 ' 将sbt的命令包起来


Common plugins


Formatter

  • project/plugins.sbt 文件添加插件
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
  • 项目根目录下添加配置文件 .scalafmt.conf
  • 示例一个最简单的配置, 官方配置说明请见: 链接
version = 2.7.5

maxColumn = 180

align = more
danglingParentheses = false
continuationIndent.defnSite = 2
  • 指定如下命令执行formatter
$ sbt scalafmt
// or
$ sbt scalafmtCheck

Package & Release

  • project/plugins.sbt 文件添加打包插件(产出胖JAR包-一个包含代码和库中所有类文件的JAR文件)
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.0.0")
  • 执行如下命令生成 JAR 包
$ sbt package
  • project/plugins.sbt 文件添加打包插件(产出部署镜像)
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.5")
  • 执行如下命令为不同系统构建部署镜像
$ sbt debian:packageBin   # deb package
$ sbt windows:packageBin  # msi package
$ sbt docker:publishLocal # docker image

Test & Coverage

  • project/plugins.sbt 文件添加插件
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1")
  • build.sbt 文件添加测试覆盖率配置
coverageEnabled in Test := true
coverageMinimum := 90
coverageFailOnMinimum := true
coverageExcludedPackages := coverageExcludedClasses.mkString(";")
  • 执行如下命令均可调用覆盖率检查
$ sbt coverage
// or 
$ sbt test
// or
$ sbt coverageReport

Scopes

之前提到了 key , 实际上可以在不同的上下文中为同一个 key 赋予不同的值,每个上下文称之为 scope

例如:如果在你的构建定义中有多个项目,在每个项目中的 version 可以是不同的值.


There are three scope axes

Scope 轴是一种类型,该类型的每个实例都能定义自己的 scope(也就是说,每个实例的key可以有自己特定的值).

  • Projects
  • Configurations
  • Tasks

Scoping by Project axis

如果你将多个项目放在同一个构建中,每个项目需要有属于自己的 settings。也就是说,keys 可以根据项目被局限在不同的上下文中.

当一个项目没有定义项目层级的 settings 的时候,构建层级的 settings 通常作为默认的设置.

  • 如下示例,利用 project 轴设置构建层级的 settings
ThisBuild / scalaVersion     := "2.13.5"
ThisBuild / version          := "0.1.0-SNAPSHOT"
ThisBuild / organization     := "com.shuailli.demo"

ThisBuild 代表的就是构建层级


Scoping by Configuration axis

一个 configuration 定义一种特定的构建,可能包含它自己的 classpath,源文件和生成的包等.

在 sbt 中你可以看到这些 configurations:

  • Compile 定义主构建 (src/main/scala).
  • Test 定义如何构建测试 (src/test/scala).
  • Runtimerun task 定义 classpath.
  • Providecompiling tasktest task 定义 classpath.

默认情况下,和编译、打包、运行相关的所有 key 都局限于一个 configuration,因此在不同的 configuration 中可能产生不同的效果。最明显的例子就是 task key:compile,package 和 run; 然而能够 影响 到这些 key 的所有其他 key(例如 sourceDirectories,scalacOptions 和 fullClasspath)也都局限于该 configuration.


Scoping by Task axis

Settings 可以影响一个 task 如何工作.

为了支持这种特性,一个 task key(例如 packageSrc)可以作为另一个 key(例如 packageOptions)的 scope.

一些和打包相关的 taskpackageSrc, packageBin, packageDoc)可以共享和打包相关的 key,例如 artifactNamepackageOptions。这些 key 对于每个打包的 task 可以有唯一的值。

packageSrc / packageOptions := ???
packageBin / packageOptions := ???
packageDoc / packageOptions := ???

Referrences