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 のバージョンを記載することで、例えば複数人で開発する場合でも同じ開発環境が構築できます。バージョンの差異によって生じるバグを回避できます。
Scala バージョンの固定
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 バージョンによる差異が基本的にありませんが、sbt のバージョンも固定しておくことはよいことです。
project/build.properties
sbt.version=0.13.9
よく使用するコマンド
インタラクティブモード
$ sbt
> run
バッチモード
$ sbt run
ファイルの変化を検知して自動コンパイル
$ sbt
> ~ compile
または
$ sbt "~ compile"
target に生成されたファイルを削除
$ sbt clean
コンパイルしてテストを実行
$ sbt test
Scala インタープリタを起動
$ sbt console
Main クラスを実行
$ sbt run
JAR 成果物を生成
$ 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
ライブラリの使用方法
- Unmanaged dependencies
lib
ディレクトリに jar ファイルを置くことで利用
- Managed dependencies
build.sbt
に設定を記載することでインターネットから jar ファイルをダウンロードして利用- Maven の
pom.xml
に記載する plugin と同等の概念
Unmanaged dependencies 設定例
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
Managed dependencies 設定例
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
JAR ファイルをキャッシュ保存
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 が実行される
ソースファイルなどはそれぞれの階層で管理します。
- root:
/src/*
- module1:
/module1/src/*
- module2:
/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
sbt-assembly 時の Merge エラー対応
上記 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 に含めない- その他は既定 (コンフリクト時にはエラー)
その他
関連記事
- Scala 文字列の処理書式指定 object Main { def main(args: Array[String]): Unit = { println("%d + %d = %d".format(1, 1, 2)) //=> 1 + 1 = 2 } } 文字列の比較 ヒアドキュメント 他の言語でいう「ヒアドキュメント」のようなものは """ で囲うことで実現できます。 object Main ...
- Scala 日付に関する処理Date クラスを文字列にフォーマット import java.util.Date object Main { def main(args: Array[String]): Unit = { // format は Date に限らない文字列用の機能です。 println("%d-%d-%d" format (1, 1, 1)) //=> 1-1-1 printl...
- Scala 関数のサンプルコード「デフォルト引数」および「Unit 型を返す関数」 object HelloWorld { def main(args: Array[String]): Unit = { def myPrint(myArg: String = "default_value") = println(myArg + "!") val result = myPrint() //=> defau...
- Scala 組み込みの制御構造if-else 条件分岐で知られる if-else は三項演算子のようにも使用されます。 object HelloWorld { def main(args: Array[String]): Unit = { val myVal = if (!args.isEmpty) args(0) else "default" println(myVal) } ...
- Scala の基本文法Scala は JVM 上で動作するバイトコードにコンパイルできる言語です。JAVA よりも柔軟な記述ができます。事前にこちらからダウンロードおよびインストールしておいてください。基本的な文法をまとめます。 変数および定数 object HelloWorld { def main(args: Array[String]): Unit = { val constVal = 1 //