sbt ハンドブック (日本語版草稿)

これは未だリリースされていない sbt 2.x のドキュメンテーションの草稿だ。一般的な概念は sbt 1.x とも一貫しているが、2.x 系および本稿の詳細は今後変更される可能性がある。

sbt logo

sbt は主に Scala と Java のためのシンプルなビルド・ツールだ。sbt は、Coursier を用いたライブラリ依存性のダウンロード、プロジェクトの差分コンパイルや差分テスト、IntelliJ や VS Code などの IDE との統合、JAR パッケージの作成、および JVM コミュニティーがパッケージ管理に用いる Central Repo への公開などを行う。

scalaVersion := "3.7.3"

Scala を始めるには、一行の build.sbt を書くだけでいい。

リンク

sbt runner のインストール

sbt プロジェクトをビルドするためには、以下の手順をたどる必要がある:

  • JDK をインストールする (Eclipse Adoptium Temurin JDK 8、11、17、もしくは ARM チップの macOS の場合、Zulu JDK を推奨)。
  • sbt runner のインストール。

sbt runner は、宣言されたバージョンの sbt を必要に応じてダウンロードして、実行するスクリプトだ。この機構によってユーザのマシン環境に依存することなく、ビルド作者が sbt のバージョンを正確に管理することができる。

システム要件

sbt は主なオペレーティング・システムにおいて動作するが、事前に JDK 8 以上がインストールされていることを必要とする。

java -version
# openjdk version "1.8.0_352"

SDKMAN からのインストール

JDK と sbt のインストールをするのに、SDKMAN を使うことができる。

sdk install java $(sdk list java | grep -o "\b8\.[0-9]*\.[0-9]*\-tem" | head -1)
sdk install sbt

ユニバーサル・パッケージ

sbt runner の確認

sbt --script-version
# 1.11.7

例題でみる sbt

このページは、 sbt runner をインストールしたことを前提とする。

sbt の内部がどうなっているかや理由みたいなことを解説する代わりに、例題を次々と見ていこう。

最小 sbt ビルドを作る

mkdir foo-build
cd foo-build
touch build.sbt
mkdir project
echo "sbt.version=2.0.0-RC6" > project/build.properties

sbt シェルを起ち上げる

$ sbt
[info] welcome to sbt 2.0.0-RC6 (Azul Systems, Inc. Java)
....
[info] started sbt server
sbt:foo-build>

sbt シェルを終了させる

sbt シェルを終了させるには、exit と入力するか、Ctrl+D (Unix) か Ctrl+Z (Windows) を押す。

sbt:foo-build> exit

プロジェクトをコンパイルする

表記の慣例として sbt:...>> というプロンプトは、sbt シェルに入っていることを意味することにする。

$ sbt
sbt:foo-build> compile
[success] elapsed time: 0 s, cache 0%, 1 onsite task

コード変更時に再コンパイルする

compile コマンド (やその他のコマンド) を ~ で始めると、プロジェクト内のソース・ファイルが変更されるたびにそのコマンドが自動的に再実行される。

sbt:foo-build> ~compile
[success] elapsed time: 0 s, cache 100%, 1 disk cache hit
[info] 1. Monitoring source files for foo-build/compile...
[info]    Press <enter> to interrupt or '?' for more options.

ソース・ファイルを書く

上記のコマンドは走らせたままにする。別のシェルかファイルマネージャーからプロジェクトのディレクトリへ行って、src/main/scala/example というディレクトリを作る。次に好きなエディタを使って example ディレクトリ内に以下のファイルを作成する:

package example

@main def main(args: String*): Unit =
  println(s"Hello ${args.mkString}")

この新しいファイルは実行中のコマンドが自動的に検知したはずだ:

[info] Build triggered by /tmp/foo-build/src/main/scala/example/Hello.scala. Running 'compile'.
[info] compiling 1 Scala source to /tmp/foo-build/target/out/jvm/scala-3.3.3/foo/backend ...
[success] elapsed time: 1 s, cache 0%, 1 onsite task
[info] 2. Monitoring source files for foo-build/compile...
[info]    Press <enter> to interrupt or '?' for more options.

~compile を抜けるには Enter を押す。

以前のコマンドを実行する

sbt シェル内で上矢印キーを 2回押して、上で実行した compile コマンドを探す。

sbt:foo-build> compile

ヘルプを読む

help コマンドを使って、基礎コマンドの一覧を表示する。

sbt:foo-build> help

  <command> (; <command>)*                       Runs the provided semicolon-separated commands.
  about                                          Displays basic information about sbt and the build.
  tasks                                          Lists the tasks defined for the current project.
  settings                                       Lists the settings defined for the current project.
  reload                                         (Re)loads the current project or changes to plugins project or returns from it.
  new                                            Creates a new sbt build.
  new                                            Creates a new sbt build.
  projects                                       Lists the names of available projects or temporarily adds/removes extra builds to the session.

....

特定のタスクの説明を表示させる:

sbt:foo-build> help run
Runs a main class, passing along arguments provided on the command line.

アプリを実行する

sbt:foo-build> run
[info] running example.main
Hello
[success] elapsed time: 0 s, cache 50%, 1 disk cache hit, 1 onsite task

sbt シェルから scalaVersion を設定する

sbt:foo-build> set scalaVersion := "3.7.3"
[info] Defining scalaVersion
[info] The new value will be used by Compile / bspBuildTarget, Compile / dependencyTreeCrossProjectId and 51 others.
[info]  Run `last` for details.
[info] Reapplying settings...
[info] set current project to foo (in build file:/tmp/foo-build/)

scalaVersion セッティングを確認する:

sbt:foo-build> scalaVersion
[info] 3.7.3

セッションを build.sbt へと保存する

アドホックに設定したセッティングは session save で保存できる。

sbt:foo-build> session save
[info] Reapplying settings...
[info] set current project to foo-build (in build file:/tmp/foo-build/)
[warn] build source files have changed
[warn] modified files:
[warn]   /tmp/foo-build/build.sbt
[warn] Apply these changes by running `reload`.
[warn] Automatically reload the build when source changes are detected by setting `Global / onChangedBuildSource := ReloadOnSourceChanges`.
[warn] Disable this warning by setting `Global / onChangedBuildSource := IgnoreSourceChanges`.

build.sbt ファイルは以下のようになったはずだ:

scalaVersion := "3.7.3"

プロジェクトに名前を付ける

エディタを使って、build.sbt を以下のように変更する:

scalaVersion := "3.3.3"
organization := "com.example"
name := "Hello"

ビルドの再読み込み

reload コマンドを使ってビルドを再読み込みする。このコマンドは build.sbt を読み直して、そこに書かれたセッティングを再適用する。

sbt:foo-build> reload
[info] welcome to sbt 2.x (Azul Systems, Inc. Java)
[info] loading project definition from /tmp/foo-build/project
[info] loading settings for project hello from build.sbt ...
[info] set current project to Hello (in build file:/tmp/foo-build/)
sbt:Hello>

プロンプトが sbt:Hello> に変わったことに注目してほしい。

libraryDependencies に toolkit-test を追加する

エディタを使って、build.sbt を以下のように変更する:

scalaVersion := "3.3.3"
organization := "com.example"
name := "Hello"
libraryDependencies += "org.scala-lang" %% "toolkit-test" % "0.1.7" % Test

reload コマンドを使って、build.sbt の変更を反映させる。

sbt:Hello> reload

差分テストを実行する

sbt:Hello> test

差分テストを継続的に実行する

sbt:Hello> ~test

テストを書く

上のコマンドを走らせたままで、エディタから src/test/scala/example/HelloSuite.scala という名前のファイルを作成する:

package example

class HelloSuite extends munit.FunSuite:
  test("Hello should start with H") {
    assert("hello".startsWith("H"))
  }
end HelloSuite

~test が検知したはずだ:

example.HelloSuite:
==> X example.HelloSuite.Hello should start with H  0.012s munit.FailException: /tmp/foo-build/src/test/scala/example/HelloSuite.scala:5 assertion failed
4:  test("Hello should start with H") {
5:    assert("hello".startsWith("H"))
6:  }
    at munit.FunSuite.assert(FunSuite.scala:11)
    at example.HelloSuite.$init$$$anonfun$1(HelloSuite.scala:5)
[error] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error]   example.HelloSuite
[error] (Test / testQuick) sbt.TestsFailedException: Tests unsuccessful
[error] elapsed time: 1 s, cache 50%, 3 disk cache hits, 3 onsite tasks

テストが通るようにする

エディタを使って src/test/scala/example/HelloSuite.scala を以下のように変更する:

package example

class HelloSuite extends munit.FunSuite:
  test("Hello should start with H") {
    assert("Hello".startsWith("H"))
  }
end HelloSuite

テストが通過したことを確認して、Enter を押して継続的テストを抜ける。

ライブラリ依存性を追加する

エディタを使って、build.sbt を以下のように変更する:

scalaVersion := "3.3.3"
organization := "com.example"
name := "Hello"
libraryDependencies ++= Seq(
  "org.scala-lang" %% "toolkit" % "0.1.7",
  "org.scala-lang" %% "toolkit-test" % "0.1.7" % Test,
)

reload コマンドを使って、build.sbt の変更を反映させる。

Scala REPL を使う

New York の現在の天気を調べてみる。

sbt:Hello> console
Welcome to Scala 3.3.3 (1.8.0_402, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala>
import sttp.client4.quick.*
import sttp.client4.Response

val newYorkLatitude: Double = 40.7143
val newYorkLongitude: Double = -74.006
val response: Response[String] = quickRequest
  .get(
    uri"https://api.open-meteo.com/v1/forecast?latitude=\$newYorkLatitude&longitude=\$newYorkLongitude&current_weather=true"
  )
  .send()

println(ujson.read(response.body).render(indent = 2))

// press Ctrl+D

// Exiting paste mode, now interpreting.

{
  "latitude": 40.710335,
  "longitude": -73.99307,
  "generationtime_ms": 0.36704540252685547,
  "utc_offset_seconds": 0,
  "timezone": "GMT",
  "timezone_abbreviation": "GMT",
  "elevation": 51,
  "current_weather": {
    "temperature": 21.3,
    "windspeed": 16.7,
    "winddirection": 205,
    "weathercode": 3,
    "is_day": 1,
    "time": "2023-08-04T10:00"
  }
}
import sttp.client4.quick._
import sttp.client4.Response
val newYorkLatitude: Double = 40.7143
val newYorkLongitude: Double = -74.006
val response: sttp.client4.Response[String] = Response({"latitude":40.710335,"longitude":-73.99307,"generationtime_ms":0.36704540252685547,"utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation":"GMT","elevation":51.0,"current_weather":{"temperature":21.3,"windspeed":16.7,"winddirection":205.0,"weathercode":3,"is_day":1,"time":"2023-08-04T10:00"}},200,,List(:status: 200, content-encoding: deflate, content-type: application/json; charset=utf-8, date: Fri, 04 Aug 2023 10:09:11 GMT),List(),RequestMetadata(GET,https://api.open-meteo.com/v1/forecast?latitude=40.7143&longitude...

scala> :q // to quit

サブプロジェクトを作成する

build.sbt を以下のように変更する:

scalaVersion := "3.3.3"
organization := "com.example"

lazy val hello = project
  .in(file("."))
  .settings(
    name := "Hello",
    libraryDependencies ++= Seq(
      "org.scala-lang" %% "toolkit" % "0.1.7",
      "org.scala-lang" %% "toolkit-test" % "0.1.7" % Test
    )
  )

lazy val helloCore = project
  .in(file("core"))
  .settings(
    name := "Hello Core"
  )

reload コマンドを使って、build.sbt の変更を反映させる。

全てのサブプロジェクトを列挙する

sbt:Hello> projects
[info] In file:/tmp/foo-build/
[info]   * hello
[info]     helloCore

サブプロジェクトをコンパイルする

sbt:Hello> helloCore/compile

サブプロジェクトに toolkit-test を追加する

build.sbt を以下のように変更する:

scalaVersion := "3.3.3"
organization := "com.example"

val toolkitTest = "org.scala-lang" %% "toolkit-test" % "0.1.7"

lazy val hello = project
  .in(file("."))
  .settings(
    name := "Hello",
    libraryDependencies ++= Seq(
      "org.scala-lang" %% "toolkit" % "0.1.7",
      toolkitTest % Test
    )
  )

lazy val helloCore = project
  .in(file("core"))
  .settings(
    name := "Hello Core",
    libraryDependencies += toolkitTest % Test
  )

コマンドをブロードキャストする

hello に送ったコマンドを helloCore にもブロードキャストするために集約を設定する:

scalaVersion := "3.3.3"
organization := "com.example"

val toolkitTest = "org.scala-lang" %% "toolkit-test" % "0.1.7"

lazy val hello = project
  .in(file("."))
  .aggregate(helloCore)
  .settings(
    name := "Hello",
    libraryDependencies ++= Seq(
      "org.scala-lang" %% "toolkit" % "0.1.7",
      toolkitTest % Test
    )
  )

lazy val helloCore = project
  .in(file("core"))
  .settings(
    name := "Hello Core",
    libraryDependencies += toolkitTest % Test
  )

reload 後、~test は両方のサブプロジェクトに作用する:

sbt:Hello> ~test

Enter を押して継続的テストを抜ける。

hello が helloCore に依存するようにする

サブプロジェクト間の依存関係を定義するには .dependsOn(...) を使う。ついでに、toolkit への依存性も helloCore に移そう。

scalaVersion := "3.3.3"
organization := "com.example"

val toolkitTest = "org.scala-lang" %% "toolkit-test" % "0.1.7"

lazy val hello = project
  .in(file("."))
  .aggregate(helloCore)
  .dependsOn(helloCore)
  .settings(
    name := "Hello",
    libraryDependencies += toolkitTest % Test
  )

lazy val helloCore = project
  .in(file("core"))
  .settings(
    name := "Hello Core",
    libraryDependencies += "org.scala-lang" %% "toolkit" % "0.1.7",
    libraryDependencies += toolkitTest % Test
  )

uJson を使って JSON をパースする

helloCore に uJson を追加しよう。

core/src/main/scala/example/core/Weather.scala を追加する:

package example.core

import sttp.client4.quick._
import sttp.client4.Response

object Weather:
  def temp() =
    val response: Response[String] = quickRequest
      .get(
        uri"https://api.open-meteo.com/v1/forecast?latitude=40.7143&longitude=-74.006&current_weather=true"
      )
      .send()
    val json = ujson.read(response.body)
    json.obj("current_weather")("temperature").num
end Weather

次に src/main/scala/example/Hello.scala を以下のように変更する:

package example

import example.core.Weather

@main def main(args: String*): Unit =
  val temp = Weather.temp()
  println(s"Hello! The current temperature in New York is $temp C.")

アプリを走らせてみて、うまくいったか確認する:

sbt:Hello> run
[info] compiling 1 Scala source to /tmp/foo-build/core/target/scala-2.13/classes ...
[info] compiling 1 Scala source to /tmp/foo-build/target/scala-2.13/classes ...
[info] running example.Hello
Hello! The current temperature in New York is 22.7 C.

一時的に scalaVersion を変更する

sbt:Hello> ++3.3.3!
[info] Forcing Scala version to 3.3.3 on all projects.
[info] Reapplying settings...
[info] Set current project to Hello (in build file:/tmp/foo-build/)

scalaVersion セッティングを確認する:

sbt:Hello> scalaVersion
[info] helloCore / scalaVersion
[info]  3.3.3
[info] scalaVersion
[info]  3.3.3

このセッティングは reload 後には無効となる。

バッチ・モード

sbt のコマンドをターミナルから直接渡して sbt をバッチモードで実行することができる。

$ sbt clean "testOnly HelloSuite"

sbt new コマンド

sbt new コマンドを使って手早く簡単な Hello world ビルドをセットアップすることができる。

$ sbt new scala/scala-seed.g8
....
A minimal Scala project.

name [My Something Project]: hello

Template applied in ./hello

プロジェクト名を入力するプロンプトが出てきたら hello と入力する。

これで、hello ディレクトリ以下に新しいプロジェクトができた。

クレジット

本ページは William “Scala William” Narmontas さん作の Essential sbt というチュートリアルに基づいて書かれた。

sbt 入門

sbt は、柔軟かつ強力なビルド定義を持つが、それを裏付けるいくつかの概念がある。それらの概念は数こそ多くはないが、sbt は他のビルドシステムとは少し違うので、ドキュメントを読まずに使おうとすると、きっと細かい点でつまづいてしまうだろう。

この sbt 入門ガイドでは、sbt ビルド定義を作成してメンテナンスしていく上で知っておくべき概念を説明していく。

このガイドを一通り読んでおくことを強く推奨したい。

sbt の存在理由

背景

Scala 3 Book に書かれてある通り、Scala においてライブラリやプログラムは Scala コンパイラ scalac によってコンパイルされる:

@main def hello() = println("Hello, World!")
$ scalac hello.scala
$ scala hello
Hello, World!

いちいち毎回 scalac に全ての Scala ソースファイル名を直接渡すのは面倒だし遅い。

さらに、例題的なプログラム以外のものを書こうとすると普通はライブラリ依存性を持つことになり、つまり間接的なライブラリ依存を持つことにもなる。Scala エコシステムは、Scala 2.12系、2.13系、3.x系、JVMプラットフォーム、 JSプラットフォーム、 Native プラットフォームなどがあるため二重、三重に複雑な問題だ。

JAR ファイルや scalac を直接用いるという代わりに、サブプロジェクトという抽象概念とビルドツールを導入することで、無意味に非効率的な苦労を回避することができる。

sbt

sbt は主に Scala と Java のためのシンプルなビルド・ツールだ。sbt を使うことで、サブプロジェクトおよびそれらの依存性、カスタム・タスクを宣言することができ、高速かつ再現性のあるビルドを得ることができる。

この目標のため、sbt はいくつかのことを行う:

  • sbt 自身のバージョンは project/build.properties にて指定される。
  • build.sbt DSL という DSL (ドメイン特定言語) を定義し、build.sbt にて Scala バージョンその他のサブプロジェクトに関する情報を宣言できるようにする。
  • Cousier を用いてサブプロジェクトのライブラリ依存性や間接的依存性を取得する。
  • Zinc を呼び出して Scala や Java ソースの差分コンパイルを行う。
  • 可能な限り、タスクを並列実行する。
  • JVM エコシステム全般と相互乗り入れが可能なように、パッケージが Maven リポジトリにどのように公開されるべきかの慣習を定義する。

sbt は、プログラムやライブラリのビルドに必要なコマンド実装の大部分を標準化していると言っても過言ではない。

build.sbt DSL の必要性

sbt はサブプロジェクトとタスクグラフを宣言するのに Scala 言語をベースとする build.sbt DSL を採用する。昨今では、YAML や XML といった設定形式の代わりに DSL を使っていることは sbt に限らない。Gradle、Google 由来の Bazel 、Meta の Buck、Apple の SwiftPM など多くのビルドツールが DSL を用いてサブプロジェクトを定義する。

build.sbt は、scalaVersionlibraryDependencies のみを宣言すればあたかも YAML ファイルのように始めることができるが、ビルドシステムへの要求が高度になってもスケールすることができる。

  • ライブラリのためのバージョン番号など同じ情報の繰り返しを回避するため、build.sbtval を使って変数を宣言できる。
  • セッティングやタスクの定義内で必要に応じて if のような Scala 言語の言語構文を使うことができる。
  • セッティングやタスクが静的型付けされているため、ビルドが始まる前にタイポミスや型の間違いなどをキャッチできる。型は、タスク間でのデータの受け渡しにも役立つ。
  • Initialized[Task[A]] を用いた構造的並行性を提供する。この DSL はいわゆる「直接スタイル」な .value 構文を用いて簡潔にタスクグラフを定義することができる。
  • プラグインを通して sbt の機能を拡張して、カスタム・タスクや Scala.JS といった言語拡張を行うという強力な権限をコミュニティーに与えている。

新しいビルドの作成

新しいビルドを作成するには、sbt new を使う。

$ mkdir /tmp/foo
$ cd /tmp/foo
$ sbt new

Welcome to sbt new!
Here are some templates to get started:
 a) scala/toolkit.local               - Scala Toolkit (beta) by Scala Center and VirtusLab
 b) typelevel/toolkit.local           - Toolkit to start building Typelevel apps
 c) sbt/cross-platform.local          - A cross-JVM/JS/Native project
 d) scala/scala3.g8                   - Scala 3 seed template
 e) scala/scala-seed.g8               - Scala 2 seed template
 f) playframework/play-scala-seed.g8  - A Play project in Scala
 g) playframework/play-java-seed.g8   - A Play project in Java
 i) softwaremill/tapir.g8             - A tapir project using Netty
 m) scala-js/vite.g8                  - A Scala.JS + Vite project
 n) holdenk/sparkProjectTemplate.g8   - A Scala Spark project
 o) spotify/scio.g8                   - A Scio project
 p) disneystreaming/smithy4s.g8       - A Smithy4s project
 q) quit
Select a template:

「a」を選択すると、いくつかの質問が追加で表示される:

Select a template: a
Scala version (default: 3.3.0):
Scala Toolkit version (default: 0.2.0):

リターンキーを押せば、デフォルト値が選択される。

[info] Updated file /private/tmp/bar/project/build.properties: set sbt.version to 1.9.8
[info] welcome to sbt 1.9.8 (Azul Systems, Inc. Java 1.8.0_352)
....
[info] set current project to bar (in build file:/private/tmp/foo/)
[info] sbt server started at local:///Users/eed3si9n/.sbt/1.0/server/d0ac1409c0117a949d47/sock
[info] started sbt server
sbt:bar> exit
[info] shutting down sbt server

以下はこのテンプレートによって作成されたファイルだ:

.
├── build.sbt
├── project
│   └── build.properties
├── src
│   ├── main
│   │   └── scala
│   │       └── example
│   │           └── Main.scala
│   └── test
│       └── scala
│           └── example
│               └── ExampleSuite.scala
└── target

build.sbt ファイルを見ていこう:

val toolkitV = "0.2.0"
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV

