始める sbt 

原文: Getting Started Guide
誤訳の報告をする。
このスタートガイドは、sbt 0.11 〜 0.12 の頃に書かれたものです。書いてある内容はまだ十分有用ですが、sbt0.13ではある程度の機能追加などもされています。 sbt0.13での変更点や新機能に興味がある人は、こちらsbt 0.13.0 の変更点 も一緒に読むといいでしょう。

「始める sbt」で君も sbt を始めよう。

sbt は柔軟で強力なビルド定義 (build definition) を作ることができるけど、そこで使われている概念の数は多くはない。 多くはないけども、sbt は他のビルドシステムとは一味ちがうから、ドキュメントを読まなければ、いくつかの細かい点でハマっちゃうと思う。

この「始める sbt」で、ビルド定義 (build definition) の作成とメンテナンスに必要な概念を説明する。

「始める sbt」を読むことを強くお勧めしたい!

どうしても忙しいという場合は、最も重要な概念は .sbt ビルド定義、 スコープ、と 他の種類のセッティング に書かれているけど、このガイドの他のページを読み飛ばしても大丈夫かは保障しない。

後のページは前のページで紹介された概念をもとにしてるから、順番に読んでいくのがベスト。

sbt を試してくれて、ありがとう。楽しもう!

sbt のインストール 

sbt プロジェクトを作るには、以下の手順をたどる:

最終的には、sbt のインストールはランチャー JAR とシェルスクリプトの設置という 2つに絞られるけども、 プラットフォームによってもう少し簡単なインストール方法をいくつか提供する。 Mac、Windows、Linux もしくは手動インストールの手順に進んでほしい。

コツと注意 

sbt の実行に上手くいかない場合は、[[Setup Notes]] のターミナルの文字エンコーディング、HTTP プロキシ、JVM のオプションにかんする説明を参照する。

Mac への sbt のインストール 

サードパーティからのパッケージを使ってのインストール 

注意: サードパーティからのパッケージは最新版を使っているとは限らない。 何か問題があれば、パッケージメンテナに連絡してほしい。

Macports 

$ port install sbt

Homebrew 

$ brew install sbt

ユニバーサルパッケージからのインストール 

ZIP か TGZ をダウンロードしてきて解凍する。

手動インストール 

手動インストールの手順を参照。

Windows への sbt のインストール 

Windows インストーラ 

msi インストーラをダウンロードしてインストールする。

ユニバーサルパッケージからのインストール 

ZIP か TGZ をダウンロードしてきて解凍する。

手動インストール 

手動インストールの手順を参照。

Linux への sbt のインストール 

ユニバーサルパッケージからのインストール 

ZIP か TGZ をダウンロードしてきて解凍する。

RPM and DEB 

以下のパッケージも公式にサポートしている:

注意: これらのパッケージに問題があれば、 sbt-launcher-package プロジェクトに報告してほしい。

Gentoo 

公式には sbt の ebuild は提供していないけども、バイナリから sbt をマージする ebuild があるみたいだ。この ebuild を使って sbt をマージするには:

$ mkdir -p /usr/local/portage && cd /usr/local/portage
$ git clone git://github.com/whiter4bbit/overlays.git
$ echo "PORTDIR_OVERLAY=$PORTDIR_OVERLAY /usr/local/portage/overlays" >> /etc/make.conf
$ emerge sbt-bin

注意: この ebuild に関する問題があれば ここ に報告してほしい。

手動インストール 

手動インストールの手順を参照。

手動インストール 

sbt-launch.jar をダウンロードして起動スクリプトを書くことで手動でインストールできる。

Unix 

sbt-launch.jar を ~/bin に置く。

~/bin/sbt に以下のスクリプトを作成して JAR を起動する:

SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"
java $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"

スクリプトを実行可能にする:

$ chmod u+x ~/bin/sbt

Windows 

ターミナルの種類と Cygwin を使っているかによって Windows 環境での手動インストールは変わってくる。 いずれにせよ、バッチファイルもしくはスクリプトにパスを通すことでコマンドプロンプトから sbt と打ち込めば sbt が起動できるようにする。 あとは、必要に応じて JVM セッティングを調整する。

非Cygwin 

標準の Windows ターミナルを使っている非Cygwin ユーザは、以下のバッチファイル sbt.bat を作る:

set SCRIPT_DIR=%~dp0
java -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -jar "%SCRIPT_DIR%sbt-launch.jar" %*

そしてダウンロードしてきた sbt-launch.jar はバッチファイルと同じディレクトリに置く。

標準 Windows ターミナルを使った Cygwin 

標準 Windows ターミナルとともに Cygwin を使っている場合は、~/bin/sbt という名前で bash スクリプトを作る:

SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"
java $SBT_OPTS -jar sbt-launch.jar "$@"

sbt-launch.jar の所はダウンロードしてきた sbt-launch.jar へのパスで置き換える。 必要ならば cygpath を使う。スクリプトを実行可能にする:

$ chmod u+x ~/bin/sbt

Ansi ターミナルを使った Cygwin 

Ansi ターミナル (Ansi エスケープをサポートして、stty によって設定できる) を使って Cygwin を実行している場合は、~/bin/sbt という名前で bash スクリプトを作る:

SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"
stty -icanon min 1 -echo > /dev/null 2>&1
java -Djline.terminal=jline.UnixTerminal -Dsbt.cygwin=true $SBT_OPTS -jar sbt-launch.jar "$@"
stty icanon echo > /dev/null 2>&1

sbt-launch.jar の所はダウンロードしてきた sbt-launch.jar へのパスで置き換える。 必要ならば cygpath を使う。スクリプトを実行可能にする:

$ chmod u+x ~/bin/sbt

scala コンソールでバックスペースが正しく動作するためには、バックスペースが stty で設定された消去文字を送信している必要がある。 デフォルトの Cygwin のターミナル (mintty) を使っていて、 消去文字が Cygwin のデフォルトである ^H を使っている場合は Options -> Keys “Backspace sends ^H” の設定をチェックする必要がある。

注意: 他の設定は現在サポートしていない。 何か良い方法があれば pull request を送ってほしい。

訳注:

Hello, World 

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

ソースコードの入ったプロジェクトディレクトリを作る 

一つのソースファイルを含むディレクトリでも、一応有効な sbt プロジェクトとなりうる。試しに、hello ディレクトリを作って、以下の内容の hw.scala というファイルを作成する:

object Hi {
  def main(args: Array[String]) = println("Hi!")
}

次に hello ディレクトリ内から sbt を起動して sbt のインタラクティブコンソールに run と打ち込む。 Linux か OS X を使っていならばコマンドは以下のようになる:

$ mkdir hello
$ cd hello
$ echo 'object Hi { def main(args: Array[String]) = println("Hi!") }' > hw.scala
$ sbt
...
> run
...
Hi!

この例では、sbt は純粋に convention(デフォルトの慣例)だけを使って動作している。 sbt は以下を自動的に検知する:

デフォルトでは、sbt は sbt 自身が使っている Scala のバージョンを使ってプロジェクトをビルドする。

sbt run を用いてプロジェクトを実行したり、sbt console を用いて Scala REPL に入ることができる。sbt console は君のプロジェクトにクラスパスを通すから、 君のプロジェクトのコードを使った Scala の例をライブで試すことができる。

ビルド定義 

ほとんどのプロジェクトは何らかの手動設定が必要だ。基本的なビルド設定は build.sbt というファイルに書かれ、 プロジェクトのベースディレクトリ (base directory) に置かれる。

例えば、君のプロジェクトが hello ディレクトリにあるなら、hello/build.sbt をこんな感じで書く:

name := "hello"

version := "1.0"

scalaVersion := "2.10.3"

.sbt ビルド定義で、build.sbt の書き方をもっと詳しく説明する。

君のプロジェクトを jar ファイルにパッケージ化する予定なら、最低でも build.sbt に name と version は書いておこう。

sbt バージョンの設定 

hello/project/build.properties というファイルを作ることで、特定のバージョンの sbt を強制することができる。 このファイルに、以下のように書く:

sbt.version=0.13.6

sbt はリリース間で 99% ソースの互換性を持たせてある。 だけど、sbt バージョンを project/build.properties に設定することで混乱を予防することできる。

ディレクトリ構造 

このページは、君が sbt をインストールして、 Hello, World を見たことを前提にする。

ベースディレクトリ 

sbt 用語では「ベースディレクトリ」(base directory) はプロジェクトが入ったディレクトリを指す。 Hello, World での例のように、hello/build.sbt と hello/hw.scala が入った hello プロジェクトを作ったとすると、ベースディレクトリは hello だ。

ソースコード 

ソースコードは hello/hw.scala のようにプロジェクトのベースディレクトリに置くこともできる。 だけど、ほとんどの人は、本物のプロジェクトではそうしない。ゴチャゴチャしすぎるからね。

sbt はデフォルトで Maven と同じディレクトリ構造を使う(全てのパスはベースディレクトリからの相対パスとする):

