一般的なベストプラクティス 

このページでは、 sbt を利用するに当たってのベストプラクティスについて説明する。

project/~/.sbt/ の使い分け 

プロジェクトをビルドするために必要なものは、 project/ に配置するべきだ。 例えばwebプラグインのようなものがこれに相当する。 ~/.sbt/ には、ビルドで使用するローカル環境のカスタマイズやコマンドなど、プロジェクトのビルドに必須ではないものを配置する。 例えば IDE のプラグインなどがこれに相当する。

ローカル環境の設定 

ユーザ独自の設定を行うには2つの方法がある。 そのようなユーザ独自の設定の一例として、 resolvers のリストのはじめにローカルのMavenリポジトリを追加することが挙げられる。

resolvers := {
  val localMaven = "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
  localMaven +: resolvers.value
}
  1. ユーザ独自の設定を, ~/.sbt/1.0/global.sbt などのグローバルな .sbt ファイルに記述する方法。 ここに記述した設定は、全てのプロジェクトに適用される。
  2. ユーザ独自の設定を <project>/local.sbt のようなプロジェクト内の .sbt ファイルに記述し、バージョンコントロールから除外しておく方法。 sbt は複数の .sbt ファイルの設定を結合するので、バージョンコントロール下に通常の <project>/build.sbt ファイルも持つことができる。

.sbtrc 

sbt 起動時に実行するコマンドは .sbtrc ファイルの各行に記述する。 これらのコマンドはプロジェクトがロードする前に実行されるため、エイリアスの定義などに便利だ。 sbt は、まず $HOME/.sbtrc 内のコマンドを実行し(ファイルが存在する場合のみ)、次に <project>/.sbtrc を実行する(ファイルが存在する場合のみ)。

生成されるファイル 

生成されるファイルは target で設定された出力ディレクトリのサブディレクトリに書き出す。 このようにしておくことで、生成ファイルが一箇所に整理され、ビルド後のクリーンアップが容易になる。 クロスビルドを効率的にするために、 Scala のバージョンごとに生成されるファイルは crossTarget の下に書き出す。

ソースとリソースの生成については、ソースファイル/リソースファイルの生成を参照して欲しい。

ハードコードしない 

出力ディレクトリの target/ などの定数をハードコードするべきではない。 これはプラグインを書く際に特に重要だ。 ユーザはこれを build/ に変更するかもしれないし、プラグインもそれを尊重すべきだ。 代わりに次のような設定を使うとよい。

myDirectory := target.value / "sub-directory"

ファイルの変更をしない 

ビルドは通常たくさんのファイル操作で成り立っている。 これをミュータブルな状態を作らないように設計されたタスクシステムでうまく扱うにはどのようにすればよいだろうか。 推奨される方法の1つは、ファイルへの書き込みを1つのタスクから、かつ1度のみにすることだ。このアプローチは sbt の既定のタスクでも採用している。

1つのビルド成果物へは、ただ1度、1つのタスクからのみ書きこみをされるべきだ。 そのタスクでは生成された File オブジェクトを返し、そのファイルを利用する他のタスクは、このタスクをmapする。 このようにすることで、ファイルの参照を取得すると同時にファイルを生成するタスクが先に実行されることを保証できる。

もちろんこれでもユーザや他のプロセスがファイルを変更することを防ぐことはできないが、 タスクレベルではファイルの内容をイミュータブルに扱うので、ビルドのコントロール下にあるI/Oをより予測可能なものにできる。

例は次の通りだ。

lazy val makeFile = taskKey[File]("Creates a file with some content.")

// ファイルを作成するタスクの定義
//  内容を書き込んでその File オブジェクトを返す
makeFile := {
    val f: File = file("/tmp/data.txt")
    IO.write(f, "Some content")
    f
}

// makeFile の帰り値は生成された File オブジェクトだ。
//  そのため、 useFile タスクは makeFile タスクを map することで、
//  ファイルへの参照を得るのと同時に、 makeFile タスクへの依存性を宣言できる。
useFile :=
    doSomething( makeFile.value )

このような書き方がいつもできるとは限らないが、例外ではなく慣例とすべきだ。

絶対パスを利用する 

絶対的な File オブジェクトのみを利用すべきだ。 次のように、絶対パスを指定するか、

file("/home/user/A.scala")

絶対的な File オブジェクトを起点として別の File オブジェクトを構築する。

base / "A.scala"

これは前述のハードコーディングしないというベストプラクティスにも関連する。 なぜなら適切な方法は、 baseDirectory の設定を参照することだからだ。 次の例では、 myPath に <base>/licenses/ ディレクトリを設定している。

myPath := baseDirectory.value / "licenses"

Java や Scala では相対的な File オブジェクトは現在のワーキングディレクトリからの相対パスを表す。 種々の理由から、このワーキングディレクトリは、常にビルドのルートディレクトリに一致するとは限らない。

このルールの唯一の例外は、プロジェクトのベースディレクトリを利用している場合だ。 この場合、sbt は利便性のため、相対的な File オブジェクトをビルドのルートディレクトリからの相対パスとして解決する。

パーサコンビネータ 

  1. タブ補完の境界を明確に区切るため、全ての場所で token を利用すること。
  2. token のオーバーラップやネストをしないこと。そのようにした場合の挙動は未定義であり、将来的にエラーを引き起こす可能性が高い。
  3. 再帰処理には flatMap を利用すること。 sbt のコンビネータは生成クラス数の上限に厳格だ。そのため flatMap を使って次のように記述するとよい。
lazy val parser: Parser[Int] =
  token(IntBasic) flatMap { i =>
    if(i <= 0)
      success(i)
    else
      token(Space ~> parser)
  }

上記の例では、負数を末尾にもつ空白区切りの整数列をパースし、最後の負数を返すパーサを定義している。