scalaVersion := "3.3.0"
libraryDependencies += toolkit
libraryDependencies += (toolkitTest % Test)

これは、ビルド定義と呼ばれ、sbt がプロジェクトをコンパイルするのに必要な情報が記述されている。これは、.sbt 形式という Scala 言語のサブセットで書かれている。

以下は src/main/scala/example/Main.scala の内容だ:

package example

@main def main(args: String*): Unit =
  println(s"Hello ${args.mkString}")

これは、Hello world のテンプレートだ。これを実行するには、sbt --client と打ち込んで sbt シェルを起動して、シェル内から run <名前> と入力する:

$ sbt --client
[info] entering *experimental* thin client - BEEP WHIRR
[info] server was not detected. starting an instance
....
info] terminate the server with `shutdown`
[info] disconnect from the server with `exit`
sbt:bar> run Raj
[info] running example.main Raj
Hello Raj
[success] Total time: 0 s, completed Feb 18, 2024 2:38:10 PM

Giter8 テンプレート

.local テンプレートもいくつかあるが、基本的に sbt newGiter8 と統合して GitHub 上でホスティングされるテンプレートを開く。例えば、scala/scala3.g8 は Scala チームによりメンテナンスされ、新しい Scala 3 のビルドを作成する:

$ /tmp
$ sbt new scala/scala3.g8

Giter8 wiki では 100 以上のテンプレートが列挙されていて、新しいビルドを手早く作ることができる。

sbt のコンポーネント

sbt runner

sbt のビルドは、「sbt という名前のシェルスクリプト」によって実行され、このスクリプトは sbt runner と呼ばれる。

project/build.properties による sbt バージョンの指定

sbt runner は、そのサブコンポーネントである sbt launcher を実行し、sbt launcher は project/build.properties を読み込んで、そのビルドに使われる sbt のバージョンを決定し、キャッシュに無ければ sbt 本体をダウンロードする:

sbt.version=2.0.0-RC6

これは、つまり

  • ビルドをチェックアウトした人が、各々の sbt runner のバージョンに関わらわず、同一の sbt のバージョンを実行し
  • sbt 本体のバージョンは git のようなバージョン管理システムによって管理されることを意味する

sbtn (sbt --client)

sbtn (native thin client) は sbt runner のサブコンポーネントの一つで、sbt runner に --client フラグを渡すと呼ばれ、sbt server にコマンドを送信するのに使われる。名前に n が付いているのは、それが GraalVM native-image によってネーティブ・コードにコンパイルされていることに由来する。sbtn と sbt server は安定しているため、最近の sbt のバージョンなら大体動作するようになっている。

sbt server (sbt --server)

sbt server は、ビルドツール本体で、そのバージョンは project/build.properties によって指定される。sbt server は、sbtn やエディタから注文を受け取るレジ係の役割を持つ。

Coursier

sbt server は、そのサブコンポーネントとして Couriser を実行して、Scala 標準ライブラリ、Scala コンパイラ、ビルドで使われるその他のライブラリ依存性の解決を行う。

Zinc

Zinc は、sbt プロジェクトにより開発、メンテされている、Scala の差分コンパイラだ。Zinc の側面として見落とされがちなのは、Zinc はここ数年に出た全てのバージョンの Scala コンパイラに対する安定した API を提供しているということがある。Coursier がどんな Scala バージョンでも解決できることと合わせると、sbt は build.sbt に一行書くだけで、ここ数年に出たどのバージョンの Scala でも走らせることができる:

scalaVersion := "3.7.3"

BSP server

sbt server は Build Server Protocol (BSP) をサポートして、ビルド対象の列挙、ビルドの実行、その他を行うことができる。これにより、IntelliJ や Metals といった IDE が既に実行中の sbt server とコードを用いて通信することが可能となる。

sbt server との接続

sbt server との接続方法を 3通りみていく。

sbtn を用いた sbt シェル

ビルドのワーキング・ディレクトリ内で sbt を実行する:

sbt

Note

In sbt 1.x, equivalent command was sbt --client

これは以下のように表示されるはずだ:

$ sbt
[info] server was not detected. starting an instance
[info] welcome to sbt 2.0.0-alpha7 (Azul Systems, Inc. Java 1.8.0_352)
[info] loading project definition from /private/tmp/bar/project
[info] loading settings for project bar from build.sbt ...
[info] set current project to bar (in build file:/private/tmp/bar/)
[info] sbt server started at local:///Users/eed3si9n/.sbt/2.0.0-alpha7/server/d0ac1409c0117a949d47/sock
[info] started sbt server
[info] terminate the server with `shutdown`
[info] disconnect from the server with `exit`
sbt:bar>

sbt をコマンドラインの引数無しで実行すると、sbt シェルが起動する。sbt シェルは、コマンド打ち込むためのプロンプトを持つが、タブ補完が効き、履歴も持っている。

例えば、sbt シェルに compile と打ち込むことができる:

sbt:bar> compile

compile を再実行するには、上矢印を押下して、リターンキーを押す。

sbt シェルを中止するには、exit と打ち込むか、 Ctrl-D (Unix) もしくは Ctrl-Z (Windows) を使う。

sbtn を用いたバッチ・モード

sbt をバッチ・モードで使うことも可能だ:

sbt compile
sbt testOnly TestA
$ sbt compile
> compile

sbt server のシャットダウン

マシン上の sbt server を全てシャットダウンするには以下を実行する:

sbt shutdownall

現行のものだけをシャットダウンするには以下を実行する:

sbt shutdown

基本タスク

このページは sbt をセットアップした後の、基本的な使い方を解説する。このページは、sbt のコンポーネントを既に読んだことを前提とする。

sbt を使っているリポジトリを pull してきたら、手軽に使ってみることができる。まずは、GitHub などから、リポジトリを選んでチェックアウトする。

$ git clone https://github.com/scalanlp/breeze.git
$ cd breeze

Note

scalanlp/breeze は sbt 1.x を使っているが、ここでは sbt 2.x を使っている想定で書いていく。

sbtn を用いた sbt シェル

sbt のコンポーネント でも言及されたように、sbt シェルを起動する:

$ sbt

これは以下のように表示されるはずだ:

$ sbt
[info] entering *experimental* thin client - BEEP WHIRR
[info] server was not detected. starting an instance
[info] welcome to sbt 1.5.5 (Azul Systems, Inc. Java 1.8.0_352)
[info] loading global plugins from /Users/eed3si9n/.sbt/1.0/plugins
[info] loading settings for project breeze-build from plugins.sbt ...
[info] loading project definition from /private/tmp/breeze/project
Downloading https://repo1.maven.org/maven2/org/scalanlp/sbt-breeze-expand-codegen_2.12_1.0/0.2.1/sbt-breeze-expand-codegen-0.2.1.pom
....
[info] sbt server started at local:///Users/eed3si9n/.sbt/1.0/server/dd982e07e85c7de1b618/sock
[info] terminate the server with `shutdown`
[info] disconnect from the server with `exit`
sbt:breeze-parent>

projects コマンド

まずは手始めに、projects コマンドを使ってサブプロジェクトを列挙してみる:

sbt:breeze-parent> projects
[info] In file:/private/tmp/breeze/
[info]     benchmark
[info]     macros
[info]     math
[info]     natives
[info]   * root
[info]     viz

現行のサプブロジェクト root を含み、合計 6つのをサブプロジェクトを持つビルドであることが分かる。

tasks コマンド

同様に、tasks コマンドを用いて、このビルドが持つ全タスクを列挙することができる:

sbt:breeze-parent> tasks

This is a list of tasks defined for the current project.
It does not list the scopes the tasks are defined in; use the 'inspect' command for that.
Tasks produce values.  Use the 'show' command to run the task and print the resulting value.

  bgRun            Start an application's default main class as a background job
  bgRunMain        Start a provided main class as a background job
  clean            Deletes files produced by the build, such as generated sources, compiled classes, and task caches.
  compile          Compiles sources.
  console          Starts the Scala interpreter with the project classes on the classpath.
  consoleProject   Starts the Scala interpreter with the sbt and the build definition on the classpath and useful imports.
  consoleQuick     Starts the Scala interpreter with the project dependencies on the classpath.
  copyResources    Copies resources to the output directory.
  doc              Generates API documentation.
  package          Produces the main artifact, such as a binary jar.  This is typically an alias for the task that actually does the packaging.
  packageBin       Produces a main artifact, such as a binary jar.
  packageDoc       Produces a documentation artifact, such as a jar containing API documentation.
  packageSrc       Produces a source artifact, such as a jar containing sources and resources.
  publish          Publishes artifacts to a repository.
  publishLocal     Publishes artifacts to the local Ivy repository.
  publishM2        Publishes artifacts to the local Maven repository.
  run              Runs a main class, passing along arguments provided on the command line.
  runMain          Runs the main class selected by the first argument, passing the remaining arguments to the main method.
  test             Executes all tests.
  testOnly         Executes the tests provided as arguments or all tests if no arguments are provided.
  testQuick        Executes the tests that either failed before, were not run or whose transitive dependencies changed, among those provided as arguments.
  update           Resolves and optionally retrieves dependencies, producing a report.

More tasks may be viewed by increasing verbosity.  See 'help tasks'

compile

compile タスクは、ライブラリ依存性の解決とダウンロードを行った後に、ソースのコンパイルを行う。

> compile

これは以下のように表示されるはずだ:

sbt:breeze-parent> compile
[info] compiling 341 Scala sources and 1 Java source to /private/tmp/breeze/math/target/scala-3.1.3/classes ...
  | => math / Compile / compileIncremental 51s

run

run タスクは、サブプロジェクトのメインクラスを実行する。sbt シェルから math/run と打ち込む:

> math/run

math/run は、math サブプロジェクトにスコープ付けされた run タスクを意味する。これは、以下のように表示されるはずだ:

sbt:breeze-parent> math/run
[info] Scala version: 3.1.3 true
....

Multiple main classes detected. Select one to run:
 [1] breeze.optimize.linear.NNLS
 [2] breeze.optimize.proximal.NonlinearMinimizer
 [3] breeze.optimize.proximal.QuadraticMinimizer
 [4] breeze.util.UpdateSerializedObjects

Enter number:

プロンプトには 1 と入力する。

test

test タスクは、以前に失敗したテスト、未だ実行されていないテスト、及び間接的依存性に変化があったテストを実行する。

> math/test

これは以下のように表示されるはずだ:

sbt:breeze-parent> math/testQuick
[info] FeatureVectorTest:
[info] - axpy fv dv (1 second, 106 milliseconds)
[info] - axpy fv vb (9 milliseconds)
[info] - DM mult (19 milliseconds)
[info] - CSC mult (32 milliseconds)
[info] - DM trans mult (4 milliseconds)
....
[info] Run completed in 58 seconds, 183 milliseconds.
[info] Total number of tests run: 1285
[info] Suites: completed 168, aborted 0
[info] Tests: succeeded 1285, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 130 s (02:10), completed Feb 19, 2024

watch (チルダ) コマンド

編集-コンパイル-テストの一連のサイクルの高速化のために、ソースが保存されるたびに自動的に再コンパイルか再テストを行うように sbt に命令することができる。

コマンドの前に ~ を付けることで、ファイルが変更されるたびに自動的にそのコマンドが実行されるようになる。例えば、sbt シェルから以下のように打ち込む:

> ~test

リターンキーを押下して、監視を中止する。~ の前置記法は sbt シェルからもバッチ・モードからでも使用可能。

ビルド定義の基本

このページは build.sbt のビルド定義を解説する。

ビルド定義とは何か

ビルド定義は、build.sbt にて定義され、プロジェクト (型は Project) の集合によって構成される。 プロジェクトという用語が曖昧であることがあるため、このガイドではこれらをサブプロジェクトと呼ぶことが多い。

例えば、カレントディレクトリにあるサブプロジェクトは build.sbt に以下のように定義できる:

scalaVersion := "3.3.3"
name := "Hello"

より明示的に書くと:

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

それぞれのサブプロジェクトは、キーと値のペアによって詳細が設定される。例えば、name というキーがあるが、それはサブプロジェクト名という文字列の値に関連付けられる。キーと値のペア列は .settings(...) メソッド内に列挙される。

build.sbt DSL

build.sbt は、Scala に基づいた build.sbt DSL と呼ばれるドメイン特定言語 (DSL) を用いてサブプロジェクトを定義する。まずは scalaVersionlibraryDependencies のみを宣言して、YAML ファイルのように build.sbt を使うことができるが、ビルドの成長に応じてその他の機能を使ってビルド定義を整理することができる。

型付けされたセッティング式

build.sbt DSL をより詳しくみていこう:

organization  :=         "com.example"
^^^^^^^^^^^^  ^^^^^^^^   ^^^^^^^^^^^^^
key           operator   (setting/task) body

それぞれのエントリーはセッティング式と呼ばれる。セッティング式にはタスク式と呼ばれるものもある。この違いは後で説明する。

セッティング式は以下の 3部から構成される:

  1. 左辺項をキー (key) という。
  2. 演算子。この場合は :=
  3. 右辺項は本文 (body)、もしくはセッティング本文という。

左辺値の nameversion、および scalaVersion はキーである。 キーは SettingKey[A]TaskKey[A]、もしくは InputKey[A] のインスタンスで、 A はその値の型である。キーの種類に関しては後述する。

name キーは SettingKey[String] に型付けされているため、 name:= 演算子も String に型付けされている。これにより、誤った型の値を使おうとするとビルド定義はコンパイルエラーになる:

name := 42 // コンパイルできない

vallazy val

ライブラリのバージョン番号など同じ情報の繰り返しを避けるために、build.sbt 内の任意の行に vallazy valdef を書くことができる。

val toolkitV = "0.2.0"
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV

scalaVersion := "3.7.3"
libraryDependencies += toolkit
libraryDependencies += (toolkitTest % Test)

上の例で val は変数を定義し、上の行から順に初期化される。そのため、toolkitV が参照される前に定義される必要がある。

以下は悪い例:

// 悪い例
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV // 未初期化の参照!
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV // 未初期化の参照!
val toolkitV = "0.2.0"

build.sbt に未初期化の事前参照を含む場合、sbt は NullPointerException による java.lang.ExceptionInInitializerError を投げて起動に失敗する。これをコンパイラに直させる方法の 1つとして、変数に lazy を付けて遅延変数として定義するという方法がある:

lazy val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
lazy val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
lazy val toolkitV = "0.2.0"

何でも lazy val を付けるのに渋い顔をする人もいるかもしれないが、僕たちは Scala 3 の lazy val は効率が良く、ビルド定義をコピー・ペーストに対して堅牢にすると思っている。

Note

build.sbt トップレベルでのクラスやオブジェクトの定義は禁止されている。 それらは、project/ ディレクトリ内の Scala ソース内に書かれる。

ライブラリ依存性の基本

このページは、sbt を使ったライブラリ依存性管理の基本を説明する。

sbt はマネージ依存性 (managed dependency) を実装するのに内部で Coursier を採用していて、Coursier、npm、PIP などのパッケージ管理を使った事がある人は違和感無く入り込めるだろう。

マネージ依存性とは何か

JAR ファイルを 1つ 1つ手でダウンロードする (アンマネージ依存性) 代わりに、マネージ依存性システムはサブプロジェクトで使われる外部ライブラリの取得を自動化する。Coursier のようなツールは宣言された ModuleID 列を解釈して、依存性解決 (全ての間接的依存性を展開して、バージョン衝突を解決して、正確なバージョンを決定する) を行い、結果となったアーティファクトをダウンロードしキャッシュして、一貫性のある JAR 管理を保証する。

libraryDependencies キー

依存性の宣言は、以下のようになる。ここで、groupIdartifactId、と revision は文字列だ:

libraryDependencies += groupID % artifactID % revision

もしくは、以下のようになる。このときの configuration は文字列もしくは Configuration の値だ (Test など)。

libraryDependencies += groupID % artifactID % revision % configuration

コンパイルを実行すると:

> compile

sbt は自動的に依存性を解決して、JAR ファイルをダウンロードする。

%% を使って正しい Scala バージョンを入手する

groupID % artifactID % revision のかわりに、 groupID %% artifactID % revision を使うと(違いは groupID の後ろの二つ連なった %%)、 sbt はプロジェクトの Scala のバイナリバージョンをアーティファクト名に追加する。これはただの略記法なので %% 無しで書くこともできる:

libraryDependencies += "org.scala-lang" % "toolkit_3" % "0.2.0"

ビルドの Scala バージョンが 3.x だとすると、以下の設定は上記と等価だ("org.scala-lang" の後ろの二つ連なった %% に注意):

libraryDependencies += "org.scala-lang" %% "toolkit" % "0.2.0"

多くの依存ライブラリは複数の Scala バイナリバージョンに対してコンパイルされており、この機構はそのうちの中からプロジェクトとバイナリ互換性のある正しいものを選択する便利機能だ。

ライブラリ依存性を一箇所にまとめる

project 内の任意の .scala ファイルがビルド定義の一部となることを利用する一つの例として project/Dependencies.scala というファイルを作ってライブラリ依存性を一箇所にまとめるということができる。

// project/Dependencies.scala にこのファイルを置く

import sbt.*

object Dependencies:
  // versions
  lazy val toolkitV = "0.2.0"

  // libraries
  val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
  val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
end Dependencies

この Dependencies オブジェクトは build.sbt 内で利用可能となる。 定義されている val が使いやすいように Dependencies.* を import しておこう。

import Dependencies.*

scalaVersion := "3.7.3"
name := "something"
libraryDependencies += toolkit
libraryDependencies += toolkitTest % Test

ライブラリ依存性の可視化

sbt シェルに Compile/dependencyTree と入力すると、ライブラリ依存性の間接的依存性を含むツリーが表示される:

> Compile/dependencyTree

これは以下のように表示されるはずだ:

sbt:bar> Compile/dependencyTree
[info] default:bar_3:0.1.0-SNAPSHOT
[info]   +-org.scala-lang:scala3-library_3:3.3.1 [S]
[info]   +-org.scala-lang:toolkit_3:0.2.0
[info]     +-com.lihaoyi:os-lib_3:0.9.1
[info]     | +-com.lihaoyi:geny_3:1.0.0
[info]     | | +-org.scala-lang:scala3-library_3:3.1.3 (evicted by: 3.3.1)
[info]     | | +-org.scala-lang:scala3-library_3:3.3.1 [S]
....

マルチプロジェクトの基本

簡単なプログラムならば単一プロジェクトから作り始めてもいいが、ビルドが複数の小さいのサブプロジェクトに分かれていくのが普通だ。

ビルド内のサブプロジェクトは、それぞれ独自のソースディレクトリを持ち、packageBin を実行すると独自の JAR ファイルを生成するなど、概ね通常のプロジェクトと同様に動作する。

サブプロジェクトは、lazy val を用いて Project 型の値を宣言することで定義される。例えば:

scalaVersion := "3.7.3"
LocalRootProject / publish / skip := true

lazy val core = (project in file("core"))
  .settings(
    name := "core",
  )

lazy val util = (project in file("util"))
  .dependsOn(core)
  .settings(
    name := "util",
  )

val 定義された変数名はプロジェクトの ID 及びベースディレクトリの名前になる。ID は sbt シェルからプロジェクトを指定する時に用いられる。

sbt は必ずルートプロジェクトを定義するので、上の例のビルド定義は合計 3つのサブプロジェクトを持つ。

サブプロジェクト間依存性

あるサブプロジェクトを、他のサブプロジェクトにあるコードに依存させたい場合、dependsOn(...) を使ってこれを宣言する。例えば、utilcore のクラスパスが必要な場合は util の定義を次のように書く:

lazy val util = (project in file("util"))
  .dependsOn(core)

タスク集約

タスク集約は、集約する側のサブプロジェクトで任意のタスクを実行するとき、集約される側の複数のサブプロジェクトでも同じタスクが実行されるという関係を意味する。

scalaVersion := "3.7.3"

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

lazy val util = (project in file("util"))

lazy val core = (project in file("core"))

上の例では、ルートプロジェクトが utilcore を集約する。そのため、sbt シェルに compile と打ち込むと、3つのサブプロジェクトが並列にコンパイルされる。

ルートプロジェクト

ビルドのルートにあるサブプロジェクトは、ルートプロジェクトと呼ばれ、ビルドの中で特別な役割を果たすことがある。もしルートディレクトリにサブプロジェクトが定義されてない場合、sbt は自動的に他のプロジェクトを集約するデフォルトのルートプロジェクトを生成する。

コモン・セッティング

sbt 2.x では、settings(...) を使わずに build.sbt にセッティングを直書きした場合、コモン・セッティングとして全サブプロジェクトに注入される。

scalaVersion := "3.7.3"

lazy val core = (project in file("core"))

lazy val app = (project in file("app"))
  .dependsOn(core)

上の例では、scalaVersion セッティングはデフォルトのルートプロジェクト、coreutil に適用される。

既にサブプロジェクトにスコープ付けされたセッティングはこのルールの例外となる。

scalaVersion := "3.7.3"

lazy val core = (project in file("core"))

lazy val app = (project in file("app"))
  .dependsOn(core)

// これは app のみに適用される
app / name := "app1"

この例外を利用して、以下のように、ルートプロジェクトにのみ適用されるセッティングを書くことができる:

scalaVersion := "3.7.3"

lazy val core = (project in file("core"))

lazy val app = (project in file("app"))
  .dependsOn(core)

// これらは root にのみ適用される
LocalRootProject / name := "root"
LocalRootProject / publish / skip := true

プラグインの基本

プラグインとは何か

プラグインは、新しいセッティングやタスクを追加することでビルド定義を拡張する。例えば、プラグインを使って githubWorkflowGenerate というタスクを追加して、GitHub Actions のための YAML を自動生成することができる。

Scaladex を使ったプラグイン・バージョンの検索

Scaladex を用いてプラグインを検索して、そのプラグインの最新バージョンを探すことができる。

プラグインの宣言

ビルドが hello というディレクトリにあるとして、sbt-github-actions をビルド定義に追加したい場合、hello/project/plugins.sbt というファイルを作成して、プラグインの ModuleID を addSbtPlugin(...) に渡すことで、プラグイン依存性を宣言する:

