酢豚の基本的な使い方 (sbt)
[履歴] [最終更新] (2016/02/25 01:32:27)
1
作品
345
技術情報
最近の投稿
ここは
趣味の電子工作を楽しむ人のための作品共有プラットフォーム

ハードウェア技術を作品にして投稿してみませんか?
新着作品

概要

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/scalasrc/main/java をコンパイルしたクラスファイルおよび src/main/resources が格納されています。

ヘルプ

$ sbt help
$ sbt "help package"

設定の再読み込み

build.sbt, project/*.scala, project/*.sbt ファイルを編集した場合は reload を実行して再読み込みする必要があります。

$ sbt
> reload

ライブラリの使用方法

sbt でライブラリを利用する方法は二種類あります。

  • 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 に含めない
  • その他は既定 (コンフリクト時にはエラー)

その他