src/
  main/
    resources/
      <メインの jar に含むファイル>
    scala/
      <メインの Scala ソース>
    java/
      <メインの Java ソース>
  test/
    resources/
      <テストの jar に含むファイル>
    scala/
      <テストの Scala ソース>
    java/
      <テストの Java ソース>

src/ 内の他のディレクトリは無視される。あとは、隠れディレクトリも無視される。

sbt ビルド定義ファイル 

プロジェクトのベースディレクトリに build.sbt があるのはもう分かった。 他の sbt 関連のファイルは project サブディレクトリに置かれる。

project には .scala ファイルを含むことができ、それは .sbt ファイルと 組み合わさって一つのビルド定義を構成する。詳しくは、.scala ビルド定義を参照。

build.sbt
project/
  Build.scala

project 内に .sbt があるのを見ることがあるかもしれないけど、それはプロジェクトの ベースディレクトリ下の .sbt とは別物だ。これに関しても、他に前提となる知識が必要なので、 後で説明する。

ビルド成果物 

生成されたファイル(コンパイルされたクラス、パッケージ化された jar ファイル、マネージファイル、キャッシュ、とドキュメンテーション)は、デフォルトで target ディレクトリに置かれる。

バージョン管理の設定 

君の .gitignore (もしくは、他のバージョン管理システムの同様のファイル)は以下を含むべきだ:

target/

これは(ディレクトリだけにマッチさせるために)語尾の / はつけているけど、 (普通の target/ に加えて project/target/ にもマッチさせるために)先頭の / は意図して つけていないことに注意。

実行 

このページではプロジェクトをセットアップした後の sbt の使い方を説明する。 君がsbt をインストールして、Hello, Worldか他のプロジェクトを作ったことを前提にする。

インタラクティブモード 

プロジェクトのディレクトリで、sbt を引数なしで実行する:

$ sbt

sbt をコマンドライン引数なしで実行するとインタラクティブモードが開始する。 インタラクティブモードにはコマンドプロンプト(とタブ補完と履歴も!)がある。

例えば、compile と sbt プロンプトに打ち込む:

> compile

もう一度 compile するには、上矢印を押して、エンターを押す。

君のプログラムを実行するには、run と打ち込む。

インタラクティブモードを終了するには、exit と打ち込むか、Ctrl+D (Unix) か Ctrl+Z (Windows) を用いる。

バッチモード 

sbt アクションを空白で区切られたリストとして引数に渡すことで、sbt をバッチモードで実行することができる。 引数を取る sbt コマンドに関しては、コマンドと引数の両方を引用符で囲むことで一つの引数として sbt に渡す。 例えば、

$ sbt clean compile "testOnly TestA TestB"

この例では、testOnly は TestA と TestB の二つの引数を取る。 アクションは順に実行される(clean、compile、そして testOnly)。

継続的ビルドとテスト 

編集-コンパイル-テストのサイクルを速めるために、ソースファイルを保存するたびに sbt を使って自動的に再コンパイルすることができる。

ソースファイルが変更されたことを検知してアクションを実行するには、 アクションの先頭に ~ を書く。例えば、インタラクティブモードで、これを試してみよう:

> ~ compile

エンターを押すと、変更の監視を中止できる。

先頭の ~ はインタラクティブモードでもバッチモードでも使うことができる。