// In project/plugins.sbt

addSbtPlugin("com.github.sbt" % "sbt-github-actions" % "0.28.0")

ビルドに sbt-assembly を追加する場合、以下を追加する:

// In project/plugins.sbt

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

ソース依存性プラグインレシピには git リポジトリにホスティングされたプラグインを直接使う実験的技法が書かれてる。

プラグインは、通常サブプロジェクトに追加されるセッティングやタスクを提供することでその機能を実現する。次のセクションでその仕組みをもう少し詳しくみていく。

auto plugin の有効化と無効化

プラグインは、自身が持つセッティング群がビルド定義に自動的に追加されるよう宣言することができ、 その場合、プラグインの利用者は何もしなくてもいい。

auto plugin 機能は、セッティング群とその依存関係がサブプロジェクトに自動的、かつ安全に設定されることを保証する。auto plugin の多くはデフォルトのセッティング群を自動的に追加するが、中には明示的な有効化を必要とするものもある。

明示的な有効化が必要な auto plugin を使っている場合は、以下を build.sbt に追加する必要がある:

lazy val util = (project in file("util"))
  .enablePlugins(FooPlugin, BarPlugin)
  .settings(
    name := "hello-util"
  )

enablePlugins メソッドを使って、そのサブプロジェクトで使用したい auto plugin を明示的に定義できる。

逆に disablePlugins メソッドを使ってプラグインを除外することもできる。例えば、util から IvyPlugin のセッティングを除外したいとすると、build.sbt を以下のように変更する:

lazy val util = (project in file("util"))
  .enablePlugins(FooPlugin, BarPlugin)
  .disablePlugins(plugins.IvyPlugin)
  .settings(
    name := "hello-util"
  )

明示的な有効化が必要か否かは、それぞれの auto plugin がドキュメントで明記しておくべきだ。あるプロジェクトでどんな auto plugin が有効化されているか気になったら、 sbt シェルから plugins コマンドを実行してみよう。

sbt:hello> plugins
In build /tmp/hello/:
  Enabled plugins in hello:
    sbt.plugins.CorePlugin
    sbt.plugins.DependencyTreePlugin
    sbt.plugins.Giter8TemplatePlugin
    sbt.plugins.IvyPlugin
    sbt.plugins.JUnitXmlReportPlugin
    sbt.plugins.JvmPlugin
    sbt.plugins.SemanticdbPlugin
Plugins that are loaded to the build but not enabled in any subprojects:
  sbt.ScriptedPlugin
  sbt.plugins.SbtPlugin

ここでは、plugins の表示によって sbt のデフォルトのプラグインが全て有効化されていることが分かる。 sbt のデフォルトセッティングは 7つのプラグインによって提供される:

  1. CorePlugin: タスクの並列実行などのコア機能。
  2. DependencyTreePlugin: 依存性のツリー表示タスク。
  3. Giter8TemplatePlugin: sbt new 機能の提供。
  4. IvyPlugin: モジュールの依存性解決と公開機能。
  5. JUnitXmlReportPlugin: junit-xml の生成。
  6. JvmPlugin: Java/Scala サブプロジェクトのコンパイル、テスト、実行、パッケージ化の機構。
  7. SemanticdbPlugin: SemanticDB の生成。

利用可能なプラグイン

Scaladex の他にプラグインのリストがある。

ビルドのレイアウト

sbt は、知らない sbt ビルドを見てもだいたい勝手が分かるようにファイルをどこに置くかの慣習を持っている:

.
├── build.sbt
├── project/
│   ├── build.properties
│   ├── Dependencies.scala
│   └── plugins.sbt
├── src/
│   ├── main/
│   │   ├── java/
│   │   ├── resources/
│   │   ├── scala/
│   │   └── scala-2.13/
│   └── test/
│       ├── java/
│       ├── resources/
│       ├── scala/
│       └── scala-2.13/
├── subproject-core/
│   └── src/
│       ├── main/
│       └── test/
├─── subproject-util/
│   └── src/
│       ├── main/
│       └── test/
└── target/
  • . で表記したローカルのルートディレクトリはビルドのスタート地点だ。
  • ベース・ディレクトリは sbt 用語で、サブプロジェクトを構成するディレクトリを指す。上の例では、.subproject-coresubproject-util は全てベース・ディレクトリだ。
  • ビルド定義は、ローカルのルートディレクトリに置かれた build.sbt にて記述される(実は *.sbt という名前のファイルなら何でもいい)。
  • sbt のバージョンは project/build.properties によって管理される。
  • 生成されたファイル (コンパイルされたクラス、パッケージ化された JAR、マネージファイル、キャッシュ、ドキュメンテーションなど) はデフォルトでは target ディレクトリに書き込まれる。

ビルドサポートファイル

In addition to build.sbt, project directory can contain .scala files that define helper objects and one-off plugins.

.
├── build.sbt
├── project/
│   ├── build.properties
│   ├── Dependencies.scala
│   └── plugins.sbt
....

project/ 内に .sbt ファイルを見かけることもあるかもしれないが、これらは通常プラグインを宣言するのに用いられる。プラグインの基本参照。

ソースコード

ソースコードに関して sbt は、デフォルトで Maven と同じディレクトリ構造を使う:

....
├── src/
│   ├── main/
│   │   ├── java/        <main Java sources>
│   │   ├── resources/   <files to include in main JAR>
│   │   ├── scala/       <main Scala sources>
│   │   └── scala-2.13/  <main Scala 2.13 specific sources>
│   └── test/
│       ├── java/        <test Java sources>
│       ├── resources/   <files to include in test JAR>
│       ├── scala/       <test Scala sources>
│       └── scala-2.13/  <test Scala 2.13 specific sources>
....

src/ 内の他のディレクトリは無視される。また、隠しディレクトリも無視される。

ソースコードは hello/app.scala のようにプロジェクトのベースディレクトリに置くこともできるが、小さいプロジェクトはともかくとして、通常のプロジェクトでは src/main/ 以下のディレクトリにソースを入れて整理するのが普通だ。

バージョン管理の設定

.gitignore (もしくは、他のバージョン管理システムの同様のファイル)には以下を追加しておくとよいだろう:

target/

Note

ここでは(ディレクトリだけにマッチさせるために)語尾の / を意図的につけていて、一方で (普通の target/ に加えて project/target/ にもマッチさせるために)先頭の / は意図的に つけていないことに注意。

sbt と IDE

エディタと sbt だけで Scala のコードを書くことも可能だが、今日日のプログラマの多くは統合開発環境 (IDE) を用いる。Scala の IDE は MetalsIntelliJ IDEA の二強で、それぞれ sbt ビルドとの統合をサポートする。

IDE を使う利点をいくつか挙げると:

  • 定義へのジャンプ
  • 静的型付けに基づくコード補完
  • コンパイルエラーの列挙と、エラー地点へのジャンプ
  • インタラクティブなデバッグ

IDE 統合のためのレシピをここにいくつか紹介する:

変更点

sbt 2.0 の変更点

互換性に影響のある変更点

sbt 1.x からのマイグレーションも参照。

  • Scala 3 を用いたメタビルド。ビルド定義やプラグインに使われる sbt 2.x build.sbt DSL は Scala 3.x ベースとなった (現行では 3.7.3) (sbt 1.x 並びに 2.x は、Scala 2.x と 3.x の両方をビルドすることが可能) by @eed3si9n, @adpi2, and others.
  • コモン・セッティング。build.sbt に直書きされたセッティングは、ルートサブプロジェクトだけではなく、全てのサブプロジェクトに追加され、これまで ThisBuild が受け持ってきた役目を果たすことができる。
  • 差分テストtest は、テスト結果をキャッシュする差分テストへと変更された。全テストを走らせたい場合は testFull を使う by @eed3si9n in #7686
  • キャッシュ化されたタスク。全てのタスクはデフォルトで、キャッシュ化されている。詳細はキャッシュ参照。
  • 依存性のツリー表示dependencyTree 関連のタスク群は 1つのインプット・タスクに統一された by @eed3si9n in #8199
  • test タスクの形が Unit から TestResult へと変更された by @eed3si9n in #8181
  • 以前 URL に型付けされていたデフォルトのセッティングやタスクキー (apiMappings, apiURL, homepage, organizationHomepage, releaseNotesURL など) は URI に変更された in #7927
  • license キーの型が Seq[(String, URL)] から Seq[License] へと変更された in #7927
  • sbt 2.x プラグインは _sbt2_3 という suffix を用いて公開される by @eed3si9n in #7671
  • sbt 2.x は、platform セッティングを追加して、ModuleID%%% 演算子を使わなくても、%% 演算子だけで JVM、JS、Native のクロスビルドができるようにした by @eed3si9n in #6746
  • useCoursier セッティングを廃止して、Coursier をオプトアウトできないようにした by @eed3si9n in #7712
  • Key.Classpath 型は、Seq[Attributed[File]] からSeq[Attributed[xsbti.HashedVirtualFileRef]] 型のへのエイリアスへと変更された。同様に、以前 File を返していたタスクキーのいくつかはHashedVirtualFileRef を返すように変更された。ファイルのキャッシュ化も参照。
  • sbt 2.x では、target のデフォルトが <subproject>/target/ から target/out/jvm/scala-3.7.3/<subproject>/ へと変更された。
  • sbt 2.x は build.sbt に変更があると、デフォルトで自動的リロードされるようになった @eed3si9n in #8211
  • 自動的タスク集約を行う Project#autoAggregate が追加された @eed3si9n in #8290

勧告どおり廃止された機能

  • IntegrationTest コンフィギュレーションは廃止された in #8184
  • sbt 0.13 スタイルのシェル構文は廃止された in #7700

新機能

  • project matrix。sbt 1.x からプラグインで使用可能だった project matrix が本体に取り込まれ、並列クロスビルドができるようになった。
  • sbt クエリ。sbt 2.x は統一スラッシュ構文を拡張してサブプロジェクトのクエリを可能とする。詳細は下記参照。
  • ローカル/リモート兼用のキャッシュ・システム。詳細は下記参照。
  • クライアントサイド・ラン。詳細は下記参照。

コモン・セッティング

sbt 2.x では、build.sbt に直書きされたセッティングはコモン・セッティングとして解釈され、全サブプロジェクトに注入される。そのため、ThisBuild スコープ付けを使わずに scalaVersion を設定できる:

scalaVersion := "3.7.3"

さらに、これはいわゆる動的ディスパッチ問題を解決する:

lazy val hi = taskKey[String]("")
hi := name.value + "!"

sbt 1.x では hi タスクはルートプロジェクトの名前を捕捉してしまっていたが、sbt 2.x は各サブプロジェクトの name! を追加する:

$ sbt show hi
[info] entering *experimental* thin client - BEEP WHIRR
[info] terminate the server with `shutdown`
> show hi
[info] foo / hi
[info]  foo!
[info] hi
[info]  root!

これは、@eed3si9n によって #6746 としてコントリされた。

sbt クエリ

サブプロジェクトが複数あるとき、選択範囲を狭めるのに sbt 2.x は sbt クエリを導入する。

$ sbt foo.../test

上の例では、foo から始まる全てのサブプロジェクトを実行する。

$ sbt ...@scalaBinaryVersion=3/test

上の例では、scalaBinaryVersion3 の全てのサブプロジェクトを実行する。これは、@eed3si9n によって #7699 としてコントリされた。

差分テスト

sbt 2.x では、test タスクはインプット・タスクとなって、実行するテスト・スイートをフィルターできるようになった:

> test *Example*

さらに、test はキャッシュ化された差分テストとなった。そのため、以前にテストが失敗したか、前回の実行から何かが変更されないと実行されないようになった。

詳細は test 参照

ローカル/リモート兼用のキャッシュ・システム

sbt 2.x は、デフォルトでキャッシュ化されたタスクを実装するため、自動的にタスクの結果をローカルのディスクもしくは Bazel 互換のリモートキャッシュにてキャッシュ化することができる。

lazy val task1 = taskKey[String]("doc for task1")

task1 := name.value + version.value + "!"

これは task1 への入力を追跡してマシン・ワイドなディスクキャッシュを作成する。これはリモート・キャッシュとしても設定できるようになっている。sbt タスクはファイルを生成することがよくあるので、ファイルのコンテンツをキャッシュ化できる仕組みも提供する。

lazy val task1 = taskKey[String]("doc for task1")

task1 := {
  val converter = fileConverter.value
  ....
  val output = converter.toVirtualFile(somefile)
  Def.declareOutput(output)
  name.value + version.value + "!"
}

詳細はキャッシュ化を参照。これは @eed3si9n によって #7464 / #7525 としてコントリされた。

クライアントサイド・ラン

sbt runner 1.10.10 以降は sbt 2.x を起動するのに、デフォルトで sbtn (GraalVM native-image のクライアント) を使う。sbt 2.0 は run タスクを sbtn に送り返して、sbtn 側で新しい JVM にフォークさせる。以下のように実行するだけでいい:

sbt run

これは、sbt server をブロックしてしまうことを回避し、かつ複数の実行を行うことができる。これは @eed3si9n によって#8060 としてコントリされた。run ドキュメンテーション参照。

性能の改善

Adrien Piquerez さんが Scala Center に在籍していたときに、性能改善に関連するコントリをいくつか行った。

  • perf: 長生きするオブジェクトの数を削減して、2.0.0-M2 比較で起動を 20% 高速化した by @adpi2 in #7866
  • perf: SettingInitialize が作成される数を削減した by @adpi2 in #7880
  • perf: Settings をリファクタリングして、集約キーのインデックス化を最適化した @adpi2 in #7879
  • perf: InfoBasicAttributeMap のインスタンスを削除した by @adpi2 in #7882

過去の変更点

以下も参照:

sbt 1.x からのマイグレーション

build.sbt DSL の Scala 3.x への移行

念の為書いておくと、sbt 1.x 系 2.x 系のどちらを使ってもScala 2.x および Scala 3.x 両方のプログラムをビルドすることが可能だ。ただし、build.sbt DSL を裏付ける Scala は sbt のバージョンによって決定される。sbt 2.0 では、Scala 3.7.x 系に移行する。

そのため、sbt 2.x 用のカスタムタスクや sbt プラグインを実装する場合、それは Scala 3.x を用いて行われることとなる。Scala 3.x に関する詳細はScala 3.x incompatibility tableScala 2 with -Xsource:3 なども参照。

// This works on Scala 2.12.20 under -Xsource:3
import sbt.{ given, * }

Import given

Scala 2.x と 3.x の違いの 1つとして、型クラスのインスタンスの言語スコープへの取り込み方の違いが挙げられる。Scala 2.x では import FooCodec._ と書いたが、Scala 3 は、import FooCodec.given と書く。

// 以下は、sbt 1.x と 2.x の両方で動作する
import sbt.librarymanagement.LibraryManagementCodec.{ given, * }

後置記法の回避

特に ModuleID 関連で、 sbt 0.13 や 1.x のコード例で後置記法を見ることは珍しく無かった:

// BAD
libraryDependencies +=
  "com.github.sbt" % "junit-interface" % "0.13.2" withSources() withJavadoc()

これは、sbt 2.x では起動に失敗する:

-- Error: /private/tmp/foo/build.sbt:9:61 --------------------------------------
9 |  "com.github.sbt" % "junit-interface" % "0.13.2" withSources() withJavadoc()
  |                                                             ^^
  |can't supply unit value with infix notation because nullary method withSources
   in class ModuleIDExtra: (): sbt.librarymanagement.ModuleID takes no arguments;
   use dotted invocation instead: (...).withSources()

これを修正するには、普通の (ピリオドを使った) 関数呼び出しの記法で書く:

// GOOD
libraryDependencies +=
  ("com.github.sbt" % "junit-interface" % "0.13.2").withSources().withJavadoc()

ベア・セッティングの変更点

version := "0.1.0"
scalaVersion := "3.7.3"

settings(...) を介さず、上の例のように build.sbt に直書きされたセッティングをベア・セッティング (bare settings) と呼ぶ。

Warning

sbt 1.x において、ベア・セッティングはルートプロジェクトにのみ適用されるセッティングだった。sbt 2.x においては、build.sbt のベア・セッティングは全サブプロジェクトに注入されるコモン・セッティングとして解釈される。

name := "root"         // 全サブプロジェクトの名前が root になる!
publish / skip := true // 全サブプロジェクトがスキップされる!

ルートプロジェクトにのみセッティングを適用させるためには、マルチ・プロジェクトなビルドを定義するか、セッティングを LocalRootProject にスコープ付けさせる:

LocalRootProject / name := "root"
LocalRootProject / publish / skip := true

ThisBuild のマイグレーション

sbt 2.x においては、ベア・セッティングが ThisBuild にスコープ付けされる場面は無くなったはずだ。コモン・セッティングの ThisBuild に対する利点として委譲の振る舞いが、分かりやすくなるということがある。これらのセッティングはプラグインのセッティングと、settings(...) の間に入るため、 Compile / scalacOptions といったセッティングを定義するのに使える。 ThisBuild ではそれは不可能だった。

キャッシュ・タスクへのマイグレーション

キャッシュ化のオプトアウトを含めキャッシュ・タスク参照。

IntegrationTest の廃止

IntegrationTest コンフィギュレーションの廃止に対応するためには、別のサブプロジェクトを定義して、普通のテストとして実装するのが推奨される。

スラッシュ構文へのマイグレーション

sbt 1.x は sbt 0.13 スタイル構文とスラッシュ構文の両方をサポートしたきた。2.x で sbt 0.13 構文のサポートが無くなるので、sbt シェルと build.sbt の両方で統一スラッシュ構文を使う:

<project-id> / Config / intask / key

具体的には、test:compile という表記はシェルからは使えなくなる。代わりに Test/compile と書く。build.sbt ファイルの半自動的なマイグレーションには統一スラッシュ構文のための syntactic Scalafix rule 参照。

scalafix --rules=https://gist.githubusercontent.com/eed3si9n/57e83f5330592d968ce49f0d5030d4d5/raw/7f576f16a90e432baa49911c9a66204c354947bb/Sbt0_13BuildSyntax.scala *.sbt project/*.scala

sbt プラグインのクロスビルド

sbt 2.x では、sbt プラグインを Scala 3.x と 2.12.x に対してクロスビルドすると自動的に sbt 1.x 向けと sbt 2.x 向けにクロスビルドするようになっている。

// sbt 2.x を使った場合
lazy val plugin = (projectMatrix in file("plugin"))
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-vimquit",
  )
  .jvmPlatform(scalaVersions = Seq("3.6.2", "2.12.20"))

projectMatrix を使った場合、プラグインを plugin/ のようなサブディレクトリに移動させる必要がある。そうしないと、デフォルトのルートプロジェクトも src/ を拾ってしまうからだ。

sbt 1.x を使った sbt プラグインのクロスビルド

sbt 1.x を使ってクロスビルドする場合 sbt 1.10.2 以降を使う必要がある。

// sbt 1.x を使った場合
lazy val scala212 = "2.12.20"
lazy val scala3 = "3.6.2"
ThisBuild / crossScalaVersions := Seq(scala212, scala3)

lazy val plugin = (project in file("plugin"))
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-vimquit",
    (pluginCrossBuild / sbtVersion) := {
      scalaBinaryVersion.value match {
        case "2.12" => "1.5.8"
        case _      => "2.0.0-RC6"
      }
    },
  )

%% の変更点

sbt 2.x において ModuleID%% 演算子は多プラットフォームに対応するようになった。JVM系のサブプロジェクトは、%% は以前同様に Maven リポジトリにおける (_3 のような) Scala バージョンの接尾辞をエンコードする。

%%% 演算子のマイグレーション

Scala.JS や Scala Native の sbt 2.x の対応ができるようになった場合、%% は (_3 のような) Scala バージョンと (_sjs1 その他の) プラットフォーム接尾辞をエンコードできるようになっている。そのため、%%%%% に置き換えれるようになるはずだ:

libraryDependencies += "org.scala-js" %% "scalajs-dom" % "2.8.0"

JVM ライブラリが必要な場合は .platform(Platform.jvm) を使う。

target に関する変更

sbt 2.x では、target ディレクトリが単一の target/ ディレクトリに統一され、それぞれのサブプロジェクトがプラットフォーム、Scala バージョン、id をエンコードしたサブディレクトリを作る。scripted test でこの変更点を吸収するために、existsabsentdelete のコマンドは glob式 ** および || をサポートする。

# before
$ absent target/out/jvm/scala-3.3.1/clean-managed/src_managed/foo.txt
$ exists target/out/jvm/scala-3.3.1/clean-managed/src_managed/bar.txt

# after
$ absent target/**/src_managed/foo.txt
$ exists target/**/src_managed/bar.txt

# どちらでも ok
$ exists target/**/proj/src_managed/bar.txt || proj/target/**/src_managed/bar.txt

PluginCompat 技法

同じ *.scala ソースから sbt 1.x と 2.x 向けのプラグインを作るには「詰め木」的なコードとして PluginCompat という名前のオブジェクトをsrc/main/scala-2.12/src/main/scala-3/ の両方に入れて違いを吸収することができる。

Classpath 型のマイグレーション

sbt 2.x からは Classpath 型がSeq[Attributed[xsbti.HashedVirtualFileRef]] 型のエイリアスとなった。以下は、sbt 1.x と 2.x の両方のクラスパスに対応するための詰め木だ。

// src/main/scala-3/PluginCompat.scala

package sbtfoo

import java.nio.file.{ Path => NioPath }
import sbt.*
import xsbti.{ FileConverter, HashedVirtualFileRef, VirtualFile }

private[sbtfoo] object PluginCompat:
  type FileRef = HashedVirtualFileRef
  type Out = VirtualFile

  def toNioPath(a: Attributed[HashedVirtualFileRef])(using conv: FileConverter): NioPath =
    conv.toPath(a.data)
  inline def toFile(a: Attributed[HashedVirtualFileRef])(using conv: FileConverter): File =
    toNioPath(a).toFile()
  def toNioPaths(cp: Seq[Attributed[HashedVirtualFileRef]])(using conv: FileConverter): Vector[NioPath] =
    cp.map(toNioPath).toVector
  inline def toFiles(cp: Seq[Attributed[HashedVirtualFileRef]])(using conv: FileConverter): Vector[File] =
    toNioPaths(cp).map(_.toFile())
