sbt は Scala および Java を主な対象としたビルドツールです。Scala Build Tool の略ではありませんが、Simple Build Tool という明示的な記述も公式ドキュメントなどには見当りません。以下 sbt の基本的な使用例をまとめます。使用した sbt のバージョンは 0.13 です。
sbt の実体は jar ファイルです。OSX, Windows, Linux 等、JVM (1.6 以上) が動作する環境であればどのプラットフォームでも動作します。Macports, Homebrew, msi インストーラ, yum RPM などが提供されていますが、結局のところ、シェルスクリプトまたはバッチファイルで JAR を実行しているだけです。CentOS で手動インストールする例を記載します。
JVM が必要なため、JDK または JRE をインストールします。
sudo yum install java-1.8.0-openjdk-devel
sbt JAR をダウンロードして適当なディレクトリに保存します。
wget https://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.9/sbt-launch.jar
mv sbt-launch.jar ~/bin/
JAR を実行するシェルスクリプトを用意します。"$@"
についてはこちらをご参照ください。
~/bin/sbt
#!/bin/bash
SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxMetaspaceSize=256M"
java $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"
パーミッションを設定します。
chmod u+x ~/bin/sbt
サンプルコードのビルド
mkdir hello
cd hello/
vi hw.scala
hw.scala
object Hi {
def main(args: Array[String]) = println("Hi!")
}
ビルド
sbt
...
> run
> exit
初回実行時は非常に時間がかかります。~/.sbt/
および ~/.ivy2/
に Scala の JAR 等、sbt の動作に必要な JAR をダウンロードするためです。
先程のサンプルコード hw.scala
のように sbt コマンドはプロジェクトのベースディレクトリのファイルもビルドできます。しかしながら、ファイル分割された一般的なプロジェクトの場合は以下のようなディレクトリ構成でファイルを整理します。これは Maven と同じ構成です。
ベースディレクトリ/
.gitignore
build.sbt
project/
build.properties
<その他の設定ファイル .scala または .sbt>
target/
<コンパイル結果 class ファイルなど>
lib/
<ライブラリ JAR ファイル>
src/
main/
resources/
<jar に含めたいファイル>
scala/
<Scala ソースコード>
java/
<Java ソースコード>
test/
resources
<test jar に含めたいファイル>
scala/
<Scala テストコード>
java/
<Java テストコード>
target/
<成果物 JAR など>
.gitignore
を利用する場合は target/
を記載します。/target/
でも target
でもなく target/
です。target ディレクトリは様々な階層に生成されます。
設定ファイルに Scala および sbt のバージョンを記載することで、例えば複数人で開発する場合でも同じ開発環境が構築できます。バージョンの差異によって生じるバグを回避できます。
scalaVersion
を指定することで Scala バージョンを固定できます。初回ビルド時は自動で該当の Scala JAR をダウンロードして ~/.ivy2/
に保存します。無指定の場合は sbt が動作のために利用している Scala バージョンが利用されます。
build.sbt
lazy val root = (project in file(".")).
settings(
name := "my-scala-app",
version := "1.0",
scalaVersion := "2.11.7"
)
sbt バージョンによる差異が基本的にありませんが、sbt のバージョンも固定しておくことはよいことです。
project/build.properties
sbt.version=0.13.9
インタラクティブモード
$ sbt
> run
バッチモード
$ sbt run
$ sbt
> ~ compile
または
$ sbt "~ compile"
$ sbt clean
$ sbt test
$ sbt console
$ sbt run
$ sbt package
jar コマンド で成果物の中身を確認してみましょう。
$ jar tf target/scala-2.11/hello_2.11-1.0.jar
META-INF/MANIFEST.MF
Hi$.class
Hi.class
src/main/scala
と src/main/java
をコンパイルしたクラスファイルおよび src/main/resources
が格納されています。
$ sbt help
$ sbt "help package"
build.sbt
, project/*.scala
, project/*.sbt
ファイルを編集した場合は reload
を実行して再読み込みする必要があります。
$ sbt
> reload
lib
ディレクトリに jar ファイルを置くことで利用build.sbt
に設定を記載することでインターネットから jar ファイルをダウンロードして利用pom.xml
に記載する plugin と同等の概念SWT で GUI ツールを作成できます。
build.sbt
lazy val root = (project in file(".")).
settings(
name := "hello",
version := "1.0",
scalaVersion := "2.11.7",
mainClass in assembly := Some("com.mycompany.app.Hi")
)
.gitignore
target/
lib/swt.jar
<動作させる予定の OS 用にダウンロードしたもの>
project/build.properties
sbt.version=0.13.9
src/main/scala/hw.scala
package com.mycompany.app
import org.eclipse.swt.SWT
import org.eclipse.swt.layout.RowLayout
import org.eclipse.swt.widgets.Button
import org.eclipse.swt.widgets.Display
import org.eclipse.swt.widgets.Shell
import org.eclipse.swt.widgets.Text
object Hi {
def main(args: Array[String]) {
val display = new Display()
val shell = new Shell(display)
shell.setText("SWT アプリケーション")
shell.setLayout(new RowLayout())
val button = new Button(shell, SWT.NULL)
button.setText("押してください")
shell.open()
while(!shell.isDisposed()) {
if(!display.readAndDispatch()) {
display.sleep()
}
}
display.dispose()
}
}
sbt-assembly プラグインを利用すると Scala JAR や lib 以下の JAR をすべて含めた全部入りの fat JAR を生成できます。Maven の Apache Maven Assembly Plugin や Apache Maven Shade Plugin のようなものです。
project/assembly.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1")
ビルド
$ sbt
> assembly
実行例 (OSX で SWT アプリケーションを実行する場合は XstartOnFirstThread
が必要です)
$ java -XstartOnFirstThread -jar ./target/scala-2.11/hello-assembly-1.0.jar
Java のロガー Logback を利用してみます。
build.sbt
lazy val root = (project in file(".")).
settings(
name := "hello",
version := "1.0",
scalaVersion := "2.11.7",
libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.1.3",
"org.slf4j" % "slf4j-api" % "1.7.12"
)
)
.gitignore
target/
/logs/
project/build.properties
sbt.version=0.13.9
src/main/scala/hw.scala
import org.slf4j.Logger
import org.slf4j.LoggerFactory
object Hi {
def main(args: Array[String]) {
val logger = LoggerFactory.getLogger("Hi")
logger.info("info: {}", 1)
logger.warn("warn: {}", 2)
logger.error("error: {}", 3)
}
}
src/main/resources/logback.xml
<configuration>
<property name="LOG_DIR" value="./logs" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/myapp.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>${LOG_DIR}/myapp.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- keep 30 days' worth of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
</root>
</configuration>
ビルド
$ sbt
> run
[info] Running Hi
info: 1
warn: 2
error: 3
[success] Total time: 2 s, completed 2016/01/26 15:48:23
ログが生成されました。
$ cat logs/myapp.log
2016-01-26 15:47:08,646 INFO [run-main-0] Hi [hw.scala:7] info: 1
2016-01-26 15:47:08,662 WARN [run-main-0] Hi [hw.scala:8] warn: 2
2016-01-26 15:47:08,662 ERROR [run-main-0] Hi [hw.scala:9] error: 3
bundler の package のようなオプションが sbt にもあります。ネットワーク上にある JAR ファイルの消失に備えてプロジェクト内に保存しておきたい場合に有効化します。
lazy val root = (project in file(".")).
settings(
...
// Copy all managed dependencies to <build-root>/lib_managed/
// This is essentially a project-local cache and is different
// from the lib_managed/ in sbt 0.7.x. There is only one
// lib_managed/ in the build root (not per-project).
retrieveManaged := true,
...
)
複数の関連プロジェクトを同じ sbt で管理することができます。
lazy val commonSettings = Seq(
scalaVersion := "2.11.7",
retrieveManaged := true,
libraryDependencies ++= Seq(
)
)
lazy val root = (project in file(".")).
aggregate(module1, module2).
dependsOn(module1, module2).
settings(commonSettings: _*).
settings(
libraryDependencies ++= Seq(
)
)
lazy val module1 = (project in file("module1")).
settings(commonSettings: _*).
settings(
name := "myapp-module2",
version := "1.0",
mainClass in assembly := Some("myapp.module1.Main"),
libraryDependencies ++= Seq(
)
)
lazy val module2 = (project in file("module2")).
settings(commonSettings: _*).
settings(
name := "myapp-module2",
version := "1.0",
mainClass in assembly := Some("myapp.module2.Main"),
libraryDependencies ++= Seq(
)
)
dependsOn(module1, module2)
によって root プロジェクトで module1, module2 のクラスを import して利用可能aggregate(module1, module2)
によって root プロジェクトで compile などを実行すると module1, module2 でも compile が実行されるソースファイルなどはそれぞれの階層で管理します。
/src/*
/module1/src/*
/module2/src/*
コンソールでのプロジェクトの切り替え
$ sbt
> projects
[info] In file:/paty/to/myapp/
[info] module1
[info] module2
[info] * root
> project module1
[info] Set current project to myapp-module1 (in build file:/paty/to/myapp/)
> projects
[info] In file:/paty/to/myapp/
[info] * module1
[info] module2
[info] root
現在のプロジェクトでタスクを実行
> run
プロジェクトを指定してタスクを実行
> root/run
> module1/run
> module2/run
上記 assembly
タスクを実行すると、依存 JAR などがすべて JAR に Merge されてパッケージングされます。その際、重複した名前のファイルが複数の JAR に含まれていると Merge 時にコンフリクトします。
[error] (*:assembly) deduplicate: different file contents found in the following:
コンフリクトを解決するための規則を予め build.sbt
に記載しておくことでこれを回避できます。
lazy val root = (project in file(".")).
settings(
name := "my-scala-app",
version := "1.0",
scalaVersion := "2.11.7",
assemblyMergeStrategy in assembly := {
case PathList("javax", "servlet", xs @ _*) => MergeStrategy.first
case PathList("path", "to", "file.txt") => MergeStrategy.discard
case "unwanted.txt" => MergeStrategy.discard
case x =>
val oldStrategy = (assemblyMergeStrategy in assembly).value
oldStrategy(x)
}
)
上記設定では以下のようにコンフリクトを解決します。パターンマッチを利用して Path 毎に設定します。
/javax/servlet/*
最初に出現したものを JAR に含める/path/to/file.txt
JAR に含めない/unwanted.txt
JAR に含めない