詳しくは、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) を使う。
run <argument>* sbt と同じ仮想マシン上で、プロジェクトのメインクラスを実行する。
package src/main/resources 内のファイルと src/main/scala と src/main/java からコンパイルされたクラスを含む jar を作る。
help <command> 指定されたコマンドの詳しい説明を表示する。コマンドが指定されていない場合は、 全てのコマンドの簡単な説明を表示する。
reload ビルド定義(build.sbt、 project/*.scala、 project/*.sbt ファイル)を再読み込みする。 ビルド定義を変更した場合に必要。

タブ補完 

インタラクティブモードには、空のプロンプトの状態を含め、タブ補完がある。 sbt の特殊な慣例として、タブを一度押すとよく使われる候補だけが表示され、 複数回押すと、より回りくどい候補が表示される。

履歴コマンド 

インタラクティブモードは、たとえ sbt を終了して再起動した後でも履歴を覚えている。 履歴にアクセスする最も簡単な方法は矢印キーを使うことだ。以下のコマンドも使うことができる:

! 履歴コマンドのヘルプを表示する。
!! 直前のコマンドを再実行する。
!: 全てのコマンド履歴を表示する。
!:n 最後の n コマンドを表示する。
!n !: で表示されたインデックス n のコマンドを実行する。
!-n n個前のコマンドを実行する。
!string 'string' から始まる最近のコマンドを実行する。
!?string 'string' を含む最近のコマンドを実行する。

.sbt ビルド定義 

このページでは、多少の「理論」も含めた sbt のビルド定義 (build definition) と build.sbt の構文を説明する。 君が、sbt の使い方を分かっていて、「始める sbt」の前のページも読んだことを前提とする。

.sbt vs. .scala 定義 

sbt のビルド定義はベースディレクトリ内の .sbt で終わるファイルと、 project サブディレクトリ内の .scala で終わるファイルを含むことができる。

どちらか一つだけを使うこともできるし、両方使うこともできる。 普通の用途には .sbt を使って、.scala を使うのは以下のような .sbt で出来ないことに限定する、というのが良い方法だ:

このページでは .sbt ファイルの説明をする。.scala ファイルの詳細と、それがどう .sbt に絡んでくるかに関しては、 (このガイドの後ほどの).scala ビルド定義を参照。

ビルド定義って何? 

ここは読んで下さい

プロジェクトを調べ、全てのビルド定義ファイルを処理した後、sbt は、ビルドを記述した不可変マップ(キーと値のペア)を最終的に作る。

例えば、name というキーがあり、それは文字列の値、つまり君のプロジェクト名に関連付けられる。

ビルド定義ファイルは直接には sbt のマップに影響を与えない。

その代わり、ビルド定義は、型が Setting[T] のオブジェクトを含んだ巨大なリストを作る。 T はマップ内の値の型だ。(Scala の Setting[T] は Java の Setting<T> と同様。) Setting は、新しいキーと値のペアや、既存の値への追加など、マップの変換を記述する。 (関数型プログラミングの精神に則り、変換は新しいマップを返し、古いマップは更新されない。)

build.sbt では、プロジェクト名の Setting[String] を以下のように作る:

name := "hello"

この Setting[String] は name キーを追加(もしくは置換)して "hello" という値に設定することでマップを変換する。 変換されたマップは新しい sbt のマップとなる。

マップを作るために、sbt はまず、同じキーへの変更が一緒に起き、かつ他のキーに依存する値の処理が依存するキーの後にくるように Setting のリストをソートする。 次に、sbt はソートされた Setting のリストを順番にみていって、一つづつマップに適用する。

まとめ: ビルド定義は Setting[T] のリストを定義し、Setting[T] は sbt のキー・値ペアへの変換を表し、T は値の型を指す。

build.sbt はどう設定値を定義するか 

以下に具体例で説明しよう:

name := "hello"

version := "1.0"

scalaVersion := "2.10.3"

build.sbt は、空行で分けられた Setting のリストだ。それぞれの Setting は Scala の式で表される。

build.sbt 内の式は、それぞれ独立しており、完全な Scala 文ではなく、式だ。 そのため、build.sbt 内ではトップレベルでの val、object、クラスやメソッドの定義は禁止されている。 (sbt0.13.0以降、val、lazy val、メソッド定義は可能になった。objectやclass、varの定義は引き続き不可能)

左辺値の name、version、および scalaVersion は キーだ。 キーは SettingKey[T]、TaskKey[T]、もしくは InputKey[T] のインスタンスで、 T はその値の型だ。キーの種類に関しては後で説明しよう。

キーには := メソッドがあり、Setting[T] を返す。 Java 的な構文でこのメソッドを呼び出すこともできる:

name.:=("hello")

だけど、Scala では name := "hello" と書ける(Scala では全てのメソッドがどちらの構文でも書ける)。

name キーの := メソッドは Setting を返すが、特に Setting[String] を返す。 String は、name の型にもあらわれ、これは、SettingKey[String] となっている。 この場合、返された Setting[String] は、キーを追加(もしくは置換)して "hello" という値に設定するマップの変換だ。

間違った型の値を使うと、ビルド定義はコンパイルしない:

name := 42  // コンパイルしない

キーは Keys オブジェクトで定義される 

組み込みのキーは [Keys] と呼ばれるオブジェクトのフィールドにすぎない。 build.sbt は、自動的に import sbt.Keys._ するため、 sbt.Keys.name は name として呼ぶことができる。

カスタムのキーは .scala ファイルか plugin で定義することができる。

設定を変換する他の方法 

:= による置換は、最も単純な変換だけど、他にもいくつかある。 例えば、+= を用いて、リスト値に追加することができる。

他の変換はスコープの理解が必要なため、 次のページがスコープで、 次の次のページで設定の詳細に関して説明する。

タスクキー 

キーには三種類ある:

TaskKey[T] は、タスクを定義しているといわれる。タスクは、compile や packae のような作業だ。 タスクは Unit を返すかもしれないし(Unit は、Scala での void だ)、 タスクに関連した値を返すかもしれない。例えば、package は作成した jar ファイルを値として返す TaskKey[File] だ。

例えばインタラクティブモードの sbt プロンプトに compile と打ち込むなどして、 タスクを実行するたびに、sbt は関連したタスクを一回だけ再実行する。

プロジェクトを記述した sbt のマップは、name のようなセッティング (setting) ならば、その文字列の値をキャッシュすることができるけど、 compile のようなタスク(task)の場合は実行可能コードを保存する必要がある (たとえその実行可能コードが最終的に同じ文字列を返したとしても、それは毎回再実行されなければいけない)。

あるキーがあるとき、それは常にタスクか素のセッティングかのどちらかを参照する。 つまり、キーの「タスク性」(毎回再実行するかどうか)はキーの特性であり、値にはよらない。

:= を使うことで、タスクに任意の演算を代入することができ、その演算は毎回再実行される:

hello := { println("Hello!") }

型システムの視点から考えると、タスクキー (task key) から作られた Setting は、セッティングキー (setting key) から作られたそれとは少し異なるものだ。 taskKey := 42 は Setting[Task[T]] の戻り値を返すが、settingKey := 42 は Setting[T] の戻り値を返す。 タスクが実行されるとタスクキーは型T の値を返すため、ほとんどの用途において、これによる影響は特にない。

T と Task[T] の型の違いによる影響が一つある。 それは、セッティングキーはキャッシュされていて、再実行されないため、タスキキーに依存できないということだ。 このことについては、後ほどの他の種類のセッティングにて詳しくみていく。

sbt インタラクティブモードにおけるキー 

sbt のインタラクティブモードからタスクの名前を打ち込むことで、どのタスクでも実行することができる。 それが compile と打ち込むことでコンパイルタスクが起動する仕組みだ。つまり、compile はタスクキーなのだ。

タスクキーのかわりにセッティングキーの名前を入力すると、セッティングキーの値が表示される。 タスクキーの名前を入力すると、タスクを実行するが、その戻り値は表示されないため、 タスクの戻り値を表示するには素の <タスク名> ではなく、show <タスク名> と入力する。

Scala の慣例にのっとり、ビルド定義ファイル内ではキーはキャメルケース(camelCase)で命名されているけども、 sbt コマンドラインではハイフン分けされて(hyphen-separated-words)命名されている。 sbt で使われているハイフン分けされた文字列はキーの定義とともに宣言されている([Keys] 参照)。 例えば、Keys.scala に以下のキーがある:

val scalacOptions = TaskKey[Seq[String]]("scalac-options", "Options for the Scala compiler.")

sbt では scalac-options と打ち込むけど、ビルド定義ファイルでは scalacOptions を使う。

あるキーについてより詳しい情報を得るためには、sbt インタラクティブモードで inspect <キー名> と打ち込む。 inspect が表示する情報の中にはまだ分からないこともあると思うけど、一番上にはセッティングの値の型と、セッテイングの簡単な説明がある。

build.sbt 内の import 文 

build.sbt の一番上に import 文を書くことができ、それらは空行でわけなくてもいい。

自動的に以下のものがデフォルトでインポートされる:

import sbt._
import Process._
import Keys._

(さらに、.scala ファイルがある場合は、それらの全ての Build と Plugin の内容もインポートされる。 これに関しては、.scala ビルド定義でさらに詳しく。)

ライブラリへの依存性を加える 

サードパーティのライブラリに依存するには二つの方法がある。 第一は lib/ に jar ファイルを入れてしまう方法で(アンマネージ依存性、unmanged dependency)、 第二はマネージ依存性(managed dependency)を加えることで、build.sbt ではこのようになる:

libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3"

これで Apache Derby ライブラリのバージョン 10.4.1.3 へのマネージ依存性を加えることができた。

libraryDependencies キーは二つの複雑な点がある: := ではなく += を使うことと、% メソッドだ。 後で他の種類のセッティングで説明するけど、+= はキーの古い値を上書きするかわりに新しい値を追加する。 % メソッドは文字列から Ivy モジュール ID を構築するのに使われ、これはライブラリ依存性で説明する。

ライブラリ依存性に関しては、このガイドの後ほどまで少しおいておくことにする。 後で、一ページ分をさいてちゃんと説明する。

スコープ 

このページではスコープの説明をする。君が、前のページの .sbt ビルド定義を読んで理解したことを前提とする。

キーに関する本当の話 

これまでは、あたかも name のようなキーは単一の sbt のマップのキー・値ペアの項目に対応するフリをして話を進めてきた。 それは単純化した話だ。

実のところは、全てのキーは、「スコープ」と呼ばれる文脈に関連付けられた値を複数もつことができる。

以下に具体例で説明する:

スコープによって値が異なる可能性があるため、あるキーへの単一の値は存在しない。

しかし、スコープ付きキーには単一の値が存在する。

これまで見てきたように、sbt が、プロジェクトを記述するキー・値のマップを生成するためにセッティングのリストを処理していくことを考えると、このキー・値マップ内のキーは、スコープ付きキーであることが分かる。 また、(build.sbt などの)ビルド定義内の、セッティングもスコープ付きキーに適用されるものだ。

スコープは、デフォルトがあったり、暗示されていたりするが、デフォルトが間違っていれば build.sbt にてスコープを指定しなければいけない。

スコープ軸 

スコープ軸(scope axis)は、型であり、そのインスタンスは独自のスコープを定義する (つまり、各インスタンスはキーの独自の値を持つことができる)。

スコープ軸は三つある:

プロジェクト軸によるスコープ付け 

一つのビルドに複数のプロジェクトを入れる場合、それぞれのプロジェクトにセッティングが必要だ。 つまり、キーはプロジェクトによりスコープ付けされる。

プロジェクト軸は「ビルド全体」に設定することもでき、その場合はセッティングは単一のプロジェクトではなくビルド全体に適用される。 ビルドレベルでのセッティングは、プロジェクトが特定のセッティングを定義しない場合のフォールバックとして使われることがよくある。

コンフィギュレーション軸によるスコープ付け 

コンフィギュレーション(configuration)は、ビルドの種類を定義し、独自のクラスパス、ソース、生成パッケージなどをもつことができる。 コンフィギュレーションの概念は、sbt が マネージ依存性 に使っている Ivy と、MavenScopes に由来する。

sbt で使われるコンフィギュレーションには以下のものがある:

デフォルトでは、コンパイル、パッケージ化、と実行に関するキーの全てはコンフィグレーションにスコープ付けされているため、 コンフィギュレーションごとに異なる動作をする可能性がある。 その最たる例が compile、package と run のタスクキーだが、 (source-directories や scalac-options や full-classpath など)それらのキーに影響を及ぼす全てのキーもコンフィグレーションにスコープ付けされている。

タスク軸によるスコープ付け 

セッティングはタスクの動作に影響を与えることもできる。例えば、pakcage-src は package-options セッティングの影響を受ける。

これをサポートするため、(package-src のような)タスクキーは、(package-option のような)別のキーのスコープとなりえる。

パッケージを構築するさまざまなタスク(package-src、package-bin、package-duc)は、artifact-name や package-option などのパッケージ関連のキーを共有することができる。これらのキーはそれぞれのパッケージタスクに対して独自の値を取ることができる。

グローバルスコープ 

それぞれのスコープ軸は、その軸の型のインスタンスを代入する(例えば、タスク軸にはタスクを代入する)か、 もしくは、Global という特殊な値を代入することができる。

Global は、予想通りのもので、その軸の全てのインスタンスに対して適用されるセッティングの値だ。 例えば、タスク軸が Global ならば、全てのタスクに適用される。

委譲 

スコープ付きキーは、そのスコープに関連付けられた値がなければ未定義であることもできる。

全てのスコープに対して、sbt には他のスコープからなるフォールバック検索パス(fallback search path)がある。 通常は、より特定のスコープに関連付けられた値が見つからなければ、 sbt は、Global や、ビルド全体スコープなど、より一般的なスコープから値を見つけ出そうとする。

この機能により、より一般的なスコープで一度値を代入することで、複数のより特定なスコープがその値を継承することを可能とする。

以下に、inspect を使ったキーのフォールバック検索パス、別名「委譲」(delegate)の探し方を説明する。

sbt 実行中のスコープ付きキーの参照方法 

コマンドラインとインタラクティブモードにおいて、sbt はスコープ付きキーを以下のように表示し(パースする):

{<ビルド-uri>}<プロジェクト-id>/コンフィギュレーション:タスクキー::キー

全ての軸において、* を使って Global スコープを表すことができる。

スコープ付きキーの一部を省略すると、以下の手順で推論される:

さらに詳しくは、Interacting with the Configuration System 参照。

スコープの検査 

sbt のインタラクティブモード内で inspect コマンドを使ってキーとそのスコープを理解することができる。 例えば、inspect test:full-classpath と試してみよう:

$ sbt
> inspect test:fullClasspath
[info] Task: scala.collection.Seq[sbt.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]  {file:/home/hp/checkout/hello/}default-aea33a/test:fullClasspath
[info] Dependencies:
[info]  test:exportedProducts
[info]  test:dependencyClasspath
[info] Reverse dependencies:
[info]  test:runMain
[info]  test:run
[info]  test:testLoader
[info]  test:console
[info] Delegates:
[info]  test:fullClasspath
[info]  runtime:fullClasspath
[info]  compile:fullClasspath
[info]  *:fullClasspath
[info]  {.}/test:fullClasspath
[info]  {.}/runtime:fullClasspath
[info]  {.}/compile:fullClasspath
[info]  {.}/*:fullClasspath
[info]  */test:fullClasspath
[info]  */runtime:fullClasspath
[info]  */compile:fullClasspath
[info]  */*:fullClasspath
[info] Related:
[info]  compile:fullClasspath
[info]  compile:fullClasspath(for doc)
[info]  test:fullClasspath(for doc)
[info]  runtime:fullClasspath

一行目からこれが(.sbt ビルド定義で説明されているとおり、セッティングではなく)タスクであることが分かる。 このタスクの戻り値は scala.collection.Seq[sbt.Attributed[java.io.File]] の型をとる。

“Provided by” は、この値を定義するスコープ付きキーを指し、この場合は、 {file:/home/hp/checkout/hello/}default-aea33a/test:full-classpath (test コンフィギュレーションと {file:/home/hp/checkout/hello/}default-aea33a プロジェクトにスコープ付けされた full-classpath キー)。

“Dependencies” は、まだ意味不明だろうけど、次のページまで待ってて。

ここで委譲も見ることができ、もし値が定義されていなければ、sbt は以下を検索する:

今度は、(inspect test:full-class のかわりに)inspect full-classpath を試してみて、違いをみてみよう。 コンフィグレーションが省略されたため、compile だと自動検知される。 そのため、inspect compile:full-classpath は inspect full-classpath と同じになるはずだ。

次に、inspect *:full-classpath も実行して違いを比べてみよう。 full-classpath はデフォルトでは、Global コンフィギュレーションには定義されていない。

より詳しくは、Interacting with the Configuration System 参照。

ビルド定義からスコープを参照する 

build.sbt で裸のキーを使ってセッティングを作った場合は、現プロジェクト、Global コンフィグレーション、Global タスクにスコープ付けされる:

name := "hello"

sbt を実行して、inspect name と入力して、キーが {file:/home/hp/checkout/hello/}default-aea33a/*:name により提供されていることを確認しよう。つまり、プロジェクトは、{file:/home/hp/checkout/hello/}default-aea33a で、コンフィギュレーションは * で、タスクは表示されていない(グローバルを指す)ということだ。

build.sbt は常に単一のプロジェクトのセッティングを定義するため、「現プロジェクト」は今 build.sbt で定義しているプロジェクトを指す。 (マルチプロジェクト・ビルドの場合は、プロジェクトごとに build.sbt がある。)

キーにはオーバーロードされた in メソッドがあり、それによりスコープを設定できる。 in への引数として、どのスコープ軸のインスタンスでも渡すことができる。 これをやる意味は全くないけど、例として Compile コンフィギュレーションでスコープ付けされた name の設定を以下に示す:

name in Compile := "hello"

また、package-bin タスクでスコープ付けされた name の設定(これも意味なし!ただの例だよ):

name in packageBin := "hello"

もしくは、例えば Compile コンフィギュレーションの packageBin の name など、複数のスコープ軸でスコープ付けする:

name in (Compile, packageBin) := "hello"

もしくは、全ての軸に対して Global を使う:

name in Global := "hello"

(name in Global は、スコープ軸である Global を全ての軸を Global に設定したスコープに暗黙の変換が行われる。 タスクとコンフィギュレーションは既にデフォルトで Global であるため、事実上行なっているのはプロジェクトを Global に指定することだ。つまり、{file:/home/hp/checkout/hello/}default-aea33a/*:name ではなく、*/*:name が定義される。)