end PluginCompat

これが sbt 1.x 用だ:

// src/main/scala-2.12/PluginCompat.scala

package sbtfoo

import sbt.*

private[sbtfoo] object PluginCompat {
  type FileRef = java.io.File
  type Out = java.io.File

  def toNioPath(a: Attributed[File])(implicit conv: FileConverter): NioPath =
    a.data.toPath()
  def toFile(a: Attributed[File])(implicit conv: FileConverter): File =
    a.data
  def toNioPaths(cp: Seq[Attributed[File]])(implicit conv: FileConverter): Vector[NioPath] =
    cp.map(_.data.toPath()).toVector
  def toFiles(cp: Seq[Attributed[File]])(implicit conv: FileConverter): Vector[File] =
    cp.map(_.data).toVector

  // This adds `Def.uncached(...)`
  implicit class DefOp(singleton: Def.type) {
    def uncached[A1](a: A1): A1 = a
  }
}

これで PluginCompat.* を import して toNioPaths(...) などを使って sbt 1.x と 2.x の違いを吸収できる。特にこれは、クラスパス型の違いを吸収して NIO Path ベクトルに変換できることを示している。

コンセプト

コマンド

コマンドは、システム・レベルでの sbt の構成要素で、ユーザや IDE とのやり取りを捕捉する。

便宜的に、各コマンドは State => State への関数だと考えることができる。 sbt において状態 (state) は以下を表す。

  1. ビルド構造 (build.sbt など)
  2. ディスク (ソースコード、JAR 成果物など)

そのため、コマンドは通常ビルド構造もしくはディスクを変更する。例えば、set コマンドはセッティングを適用してビルド構造を変更する。

> set name := "foo"

act コマンドは、compile のようなタスクをコマンドに持ち上げる:

> compile

コンパイルはディスクからの読み込みを行い、アウトプットをディスクに書き込むか画面にエラーメッセージを表示する。

コマンドは逐次処理される

状態は 1つしか存在しないものなので、コマンドは 1つづつ実行されるという特徴がある。

command

一部例外もあるが、基本的にコマンドは逐次実行される。メンタルモデルとしては、コマンドはカフェで注文を受け取るレジ係で、注文の順番に処理が行われると考えることができる。

タスクは並列実行される

前述の通り、act コマンドはタスクをコマンドのレベルに持ち上げる。その際に act コマンドは、集約されたサブプロジェクトにタスクを転送して、独立したタスクを並列実装する。

同様に、セッション起動時に実行される reload コマンドはセッティングを並列に初期化する。

act

sbt server の役割

sbt server は、コマンドラインもしくは Build Server Protocol と呼ばれるネットワーク API 経由からコマンドを受け取ることができるサービスだ。この機構によって、ビルドユーザと IDE が同一の sbt セッションを共有することができる。

クロスビルド

同じソースファイルの集合から複数のターゲットに対してビルドすることをクロスビルドと呼ぶ。これは、複数の Scala リリースを対象とする Scala クロスビルド、JVM、Scala.JS、Scala Native を対象とするプラットフォーム・クロスビルド、Spark バージョンのような仮想軸を含む。

クロスビルドされたライブラリの使用

複数の Scala バージョンに対してビルドされたライブラリを使うには ModuleID の最初の % を 2重に %% とする。これは、依存性の名前に Scala ABI (アプリケーション・バイナリ・インターフェイス) サフィックスを付随するという sbt への指示となる。具体例を用いて説明すると:

libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.4"

現行 Scala バージョンが Scala 3.x であるならば、上の例は以下と等価となる:

libraryDependencies += "org.typelevel" % "cats-effect_3" % "3.5.4"

セットアップに関しては、cross building setup 参照。

歴史的経緯

Scala の初期時代 (Scala 2.9 以前) は、Scala 標準ライブラリがパッチレベルでもバイナリ互換性を保たなかったため、新しい Scala バージョンがリリースされるたびに全ライブラリが、新しい Scala バージョンに対して再リリースされる必要があった。そのため、ライブラリのユーザ側は、自分が使う Scala バージョンに互換の特定のライブラリのバージョンを選ぶ必要があった。

Scala 2.9.x 以降も Scala 標準ライブラリはマイナーレベルでの互換性を持たなかったため、Scala 2.10.x に対してコンパイルされたライブラリは 2.11.x と互換性を持たなかった。

これらの問題の対策として、sbt は以下の特徴を持つクロスビルド機構を開発した:

  • 同じソース・ファイルの集合から、複数の Scala バージョンに対してコンパイルできる
  • Maven のアーティファクト名に ABI バージョン (_2.12 など) を付随するという慣例を定義した
  • この機構は Scala.JS その他のプラットフォームもサポートするよう拡張された

Project matrix

sbt 2.x introduces project matrix, which enables cross building to happen in parallel.

organization := "com.example"
scalaVersion := "3.7.3"
version      := "0.1.0-SNAPSHOT"

lazy val core = (projectMatrix in file("core"))
  .settings(
    name := "core"
  )
  .jvmPlatform(scalaVersions = Seq("3.7.3", "2.13.17"))

セットアップに関しては、cross building setup 参照。

sbt クエリ

sbt 2.x はスラッシュ構文を拡張してサブプロジェクトの集約を可能とする:

act ::= [ query / ] [ config / ] [ in-task / ] ( taskKey | settingKey )

言い換えると、sbt クエリはサブプロジェクト軸の新しい書き方だと言える。

サブプロジェクトの参照

サブプロジェクトの参照は、サブプロジェクトを選択するクエリとしてそのまま使える:

build.sbt example 1

scalaVersion := "3.7.3"

lazy val foo = project

上のようなビルドがあるとき、sbt 1.x と同様の構文を使って foo サブプロジェクトのテストを実行できる:

foo/test

... ワイルドカード

... ワイルドカードはどの文字列にもマッチして、他の文字や数字とも組み合わせて、ルートの集約リストを絞り込むことができる。例えば、以下のようにして foo で始まる全サブプロジェクトのテストを実行することができる:

foo.../test

備考: * vs ...

sbt クエリは、直感的に分かりやすそうな * (アスタリスク) でなく、意図的に ... (ドット・ドット・ドット) を採用する。これは * がシェル環境においてワイルドカードとして使われることが多いからだ。そのため、常にクォートで囲む必要があり、また */test をクォートし忘れると src/test のようなディレクトリにマッチしてしまう可能性が高い。

@scalaBinaryVersion パラメータ

@scalaBinaryVersion パラメータは、サブプロジェクトの scalaBinaryVersion セッティングにマッチする。

Example

val toolkitV = "0.5.0"
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV

lazy val foo = projectMatrix
  .settings(
    libraryDependencies += toolkit,
  )
  .jvmPlatform(scalaVersions = Seq("3.7.3", "2.13.17"))

lazy val bar = projectMatrix
  .settings(
    libraryDependencies += toolkit,
  )
  .jvmPlatform(scalaVersions = Seq("3.7.3", "2.13.17"))

例えば、全ての 3.x サブプロジェクトのテストを以下のように実行できる:

...@scalaBinaryVersion=3/test

以下のように、ターミナル上からも使うことができる:

$ sbt ...@scalaBinaryVersion=3/test
[info] entering *experimental* thin client - BEEP WHIRR
[info] terminate the server with `shutdown`
> ...@scalaBinaryVersion=3/test
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[info] No tests to run for Test / testQuick
[info] compiling 1 Scala source to /tmp/foo/target/out/jvm/scala-3.6.4/foo/test-backend ...
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[info] No tests to run for bar / Test / testQuick
example.ExampleSuite:
  + Scala version 0.003s
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1

projectMatrix を使っていると集約サブプロジェクトの絞り込みが欲しくなる場面が多々あるが、sbt クエリがこれが解決する。

キャッシュ化

sbt 2.0 は、ローカル/リモートの両方で使えるハイブリッドなキャッシュ・システムを導入し、タスク結果をローカルのディスクもしくは、Bazel 互換のリモート・キャッシュにキャッシュ化することができる。 過去のリリースを通じて、sbt は update のキャッシュ、差分コンパイルなど、様々なキャッシュ化を実装してきたが、sbt 2.x のキャッシュはいくつかの理由により、大きな変化となる:

  1. 自動化。sbt 1.x ではプラグイン作者がタスク実装内でキャッシュ化関数を呼び出す必要があったが、sbt 2.x キャッシュはタスク・マクロに組み込まれているため、自動化されている。
  2. マシン・ワイド。sbt 2.x のディスク・キャッシュはマシン上の全ビルドから共有されている。
  3. リモート対応。sbt 2.x では、キャッシュのストレージは独立して設定可能なため、全てのキャッシュ可能なタスクは自動的にリモート・キャッシュに対応している。

キャッシュ化の全体目標は、コード量が増えるにつれて増加していくビルドとテスト時間の成長率を現状よりも平たく抑えることにある。そのため、高速化の比率はコード量などにもよるが、現行のテストに 10分以上かかるようなビルドの場合、5倍から 20倍を狙っていくことも可能となってくる。

キャッシュ化の基本

ビルドプロセスがあたかも (A1, A2, A3, ...) というインプットを受け取り、 (R1, List(O1, O2, O3, ...)) というアウトプットを返す純粋関数であるかのように扱うというのがキャッシュ化の基本的な考えだ。例えば、ソース・ファイルのリストと Scala バージョンを受け取って、*.jar ファイルを生成することができる。もし仮定が成立するなら、同一のインプットに対してはアウトプットの JAR を全員に対してメモ化できる。メモ化された JAR を使うほうが、Scala コンパイルのような実際のタスクを実行するよりも高速であることがこのような技法を使うメリットとなる。

密閉ビルド

「純粋関数としてのビルド」のメンタルモデルとして、ビルド界隈のエンジニアは密閉ビルド (hermetic build)、つまり砂漠の真ん中に置かれたコンテナの中で時計も Internet も無い状態で行われるビルドという用語を使うことがある。そのような状態で JAR ファイルを生成することができれば、その JAR ファイルはどのマシンと共有しても大丈夫なはずだ。何故時計の話が出てきたのだろうか? それは、JAR ファイルが仕様としてタイムスタンプを捕捉するため、毎回少しづつ違った JAR を生成するからだ。これを回避するために、「密閉な」ビルドツールはいつビルドが実行されてもタイムスタンプを 2010-01-01 に上書きする慣習がある。

逆に、不安定なインプットを捕捉してしまったビルドは「密閉性を壊した」、または「非密閉である」という。密閉性が壊れるもう 1つのよくあるパターンは、インプットもしくはアウトプットで絶対パスを捕捉してしまうことだ。時としてはパスはマクロ経由で JAR に入ってしまう事があるので、バイトコードの差を検査しないと発見できないかもしれない。

自動的キャッシュ化

以下に、自動的キャッシュ化を具体例を用いて解説する:

val someKey = taskKey[String]("something")

someKey := name.value + version.value + "!"

sbt 2.x では、このタスクの結果は、nameversion という 2つのセッティングの値に基づいて自動的にキャッシュ化される。最初にこのタスクが実行されたときには、オンサイトで実行され、2回目以降はディスク・キャッシュの値が用いられる:

sbt:demo> show someKey
[info] demo0.1.0-SNAPSHOT!
[success] elapsed time: 0 s, cache 0%, 1 onsite task
sbt:demo> show someKey
[info] demo0.1.0-SNAPSHOT!
[success] elapsed time: 0 s, cache 100%, 1 disk cache hit

キャッシュ化はシリアライゼーション問題と同等に困難だ

キャッシュの自動化に参加するためには、(nameversion などの) インプットキーは sjsonnew.HashWriter 型クラスの given、戻り値は sjsonnew.JsonFormat 型クラスの given を提供する必要がある。Contraband を使って sjson-new のコーデックを生成することができる。

ファイルのキャッシュ化

ファイル (java.io.File など) のキャッシュ化は特別な配慮を必要とするが、それは技術的に難しいからというよりは、ファイルが関わったときに発生する曖昧さと思い込みによる所が大きい。一言に「ファイル」といっても、実は様々なことを意味する:

  1. 取り決められた場所からの相対パス
  2. 現物化された実際のファイル
  3. コンテンツ・ハッシュ値などの一意的なファイルの証明

厳密には、File はファイルパスのみを指すため、target/a/b.jar というようなファイル名のみを復元すればいい。これは、下流のタスクが target/a/b.jar というファイルがファイル・システムに存在すると思い込んでいた場合失敗してしまう。これを明瞭化しつつ、絶対パスを回避するために、sbt 2.x は 3つのそれぞれの場合に対して別々の型を提供する。

  • xsbti.VirtualFileRef は、ただの相対パスのみを表すのに用いられ、これは文字列を渡すのと等価だ
  • xsbti.VirtualFile は、コンテンツを持つ現物化されたファイルを表し、仮想ファイルもしくはディスク上のファイルであってもいい

しかしながら、密閉ビルドという観点から見ると、ファイルのリストを表すのにどちらも優れていない。ファイル名のみを持っていてもファイルが同一性を保証できないし、ファイルの全コンテンツを持ち回すのは JSON などに使うには非効率的だ。

ここで謎の 3つ目方法、ファイルの一意的な証明が便利になる。HashedVirtualFileRef は、相対パスの他にも SHA-256 コンテンツ・ハッシュとファイル・サイズを追跡する。これは、JSON に簡単にシリアライズできるが、特定のファイルを参照することもできる。

ファイル作成の作用

ファイルを生成するが、VirtualFile を戻り値の型として使わないタスクがたくさんある。例えば、sbt 1.x では compileAnalysis を返し、*.class ファイルの生成は「副作用」として行われる。

キャッシュ化に参加するためには、これらの作用も後に残しておきたいものとして宣言する必要がある。

someKey := {
  val conv = fileConverter.value
  val out: java.nio.file.Path = createFile(...)
  val vf: xsbti.VirtualFile = conv.toVirtualFile(out)
  Def.declareOutput(vf)
  vf: xsbti.HashedVirtualFileRef
}

リモート・キャッシュ

オプションとして、ビルドを拡張してローカルでのディスク・キャッシュの他にリモート・キャッシュも使うことができる。リモート・キャッシュは、複数のマシンがビルドの成果物やアウトプットを共有することでビルドの性能を向上できる。

自分のプロジェクトもしくは会社に 10名ぐらいのメンバーがいると想像してほしい。毎朝、その 10名の書いた変更を git pull で取り込んで、書かれたコードをビルドする必要がある。プロジェクトが順調にいけば、コード量は時間とともに増加していき、一日のうち他人のコードをビルドする時間の割合も増えていく。これは、いずれチーム規模とコード量の制限要因となる。リモート・キャッシュは、CI システムにキャッシュを補給させ自分たちは成果物やタスクのアウトプットをダウンロードできるようにすることでこの傾向を逆転させる。

sbt 2.x は、Bazel 互換の gRPC インターフェイスを実装するため、オープンソース及び商用の複数のバックエンド・システムと統合する。詳細は、リモート・キャッシュのセットアップ参照。

レファレンス

キャッシュ・タスクのレファレンスガイドも参照。

レファレンス

sbt

基本的な導入としては、sbt 入門ガイドの基本タスクをまず読んでみてほしい。

書式

sbt
sbt command args
sbt --server
sbt --script-version

説明

sbt は最初は Scala と Java のために作られたシンプルなビルド・ツールだ。sbt はサブプロジェクト、様々な依存性、カスタムタスクなどを宣言することで高速かつ再現性の高いビルドが得られることを保証する。

sbt runner と sbt server

  • sbt runner は sbt という名前のシステム・シェル・スクリプトで、Windows では sbt.bat と呼ばれる。これは、どのバージョンの sbt でも実行することが可能で、これは「sbt という名前のシェル・スクリプト」とも呼ばれる。
    • sbt 2.x が検知されると、sbt runner は、典型的には GraalVM ネイティブで実装されたクライアント・プログラムである sbtn を用いて、クライアント・モードで実行する。
    • sbt runner は sbt ランチャーという、全てのバージョンの sbt を起動できるランチャーを実行する。
    • sbt をインストールした場合、インストールされるのは sbt runner だ。
  • sbt server は、sbt の本体で、実際のビルドツールだ。
    • sbt のバージョンは、それぞれのワーキング・ディレクトリ内にある project/build.properties によって決定される。
    • sbt server は、sbtn、ネットワーク API、もしくは独自の sbt シェルのいずれかからコマンドを受け取る。
sbt.version=2.0.0-RC6

この機構によってビルドを特定のバージョンの sbt に設置することができ、プロジェクトで作業する人全員が、マシンにインストールされた sbt runner に関わらず同一のビルド意味論を共有できるようになる。

このような分割があるため、機能の一部は sbt runner や sbtn のレベルで実装され、その他の機能は sbt server レベルで実装される。

sbt コマンド

コマンドに関する備考

sbt には、サブプロジェクトのレベルで動作するタスク (compile など) とビルド定義そのものを操作することも可能な狭義のコマンド (set など) がある。

しかし、act コマンドによってセッティングやタスクもコマンドに持ち上げることが可能なため「sbt シェルに打ち込むことができるもの全て」を広義のコマンドとしても解釈できる。 詳細はコマンドのコンセプトのページを参照。

サブプロジェクト・レベルのタスク

  • clean 生成されたファイル (target ディレクトリ) を削除する。
  • publish publishTo セッティングで指定されたリポジトリにJAR ファイルなどのアーティファクトを公開する。
  • publishLocal JAR ファイルなどのアーティファクトをローカルの Ivy リポジトリに公開する。
  • update ライブラリ依存性の解決と取得を行う。

コンフィギュレーション・レベルのタスク

コンフィギュレーション・レベルのタスクは、コンフィギュレーションに関連付けされたタスクだ。例えば、compileCompile/compile と等価であり、(Compile コンフィギュレーションで管理される) main のソースコードをコンパイルする。Test/compile は (Test コンフィギュレーションで管理される) テストのソースコードをコンパイルする。Compile コンフィギュレーションのほとんどのタスクはTest コンフィギュレーション内に対応するものがあり、Test/ とプレフィックスを付けることで実行できる。

  • compile (src/main/scala ディレクトリの中の) main のソースをコンパイルする。Test/compile は、 (src/test/scala ディレクトリの中の) テストのソースをコンパイルする。

  • console コンパイルされたソース、lib ディレクトリ内の全ての JAR、マネージ依存性を含んだクラスパスを用いて Scala インタプリタを起動する。sbt へ戻るには、:quit、Ctrl+D (Unix)、もしくは Ctrl+Z (Windows) と打ち込む。同様に、Test/console はテストクラスとクラスパスを用いてインタプリタを起動する。

  • doc scaladoc を用いて src/main/scala/ 内の Scala ソースの API ドキュメンテーションを生成する。Test/doc は、src/test/scala 内のソースファイルのための API ドキュメンテーションを生成する。

  • package src/main/resources 内のファイルと src/main/scala からコンパイルされたクラスを含む JAR ファイルを作成する。Test/package は、src/test/resources 内のファイルと src/test/scala からコンパイルされたクラスを含む JAR ファイルを作成する。

  • packageDoc src/main/scala 内の Scala ソースより生成された API ドキュメンテーションを含む JAR ファイルを作成する。Test/packageDoc は、src/test/scala 内のテスト・ソースより生成された API ドキュメンテーションを含む JAR ファイルを作成する。

  • packageSrc 全ての main のソースファイルとリソースを含む JAR ファイルを作成する。パッケージは src/main/scala および src/main/resources からの相対パスとなる。同様に、Test/packageSrc はテストソースとリソースをパッケージ化する。

  • run <引数>* サブプロジェクトのメインクラスを sbt と同じ JVM 上から実行する。引数はそのままメインクラスに渡される。

  • runMain <メインクラス> <引数>* サブプロジェクトから指定されたメインクラスを sbt と同じ JVM 上から実行する。引数はそのままメインクラスに渡される。

  • test <test>* 引数で指定されたテスト (省略された場合は全てのテスト) を以下の条件で実行する:

    1. 未だ実行されていない、もしくは
    2. 前回実行されたときに失敗した、もしくは
    3. 前に成功してから間接的依存性のいずれかが再コンパイルされた
      * は、テスト名のワイルドカードとして解釈される。
  • testFull テストのコンパイル時に検知された全てのテストを実行する。

一般コマンド

  • shutdown sbt server をシャットダウンして現行の sbt セッションを終了する。
  • exit or quit End the current interactive session or build. Additionally, Ctrl+D (Unix) or Ctrl+Z (Windows) will exit the interactive prompt.
  • help <command> Displays detailed help for the specified command. If the command does not exist, help lists detailed help for commands whose name or description match the argument, which is interpreted as a regular expression. If no command is provided, displays brief descriptions of the main commands. Related commands are tasks and settings.
  • projects [add|remove <URI>] List all available projects if no arguments provided or adds/removes the build at the provided URI.
  • Watch command ~ <command> Executes the project specified action or method whenever source files change.

  • < filename Executes the commands in the given file. Each command should be on its own line. Empty lines and lines beginning with '#' are ignored

  • A ; B Execute A and if it succeeds, run B. Note that the leading semicolon is required.

  • eval <Scala-expression> Evaluates the given Scala expression and returns the result and inferred type. This can be used to set system properties, as a calculator, to fork processes, etc ... For example:

    > eval System.setProperty("demo", "true")
    > eval 1+1
    > eval "ls -l" !
    

Commands for managing the build definition

  • reload [plugins|return] If no argument is specified, reloads the build, recompiling any build or plugin definitions as necessary. reload plugins changes the current project to the build definition project (in project/). This can be useful to directly manipulate the build definition. For example, running clean on the build definition project will force snapshots to be updated and the build definition to be recompiled. reload return changes back to the main project.

  • set <setting-expression> Evaluates and applies the given setting definition. The setting applies until sbt is restarted, the build is reloaded, or the setting is overridden by another set command or removed by the session command.

  • session <command> Manages session settings defined by the set command. It can persist settings configured at the prompt.

  • inspect <setting-key> Displays information about settings, such as the value, description, defining scope, dependencies, delegation chain, and related settings.

