sbt には、柔軟かつ強力なビルド定義(Build Definition)を支えるための独自の概念がいくつか存在している。 その概念は決して多くはないが、sbt は他のビルドシステムとは一味違うので、ドキュメントを読まずに使おうとすると、きっと細かい点でつまづいてしまうだろう。
この「始める sbt」では、sbt ビルド定義を作成してメンテナンスしていく上で知っておくべき概念を説明していく。
このガイドを一通り読んでおくことを強く推奨したい。
もしどうしても時間がないというなら、最も重要な概念は .sbt ビルド定義、 スコープ、と タスク・グラフ に書かれている。 ただし、それ以外のページを読み飛ばしても大丈夫かは保証できない。
このガイドの読み方だが、後ろの方のページはその前のページで紹介された概念の理解を前提に書かれているので、最初から順番に読み進めていくのがベストだ。
sbt を試してくれることに感謝する。ぜひ楽しいんでほしい!
誤訳の報告はこちらへ。
sbt0.13での変更点や新機能に興味があるなら、sbt 0.13.0 の変更点 を読むとよいだろう。
sbt プロジェクトを作るためには、以下の手順をたどる必要がある:
究極的には sbt のインストールはランチャー JAR とシェルスクリプトの 2 つを用意するだけだが、 利用するプラットフォームによってはもう少し簡単なインストール方法もいくつか提供されている。 macOS、Windows、もしくは Linux の手順を参照してほしい。
sbt
の実行が上手くいかない場合は、Setup Notes のターミナルの文字エンコーディング、HTTP プロキシ、JVM のオプションに関する説明を参照してほしい。
Install に従い Coursier を用いて Scala をインストールする。これは最新の安定版の sbt
を含む。
リンクをたどって JDK 8 もしくは JDK 11 をインストールする、 もしくは SDKMAN! を使う。
$ sdk install java $(sdk list java | grep -o "\b8\.[0-9]*\.[0-9]*\-tem" | head -1)
$ sdk install sbt
注意: サードパーティが提供するパッケージは最新版を使っているとは限らない。 何か問題があれば、パッケージメンテナに報告してほしい。
$ brew install sbt
Install に従い Coursier を用いて Scala をインストールする。これは最新の安定版の sbt
を含む。
リンクをたどって JDK 8 もしくは JDK 11 をインストールする。
msi インストーラをダウンロードしてインストールする。
注意: サードパーティが提供するパッケージは最新版を使っているとは限らない。 何か問題があれば、パッケージメンテナに報告してほしい。
$ scoop install sbt
Install に従い Coursier を用いて Scala をインストールする。これは最新の安定版の sbt
を含む。
JDK と sbt をするのに、SDKMAN の導入を検討してほしい。
$ sdk install java $(sdk list java | grep -o "\b8\.[0-9]*\.[0-9]*\-tem" | head -1)
$ sdk install sbt
Coursier もしくは SDKMAN を使うことには 2つの利点がある。
tgz
パッケージをインストールできる (DEB と RPM版は帯域の節約のために JAR ファイルが含まれていない)。
まず JDK をインストールする必要がある。Eclipse Adoptium Temurin JDK 8、JDK 11、もしくは JDK 17 を推奨する。
パッケージ名はディストリビューションによって異なる。例えば、Ubuntu xenial (16.04LTS) には openjdk-8-jdk がある。Redhat 系は java-1.8.0-openjdk-devel と呼んでいる。
DEB は sbt による公式パッケージだ。
Ubuntu 及びその他の Debian ベースのディストリビューションは DEB フォーマットを用いるが、
ローカルの DEB ファイルからソフトウェアをインストールすることは稀だ。
これらのディストロは通常コマンドラインや GUI 上から使えるパッケージ・マネージャがあって
(例: apt-get
、aptitude
、Synaptic など)、インストールはそれらから行う。
ターミナル上から以下を実行すると sbt
をインストールできる (superuser 権限を必要とするため、sudo
を使っている)。
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 apt-key add
sudo apt-get update
sudo apt-get install sbt
パッケージ・マネージャは設定されたリポジトリに指定されたパッケージがあるか確認しにいく。 このリポジトリをパッケージ・マネージャに追加しさえすればよい。
sbt
を最初にインストールした後は、このパッケージは aptitude
や Synaptic
上から管理することができる (パッケージ・キャッシュの更新を忘れずに)。
追加された APT リポジトリは「システム設定 -> ソフトウェアとアップデート -> 他のソフトウェア」 の一番下に表示されているはずだ:
注意: Ubuntu で Server access Error: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty url=https://repo1.maven.org/maven2/org/scala-sbt/sbt/1.1.0/sbt-1.1.0.pom
という SSL エラーが多く報告されている。cert-bug などによると、これは OpenJDK 9 が /etc/ssl/certs/java/cacerts
に PKCS12 フォーマットを採用したことに起因するらしい。https://stackoverflow.com/a/50103533/3827 によるとこの問題は Ubuntu Cosmic (18.10) で修正されているが、Ubuntu Bionic LTS (18.04) はリリース待ちらしい。回避策も Stackoverflow を参照。
RPM は sbt による公式パッケージだ。
Red Hat Enterprise Linux 及びその他の RPM ベースのディストリビューションは RPM フォーマットを用いる。
ターミナル上から以下を実行すると sbt
をインストールできる (superuser 権限を必要とするため、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 bintray-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
注意: これらのパッケージに問題があれば、 sbt プロジェクトに報告してほしい。
公式には sbt の ebuild は提供されていないが、 バイナリから sbt をマージする ebuild が公開されているようだ。 この ebuild を使って sbt をマージするには:
emerge dev-java/sbt
このページは、 sbt 1 をインストールしたことを前提とする。
sbt の内部がどうなっているかや理由みたいなことを解説する代わりに、例題を次々と見ていこう。
$ mkdir foo-build
$ cd foo-build
$ touch build.sbt
$ sbt
[info] Updated file /tmp/foo-build/project/build.properties: set sbt.version to 1.9.3
[info] welcome to sbt 1.9.3 (Eclipse Adoptium Java 17.0.8)
[info] Loading project definition from /tmp/foo-build/project
[info] loading settings for project foo-build from build.sbt ...
[info] Set current project to foo-build (in build file:/tmp/foo-build/)
[info] sbt server started at local:///Users/eed3si9n/.sbt/1.0/server/abc4fb6c89985a00fd95/sock
[info] started sbt server
sbt:foo-build>
sbt シェルを終了させるには、exit
と入力するか、Ctrl+D (Unix) か Ctrl+Z (Windows) を押す。
sbt:foo-build> exit
表記の慣例として sbt:...>
や >
というプロンプトは、sbt シェルに入っていることを意味することにする。
$ sbt
sbt:foo-build> compile
compile
コマンド (やその他のコマンド) を ~
で始めると、プロジェクト内のソース・ファイルが変更されるたびにそのコマンドが自動的に再実行される。
sbt:foo-build> ~compile
[success] Total time: 0 s, completed 28 Jul 2023, 13:32:35
[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
object Hello {
def main(args: Array[String]): Unit = {
println("Hello")
}
}
この新しいファイルは実行中のコマンドが自動的に検知したはずだ:
[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/scala-2.12/classes ...
[success] Total time: 0 s, completed 28 Jul 2023, 13:38:55
[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.Hello
Hello
[success] Total time: 0 s, completed 28 Jul 2023, 13:40:31
sbt:foo-build> set ThisBuild / scalaVersion := "2.13.12"
[info] Defining ThisBuild / scalaVersion
[info] The new value will be used by Compile / bspBuildTarget, Compile / dependencyTreeCrossProjectId and 50 others.
[info] Run `last` for details.
[info] Reapplying settings...
[info] set current project to foo-build (in build file:/tmp/foo-build/)
scalaVersion
セッティングを確認する:
sbt:foo-build> scalaVersion
[info] 2.13.12
アドホックに設定したセッティングは 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
ファイルは以下のようになったはずだ:
ThisBuild / scalaVersion := "2.13.12"
エディタを使って、build.sbt
を以下のように変更する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / organization := "com.example"
lazy val hello = (project in file("."))
.settings(
name := "Hello"
)
reload
コマンドを使ってビルドを再読み込みする。このコマンドは build.sbt
を読み直して、そこに書かれたセッティングを再適用する。
sbt:foo-build> reload
[info] welcome to sbt 1.9.3 (Eclipse Adoptium Java 17.0.8)
[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>
に変わったことに注目してほしい。
エディタを使って、build.sbt
を以下のように変更する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / organization := "com.example"
lazy val hello = project
.in(file("."))
.settings(
name := "Hello",
libraryDependencies += "org.scala-lang" %% "toolkit-test" % "0.1.7" % Test
)
reload
コマンドを使って、build.sbt
の変更を反映させる。
sbt:Hello> reload
sbt:Hello> test
sbt:Hello> ~testQuick
上のコマンドを走らせたままで、エディタから src/test/scala/example/HelloSuite.scala
という名前のファイルを作成する:
class HelloSuite extends munit.FunSuite {
test("Hello should start with H") {
assert("hello".startsWith("H"))
}
}
~testQuick
が検知したはずだ:
[info] 2. Monitoring source files for hello/testQuick...
[info] Press <enter> to interrupt or '?' for more options.
[info] Build triggered by /tmp/foo-build/src/test/scala/example/HelloSuite.scala. Running 'testQuick'.
[info] compiling 1 Scala source to /tmp/foo-build/target/scala-2.13/test-classes ...
HelloSuite:
==> X HelloSuite.Hello should start with H 0.004s munit.FailException: /tmp/foo-build/src/test/scala/example/HelloSuite.scala:4 assertion failed
3: test("Hello should start with H") {
4: assert("hello".startsWith("H"))
5: }
at munit.FunSuite.assert(FunSuite.scala:11)
at HelloSuite.$anonfun$new$1(HelloSuite.scala:4)
[error] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error] HelloSuite
[error] (Test / testQuick) sbt.TestsFailedException: Tests unsuccessful
エディタを使って src/test/scala/example/HelloSuite.scala
を以下のように変更する:
class HelloSuite extends munit.FunSuite {
test("Hello should start with H") {
assert("Hello".startsWith("H"))
}
}
テストが通過したことを確認して、Enter
を押して継続的テストを抜ける。
エディタを使って build.sbt
を以下のように変更する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / 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
)
)
New York の現在の天気を調べてみる:
sbt:Hello> console
[info] Starting scala interpreter...
Welcome to Scala 2.13.12 (OpenJDK 64-Bit Server VM, Java 17).
Type in expressions for evaluation. Or try :help.
scala> :paste
// Entering paste mode (ctrl-D to finish)
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¤t_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
を以下のように変更する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / 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:/private/tmp/foo-build/
[info] * hello
[info] helloCore
sbt:Hello> helloCore/compile
build.sbt
を以下のように変更する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / 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
にもブロードキャストするために集約を設定する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / 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
後、~testQuick
は両方のサブプロジェクトに作用する:
sbt:Hello> ~testQuick
Enter
を押して継続的テストを抜ける。
サブプロジェクト間の依存関係を定義するには .dependsOn(...)
を使う。ついでに、toolkit への依存性も helloCore
に移そう。
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / 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
)
helloCore
に uJson を追加しよう。
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / 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
)
reload
後、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¤t_weather=true"
)
.send()
val json = ujson.read(response.body)
json.obj("current_weather")("temperature").num
}
}
次に src/main/scala/example/Hello.scala
を以下のように変更する:
package example
import example.core.Weather
object Hello {
def main(args: Array[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.
エディタを使って project/plugins.sbt
を追加する:
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.4")
次に build.sbt
を以下のように変更して JavaAppPackaging
を追加する:
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / organization := "com.example"
val toolkitTest = "org.scala-lang" %% "toolkit-test" % "0.1.7"
lazy val hello = project
.in(file("."))
.aggregate(helloCore)
.dependsOn(helloCore)
.enablePlugins(JavaAppPackaging)
.settings(
name := "Hello",
libraryDependencies += toolkitTest % Test,
maintainer := "A Scala Dev!"
)
lazy val helloCore = project
.in(file("core"))
.settings(
name := "Hello Core",
libraryDependencies += "org.scala-lang" %% "toolkit" % "0.1.7",
libraryDependencies += toolkitTest % Test
)
sbt:Hello> reload
...
sbt:Hello> dist
[info] Wrote /private/tmp/foo-build/target/scala-2.13/hello_2.13-0.1.0-SNAPSHOT.pom
[info] Main Scala API documentation to /tmp/foo-build/target/scala-2.13/api...
[info] Main Scala API documentation successful.
[info] Main Scala API documentation to /tmp/foo-build/core/target/scala-2.13/api...
[info] Wrote /tmp/foo-build/core/target/scala-2.13/hello-core_2.13-0.1.0-SNAPSHOT.pom
[info] Main Scala API documentation successful.
[success] All package validations passed
[info] Your package is ready in /tmp/foo-build/target/universal/hello-0.1.0-SNAPSHOT.zip
パッケージ化されたアプリの実行は以下のように行う:
$ /tmp/someother
$ cd /tmp/someother
$ unzip -o -d /tmp/someother /tmp/foo-build/target/universal/hello-0.1.0-SNAPSHOT.zip
$ ./hello-0.1.0-SNAPSHOT/bin/hello
Hello! The current temperature in New York is 22.7 C.
``
### アプリを Docker化させる
sbt:Hello> Docker/publishLocal …. [info] Built image hello with tags [0.1.0-SNAPSHOT] ```
Docker化されたアプリは以下のように実行する:
`
$ docker run hello:0.1.0-SNAPSHOT
Hello! The current temperature in New York is 22.7 C.
build.sbt
を以下のように変更する:
ThisBuild / version := "0.1.0"
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / organization := "com.example"
val toolkitTest = "org.scala-lang" %% "toolkit-test" % "0.1.7"
lazy val hello = project
.in(file("."))
.aggregate(helloCore)
.dependsOn(helloCore)
.enablePlugins(JavaAppPackaging)
.settings(
name := "Hello",
libraryDependencies += toolkitTest % Test,
maintainer := "A Scala Dev!"
)
lazy val helloCore = project
.in(file("core"))
.settings(
name := "Hello Core",
libraryDependencies += "org.scala-lang" %% "toolkit" % "0.1.7",
libraryDependencies += toolkitTest % Test
)
sbt:Hello> ++3.3.1!
[info] Forcing Scala version to 3.3.1 on all projects.
[info] Reapplying settings...
[info] Set current project to Hello (in build file:/private/tmp/foo-build/)
scalaVersion
セッティングを確認する:
sbt:Hello> scalaVersion
[info] helloCore / scalaVersion
[info] 3.3.1
[info] scalaVersion
[info] 3.3.1
このセッティングは reload
後には無くなる。
dist
タスクのことをもっと調べるために、help
と inspect
を実行してみる。
sbt:Hello> help dist
Creates the distribution packages.
sbt:Hello> inspect dist
依存タスクに対して inspect
を再帰的に呼び出すには inspect tree
を使う。
sbt:Hello> inspect tree dist
[info] dist = Task[java.io.File]
[info] +-Universal / dist = Task[java.io.File]
....
sbt のコマンドをターミナルから直接渡して sbt をバッチモードで実行することができる。
$ sbt clean "testOnly HelloSuite"
Note: バッチモードでの実行は JVM のスピンアップと JIT を毎回行うため、ビルドかなり遅くなる。
普段のコーディングでは sbt シェル、
もしくは ~testQuick
のような継続的テストを使うことを推奨する。
sbt new
コマンドを使って手早く簡単な Hello world ビルドをセットアップすることができる。
$ sbt new sbt/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 用語では「ベースディレクトリ(base directory) 」はプロジェクトが入ったディレクトリを指す。
例題でみる sbt での例のように、/tmp/foo-build/build.sbt
が入った
hello
プロジェクトを作った場合、ベースディレクトリは /tmp/foo-build
となる。
sbt はデフォルトで Maven と同じディレクトリ構造を使う(全てのパスはベースディレクトリからの相対パスとする):
src/
main/
resources/
<メインの jar に含むデータファイル>
scala/
<メインの Scala ソースファイル>
scala-2.12/
<メインの Scala 2.12 に特定のソースファイル>
java/
<メインの Java ソースファイル>
test/
resources/
<テストの jar に含むデータファイル>
scala/
<テストの Scala ソースファイル>
scala-2.12/
<テストの Scala 2.12 に特定のソースファイル>
java/
<テストの Java ソースファイル>
src/
内の他のディレクトリは無視される。また、隠しディレクトリも無視される。
ソースコードは hello/app.scala
のようにプロジェクトのベースディレクトリに置くこともできるが、
小さいプロジェクトはともかくとして、通常のプロジェクトでは
src/main/
以下のディレクトリにソースを入れて整理するのが普通だ。
ベースディレクトリに *.scala
ソースコードを配置できるのは小手先だけのトリックに見えるかもしれないが、
この機能は後ほど重要になる。
ビルド定義はプロジェクトのベースディレクトリ以下の build.sbt
(実は *.sbt
ならどのファイルでもいい) にて記述する。
build.sbt
build.sbt
の他に、project
ディレクトリにはヘルパーオブジェクトや一点物のプラグインを定義した
*.scala
ファイルを含むことができる。
詳しくは、ビルドの整理を参照。
build.sbt
project/
Dependencies.scala
project
内に .sbt
があるのを見ることがあるかもしれないが、それはプロジェクトのベースディレクトリ下の .sbt
とはまた別物だ。
これに関しては他に前提となる知識が必要なので後ほど説明する。
生成されたファイル(コンパイルされたクラスファイル、パッケージ化された jar ファイル、managed 配下のファイル、キャッシュとドキュメンテーション)は、デフォルトでは target
ディレクトリに出力される。
.gitignore
(もしくは、他のバージョン管理システムの同様のファイル)には以下を追加しておくとよいだろう:
target/
ここでは(ディレクトリだけにマッチさせるために)語尾の /
は意図的につけていて、一方で
(普通の target/
に加えて project/target/
にもマッチさせるために)先頭の /
は意図的に
つけていないことに注意。
このページではプロジェクトをセットアップした後の sbt
の使い方を説明する。
君がsbt をインストールして、
例題でみる sbtを実行したことを前提とする。
プロジェクトのベースディレクトリで、sbt を引数なしで実行する:
$ sbt
sbt をコマンドライン引数なしで実行すると sbt シェルが起動される。 インタラクティブモードにはコマンドプロンプト(とタブ補完と履歴も!)がある。
例えば、compile
と sbt シェルに入力する:
> compile
もう一度 compile
するには、上矢印を押して、エンターキーを押す。
プログラムを実行するには、run
と入力する。
sbt シェルを終了するには、exit
と入力するか、Ctrl+D (Unix) か Ctrl+Z (Windows) を押す。
sbt のコマンドを空白で区切られたリストとして引数に指定すると sbt をバッチモードで実行することができる。
引数を取る sbt コマンドの場合は、コマンドと引数の両方を引用符で囲むことで一つの引数として sbt
に渡す。
例えば、
$ sbt clean compile "testOnly TestA TestB"
この例では、testOnly
は TestA
と TestB
の二つの引数を取る。
コマンドは順に実行される(この場合 clean
、compile
、そして testOnly
)。
Note: バッチモードでの実行は JVM のスピンアップと JIT を毎回行うため、ビルドかなり遅くなる。 普段のコーディングでは sbt シェル、 もしくは以下に説明する継続的ビルドとテストを使うことを推奨する。
編集〜コンパイル〜テストのサイクルを速めるために、ソースファイルを保存する度 sbt に自動的に再コンパイルを実行させることができる。
ソースファイルが変更されたことを検知してコマンドを実行するには、コマンドの先頭に ~
をつける。
例えば、インタラクティブモードで、これを試してみよう:
> ~testQuick
このファイル変更監視状態を止めるにはエンターキーを押す。
先頭の ~
は sbt シェルでもバッチモードでも使うことができる。
詳しくは、Triggered Execution 参照。
最もよく使われる sbt コマンドを紹介する。全ての一覧は Command Line Reference を参照。
clean | (target ディレクトリにある)全ての生成されたファイルを削除する。 |
compile | (src/main/scala と src/main/java ディレクトリにある) メインのソースをコンパイルする。 |
test | 全てのテストをコンパイルし実行する。 |
console | コンパイル済のソースと依存ライブラリにクラスパスを通して、Scala インタプリタを開始する。 sbt に戻るには、:quit と入力するか、Ctrl+D (Unix) か Ctrl+Z (Windows) を押す。 |
sbt と同じ仮想マシン上で、プロジェクトのメインクラスを実行する。 | |
package | src/main/resources 内のファイルと src/main/scala と src/main/java からコンパイルされたクラスファイルを含む jar を作る。 |
help <command> | 指定されたコマンドの詳細なヘルプを表示する。コマンドが指定されていない場合は、 全てのコマンドの簡単な説明を表示する。 |
reload | ビルド定義(build.sbt、 project/*.scala、 project/*.sbt ファイル)を再読み込みする。 ビルド定義を変更した場合に必要。 |
sbt シェルには、空のプロンプトの状態を含め、タブ補完がある。 sbt の特殊な慣例として、タブを一度押すとよく使われる候補だけが表示され、 複数回押すと、より多くの冗長な候補一覧が表示される。
sbt シェルは、 sbt を終了して再起動した後でも履歴を覚えている。 履歴にアクセスする最も簡単な方法は矢印キーを使うことだ。
注意: Ctrl-R
を使って履歴を逆方向にインクリメンタル検索できる。
JLine のターミナル環境との統合によって $HOME/.inputrc
ファイルを変更することで
sbt シェルをカスタマイズすることが可能だ。
例えば、以下の設定を $HOME/.inputrc
に書くことで上矢印キーと下矢印キーが履歴の前方一致検索をするようになる。
"\e[A": history-search-backward
"\e[B": history-search-forward
"\e[C": forward-char
"\e[D": backward-char
sbt シェルはその他にも以下のコマンドをサポートする:
! | 履歴コマンドのヘルプを表示する。 |
!! | 直前のコマンドを再実行する。 |
!: | 全てのコマンド履歴を表示する。 |
!:n | 最後の n コマンドを表示する。 |
!n | !: で表示されたインデックス n のコマンドを実行する。 |
!-n | n個前のコマンドを実行する。 |
!string | 'string' から始まる最近のコマンドを実行する。 |
!?string | 'string' を含む最近のコマンドを実行する。 |
エディタと sbt だけで Scala のコードを書くことも可能だが、今日日のプログラマの多くは統合開発環境 (IDE) を用いる。 Scala の IDE は Metals と IntelliJ IDEA の二強で、それぞれ sbt ビルドとの統合をサポートする。
Metals は、Scala のためのオープンソースな言語サーバであり、VS Code その他の LSP をサポートするエディタのバックエンドとして機能することができる。 一方で Metals は、Build Server Protocol (BSP) 経由で sbt を含む異なるビルドサーバをサポートする。
VS Code で Metals を使うには:
build.sbt
ファイルを含むディレクトリを開く。
Cmd-Shift-P
) を開き Metals: Switch build server と打ち込み、「sbt」を選択する。一部のサブプロジェクトを BSP へ入れたく無い場合は、以下のセッティングを使うことができる。
bspEnabled := false
コードに変更を加えて保存 (macOS だと Cmd-S
) すると、Metals は sbt を呼び出して実際のビルド作業を行う。
Igal Tabachnik さんの Using BSP effectively in IntelliJ and Scala という記事が参考になる。
インタラクティブ・デバッグが開始してからの操作方法の詳細は VS Code ドキュメンテーションの Debugging ページ参照。
Metals がビルドサーバとして sbt を使う間、シンクライアントを使って同じ sbt セッションにログインすることができる。
sbt --client
と打ち込む。これで Metals が開始した sbt セッションにログインすることができた。その中でコードが既にコンパイルされた状態から testOnly
その他のタスクを実行できる。
IntelliJ IDEA は JetBrains社が開発した IDE で、Community Edition は Apache v2 ライセンスの元でオープンソース化されている。 IntelliJ は sbt を含む多くのビルドツールと統合して、プロジェクトをインポートすることができる。 これは従来の方法で、BSP よりも多くの場合安定性が高い。
IntelliJ IDEA にビルドをインポートするには:
build.sbt
ファイルを含んだディレクトリを開く:IntelliJ Scala プラグインは独自の軽量コンパイラエンジンを用いてエラーの検知を行うが、これは高速であるが正しくないこともある。Compiler-based highlighting といって、 IntelliJ を Scala コンパイラを使ってエラー・ハイライトを行うように設定することも可能だ。
インタラクティブ・デバッグが開始してからの操作方法の詳細は IntelliJ ドキュメンテーションの Debug code ページ参照。
IntelliJ へビルドをインポートするということは、事実上 IntelliJ をビルドツールやコンパイラとして採用してコードを書いているということだ (compiler-based highlighting も参照)。 多くのユーザはそのエキスペリエンスで満足しているが、一方でコードベースによってはコンパイラエラーが間違っていたり、ソース生成を行うプラグインと動作しなかったり、sbt と同一のビルド意味論を用いてコードを書きたいと思う人もいる。 幸いなことに、現代の IntelliJ は Build Server Protocol (BSP) 経由で sbt を含む異なるビルドサーバをサポートする。
IntelliJ において BSP を使う利点は、実際のビルド作業を sbt を用いて行うため、今までも sbt セッションを立ち上げながら IntelliJ を使っていた人は、二重でコンパイルしなくてもよくなるという利点がある。
IntelliJ へインポート | BSP を使った IntelliJ | |
---|---|---|
信頼性 | ✅ 安定した動作 | ⚠️ 技術的に枯れていないため、UX 問題などにあう可能性がある |
応答性 | ✅ | ⚠️ |
正確性 | ⚠️ 独自のコンパイラを用いた型検査。scalac に設定することも可能。 | ✅ Zinc + Scala コンパイラを用いた型検査 |
❌ ソース生成するごとに再同期が必要 | ✅ | |
ビルド | ❌ sbt を併用すると二重ビルドが必要になる | ✅ |
IntelliJ のビルドサーバとして sbt を用いるには:
Cmd-Shift-P
) より「Existing」 と打ち込んで「Import Project From Existing Sources」を探す:build.sbt
ファイルを開く。ダイアログが表示されたら BSP を選択する:一部のサブプロジェクトを BSP へ入れたく無い場合は、以下のセッティングを使うことができる。
bspEnabled := false
コードに変更を加えて保存 (macOS だと Cmd-S
) すると、IntelliJ は sbt を呼び出して実際のビルド作業を行う。
シンクライアントを使って既存の sbt セッションにログインすることができる。
sbt --client
と打ち込む。これで IntelliJ が開始した sbt セッションにログインすることができた。その中でコードが既にコンパイルされた状態から testOnly
その他のタスクを実行できる。
Neovim は、Vim エディタのモダンなフォークで、組み込みで LSP をサポートしていたりする。 そのため Neovim は Metals のフロントエンドとして設定可能だ。
Metals メンテナの一人である Chris Kipps さんが nvim-metals というプラグインを作っており、これは Metals 機能を網羅的にサポートする。
nvim-metals をインストールするには、Chris Kipps さんの lsp.lua を元に
$XDG_CONFIG_HOME/nvim/lua/
以下に設定ファイルを書き、自分の好みに合わせていく。
例えば、vim-plug など別のプラグインマネージャを使っている場合はプラグインの部分をコメントアウトする必要がある。
init.vim
から以下のようにして読み込める:
lua << END
require('lsp')
END
lsp.lua
によると、g:metals_status
はステータスラインに表示させるべきと書いてあり、これは lualine.nvim などを使って実現できる。
:MetalsInstall
を実行する。
:MetalsStartServer
を実行する。
gD
を使ってジャンプできる (具体的なキーバインドは好みのものにカスタマイズできる):Ctrl-O
を使って古いバッファーに戻る。
K
を使う:<leader>aa
を使う::cnext
や :cprev
といったコマンドを使ってエラーや警告を見ていける。
<leader>ae
を使う。
<leader>dt
を用いてブレークポイントを設定していく:K
) で確認して、debug continue (<leader>dc
) でデバッガを開始する。
プロンプトが表示されたら、「1: RunOrTest」を選ぶ。
<leader>dK
) を使って変数の値を検査することができる:<leader>dc
) してセッションを終了させる。
詳細は nvim-metals 参考。
シンクライアントを使って既存の sbt セッションにログインすることができる。
:terminal
と打ち込んで組み込みのターミナルを立ち上げる。
sbt --client
と打ち込むNeovim の中だが、タブ補完なども普通に動作する。
このページでは、多少の「理論」も含めた sbt のビルド定義 (build definition) と build.sbt
の構文を説明する。
sbt 0.13.13 など最近のバージョンをインストール済みで、
sbt の使い方を分かっていて、「始める sbt」の前のページも読んだことを前提とする。
このページでは build.sbt
ビルド定義を紹介する。
ビルド定義の一部としてビルドに用いる sbt のバージョンを指定する。
これによって異なる sbt ランチャーを持つ複数の人がいても同じプロジェクトを同じようにビルドすることができる。
そのためには、project/build.properties
という名前のファイルを作成して以下のように
sbt バージョンを指定する:
sbt.version=1.9.8
もしも指定されたバージョンがローカルマシンに無ければ、
sbt
ランチャーは自動的にダウンロードを行う。
このファイルが無ければ、sbt
ランチャーは任意のバージョンを選択する。
これはビルドの移植性を下げるため、推奨されない。
ビルド定義は、build.sbt
にて定義され、プロジェクト (型は Project)
の集合によって構成される。
プロジェクトという用語が曖昧であることがあるため、このガイドではこれらをサブプロジェクトと呼ぶことが多い。
例えば、カレントディレクトリにあるサブプロジェクトは build.sbt
に以下のように定義できる:
lazy val root = (project in file("."))
.settings(
name := "Hello",
scalaVersion := "2.12.7"
)
それぞれのサブプロジェクトは、キーと値のペアによって詳細が設定される。
例えば、name
というキーがあるが、それはサブプロジェクト名という文字列の値に関連付けられる。
キーと値のペア列は .settings(...)
メソッド内に列挙される:
lazy val root = (project in file("."))
.settings(
name := "Hello",
scalaVersion := "2.12.7"
)
build.sbt
はどのように settings を定義するか build.sbt
において定義されるサブプロジェクトは、キーと値のペア列を持つと言ったが、
このペアはセッティング式 (setting expression) と呼ばれ、build.sbt DSL にて記述される。
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "hello"
)
build.sbt DSL を詳しくみてみよう:
それぞれのエントリーはセッティング式 (setting expression) と呼ばれる。
中にはタスク式と呼ばれるものもある。この違いはこのページの後で説明する。
セッティング式は以下の 3部から構成される:
:=
。
左辺値の name
、version
、および scalaVersion
はキーである。
キーは
SettingKey[T]
、
TaskKey[T]
、もしくは
InputKey[T]
のインスタンスで、
T
はその値の型である。キーの種類に関しては後述する。
name
キーは SettingKey[String]
に型付けされているため、
name
の :=
演算子も String
に型付けされている。
誤った型の値を使おうとするとビルド定義はコンパイルエラーになる:
lazy val root = (project in file("."))
.settings(
name := 42 // コンパイルできない
)
build.sbt
内には val
、lazy val
、def
を定義することもできる。
build.sbt
において、トップレベルで object
や class
を定義することはできない。
それらが必要なら project/
配下にScala ソースファイル (.scala
) を置くべきだろう。
キーには三種類ある:
SettingKey[T]
: 一度だけ値が計算されるキー(値はサブプロジェクトの読み込み時に計算され、保存される)。
TaskKey[T]
: 毎回再計算されるタスクを呼び出す、副作用を伴う可能性のある値のキー。
InputKey[T]
: コマンドラインの引数を入力として受け取るタスクのキー。
「始める sbt」では InputKey
を説明しないので、このガイドを終えた後で、Input Tasks を読んでみよう。
組み込みのキーは Keys と呼ばれるオブジェクトのフィールドにすぎない。
build.sbt
は、自動的に import sbt.Keys._
するため、sbt.Keys.name
は name
として参照することができる。
カスタムキーは settingKey
、 taskKey
、 inputKey
といった生成メソッドを用いて定義する。
どのメソッドでもキーに関連する型パラメータを必要とする。
キーの名前は val
で宣言された変数の名前がそのまま用いられる。
例として、新しく hello
と名づけたキーを定義してみよう。
lazy val hello = taskKey[Unit]("An example task")
実は .sbt
ファイルには、設定を記述するのに必要な val
や def
を含めることもできる。
これらの定義はファイル内のどこで書かれてもプロジェクトの設定より前に評価される。
注意 一般的に、初期化順問題を避けるために val の代わりに lazy val が用いられることが多い。
TaskKey[T]
は、タスクを定義しているといわれる。タスクは、compile
や package
のような作業だ。
タスクは Unit
を返すかもしれないし(Unit
は、Scala での void
だ)、
タスクに関連した値を返すかもしれない。例えば、package
は作成した jar ファイルを値として返す TaskKey[File]
だ。
例えばインタラクティブモードの sbt プロンプトに compile
と入力するなど、何らかのタスクを実行する度に、
sbt はそのタスクを一回だけ再実行する。
サブプロジェクトを記述する sbt のキーと値の列は、name
のようなセッティング (setting) であれば、
その文字列の値をキャッシュすることができるが、
compile
のようなタスク(task)の場合は実行可能コードを保持しておく必要がある
(たとえその実行可能コードが最終的に文字列を返したとしても、それは毎回再実行されなければならない)。
あるキーがあるとき、それは常にタスクかただのセッティングかのどちらかを参照する。 つまり、キーの「タスク性」(毎回再実行するかどうか)はそのキーの特性であり、その値にはよらない。
:=
を使うことで、タスクに任意の演算を代入することができる。
セッティングを定義すると、その値はプロジェクトがロードされた時に一度だけ演算が行われる。
タスクを定義すると、その演算はタスクの実行毎に毎回再実行される。
例えば、少し前に宣言した hello
というタスクはこのように実装できる:
lazy val hello = taskKey[Unit]("An example task")
lazy val root = (project in file("."))
.settings(
hello := { println("Hello!") }
)
セッティングの定義は既に何度か見ていると思うが、プロジェクト名の定義はこのようにできる:
lazy val root = (project in file("."))
.settings(
name := "hello"
)
型システムの視点から考えると、タスクキー (task key) から作られた Setting
は、セッティングキー (setting key) から作られたそれとは少し異なるものだ。
taskKey := 42
は Setting[Task[T]]
の戻り値を返すが、settingKey := 42
は Setting[T]
の戻り値を返す。
タスクが実行されるとタスクキーは型T
の値を返すため、ほとんどの用途において、これによる影響は特にない。
T
と Task[T]
の型の違いによる影響が一つある。
それは、セッティングキーはキャッシュされていて、再実行されないため、タスキキーに依存できないということだ。
このことについては、後ほどのタスク・グラフにて詳しくみていく。
sbt のインタラクティブモードからタスクの名前を入力することで、どのタスクでも実行することができる。
それが compile
と入力することでコンパイルタスクが起動する仕組みだ。つまり、compile
はタスクキーだ。
タスクキーのかわりにセッティングキーの名前を入力すると、セッティングキーの値が表示される。
タスクキーの名前を入力すると、タスクを実行するが、その戻り値は表示されないため、
タスクの戻り値を表示するには素の <タスク名>
ではなく、show <タスク名>
と入力する。
Scala の慣例にならい、ビルド定義ファイル内ではキーはキャメルケース(camelCase
)で命名する。
あるキーについてより詳しい情報を得るには、sbt インタラクティブモードで inspect <キー名>
と入力する。
inspect
が表示する情報の中にはまだよく分からない点もあるかもしれないが、一番上にはセッティングの値の型と、セッティングの簡単な説明が表示されていることだろう。
build.sbt
内の import 文 build.sbt
の一番上に import 文を書くことができ、それらは空行で分けなくてもよい。
デフォルトでは以下のものが自動的にインポートされる:
import sbt._
import Keys._
(さらに、auto plugin があれば autoImport
以下の名前がインポートされる。)
セッティングは、.settings(...)
の呼び出しの中だけではなく build.sbt
に直書きすることができ、
これは 「bare style」と呼ばれる。
ThisBuild / version := "1.0"
ThisBuild / scalaVersion := "2.12.18"
この構文は ThisBuild
にスコープ付けされたセッティングを書いたり、プラグインを追加するのに向いている。
スコープやプラグインに関してはまた後ほど。
サードパーティのライブラリに依存するには二つの方法がある。
第一は lib/
に jar ファイルを入れてしまう方法で(アンマネージ依存性、unmanged dependency)、
第二はマネージ依存性(managed dependency)を加えることで、build.sbt
ではこのようになる:
val derby = "org.apache.derby" % "derby" % "10.4.1.3"
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "Hello",
libraryDependencies += derby
)
これで Apache Derby ライブラリのバージョン 10.4.1.3 へのマネージ依存性を加えることができた。
libraryDependencies
キーは二つの複雑な点がある:
:=
ではなく +=
を使うことと、%
メソッドだ。
後でタスク・グラフで説明するが、+=
はキーの古い値を上書きする代わりに新しい値を追加する。
%
メソッドは文字列から Ivy モジュール ID を構築するのに使われ、これはライブラリ依存性で説明する。
ライブラリ依存性に関する詳細については、このガイドの後ろの方までとっておくことにする。 後ほど一ページを割いて丁寧に説明する。
このページでは、一つのビルドで複数のサブプロジェクトを管理する方法を紹介する。 このガイドのこれまでのページを読んでおいてほしい。 特に build.sbt を理解していることが必要になる。
一つのビルドに複数の関連するサブプロジェクトを入れておくと、 サブプロジェクト間に依存性がある場合や同時に変更されることが多い場合に便利だ。
ビルド内の個々のサブプロジェクトは、それぞれ独自のソースディレクトリを持ち、
package
を実行すると独自の jar ファイルを生成するなど、概ね通常のプロジェクトと同様に動作する。
個々のプロジェクトは lazy val を用いて Project 型の値を宣言することで定義される。例として、以下のようなものがプロジェクトだ:
lazy val util = (project in file("util"))
lazy val core = (project in file("core"))
val で定義された名前はプロジェクトの ID 及びベースディレクトリの名前になる。 ID は sbt シェルからプロジェクトを指定する時に用いられる。
ベースディレクトリ名が ID と同じ名前であるときは省略することができる。
lazy val util = project
lazy val core = project
複数プロジェクトに共通なセッティングをくくり出す場合、
セッティングを ThisBuild
にスコープ付けする。
ただし、右辺値には純粋な値か Global
もしくは ThisBuild
にスコープ付けされたセッティングしか置くことができない、
またサブプロジェクトにスコープ付けされたセッティングがデフォルトで存在しない必要があるというという制約がある。
(スコープ参照)
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val core = (project in file("core"))
.settings(
// other settings
)
lazy val util = (project in file("util"))
.settings(
// other settings
)
これで version
を一箇所で変更すれば、再読み込み後に全サブプロジェクトに反映されるようになる。
複数プロジェクトに共通なセッティングをくくり出す場合、
commonSettings
という名前のセッティングの Seq を作って、
それを引数として各プロジェクトの settings
メソッドを呼び出せばよい。
lazy val commonSettings = Seq(
target := { baseDirectory.value / "target2" }
)
lazy val core = (project in file("core"))
.settings(
commonSettings,
// other settings
)
lazy val util = (project in file("util"))
.settings(
commonSettings,
// other settings
)
一つのビルドの中の個々のプロジェクトはお互いに完全に独立した状態であってもよいが、
普通、何らかの形で依存関係を持っているだろう。
ここでは集約(aggregate
)とクラスパス(classpath
)という二種類の依存関係がある。
集約とは、集約する側のプロジェクトであるタスクを実行するとき、集約される側の複数のプロジェクトでも同じタスクを実行するという関係を意味する。例えば、
lazy val root = (project in file("."))
.aggregate(util, core)
lazy val util = (project in file("util"))
lazy val core = (project in file("core"))
上の例では、root
プロジェクトが util
と core
を集約している。
この状態で sbt を起動してコンパイルしてみよう。
3 つのプロジェクトが全てコンパイルされることが分かると思う。
集約プロジェクト内で(この場合は root
プロジェクトで)、
タスクごとに集約をコントロールすることができる。
例えば、update
タスクの集約を以下のようにして回避できる:
lazy val root = (project in file("."))
.aggregate(util, core)
.settings(
aggregate in update := false
)
[...]
aggregate in update
は、update
タスクにスコープ付けされた aggregate
キーだ
(スコープ参照)。
注意: 集約は、集約されるタスクを順不同に並列実行する。
あるプロジェクトが、他のプロジェクトにあるコードに依存させたい場合、
dependsOn
メソッドを呼び出して実現すればよい。
例えば、core
に util
のクラスパスが必要な場合は core
の定義を次のように書く:
lazy val core = project.dependsOn(util)
これで core
内のコードから util
の class を利用することができるようになった。
また、これにより core
がコンパイルされる前に util
の update
と compile
が実行されている必要があるので
プロジェクト間でコンパイル実行が順序付けられることになる。
複数のプロジェクトに依存するには、dependsOn(bar, baz)
というふうに、
dependsOn
に複数の引数を渡せばよい。
foo dependsOn(bar)
は、foo
の Compile
コンフィギュレーションが
bar
の Compile
コンフィギュレーションに依存することを意味する。
これを明示的に書くと、dependsOn(bar % "compile->compile")
となる。
この "compile->compile"
内の ->
は、「依存する」という意味で、
"test->compile"
は、foo
の Test
コンフィギュレーションが
bar
の Compile
コンフィギュレーションに依存することを意味する。
->config
の部分を省くと、->compile
だと解釈されるため、
dependsOn(bar % "test")
は、foo
の Test
コンフィギュレーションが
bar
の Compile
コンフィギュレーションに依存することを意味する。
特に、Test
が Test
に依存することを意味する "test->test"
は役に立つ宣言だ。
これにより、例えば、bar/src/test/scala
にテストのためのユーティリティコードを
置いておき、それを foo/src/test/scala
内のコードから利用することができる。
複数のコンフィギュレーション依存性を宣言する場合は、セミコロンで区切る。
例えば、dependsOn(bar % "test->test;compile->compile")
と書ける。
多くのファイルとサブプロジェクトを持った巨大なビルドでは sbt は全てのファイルを監視したり、大量に発生するディスクやシステム I/O によって高性能とは言えない反応になるかもしれない。
一つの対策として sbt は compile
を呼び出した時に依存するサブプロジェクトのコンパイルを
行うかどうかを制御する trackInternalDependencies
と exportToInternal
というセッティングがある。両者とも
TrackLevel.NoTracking
、TrackLevel.TrackIfMissing
、TrackLevel.TrackAlways
という 3つの値を取ることができる。デフォルトは両方とも TrackLevel.TrackAlways
だ。
trackInternalDependencies
が TrackLevel.TrackIfMissing
に設定されると、sbt は *.class
ファイル
(exportJars
が true
の場合は JAR ファイル)
が一切無い場合を除き自動的に内部 (サブプロジェクト) 依存性をコンパイルすることを止める。
TrackLevel.NoTracking
に設定すると内部依存性のコンパイルは無視される。
ただし、クラスパスは通常どおり追加されるため、依存性グラフは依存性だと表示する。
この動機は開発時に大量のサブプロジェクトの変更の確認に伴う I/O
オーバーヘッドを回避することにある。全てのサブプロジェクトを TrackIfMissing
に設定する方法を以下に示す。
ThisBuild / trackInternalDependencies := TrackLevel.TrackIfMissing
ThisBuild / exportJars := true
lazy val root = (project in file("."))
.aggregate(....)
exportToInternal
セッティングは依存された側から内部トラッキングをオプトアウトすることを可能にして、
これを使うことでほとんどのサブプロジェクトは追跡したいが、一部を抜きたいという時に使える。
trackInternalDependencies
と exportToInternal
の交叉が実際の追跡レベルを決定する。
以下が 1つのサブプロジェクトをオプトアウトさせる例だ:
lazy val dontTrackMe = (project in file("dontTrackMe"))
.settings(
exportToInternal := TrackLevel.NoTracking
)
もしプロジェクトがルートディレクトリに定義されてなかったら、 sbt はビルド時に他のプロジェクトを集約するデフォルトプロジェクトを勝手に生成する。
プロジェクト hello-foo
は、base = file("foo")
と共に定義されているため、
サブディレクトリ foo
に置かれる。
そのソースは、foo/Foo.scala
のように foo
の直下に置かれるか、
foo/src/main/scala
内に置かれる。
ビルド定義ファイルを除いては、通常の sbt ディレクトリ構造が foo
以下に適用される。
sbt インタラクティブプロンプトから、projects
と入力することでプロジェクトの全リストが表示され、
project <プロジェクト名>
で、カレントプロジェクトを選択できる。
compile
のようなタスクを実行すると、それはカレントプロジェクトに対して実行される。
これにより、ルートプロジェクトをコンパイルせずに、サブプロジェクトのみをコンパイルすることができる。
また subProjectID/compile
のように、プロジェクト ID を明示的に指定することで、そのプロジェクトのタスクを実行することもできる。
.sbt
ファイルで定義された値は、他の .sbt
ファイルからは見えない。 .sbt
ファイル間でコードを共有するためには、 ベースディレクトリにある project/
配下に Scala ファイルを用意すればよい。
詳細はビルドの整理を参照。
foo
内の全ての .sbt
ファイル、例えば foo/build.sbt
は、
hello-foo
プロジェクトにスコープ付けされた上で、ビルド全体のビルド定義に取り込まれる。
ルートプロジェクトが hello
にあるとき、hello/build.sbt
、hello/foo/build.sbt
、
hello/bar/build.sbt
においてそれぞれ別々のバージョンを定義してみよう(例: version := "0.6"
)。
次に、インタラクティブプロンプトで show version
と打ち込んでみる。
以下のように表示されるはずだ(定義したバージョンによるが):
> show version
[info] hello-foo/*:version
[info] 0.7
[info] hello-bar/*:version
[info] 0.9
[info] hello/*:version
[info] 0.5
hello-foo/*:version
は、hello/foo/build.sbt
内で定義され、
hello-bar/*:version
は、hello/bar/build.sbt
内で定義され、
hello/*:version
は、hello/build.sbt
内で定義される。
スコープ付けされたキーの構文を復習しておこう。
それぞれの version
キーは、build.sbt
の場所により、
特定のプロジェクトにスコープ付けされている。
だが、三つの build.sbt
とも同じビルド定義の一部だ。
スタイルの選択:
*.sbt
ファイル内で宣言することができる。その場合、build.sbt
は lazy val foo = (project in file("foo"))
といった形で最小の project 宣言のみを行いセッティングは書かない。
build.sbt
に書けば全てのビルド定義を 1つのファイルにまとめることができるので、その方法を推奨する。ただし、これは好みの問題だから、好きにやっていい。
注意: サブプロジェクトは、project
サブディレクトリや、project/*.scala
ファイルを持つことができない。
foo/project/Build.scala
は無視される。
ビルド定義に引き続き、このページでは build.sbt
定義をより詳しく解説する。
settings
をキーと値のペア群だと考えるよりも、
より良いアナロジーは、辺を事前発生 (happens-before) 関係とするタスクの有向非巡回グラフ (DAG)
だと考える事だ。
これをタスク・グラフと呼ぼう。
重要な用語をおさらいしておく。
.settings(...)
内のエントリー。
SettingKey[A]
、 TaskKey[A]
、もしくは InputKey[A]
となる。
SettingKey[A]
を持つセッティング式によって定義される。値はロード時に一度だけ計算される。
TaskKey[A]
を持つタスク式によって定義される。値は呼び出さるたびに計算される。
build.sbt
DSL では .value
メソッドを用いて他のタスクやセッティングへの依存性を表現する。
この value
メソッドは特殊なもので、:=
(もしくは後に見る +=
や ++=
) の右辺項内でしか使うことができない。
最初の例として、update
と clean
というタスクに依存した形で
scalacOption
を定義したいとする。
(Keys より)以下の二つのキーを例に説明する。
注意: ここで計算される scalacOptions
の値はナンセンスなもので、説明のためだけのものだ:
val scalacOptions = taskKey[Seq[String]]("Options for the Scala compiler.")
val update = taskKey[UpdateReport]("Resolves and optionally retrieves dependencies, producing a report.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")
以下のように scalacOptions
を再配線できる:
scalacOptions := {
val ur = update.value // update タスクは scalacOptions よりも事前発生する
val x = clean.value // clean タスクは scalacOptions よりも事前発生する
// ---- scalacOptions はここから始まる ----
ur.allConfigurations.take(3)
}
update.value
と clean.value
はタスク依存性を宣言していて、
ur.allConfigurations.take(3)
がタスクの本文となる。
.value
は普通の Scala のメソッド呼び出しではない。
build.sbt
DSL はマクロを用いてこれらをタスクの本文から持ち上げる。
update
と clean
の両タスクとも、本文内のどの行に現れようと、
タスクエンジンが scalacOption
の開始中括弧 ({
) を評価するときには既に完了済みである。
具体例で説明しよう:
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "Hello",
scalacOptions := {
val out = streams.value // streams タスクは scalacOptions よりも事前発生する
val log = out.log
log.info("123")
val ur = update.value // update タスクは scalacOptions よりも事前発生する
log.info("456")
ur.allConfigurations.take(3)
}
)
次に、sbt シェル内で scalacOptions
と打ち込む:
> scalacOptions
[info] Updating {file:/xxx/}root...
[info] Resolving jline#jline;2.14.1 ...
[info] Done updating.
[info] 123
[info] 456
[success] Total time: 0 s, completed Jan 2, 2017 10:38:24 PM
val ur = ...
は log.info("123")
と
log.info("456")
の間に挟まっているが、
update
タスクは両者よりも事前発生している。
もう一つの例:
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.18"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "Hello",
scalacOptions := {
val ur = update.value // update task happens-before scalacOptions
if (false) {
val x = clean.value // clean task happens-before scalacOptions
}
ur.allConfigurations.take(3)
}
)
sbt シェル内で run
それから scalacOptions
と打ち込む:
> run
[info] Updating {file:/xxx/}root...
[info] Resolving jline#jline;2.14.1 ...
[info] Done updating.
[info] Compiling 1 Scala source to /Users/eugene/work/quick-test/task-graph/target/scala-2.12/classes...
[info] Running example.Hello
hello
[success] Total time: 0 s, completed Jan 2, 2017 10:45:19 PM
> scalacOptions
[info] Updating {file:/xxx/}root...
[info] Resolving jline#jline;2.14.1 ...
[info] Done updating.
[success] Total time: 0 s, completed Jan 2, 2017 10:45:23 PM
ここで target/scala-2.12/classes/
を探してみてほしい。
if (false)
に囲まれていても clean
タスクが実行されたため、そのディレクトリは存在しないはずだ。
もう一つ重要なのは、update
と clean
のタスクの間では順序付けの保証が無いことだ。
update
してから clean
が実行されるかもしれないし、
clean
してから update
が実行されるかもしれないし、
両者が並列に実行される可能性もある。
上で解説したように、.value
は他のタスクやセッティングへの依存性を表現するための特殊なメソッドだ。
build.sbt に慣れるまでは、.value
の呼び出しをタスク本文の一番上にまとめておくことをお勧めする。
しかし、慣れてくると .value
呼び出しをインライン化して、
タスクやセッティングを簡略に書きたいと思うようになるだろう。
変数名をいちいち考えなくてもいいのも楽だ。
インライン化するとこう書ける:
scalacOptions := {
val x = clean.value
update.value.allConfigurations.take(3)
}
.value
の呼び出しがインライン化されていようが、タスク本文内のどこに書かれていても
タスク本文に入る前に評価は完了する。
上の例では scalacOptions
は update
と clean
というタスクに依存性 (dependency) を持つ。
上のタスクを build.sbt
に書いて、sbt シェル内から inspect scalacOptions
と打ち込むと以下のように表示される (一部抜粋):
> inspect scalacOptions
[info] Task: scala.collection.Seq[java.lang.String]
[info] Description:
[info] Options for the Scala compiler.
....
[info] Dependencies:
[info] *:clean
[info] *:update
....
これは sbt が、どのセッティングが他のセッティングに依存しているかをどう把握しているかを示している。
また、inspect tree compile
と打ち込むと、compile
は incCompileSetup
に依存していて、それは dependencyClasspath
などの他のキーに依存していることが分かる。
依存性の連鎖をたどっていくと、魔法に出会う。
> inspect tree compile
[info] compile:compile = Task[sbt.inc.Analysis]
[info] +-compile:incCompileSetup = Task[sbt.Compiler$IncSetup]
[info] | +-*/*:skip = Task[Boolean]
[info] | +-compile:compileAnalysisFilename = Task[java.lang.String]
[info] | | +-*/*:crossPaths = true
[info] | | +-{.}/*:scalaBinaryVersion = 2.12
[info] | |
[info] | +-*/*:compilerCache = Task[xsbti.compile.GlobalsCache]
[info] | +-*/*:definesClass = Task[scala.Function1[java.io.File, scala.Function1[java.lang.String, Boolean]]]
[info] | +-compile:dependencyClasspath = Task[scala.collection.Seq[sbt.Attributed[java.io.File]]]
[info] | | +-compile:dependencyClasspath::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | | +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | |
[info] | | +-compile:externalDependencyClasspath = Task[scala.collection.Seq[sbt.Attributed[java.io.File]]]
[info] | | | +-compile:externalDependencyClasspath::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | | | +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info] | | | |
[info] | | | +-compile:managedClasspath = Task[scala.collection.Seq[sbt.Attributed[java.io.File]]]
[info] | | | | +-compile:classpathConfiguration = Task[sbt.Configuration]
[info] | | | | | +-compile:configuration = compile
[info] | | | | | +-*/*:internalConfigurationMap = <function1>
[info] | | | | | +-*:update = Task[sbt.UpdateReport]
[info] | | | | |
....
例えば compile
と打ち込むと、sbt は自動的に update
を実行する。
これが「とにかくちゃんと動く」理由は、compile
の計算に入力として必要な値が sbt に update
の計算を先に行うことを強制しているからだ。
このようにして、sbt の全てのビルドの依存性は、明示的には宣言されず、自動化されている。 あるキーの値を別の計算で使うと、その計算はキーに依存することになる。
scalacOptions
はタスク・キーだ。
何らかの値に既に設定されていて、Scala 2.12 以外の場合は
"-Xfatal-warnings"
と "-deprecation"
を除外したいとする。
lazy val root = (project in file("."))
.settings(
name := "Hello",
organization := "com.example",
scalaVersion := "2.12.18",
version := "0.1.0-SNAPSHOT",
scalacOptions := List("-encoding", "utf8", "-Xfatal-warnings", "-deprecation", "-unchecked"),
scalacOptions := {
val old = scalacOptions.value
scalaBinaryVersion.value match {
case "2.12" => old
case _ => old filterNot (Set("-Xfatal-warnings", "-deprecation").apply)
}
}
)
sbt シェルで試すとこうなるはずだ:
> show scalacOptions
[info] * -encoding
[info] * utf8
[info] * -Xfatal-warnings
[info] * -deprecation
[info] * -unchecked
[success] Total time: 0 s, completed Jan 2, 2017 11:44:44 PM
> ++2.11.8!
[info] Forcing Scala version to 2.11.8 on all projects.
[info] Reapplying settings...
[info] Set current project to Hello (in build file:/xxx/)
> show scalacOptions
[info] * -encoding
[info] * utf8
[info] * -unchecked
[success] Total time: 0 s, completed Jan 2, 2017 11:44:51 PM
次に (Keys より) 以下の二つのキーを例に説明する:
val scalacOptions = taskKey[Seq[String]]("Options for the Scala compiler.")
val checksums = settingKey[Seq[String]]("The list of checksums to generate and to verify for dependencies.")
注意: scalacOptions
と checksums
はお互い何の関係もない、ただ同じ値の型を持つ二つのキーで片方がタスクというだけだ。
build.sbt
の中で scalacOptions
を checksums
のエイリアスにすることはできるが、その逆はできない。例えば、以下の例はコンパイルが通る:
// scalacOptions タスクは checksums セッティングの値を用いて定義される
scalacOptions := checksums.value
逆方向への依存、つまりタスクの値に依存したセッティングキーの値を定義することはどうしてもできない。 なぜなら、セッティングキーの値はプロジェクトのロード時に一度だけしか計算されず、毎回再実行されるべきタスクが毎回実行されなくなってしまうからだ。
// 悪い例: checksums セッティングは scalacOptions タスクに関連付けて定義することはできない!
checksums := scalacOptions.value
実行のタイミングという観点から見ると、セッティングはロード時に評価される特殊なタスクと考えることができる。
プロジェクトの名前と同じ organization
を定義してみよう。
// プロジェクトの name に基いて organization 名を付ける (どちらも型は SettingKey[String])
organization := name.value
実用的な例もみてみる。
これは Compile / scalaSource
というキーを scalaBinaryVersion
が "2.11"
の場合のみ別のディレクトリに再配線する。
Compile / scalaSource := {
val old = (Compile / scalaSource).value
scalaBinaryVersion.value match {
case "2.11" => baseDirectory.value / "src-2.11" / "main" / "scala"
case _ => old
}
}
build.sbt
DSL は、セッティングやタスクの有向非巡回グラフを構築するためのドメイン特化言語だ。
セッティング式はセッティング、タスク、そしてそれらの間の依存性をエンコードする。
この構造は Make (1976)、 Ant (2000)、 Rake (2003) などにも共通する。
Makefile の基本的な構文は以下のようになる:
target: dependencies
[tab] system command1
[tab] system command2
対象 (target、デフォルトの target は all
と呼ばれる) が与えられたとき、
Makefile
の具体例で説明しよう:
CC=g++
CFLAGS=-Wall
all: hello
hello: main.o hello.o
$(CC) main.o hello.o -o hello
%.o: %.cpp
$(CC) $(CFLAGS) -c $< -o $@
make
を実行すると、デフォルトで all
という名前の対象を選択する。
その対象は hello
を依存性として列挙するが、それは未だビルドされいないので、Make は次に hello
をビルドする。
次に、Make は hello
という対象の依存性がビルド済みかを調べる。
hello
は main.o
と hello.o
という 2つの対象を列挙する。
これらの対象が最後のパターンマッチを用いたルールによってビルドされた後でやっと
main.o
と hello.o
をリンクするシステムコマンドが実行される。
make
を実行しているだけなら、対象として何がほしいのかだけを考えればよく、
中間成果物をビルドするための正確なタイミングやコマンドなどは Make がやってくれる。
これを依存性指向プログラミングもしくはフローベースプログラミングだと考えることができる。
DSL は対象の依存性を記述するが、アクションはシステムコマンドに委譲されるため、正確には
Make はハイブリッドシステムに分類される。
このハイブリッド性も実は Make の後継である Ant、Rake、sbt といったツールにも受け継がれている。 Rakefile の基本的な構文をみてほしい:
task name: [:prereq1, :prereq2] do |t|
# actions (may reference prereq as t.name etc)
end
Rake でのブレークスルーは、アクションをシステムコマンドの代わりにプログラミング言語を使って記述したことだ。
ビルドをこのように構成する動機がいくつかある。
第一は非重複化だ。フローベースプログラミングではあるタスクが複数のタスクから依存されていても一度だけしか実行されない。
例えば、タスクグラフ上の複数のタスクが Compile / compile
に依存していたとしても、実際のコンパイルは唯一一回のみ実行される。
第二は並列処理だ。タスクグラフを用いることでタスクエンジンは相互に非依存なタスクを並列にスケジュールすることができる。
第三は関心事の分離と柔軟さだ。 タスクグラフはビルドの作者が複数のタスクを異なる方法で配線することを可能にする。 一方、sbt やプラグインはコンパイルやライブラリ依存性の管理といった機能を再利用な形で提供できる。
ビルド定義のコアなデータ構造は、辺を事前発生 (happens-before) 関係とするタスクの DAG だ。
build.sbt
は、依存性指向プログラミングもしくはフローベースプログラミングを表現するための DSL で、Makefile
や Rakefile
に似ている。
フローベースプログラミングを行う動機は、非重複化、並列処理、とカスタム化の容易さだ。
このページではスコープの説明をする。前のページの .sbt ビルド定義、 タスク・グラフ を読んで理解したことを前提とする。
前のページでは、あたかも name
のようなキーは単一の sbt の Map のキー・値ペアの項目に対応するかのように説明をしてきた。
しかし、それは実際よりも物事を単純化している。
実のところ、全てのキーは「スコープ」と呼ばれる文脈に関連付けられた値を複数もつことができる。
以下に具体例で説明する:
compile
キーは別の値をとることができる。
packageOption
キーはクラスファイルのパッケージ(packageBin
)とソースコードのパッケージ(packageSrc
)で異なる値をとることができる。
スコープによって値が異なる可能性があるため、あるキーへの単一の値は存在しない。
しかし、スコープ付きキーには単一の値が存在する。
これまで見てきたように sbt がプロジェクトを記述するキーと値のマップを生成するためのセッティングキーのリストを処理していると考えるなら、
そのキーと値の Map におけるキーとは、実はスコープ付きキーである。
また、(build.sbt
などの)ビルド定義内のセッティングもまたスコープ付きキーである。
スコープは、暗黙に存在していたり、デフォルトのものがあったりするが、
もしそのデフォルトが適切でなければ build.sbt
で必要なスコープを指定する必要があるだろう。
スコープ軸(scope axis)は、Option[A]
に似た型コンストラクタであり、
スコープの各成分を構成する。
スコープ軸は三つある:
軸という概念に馴染みがなければ、RGB 色空間を例に取ってみるといいかもしれない。
RGB 色モデルにおいて、全ての色は赤、緑、青の成分を軸とする立方体内の点として表すことができ、それぞれの成分は数値化することができる。 同様に、sbt におけるスコープはサブプロジェクト、コンフィギュレーション、タスクのタプルにより成り立つ:
projA / Compile / console / scalacOptions
これは以下のスコープ付きキーを sbt 1.1 で導入されたスラッシュ構文で書いたものだ:
scalacOptions in (
Select(projA: Reference),
Select(Compile: ConfigKey),
Select(console.key)
)
一つのビルドに複数のプロジェクトを入れる場合、それぞれのプロジェクトにセッティングが必要だ。 つまり、キーはプロジェクトによりスコープ付けされる。
プロジェクト軸は ThisBuild
という「ビルド全体」を表す値に設定することもでき、その場合はセッティングは単一のプロジェクトではなくビルド全体に適用される。
ビルドレベルでのセッティングは、プロジェクトが特定のセッティングを定義しない場合のフォールバックとして使われることがよくある。
依存性コンフィギュレーション(dependency configuration、もしく単に「コンフィギュレーション」) は、ライブラリ依存性のグラフを定義し、独自のクラスパス、ソース、生成パッケージなどをもつことができる。 コンフィギュレーションの概念は、sbt が マネージ依存性 に使っている Ivy と、MavenScopes に由来する。
sbt で使われる代表的なコンフィギュレーションには以下のものがある:
Compile
は、メインのビルド(src/main/scala
)を定義する。
Test
は、テスト(src/test/scala
)のビルド方法を定義する。
Runtime
は、run
タスクのクラスパスを定義する。
デフォルトでは、コンパイル、パッケージ化と実行に関するキーの全ては依存性コンフィグレーションにスコープ付けされているため、
依存性コンフィギュレーションごとに異なる動作をする可能性がある。
その最たる例が compile
、package
と run
のタスクキーだが、
(sourceDirectories
や scalacOptions
や fullClasspath
など)それらのキーに影響を及ぼす全てのキーもコンフィグレーションにスコープ付けされている。
もう一つコンフィギュレーションで大切なのは、他のコンフィギュレーションを拡張できることだ。 以下に代表的なコンフィギュレーションの拡張関係を図で示す。
Test
と IntegrationTest
は Runtime
を拡張し、Runtime
は Compile
を拡張し、
CompileInternal
は Compile
、Optional
、Provided
の 3つを拡張する。
セッティングはタスクの動作に影響を与えることもできる。例えば、packageSrc
は packageOptions
セッティングの影響を受ける。
これをサポートするため、(packageSrc
のような)タスクキーは、(packageOption
のような)別のキーのスコープとなりえる。
パッケージを構築するさまざまなタスク(packageSrc
、packageBin
、packageDoc
)は、artifactName
や packageOption
などのパッケージ関連のキーを共有することができる。これらのキーはそれぞれのパッケージタスクに対して独自の値を取ることができる。
各スコープ軸は、Some(_)
のようにその軸の型のインスタンスを持つか、Zero
という特殊な値を持つことができる。
つまり、Zero
は None
と同様だと考えることができる。
Zero
は全てのスコープ軸に対応する普遍的なフォールバックであるが、多くの場合直接それを使うのは sbt 本体もしくはプラグインの作者に限定されるべきだ。
Global
は、全ての軸を Zero
とするスコープ、Zero / Zero / Zero
だ。そのため、Global / someKey
は Zero / Zero / Zero / someKey
を略記したものだと考えることができる。
build.sbt
で裸のキーを使ってセッティングを作った場合は、(現プロジェクト / コンフィグレーション Zero
/ タスク Zero
) にスコープ付けされる:
lazy val root = (project in file("."))
.settings(
name := "hello"
)
sbt を実行して、inspect name
と入力して、キーが
ProjectRef(uri("file:/private/tmp/hello/"), "root") / name
により提供されていることを確認しよう。つまり、プロジェクトは、
ProjectRef(uri("file:/private/tmp/hello/"), "root")
で、コンフィギュレーション軸もタスク軸も表示されない (これは Zero
を意味する)。
右辺項に置かれた裸のキーも (現プロジェクト / コンフィグレーション Zero
/ タスク Zero
) にスコープ付けされる:
organization := name.value
全てのスコープ軸の型には /
演算子が導入されている。
/
は引数としてキーもしくは別のスコープ軸を受け取ることができる。
これをやる意味は全くないけど、例として Compile
コンフィギュレーションでスコープ付けされた name
の設定を以下に示す:
Compile / name := "hello"
また、packageBin
タスクでスコープ付けされた name
の設定(これも意味なし!ただの例だよ):
packageBin / name := "hello"
もしくは、例えば Compile
コンフィギュレーションの packageBin
の name
など、複数のスコープ軸でスコープ付けする:
Compile / packageBin / name := "hello"
もしくは、全ての軸に対して Global
を使う:
// same as Zero / Zero / Zero / concurrentRestrictions
Global / concurrentRestrictions := Seq(
Tags.limitAll(1)
)
(Global / concurrentRestrictions
は、Zero / Zero / Zero / concurrentRestrictions
へと暗黙の変換が行われ、全ての軸を Zero
に設定する。
タスクとコンフィギュレーションは既にデフォルトで Zero
であるため、事実上行なっているのはプロジェクトを Zero
に指定することだ。つまり、ProjectRef(uri("file:/tmp/hello/"), "root") / Zero / Zero / concurrentRestrictions
ではなく、Zero / Zero / Zero / concurrentRestrictions
が定義される。)
コマンドラインと sbt シェルにおいて、sbt はスコープ付きキーを以下のように表示する(そして、パースする):
ref / Config / intask / キー
ref
は、サブプロジェクト軸を特定する。これは <プロジェクト-id>
、ProjectRef(uri("file:..."), "id")
、もしくは「ビルド全体」を意味する ThisBuild
という値を取ることができる。
Config
は、コンフィギュレーション軸を特定し、大文字から始まる Scala 識別子を使う。
intask
は、タスク軸を特定する。
キー
は、スコープ付けされるキーを特定する。
全ての軸において、Zero
を使うことができる。
スコープ付きキーの一部を省略すると、以下の手順で推論される:
Config
もしくは intask
を省略した場合は、キーに依存したコンフィギュレーションが自動検知される。
さらに詳しくは、Interacting with the Configuration System 参照。
fullClasspath
はキーのみを指定し、デフォルトスコープを用いる。ここでは、カレントプロジェクト、キーに依存したコンフィギュレーション、Zero
タスクスコープとなる。
Test / fullClasspath
はコンフィギュレーションを指定する。つまりプロジェクト軸とタスク軸はデフォルトを用いつつも Test
コンフィギュレーションにおける fullClasspath
というキーを表す。
root / fullClasspath
は root
というプロジェクトid によって特定されるプロジェクトをプロジェクト軸に指定する。
root / Zero / fullClasspath
は root
プロジェクトと、デフォルトのコンフィギュレーションの代わりに Zero
をコンフィギュレーション軸に指定する。
doc / fullClasspath
は fullClasspath
キーを doc
タスク、プロジェクト軸とコンフィギュレーション軸はデフォルト値へと指定する。
ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath
はプロジェクト ProjectRef(uri("file:/tmp/hello/"), "root")
、Test コンフィギュレーション、デフォルトのタスク軸を指定する。
ThisBuild / version
はプロジェクト軸をこの「ビルド全体」である ThisBuild
、デフォルトのコンフィギュレーション軸へと指定する。
Zero / fullClasspath
はプロジェクト軸を Zero
、コンフィギュレーション軸をデフォルト値へと指定する。
root / Compile / doc / fullClasspath
は 3つ全てのスコープ軸を指定する。
sbt シェルで inspect
コマンドを使ってキーとそのスコープを把握することができる。
例えば、inspect Test/fullClasspath
と試してみよう:
$ sbt
sbt:Hello> inspect Test / fullClasspath
[info] Task: scala.collection.Seq[sbt.internal.util.Attributed[java.io.File]]
[info] Description:
[info] The exported classpath, consisting of build products and unmanaged and managed, internal and external dependencies.
[info] Provided by:
[info] ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath
[info] Defined at:
[info] (sbt.Classpaths.classpaths) Defaults.scala:1639
[info] Dependencies:
[info] Test / dependencyClasspath
[info] Test / exportedProducts
[info] Test / fullClasspath / streams
[info] Reverse dependencies:
[info] Test / testLoader
[info] Delegates:
[info] Test / fullClasspath
[info] Runtime / fullClasspath
[info] Compile / fullClasspath
[info] fullClasspath
[info] ThisBuild / Test / fullClasspath
[info] ThisBuild / Runtime / fullClasspath
[info] ThisBuild / Compile / fullClasspath
[info] ThisBuild / fullClasspath
[info] Zero / Test / fullClasspath
[info] Zero / Runtime / fullClasspath
[info] Zero / Compile / fullClasspath
[info] Global / fullClasspath
[info] Related:
[info] Compile / fullClasspath
[info] Runtime / fullClasspath
一行目からこれが(.sbt ビルド定義で説明されているとおり、セッティングではなく)タスクであることが分かる。
このタスクの戻り値は scala.collection.Seq[sbt.Attributed[java.io.File]]
の型をとる。
“Provided by” は、この値を定義するスコープ付きキーを指し、この場合は、
ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath
(Test
コンフィギュレーションと ProjectRef(uri("file:/tmp/hello/"), "root")
プロジェクトにスコープ付けされた fullClasspath
キー)。
“Dependencies” に関しては、前のページで解説した。
“Delegates” (委譲) に関してはまた後で。
今度は、(inspect Test/fullClasspath
のかわりに)inspect fullClasspath
を試してみて、違いをみてみよう。
コンフィグレーションが省略されたため、Compile
だと自動検知される。
そのため、inspect Compile/fullClasspath
は inspect fullClasspath
と同じになるはずだ。
次に、inspect ThisBuild / Zero / fullClasspath
も実行して違いを比べてみよう。
fullClasspath
はデフォルトでは、Zero
スコープには定義されていない。
より詳しくは、Interacting with the Configuration System 参照。
あるキーが、通常スコープ付けされている場合は、スコープを指定してそのキーを使う必要がある。
例えば、compile
タスクは、デフォルトで Compile
と Test
コンフィギュレーションにスコープ付けされているけど、
これらのスコープ外には存在しない。
そのため、compile
キーに関連付けられた値を変更するには、Compile / compile
か Test / compile
のどちらかを書く必要がある。
素の compile
を使うと、コンフィグレーションにスコープ付けされた標準のコンパイルタスクをオーバーライドするかわりに、カレントプロジェクトにスコープ付けされた新しいコンパイルタスクを定義してしまう。
“Reference to undefined setting“ のようなエラーに遭遇した場合は、スコープを指定していないか、間違ったスコープを指定したことによることが多い。 君が使っているキーは何か別のスコープの中で定義されている可能性がある。 エラーメッセージの一部として sbt は、意味したであろうものを推測してくれるから、“Did you mean Compile / compile?” を探そう。
キーの名前はキーの一部でしかないと考えることもできる。
実際の所は、全てのキーは名前と(三つの軸を持つ)スコープによって構成される。
つまり、Compile / packageBin / packageOptions
という式全体でキー名だということだ。
単に packageOptions
と言っただけでもキー名だけど、それは別のキーだ
(スラッシュ無しのキーのスコープは暗黙で決定され、現プロジェクト、Zero
コンフィグレーション、Zero
タスクとなる)。
サブプロジェクト間に共通なセッティングを一度に定義するための上級テクニックとしてセッティングを
ThisBuild
にスコープ付けするという方法がある。
もし特定のサブプロジェクトにスコープ付けされたキーが見つから無かった場合、
sbt はフォールバックとして ThisBuild
内を探す。
この仕組みを利用して、
version
、 scalaVersion
、 organization
といったよく使われるキーに対してビルドレベルのデフォルトのセッティングを定義することができる。
ThisBuild / organization := "com.example",
ThisBuild / scalaVersion := "2.12.18",
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "Hello",
publish / skip := true
)
lazy val core = (project in file("core"))
.settings(
// other settings
)
lazy val util = (project in file("util"))
.settings(
// other settings
)
便宜のため、セッティング式のキーと本文の両方を ThisBuild
にスコープ付けする
inThisBuild(...)
という関数が用意されている。
セッティング式を渡すと、それに ThisBuild /
を可能な所に追加したのと同じものが得られる。
ただし、後で説明するスコープ委譲の性質上、ビルドレベル・セッティングは
純粋な値または Global
か ThisBuild
にスコープ付けされたセッティングのみを代入するべきだ。
スコープ付きキーは、そのスコープに関連付けられた値がなければ未定義であることもできる。
全てのスコープ軸に対して、sbt には他のスコープ値からなるフォールバック検索パス(fallback search path)がある。
通常は、より特定のスコープに関連付けられた値が見つからなければ、sbt は、ThisBuild
など、より一般的なスコープから値を見つけ出そうとする。
この機能により、より一般的なスコープで一度だけ値を代入して、複数のより特定なスコープがその値を継承することを可能とする。 スコープ委譲に関する詳細は後ほど解説する。
+=
と ++=
:=
による置換が最も単純な変換だが、キーには他のメソッドもある。
SettingKey[T]
の T
が列の場合、つまりキーの値の型が列の場合は、置換のかわりに列に追加することができる。
+=
は、列に単一要素を追加する。
++=
は、別の列を連結する。
例えば、Compile / sourceDirectories
というキーの値の型は Seq[File]
だ。
デフォルトで、このキーの値は src/main/scala
を含む。
(どうしても標準的なやり方では気が済まない君が)source
という名前のディレクトリに入ったソースもコンパイルしたい場合、
以下のようにして設定できる:
Compile / sourceDirectories += new File("source")
もしくは、sbt パッケージに入っている file()
関数を使って:
Compile / sourceDirectories += file("source")
(file()
は、単に新しい File
作る)
++=
を使って複数のディレクトリを一度に加える事もできる:
Compile / sourceDirectories ++= Seq(file("sources1"), file("sources2"))
ここでの Seq(a, b, c, ...)
は、列を構築する標準的な Scala の構文だ。
デフォルトのソースディレクトリを完全に置き換えてしまいたい場合は、当然 :=
を使えばいい:
Compile / sourceDirectories := Seq(file("sources1"), file("sources2"))
セッティングが :=
や +=
や ++=
を使って自分自身や他のキーへの依存が生まれるとき、その依存されるキーの値が存在しなくてならない。
もしそれが存在しなければ sbt に怒られることになるだろう。例えば、“Reference to undefined setting“ のようなエラーだ。
これが起こった場合は、キーが定義されている正しいスコープで使っているか確認しよう。
これはエラーになるが、循環した依存性を作ってしまうことも起こりうる。sbt が君がそうしてしまったことを教えてくれるだろう。
あるタスクの値を定義するために他のタスクの値を計算する必要があるかもしれない。
そのような場合には、:=
や +=
や ++=
の引数に Def.task
を使えばよい。
例として、sourceGenerators
にプロジェクトのベースディレクトリやコンパイル時のクラスパスを加える設定をみてみよう。
Compile / sourceGenerators += Def.task {
myGenerator(baseDirectory.value, (Compile / managedClasspath).value)
}
+=
と ++=
他のキーを使って既存のセッティングキーやタスクキーへ値を追加するには +=
を使えばよい。
例えば、プロジェクト名を使って名付けたカバレッジレポートがあって、それを clean
が削除するファイルリストに追加するなら、このようになる:
cleanFiles += file("coverage-report-" + name.value + ".txt")
このページはスコープ委譲を説明する。前のページの .sbt ビルド定義、 スコープ を読んで理解したことを前提とする。
スコープ付けの説明が全て終わったので、.value
照会の詳細を解説できる。
難易度は高めなので、始めてこのガイドを読む場合はこのページは飛ばしてもいい。
これまでに習ったことをおさらいしておこう。
Zero
特殊なスコープ成分がある。
ThisBuild
特殊なスコープ成分がある。
Test
コンフィギュレーションは Runtime
を拡張し、Runtime
は Compile
を拡張する。
${current subproject} / Zero / Zero
にスコープ付けされる。
/
演算子を使ってさらにスコープ付けできる。
以下のようなビルド定義を考える:
lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")
lazy val projX = (project in file("x"))
.settings(
foo := {
(Test / bar).value + 1
},
Compile / bar := 1
)
foo
のセッティング本文内において、スコープ付きキー Test / bar
への依存性が宣言されている。
しかし、projX
において Test / bar
が未定義であるにも関わらず、sbt
は別のスコープ付きキーへと解決して foo
は 2
に初期化される。
sbt はキーのフォールバックのための検索パスを厳密に定義し、これをスコープ委譲 (scope delegation) と呼ぶ。 この機能により、より一般的なスコープで一度だけ値を代入して、複数のより特定なスコープがその値を継承することを可能とする。
スコープ委譲のルールは以下の通り:
Zero
(これはタスクスコープ付けを行わないもののこと)。
Zero
( これはコンフィギュレーションのスコープ付けを行わないものと同じ)。
ThisBuild
そして Zero
。
それぞれのルールを以下に説明していく。
言い換えると、2つのスコープ候補があるとき、一方がサブプロジェクト軸により特定な値を持つとき、コンフィギュレーションやタスク軸のスコープに関わらず必ず勝つということだ。 同様に、サブプロジェクトが同じ場合、コンフィギュレーションに特定な値を持つものがタスクのスコープ付けに関わらず勝つ。 「より特定」とは何かは、以下のルールで定義していく。
Zero
(これはタスクスコープ付けを行わないもののこと)。
ここでやっとキーが与えられたとき sbt がどのようにして委譲スコープを生成するかの具体的なルールが出てきた。
任意の (xxx / yyy).value
が与えられたときに、どのような検索パスを取るかを示していることに注目してほしい。
練習問題 A: 以下のビルド定義を考える:
lazy val projA = (project in file("a"))
.settings(
name := {
"foo-" + (packageBin / scalaVersion).value
},
scalaVersion := "2.11.11"
)
name in projA
(sbt シェルだと projA/name
) の値は何か?
"foo-2.11.11"
"foo-2.12.18"
正解は "foo-2.11.11"
。
.settings(...)
内において、scalaVersion
は自動的に projA / Zero / Zero
にスコープ付けされるため、
packageBin / scalaVersion
は projA / Zero / packageBin / scalaVersion
となる。
そのスコープ付きキーは未定義だ。
ルール 2に基いて、sbt はタスク軸を Zero
に置換して projA / Zero / Zero
になる (projA / scalaVersion
)。
そのスコープ付きキーは "2.11.11"
として定義されている。
Zero
( これはコンフィギュレーションのスコープ付けを行わないものと同じ)。
これを説明する例は上に見た projX
だ:
lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")
lazy val projX = (project in file("x"))
.settings(
foo := {
(Test / bar).value + 1
},
Compile / bar := 1
)
フルスコープを書き出してみると projX / Test / Zero
となる。
また、Test
コンフィギュレーションは Runtime
を拡張し、Runtime
は Compile
を拡張することを思い出してほしい。
Test / bar
は未定義だが、ルール3 に基いて sbt
は projX / Test / Zero
、projX / Runtime / Zero
、そして
projX / Compile / Zero
の順に bar
をスコープ付けして検索していく。
最後のものが見つかり、それは Compile / bar
だ。
ThisBuild
そして Zero
。
練習問題 B: 以下のビルド定義を考える:
ThisBuild / organization := "com.example"
lazy val projB = (project in file("b"))
.settings(
name := "abc-" + organization.value,
organization := "org.tempuri"
)
name in projB
(sbt シェルだと projB/name
) の値は何か?
"abc-com.example"
"abc-org.tempuri"
正解は abc-org.tempuri
だ。
ルール 4に基づき、最初の検索パスは projB / Zero / Zero
にスコープ付けされた organization
で、
これは projB
内で "org.tempuri"
として定義されている。
これは、ビルドレベルのセッティングである ThisBuild / organization
よりも高い優先順位を持つ。
練習問題 C: 以下のビルド定義を考える:
ThisBuild / packageBin / scalaVersion := "2.12.2"
lazy val projC = (project in file("c"))
.settings(
name := {
"foo-" + (packageBin / scalaVersion).value
},
scalaVersion := "2.11.11"
)
projC / name
の値は何か?
"foo-2.12.2"
"foo-2.11.11"
正解は foo-2.11.11
。
projC / Zero / packageBin
にスコープ付けされた scalaVersion
は未定義だ。
ルール 2 は projC / Zero / Zero
を見つける。ルール 4 は ThisBuild / Zero / packageBin
を見つける。
ルール 1 の規定により、より特定なサブプロジェクト軸が勝ち、それは
projC / Zero / Zero
で "2.11.11"
と定義されている。
練習問題 D: 以下のビルド定義を考える:
ThisBuild / scalacOptions += "-Ywarn-unused-import"
lazy val projD = (project in file("d"))
.settings(
test := {
println((Compile / console / scalacOptions).value)
},
console / scalacOptions -= "-Ywarn-unused-import",
Compile / scalacOptions := scalacOptions.value // added by sbt
)
projD/test
を実行した場合の出力は何か?
List()
List(-Ywarn-unused-import)
正解は List(-Ywarn-unused-import)
。
ルール 2 は projD / Compile / Zero
を見つけ、
ルール 3 は projD / Zero / console
を見つけ、
ルール 4 は ThisBuild / Zero / Zero
を見つける。
projD / Compile / Zero
はサブプロジェクト軸に projD
を持ち、
またコンフィギュレーション軸はタスク軸よりも高い優先順位を持つのでルール 1 は
projD / Compile / Zero
を選択する。
次に、Compile / scalacOptions
は scalacOptions.value
を参照するため、
projD / Zero / Zero
のための委譲を探す必要がある。
ルール 4 は ThisBuild / Zero / Zero
を見つけ、これは List(-Ywarn-unused-import)
に解決される。
何が起こっているのか手早く調べたい場合は inspect
を使えばいい。
sbt:projd> inspect projD / Compile / console / scalacOptions
[info] Task: scala.collection.Seq[java.lang.String]
[info] Description:
[info] Options for the Scala compiler.
[info] Provided by:
[info] ProjectRef(uri("file:/tmp/projd/"), "projD") / Compile / scalacOptions
[info] Defined at:
[info] /tmp/projd/build.sbt:9
[info] Reverse dependencies:
[info] projD / test
[info] projD / Compile / console
[info] Delegates:
[info] projD / Compile / console / scalacOptions
[info] projD / Compile / scalacOptions
[info] projD / console / scalacOptions
[info] projD / scalacOptions
[info] ThisBuild / Compile / console / scalacOptions
[info] ThisBuild / Compile / scalacOptions
[info] ThisBuild / console / scalacOptions
[info] ThisBuild / scalacOptions
[info] Zero / Compile / console / scalacOptions
[info] Zero / Compile / scalacOptions
[info] Zero / console / scalacOptions
[info] Global / scalacOptions
....
“Provided by” は projD / Compile / console / scalacOptions
が
projD / Compile / scalacOptions
によって提供されることを表示しているのに注目してほしい。
“Delegates” 以下に全ての委譲スコープ候補が優先順に列挙されている!
projD
にスコープ付けされているスコープが当然最初に表示されて、ThisBuild
、Zero
と続いている。
Compile
にスコープ付けされいるのが最初に表示されて、Zero
にフォールバックしている。
cosole /
が来て、次にタスクスコープ無しが来ている。
スコープ委譲はオブジェクト指向言語のクラス継承に似ていると思うかもしれないが、注意するべき違いがある。
Scala のような OO言語では、Shape
トレイトに drawShape
というメソッドがあれば、たとえそれが
Shape
トレイトの他のメソッドから呼ばれているとしても子クラス側で振る舞いをオーバーライドすることができ、これは動的ディスパッチと呼ばれる。
一方 sbt は、スコープ委譲によってあるスコープをより一般的なスコープに委譲することができ、 例えばプロジェクトレベルのセッティングからビルドレベルのセッティングへ委譲といったことができるが、 ビルドレベルのセッティングはプロジェクトレベルのセッティングを参照することはできない。
練習問題 E: 以下のビルド定義を考える:
lazy val root = (project in file("."))
.settings(
inThisBuild(List(
organization := "com.example",
scalaVersion := "2.12.2",
version := scalaVersion.value + "_0.1.0"
)),
name := "Hello"
)
lazy val projE = (project in file("e"))
.settings(
scalaVersion := "2.11.11"
)
projE / version
の値は何か?
"2.12.2_0.1.0"
"2.11.11_0.1.0"
正解は "2.12.2_0.1.0"
。
projE / version
は ThisBuild / version
に委譲する。
一方 ThisBuild / version
は ThisBuild / scalaVersion
に依存する。
このように振る舞うため、ビルドレベルのセッティングは単純な値の代入に限定するべきだ。
練習問題 F: 以下のビルド定義を考える:
ThisBuild / scalacOptions += "-D0"
scalacOptions += "-D1"
lazy val projF = (project in file("f"))
.settings(
compile / scalacOptions += "-D2",
Compile / scalacOptions += "-D3",
Compile / compile / scalacOptions += "-D4",
test := {
println("bippy" + (Compile / compile / scalacOptions).value.mkString)
}
)
projF/test
を実行した場合の出力は何か?
"bippy-D4"
"bippy-D2-D4"
"bippy-D0-D3-D4"
正解は "bippy-D0-D3-D4"
。
これは、Paul Phillips
さんが考案した練習問題を元にしている。
someKey += "x"
は以下のように展開されるため、全てのルールをデモする素晴らしい問題だ。
someKey += {
val old = someKey.value
old :+ "x"
}
このとき、古い方の .value
を取得するときに委譲が発生して、ルール5 に基いてそれは別のスコープ付きキー扱いする必要がある。
まずは +=
を取り除いて、古い .value
の委譲が何になるかをコメントで注釈する。
ThisBuild / scalacOptions := {
// Global / scalacOptions <- Rule 4
val old = (ThisBuild / scalacOptions).value
old :+ "-D0"
}
scalacOptions := {
// ThisBuild / scalacOptions <- Rule 4
val old = scalacOptions.value
old :+ "-D1"
}
lazy val projF = (project in file("f"))
.settings(
compile / scalacOptions := {
// ThisBuild / scalacOptions <- Rules 2 and 4
val old = (compile / scalacOptions).value
old :+ "-D2"
},
Compile / scalacOptions := {
// ThisBuild / scalacOptions <- Rules 3 and 4
val old = (Compile / scalacOptions).value
old :+ "-D3"
},
Compile / compile / scalacOptions := {
// projF / Compile / scalacOptions <- Rules 1 and 2
val old = (Compile / compile / scalacOptions).value
old :+ "-D4"
},
test := {
println("bippy" + (Compile / compile / scalacOptions).value.mkString)
}
)
評価するとこうなる:
ThisBuild / scalacOptions := {
Nil :+ "-D0"
}
scalacOptions := {
List("-D0") :+ "-D1"
}
lazy val projF = (project in file("f"))
.settings(
compile / scalacOptions := List("-D0") :+ "-D2",
Compile / scalacOptions := List("-D0") :+ "-D3",
Compile / compile / scalacOptions := List("-D0", "-D3") :+ "-D4",
test := {
println("bippy" + (Compile / compile / scalacOptions).value.mkString)
}
)
このページは、このガイドのこれまでのページ、特に .sbt ビルド定義、スコープ、と タスク・グラフ を読んでいることを前提とする。
ライブラリ依存性は二つの方法で加えることができる:
lib
ディレクトリに jar ファイルを入れることでできるアンマネージ依存性(unmanaged dependencies)
ほとんどの人はアンマネージ依存性ではなくマネージ依存性を使う。 しかし、アンマネージの方が最初に始めるにあたってはより簡単かもしれない。
アンマネージ依存性はこんな感じのものだ: jar ファイルを lib
配下に置いておけばプロジェクトのクラスパスに追加される、以上!
ScalaCheck、Specs2、ScalaTest のようなテスト用の jar ファイルも lib
に配置できる。
lib
配下の依存ライブラリは(compile
、test
、run
、そして console
の)全てのクラスパスに追加される。
もし、どれか一つのクラスパスを変えたい場合は、例えば Compile / dependencyClasspath
や
Runtime / dependencyClasspath
などを適宜調整する必要がある。
アンマネージ依存性を利用するのに、build.sbt
には何も書く必要はないが、デフォルトの lib
以外のディレクトリを使いたい場合は unmanagedBase
キーで変更することができる。
lib
のかわりに、custom_lib
を使うならこのようになる:
unmanagedBase := baseDirectory.value / "custom_lib"
baseDirectory
はプロジェクトのベースディレクトリで、
タスク・グラフで説明したとおり、ここでは unmanagedBase
を value
を使って取り出した baseDirectory
の値を用いて変更している。
他には、unmangedJars
という unmanagedBase
ディレクトリに入っている jar ファイルのリストを返すタスクがある。
複数のディレクトリを使うとか、何か別の複雑なことを行う場合は、この unmanagedJar
タスクを何か別のものに変える必要があるかもしれない。
例えば Compile
コンフィギュレーション時に lib
ディレクトリのファイルを無視したい、など。
Compile / unmanagedJars := Seq.empty[sbt.Attributed[java.io.File]]
sbt は [Apache Ivy] を使ってマネージ依存性を実装しているので、既に Maven か Ivy に慣れているなら、違和感無く入り込めるだろう。
libraryDependencies
キー 大体の場合、依存性を libraryDependencies
セッティングに列挙するだけでうまくいくだろう。
Maven POM ファイルや、Ivy コンフィギュレーションファイルを書くなどして、依存性を外部で設定してしまって、
sbt にその外部コンフィギュレーションファイルを使わせるということも可能だ。
これに関しては、[Library Management] を参照。
依存性の宣言は、以下のようになる。ここで、groupId
、artifactId
、と revision
は文字列だ:
libraryDependencies += groupID % artifactID % revision
もしくは、以下のようになる。このときの configuration
は文字列もしくは Configuration
の値だ (Test
など)。
libraryDependencies += groupID % artifactID % revision % configuration
libraryDependencies
は [Keys] で以下のように定義されている:
val libraryDependencies = settingKey[Seq[ModuleID]]("Declares managed dependencies.")
%
メソッドは、文字列から ModuleID
オブジェクトを作るので、君はその ModuleID
を libraryDependencies
に追加するだけでいい。
当然ながら、sbt は(Ivy を通じて)モジュールをどこからダウンロードしてくるかを知っていなければならない。 もしそのモジュールが sbt に初めから入っているデフォルトのリポジトリの一つに存在していれば、何もしなくてもそのままで動作する。 例えば、Apache Derby は Maven2 の標準リポジトリ(訳注: sbt にあらかじめ入っているデフォルトリポジトリの一つ)に存在している:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3"
これを build.sbt
に記述して update
を実行すると、sbt は Derby を $COURSIER_CACHE/https/repo1.maven.org/maven2/org/apache/derby/
にダウンロードするはずだ。
(ちなみに、update
は compile
の依存性であるため、ほとんどの場合、手動で update
と入力する必要はないだろう)
もちろん ++=
を使って依存ライブラリのリストを一度に追加することもできる:
libraryDependencies ++= Seq(
groupID % artifactID % revision,
groupID % otherID % otherRevision
)
libraryDependencies
に対して :=
を使う機会があるかもしれないが、おそらくそれは稀だろう。
%%
を使って正しい Scala バージョンを入手する groupID % artifactID % revision
のかわりに、
groupID %% artifactID % revision
を使うと(違いは groupID の後ろの二つ連なった %%
)、
sbt はプロジェクトの Scala のバイナリバージョンをアーティファクト名に追加する。
これはただの略記法なので %%
無しで書くこともできる:
libraryDependencies += "org.scala-stm" % "scala-stm_2.13" % "0.9.1"
君のビルドの Scala バージョンが 2.13.12
だとすると、以下の設定は上記と等価だ(“org.scala-stm” の後ろの二つ連なった %% に注意):
libraryDependencies += "org.scala-stm" %% "scala-stm" % "0.9.1"
多くの依存ライブラリは複数の Scala バイナリバージョンに対してコンパイルされており、 ライブラリの利用者はバイナリ互換性のあるものを選択したいと思うはずである。
詳しくは、Cross Build を参照。
groupID % artifactID % revision
の revision
は、単一の固定されたバージョン番号でなくてもよい。
Ivy は指定されたバージョン指定の制限の中でモジュールの最新の revision を選ぶことができる。
"1.6.1"
のような固定 revision ではなく、"latest.integration"
、"2.9.+"
、や "[1.0,)"
など指定できる。
詳しくは、[Ivy revisions] を参照。
全てのパッケージが一つのサーバに置いてあるとは限らない。 sbt は、デフォルトで Maven の標準リポジトリ(訳注:Maven Central Repository)を使う。 もし依存ライブラリがデフォルトのリポジトリに存在しないなら、Ivy がそれを見つけられるよう resolver を追加する必要がある。
リポジトリを追加するには、以下のように:
resolvers += name at location
二つの文字列の間の特別な at
を使う。
例えばこのようになる:
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
Keys で定義されている resolvers
キーは以下のようになっている:
val resolvers = settingKey[Seq[Resolver]]("The user-defined additional resolvers for automatically managed dependencies.")
at
メソッドは、二つの文字列から Resolver
オブジェクトを作る。
sbt は、リポジトリとして追加すれば、ローカル Maven リポジトリも検索することができる:
resolvers += "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
こんな便利な指定方法もある:
resolvers += Resolver.mavenLocal
他の種類のリポジトリの定義の詳細に関しては、[Resolvers] 参照。
resolvers
は、デフォルトの resolver を含まず、ビルド定義によって加えられる追加のものだけを含む。
sbt
は、resolvers
をデフォルトのリポジトリと組み合わせて external-resolvers
を形成する。
そのため、デフォルトの resolver を変更したり、削除したい場合は、resolvers
ではなく、external-resolvers
をオーバーライドする必要がある。
依存ライブラリをテストコード(Test
コンフィギュレーションでコンパイルされる src/test/scala
内のコード)から使いたいが、
メインのコードでは使わないということがよくある。
ある依存ライブラリが Test
コンフィギュレーションのクラスパスには出てきてほしいが、Compile
コンフィギュレーションでは要らないという場合は、以下のように % "test"
と追加する:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % "test"
Test
コンフィグレーションの型安全なバージョンを使ってもよい:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % Test
この状態で sbt のインタラクティブモードで show Compile/dependencyClasspath
と入力しても Derby は出てこないはずだ。
だが、show Test/dependencyClasspath
と入力すると、Derby の jar がリストに含まれていることを確認できるだろう。
普通は、ScalaCheck、Specs2、ScalaTest などのテスト関連の依存ライブラリは % "test"
と共に定義される。
ライブラリの依存性に関しては、もうこの入門用のページで見つからない情報があれば、このページに もう少し詳細やコツが書いてある。
このガイドのこれまでのページを読んでおいてほしい。 特に build.sbt、 タスク・グラフ、 とライブラリ依存性を理解していることが必要になる。
sbt のプラグインは、最も一般的には新しいセッティングを追加することでビルド定義を拡張するものである。
その新しいセッティングは新しいタスクでもよい。
例えば、テストカバレッジレポートを生成する codeCoverage
というタスクを追加するプラグインなどが考えられる。
プロジェクトが hello
ディレクトリにあり、ビルド定義に sbt-site プラグインを追加する場合、
hello/project/site.sbt
を新しく作成し、
Ivy のモジュール ID を addSbtPlugin
メソッドに渡してプラグイン依存性を定義する:
addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.7.0")
sbt-assembly プラグインを追加するなら、以下のような内容で hello/project/assembly.sbt
をつくる:
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")
全てのプラグインがデフォルトのリポジトリに存在するわけではないので、 プラグインのドキュメントでそのプラグインが見つかるリポジトリを resolvers に追加するよう指示されていることもあるだろう。
resolvers ++= Resolver.sonatypeOssRepos("public")
プラグインは普通、プロジェクトでそのプラグインの機能を有効にするためのセッティング群を提供している。 これは次のセクションで説明する。
プラグインは、自身が持つセッティング群がビルド定義に自動的に追加されるよう宣言することができ、 その場合、プラグインの利用者は何もしなくてもいい。
sbt 0.13.5 から、プラグインを自動的に追加して、そのセッティング群と依存関係がプロジェクトに設定されていることを安全に保証する 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
コマンドを実行してみよう。
例えば、このようになる。
> plugins
In file:/home/jsuereth/projects/sbt/test-ivy-issues/
sbt.plugins.IvyPlugin: enabled in scala-sbt-org
sbt.plugins.JvmPlugin: enabled in scala-sbt-org
sbt.plugins.CorePlugin: enabled in scala-sbt-org
sbt.plugins.JUnitXmlReportPlugin: enabled in scala-sbt-org
ここでは、plugins
の表示によって sbt のデフォルトのプラグインが全て有効化されていることが分かる。
sbt のデフォルトセッティングは 3 つのプラグインによって提供される:
CorePlugin
: タスクの並列実行などのコア機能。
IvyPlugin
: モジュールの公開や依存性の解決機能。
JvmPlugin
: Java/Scala プロジェクトのコンパイル/テスト/実行/パッケージ化。
さらに JUnitXmlReportPlugin
は実験的に junit-xml の生成機能を提供する。
古くからある auto plugin ではないプラグインは、マルチプロジェクトビルド内に 異なるタイプのプロジェクトを持つことができるように、セッティング群を明示的に追加することを必要とする。
各プラグインのドキュメントに設定方法が明記されているかと思うが、 一般的にはベースとなるセッティング群を追加して、必要に応じてカスタマイズするというパターンが多い。
例えば sbt-site プラグインの例で説明すると site.sbt
というファイルを新しく作って
site.settings
を site.sbt
に記述することで有効化できる。
ビルド定義がマルチプロジェクトの場合は、プロジェクトに直接追加する:
// don't use the site plugin for the `util` project
lazy val util = (project in file("util"))
// enable the site plugin for the `core` project
lazy val core = (project in file("core"))
.settings(site.settings)
プラグインを $HOME/.sbt/1.0/plugins/
以下で宣言することで全てのプロジェクトに対して一括してプラグインをインストールすることができる。
$HOME/.sbt/1.0/plugins/
はそのクラスパスをすべての sbt ビルド定義に対して export する sbt プロジェクトだ。
大雑把に言えば、$HOME/.sbt/1.0/plugins/
内の .sbt
ファイルや .scala
ファイルは、それが全てのプロジェクトの project/
ディレクトリに入っているかのようにふるまう。
$HOME/.sbt/1.0/plugins/build.sbt
を作って、そこに addSbtPlugin()
式を書くことで
全プロジェクトにプラグインを追加することができる。
しかし、これを多用するとマシン環境への依存性を増やしてしまうことになるので、この機能は注意してほどほどに使うべきだ。
ベスト・プラクティスも参照してほしい。
プラグインのリストがある。
特に人気のプラグインは:
プラグイン開発の方法など、プラグインに関する詳細は Plugins を参照。 ベストプラクティスを知りたいなら、ベスト・プラクティス を見てほしい。
このページでは、独自のセッティングやタスクの作成を紹介する。
このページを理解するには、このガイドの前のページ、 特に build.sbt と タスク・グラフ を読んである必要がある。
Keys は、キーをどのように定義するかを示すサンプル例が満載だ。 多くのキーは、Defaults で実装されている。
キーには 3 つの型がある。
SettingKey
と TaskKey
は .sbt ビルド定義で説明した。
InputKey
に関しては Input Tasks を見てほしい。
以下に Keys からの具体例を示す:
val scalaVersion = settingKey[String]("The version of Scala used for building.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")
キーのコンストラクタは、二つの文字列のパラメータを取る。
キー名("scala-version"
)と解説文("The version of scala used for building."
)だ。
.sbt ビルド定義でみた通り、SettingKey[T]
内の型パラメータ T
は、セッティングの値の型を表す。
TaskKey[T]
内の T
は、タスクの結果の型を表す。
また、.sbt ビルド定義でみた通り、セッティングはプロジェクトが再読み込みされるまでは固定値を持ち、 タスクは「タスク実行」の度(sbt のインタラクティブモードかバッチモードでコマンドが入力される度)に再計算される。
キーは .sbt ファイル、.scala ファイル、または auto plugin 内で定義する事が出来る。
有効化された auto plugin の autoImport
オブジェクト内で定義された val
は全て .sbt
ファイルに自動的にインポートされる。
タスクで使えるキーを定義したら、次はそのキーをタスク定義の中で使ってみよう。
自前のタスクを定義しようとしているかもしれないし、既存のタスクを再定義してようと考えているかもしれないが、
いずれにせよ、やることは同じだ。:=
を使ってタスクのキーになんらかのコードを関連付けよう:
val sampleStringTask = taskKey[String]("A sample string task.")
val sampleIntTask = taskKey[Int]("A sample int task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val library = (project in file("library"))
.settings(
sampleStringTask := System.getProperty("user.home"),
sampleIntTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
)
もしタスクに依存してるものがあれば、[タスク・グラフ][More-About-Settings]で説明したとおり value
を使ってその値を参照すればよい。
タスクを実装する上で一番難しい点は、多くの場合 sbt 固有の問題ではない。なぜならタスクはただの Scala コードだからだ。 難しいのはそのタスクが実行したいことの「本体」部分を書くことだ。
例えば HTML を整形したいとすると、今度は HTML のライブラリを利用したくなるかもしれない (おそらくビルド定義にライブラリ依存性を追加して、その HTML ライブラリに基づいたコードを書けばよいだろう)。
sbt には、いくつかのユーティリティライブラリや便利な関数があり、特にファイルやディレクトリの取り扱いには Scaladocs-IO にある API がしばしば重宝するだろう。
カスタムタスクから value
を使って他のタスクに依存するとき、
タスクの実行意味論 (execution semantics) に注意する必要がある。
ここでいう実行意味論とは、実際どの時点でタスクが評価されるかを決定するものとする。
sampleIntTask
を例に取ると、タスク本文の各行は一行ずつ正格評価 (strict evaluation) されているはずだ。
これは逐次実行の意味論だ:
sampleIntTask := {
val sum = 1 + 2 // first
println("sum: " + sum) // second
sum // third
}
実際には JVM は sum
を 3
とインライン化したりするかもしれないが、観測可能なタスクの作用は、各行ずつ逐次実行したものと同一のものとなる。
次に、startServer
と stopServer
という 2つのカスタムタスクを定義して、sampleIntTask
を以下のように書き換えたとする:
val startServer = taskKey[Unit]("start server")
val stopServer = taskKey[Unit]("stop server")
val sampleIntTask = taskKey[Int]("A sample int task.")
val sampleStringTask = taskKey[String]("A sample string task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
stopServer := {
println("stopping...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
stopServer.value // THIS WON'T WORK
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
}
)
sampleIntTask
を sbt のインタラクティブ・プロンプトから実行すると以下の結果となる:
> sampleIntTask
stopping...
starting...
sum: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:00:00 PM
何が起こったのかを考察するために、sampleIntTask
を視覚化してみよう:
素の Scala のメソッド呼び出しと違って、タスクの value
メソッドの呼び出しは正格評価されない。
代わりに、sampleIntTask
が startServer
タスクと stopServer
タスクに依存するということを表すマークとして機能する。
sampleIntTask
がユーザによって呼び出されると、sbt のタスクエンジンは以下を行う:
sampleIntTask
を評価する前にタスク依存性を評価する。(半順序)
非重複化を説明するために、sbt インタラクティブ・プロンプトから sampleStringTask
を実行する。
> sampleStringTask
stopping...
starting...
sum: 3
s: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:30:00 PM
sampleStringTask
は startServer
と sampleIntTask
の両方に依存し、
sampleIntTask
もまた startServer
タスクに依存するため、startServer
はタスク依存性として 2 度現れる。
しかし、value
はタスク依存性を表記するだけなので、評価は 1 回だけ行われる。
以下は sampleStringTask
の評価を視覚化したものだ:
もしタスク依存性を非重複化しなければ、Test / test
のタスク依存性として Test / compile
が何度も現れるため、テストのソースコードを何度もコンパイルすることになる。
stopServer
タスクはどう実装するべきだろうか?
タスクは依存性を保持するものなので、終了処理タスクという考えはタスクの実行モデルにそぐわないものだ。
最後の処理そのものもタスクになるべきで、そのタスクが他の中間タスクに依存すればいい。
例えば、stopServer
が sampleStringTask
に依存するべきだが、
その時点で stopServer
は sampleStringTask
と呼ばれるべきだろう。
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
},
sampleStringTask := {
val old = sampleStringTask.value
println("stopping...")
Thread.sleep(500)
old
}
)
これが動作することを調べるために、インタラクティブ・プロンプトから sampleStringTask
を実行してみよう:
> sampleStringTask
starting...
sum: 3
s: 3
stopping...
[success] Total time: 1 s, completed Dec 22, 2014 6:00:00 PM
何かが起こったその後に別の何かが起こることを保証するもう一つの方法は Scala を使うことだ。
例えば project/ServerUtil.scala
に簡単な関数を書いたとすると、タスクは以下のように書ける:
sampleIntTask := {
ServerUtil.startServer
try {
val sum = 1 + 2
println("sum: " + sum)
} finally {
ServerUtil.stopServer
}
sum
}
素のメソッド呼び出しは逐次実行の意味論に従うので、全ては順序どおりに実行される。 非重複化もされなくなるので、それは気をつける必要がある。
.scala
ファイルに大量のカスタムコードがあることに気づいたら、
プラグインを作って複数のプロジェクト間で再利用できないか考えてみよう。
以前にちょっと触れたし、詳しい解説はここにあるが、 プラグインを作るのはとても簡単だ。
このページは簡単な味見だけで、カスタムタスクに関しては Tasksページで詳細に解説されている。
このページではビルド構造の整理について説明する。
このガイドの前のページ、特に build.sbt、 タスク・グラフ、 ライブラリ依存性、 そしてマルチプロジェクト・ビルドを理解していることが必要になる。
build.sbt
は sbt の実際の動作を隠蔽している。
sbt のビルドは、Scala コードにより定義されている。そのコード自身もビルドされなければいけない。
当然これも sbt でビルドされる。sbt でやるより良い方法があるだろうか?
project
ディレクトリは、ビルドをビルドする方法を記述したビルドの中のビルドだ。
これらのビルドを区別するために、一番上のビルドをプロパービルド (proper build) 、
project
内のビルドをメタビルド (meta-build) と呼んだりする。
メタビルド内のプロジェクトは、他のプロジェクトができる全てのことをこなすことができる。
つまり、ビルド定義もまた sbt プロジェクトなのだ。
この入れ子構造は永遠に続く。project/project
ディレクトリを作ることで
ビルド定義のビルド定義プロジェクトをカスタム化することができる。
以下に具体例で説明する:
hello/ # ビルドのルート・プロジェクトのベースディレクトリ
Hello.scala # ビルドのルート・プロジェクトのソースファイル
# (src/main/scala に入れることもできる)
build.sbt # build.sbt は、project/ 内のメタビルドの
# ルート・プロジェクトのソースの一部となる。
# つまり、プロパービルドのビルド定義
project/ # メタビルドのルート・プロジェクトのベースディレクトリ
Dependencies.scala # メタビルドのルート・プロジェクトのソースファイル、
# つまり、ビルド定義のソースファイル。
# プロパービルドのビルド定義
assembly.sbt # これは、project/project 内のメタメタビルドの
# ルート・プロジェクトのソースの一部となり、
# ビルド定義のビルド定義となる
project/ # メタメタビルドのルート・プロジェクトのベースディレクトリ
MetaDeps.scala # project/project/ 内のメタメタビルドの
# ルート・プロジェクトのソースファイル
心配しないでほしい! 普通はこういうことをする必要は全くない。 しかし、原理を理解しておくことはきっと役立つことだろう。
ちなみに、.scala
や .sbt
の拡張子で終わっていればどんなファイル名でもよく、build.sbt
や Dependencies.scala
と命名するのは慣例にすぎない。
これは複数のファイルを使うことができるということも意味する。
project
内の任意の .scala
ファイルがビルド定義の一部となることを利用する一つの例として
project/Dependencies.scala
というファイルを作ってライブラリ依存性を一箇所にまとめるということができる。
import sbt._
object Dependencies {
// Versions
lazy val akkaVersion = "2.6.21"
// Libraries
val akkaActor = "com.typesafe.akka" %% "akka-actor" % akkaVersion
val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % akkaVersion
val specs2core = "org.specs2" %% "specs2-core" % "4.20.0"
// Projects
val backendDeps =
Seq(akkaActor, specs2core % Test)
}
この Dependencies
は build.sbt
内で利用可能となる。
定義されている val
が使いやすいように Dependencies._
を import しておこう。
import Dependencies._
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val backend = (project in file("backend"))
.settings(
name := "backend",
libraryDependencies ++= backendDeps
)
マルチプロジェクトでのビルド定義が肥大化して、サブプロジェクト間で同じ依存ライブラリを持っているかを保証したくなったとき、このようなテクニックは有効だ。
.scala
ファイルを使うべきか .scala
ファイルでは、トップレベルの class や object 定義を含む Scala コードを自由に記述できる。
推奨される方法はマルチプロジェクトを定義する build.sbt
ファイル内にほとんどのセッティングを定義し、
project/*.scala
ファイルはタスクの実装や、共有したい値やキーを定義するのに使うことだ。
また .scala
ファイルを使うかどうかの判断には、君や君のチームがどれくらい Scala に慣れているかということも関係するだろう。
上級ユーザ向けのビルドの整理方法として、project/*.scala
内に専用の auto plugin を書くという方法がある。
連鎖プラグイン (triggered plugin) を定義することで auto plugin を全サブプロジェクトにカスタムタスクやコマンドを追加する手段として使うことができる。
このページではこのガイドを総括する。
sbt を使うのに、理解すべき概念の数はさほど多くない。 確かに、これらには多少の学習曲線があるが、 sbt にはこれらの概念以外のことは特にないとも考えることもできる。 sbt は、強力なコア・コンセプトだけを用いて全てを実現している。
この「始める sbt」シリーズをここまで読破したのであれば、知るべきことが何かはもう分かっているはずだ。
Setting
を作成するために :=
、+=
、++=
のようなキーに定義されたメソッドを呼び出す。
タスクは、特殊なセッティングで、タスクを実行するたびに、キーの値を生成する計算が再実行される。
非タスクのセッティングは、ビルド定義の読み込み時に一度だけ値が計算される。
Compile
)や、テスト用のもの(Test
)のようなビルドの種類だ。
build.sbt
にほとんどの設定を置くが、class 定義や大きめのタスク実装などは .scala
ビルド定義を使う。
プラグインは、addSbtPlugin
メソッドを用いて project/plugins.sbt
に追加する。
(プロジェクトのベースディレクトリにある build.sbt
ではないことに注意)
上記のうち、一つでも分からないことがあれば、質問してみるか、このガイドをもう一度読み返すか、sbt のインタラクティブモードで実験してみよう。
健闘を祈る!
sbt はオープンソースであるため、いつでもソースを見れることも忘れずに!
このページでは旧式の .sbt
ビルド定義の説明をする。
現在の推奨はマルチプロジェクト .sbt ビルド定義だ。
明示的に Project を定義する
マルチプロジェクト .sbt ビルド定義や .scala ビルド定義と違って
bare ビルド定義は .sbt
ファイルの位置から暗黙にプロジェクトが定義される。
Project
を定義する代わりに、bare .sbt
ビルド定義は Setting[_]
式のリストから構成される。
name := "hello"
version := "1.0"
scalaVersion := "2.12.18"
注意: 0.13.7 以降は空白行の区切りを必要としない。
こんな風に build.sbt
を書くことはできない。
// 空白行がない場合はコンパイルしない
name := "hello"
version := "1.0"
scalaVersion := "2.10.3"
sbt はどこまでで式が終わってどこからが次の式なのかを判別するために、何らかの区切りを必要とする。
一般的な情報。
sbt のリリースごとの変更点など。
移植に関しては Migrating from sbt 0.13.x も参照。
.copy(foo = xxx)
は withFoo(xxx)
に書き換える必要がある。例えば、UpdateConfiguration
、 RetrieveConfiguration
、 PublishConfiguration
などはビルダーパターンを使うようにリファクタリングした。
config("xyz")
は、val Xyz = config("xyz")
のように 頭文字が大文字の val
に直接代入する必要がある。これは左辺項の識別子を捕捉して後でシェルから使えるようにするためだ。
publishTo
と otherResolvers
は SettingKey から TaskKey へと変更した。#2059/#2662 by @dwijnand
Path.relativizeFile(baseFile, file)
は IO.relativizeFile(baseFile, file)
へと名前が変わった。
PathFinder
の .***
メソッドは .allPaths
メソッドへと名前が変わった。
PathFinder.x_!(mapper)
は PathFinder
の def pair
に変更された。
sbt.Path
の多くのメソッド (relativeTo
、rebase
、 flat
など) は以前は sbt
のパッケージオブジェクト経由でデフォルトの名前空間に入っていたが、それが無くなったので sbt.io.Path
を使ってアクセスしてほしい。
Global
を Zero
と名前を変えて、GlobalScope
と区別するようにした。 @eed3si9n
update.value.configuration(...)
のような所でコンフィギュレーションを参照するのに文字列が使われていたのを、ConfigRef
を使うように変更した。
sourceArtifactTypes
と docArtifactTypes
を Set[String]
から Seq[String]
セッティングへと変更した。
--<command>
という構文から early(<command>)
へと変更した。
publish-local
から publishLocal
に移行する)。
"early(error)"
などの代わりに -error
、 -warn
、 -info
、 -debug
オプションを追加した。
sbt.Process
と sbt.ProcessExtra
は撤廃した。scala.sys.process
に移行する。
incOptions.value.withNameHashing(...)
はオプションは無くなる。
TestResult.Value
は TestResult
に名前を変更する。
%%
を使う必要がある。
以前より廃止勧告が出ていて、今回撤廃されたもの:
Build
trait は sbt 0.13.12 に廃止勧告となり、この度削除した。build.sbt へと移行する必要がある。Auto plugin と Build
trait は相性が悪く、またこの機能は既に普及しているマルチプロジェクト build.sbt によって置き換えられた。
Project(...)
コンストラクタは、2つのパラメータを受け取るものだけに制限する。これは、settings
パラメータは Auto plugin と相性が悪いからだ。代わりに、project
を使ってほしい。
<<=
, <+=
, <++=
は撤廃した。:=、 +=、および ++= 演算子へと移行してほしい。古い演算子は多くのユーザにとって混乱の元となっており、長らく 0.13 のドキュメンテーションからは削除され、sbt 0.13.13 以降正式に撤廃勧告が出ていた。
sbt.Plugin
を撤廃した。AutoPlugin
へと移行してほしい。Auto plugin の方が設定が簡単で、プラグイン間の協調が可能だからだ。
Project
より settingsSet
メソッドおよび add/setSbtFiles
を削除する。
InputTask
apply
メソッドと inputTask
DSL メソッドを撤廃する。Def.inputTask
と Def.spaceDelimited().parsed
へと移行してほしい。
ProjectReference
への暗黙の変換を撤廃する。RootProject(<uri>)
、RootProject(<file>)
、もしくは LocalProject(<string>)
へと移行してほしい。
seq(...)
DSL メソッドを撤廃する。Seq(...)
を使うか、そのまま setting を渡すようにしてほしい。
File
/Seq[File]
セッティングの暗黙の変換を撤廃する。.value
と Def.setting
へと移行してほしい。
SubProcess
の apply
オーバーロードを撤廃する。SubProcess(ForkOptions(runJVMOptions = ..))
へと移行する。
toError(opt: Option[String]): Unit
を廃止する (opt foreach sys.error
と同様)。ScalaRun#run
と併用する場合、scalaRun.run(...).failed foreach (sys error _.getMessage)
というように書き換える。
build.sbt
の静的バリデーション。(詳細は以下の項目)
^
と ^^
コマンドの移植。on projects that adopts Scalafmt
scalas
を使ったときの、スタートアップのログレベルを -error
まで落とした。 #840 by @eed3si9n
++
の振る舞いが変更され、Scala バージョンのサポートを予め列挙するサブプロジェクトのみが変更されるようになった。しかし、!
を追加することで全てのプロジェクトを変更することもできる。どのプロジェクトが変更されたのかの詳細な情報を表示するための、-v
オプションも追加された。#2613 by @jroper
ivyLoggingLevel
を UpdateLogging.Quiet
に落とすようにした。 @eed3si9n
build.sbt
(*.sbt
) ファイル名をログに表示するようにした。 #1911 by @valydia
build.sbt
ファイルから aggregate
を呼べるようにした。 By [@xuwei-k][@xuwei-k]
inspect tree
などで表示される ASCII グラフの最大幅を決める asciiGraphWidth
という新しいグローバルセッティングを追加した。デフォルトでは、40文字。By @RomanIakovlev.
autoImport
の検知に Java リフレクションを使うようにした。 #3115 by @jvican
InteractionService
を追加した。 #3182 by @eed3si9n
PollingWatchService
と Java NIO を抽象化する新しい WatchService
を追加した。 io#47 by @Duhemm on behalf of The Scala Center.
IO.copyFile
と IO.copyDirectory
に sbt.io.CopyOptions()
を受け取るバリエーションを追加した。(詳細は以下の項目)
Path.directory
と Path.contentOf
を sbt-native-packager から寄付してもらった。 io#38 by @muuki88
(Lightbend の委託で) Grzegorz Kossakowski が Zinc 1 にもたらした大きな改善として、クラスベースの name hashing がある。これは、大規模な Scala プロジェクトにおいて差分コンパイルが高速化することが見込まれる。
Zinc 1 の name hashing は、コード間の依存性をファイルではなく、クラスのレベルで追跡する。GitHub issue sbt/sbt#1104 に有名なプロジェクトの既存のクラスにメソッドを追加した場合の比較データがある:
ScalaTest AndHaveWord class: Before 49s, After 4s (12x)
Specs2 OptionResultMatcher class: Before 48s, After 1s (48x)
scala/scala Platform class: Before 59s, After 15s (3.9x)
scala/scala MatchCodeGen class: Before 48s, After 17s (2.8x)
これは、クラスがどのようにまとめられているかといった様々な要素に依存するが、3x ~ 40x の向上が見られるのが分かる。高速化の理由は、クラスをソースファイルという「くくり」から分けたことで少ない数のソースファイルをコンパイルしているからだ。scala/scala の Platform クラスにメソッドを追加した例だと、sbt 0.13 の name hashing は 72 のソースをコンパイルしていたのに対し、新しい Zinc は 6 のソースをコンパイルしている。
xsbti.compile
パッケージ以下の IncOptions
などの Java クラスはコンストラクタを隠蔽する。ファクトリーメソッドである xsbti.compile.Foo.of(...)
に移行する。
ivyScala: IvyScala
キーは scalaModuleInfo: ScalaModuleInfo
に名前が変わる。
xsbti.Reporter#log(...)
は xsbti.Problem
をパラメータとして受け取るようになった。log(problem.position, problem.message, problem.severity)
と呼び出すことで以前の log(...)
に委譲できる。
xsbi.Maybe
、xsbti.F0
、sxbti.F1
は対応する Java 8 クラスである java.util.Optional
、java.util.Supplier
、および java.util.Function
に変更する。
sbt 1.0 はサーバ機能を含み、IDE や他のツールは JSON API を用いてビルドのセッティングをクエリしたり、コマンドを呼び出すことができる。sbt 0.13 においてインタラクティブ・シェルが shell
コマンドによって実装されていたのと同様に、「サーバ」も shell
コマンドによって実装されていて、人間とネットワークの両方の入力を受け取るようになっている。ユーザ視点で見ると、サーバが加わったことによる影響はほとんど無いはずだ。
2016年3月に「サーバ」機能が最小限になるようにリブートが行われた。JetBrain社で IntelliJ の sbt インターフェイスを担当する @jastice とコラボして機能のリストを絞っていった。sbt 1.0 の段階では当初欲しかった機能の全ては入っていないが、長期的に IDE と sbt エコシステムの連携が向上する布石になることを目指している。例えば、IDE 側から compile タスクを命令して、コンパイラ警告を JSON イベントして受け取るといったことができる:
{"type":"xsbti.Problem","message":{"category":"","severity":"Warn","message":"a pure expression does nothing in statement position; you may be omitting necessary parentheses","position":{"line":2,"lineContent":" 1","offset":29,"pointer":2,"pointerSpace":" ","sourcePath":"/tmp/hello/Hello.scala","sourceFile":"file:/tmp/hello/Hello.scala"}},"level":"warn"}
関連して追加された機能として、テスト中にバックグラウンドで web サーバなどを実行するのに使える bgRun
タスクがある。
sbt 1.0 は、Log4J 2 と sjson-new を用いて実装したイベント・ロギングを導入する。 普通の String ベースのログの他に、logger に対して case clase や Contraband によって生成された疑似 case class を渡すことができる:
def registerStringCodec[A: ShowLines: TypeTag]: Unit = ...
final def debugEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Debug, event)
final def infoEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Info, event)
final def warnEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Warn, event)
final def errorEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Error, event)
[success]
メッセージといった様々なイベントは、内部でイベント・ロギングを用いて送信されている。
この機構をサーバと併用することで、プラグインやコンパイラから JSON イベントを発行することができる。
また、Log4J 2 を内部に採用したことで SLF4J のバインディングを提供するようになった。
sbt 1.0 は、タスク内において if 式の本文や匿名関数内からの .value
の呼び出しを禁止する。@sbtUnchecked
アノテーションを使ってこのチェックを無効化できる。
他に、静的バリデーションは、タスクの本文内から .value
を呼び忘れるのも予防する。
sbt 1.0 は eviction 警告の表示を改善する。
ビフォー:
[warn] There may be incompatibilities among your library dependencies.
[warn] Here are some of the libraries that were evicted:
[warn] * com.google.code.findbugs:jsr305:2.0.1 -> 3.0.0
[warn] Run 'evicted' to see detailed eviction warnings
アフター:
[warn] Found version conflict(s) in library dependencies; some are suspected to be binary incompatible:
[warn]
[warn] * com.typesafe.akka:akka-actor_2.12:2.5.0 is selected over 2.4.17
[warn] +- de.heikoseeberger:akka-log4j_2.12:1.4.0 (depends on 2.5.0)
[warn] +- com.typesafe.akka:akka-parsing_2.12:10.0.6 (depends on 2.4.17)
[warn] +- com.typesafe.akka:akka-stream_2.12:2.4.17 () (depends on 2.4.17)
[warn]
[warn] Run 'evicted' to see detailed eviction warnings
@jrudolph の sbt-cross-building はプラグイン作者のためのプラグインだ。
^
(クロス) コマンドと ^^
(sbtVersion スイッチ) コマンドを追加して、これは +
を ++
を sbt のメジャーバージョン間の切り替えに対応させたものだと考えることができる。
プラグインを sbt 1.0 に対応させるのに便利なので、sbt 0.13.16 においてこれらのコマンドを sbt 本体にマージした。
シェルから sbtVersion in pluginCrossBuild
をスイッチするには以下を実行する:
^^ 1.0.0
これで sbt 1.0.0 (とその Scala バージョンである 2.12) を使うようになる。
sbt バージョンに特定のコードを含む必要があれば、src/main/scala-sbt-0.13
、src/main/scala-sbt-1.0
などバイナリ sbt バージョンを末尾に追加したディレクトリを作る。
複数の sbt バージョンをまたいでコマンドを実行するには、まず:
crossSbtVersions := Vector("0.13.16", "1.0.0")
と設定して、以下を実行する:
^ compile
#3133 by @eed3si9n (forward ported from 0.13.16-M1)
sbt IO 1.0 は IO.copyFile
と IO.copyDirectory
のバリエーションとして sbt.io.CopyOptions()
を受け取るものを追加する。
CopyOptions()
は疑似 case class の一例で、ビルダーパターンに似ている。
import sbt.io.{ IO, CopyOptions }
IO.copyDirectory(source, target)
// The above is same as the following
IO.copyDirectory(source, target, CopyOptions()
.withOverwrite(false)
.withPreserveLastModified(true)
.withPreserveExecutable(true))
sbt 1.0 は Lightbend社の Eugene Yokota (@eed3si9n) と Scala Center の Martin Duhem (@Duhemm) 共著で書かれた Library management API を追加する。 この API は Apache Ivy および cached resolution や Coursier といったその他の代替依存性解決エンジンを抽象化することを目指している。
Ivy エンジンのためのアーティファクトの並列ダウンロードは Scala Center の Jorge (@jvican) によってコントリビュートされた。 また、これは Gigahorse OkHttp を Network API として導入し、内部で Square OkHttp をアーティファクトのダウンロードにも用いる。
lm#124 by @eed3si9n/@Duhemm, lm#90 by @jvican/@jsuereth and lm#104 by @eed3si9n.
Zinc の内部構造の保存方法として Google Protocol Buffer を用いたバイナリ形式が Scala Center の Jorge (@jvican) によってコントリビュートされた。この新形式は主に 3つの利点がある:
ライブラリ依存性のロッキング機能はまだ実装途中だが、Scala Center の Jorge (@jvican) は関連する機能を追加して、最終的にロッキングが可能となる予定だ。
感謝しなければいけない人が多すぎでここにおさまらなった。Credits を参照してほしい。
このパートでは,sbtの各トピックを詳細に扱う。 これを読む前に、基礎知識として始める sbtを読む必要があるだろう。
このページでは、 sbt を利用するに当たってのベストプラクティスについて説明する。
project/
と ~/.sbt/
の使い分け プロジェクトをビルドするために必要なものは、 project/
に配置するべきだ。
例えばwebプラグインのようなものがこれに相当する。
~/.sbt/
には、ビルドで使用するローカル環境のカスタマイズやコマンドなど、プロジェクトのビルドに必須ではないものを配置する。
例えば IDE のプラグインなどがこれに相当する。
ユーザ独自の設定を行うには2つの方法がある。 そのようなユーザ独自の設定の一例として、 resolvers のリストのはじめにローカルのMavenリポジトリを追加することが挙げられる。
resolvers := {
val localMaven = "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
localMaven +: resolvers.value
}
$HOME/.sbt/1.0/global.sbt
などのグローバルな .sbt
ファイルに記述する方法。
ここに記述した設定は、全てのプロジェクトに適用される。
<project>/local.sbt
のようなプロジェクト内の .sbt
ファイルに記述し、バージョンコントロールから除外しておく方法。
sbt は複数の .sbt ファイルの設定を結合するので、バージョンコントロール下に通常の <project>/build.sbt
ファイルも持つことができる。
sbt 起動時に実行するコマンドは .sbtrc
ファイルの各行に記述する。
これらのコマンドはプロジェクトがロードする前に実行されるため、エイリアスの定義などに便利だ。
sbt は、まず $HOME/.sbtrc
内のコマンドを実行し(ファイルが存在する場合のみ)、次に <project>/.sbtrc
を実行する(ファイルが存在する場合のみ)。
生成されるファイルは target
で設定された出力ディレクトリのサブディレクトリに書き出す。
このようにしておくことで、生成ファイルが一箇所に整理され、ビルド後のクリーンアップが容易になる。
クロスビルドを効率的にするために、 Scala のバージョンごとに生成されるファイルは crossTarget
の下に書き出す。
ソースとリソースの生成については、ソースファイル/リソースファイルの生成を参照して欲しい。
出力ディレクトリの target/
などの定数をハードコードするべきではない。
これはプラグインを書く際に特に重要だ。
ユーザはこれを build/
に変更するかもしれないし、プラグインもそれを尊重すべきだ。
代わりに次のような設定を使うとよい。
myDirectory := target.value / "sub-directory"
ビルドは通常たくさんのファイル操作で成り立っている。 これをミュータブルな状態を作らないように設計されたタスクシステムでうまく扱うにはどのようにすればよいだろうか。 推奨される方法の1つは、ファイルへの書き込みを1つのタスクから、かつ1度のみにすることだ。このアプローチは sbt の既定のタスクでも採用している。
1つのビルド成果物へは、ただ1度、1つのタスクからのみ書きこみをされるべきだ。 そのタスクでは生成された File オブジェクトを返し、そのファイルを利用する他のタスクは、このタスクをmapする。 このようにすることで、ファイルの参照を取得すると同時にファイルを生成するタスクが先に実行されることを保証できる。
もちろんこれでもユーザや他のプロセスがファイルを変更することを防ぐことはできないが、 タスクレベルではファイルの内容をイミュータブルに扱うので、ビルドのコントロール下にあるI/Oをより予測可能なものにできる。
例は次の通りだ。
lazy val makeFile = taskKey[File]("Creates a file with some content.")
// ファイルを作成するタスクの定義
// 内容を書き込んでその File オブジェクトを返す
makeFile := {
val f: File = file("/tmp/data.txt")
IO.write(f, "Some content")
f
}
// makeFile の帰り値は生成された File オブジェクトだ。
// そのため、 useFile タスクは makeFile タスクを map することで、
// ファイルへの参照を得るのと同時に、 makeFile タスクへの依存性を宣言できる。
useFile :=
doSomething( makeFile.value )
このような書き方がいつもできるとは限らないが、例外ではなく慣例とすべきだ。
絶対的な File オブジェクトのみを利用すべきだ。 次のように、絶対パスを指定するか、
file("/home/user/A.scala")
絶対的な File オブジェクトを起点として別の File オブジェクトを構築する。
base / "A.scala"
これは前述のハードコーディングしないというベストプラクティスにも関連する。
なぜなら適切な方法は、 baseDirectory
の設定を参照することだからだ。
次の例では、 myPath に <base>/licenses/
ディレクトリを設定している。
myPath := baseDirectory.value / "licenses"
Java や Scala では相対的な File オブジェクトは現在のワーキングディレクトリからの相対パスを表す。 種々の理由から、このワーキングディレクトリは、常にビルドのルートディレクトリに一致するとは限らない。
このルールの唯一の例外は、プロジェクトのベースディレクトリを利用している場合だ。 この場合、sbt は利便性のため、相対的な File オブジェクトをビルドのルートディレクトリからの相対パスとして解決する。
token
を利用すること。
flatMap
を利用すること。 sbt のコンビネータは生成クラス数の上限に厳格だ。そのため flatMap を使って次のように記述するとよい。
lazy val parser: Parser[Int] =
token(IntBasic) flatMap { i =>
if(i <= 0)
success(i)
else
token(Space ~> parser)
}
上記の例では、負数を末尾にもつ空白区切りの整数列をパースし、最後の負数を返すパーサを定義している。
テストの話をしよう。一度プラグインを書いてしまうと、どうしても長期的なものになってしまう。新しい機能を加え続ける(もしくはバグを直し続ける)ためにはテストを書くのが合理的だ。
sbt は、scripted test framework というものが付いてきて、ビルドの筋書きをスクリプトに書くことができる。これは、もともと 変更の自動検知や、部分コンパイルなどの複雑な状況下で sbt 自体をテストするために書かれたものだ:
ここで、仮に B.scala を削除するが、A.scala には変更を加えないものとする。ここで、再コンパイルすると、A から参照される B が存在しないために、エラーが得られるはずだ。 [中略 (非常に複雑なことが書いてある)]
scripted test framework は、sbt が以上に書かれたようなケースを的確に処理しているかを確認するために使われている。
このフレームワークは scripted-plugin 経由で利用可能だ。 このページはプラグインにどのようにして scripted-plugin を導入するかを解説する。
scripted-plugin はプラグインをローカルに publish するため、まずは version を -SNAPSHOT なものに設定しよう。ここで SNAPSHOT を使わないと、あなたと世界のあなた以外の人が別々のアーティファクトを観測するといった酷い不整合な状態に入り込む場合があるからだ。
build.sbt
で SbtPlugin
を enable する。
lazy val root = (project in file("."))
.enablePlugins(SbtPlugin)
.settings(
name := "sbt-something"
)
以下のセッティングを build.sbt
に加える:
lazy val root = (project in file("."))
.enablePlugins(SbtPlugin)
.settings(
name := "sbt-something",
scriptedLaunchOpts := { scriptedLaunchOpts.value ++
Seq("-Xmx1024M", "-Dplugin.version=" + version.value)
},
scriptedBufferLog := false
)
注意: SbtPlugin
は sbt 1.2.1 以上を必要とする。
src/sbt-test/<テストグループ>/<テスト名>
というディレクトリ構造を作る。とりあえず、src/sbt-test/<プラグイン名>/simple
から始めるとする。
ここがポイントなんだけど、simple
下にビルドを作成する。プラグインを使った普通のビルド。手動でテストするために、いくつか既にあると思うけど。以下に、build.sbt
の例を示す:
lazy val root = (project in file("."))
.settings(
version := "0.1",
scalaVersion := "2.10.6",
assembly / assemblyJarName := "foo.jar"
)
これが、project/plugins.sbt
:
sys.props.get("plugin.version") match {
case Some(x) => addSbtPlugin("com.eed3si9n" % "sbt-assembly" % x)
case _ => sys.error("""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin)
}
これは earldouglas/xsbt-web-plugin@feabb2 から拝借してきた技で、これで scripted テストに version を渡すことができる。
他に、src/main/scala/hello.scala
も用意した:
object Main {
def main(args: Array[String]): Unit = {
println("hello")
}
}
次に、好きな筋書きを記述したスクリプトを、テストビルドのルート下に置いた test
というファイルに書く。
# ファイルが作成されたかを確認
> assembly
$ exists target/scala-2.10/foo.jar
スクリプトの文法は以下の通り:
#
は一行コメントを開始する
>
name
はタスクを sbt に送信する(そして結果が成功したかをテストする)
$
name arg*
はファイルコマンドを実行する(そして結果が成功したかをテストする)
->
name
タスクを sbt に送信するが、失敗することを期待する
-$
name arg*
ファイルコマンドを実行するが、失敗することを期待する
ファイルコマンドは以下のとおり:
touch
path+
は、ファイルを作成するかタイムスタンプを更新する
delete
path+
は、ファイルを削除する
exists
path+
は、ファイルが存在するか確認する
mkdir
path+
は、ディレクトリを作成する
absent
path+
は、はファイルが存在しないことを確認する
newer
source target
は、source
の方が新しいことを確認する
must-mirror
source target
は、source
が同一であることを確認する
pause
は、enter が押されるまで待つ
sleep
time
は、スリープする
exec
command args*
は、別のプロセスでコマンドを実行する
copy-file
fromPath toPath
は、ファイルをコピーする
copy
fromPath+ toDir
は、パスを相対構造を保ったまま toDir
下にコピーする
copy-flat
fromPath+ toDir
は、パスをフラットに toDir
下にコピーする
ということで、僕のスクリプトは、assembly
タスクを実行して、foo.jar
が作成されたかをチェックする。もっと複雑なテストは後ほど。
スクリプトを実行するためには、プラグインのプロジェクトに戻って、以下を実行する:
> scripted
これはテストビルドをテンポラリディレクトリにコピーして、test
スクリプトを実行する。もし全て順調にいけば、まず publishLocal
の様子が表示され、以下のようなものが表示される:
Running sbt-assembly / simple
[success] Total time: 18 s, completed Sep 17, 2011 3:00:58 AM
ファイルコマンドは便利だけど、実際のコンテンツをテストしないため、それだけでは不十分だ。コンテンツをテストする簡単な方法は、テストビルドにカスタムのタスクを実装してしまうことだ。
上記の hello プロジェクトを例に取ると、生成された jar が “hello” と表示するかを確認したいとする。scala.sys.process.Process
を用いて jar を走らせることができる。失敗を表すには、単にエラーを投げればいい。以下に build.sbt
を示す:
import scala.sys.process.Process
lazy val root = (project in file("."))
.settings(
version := "0.1",
scalaVersion := "2.10.6",
assembly / assemblyJarName := "foo.jar",
TaskKey[Unit]("check") := {
val process = Process("java", Seq("-jar", (crossTarget.value / "foo.jar").toString))
val out = (process!!)
if (out.trim != "bye") sys.error("unexpected output: " + out)
()
}
)
ここでは、テストが失敗するのを確認するため、わざと “bye” とマッチするかテストしている。
これが test
:
# ファイルが作成されたかを確認
> assembly
$ exists target/foo.jar
# hello って言うか確認
> check
scripted
を走らせると、意図通りテストは失敗する:
[info] [error] {file:/private/var/folders/Ab/AbC1EFghIj4LMNOPqrStUV+++XX/-Tmp-/sbt_cdd1b3c4/simple/}default-0314bd/*:check: unexpected output: hello
[info] [error] Total time: 0 s, completed Sep 21, 2011 8:43:03 PM
[error] x sbt-assembly / simple
[error] {line 6} Command failed: check failed
[error] {file:/Users/foo/work/sbt-assembly/}default-373f46/*:scripted: sbt-assembly / simple failed
[error] Total time: 14 s, completed Sep 21, 2011 8:00:00 PM
慣れるまでは、テスト自体がちゃんと振る舞うのに少し時間がかかるかもしれない。ここで使える便利なテクニックがいくつある。
まず最初に試すべきなのは、ログバッファリングを切ることだ。
> set scriptedBufferLog := false
これにより、例えばテンポラリディレクトリの場所などが分かるようになる:
[info] [info] Set current project to default-c6500b (in build file:/private/var/folders/Ab/AbC1EFghIj4LMNOPqrStUV+++XX/-Tmp-/sbt_8d950687/simple/project/plugins/)
...
テスト中にテンポラリディレクトリを見たいような状況があるかもしれない。test
スクリプトに以下の一行を加えると、scripted はエンターキーを押すまで一時停止する:
$ pause
もしうまくいかなくて、 sbt/sbt-test/sbt-foo/simple
から直接 sbt
を実行しようと思っているなら、それは止めたほうがいい。正しいやり方はディレクトリごと別の場所にコピーしてから走らせることだ。
sbt プロジェクト下には文字通り 100+ の scripted テストがある。色々眺めてみて、インスパイアされよう。
例えば、以下に by-name と呼ばれるものを示す:
> compile
# change => Int to Function0
$ copy-file changes/A.scala A.scala
# Both A.scala and B.scala need to be recompiled because the type has changed
-> compile
xsbt-web-plugin や sbt-assembly にも scripted テストがある。
これでおしまい!プラグインをテストしてみた経験などを聞かせて下さい!
How to 記事の一覧は目次を参照してください。
sbt にはソースコードやリソースの生成を行うタスクを登録する標準的なフックが用意されている。
ソースコードを生成するタスクでは、ソースコードを sourceManaged
のサブディレクトリに生成し、
生成した File オブジェクトを返すように実装するのがよいだろう。
タスクの実装の核となる、ソースを生成する関数のシグネチャは次のようになる。
def makeSomeSources(base: File): Seq[File]
ソースを生成するタスクは sourceGenerators
キーに追加する。
ここでは、実行結果の値ではなくタスク自体を追加するため、通常の value
ではなく、 taskValue
を使う。
sourceGenerators
には生成するソースが main か test かに応じて、それぞれ Compile
、 Test
のスコープ付けをしておく。
大まかな定義は次のようになる。
Compile / sourceGenerators += <task of type Seq[File]>.taskValue
これは、 def makeSomeSources(base: File): Seq[File]
を用いて次のように書ける。
Compile / sourceGenerators += Def.task {
makeSomeSources((Compile / sourceManaged).value / "demo")
}.taskValue
より具体的な例を示そう。
次の例では、 source generator は、実行するとコンソールに "Hi"
と表示する Test.scala
というアプリケーションオブジェクトを生成する。
Compile / sourceGenerators += Def.task {
val file = (Compile / sourceManaged).value / "demo" / "Test.scala"
IO.write(file, """object Test extends App { println("Hi") }""")
Seq(file)
}.taskValue
これを run
タスクで実行すると、次のように "Hi"
と表示されるだろう。
> run
[info] Running Test
Hi
テスト用のソースコードを生成したい場合は、上記の Compile
の部分を Test
に変更する。
注意:
ビルドを効率化するために、 sourceGenerators
では、呼び出しの度にソースの生成を行うのではなく、
sbt.Tracked.{ inputChanged, outputChanged }
などを用いて、必ず入力値に基づいたキャッシングを行うべきである。
デフォルトでは、生成したソースコードはビルド成果物のパッケージには含まれない。 追加するには、別途 mappings への追加が必要になる。 この詳細は、Adding files to a packageを参照して欲しい。 source generator は Java のソースも Scala のソースも1つの Seq で一緒に返すが、 後続の処理は拡張子を元にそれらを区別できる。
リソースを生成するタスクは、リソースを resourceManaged
のサブディレクトリに生成し、
生成した File オブジェクトを返すように実装するのがよいだろう。
ソースの生成の場合と同様に、タスクの実装の核となる、リソースを生成する関数のシグネチャは次のようになる。
def makeSomeResources(base: File): Seq[File]
リソースを生成するタスクは resourceGenerators
キーに追加する。
ここでも、実行結果の値ではなくタスク自体を追加するため、通常の value
ではなく、 taskValue
を使う。
resourceGenerators
にも、生成するリソースが main か test かに応じて、それぞれ Compile
、 Test
のスコープ付けをしておく。
大まかな定義は次のようになる。
Compile / resourceGenerators += <task of type Seq[File]>.taskValue
これは、 def makeSomeResources(base: File): Seq[File]
を用いて次のように書ける。
Compile / resourceGenerators += Def.task {
makeSomeResources((Compile / resourceManaged).value / "demo")
}.taskValue
上記の例を、run
タスク、または package
タスク (compile
タスクでないことに注意) で実行すると、
resourceManaged
が示す "target/scala-*/resource_managed"
の中に demo
というファイルが生成される。
デフォルトでは、生成したリソースはビルド成果物のパッケージには含まれない。
追加するには、別途 mappings への追加が必要になる。
こちらについても、詳細は Adding files to a package を参照して欲しい。
次の例では、アプリケーション名とバージョンが書かれた myapp.properties
というプロパティファイルが生成される。
Compile / resourceGenerators += Def.task {
val file = (Compile / resourceManaged).value / "demo" / "myapp.properties"
val contents = "name=%s\nversion=%s".format(name.value,version.value)
IO.write(file, contents)
Seq(file)
}.taskValue
テスト用のリソースとして扱いたい場合は Compile
を Test
に変更する。
注意:
ビルドを効率化するために、 resourceGenerators
では、呼び出しの度にリソースの生成を行うのではなく、
sbt.Tracked.{ inputChanged, outputChanged }
などを用いて、必ず入力値に基づいたキャッシングを行うべきである。
sbt で最もよくある質問の一つに「X をやった後で Y をするにはどうすればいいのか?」というものがある。
一般論としては、sbt のタスクはそのように作られていない。なぜなら、build.sbt はタスクの依存グラフ作るための DSL だからだ。これに関してはタスクの実行意味論で解説してある。そのため、理想的にはタスク Y を自分で定義して、そこからタスク X に依存させるべきだ。
taskY := {
val x = taskX.value
x + 1
}
これは、以下のような、副作用のあるメソッド呼び出しを続けて行っているような命令型の素の Scala と比べるとより制限されていると言える:
def foo(): Unit = {
doX()
doY()
}
この依存指向なプログラミング・モデルの利点は sbt のタスク・エンジンがタスクの実行の順序を入れ替えることができることにある。実際、可能な限り sbt は依存タスクを並列に実行する。もう一つの利点は、グラフを非重複化して一回のコマンド実行に対して Compile / compile
などのタスクは一度だけ実行することで、同じソースを何度もコンパイルすることを回避している。
タスク・システムがこのような設計になっているため、何かを逐次実行させるというのは一応可能ではあるけども、システムの流れに反する行為であり、簡単だとは言えない。
sbt 0.13.8 で Def.sequential
という関数が追加されて、準逐次な意味論でタスクを実行できるようになった。
逐次タスクの説明として compilecheck
というカスタムタスクを定義してみよう。これは、まず Compile / compile
を実行して、その後で scalastyle-sbt-plugin の Compile / scalastyle
を呼び出す。
セットアップはこのようになる。
sbt.version=1.9.8
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
lazy val compilecheck = taskKey[Unit]("compile and then scalastyle")
lazy val root = (project in file("."))
.settings(
Compile / compilecheck := Def.sequential(
Compile / compile,
(Compile / scalastyle).toTask("")
).value
)
このタスクを呼び出すには、シェルから compilecheck
と打ち込む。もしコンパイルが失敗すると、compilecheck
はそこで実行を中止する。
root> compilecheck
[info] Compiling 1 Scala source to /Users/x/proj/target/scala-2.10/classes...
[error] /Users/x/proj/src/main/scala/Foo.scala:3: Unmatched closing brace '}' ignored here
[error] }
[error] ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
これで、タスクを逐次実行できた。
逐次タスクだけで十分じゃなければ、次のステップは動的タスクだ。純粋な型 A
の値を返すことを期待する Def.task
と違って、Def.taskDyn
は sbt.Def.Initialize[sbt.Task[A]]
という型のタスク・エンジンが残りの計算を継続するタスクを返す。
Compile / compile
を実行した後で scalastyle-sbt-plugin の Compile / scalastyle
タスクを実行するカスタムタスク、compilecheck
を実装してみよう。
sbt.version=1.9.8
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
lazy val compilecheck = taskKey[sbt.inc.Analysis]("compile and then scalastyle")
lazy val root = (project in file("."))
.settings(
compilecheck := (Def.taskDyn {
val c = (Compile / compile).value
Def.task {
val x = (Compile / scalastyle).toTask("").value
c
}
}).value
)
これで逐次タスクと同じものができたけども、違いは最初のタスクの結果である c
を返していることだ。
Compile / compile
の戻り値と同じ型を返せるようになったので、もとのキーをこの動的タスクで再配線 (rewire) できるかもしれない。
lazy val root = (project in file("."))
.settings(
Compile / compile := (Def.taskDyn {
val c = (Compile / compile).value
Def.task {
val x = (Compile / scalastyle).toTask("").value
c
}
}).value
)
これで、Compile / compile
をシェルから呼び出してやりたかったことをやらせれるようになった。
ここまでタスクに焦点を当ててみてきた。タスクには他にインプットタスクというものがあって、これはユーザからの入力をシェル上で受け取る。
典型的な例としては Compile / run
タスクがある。scalastyle
タスクも実はインプットタスクだ。インプットタスクの詳細は Input Task 参照。
ここで、Compile / run
タスクの実行後にテスト用にブラウザを開く方法を考えてみる。
object Greeting {
def main(args: Array[String]): Unit = {
println("hello " + args.toList)
}
}
lazy val runopen = inputKey[Unit]("run and then open the browser")
lazy val root = (project in file("."))
.settings(
runopen := {
(Compile / run).evaluated
println("open browser!")
}
)
ここでは、ブラウザを本当に開く代わりに副作用のある println
で例示した。シェルからこのタスクを呼び出してみよう:
> runopen foo
[info] Compiling 1 Scala source to /x/proj/...
[info] Running Greeting foo
hello List(foo)
open browser!
この新しいインプットタスクを Compile / run
に再配線することで、実は runopen
キーを外すことができる:
lazy val root = (project in file("."))
.settings(
Compile / run := {
(Compile / run).evaluated
println("open browser!")
}
)
ここで、プラグインが openbrowser
というブラウザを開くタスクを既に提供していると仮定する。それをインプットタスクの後で呼び出す方法を考察する。
lazy val runopen = inputKey[Unit]("run and then open the browser")
lazy val openbrowser = taskKey[Unit]("open the browser")
lazy val root = (project in file("."))
.settings(
runopen := (Def.inputTaskDyn {
import sbt.complete.Parsers.spaceDelimited
val args = spaceDelimited("<args>").parsed
Def.taskDyn {
(Compile / run).toTask(" " + args.mkString(" ")).value
openbrowser
}
}).evaluated,
openbrowser := {
println("open browser!")
}
)
この動的インプットタスクを Compile / run
に再配線するのは複雑な作業だ。内側の Compile / run
は既に継続タスクの中に入ってしまっているので、単純に再配線しただけだと循環参照を作ってしまうことになる。
この循環を断ち切るためには、Compile / run
のクローンである Compile / actualRun
を導入する必要がある:
lazy val actualRun = inputKey[Unit]("The actual run task")
lazy val openbrowser = taskKey[Unit]("open the browser")
lazy val root = (project in file("."))
.settings(
Compile / run := (Def.inputTaskDyn {
import sbt.complete.Parsers.spaceDelimited
val args = spaceDelimited("<args>").parsed
Def.taskDyn {
(Compile / actualRun).toTask(" " + args.mkString(" ")).value
openbrowser
}
}).evaluated,
Comile / actualRun := Defaults.runTask(
Runtime / fullClasspath,
Compile / run / mainClass,
Compile / run / runner
).evaluated,
openbrowser := {
println("open browser!")
}
)
この Compile / actualRun
の実装は Defaults.scala にある run
の実装からコピペしてきた。
これで run foo
をシェルから打ち込むと、Compile / actualRun
を引数とともに評価して、その後で openbrowser
タスクを評価するようになった。
副作用にしか使っていなくて、人間がコマンドを打ち込んでいるのを真似したいだけならば、カスタムコマンドを作れば済むことかもしれない。これは例えば、リリース手順とかに役立つ。
これは sbt そのもののビルドスクリプトから抜粋だ:
commands += Command.command("releaseNightly") { state =>
"stampVersion" ::
"clean" ::
"compile" ::
"publish" ::
"bintrayRelease" ::
state
}
このページは、既にsbt 0.13.13 以上をインストールしたことを前提とする。
sbt 0.13.13 以降を使っている場合は、sbt new
コマンドを使って手早く簡単な Hello world ビルドをセットアップすることができる。
以下をターミナルから打ち込む。
$ sbt new sbt/scala-seed.g8
....
Minimum Scala build.
name [My Something Project]: hello
Template applied in ./hello
プロジェクト名を入力するプロンプトが出てきたら hello
と入力する。
これで、hello
ディレクトリ以下に新しいプロジェクトができた。
次に hello
ディレクトリ内から sbt を起動して sbt のシェルから
run
と入力する。Linux や OS X の場合、コマンドは以下のようになる:
$ cd hello
$ sbt
...
> run
...
[info] Compiling 1 Scala source to /xxx/hello/target/scala-2.12/classes...
[info] Running example.Hello
hello
後で他のタスクもみていく。
sbt シェルを終了するには、exit
と入力するか、Ctrl+D (Unix) か Ctrl+Z (Windows) を押す。
> exit
ビルド設定方法はプロジェクトのベースディレクトリに build.sbt
というファイルとして配置される。
ファイルを読んでみてもいいが、このビルドファイルに書いてあることが分からなくても心配しないでほしい。
ビルド定義で、build.sbt
の書き方を説明する。