Scala に慣れていない場合に注意して欲しいのは、in や := はただのメソッドであって、魔法ではないということだ。 Scala ではキレイに書くことができるけど、Java 風に以下のようにも書き下すこともできる:

name.in(Compile).:=("hello")

こんな醜い構文で書く必要は一切無いけど、これらが実際にメソッドであることを示している。

いつスコープを指定するべきか 

あるキーが、通常スコープ付けされている場合は、スコープを指定してそのキーを使う必要がある。 例えば、compile タスクは、デフォルトで Compile と Test コンフィギュレーションにスコープ付けされているけど、 これらのスコープ外には存在しない。

そのため、compile キーに関連付けられた値を変更するには、compile in Compile か compile in Test のどちらかを書く必要がある。 素の compile を使うと、コンフィグレーションにスコープ付けされた標準のコンパイルタスクをオーバーライドするかわりに、現在のプロジェクトにスコープ付けされた新しいコンパイルタスクを定義してしまう。

“Reference to undefined setting“ のようなエラーに遭遇した場合は、スコープを指定していないか、間違ったスコープを指定したことによることが多い。 君が使っているキーは何か別のスコープの中で定義されている可能性がある。 エラーメッセージの一部として sbt は、君が意味したであろうものを推測してくれるから、“Did you mean compile:compile?” を探そう。

キーの名前はキーの一部であると考えることもできる。 実際の所は、全てのキーは名前と(三つの軸を持つ)スコープによって構成される。 つまり、packageOptions in (Compile, packageBin) という式全体でキー名だということだ。 単に packageOptions と言っただけでもキー名だけど、それは別のキーだ (in 無しのキーのスコープは暗黙で決定され、現プロジェクト、Global コンフィグレーション、Global タスクとなる)。

他の種類のセッティング 

このページでは、基本的な := メソッドを超えた、より高度な Settings の作り方を説明する。 君が、.sbt ビルド定義とスコープを読んだことを前提とする。

復習: セッティング 

ビルド定義は Setting のリストを作り、それが sbt の(キーと値のペアのマップで表現される)ビルドの記述を変換するのに使われるということは覚えていると思う。 Setting は、は古いマップをインプットにとり、新たなマップをアウトプットに出す変換だ。新たなマップが sbt の新しい内部状態となる。

セッティングは種類により異なる方法でマップを変換する。これまでは、:= メソッドをみてきた。

:= が作る Setting は、不変の固定値を新たに変換されたマップに代入する。 例えば、マップを name := "hello" というセッティングで変換すると、新しいマップは name キーの中に "hello" を格納する。

セッティングがその効果を発揮するにはセッティングのマスターリストに入らなくてはいけない (build.sbt の全ての行は自動的にそのリストに入るけど、 .scala ファイルの場合は、sbt が検知しない場所に Setting を作ってしまうことができる)。

既存の値に追加する: += と ++= 

:= による置換が最も単純な変換だけど、キーには他のメソッドもある。 SettingKey[T] の T が列の場合、つまりキーの値の型が列の場合は、置換のかわりに列に追加することができる。

例えば、sourceDirectories in Compile というキーの値の型は Seq[File] だ。 デフォルトで、このキーの値は src/main/scala を含む。 (君がどうしても非標準じゃないと気が済まないので)source という名前のディレクトリに入ったソースもコンパイルしたいとすると、 以下のようにして設定できる:

sourceDirectories in Compile += new File("source")

もしくは、sbt パッケージに入っている file() 関数を使って:

sourceDirectories in Compile += file("source")

(file() は、単に新しい File 作る)

++= を使って複数のディレクトリを一度に加える事もできる:

sourceDirectories in Compile ++= Seq(file("sources1"), file("sources2"))

このでの Seq(a, b, c, ...) は、列を構築する標準的な Scala の構文だ。

デフォルトのソースディレクトリを完全に置き換えてしまいたい場合は、当然 := を使えばいい:

sourceDirectories in Compile := Seq(file("sources1"), file("sources2"))

値の変換: ~= 

sourceDirectories in Compile の先頭に要素を追加したり、デフォルトのディレクトリを filter したい場合はどうすればいいだろう?

既存の値に依存する Setting を作ることができる。

sourceDirectories in Compile を変更するには、以下のように ~= を用いる:

// src/main/scala を filter out する
sourceDirectories in Compile ~= { srcDirs => srcDirs filter(!_.getAbsolutePath.endsWith("src/main/scala")) }

ここでは、srcDirs は匿名関数のパラメータで、sourceDirectories in Compile の古い値が匿名関数に渡される。 この関数の戻り値が sourceDirectories in Compile の新たな値となる。

もっと簡単な例だと:

// プロジェクト名を大文字にする
name ~= { _.toUpperCase }

関数は、キーの値を同じ型の別の値に変換するため、キーの型が SettingKey[T] か TaskKey[T] のとき、~= に渡す関数は常に T => T 型でなければいけない。

別のキーの値から値を計算する: <<= 