sbt runner and launcher

When launching the sbt runner from the system shell, various system properties or JVM extra options can be specified to influence its behaviour.

sbt JVM options and system properties

If the JAVA_OPTS and/or SBT_OPTS environment variables are defined when sbt starts, their content is passed as command line arguments to the JVM running sbt server.

If a file named .jvmopts exists in the current directory, its content is appended to JAVA_OPTS at sbt startup. Similarly, if .sbtopts and/or /etc/sbt/sbtopts exist, their content is appended to SBT_OPTS. The default value of JAVA_OPTS is -Dfile.encoding=UTF8.

You can also specify JVM system properties and command line options directly as sbt arguments: any -Dkey=val argument will be passed as-is to the JVM, and any -J-Xfoo will be passed as -Xfoo.

See also sbt --help for more details.

sbt JVM heap, permgen, and stack sizes

If you find yourself running out of permgen space or your workstation is low on memory, adjust the JVM configuration as you would for any java application.

For example a common set of memory-related options is:

export SBT_OPTS="-Xmx2048M -Xss2M"
sbt

Or if you prefer to specify them just for this session:

sbt -J-Xmx2048M -J-Xss2M

Boot directory

sbt runner is just a bootstrap, the actual sbt server, Scala compiler and standard library are by default downloaded to the shared directory \$HOME/.sbt/boot/.

To change the location of this directory, set the sbt.boot.directory system property. A relative path will be resolved against the current working directory, which can be useful if you want to avoid sharing the boot directory between projects. For example, the following uses the pre-0.11 style of putting the boot directory in project/boot/:

sbt -Dsbt.boot.directory=project/boot/

Terminal encoding

The character encoding used by your terminal may differ from Java's default encoding for your platform. In this case, you will need to specify the file.encoding=<encoding> system property, which might look like:

export JAVA_OPTS="-Dfile.encoding=Cp1252"
sbt

HTTP/HTTPS/FTP Proxy

On Unix, sbt will pick up any HTTP, HTTPS, or FTP proxy settings from the standard http_proxy, https_proxy, and ftp_proxy environment variables. If you are behind a proxy requiring authentication, you need to pass some supplementary flags at sbt startup. See JVM networking system properties for more details.

For example:

sbt -Dhttp.proxyUser=username -Dhttp.proxyPassword=mypassword

On Windows, your script should set properties for proxy host, port, and if applicable, username and password. For example, for HTTP:

sbt -Dhttp.proxyHost=myproxy -Dhttp.proxyPort=8080 -Dhttp.proxyUser=username -Dhttp.proxyPassword=mypassword

Replace http with https or ftp in the above command line to configure HTTPS or FTP.

Other system properties

The following system properties can also be passed to sbt runner:

-Dsbt.banner=true

Show a welcome banner advertising new features.

-Dsbt.ci=true

Default false (unless then env var BUILD_NUMBER is set). For continuous integration environments. Suppress supershell and color.

-Dsbt.client=true

Run the sbt client.

-Dsbt.color=auto

  • To turn on color, use always or true.
  • To turn off color, use never or false.
  • To use color if the output is a terminal (not a pipe) that supports color, use auto.

-Dsbt.coursier.home=$HOME/.cache/coursier/v1

Location of the Coursier artifact cache, where the default is defined by Coursier cache resolution logic. You can verify the value with the command csrCacheDirectory.

-Dsbt.genbuildprops=true

Generate build.properties if missing. If unset, this defers to sbt.skip.version.write.

-Dsbt.global.base=$HOME/.sbt/

The directory containing global settings and plugins.

-Dsbt.override.build.repos=true

If true, repositories configured in a build definition are ignored and the repositories configured for the launcher are used instead.

-Dsbt.repository.config=$HOME/.sbt/repositories

A file containing the repositories to use for the launcher. The format is the same as a [repositories] section for a sbt launcher configuration file. This setting is typically used in conjunction with setting sbt.override.build.repos to true.

sbt update

See library depdency basics in the Getting Started guide to learn about the basics.

書式

sbt [query / ] update

説明

sbt uses Coursier to implement library management, also known as a package manager in other ecosystems. The general idea of library management is that you can specify external libraries you would like to use in your subprojects, and the library management system would:

  • Check if such versions exists in the listed repositories
  • Look for the transitive dependencies (i.e. the libraries used by the libraries)
  • Attempt to resolve version conflicts, if any
  • Download the artifacts, such as JAR files, from the repositories

Dependencies

Declaring a dependency looks like:

libraryDependencies += groupID %% artifactID % revision

or

libraryDependencies += groupID %% artifactID % revision % configuration

Also, several dependencies can be declared together:

libraryDependencies ++= Seq(
  groupID %% artifactID % revision,
  groupID %% otherID % otherRevision
)

If you are using a dependency that was built with sbt, double the first % to be %%:

libraryDependencies += groupID %% artifactID % revision

This will use the right JAR for the dependency built with the version of Scala that you are currently using. If you get an error while resolving this kind of dependency, that dependency probably wasn't published for the version of Scala you are using. See Cross building for details.

versionScheme and eviction errors

sbt allows library authors to declare the version semantics using the versionScheme setting:

// Semantic Versioning applied to 0.x, as well as 1.x, 2.x, etc
versionScheme := Some(VersionScheme.EarlySemVer)

When Coursier finds multiple versions of a library, for example Cats Effect 2.x and Cats Effect 3.0.0-M4, it often resolves the conflict by removing the older version from the graph. This process is colloquially called eviction, like "Cats Effect 2.2.0 was evicted."

This would work if the new tenant is binary compatible with Cats Effect 2.2.0. In this case, the library authors have declared that they are not binary compatible, so the eviction was actually unsafe. An unsafe eviction would cause runtime issues such as ClassNotFoundException. Instead Coursier should've failed to resolve.

lazy val use = project
  .settings(
    name := "use",
    libraryDependencies ++= Seq(
      "org.http4s" %% "http4s-blaze-server" % "0.21.11",
      "org.typelevel" %% "cats-effect" % "3.0.0-M4",
    ),
  )

sbt performs this secondary compatibility check after Coursier returns a candidate:

[error] stack trace is suppressed; run last use / update for the full output
[error] (use / update) found version conflict(s) in library dependencies; some are suspected to be binary incompatible:
[error]
[error]   * org.typelevel:cats-effect_2.12:3.0.0-M4 (early-semver) is selected over {2.2.0, 2.0.0, 2.0.0, 2.2.0}
[error]       +- use:use_2.12:0.1.0-SNAPSHOT                        (depends on 3.0.0-M4)
[error]       +- org.http4s:http4s-core_2.12:0.21.11                (depends on 2.2.0)
[error]       +- io.chrisdavenport:vault_2.12:2.0.0                 (depends on 2.0.0)
[error]       +- io.chrisdavenport:unique_2.12:2.0.0                (depends on 2.0.0)
[error]       +- co.fs2:fs2-core_2.12:2.4.5                         (depends on 2.2.0)
[error]
[error]
[error] this can be overridden using libraryDependencySchemes or evictionErrorLevel

This mechanism is called the eviction error.

Opting out of the the eviction error

If the library authors have declared the compatibility breakage, but if you want to ignore the strict check (often for scala-xml), you can write this in project/plugins.sbt and build.sbt:

libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always

To ignore all eviction errors:

evictionErrorLevel := Level.Info

Resolvers

sbt uses the standard Maven Central repository by default. Declare additional repositories with the form:

resolvers += name at location

For example:

libraryDependencies ++= Seq(
    "org.apache.derby" % "derby" % "10.4.1.3",
    "org.specs" % "specs" % "1.6.1"
)

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

sbt can search your local Maven repository if you add it as a repository:

resolvers += Resolver.mavenLocal

Override default resolvers

resolvers configures additional, inline user resolvers. By default, sbt combines these resolvers with default repositories (Maven Central and the local Ivy repository) to form externalResolvers. To have more control over repositories, set externalResolvers directly. To only specify repositories in addition to the usual defaults, configure resolvers.

For example, to use the Sonatype OSS Snapshots repository in addition to the default repositories,

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

To use the local repository, but not the Maven Central repository:

externalResolvers := Resolver.combineDefaultResolvers(resolvers.value.toVector, mavenCentral = false)

Override all resolvers for all builds

The repositories used to retrieve sbt, Scala, plugins, and application dependencies can be configured globally and declared to override the resolvers configured in a build or plugin definition. There are two parts:

  1. Define the repositories used by the launcher.
  2. Specify that these repositories should override those in build definitions.

The repositories used by the launcher can be overridden by defining ~/.sbt/repositories, which must contain a [repositories] section with the same format as the Launcher configuration file. For example:

[repositories]
local
my-maven-repo: https://example.org/repo
my-ivy-repo: https://example.org/ivy-repo/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]

A different location for the repositories file may be specified by the sbt.repository.config system property in the sbt startup script. The final step is to set sbt.override.build.repos to true to use these repositories for dependency resolution and retrieval.

Exclude Transitive Dependencies

In certain cases a transitive dependency should be excluded from all dependencies. This can be achieved by setting up ExclusionRules in excludeDependencies.

excludeDependencies ++= Seq(
  // commons-logging is replaced by jcl-over-slf4j
  ExclusionRule("commons-logging", "commons-logging")
)

To exclude certain transitive dependencies of a dependency, use the excludeAll or exclude methods. The exclude method should be used when a pom will be published for the project. It requires the organization and module name to exclude. For example,

libraryDependencies += 
  ("log4j" % "log4j" % "1.2.15").exclude("javax.jms", "jms")

Explicit URL

If your project requires a dependency that is not present in a repository, a direct URL to its jar can be specified as follows:

libraryDependencies += "slinky" % "slinky" % "2.1" from "https://slinky2.googlecode.com/svn/artifacts/2.1/slinky.jar"

The URL is only used as a fallback if the dependency cannot be found through the configured repositories. Also, the explicit URL is not included in published metadata (that is, the pom or ivy.xml).

Disable Transitivity

By default, these declarations fetch all project dependencies, transitively. In some instances, you may find that the dependencies listed for a project aren't necessary for it to build. Projects using the Felix OSGI framework, for instance, only explicitly require its main jar to compile and run. Avoid fetching artifact dependencies with either intransitive() or notTransitive(), as in this example:

libraryDependencies += ("org.apache.felix" % "org.apache.felix.framework" % "1.8.0").intransitive()

Classifiers

You can specify the classifier for a dependency using the classifier method. For example, to get the jdk15 version of TestNG:

libraryDependencies += ("org.testng" % "testng" % "5.7").classifier("jdk15")

For multiple classifiers, use multiple classifier calls:

libraryDependencies += 
  "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-windows" classifier "natives-linux" classifier "natives-osx"

To obtain particular classifiers for all dependencies transitively, run the updateClassifiers task. By default, this resolves all artifacts with the sources or javadoc classifier. Select the classifiers to obtain by configuring the transitiveClassifiers setting. For example, to only retrieve sources:

transitiveClassifiers := Seq("sources")

Download Sources

Downloading source and API documentation jars is usually handled by an IDE plugin. These plugins use the updateClassifiers and updateSbtClassifiers tasks, which produce an Update-Report referencing these jars.

To have sbt download the dependency's sources without using an IDE plugin, add withSources() to the dependency definition. For API jars, add withJavadoc(). For example:

libraryDependencies += 
  ("org.apache.felix" % "org.apache.felix.framework" % "1.8.0").withSources().withJavadoc()

Note that this is not transitive. Use the update*Classifiers tasks for that.

sbt compile

書式

sbt [query / ] compile
sbt [query / ] Test / compile

説明

The compile task compiles the selected subprojects and their subproject dependencies. Since sbt 2.x, the compiled artifacts are cached automatically.

Compiling Scala code with the raw Scala compiler has been slow, so significant poriton of sbt's development efforts deal with various strategies for speeding up compilation.

Reduce the overhead of restarting the compiler

sbt server stays up in the background, allowing Scala compilation to run the same Java virtual machine (JVM). Keeping the JVM warm makes compilation significantly faster because it takes a long time to classload the compiler and for the Just-in-Time compiler to optimize it.

Incremental compilation

When a source file A.scala is modified, sbt goes to great effort to minimize the other source files recompiled due to A.scala's change. This process of tracking dependencies between the language constructs and recompiling only the required sources is called incremental compilation.

(Remote) caching

In sbt 2.x, compiled artifacts are not only cached across the sessions and builds, but can optionally be cached across different machines using Bazel-compatible remote cache. See Caching for details.

Test / compile

Scoping the compile task with a configuration, like Test / compile will compile the test sources and their source dependencies.

Compilation settings

scalaVersion

The version of Scala used for compilation.

scalaVersion := "3.7.3"

scalacOptions

Options for the Scala compiler.

Compile / scalacOptions += "-Werror"

javacOptions

Options for the Java compiler.

Compile / javacOptions ++= List("-Xlint", "-Xlint:-serial")

sbt run

書式

sbt [query / ] run [args]

説明

The run task provides a means for running the user program.

In sbt 1.x and earlier, run task ran the user program in the same Java virtual machine (JVM) as the sbt server. sbt 2.x implements client-side run: the run task creates a sandbox environment that contains the program, sends the information back to sbtn, and sbtn launches the user program in a fresh JVM.

Motivations

There are several motivations for the client-side run.

  1. sys.exit support. User code can call sys.exit, which normally shuts down the JVM. In sbt 1.x, we needed to trap these sys.exit calls to prevent run from shutting down the sbt session, using the JDK SecurityManager; however, TrapExit was dropped in sbt 1.6.0 (2021) since JDK 17 deprecated SecurityManager feature. Because client-side run runs the user program in its own JVM, it can call sys.exit.
  2. Isolation. User code can also start threads, or otherwise allocate resources that can be left running after the main method returns. Running user code in a separate JVM gives isolation between the sbt server and the user code.
  3. sbt server availability. Since the program will run outside of the sbt server, it can become available to the more requests by other clients, for example test or IDE integration.

sbt test

書式

sbt [query / ] test [testname1 testname2] [ -- options ]

説明

The test task provides a means for compiling and running the tests.

By default, the test task in sbt 2.x:

  1. Subproject parallelism. Performs compilation of the relevant subprojects in parallel, specified by the query.
  2. Test suite parallelism. Maps discovered test suites, to tasks and executes them in parallel.
  3. Incremental test. Runs only the tests that either failed in the previous run, never run, or if sbt detects changes in the test or its dependencies.
  4. Cached. The test result is cached machine-wide, and optionally remote cached.

The standard source locations for testing are:

  • Scala sources in src/test/scala/
  • Java sources in src/test/java/
  • Resources for the test classpath in src/test/resources/

The resources may be accessed from tests by using the getResource methods of java.lang.Class or java.lang.ClassLoader.

Test interfaces

sbt defines the common interface for JVM-based test frameworks, allowing automatic test suite discovery and parallel execution. By default sbt integrates with MUnit, ScalaTest, Hedgehog, ScalaCheck, Specs2, Weaver, ZIO Test, and JUnit 4; this means you only need to add the test framework to the classpath to work with sbt. For example, MUnit may be used by declaring it as a libraryDependency:

lazy val munit = "org.scalameta" %% "munit" % "1.2.0"

libraryDependencies += munit % Test

In the above, Test denotes the Test configuration, and means that MUnit will only be on the test classpath and it isn't needed by the main sources.

JUnit

Support for JUnit 5 is provided by sbt-jupiter-interface. To add JUnit Jupiter support into your project, add the jupiter-interface dependency in your project's main build.sbt file.

libraryDependencies += "com.github.sbt.junit" % "jupiter-interface" % "0.15.1" % Test

and the sbt-jupiter-interface plugin to your project/plugins.sbt:

addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.15.1")

Support for JUnit 4 is provided by junit-interface. Add the junit-interface dependency in your project's main build.sbt file.

libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" % Test

Test filtering

In sbt 2.x, the test task accepts a whitespace separated list of test names to run. For example:

> test example.ExampleSuite example.ExampleSuite2

Here's an example output:

> test example.ExampleSuite example.ExampleSuite2
[info] compiling 1 Scala source to /tmp/foo/target/out/jvm/scala-3.7.2/foo/backend ...
[info] compiling 2 Scala sources to /tmp/foo/target/out/jvm/scala-3.7.2/foo/test-backend ...
example.ExampleSuite:
  + addition 0.003s
example.ExampleSuite2:
  + subtraction 0.003s
[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] elapsed time: 3 s, cache 49%, 25 disk cache hits, 26 onsite tasks

It supports wildcards as well:

> test *Example*

Incremental testing

In addition to the explicit filter, the test task runs only the tests that satisfy one of the following conditions are run:

  • The tests that failed in the previous run
  • The tests that were not run before
  • The tests that have one or more transitive dependencies, maybe in a different project, recompiled.

Full testing

To run, uncached full tests, like sbt 1.x, use the testFull task.

Other tasks

Tasks that are available for main sources are generally available for test sources, but are prefixed with Test / on the command line and are referenced in Scala code with Test / as well. These tasks include:

  • Test / compile
  • Test / console
  • Test / consoleQuick
  • Test / run
  • Test / runMain

See sbt run for details on these tasks.

Output

By default, logging is buffered for each test source file until all tests for that file complete. This can be disabled by setting logBuffered:

Test / logBuffered := false

Test Reports

By default, sbt will generate JUnit XML test reports for all tests in the build, located in the target/test-reports directory for a project. This can be disabled by disabling the JUnitXmlReportPlugin

val myProject = (project in file(".")).disablePlugins(plugins.JUnitXmlReportPlugin)

Options

Test framework arguments

Arguments to the test framework may be provided on the command line to the test tasks following a -- separator. For example:

> test org.example.MyTest -- -verbosity 1

To specify test framework arguments as part of the build, add options constructed by Tests.Argument:

Test / testOptions += Tests.Argument("-verbosity", "1")

To specify them for a specific test framework only:

Test / testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "1")

Setup and Cleanup

Specify setup and cleanup actions using Tests.Setup and Tests.Cleanup. These accept either a function of type () => Unit or a function of type ClassLoader => Unit. The variant that accepts a ClassLoader is passed the class loader that is (or was) used for running the tests. It provides access to the test classes as well as the test framework classes.

Note

When forking, the ClassLoader containing the test classes cannot be provided because it is in another JVM. Only use the () => Unit variants in this case.

Examples:

Test / testOptions += Tests.Setup( () => println("Setup") )
Test / testOptions += Tests.Cleanup( () => println("Cleanup") )
Test / testOptions += Tests.Setup( loader => ... )
Test / testOptions += Tests.Cleanup( loader => ... )

Disable parallel execution of test suites

By default, sbt runs all tasks in parallel and within the same JVM as sbt itself. Because each test suite is mapped to a task, tests are also run in parallel by default. To make tests within a given project execute serially:

Test / parallelExecution := false

Note that tests from different projects may still execute concurrently.

Filter classes

If you want to only run test classes whose name ends with "Test", use Tests.Filter:

Test / testOptions := Seq(Tests.Filter(s => s.endsWith("Test")))

Forking tests

The setting:

Test / fork := true

specifies that all tests will be executed in a single external JVM.

More control over how tests are assigned to JVMs and what options to pass to those is available with testGrouping key.

Control the number of forked JVMs allowed to run at the same time by setting the limit on Tags.ForkedTestGroup tag, which is 1 by default. Setup and Cleanup actions cannot be provided with the actual test class loader when a group is forked.

sbt inspect

書式

sbt inspect [subproject / ] [ config / ] task
sbt inspect actual [subproject / ] [ config / ] task
sbt inspect tree [subproject / ] [ config / ] task

説明

The inspect command provides a means to inspect the task and setting graph. For instace, it can be used to determine which setting should be modified to affect another task.

Value, Description, and Provided By

The first piece of information provided by inspect is the type of a task or the value and type of a setting.

For example,

$ sbt inspect libraryDependencies
[info] Setting: interface scala.collection.immutable.Seq =
  List(org.scala-lang:scala3-library:3.7.2,
       org.typelevel:toolkit:0.1.29,
       org.typelevel:toolkit-test:0.1.29:test)
[info] Description:
[info]  Declares managed dependencies.
[info] Provided by:
[info]  ProjectRef(uri("file:/tmp/aaa/"), "aaa") / libraryDependencies
....

The following section of output is labeled "Provided by". This shows the actual scope where the setting is defined.

This shows that libraryDependencies has been defined on the current project (ProjectRef(uri("file:/tmp/aaa/"), "aaa")).

The Related section of inspect output lists all of the definitions of a key. For example,

> inspect compile
...
[info] Related:
[info]  Test / compile

This shows that in addition to the requested Compile / compile task, there is also a Test / compile task.

Dependencies

Forward dependencies show the other settings (or tasks) used to define a setting (or task). Reverse dependencies go the other direction, showing what uses a given setting. inspect provides this information based on either the requested dependencies or the actual dependencies. Requested dependencies are those that a setting directly specifies. Actual settings are what those dependencies get resolved to. This distinction is explained in more detail in the following sections.

Requested Dependencies

As an example, we'll look at console:

$ sbt inspect console
...
[info] Dependencies:
[info]  Compile / console / initialCommands
[info]  Compile / console / compilers
[info]  Compile / state
[info]  Compile / console / cleanupCommands
[info]  Compile / console / taskTemporaryDirectory
[info]  Compile / console / scalaInstance
[info]  Compile / console / scalacOptions
[info]  Compile / console / fullClasspath
[info]  Compile / fileConverter
[info]  Compile / console / streams

...

This shows the inputs to the console task. We can see that it gets its classpath and options from Compile / console / fullClasspath and Compile / console / scalacOptions. The information provided by the inspect command can thus assist in finding the right setting to change. The convention for keys, like console and fullClasspath, is that the Scala identifier is camel case, while the String representation is lowercase and separated by dashes. The Scala identifier for a configuration is uppercase to distinguish it from tasks like compile and test. For example, we can infer from the previous example how to add code to be run when the Scala interpreter starts up:

> set Compile / console / initialCommands := "import mypackage._"
> console
...
import mypackage._
...

inspect showed that console used the setting Compile / console / initialCommands. Translating the initialCommands string to the Scala identifier gives us initialCommands. compile indicates that this is for the main sources. console / indicates that the setting is specific to console. Because of this, we can set the initial commands on the console task without affecting the consoleQuick task, for example.

Actual Dependencies

inspect actual <scoped-key> shows the actual dependency used. This is useful because delegation means that the dependency can come from a scope other than the requested one. Using inspect actual, we see exactly which scope is providing a value for a setting. Combining inspect actual with plain inspect, we can see the range of scopes that will affect a setting. Returning to the example in Requested Dependencies,

$ sbt inspect actual console
...
[info] Dependencies:
[info]  Compile / console / streams
[info]  Global / taskTemporaryDirectory
[info]  scalaInstance
[info]  Compile / scalacOptions
[info]  Global / initialCommands
[info]  Global / cleanupCommands
[info]  Compile / fullClasspath
[info]  console / compilers
...

For initialCommands, we see that it comes from the global scope (Global). Combining this with the relevant output from inspect console:

Compile / console / initialCommands

we know that we can set initialCommands as generally as the global scope, as specific as the current project's console task scope, or anything in between. This means that we can, for example, set initialCommands for the whole project and will affect console:

> set initialCommands := "import mypackage._"
...

The reason we might want to set it here this is that other console tasks will use this value now. We can see which ones use our new setting by looking at the reverse dependencies output of inspect actual:

$ sbt inspect actual Global/initialCommands
...
[info] Reverse dependencies:
[info]  Compile / console
[info]  consoleProject
[info]  Test / console
[info]  Test / consoleQuick
[info]  Compile / consoleQuick
...

We now know that by setting initialCommands on the whole project, we affect all console tasks in all configurations in that project. If we didn't want the initial commands to apply for consoleProject, which doesn't have our project's classpath available, we could use the more specific task axis:

> set console / initialCommands := "import mypackage._"
> set consoleQuick / initialCommands := "import mypackage._"`

or configuration axis:

> set Compile/ initialCommands := "import mypackage._"
> set Test / initialCommands := "import mypackage._"

The next part describes the Delegates section, which shows the chain of delegation for scopes.

Delegates

A setting has a key and a scope. A request for a key in a scope A may be delegated to another scope if A doesn't define a value for the key. The delegation chain is well-defined and is displayed in the Delegates section of the inspect command. The Delegates section shows the order in which scopes are searched when a value is not defined for the requested key.

As an example, consider the initial commands for console again:

$ sbt inspect console/initialCommands
...
[info] Delegates:
[info]  console / initialCommands
[info]  initialCommands
[info]  ThisBuild / console / initialCommands
[info]  ThisBuild / initialCommands
[info]  Zero / console / initialCommands
[info]  Global / initialCommands
...

This means that if there is no value specifically for console/initialCommands, the scopes listed under Delegates will be searched in order until a defined value is found.

Inspect tree

In addition to displaying immediate forward and reverse dependencies as described in the previous section, the inspect tree command can display the full dependency tree for a task or setting. For example,

$ sbt inspect tree console
[info] Compile / console = Task[void]
[info]   +-Global / cleanupCommands =
[info]   +-console / compilers = Task[class xsbti.compile.Compilers]
[info]   +-Compile / fullClasspath = Task[Seq[class sbt.internal.util.Attributed]]
[info]   +-Global / initialCommands =
[info]   +-scalaInstance = Task[class sbt.internal.inc.ScalaInstance]
[info]   +-Compile / scalacOptions = Task[Seq[class java.lang.String]]
[info]   +-Compile / console / streams = Task[interface sbt.std.TaskStreams]
[info]   | +-Global / streamsManager = Task[interface sbt.std.Streams]
[info]   |
[info]   +-Global / taskTemporaryDirectory = target/....
[info]   +-Global / fileConverter = sbt.internal.inc.MappedFileConverter@10095d95
[info]   +-Global / state = Task[class sbt.State]
[info]
[success] elapsed: 0 s

For each task, inspect tree show the type of the value generated by the task. For a setting, the toString of the setting is displayed.

sbt publish

書式

sbt [query / ] publish
sbt [query / ] publishSigned
sbt [query / ] publishLocal
sbt [query / ] publishM2

説明

The publish family of tasks provide means for compiling and publishing your project. Publishing in this context consists of uploading a descriptor, such as a Maven POM or ivy.xml, and artifacts, such as a JAR or war file, to a repository so that other projects can specify your project as a dependency.

  • The publish task publishes your project to a remote repository, such as JFrog Artifactory or Sonatype Nexus instance.
  • The publishSigned task, enabled using sbt-pgp plugin, is used to publish GPG-signed artifacts.
  • The publishLocal task publishes your project to the Ivy local file repository, which is usually located at $HOME/.ivy2/local/. You can then use this project from other projects on the same machine.
  • The publishM2 task publishes your project to the local Maven repository.

There's a specific recipe for publishing to the Central Repo.

Skip publishing

To avoid publishing a project, add the following setting to the subprojects that you want to skip:

publish / skip := true

Common use case is to prevent publishing of the root project.

Define the repository

To specify the repository, assign a repository to publishTo and optionally set the publishing style. For example, to upload to Nexus:

publishTo := Some("Sonatype Snapshots Nexus" at "https://oss.sonatype.org/content/repositories/snapshots")

To publish to a local maven repository:

publishTo := Some(MavenCache("local-maven", file("path/to/maven-repo/releases")))

To publish to a local Ivy repository:

publishTo := Some(Resolver.file("local-ivy", file("path/to/ivy-repo/releases")))

If you're publishing the Central Repository, you will also have to select the right repository depending on your artifacts: SNAPSHOT versions go to the central-snapshots repository while other versions go to the local staging repository. Doing this selection can be done by using the value of the version setting:

publishTo := {
  val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/"
  if version.value.endsWith("-SNAPSHOT") then Some("central-snapshots" at centralSnapshots)
  else localStaging.value
}

Publishing locally

The publishLocal task will publish to the "local" Ivy repository. By default, this is at $HOME/.ivy2/local/. Other builds on the same machine can then list the project as a dependency. For example, if the project you are publishing has configuration parameters like:

organization := "com.example"
version := "0.1-SNAPSHOT"
name := "hello"

Then another build on the same machine can depend on it:

libraryDependencies += "com.example" %% "hello" % "0.1-SNAPSHOT"

The version number you select must end with SNAPSHOT, or you must change the version number each time you publish to indicate that it's a changing artifact.

Warning

Generally the use of SNAPSHOT dependencies should be avoided beyond testing on a single machine since it makes dependency resolution slower and the build non-repeatable.

Similar to publishLocal, publishM2 task will publish the user's Maven local repository. This is at the location specified by $HOME/.m2/settings.xml or at $HOME/.m2/repository/ by default. Another build would require Resolver.mavenLocal to resolve out of it:

resolvers += Resolver.mavenLocal

Credentials

There are two ways to specify credentials for such a repository.

The first and better way is to load them from a file, for example:

credentials += Credentials(Path.userHome / ".sbt" / ".credentials")

The credentials file is a properties file with keys realm, host, user, and password. For example:

realm=Sonatype Nexus Repository Manager
host=my.artifact.repo.net
user=admin
password=admin123

The second way is to specify them inline:

credentials += Credentials("Sonatype Nexus Repository Manager", "my.artifact.repo.net", "admin", "admin123")

Note

Credentials matching is done using both: realm and host keys. The realm key is the HTTP WWW-Authenticate header's realm directive, which is part of the response of HTTP servers for HTTP Basic Authentication. For a given repository, this can be found by reading all the headers received. For example:

curl -D - my.artifact.repo.net

Cross-publishing

To support multiple incompatible Scala versions, use projectMatrix and publish (see Cross building setup).

Overriding the publishing convention

By default sbt will publish your artifact with the binary version of Scala you're using. For example if your project is using Scala 2.13.x your example artifact would be published under example_2.13. This is often what you want, but if you're publishing a pure Java artifact or a compiler plugin you'll want to change the CrossVersion. See the Cross building setup page for more details under the Publishing convention section.

Published artifacts

By default, the main binary JAR, a sources JAR, and a API documentation JAR are published. You can declare other types of artifacts to publish and disable or modify the default artifacts. See the Artifact page for details.

Version scheme

versionScheme setting tracks the version scheme of the build:

versionScheme := Some("early-semver")

The supported values are "early-semver", "pvp", "semver-spec", and "strict". sbt will include this information into pom.xml and ivy.xml as a property.

  • Some("early-semver"): Early Semantic Versioning that would keep binary compatibility across patch updates within 0.Y.z (for instance 0.13.0 and 0.13.2). Once it goes 1.0.0, it follows the regular Semantic Versioning where 1.1.0 is bincompat with 1.0.0.
  • Some("semver-spec"): Semantic Versioning where all 0.y.z are treated as initial development (no bincompat guarantees).
  • Some("pvp"). Haskell Package Versioning Policy where X.Y are treated as major version.
  • Some("strict"). Requires exact match of version.

This information will be annotated into the pom.xml file, which helps downstream projects determine whether a version conflict is safe to resolve or not. See Preventing version conflicts with versionScheme (2021).

Modifying the generated POM

When publishMavenStyle is true, a POM is generated by the makePom action and published to the repository instead of an Ivy file. This POM file may be altered by changing a few settings. Set pomExtra to provide XML (scala.xml.NodeSeq) to insert directly into the generated pom. For example:

pomExtra := <something></something>

There is also a pomPostProcess setting that can be used to manipulate the final XML before it is written. It's type is Node => Node.

pomPostProcess := { (node: Node) =>
  ....
}

makePom adds to the POM any Maven-style repositories you have declared. You can filter these by modifying pomRepositoryFilter, which by default excludes local repositories. To instead only include local repositories:

pomIncludeRepository := { (repo: MavenRepository) =>
  repo.root.startsWith("file:")
}

Watch command

書式

sbt ~ command1
sbt ~ command1 [ ; command2 ; ... ]

Descrption

The watch command is denoted by ~ (tilde), and it provides the ability to monitor the input files for particular tasks and repeat the tasks when changes to those files occur.

Some example usages are described below:

Compile

A common use-case is continuous compilation. The following commands will make sbt watch for source changes in the Test and Compile (default) configurations respectively and re-run the compile command.

> ~ Test / compile

> ~ compile

Note that because Test / compile depends on Compile / compile, source changes in the main source directory will trigger recompilation of the test sources.

Testing

Triggered execution is often used when developing in a test driven development (TDD) style. The following command will monitor changes to both the main and test source sources for the build and re-run only the tests that reference classes that have been re-compiled since the last test run.

> ~ test

It is also possible to re-run only a particular test if its dependencies have changed.

> ~ test foo.BarTest

It is possible to always re-run a test when source changes are detected regardless of whether the test depends on any of the updated source files.

> ~ testOnly foo.BarTest

To run all of the tests in the project when any sources change, use

> ~ testFull

Running Multiple Commands

The watch command supports watching multiple, semicolon separated, tasks. For example, the following command will monitor for source file changes and run clean and test:

> ~ clean; test

Build sources

If the build is configured to automatically reload when build source changes are made by setting Global / onChangedBuildSource := ReloadOnSourceChanges, then sbt will monitor the build sources (i.e. *.sbt and *.{java,scala} files in the project directory). When build source changes are detected, the build will be reloaded and sbt will re-enter triggered execution mode when the reload completes.

Clearing the screen

sbt can clear the console screen before it evaluates the task or after it triggers an event. To configure sbt to clear the screen after an event is triggered add

ThisBuild / watchTriggeredMessage := Watch.clearScreenOnTrigger

to the build settings. To clear the screen before running the task, add

ThisBuild  / watchBeforeCommand := Watch.clearScreen

to the build settings.

Configuration

The behavior of triggered execution can be configured via a number of settings.

  • watchTriggers: Seq[Glob] adds search queries for files that should task trigger evaluation but that the task does not directly depend on. For example, if the project build.sbt file contains foo / watchTriggers += baseDirectory.value.toGlob / "*.txt", then any modifications to files ending with the txt extension will cause the foo command to trigger when in triggered execution mode.

  • watchTriggeredMessage: (Int, Path, Seq[String]) => Option[String] sets the message that is displayed when a file modification triggers a new build. Its input parameters are the current watch iteration count, the file that triggered the build and the command(s) that are going to be run. By default, it prints a message indicating what file triggered the build and what commands its going to run. No message is printed when the function returns None. To clear the screen before printing the message, just add Watch.clearScreen() inside of the task definition. This will ensure that the screen is cleared and that the message, if any is defined, will be printed after the screen clearing.

  • watchInputOptions: Seq[Watch.InputOption] allows the build to override the default watch options. For example, to add the ability to reload the build by typing the 'l' key, add ThisBuild / watchInputOptions += Watch.InputOption('l', "reload", Watch.Reload) to the build.sbt file. When using the default watchStartMessage, this will also add the option to the list displayed by the '?' option.

  • watchBeforeCommand: () => Unit provides a callback to run before evaluating the task. It can be used to clear the console screen by adding ThisBuild / watchBeforeCommand := Watch.clearScreen to the project build.sbt file. By default it is no-op.

  • watchLogLevel sets the logging level of the file monitoring system. This can be useful if the triggered execution is not being evaluated when source files or modified or if is unexpectedly triggering due to modifications to files that should not be monitored.

  • watchInputParser: Parser[Watch.Action] changes how the monitor handles input events. For example, setting watchInputParser := 'l' ^^^ Watch.Reload | '\r' ^^^ new Watch.Run("") will make it so that typing the 'l' key will reload the build and typing a newline will return to the shell. By default this is automatically derived from the watchInputOptions.

  • watchStartMessage: (Int, ProjectRef, Seq[String]) => Option[String] sets the banner that is printed while the watch process is waiting for file or input events. The inputs are the iteration count, the current project and the commands to run. The default message includes instructions for terminating the watch or displaying all available options. This banner is only displayed if watchOnIteration logs the result of watchStartMessage.

  • watchOnIteration: (Int, ProjectRef, Seq[String]) => Watch.Action a function that is evaluated before waiting for source or input events. It can be used to terminate the watch early if, for example, a certain number of iterations have been reached. By default, it just logs the result of watchStartMessage.

  • watchForceTriggerOnAnyChange: Boolean configures whether or not the contents of a source file must change in order to trigger a build. The default value is false.

  • watchPersistFileStamps: Boolean toggles whether or not sbt will persist the file hashes computed for source files across multiple task evaluation runs. This can improve performance for projects with many source files. Because the file hashes are cached, it is possible for the evaluated task to read an invalid hash if many source files are being concurrently modified. The default value is false.

  • watchAntiEntropy: FiniteDuration controls the time that must elapse before a build is re-triggered by the same file that previously triggered the build. This is intended to prevent spurious builds that can occur when a file is modified in short bursts. The default value is 500ms.

Cached task

This page covers the cached task details. See Caching for a general explanation.

自動的キャッシュ化

val someKey = taskKey[String]("something")

someKey := name.value + version.value + "!"

In sbt 2.x, the task result will be automatically cached based on the two settings name and version. The first time we run the task it will be executed onsite, but the second time onward, it will use the disk cache:

sbt:demo> show someKey
[info] demo0.1.0-SNAPSHOT!
[success] elapsed time: 0 s, cache 0%, 1 onsite task
sbt:demo> show someKey
[info] demo0.1.0-SNAPSHOT!
[success] elapsed time: 0 s, cache 100%, 1 disk cache hit

Caching is serialization-hard

To participate in the automatic caching, the input keys (e.g. name and version) must provide a given for sjsonnew.HashWriter typeclass and return type must provide a given for sjsonnew.JsonFormat. Contraband can be used to generate sjson-new codecs.

Effect tracking

The effect of file creation

To cache the effect of file creation, not just returning the name of the file, we need to track the effect of file creation using Def.declareOutput(vf).

someKey := {
  val conv = fileConverter.value
  val out: java.nio.file.Path = createFile(...)
  val vf: xsbti.VirtualFile = conv.toVirtualFile(out)
  Def.declareOutput(vf)
  vf: xsbti.HashedVirtualFileRef
}

Opting out from caching

Build-wide opt-out

To opt out of by-default custom task caching, add the following to project/plugins.sbt:

Compile / scalacOptions += "-Xmacro-settings:sbt:no-default-task-cache"

Note

This applies only to the custom tasks introduced in the build. Any cached tasks provided by sbt or plugins will remain cached.

Per-task-key opt-out

Next, if you want to opt some task keys from caching, you can set the cache level as follows:

@transient
val someKey = taskKey[String]("something")

or

@cacheLevel(include = Array.empty)
val someKey = taskKey[String]("something")

Per-task opt-out

To opt out of the cache individually, use Def.uncached(...) as follows:

val someKey = taskKey[String]("something")

someKey := Def.uncached {
  name.value + somethingUncachable.value + "!"
}

リモート・キャッシュ

sbt 2.x implements Bazel-compatible gRPC interface, which works with number of backend both open source and commercial. See Remote cache setup for more details.

Cross building setup

This page covers cross building setup. See Cross building for general explanation.

Using cross-built libraries

To use a library built against multiple versions of Scala, double the first % in a ModuleID to be %%. This tells sbt that it should append the current version of Scala being used to build the library to the dependency’s name. For example:

libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.4"

A nearly equivalent, manual alternative for a fixed version of Scala is:

libraryDependencies += "org.typelevel" % "cats-effect_3" % "3.5.4"

Scala 3 specific cross-versions

If you are developing an application in Scala 3, you can use Scala 2.13 libraries:

("a" % "b" % "1.0").cross(CrossVersion.for3Use2_13)

This is equivalent to using %% except it resolves the _2.13 variant of the library when scalaVersion is 3.x.y.

Conversely we have CrossVersion.for2_13Use3 to use the _3 variant of the library when scalaVersion is 2.13.x:

("a" % "b" % "1.0").cross(CrossVersion.for2_13Use3)

Warning

Warning for library authors: It is generally not safe to publish a Scala 3 library that depends on a Scala 2.13 library or vice-versa. Doing so could introduce two versions of the same library like scala-xml_2.13 and scala-xml_3 on the end users' classpath.

More about using cross-built libraries

You can have fine-grained control over the behavior for different Scala versions by using the cross method on ModuleID These are equivalent:

"a" % "b" % "1.0"
("a" % "b" % "1.0").cross(CrossVersion.disabled)

These are equivalent:

"a" %% "b" % "1.0"
("a" % "b" % "1.0").cross(CrossVersion.binary)

This overrides the defaults to always use the full Scala version instead of the binary Scala version:

("a" % "b" % "1.0").cross(CrossVersion.full)

CrossVersion.patch sits between CrossVersion.binary and CrossVersion.full in that it strips off any trailing -bin-... suffix which is used to distinguish variant but binary compatible Scala toolchain builds.

("a" % "b" % "1.0").cross(CrossVersion.patch)

CrossVersion.constant fixes a constant value:

("a" % "b" % "1.0").cross(CrossVersion.constant("2.9.1"))

It is equivalent to:

"a" % "b_2.9.1" % "1.0"

Project matrix

sbt 2.x introduces project matrix, which enables cross building to happen in parallel.

organization := "com.example"
scalaVersion := "3.7.3"
version      := "0.1.0-SNAPSHOT"

lazy val core = (projectMatrix in file("core"))
  .settings(
    name := "core"
  )
  .jvmPlatform(scalaVersions = Seq("3.7.3", "2.13.17"))

Publishing convention

We use the Scala ABI (application binary interface) version as suffix to denote which version of Scala was used to compile a library. For example, the artifact name cats-effect_2.13 means Scala 2.13.x was used. cats-effect_3 means Scala 3.x was used. This fairly simple approach allows interoperability with users of Maven, Ant and other build tools. For pre-prelease versions of Scala, such as 2.13.0-RC1, full version will be considered the ABI version.

crossVersion setting can be used to override the publishing convention:

  • CrossVersion.disabled (no suffix)
  • CrossVersion.binary (_<scala-abi-version>)
  • CrossVersion.full (_<scala-version>)

The default is either CrossVersion.binary or CrossVersion.disabled depending on the value of crossPaths. Because (unlike Scala library) Scala compiler is not forward compatible among the patch releases, compiler plugins should use CrossVersion.full.

Remote cache setup

This page covers remote caching setup. See Caching for general explanation of the caching system.

gRPC remote cache

While there might be multiple remote cache store implemention in the future, sbt 2.0 ships with a gRPC client that is compatible with the Bazel remote cache backends. To configure sbt 2.x, add the following to project/plugins.sbt

addRemoteCachePlugin

There are many Bazel remote cache backends, both open source and commercial solutions. While this page documents is not an exhaustive list of all Bazel remote cache implementations, hopefully it shows how sbt 2.x can be set up for wide array of them.

Authentication

There are a few flavors of gRPC authentication, and Bazel remote cache backends use various kind of them:

  1. Unauthenticated. Useful for testing.
  2. Default TLS/SSL.
  3. TLS/SSL with custom server certificate.
  4. TTL/SSL with custom server and client certificate, mTLS.
  5. Default TLS/SSL with API token header.

bazel-remote without authentication

You can grab the code from buchgr/bazel-remote and run it on a laptop using Bazel:

bazel run :bazel-remote  -- --max_size 5 --dir $HOME/work/bazel-remote/temp \
  --http_address localhost:8000 \
  --grpc_address localhost:2024

To configure sbt 2.x, add the following to project/plugins.sbt

addRemoteCachePlugin

and append the following to build.sbt:

Global / remoteCache := Some(uri("grpc://localhost:2024"))

bazel-remote with mTLS

In a real environment, mTLS can ensure that the transport is encrypted and mutually authenticated. bazel-remote can be started with something like the follows:

bazel run :bazel-remote  -- --max_size 5 --dir $HOME/work/bazel-remote/temp \
  --http_address localhost:8000 \
  --grpc_address localhost:2024 \
  --tls_ca_file /tmp/sslcert/ca.crt \
  --tls_cert_file /tmp/sslcert/server.crt \
  --tls_key_file /tmp/sslcert/server.pem

sbt 2.x setting would look like this in this scenario:

Global / remoteCache := Some(uri("grpcs://localhost:2024"))
Global / remoteCacheTlsCertificate := Some(file("/tmp/sslcert/ca.crt"))
Global / remoteCacheTlsClientCertificate := Some(file("/tmp/sslcert/client.crt"))
Global / remoteCacheTlsClientKey := Some(file("/tmp/sslcert/client.pem"))

Note the grpcs://, as opposed to grpc://.

EngFlow

EngFlow GmbH is a build solution company founded in 2020 by core members of Bazel team, providing build analytics and remote execution backend for Bazel, which includes remote cache.

After signing up for trial on https://my.engflow.com/, the page instructs you to start a trial cluster using a docker. If you followed the instruction, this should start a remote cache service on port 8080. The sbt 2.x configuration would look like this for the trial cluster:

Global / remoteCache := Some(uri("grpc://localhost:8080"))

BuildBuddy

BuildBuddy is a build solution company founded by ex-Google engineers, providing build analytics and remote execution backend for Bazel. It's also available open source as buildbuddy-io/buildbuddy.

After signing up, BuildBuddy Personal plan lets you use BuildBuddy across the Internet.

  1. From https://app.buildbuddy.io/, go to Settings, and change the Organization URL to <something>.buildbuddy.io.
  2. Next, go to Quickstart and take note of the URLs and --remote_headers.
  3. Create a file called $HOME/.sbt/buildbuddy_credential.txt and put in the API key:
x-buildbuddy-api-key=*******

The sbt 2.x configuration would look like this:

Global / remoteCache := Some(uri("grpcs://something.buildbuddy.io"))
Global / remoteCacheHeaders += IO.read(BuildPaths.defaultGlobalBase / "buildbuddy_credential.txt").trim

NativeLink is an open-source Bazel remote execution backend implementated in Rust with emphasis on performance. As of June 2024, there's NativeLink Cloud in beta.

  1. From https://app.nativelink.com/, go to Quickstart and take note of the URLs and --remote_header.
  2. Create a file called $HOME/.sbt/nativelink_credential.txt and put in the API key:
x-nativelink-api-key=*******

The sbt 2.x configuration would look like this:

Global / remoteCache := Some(uri("grpcs://something.build-faster.nativelink.net"))
Global / remoteCacheHeaders += IO.read(BuildPaths.defaultGlobalBase / "nativelink_credential.txt").trim

Artifact

説明

An artifact is a single file ready for publishing a specific version of a subproject. This is a concept that originated in Apache Maven and Ivy.

In the JVM ecosystem, common artifacts are Java archives, or JAR files. Compressed package formats are often preferred because they are easier to manage, download, and store.

To illustrate, the following is a list of artifacts for a library, enumerated in an ivy.xml file:

  <publications>
    <artifact name="core_3" type="jar" ext="jar" conf="compile"/>
    <artifact e:classifier="sources" name="core_3" type="src" ext="jar" conf="sources"/>
    <artifact e:classifier="javadoc" name="core_3" type="doc" ext="jar" conf="docs"/>
    <artifact name="core_3" type="pom" ext="pom" conf="pom"/>
  </publications>

This shows that an artifact has a name, a type, and an extention, and optionally a classifier.

  • name. This is going to be the same as the subproject's module name.
  • type. The functional category of the artifact, such as jar, src, and doc.
  • extension. The file extention, such as jar, war, zip, xml etc.
  • classifier. In Maven, classifier is an arbitrary string that can be appended for an alternative or secondary artifact.

Selecting default artifacts

By default, the published artifacts are:

  1. The main binary JAR
  2. The JAR containing the main sources and resources
  3. The JAR containing the API documentation

You can add artifacts for the test classes, sources, or API or you can disable some of the main artifacts.

To add all Test artifacts:

lazy val app = (project in file("app"))
  .settings(
    Test / publishArtifact := true,
  )

To add them individually:

lazy val app = (project in file("app"))
  .settings(
    // enable publishing the jar produced by `Test/package`
    Test / packageBin / publishArtifact := true,

    // enable publishing the test API jar
    Test / packageDoc / publishArtifact := true,

    // enable publishing the test sources jar
    Test / packageSrc / publishArtifact := true,
  )

To disable main artifacts individually:

lazy val app = (project in file("app"))
  .settings(
    // disable publishing the main jar produced by `package`
    Compile / packageBin / publishArtifact := false,

    // disable publishing the main API jar
    Compile / packageDoc / publishArtifact := false,

    // disable publishing the main sources jar
    Compile / packageSrc / publishArtifact := false,
  )

Modifying default artifacts

Each built-in artifact has several configurable settings in addition to publishArtifact. The basic ones are artifact (of type SettingKey[Artifact]), mappings (of type TaskKey[(File, String)]), and artifactPath (of type SettingKey[File]). They are scoped by (Config / <task>) as indicated in the previous section.

To modify the type of the main artifact, for example:

Compile / packageBin / artifact := {
  val prev: Artifact = (Compile / packageBin / artifact).value
  prev.withType("bundle")
}

The generated artifact name is determined by the artifactName setting. This setting is of type (ScalaVersion, ModuleID, Artifact) => String. The ScalaVersion argument provides the full Scala version String and the binary compatible part of the version String. The String result is the name of the file to produce. The default implementation is Artifact.artifactName _. The function may be modified to produce different local names for artifacts without affecting the published name, which is determined by the artifact definition combined with the repository pattern.

For example, to produce a minimal name without a classifier or cross path:

artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  artifact.name + "-" + module.revision + "." + artifact.extension
}

(Note that in practice you rarely want to drop the classifier.)

Finally, you can get the (Artifact, File) pair for the artifact by mapping the packagedArtifact task. Note that if you don't need the Artifact, you can get just the File from the package task (package, packageDoc, or packageSrc). In both cases, mapping the task to get the file ensures that the artifact is generated first and so the file is guaranteed to be up-to-date.

For example:

val myTask = taskKey[Unit]("My task.")

myTask :=  {
  val (art, file) = (Compile / packageBin / packagedArtifact).value
  println("Artifact definition: " + art)
  println("Packaged file: " + file.getAbsolutePath)
}

Defining custom artifacts

In addition to configuring the built-in artifacts, you can declare other artifacts to publish. Multiple artifacts are allowed when using Ivy metadata, but a Maven POM file only supports distinguishing artifacts based on classifiers and these are not recorded in the POM.

Basic Artifact construction look like:

Artifact("name", "type", "extension")
Artifact("name", "classifier")
Artifact("name", url: URL)
Artifact("name", Map("extra1" -> "value1", "extra2" -> "value2"))

For example:

Artifact("myproject", "zip", "zip")
Artifact("myproject", "image", "jpg")
Artifact("myproject", "jdk15")

See the Ivy documentation for more details on artifacts. See the Artifact API for combining the parameters above and specifying [Configurations] and extra attributes.

To declare these artifacts for publishing, map them to the task that generates the artifact:

val myImageTask = taskKey[File](...)

myImageTask := {
  val artifact: File = makeArtifact(...)
  artifact
}

addArtifact(Artifact("myproject", "image", "jpg"), myImageTask)

addArtifact returns a sequence of settings (wrapped in a SettingsDefinition). In a full build configuration, usage looks like:

lazy val app = (project in file("app"))
  .settings(
    addArtifact(...)
  )

Publishing .war files

A common use case for web applications is to publish the .war file instead of the .jar file.

lazy val app = (project in file("app"))
  .settings(
    // disable .jar publishing
    Compile / packageBin / publishArtifact := false,

    // create an Artifact for publishing the .war file
    Compile / packageWar / artifact := {
      val prev: Artifact = (Compile / packageWar / artifact).value
      prev.withType("war").withExtension("war")
    },

    // add the .war file to what gets published
    addArtifact(Compile / packageWar / artifact, packageWar),
  )

Using dependencies with artifacts

To specify the artifacts to use from a dependency that has custom or multiple artifacts, use the artifacts method on your dependencies. For example:

libraryDependencies += ("org" % "name" % "rev").artifacts(Artifact("name", "type", "ext"))

The from and classifer methods (described on the sbt update page) are actually convenience methods that translate to artifacts:

def from(url: String) = artifacts(Artifact(name, new URL(url)))
def classifier(c: String) = artifacts(Artifact(name, c))

That is, the following two dependency declarations are equivalent:

libraryDependencies += ("org.testng" % "testng" % "5.7").classifier("jdk15")

libraryDependencies += ("org.testng" % "testng" % "5.7").artifacts(Artifact("testng", "jdk15"))

Input task

sbt provides a capability to define custom tasks that can parse user inputs and offer tab completion. The details of the parser will be covered in tab-completion parser later.

This page describes how to hook those parser combinators into the input task system.

Input keys

A key for an input task is of type InputKey and represents the input task like a SettingKey represents a setting or a TaskKey represents a task. Define a new input task key using the inputKey.apply factory method:

// goes in project/Build.scala or in build.sbt
val demo = inputKey[Unit]("A demo input task.")

The definition of an input task is similar to that of a normal task, but it can also use the result of a

Parser applied to user input. Just as the special value method gets the value of a setting or task, the special parsed method gets the result of a Parser.

Basic input task definition

The simplest input task accepts a space-delimited sequence of arguments. It does not provide useful tab completion and parsing is basic. The built-in parser for space-delimited arguments is constructed via the spaceDelimited method, which accepts as its only argument the label to present to the user during tab completion.

For example, the following task prints the current Scala version and then echoes the arguments passed to it on their own line.

import complete.DefaultParsers.{ *, given }

demo := {
  // get the result of parsing
  val args: Seq[String] = spaceDelimited("<arg>").parsed
  // Here, we also use the value of the `scalaVersion` setting
  println("The current Scala version is " + scalaVersion.value)
  println("The arguments to demo were:")
  args.foreach(println(_))
}

Input task using Parsers

The Parser provided by the spaceDelimited method does not provide any flexibility in defining the input syntax. Using a custom parser is just a matter of defining your own Parser as described on the Parsing Input page.

Constructing the Parser

The first step is to construct the actual Parser by defining a value of one of the following types:

  1. Parser[I]: a basic parser that does not use any settings
  2. Initialize[Parser[I]]: a parser whose definition depends on one or more settings
  3. Initialize[State => Parser[I]]: a parser that is defined using both settings and the current state

We already saw an example of the first case with spaceDelimited, which doesn't use any settings in its definition. As an example of the third case, the following defines a contrived Parser that uses the project's Scala and sbt version settings as well as the state. To use these settings, we need to wrap the Parser construction in Def.setting and get the setting values with the special value method:

import sbt.complete.DefaultParsers.{ *, given }
import sbt.complete.Parser

val parser: Def.Initialize[State => Parser[(String,String)]] =
Def.setting {
  (state: State) =>
    ( token("scala" <~ Space) ~ token(scalaVersion.value) ) |
    ( token("sbt" <~ Space) ~ token(sbtVersion.value) ) |
    ( token("commands" <~ Space) ~
        token(state.remainingCommands.size.toString) )
}

This Parser definition will produce a value of type (String,String). The input syntax defined isn't very flexible; it is just a demonstration. It will produce one of the following values for a successful parse (assuming the current Scala version is 3.7.3, the current sbt version is 2.0.0-RC6, and there are 3 commands left to run):

  • (scala,3.7.3)
  • (sbt,2.0.0-RC6)
  • (commands,3)

Again, we were able to access the current Scala and sbt version for the project because they are settings. Tasks cannot be used to define the parser.

Constructing the Task

Next, we construct the actual task to execute from the result of the Parser. For this, we define a task as usual, but we can access the result of parsing via the special parsed method on Parser.

The following contrived example uses the previous example's output (of type (String,String)) and the result of the package task to print some information to the screen.

demo := {
    val (tpe, value) = parser.parsed
    println("Type: " + tpe)
    println("Value: " + value)
    println("Packaged: " + packageBin.value.getAbsolutePath)
}

The InputTask type

It helps to look at the InputTask type to understand more advanced usage of input tasks. The core input task type is:

class InputTask[A1](val parser: State => Parser[Task[A1]])

Normally, an input task is assigned to a setting and you work with Initialize[InputTask[A1]].

Breaking this down,

  1. You can use other settings (via Initialize) to construct an input task.
  2. You can use the current State to construct the parser.
  3. The parser accepts user input and provides tab completion.
  4. The parser produces the task to run.

So, you can use settings or State to construct the parser that defines an input task's command line syntax. This was described in the previous section. You can then use settings, State, or user input to construct the task to run. This is implicit in the input task syntax.

Using other input tasks

The types involved in an input task are composable, so it is possible to reuse input tasks. The .parsed and .evaluated methods are defined on InputTasks to make this more convenient in common situations:

  • Call .parsed on an InputTask[A1] or Initialize[InputTask[A1]] to get the Task[A1] created after parsing the command line
  • Call .evaluated on an InputTask[A1] or Initialize[InputTask[A1]] to get the value of type A1 from evaluating that task

In both situations, the underlying Parser is sequenced with other parsers in the input task definition. In the case of .evaluated, the generated task is evaluated.

The following example applies the run input task, a literal separator parser --, and run again. The parsers are sequenced in order of syntactic appearance, so that the arguments before -- are passed to the first run and the ones after are passed to the second.

val run2 = inputKey[Unit](
    "Runs the main class twice with different argument lists separated by --")

val separator: Parser[String] = "--"

run2 := {
   val one = (Compile / run).evaluated
   val sep = separator.parsed
   val two = (Compile / run).evaluated
}

For a main class Demo that echoes its arguments, this looks like:

$ sbt
> run2 a b -- c d
[info] Running Demo c d
[info] Running Demo a b
c
d
a
b

Preapplying input

Because InputTasks are built from Parsers, it is possible to generate a new InputTask by applying some input programmatically. (It is also possible to generate a Task, which is covered in the next section.) Two convenience methods are provided on InputTask[T] and Initialize[InputTask[T]] that accept the String to apply.

  • partialInput applies the input and allows further input, such as from the command line
  • fullInput applies the input and terminates parsing, so that further input is not accepted

In each case, the input is applied to the input task's parser. Because input tasks handle all input after the task name, they usually require initial whitespace to be provided in the input.

Consider the example in the previous section. We can modify it so that we:

  • Explicitly specify all of the arguments to the first run. We use name and version to show that settings can be used to define and modify parsers.
  • Define the initial arguments passed to the second run, but allow further input on the command line.

Note

If the input derives from settings you need to use, for example, Def.taskDyn { ... }.value

lazy val run2 = inputKey[Unit]("Runs the main class twice: " +
   "once with the project name and version as arguments"
   "and once with command line arguments preceded by hard coded values.")

// The argument string for the first run task is ' <name> <version>'
lazy val firstInput: Initialize[String] =
   Def.setting(s" ${name.value} ${version.value}")

// Make the first arguments to the second run task ' red blue'
lazy val secondInput: String = " red blue"

run2 := {
   val one = (Compile / run).fullInput(firstInput.value).evaluated
   val two = (Compile / run).partialInput(secondInput).evaluated
}

For a main class Demo that echoes its arguments, this looks like:

$ sbt
> run2 green
[info] Running Demo demo 1.0
[info] Running Demo red blue green
demo
1.0
red
blue
green

Get a Task from an InputTask

The previous section showed how to derive a new InputTask by applying input. In this section, applying input produces a Task. The toTask method on Initialize[InputTask[A1]] accepts the String input to apply and produces a task that can be used normally. For example, the following defines a plain task runFixed that can be used by other tasks or run directly without providing any input:

lazy val runFixed = taskKey[Unit]("A task that hard codes the values to `run`")

runFixed := {
   val _ = (Compile / run).toTask(" blue green").value
   println("Done!")
}

For a main class Demo that echoes its arguments, running runFixed looks like:

$ sbt
> runFixed
[info] Running Demo blue green
blue
green
Done!

Each call to toTask generates a new task, but each task is configured the same as the original InputTask (in this case, run) but with different input applied. For example:

lazy val runFixed2 = taskKey[Unit]("A task that hard codes the values to `run`")

run / fork := true

runFixed2 := {
   val x = (Compile / run).toTask(" blue green").value
   val y = (Compile / run).toTask(" red orange").value
   println("Done!")
}

The different toTask calls define different tasks that each run the project's main class in a new jvm. That is, the fork setting configures both, each has the same classpath, and each run the same main class. However, each task passes different arguments to the main class. For a main class Demo that echoes its arguments, the output of running runFixed2 might look like:

$ sbt
> runFixed2
[info] Running Demo blue green
[info] Running Demo red orange
blue
green
red
orange
Done!

Tab-completion parser

This page describes the parser combinators in sbt. These parsers are used to parse user input and provide tab completion for input tasks and commands.

Parser combinators build up a parser from smaller parsers. A Parser[A] in its most basic usage is a function String => Option[A]. It accepts a String to parse and produces a value wrapped in Some if parsing succeeds or None if it fails. Error handling and tab completion make this picture more complicated, but we'll stick with Option for this discussion.

Basic parsers

The simplest parser combinators match exact inputs:

import sbt.{ *, given }
import sbt.complete.DefaultParsers.{ *, given }

// A parser that succeeds if the input is 'x', returning the Char 'x'
//  and failing otherwise
val singleChar: Parser[Char] = 'x'

// A parser that succeeds if the input is "blue", returning the String "blue"
//   and failing otherwise
val litString: Parser[String] = "blue"

In these examples, implicit conversions produce a literal Parser from a Char or String. Other basic parser constructors are the charClass, success and failure methods:

import sbt.{ *, given }
import sbt.complete.DefaultParsers.{ *, given }

// A parser that succeeds if the character is a digit, returning the matched Char
//   The second argument, "digit", describes the parser and is used in error messages
val digit: Parser[Char] = charClass((c: Char) => c.isDigit, "digit")

// A parser that produces the value 3 for an empty input string, fails otherwise
val alwaysSucceed: Parser[Int] = success(3)

// Represents failure (always returns None for an input String).
//  The argument is the error message.
val alwaysFail: Parser[Nothing] = failure("Invalid input.")

Built-in parsers

sbt comes with several built-in parsers defined in sbt.complete.DefaultParsers.

Some commonly used built-in parsers are:

  • Space, NotSpace, OptSpace, and OptNotSpace for parsing spaces or non-spaces, required or not.
  • StringBasic for parsing text that may be quoted.
  • IntBasic for parsing a signed Int value.
  • Digit and HexDigit for parsing a single decimal or hexadecimal digit.
  • Bool for parsing a Boolean value

See the DefaultParsers API for details.

Combining parsers

We build on these basic parsers to construct more interesting parsers. We can combine parsers in a sequence, choose between parsers, or repeat a parser.

// A parser that succeeds if the input is "blue" or "green",
//  returning the matched input
val color: Parser[String] = "blue" | "green"

// A parser that matches either "fg" or "bg"
val select: Parser[String] = "fg" | "bg"

// A parser that matches "fg" or "bg", a space, and then the color, returning the matched values.
val setColor: Parser[(String, Char, String)] =
  select ~ ' ' ~ color

// Often, we don't care about the value matched by a parser, such as the space above
//  For this, we can use ~> or <~, which keep the result of
//  the parser on the right or left, respectively
val setColor2: Parser[(String, String)]  =  select ~ (' ' ~> color)

// Match one or more digits, returning a list of the matched characters
val digits: Parser[Seq[Char]] = charClass(_.isDigit, "digit").+

// Match zero or more digits, returning a list of the matched characters
val digits0: Parser[Seq[Char]] = charClass(_.isDigit, "digit").*

// Optionally match a digit
val optDigit: Parser[Option[Char]] = charClass(_.isDigit, "digit").?

Transforming results

A key aspect of parser combinators is transforming results along the way into more useful data structures. The fundamental methods for this are map and flatMap. Here are examples of map and some convenience methods implemented on top of map.

// Apply the `digits` parser and apply the provided function to the matched
//   character sequence
val num: Parser[Int] = digits.map: (chars: Seq[Char]) =>
  chars.mkString.toInt }

// Match a digit character, returning the matched character or return '0' if the input is not a digit
val digitWithDefault: Parser[Char] = charClass(_.isDigit, "digit") ?? '0'

// The previous example is equivalent to:
val digitDefault: Parser[Char] =
  charClass(_.isDigit, "digit").?.map: (d: Option[Char]) =>
    d.getOrElse('0')

// Succeed if the input is "blue" and return the value 4
val blue = "blue" ^^^ 4

// The above is equivalent to:
val blueM = "blue".map((s: String) => 4)

Controlling tab completion

Most parsers have reasonable default tab completion behavior. For example, the string and character literal parsers will suggest the underlying literal for an empty input string. However, it is impractical to determine the valid completions for charClass, since it accepts an arbitrary predicate. The examples method defines explicit completions for such a parser:

val digit = charClass(_.isDigit, "digit").examples("0", "1", "2")

Tab completion will use the examples as suggestions. The other method controlling tab completion is token. The main purpose of token is to determine the boundaries for suggestions. For example, if your parser is:

("fg" | "bg") ~ ' ' ~ ("green" | "blue")

then the potential completions on empty input are: console fg green fg blue bg green bg blue

Typically, you want to suggest smaller segments or the number of suggestions becomes unmanageable. A better parser is:

token( ("fg" | "bg") ~ ' ') ~ token("green" | "blue")

Now, the initial suggestions would be (with _ representing a space): console fg_ bg_

Be careful not to overlap or nest tokens, as in token("green" ~ token("blue")). The behavior is unspecified (and should generate an error in the future), but typically the outer most token definition will be used.

Dependent parsers

Sometimes a parser must analyze some data and then more data needs to be parsed, and it is dependent on the previous one. The key for obtaining this behaviour is to use the flatMap function.

As an example, it will shown how to select several items from a list of valid ones with completion, but no duplicates are possible. A space is used to separate the different items.

def select1(items: Iterable[String]) =
  token(Space ~> StringBasic.examples(FixedSetExamples(items)))

def selectSome(items: Seq[String]): Parser[Seq[String]] = {
   select1(items).flatMap: v =>
     val remaining = items.filter(_ != v)
     if remaining.size == 0 then success(v :: Nil)
     else selectSome(remaining).?.map(v +: _.getOrElse(Seq()))
 }

As you can see, the flatMap function provides the previous value. With this info, a new parser is constructed for the remaining items. The map combinator is also used in order to transform the output of the parser.

The parser is called recursively, until it is found the trivial case of no possible choices.

Community Plugins

The GitHub sbt Organization

The sbt organization is available for use by any sbt plugin. Developers who contribute their plugins into the community organization will still retain control over their repository and its access. The goal of the sbt organization is to organize sbt software into one central location.

A side benefit to using the sbt organization for projects is that you can use gh-pages to host websites under the https://www.scala-sbt.org domain.

The sbt autoplugin giter8 template is a good place to start. This sets up a new sbt plugin project appropriately. The generated README includes a summary of the steps for publishing a new community plugin.

Plugins available for sbt 2.x

[Edit] this page to submit a pull request that adds your plugin to the list.

### Code formatter plugins

One jar plugins

Verification plugins

Language support plugins

  • sbt-frege: build Frege code with sbt.
  • sbt-cc: compile C and C++ source files with sbt.

Release plugins

Deployment integration plugins

  • sbt-heroku: deploy applications directly to Heroku.
  • sbt-docker-compose: launch Docker images using docker compose.
  • sbt-appengine deploy your webapp to Google App Engine.
  • sbt-marathon: deploy applications on Apache Mesos using the Marathon framework.
  • sbt-riotctl: deploy applications as systemd services directly to a Raspberry Pi, ensuring dependencies (e.g. wiringpi) are met.
  • sbt-kind: load built docker images into a kind cluster.

IDE integration plugins

  • sbt-structure: extract project structure in XML for IntelliJ Scala plugin.

Test plugins

  • scripted: integration testing for sbt plugins.
  • sbt-jmh: run Java Microbenchmark Harness (JMH) benchmarks from sbt.
  • gatling-sbt: performance and load-testing using Gatling.
  • sbt-multi-jvm: run tests using multiple JVMs.
  • sbt-scalaprops: scalaprops property-based testing integration.
  • sbt-testng: TestNG framework integration.
  • sbt-jcstress: Java Concurrency Stress Test (jcstress) integration.
  • sbt-cached-ci: Incremental sbt builds for CI environments.

Library dependency plugins

Web and frontend development plugins

  • sbt-war: package and run WAR files
  • sbt-web: library for building sbt plugins for the web.

Database plugins

Code generator plugins

  • sbt-scalaxb: generate model classes from XML schemas and WSDL.

  • sbt-header: auto-generate source code file headers (such as copyright notices).

  • sbt-boilerplate: TupleX and FunctionX boilerplate code generator.

  • sbt-avro: Apache Avro schema and protocol generator.

  • sbt-aspectj: AspectJ weaving for sbt.

  • sbt-protoc: protobuf code generator using protoc.

  • sbt-contraband (docs): generate pseudo-case classes from GraphQL schemas.

  • sbt-antlr4: run ANTLR v4 from sbt.

  • sbt-sql: generate model classes from SQL.

  • sbt-partial-unification: enable partial unification support in Scala (SI-2712).

  • sbt-i18n: transform your i18n bundles into Scala code.

  • sbt-lit: build literate code with sbt.

  • sbt-embedded-files: generate Scala objects containing the contents of glob-specified files as strings or byte-arrays.

  • sbt-scala-ts: generate TypeScript code according compiled Scala types (case class, trait, object, ...).

Static code analysis plugins

Utility and system plugins

- [sbt-revolver](https://github.com/spray/sbt-revolver): auto-restart forked JVMs on update. - [sbt-conscript](https://github.com/foundweekends/conscript) ([docs](https://www.foundweekends.org/conscript/)): distribute apps using GitHub and Maven Central. - [sbt-errors-summary](https://github.com/Duhemm/sbt-errors-summary): show a summary of compilation errors. - [MiMa](https://github.com/lightbend/mima): binary compatibility management for Scala libraries. - [sbt-groll](https://github.com/sbt/sbt-groll): navigate git history inside sbt. - [sbt-prompt](https://github.com/agemooij/sbt-prompt): add promptlets and themes to your sbt prompt. - [sbt-crossproject](https://github.com/portable-scala/sbt-crossproject): cross-build Scala, Scala.js and Scala Native. - [sbt-proguard](https://github.com/sbt/sbt-proguard): run ProGuard on compiled sources. - [sbt-jni](https://github.com/sbt/sbt-jni): helpers for working with projects that use JNI. - [sbt-jol](https://github.com/ktoso/sbt-jol): inspect OpenJDK Java Object Layout from sbt. - [sbt-musical](https://github.com/tototoshi/sbt-musical): control iTunes from sbt (Mac only). - [sbt-travisci](https://github.com/dwijnand/sbt-travisci): integration with Travis CI. - [horder](https://github.com/romanowski/hoarder): cache compilation artefacts for future builds. - [sbt-javaagent](https://github.com/sbt/sbt-javaagent): add Java agents to projects.

Documentation plugins

  • tut: documentation and tutorial generator.

  • Laika: Transform Markdown or reStructuredText into HTML or PDF with Templating.

  • sbt-site: site generator.

  • sbt-microsites: generate and publish microsites using Jekyll.

  • sbt-ghpages: publish generated sites to GitHub pages.

  • sbt-api-mappings: generate Scaladoc apiMappings for common Scala libraries.

  • literator: generate literate-style markdown docs from your sources.

  • sbt-example: generate ScalaTest test suites from examples in Scaladoc.

  • sbt-delombok: delombok Java sources files that contain Lombok annotations to make Javadoc contain Lombok-generated classes and methods.

  • sbt-alldocs: collect all the docs for a project and dependencies into a single folder.

  • sbt-apidoc: A port of apidocjs to sbt, to document REST Api.

  • sbt-github-pages (docs): publish a website to GitHub Pages with minimal effort - works well with GitHub Actions.

  • sbt-docusaur (docs): build a website using Docusaurus and publish to GitHub Pages with minimal effort - works well with GitHub Actions.

  • sbt-hl-compiler: compile the code snippets from documentation (to keep it consistent).

  • sbt-scaladoc-compiler: compile the code snippets included in Scaladoc comments.

### Code coverage plugins

Create new project plugins

  • sbt-fresh: create an opinionated fresh sbt project.

Framework-specific plugins

  • sbt-newrelic: NewRelic support for artefacts built with sbt-native-packager.
  • sbt-spark: Spark application configurator.
  • sbt-api-builder: support for ApiBuilder from within sbt's shell.

Recipes

The recipe section of the documentation focuses on the objectives with minimal explanations.

If you are new to sbt, see sbt by example and Getting Started section first.

How to write hello world

Objective

I want to write a hello world program in Scala, and run it.

Steps

  1. Create a fresh directory, like hello_scala/

  2. Create a directory named project/ under hello_scala/, and create project/build.properties with

    sbt.version=2.0.0-RC6
    
  3. Under hello_scala/, create build.sbt:

    scalaVersion := "3.7.3"
    
  4. Under hello_scala/, create Hello.scala:

     @main def main(args: String*): Unit =
       println(s"Hello ${args.mkString}")
    
  5. Navigate to hello_scala/ from the terminal, and run sbt:

    $ sbt
    
  6. When the prompt appears, type run:

    sbt:hello_scala> run
    
  7. Type exit to exit the sbt shell:

    sbt:hello_scala> exit
    

Alternatives

When you're in a hurry, you can run sbt init in a fresh directory, and select the first template.

Publishing to the Central Repo

Note

The recipe section of the documentation focuses on the objectives with minimal explanations.

See also Sonatype's [Publish guides][sonatype-central-portal-register] for general concepts around publishing to the Central Portal.

Objective

I want to publish my project to the Central Repository.

Steps

Preliminary 1: Central Portal registration

Create a Central Portal account, following Sonatype's Publish guides.

  • If you had an OSSRH account, use Forgot password flow to convert the account to the new Central Portal, which lets you keep the previous namespace associations.
  • If you authenticate via GitHub, io.github.<user_name> will automatically be associated with the account.

Follow the steps described in register a namespace guide to associate a domain name with your account.

Preliminary 2: PGP key pair

Follow the Sonatype's GPG guide to generate a PGP key pair.

Install GnuPG, and verify the version:

$ gpg --version
gpg (GnuPG/MacGPG2) 2.2.8
libgcrypt 1.8.3
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>

Next generate a key:

$ gpg --gen-key

List the keys:

$ gpg --list-keys

/home/foo/.gnupg/pubring.gpg
------------------------------

pub   rsa4096 2018-08-22 [SC]
      1234517530FB96F147C6A146A326F592D39AAAAA
uid           [ultimate] your name <[email protected]>
sub   rsa4096 2018-08-22 [E]

Distribute the key:

$ gpg --keyserver keyserver.ubuntu.com --send-keys 1234517530FB96F147C6A146A326F592D39AAAAA

Step 1: sbt-pgp

The sbt-pgp plugin can sign the published artifacts with GPG/PGP. (Optionally sbt-ci-release can automate the publishing process.)

Add the following line to your project/plugins.sbt file to enable it for your build:

addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")

Note

Make sure that the gpg command is in PATH available to the sbt.

Step 2: Credentials

Generate a user token from the portal to be used for the credentials. The token must be stored somewhere safe (NOT in the repository).

sbt 2.x can also reads from the environment variables SONATYPE_USERNAME and SONATYPE_PASSWORD and appends a credential for central.sonatype.com out-of-box, which might be useful for automatic publishing from the CI environment, such as GitHub Actions.

- run: sbt ci-release
  env:
    PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
    PGP_SECRET: ${{ secrets.PGP_SECRET }}
    SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
    SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}

On a local machine, a common convention is a $HOME/.sbt/2/credentials.sbt file, with the following:

credentials += Credentials(Path.userHome / ".sbt" / "sonatype_central_credentials")

Next create a file $HOME/.sbt/sonatype_central_credentials:

host=central.sonatype.com
user=<your username>
password=<your password>

Step 3: Configure build.sbt

To publish to a Maven repository, you'll need to configure a few settings so that the correct metadata is generated.

Note: To publish to the Central Portal, publishTo must be set to the localStaging repository:

// new setting for the Central Portal
publishTo := {
  val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/"
  if version.value.endsWith("-SNAPSHOT") then Some("central-snapshots" at centralSnapshots)
  else localStaging.value
}

Add these settings at the end of build.sbt or a separate publish.sbt:

organization := "com.example.project2"
organizationName := "example"
organizationHomepage := Some(url("http://example.com/"))

scmInfo := Some(
  ScmInfo(
    url("https://github.com/your-account/your-project"),
    "scm:[email protected]:your-account/your-project.git"
  )
)
developers := List(
  Developer(
    id = "Your identifier",
    name = "Your Name",
    email = "your@email",
    url = url("http://your.url")
  )
)

description := "Some description about your project."
licenses := List(License.Apache2)
homepage := Some(url("https://github.com/example/project"))

// Remove all additional repository other than Maven Central from POM
pomIncludeRepository := { _ => false }
publishMavenStyle := true

// new setting for the Central Portal
publishTo := {
  val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/"
  if version.value.endsWith("-SNAPSHOT") then Some("central-snapshots" at centralSnapshots)
  else localStaging.value
}

The full format of a pom.xml (an end product of the project configuration used by Maven) file is outlined in POM Reference. You can add more data to it with the pomExtra option in build.sbt.

Step 4: Stage the artifacts

From sbt shell run:

> publishSigned

Step 5: Upload or release the bundle

From sbt shell run:

> sonaUpload

This will upload the bundle to the Central Portal. Hit the "Publish" button to publish to the Central Repository.

If you want to automate the publishing, run:

> sonaRelease

It might take 10 minutes to a few hours for the published artifacts to be visible on the Central Repository https://repo1.maven.org/maven2/.

Use sbt as Metals build server

Warning

This is a draft documentation of sbt 2.x that is yet to be released. This is a placeholder, copied from sbt 1.x.

Objective

I want to use Metals on VS Code with sbt as the build server.

Steps

To use Metals on VS Code:

  1. Install Metals from Extensions tab:
    Metals
  2. Open a directory containing a build.sbt file.
  3. From the menubar, run View > Command Palette... (Cmd-Shift-P on macOS) "Metals: Switch build server", and select "sbt"
    Metals
  4. Once the import process is complete, open a Scala file to see that code completion works:
    Metals

Use the following setting to opt-out some of the subprojects from BSP.

bspEnabled := false

When you make changes to the code and save them (Cmd-S on macOS), Metals will invoke sbt to do the actual building work.

Interactive debugging on VS Code

  1. Metals supports interactive debugging by setting break points in the code:
    Metals
  2. Interactive debugging can be started by right-clicking on an unit test, and selecting "Debug Test." When the test hits a break point, you can inspect the values of the variables:
    Metals

See Debugging page on VS Code documentation for more details on how to navigate an interactive debugging session.

Logging into sbt session

While Metals uses sbt as the build server, we can also log into the same sbt session using a thin client.

  • From Terminal section, type in sbt --client
    Metals

This lets you log into the sbt session Metals has started. In there you can call testOnly and other tasks with the code already compiled.

Import to IntelliJ IDEA

Warning

This is a draft documentation of sbt 2.x that is yet to be released. This is a placeholder, copied from sbt 1.x.

Objective

I want to import sbt build to IntelliJ IDEA.

Steps

IntelliJ IDEA is an IDE created by JetBrains, and the Community Edition is open source under Apache v2 license. IntelliJ integrates with many build tools, including sbt, to import the project.

To import a build to IntelliJ IDEA:

  1. Install Scala plugin on the Plugins tab:
    IntelliJ
  2. From Projects, open a directory containing a build.sbt file.
    IntelliJ
  3. Once the import process is complete, open a Scala file to see that code completion works.

IntelliJ Scala plugin uses its own lightweight compilation engine to detect errors, which is fast but sometimes incorrect. Per compiler-based highlighting, IntelliJ can be configured to use the Scala compiler for error highlighting.

Interactive debugging with IntelliJ IDEA

  1. IntelliJ supports interactive debugging by setting break points in the code:
    IntelliJ
  2. Interactive debugging can be started by right-clicking on an unit test, and selecting "Debug '<test name>'." Alternatively, you can click the green "run" icon on the left part of the editor near the unit test. When the test hits a break point, you can inspect the values of the variables:
    IntelliJ

See Debug Code page on IntelliJ documentation for more details on how to navigate an interactive debugging session.

Alternative

Using sbt as IntelliJ IDEA build server (advanced)

Importing the build to IntelliJ means that you're effectively using IntelliJ as the build tool and the compiler while you code (see also compiler-based highlighting). While many users are happy with the experience, depending on the code base some of the compilation errors may be false, it may not work well with plugins that generate sources, and generally you might want to code with the identical build semantics as sbt. Thankfully, modern IntelliJ supports alternative build servers including sbt via the Build Server Protocol (BSP).

The benefit of using BSP with IntelliJ is that you're using sbt to do the actual build work, so if you are the kind of programmer who had sbt session up on the side, this avoids double compilation.

Import to IntelliJ BSP with IntelliJ
Reliability ✅ Reliable behavior ⚠️ Less mature. Might encounter UX issues.
Responsiveness ⚠️
Correctness ⚠️ Uses its own compiler for type checking, but can be configured to use scalac ✅ Uses Zinc + Scala compiler for type checking
Generated source ❌ Generated source requires resync
Build reuse ❌ Using sbt side-by-side requires double build

To use sbt as build server on IntelliJ:

  1. Install Scala plugin on the Plugins tab.
  2. To use the BSP approach, do not use Open button on the Project tab:
    IntelliJ
  3. From menubar, click New > "Project From Existing Sources", or Find Action (Cmd-Shift-P on macOS) and type "Existing" to find "Import Project From Existing Sources":
    IntelliJ
  4. Open a build.sbt file. Select BSP when prompted:
    IntelliJ
  5. Select sbt (recommended) as the tool to import the BSP workspace:
    IntelliJ
  6. Once the import process is complete, open a Scala file to see that code completion works:
    IntelliJ

Use the following setting to opt-out some of the subprojects from BSP.

bspEnabled := false
  • Open Preferences, search BSP and check "build automatically on file save", and uncheck "export sbt projects to Bloop before import":
    IntelliJ

When you make changes to the code and save them (Cmd-S on macOS), IntelliJ will invoke sbt to do the actual building work.

See also Igal Tabachnik's Using BSP effectively in IntelliJ and Scala for more details.

Logging into sbt session

We can also log into the existing sbt session using the thin client.

  • From Terminal section, type in sbt --client IntelliJ

This lets you log into the sbt session IntelliJ has started. In there you can call testOnly and other tasks with the code already compiled.

Source dependency plugin

Note

The recipe section of the documentation focuses on the objectives with minimal explanations.

Objective

I want to use a plugin hosted on a git repository, without publishing to the Central Repo.

Steps

  1. Host an sbt 2.x plugin on a git repository, built using sbt 2.x.

  2. Add the following to project/plugins.sbt:

    // In project/plugins.sbt
    lazy val jmhRef = ProjectRef(
      uri("https://github.com/eed3si9n/sbt-jmh.git#303c3e98e1d1523e6a4f99abe09c900165028edb"),
      "plugin")
    BareBuildSyntax.dependsOn(jmhRef)
    
  3. When you start sbt, it will automatically clone the repository under $HOME/.sbt/2/staging/.

In the above, https://github.com/eed3si9n/sbt-jmh.git is the HTTP endpoint for a plugin hosted on GitHub, and 303c3e98e1d1523e6a4f99abe09c900165028edb is a commit id on the default branch.

Glossary

Symbols

:=, +=, ++=

These construct a Setting, which is the fundamental type in the settings system.

%

This is used to build up a ModuleID.

%%

This is similar to % except that it identifies a dependency that has been cross built.

%%%

This is defined in sbt-platform-deps in sbt 1.x.

C

コマンド

A system-level building block of sbt, often used to capture user interaction or IDE interaction. See Command.

クロスビルド

The idea of building multiple targets from the same set of source file. This includes Scala cross building, targetting multiple versions of Scala releases; platform cross building, targetting JVM, Scala.JS, and Scala Native; and custom virtual axis like Spark versions.

D

Dependency resolution

During library management, when multiple version candidates (e.g. foo:2.2.0 and foo:3.0.0) are found for a library foo within a dependency graph, it is called a dependency conflict. The process of mediating the conflict into a single version is called dependency resolution. Often, this would result in the older version beging removed from the dependency graph, which is called an eviction of foo:2.2.0. In some cases, an eviction is considered unsafe because the candidates are not replacable. See sbt update.

E

Eviction

See dependency resolution.

V

value

.value is used to denote a happens-before relationship from one task or setting to another. This method is special (it is a macro) and cannot be used except in := or in the standalone construction methods Def.setting and Def.task.

Setup Notes

See Installing sbt runner for the instruction on general setup. Using Coursier or SDKMAN has two advantages.

  1. They will install the official packaging by Eclipse Adoptium etc, as opposed to the "mystery meat OpenJDK builds".
  2. They will install tgz packaging of sbt that contains all JAR files. (DEB and RPM packages do not to save bandwidth)

This page describes alternative ways of installing the sbt runner. Note that some of the third-party packages may not provide the latest version.

OS specific setup

macOS

Homebrew

$ brew install sbt

Warning

Homebrew maintainers have added a dependency to JDK 13 because they want to use more brew dependencies (brew#50649). This causes sbt to use JDK 13 even when java available on PATH is JDK 8 or 11. To prevent sbt from running on JDK 13, install jEnv or switch to using SDKMAN.

Windows

Chocolatey

> choco install sbt

Scoop

> scoop install sbt

Linux

Ubuntu and other Debian-based distributions

DEB package is officially supported by sbt, but it does not contain JAR files to save bandwidth.

Ubuntu and other Debian-based distributions use the DEB format, but usually you don't install your software from a local DEB file. Instead they come with package managers both for the command line (e.g. apt-get, aptitude) or with a graphical user interface (e.g. Synaptic). Run the following from the terminal to install sbt (You'll need superuser privileges to do so, hence the sudo).

sudo apt-get update
sudo apt-get install apt-transport-https curl gnupg -yqq
echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list
echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | sudo tee /etc/apt/sources.list.d/sbt_old.list
curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo -H gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/scalasbt-release.gpg --import
sudo chmod 644 /etc/apt/trusted.gpg.d/scalasbt-release.gpg
sudo apt-get update
sudo apt-get install sbt

Package managers will check a number of configured repositories for packages to offer for installation. You just have to add the repository to the places your package manager will check.

Once sbt is installed, you'll be able to manage the package in aptitude or Synaptic after you updated their package cache. You should also be able to see the added repository at the bottom of the list in System Settings -> Software & Updates -> Other Software:

Ubuntu Software & Updates Screenshot

sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823 may not work on Ubuntu Bionic LTS (18.04) since it's using a buggy GnuPG, so we are advising to use web API to download the public key in the above.

Red Hat Enterprise Linux and other RPM-based distributions

RPM package is officially supported by sbt, but it does not contain JAR files to save bandwidth.

Red Hat Enterprise Linux and other RPM-based distributions use the RPM format. Run the following from the terminal to install sbt (You'll need superuser privileges to do so, hence the sudo).

# remove old Bintray repo file
sudo rm -f /etc/yum.repos.d/bintray-rpm.repo
curl -L https://www.scala-sbt.org/sbt-rpm.repo > sbt-rpm.repo
sudo mv sbt-rpm.repo /etc/yum.repos.d/
sudo yum install sbt

On Fedora (31 and above), use sbt-rpm.repo:

# remove old Bintray repo file
sudo rm -f /etc/yum.repos.d/bintray-rpm.repo
curl -L https://www.scala-sbt.org/sbt-rpm.repo > sbt-rpm.repo
sudo mv sbt-rpm.repo /etc/yum.repos.d/
sudo dnf install sbt