例題でみる sbt

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

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

最小 sbt ビルドを作る

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

sbt シェルを起ち上げる

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

sbt シェルを終了させる

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

sbt:foo-build> exit

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

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

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

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

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

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

ソース・ファイルを書く

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

package example

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

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

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

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

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

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

sbt:foo-build> compile

ヘルプを読む

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

sbt:foo-build> help

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

....

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

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

アプリを実行する

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

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

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

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

sbt:foo-build> scalaVersion
[info] 3.7.2

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

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

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

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

scalaVersion := "3.7.2"

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

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

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

ビルドの再読み込み

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

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

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

libraryDependencies に toolkit-test を追加する

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

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

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

sbt:Hello> reload

差分テストを実行する

sbt:Hello> test

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

sbt:Hello> ~test

テストを書く

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

package example

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

~test が検知したはずだ:

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

テストが通るようにする

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

package example

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

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

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

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

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

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

Scala REPL を使う

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

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

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

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

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

// press Ctrl+D

// Exiting paste mode, now interpreting.

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

scala> :q // to quit

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

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

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

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

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

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

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

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

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

sbt:Hello> helloCore/compile

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

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

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

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

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

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

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

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

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

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

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

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

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

sbt:Hello> ~test

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

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

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

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

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

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

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

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

helloCore に uJson を追加しよう。

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

package example.core

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

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

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

package example

import example.core.Weather

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

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

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

一時的に scalaVersion を変更する

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

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

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

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

バッチ・モード

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

$ sbt clean "testOnly HelloSuite"

sbt new コマンド

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

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

name [My Something Project]: hello

Template applied in ./hello

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

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

クレジット

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