~= は、キーの古い値に基づいて新たな値を定義する。 だけど、他のキーの値に基づいて値を定義したいとしたらどうだろう?

<<= は、型 Initialize[T] の引数を一つ取る。 Initialize[T] のインスタンスは、あるキーの集合に関連付けられた値をインプットに取り、その値に基づいて型Tの値を返す。 つまり、型T の値を初期化する。

(:=、+=、~= その他同様)Initialize[T] を受け取った <<= は、Setting[T] を返す。

単純な Initialize[T]: 単一のキーに依存した <<= 

全てのキーは Initilize trait を拡張するため、最も単純な Initialize は、ただのキーだ:

// 意味はないけど、妥当だ
name <<= name

Initialize[T] として取り扱った場合、SettingKey[T] はその現在値を計算する。 そのため、name <<= name は name の値を name の値に代入する。

別のキーをキーに代入することで、少しは役に立つようになる。キーは同じ値の型を持たなくてはいけない。

// プロジェクト名を使って組織名を命名する(両者とも SettingKey[String])
organization <<= name

(注意: これが別のキーへのエイリアスの作り方だ)

値の型が同一じゃない場合は、Initialize[T] から例えば Initialize[S] みたいな感じで別の型にしてやる必要がある。 これには、以下のように Initialize の apply メソッドを使う:

// name は Key[String] で、baseDirectory は Key[File] だ。
// プロジェクトの現在ディレクトリに基づいて名前を付ける。
name <<= baseDirectory.apply(_.getName)

apply は Scala の特殊なメソッドで、関数の呼び出し構文を使ってオブジェクトを叩くことができるため、このように書ける:

name <<= baseDirectory(_.getName)

これは、baseDirectory の値を、File を受け取り String を返す _.getName という関数を使って変換する。 getName は、普通の java.io.File オブジェクトにあるメソッドだ。

依存性を持ったセッティング 

name <<= baseDirectory(_.getName) というセッティングにおいて、name は、baseDirectory に依存性(dependency)を持つ。上記を build.sbt に書いて、sbt のインタラクティブモードで走らせ、inspect name と打ち込むと、以下のように表示される(一部抜粋):

[info] Dependencies:
[info] 	*:base-directory

このようにして、sbt はどのセッティングが別のセッティングに依存するかを知っている。 タスクを記述するセッティングもあるため、この方法でタスク間の依存性も作ることができる。

例えば、inspect compile すると、compile-inputs に依存することが分かり、 compile-input を inspect すると、それがまた別のキーに依存していることが分かる。 依存性の連鎖をたどっていくと、魔法に出会う。 例えば、compile と打ち込むと、sbt は自動的に update を実行する。 これが「とにかくちゃんと動く」理由は、compile の計算にインプットとして必要な値が、update の計算を先に行うことを強制しているからだ。

このようにして、sbt の全てのビルドの依存性は、明示的には宣言されず、自動化されている。 あるキーの値を別の計算で使うと、その計算はキーに依存する。とにかくちゃんと動く!

複雑な Initialize[T]: 複数のキーへ依存する <<= 

複数のキーへの依存性をサポートするために、sbt は、Initialize オブジェクトのタプルに apply メソッドと identity メソッドを追加する。 Scala では、タプルを (1, "a") のように書く(これは、(Int, String) の型を持つ)。

例えば、ここに三つの Initialize オブジェクトから成るタプルがあるとするとき、 その型は (Initialize[A], Initialize[B], Initialize[C]) だ。 全ての SettingKey[T] は、Initialize[T] のインスタンスでもあるため、この Initialize オブジェクトはキーでもよい。

以下に、全てのキーが文字列の場合の単純な例を示す:

// 三つの SettingKey[String] のタプル。三つの Initialize[String] のタプルでもある。
(name, organization, version)

Initialize のタプルの apply メソッドは、関数を引数として受け取る。 タプル中のそれぞれの Initialize を使って、sbt は対応する値を計算する(キーの現在値)。 これらの値は関数に渡され、その関数は単一の値を返し、これは新たな Initialize でラッピングされる。 明示的な型を書き下すと(Scala はこれを強要しない)、こんな感じ:

val tuple: (Initialize[String], Initialize[String], Initialize[String]) = (name, organization, version)
val combined: Initialize[String] = tuple.apply({ (n, o, v) =>
  "project " + n + " from " + o + " version " + v })
val setting: Setting[String] = name <<= combined

それぞれのキーは既に Initialize 型だけど、(キーのような)単純な Initialize をタプルに入れて、appy メソッドを呼び出すことで最大九つまで一つの Initialize として合成できる。

SettingKey[T] の <<= メソッドは、Initialize[T] を受け取るため、このテクニックを使って複数の任意のキーへの依存性を作ることができる。

Scala では関数の呼び出し構文が apply メソッドを呼び出すため、明示的な .apply を省いて、tupple を関数として扱い、以下のように書きなおすことができる:

val tuple: (Initialize[String], Initialize[String], Initialize[String]) = (name, organization, version)
val combined: Initialize[String] = tuple({ (n, o, v) =>
  "project " + n + " from " + o + " version " + v })
val setting: Setting[String] = name <<= combined

.sbt ファイルに書くことが許されているのは単一の式だけであり、複数の文は書けないため、 build.sbt では、このような val を中間値として使ったコードは動作しない。

そこで、build.sbt では、以下のような、より簡略化した構文が用いられる:

name <<= (name, organization, version) { (n, o, v) => "project " + n + " from " + o + " version " + v }

ここでは、Initialize のタプル(SettingKey のタプルでもある)が関数のようにはたらき、 {} で囲まれた匿名関数を受け取り、Initialize[T](T は匿名関数の戻り値の型)を返している。

Initialize のタプルは、identity というメソッドも持ち、これは単にタプル値を Initialize に入れて返す。 (a: Initialize[A], b: Initialize[B]).identity は、Initialize[(A, B)] 型の値を返す。 identity は、二つの Initialize を値を変更したり失うこと無く一つに合成する。

セッティングが未定義の場合 

セッティングが ~= や <<= を使って自分自身や他のキーへの依存性を作る場合、 依存されたキーには値が存在しなくてはならない。 存在しなければ、sbt に怒られる。 例えば、“Reference to undefined setting“ なんて言われるかもしれない。 これが起こった場合は、キーが定義されている正しいスコープで使っているか確認しよう。

環状の依存性を作ってしまうことも可能で、これもまたエラーになり、sbt に怒られる。

依存性を持ったタスク 

.sbt ビルド定義でみた通り、タスクキーは :=、<<=、その他でセッティングを作ると Setting[T] ではなく、Setting[Task[T]]を作る。 同様に、タスクキーは Initialize[T] ではなく、Initialize[Task[T]] のインスタンスで、 タスクキーの <<= は Initialize[Task[T]] をパラメータとして受け取る。

この実践上の大切さは、非タスクのセッティングはタスクを依存性としてもつことができないということだ。

([Keys] より)以下の二つのキーを例に説明する:

val scalacOptions = TaskKey[Seq[String]]("scalac-options", "Options for the Scala compiler.")
val checksums = SettingKey[Seq[String]]("checksums", "The list of checksums to generate and to verify for dependencies.")

(scalacOptions と checksums は、同じ値の型を持つ二つのキーで、片方がタスクというだけで、お互い全く関係のないキーだ。)

どちらか一方をもう片方のエイリアスにしようとしても、build.sbt をコンパイルすることができない:

scalacOptions <<= checksums

checksums <<= scalacOptions

問題は、scalacOptions.<<= は、Initialize[Task[Seq[String]]] を受け取り、 checksums.<<= は、Initialize[Seq[String]] を受け取るということだ。 だけど、Initialize[T] から Initialize[Task[T]] に変換する方法があり、map と呼ばれる。

(identity は標準の Scala 関数で、与えられたインプットを返す)

これを逆方向、つまりセッティングキーをタスクキーに依存されることは不可能だ。 これは、セッティングキーがプロジェクトの読み込み時に一度だけ計算されるため、 タスクが再実行されなくなってしまうが、 タスクは毎回再実行されることが期待されているからだ。

タスクはセッティングと他のタスクとの両方に依存することができる。 apply のかわりに map を使うことで、Initialize[T] のかわりに Initialize[Task[T]] を作る。 非タスクセッティングでの apply の用法は以下のようだ:

name <<= (name, organization, version) { (n, o, v) => "project " + n + " from " + o + " version " + v }

((name, organization, version) には apply メソッドがあるため、これは中括弧 {} で囲まれた匿名関数をパラメータとして受け取る関数だ。)

Initialize[Task[T]] を作るには、apply のかわりに map を使う:

// (<<= の左辺値の)name はタスクではなく、かつ map を使っているため、でコンパイルが通らない
name <<= (name, organization, version) map { (n, o, v) => "project " + n + " from " + o + " version " + v }

// packageBin はタスクであり、かつ map を使っているため、コンパイルは通る
packageBin in Compile <<= (name, organization, version) map { (n, o, v) => file(o + "-" + n + "-" + v + ".jar") }

