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 といった言語拡張を行うという強力な権限をコミュニティーに与えている。