// name がタスクではなく、かつ apply を使っているため、コンパイルは通る
name <<= (name, organization, version) { (n, o, v) => "project " + n + " from " + o + " version " + v }

// packageBin はタスクであり、かつ apply を使っているため、コンパイルは通らない
packageBin in Compile <<= (name, organization, version) { (n, o, v) => file(o + "-" + n + "-" + v + ".jar") }

まとめると: キーのタプルを Initialize[Task[T]] に変換したいときは map を使う。 キーのタプルを Initialize[T] に変換したいときは apply を使う。 <<= の左辺のキーが SettingKey[T] ではなくて、TaskKey[T] であるときに、Initialize[Task[T]] が必要となる。

エイリアスに必要なのは := ではなく、<<= 

あるキーが別のキーのエイリアスになって欲しいとき、つい := を使って以下のような間違ったエイリアスを作ってしまうかもしれない:

// 動作しないし、役に立たない
packageBin in Compile := packageDoc in Compile

問題は := の引数は値(タスクの場合は値を返す関数)でなくちゃいけないことだ。 TaskKey[File] である packageBin の場合は、File もしくは => File 関数でなければいけない。 packageDoc は、キーであり、File ではない。

正しい方法は、キーを受け取る <<= を使うことだ(実際に受け取っているのは Initialize だけど、全てのキーは Initialize のインスタンスでもある):

// 動作するけど、やっぱり役に立たない
packageBin in Compile <<= packageDoc in Compile

ここで、<<= は、後で(sbt がタスクを実行したとき)ファイルを返す計算である Initialize[Task[File]] を期待する。 これが思った通りの振る舞いだ。つまり、タスクのエイリアスを作ったときに期待されるのは、そのをタスクを実行することであって、 sbt がプロジェクトを読み込んだ時に一回だけ値を読み込むことではない。

(ちなみに、packageBin のようなパッケージ化タスクは、グローバルではなく、コンフィギュレーションごとに定義されているので、in Compile スコープが無ければ「未定義」エラーが発生する。)

依存性を持った追加: <+= and <++= 

リストに追加するためのメソッドにはもう二つあり、それらは += や ++= を <<= と組み合わせたものだ。 つまり、他のキーへの依存性を使いながらリストの新しい値や連結するための別のリストを計算できる。

これらのメソッドは、依存性から得られた値を変換するのに書く関数が T のかわりに Seq[T] を返さなくてはいけないこと以外の点では <<= と全く同じように動作する。

当然、<<= のように既存の値を置き換えるのではなく、<+= と <++= は既存の値に追加するわけだけど。

例えば、プロジェクト名を使って名付けたカバレッジレポートがあるとして、それを clean が削除するファイルのリストに追加したいとする:

cleanFiles <+= (name) { n => file("coverage-report-" + n + ".txt") }

ライブラリ依存性 

このページは、このガイドのこれまでのページ、特に .sbt ビルド定義、スコープ、と 他の種類のセッティング を読んでいることを前提とする。

ライブラリ依存性は二つの方法で加えることができる:

アンマネージ依存性 

ほとんどの人は、アンマネージ依存性ではなく、マネージ依存性を使っている。だけど、始めはアンマネージの方が簡単なので分かりやすい。

アンマネージ依存性を説明すると、こんな感じになる。jar ファイルを lib に入れると、それはプロジェクトのクラスパスに追加される。 以上!

例えば、ScalaCheck、specs、ScalaTest などのテスト用の jar を lib に加えることもできる。

lib の依存性は(compile、test、run、そして console の)全てのクラスパスに追加される。 もし、どれか一つのクラスパスを変えたい場合は、例えば dependencyClasspath in Compile や dependencyClasspath in Runtime などを適宜調整する必要がある。 ~= を使って既存のクラスパスの値を受け取り、いらないものを filter で外して、新しいクラスパスの値を返せばいい。 ~= の詳細に関しては、他の種類のセッティング参照。

アンマネージ依存性を利用するのに、build.sbt には何も書かなくてもいいけど、 デフォルトの lib 以外のディレクトリを使いたい場合は、 unmanaged-base キーを変更することができる。

lib のかわりに、custom_lib を使うには:

unmanagedBase <<= baseDirectory { base => base / "custom_lib" }

baseDirectory はプロジェクトのルートディレクトリで、 他の種類のセッティングで説明したとおり、ここでは unmanagedBase を <<= を使って baseDirectory の値に基づいて変更している。

他には、unmanged-jars という unmanaged-base ディレクトリに入っている jar ファイルのリストを返すタスクがある。 複数のディレクトリを使うとか、何か別の複雑なことを行う場合は、この unmanaged-jar タスクを何か別のものに変える必要があるかもしれない。

マネージ依存性 

sbt は、[Apache Ivy] を使ってマネージ依存性を実装するため、既に Maven か Ivy に慣れていれば、違和感無く入り込めるだろう。

libraryDependencies キー 

依存性を libraryDependencies セッティングに列挙するだけで、普通はうまくいく。 Maven POM ファイルや、Ivy コンフィギュレーションファイルを書くなどして、依存性を外部で設定してしまって、 sbt にその外部コンフィギュレーションファイルを使わせるということも可能だ。 これに関しては、[Library Management] を参照。

依存性の宣言は、以下のようになる。ここで、groupId、artifactId、と revision は文字列だ:

libraryDependencies += groupID % artifactID % revision

もしくは、以下のようになる。このときの configuration も文字列だ。

libraryDependencies += groupID % artifactID % revision % configuration

libraryDependencies は [Keys] で以下のように定義されている:

val libraryDependencies = SettingKey[Seq[ModuleID]]("library-dependencies", "Declares managed dependencies.")

% メソッドは、文字列から ModuleID オブジェクトを作り、その ModuleID を libraryDependencies に追加するだけでいい。

当然、sbt は(Ivy を通じて)モジュールをどこからダウンロードしてくるかを知らなければいけない。 もしモジュールが sbt に最初から入っているデフォルトのリポジトリの一つにあれば、ちゃんと動く。 例えば、Apache Derby はデフォルトのリポジトリにある:

libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3"

これを build.sbt に打ち込んで、update を実行すると、sbt は Derby を ~/.ivy2/cache/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-tools" % "scala-stm_2.9.1" % "0.3"

ビルドのバージョンが scalaVersion が 2.9.1 だとすると、以下は等価だ:

libraryDependencies += "org.scala-tools" %% "scala-stm" % "0.3"

多くの依存ライブラリが複数の Scala バージョンに対してコンパイルされていて、 プロジェクトに合ったものを選択したいときに使うというのが考えだ。

実践上での問題として、多くの場合依存ライブラリは少しズレた Scala バージョンが使われることがあるけど、 %% はそこまは賢くない。そのため、依存ライブラリが 2.9.0 までしか出てなくて、 プロジェクトが scalaVersion := "2.9.1" の場合、2.9.0 の依存ライブラリが多分動作するにも関わらず %% を使うことができない。 もし、%% が動かなくなったら、依存ライブラリが使っている実際のバージョンを確認して、 動くだろうバージョン(それががあればの話だけど)に決め打ちすればいい。

詳しくは、[Cross Build] を参照。

Ivy revision 

groupID % artifactID % revision の revision は、単一の固定されたバージョン番号じゃなくてもいい。 Ivy は与えられた制限の中でモジュールの最新の revision を選ぶことができる。 "1.6.1" のような固定 revision ではなく、"latest.integration"、"2.9.+"、や "[1.0,)" など指定できる。 詳しくは、[Ivy revisions] を参照。

Resolvers 

全てのパッケージが一つのサーバに置いてあるとは限らない。 sbt は、デフォルトで standard Maven2 repository のリポジトリを使う。 もし依存ライブラリがこのデフォルトのリポジトリに無ければ、Ivy がそれを見つけられるように resolver を追加する必要がある。

リポジトリを追加するには、以下のようにする:

resolvers += name at location

例えば:

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 のオーバーライド 

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"

sbt のインタラクティブモードで show compile:dependency-classpath と打ち込んでも、Derby は出てこないはずだ。 だけど、show test:dependency-classpath と打ち込むと、Derby の jar がリストにあるのが確認できる。

普通は、ScalaCheck、specs、ScalaTest などのテスト関連の依存ライブラリは % "test" と共に定義される。

ライブラリの依存性に関しては、もうこの入門用のページで見つからない情報があれば、このページに もう少し詳細やコツが書いてある。

マルチプロジェクト・ビルド 

このページでは、一つのプロジェクトで複数のプロジェクトを管理する方法を紹介する。

このガイドの前のページ、特に build.sbt を理解していることが必要になる。

複数のプロジェクト 

単一のビルドに複数のプロジェクトを入れておくと、 プロジェクト間に依存性がある場合や、 プロジェクトが同時に変更されることが多い場合などで便利だ。

ビルド内のサブプロジェクトは、それぞれに独自の src/main/scala を持ち、 package を実行すると独自の jar ファイルを生成するなど、 普通のプロジェクト同様に振る舞う。

.scala ファイル内でのプロジェクトの定義 

複数のプロジェクトを持つには、全てのプロジェクトとその関係を .scala ファイルで宣言する必要があり、 .sbt ファイルからは不可能だ。 だけど、それぞれのプロジェクトのセッティングは .sbt ファイルからでも定義することができる。 以下に、ルートプロジェクト hello が、二つのサブプロジェクト hello-foo と hello-bar を 集約(aggregate)する .scala ビルド定義を例に説明する:

import sbt._
import Keys._

object HelloBuild extends Build {
    lazy val root = Project(id = "hello",
                            base = file(".")) aggregate(foo, bar)

    lazy val foo = Project(id = "hello-foo",
                           base = file("foo"))

    lazy val bar = Project(id = "hello-bar",
                           base = file("bar"))
}

sbt は、リフレクションを用いて Build オブジェクト内の Project 型を持ったフィールドを検索することで、Project オブジェクトの全リストを作成する。

プロジェクト hello-foo は、base = file("foo") と共に定義されているため、 サブディレクトリ foo に置かれる。 そのソースは、foo/Foo.scala のように foo の直下に置かれるか、 foo/src/main/scala 内に置かれる。 ビルド定義ファイルを除いては、通常の sbt ディレクトリ構造が foo 以下に適用される。

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 とも同じビルド定義の一部だ。

.scala ファイルは、上に示したように、単にプロジェクトとそのベースディレクトリを列挙するだけの簡単なものにして、 それぞれのプロジェクトのセッティングは、そのプロジェクトのベースディレクトリ直下の .sbt ファイル内で宣言することができる。 全てのセッティングを .scala ファイル内で宣言することは義務付けられいるわけではない。

ビルド定義の全てを単一の project ディレクトリ内の場所にまとめるために、 .scala ファイル内にセッティングも含めてしまうほうが洗練されていると思うかもしれない。 ただし、これは好みの問題だから、好きにやっていい。

サブプロジェクトは、project サブディレクトリや、project/*.scala ファイルを持つことができない。 foo/project/Build.scala は無視される。

集約 

もし望むなら、ビルド内のプロジェクトは、お互いに対して完全に独立であることができる。

だけど、上の例では、aggregate(foo, bar) というメソッドが呼び出されていることが分かる。 これは、hello-foo と hello-bar を、ルートプロジェクト下に集約する。

集約とは、集約プロジェクトで実行されたタスクが部分プロジェクトでも実行されることを意味する。 例のような、二つのサブプロジェクトがある状態で sbt を起動して、compile を実行してみよう。 三つのプロジェクト全てがコンパイルされたことが分かると思う。

集約プロジェクト内で(この場合は、ルートの hello プロジェクトで)、 タスクごとに集約をコントロールすることができる。 例えば hello/build.sbt 内で、update タスクの集約を以下のようにして回避できる:

aggregate in update := false

aggregate in update は、update タスクにスコープ付けされた aggregate キーだ (スコープ参照)。

注意: 集約は、集約されるタスクを順不同に並列実行する。

クラスパス依存性 

プロジェクトは、他のプロジェクトのコードに依存することができる。 これは、dependsOn メソッドを呼び出すことで実現する。 例えば、hello-foo が hello-bar のクラスパスが必要な場合は、 Build.scala 内に以下のように書く:

    lazy val foo = Project(id = "hello-foo",
                           base = file("foo")) dependsOn(bar)

これで hello-foo 内のコードから hello-bar のクラスを利用することができる。 これは、プロジェクトをコンパイルするときの順序も作り出す。 この場合、hello-foo がコンパイルされる前に、hello-bar が更新(update)され、 コンパイルされる必要がある。

複数のプロジェクトに依存するには、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 インタラクティブプロンプトから、projects と打ち込むことでプロジェクトの全リストが表示され、 project <プロジェクト名> で、現在プロジェクトを選択できる。 compile のようなタスクを実行すると、それは現在プロジェクトに対して実行される。 これにより、ルートプロジェクトをコンパイルせずに、サブプロジェクトのみをコンパイルすることができる。

プラグインの使用 

このガイドの前のページをまず読んでほしい。 特に build.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 += Resolver.sonatypeRepo("public")

通常、プラグインは、プロジェクトに追加されるセッティングを提供することで機能を追加する。 これを次に説明しよう。

auto plugin の有効化と無効化 

プラグインはビルド定義に自動的に追加されるセッティングを宣言することができ、 その場合は何もしなくてもいい。

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つのプラグインによって提供される:

  1. CorePlugin: タスクの並列実行などのコア機能。
  2. IvyPlugin: モジュールの公開や依存性の解決機能。
  3. 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 : _*)

グローバル・プラグイン 

プラグインを ~/.sbt/0.13/plugins/ 以下で宣言することで全てのプロジェクトに対して一括してプラグインをインストールすることができる。 ~/.sbt/0.13/plugins/ は、sbt プロジェクトで、そのクラスパスは全ての sbt ビルド定義にエクスポートされる。 大まかに言うと、~/.sbt/0.13/plugins/ 内の .sbt ファイルは、それが全てのプロジェクトの project/ ディレクトリに入っているかのように振る舞う。

~/.sbt/0.13/plugins//build.sbt を作って、そこに addSbtPlugin() 式を書くことで 全プロジェクトにプラグインを追加することができる。 しかし、これを多用するとマシン環境への依存性を増やしてしまうことになるので、この機能は注意してほどほどに使うべきだ。 ベスト・プラクティスも参照してほしい。

利用可能なプラグイン 

プラグインのリストがある。

特に人気のプラグインは:

プラグインの作成などプラグインに関する詳細は Plugins とプラグインのベスト・プラクティス を参照。

カスタムセッティングとタスク 

このページでは、独自のセッティングやタスクの作成を紹介する。

このページを理解するには、このガイドの前のページ、 特に build.sbt と 他の種類のセッティング を読んである必要がある。

キーの定義 

[Keys] は、キーの定義の方法で満載だ。 多くのキーは、[Defaults] で実装されている。

キーは三つの型のうちどれかを持つ。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 のインタラクティブモードかバッチモードでコマンドが打ち込まれるたび)に再計算される。

(.scala ビルド定義 でみたように、).scala ファイル内、 もしくは(プラグインの使用 でみたように、)プラグイン内でキーを定義することができる。 .scala ビルド定義ファイル内の Build オブジェクト内の val、 もしくはプラグイン内の Plugin オブジェクト内の val は全て .sbt ファイルに自動的にインポートされる。

タスクの実装 

キーを定義したら、次はなんらかのタスクからそのキーを使ってみよう。 独自のタスクを定義してもいいし、既存のタスクを再定義する予定なのかもしれない。 いずれにせよ、やることは同じだ。 もしタスクに他のセッティングやタスクへの依存性が無ければ、:= を使ってタスクのキーになんらかのコードを関連付ける:

val sampleStringTask = taskKey[String]("A sample string task.")

val sampleIntTask = taskKey[Int]("A sample int task.")

sampleStringTask := System.getProperty("user.home")

sampleIntTask := {
  val sum = 1 + 2
  println("sum: " + sum)
  sum
}

もしタスクに依存性があれば、他の種類のセッティングで説明したとおり value を使って値を参照する。

タスクは、ただの Scala のコードであるため、実装の一番難しい部分は、多くの場合 sbt 特定の問題ではない。 難しいのは、実行したい何らかの「中身」の部分を書くことで、 例えば HTML を整形したいとすると、 今度は HTML のライブラリを利用する必要があるかもしれない (その場合は、ビルド定義にライブラリ依存性を追加して、その HTML ライブラリに基づいたコードを書く)。

sbt には、いくつかのユーティリティ・ライブラリや便利な関数があって、 特にファイルやディレクトリの取り扱いには [Scaladocs-IO] にある API を重宝する。

置換しない場合のタスクの拡張 

既存のタスクを実行して、他の別のアクションも実行したい場合は、 ~= か <<= を用いて、既存のタスクをインプットとして取り(これはそのタスクを実行することを意味する)、 既存の実装が完了した後で、別に好きな事をできる。

// 以下の二つのセッティングは等価だ。
intTask <<= intTask map { (value: Int) => value + 1 }
intTask ~= { (value: Int) => value + 1 }

プラグインを使おう! 

.scala ファイルに大量のカスタムコードがあることに気づいたら、 プラグインを作って複数のプロジェクト間で再利用できないか考えてみよう。

以前にちょっと触れたし、 詳しい解説はここにあるけど、 プラグインを作るのはすごく簡単だ。

このページは簡単な味見だけで、 カスタムタスクに関しては Tasks ページで詳細に解説されている。

.scala ビルド定義 

このページは、このガイドのこれまでのページ、特に .sbt ビルド定義 と 他の種類のセッティングを読んでいることを前提とする。

sbt は再帰的だ 

build.sbt は単純化しすぎていて、sbt の実際の動作を隠蔽している。 sbt のビルドは、Scala コードにより定義されている。そのコード自身もビルドされなければいけない。 当然これも sbt でビルドされる。

project ディレクトリは君のプロジェクトのビルド方法を記述したプロジェクトの中のプロジェクトだ。 project 内のプロジェクトは、他のプロジェクトができる全てのことを(理論的には)こなすことができる。 つまり、ビルド定義は sbt プロジェクトであるということだ_。

この入れ子構造は永遠に続く。project/project ディレクトリを作ることで ビルド定義のビルド定義プロジェクトをカスタム化することができる。

以下に具体例で説明する:

hello/                  # プロジェクトのベースディレクトリ

    Hello.scala         # プロジェクトのソースファイル  
                        # (src/main/scala に入れることもできる)

    build.sbt           # build.sbt は、project/ 内のビルド定義プロジェクトの
                        # 一部となる

    project/            # ビルド定義プロジェクトのベースディレクトリ
	   
        Build.scala     # project/ プロジェクトのソースファイル、
                        # つまり、ビルド定義のソースファイル

        build.sbt       # これは、project/project 内のビルド定義プロジェクトの
		                    # 一部となり、ビルド定義のビルド定義となる
						   
        project/        # ビルド定義プロジェクトのためのビルド定義プロジェクトの
                        # ベースディレクトリ

            Build.scala # project/project/ プロジェクトのソースファイル

普通はこういうことをする必要は全く無いので、安心してほしい! だけど、原理を理解すると役立つことがある。

ちなみに、.scala や .sbt で終わる全てのファイルが用いられ、 build.sbt や Build.scala と命名するのは慣例にすぎない。 これは複数のファイルを使っていいということも意味する。

ビルド定義プロジェクトにおける .scala ソースファイル 

.sbt ファイルは、その兄弟の project ディレクトリにマージされる。 プロジェクトの構造をもう一度見てみる:

hello/                  # プロジェクトのベースディレクトリ

    build.sbt           # build.sbt は、project/ 内のビルド定義プロジェクトの
                        # 一部となる

    project/            # ビルド定義プロジェクトのベースディレクトリ

        Build.scala     # project/ プロジェクトのソースファイル、
                        # つまり、ビルド定義のソースファイル

build.sbt 内の Scala 式は別々にコンパイルされ、 Build.scala(もしくは、project/ ディレクトリ内の他の .scala ファイル) に編入される。

ベースディレクトリの .sbt ファイルは、 ベースディレクトリ直下の project 内のビルド定義プロジェクトの一部となる。

つまり、.sbt ファイルは、ビルド定義プロジェクトにセッティングを追加するための、 便利な略記法ということだ。

build.sbt と Build.scala の関係 

ビルド定義の中で、.sbt と .scala を混ぜて使うには、両者の関係を理解する必要がある。

以下に、具体例で説明する。プロジェクトが hello にあるとすると、 hello/project/Build.scala を以下のように作る:

import sbt._
import Keys._

object HelloBuild extends Build {
  val sampleKeyA = settingKey[String]("demo key A")
  val sampleKeyB = settingKey[String]("demo key B")
  val sampleKeyC = settingKey[String]("demo key C")
  val sampleKeyD = settingKey[String]("demo key D")

  override lazy val settings = super.settings ++
    Seq(
      sampleKeyA := "A: in Build.settings in Build.scala",
      resolvers := Seq()
    )

  lazy val root = Project(id = "hello",
    base = file("."),
    settings = Seq(
      sampleKeyB := "B: in the root project settings in Build.scala"
    ))
}

次に、hello/build.sbt を以下のように書く:

sampleKeyC in ThisBuild := "C: in build.sbt scoped to ThisBuild"

sampleKeyD := "D: in build.sbt"

sbt のインタラクティブプロンプトを起動する。inspect sample-a と打ち込むと、以下のように表示されるはず(一部抜粋):

[info] Setting: java.lang.String = A: in Build.settings in Build.scala
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}/*:sampleKeyA

次に、inspect sample-c と打ち込むと、以下のように表示される:

[info] Setting: java.lang.String = C: in build.sbt scoped to ThisBuild
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}/*:sampleKeyC

二つの値とも、“Provided by” は同じスコープを表示していることに注意してほしい。 つまり、.sbt ファイルの sampleKeyC in ThisBuild は、 .scala ファイルの Build.settings リストにセッティングを追加するのと等価とういうことだ。 sbt は、ビルド全体にスコープ付けされたセッティングを両者から取り込んでビルド定義を作成する。

次は、inspect sample-b:

[info] Setting: java.lang.String = B: in the root project settings in Build.scala
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}hello/*:sampleKeyB

sample-b は、 ビルド全体({file:/home/hp/checkout/hello/})ではなく、 特定のプロジェクト({file:/home/hp/checkout/hello/}hello) にスコープ付けされいることに注意してほしい。

もうお分かりだと思うが、inspect sample-d は sample-b に対応する:

[info] Setting: java.lang.String = D: in build.sbt
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}hello/*:sampleKeyD

sbt は .sbt ファイルからのセッティングを Build.settings と Project.settings に追加するため、 これは .sbt 内のセッティングの優先順位が高いことを意味する。 Build.scala を変更して、build.sbt でも設定されている sample-c か sample-d キーを設定してみよう。 build.sbt 内のセッティングが、Build.scala 内のそれに「勝つ」はずだ。

もう一つ気づいたかもしれないが、sampleC と sampleD は build.sbt でそのまま使うことができる。 これは、sbt が Build オブジェクトのコンテンツを自動的に .sbt ファイルにインポートすることにより実現されている。 具体的には、build.sbt ファイル内で import HelloBuild._ が暗黙に呼ばれている。

まとめてみると:

いつ .scala ファイルを使うか 

.scala ファイルでは、セッティング式の羅列に限定されない。 val、object やメソッド定義など、Scala コードを自由に書ける。

推奨される方法の一つとしては、.scala ファイルは val や object やメソッド定義を くくり出すのに使用して、セッティングの定義は .sbt で行うことだ。

.sbt 形式は、単一の式のみが許されているので、式の間でコードを共有する方法を持たない。 コードを共有したければ、共通の変数やメソッドの定義ができるように .scala ファイルが必要になる。 (sbt 0.13.0 からbuild.sbtでも、val、lazy val、メソッド定義は可能になりました。classやobjectが必要な場合は引き続きbuild.scalaが必要です)

.sbt ファイルと .scala ファイルの両方がコンパイルされ、一つのビルド定義が作られる。

.scala ファイルは、単一のビルド内で複数のプロジェクトを定義する場合にも必須だ。 これに関しては、マルチプロジェクトで後ほど説明する。

(マルチプロジェクトで .sbt ファイルを使うことの欠点は、 .sbt ファイルが異なるディレクトリに散らばってしまうことだ。 そのため、サブプロジェクトがある場合は、セッティングを .scala に置くことを好む人もいる。 これは、マルチプロジェクトのふるまいを理解すると、すぐ分かるようになる。)

インタラクティブモードにおけるビルド定義 

sbt のインタラクティブプロンプトの現プロジェクトを project/ 内のビルド定義プロジェクトに切り替えることができる。 reload plugins と打ち込むことで切り替わる:

> reload plugins
[info] Set current project to default-a0e8e4 (in build file:/home/hp/checkout/hello/project/)
> show sources
[info] ArrayBuffer(/home/hp/checkout/hello/project/Build.scala)
> reload return
[info] Loading project definition from /home/hp/checkout/hello/project
[info] Set current project to hello (in build file:/home/hp/checkout/hello/)
> show sources
[info] ArrayBuffer(/home/hp/checkout/hello/hw.scala)
>

上記にあるとおり、reload return を使ってビルド定義プロジェクトから普通のプロジェクトに戻る。

注意: 全て immutable だ 

build.sbt 内のセッティングが、Build や Project オブジェクトの settings フィールドに 追加されると考えるのは間違っている。 そうじゃなくて、Build や Project のセッティングリストと build.sbt のセッティングが 連結されて別の不変リストになって、それが sbt に使われるというのが正しい。 Build と Project オブジェクトは、immutable なコンフィギュレーションであり、 ビルド定義の全体からすると、たった一部にすぎない。

事実、セッティングには他にも出どころがある。具体的には、以下の順で追加される:

後続のセッティングは古いものをオーバーライドする。このリスト全体でビルド定義が構成される。

まとめ 

このページで、このガイドを総括してみよう。

sbt を使うのに、理解しなければいけない概念は少しの数しかない。 確かに、これらには多少の学習曲線があるが、 sbt にはこれらの概念以外のことは特にないとも考えることもできる。 sbt は、強力なコア・コンセプトだけを用いて全てを実現している。

もし、この「始める sbt」シリーズをここまで読破したなら、何を知るべきかはもう分かっていると思う。

sbt: コア・コンセプト 

以上のうち、一つでも分からないことがあれば、質問してみるか、 もう一度再読してみるか、 sbt のインタラクティブモードで実験してみよう。

じゃ、頑張って!

上級者への注意 

sbt はオープンソースであるため、いつでもソースを見れることも忘れずに!