このページでは、一つのビルドで複数のサブプロジェクトを管理する方法を紹介する。 このガイドのこれまでのページを読んでおいてほしい。 特に build.sbt を理解していることが必要になる。
一つのビルドに複数の関連するサブプロジェクトを入れておくと、 サブプロジェクト間に依存性がある場合や同時に変更されることが多い場合に便利だ。
ビルド内の個々のサブプロジェクトは、それぞれ独自のソースディレクトリを持ち、
package
を実行すると独自の jar ファイルを生成するなど、概ね通常のプロジェクトと同様に動作する。
個々のプロジェクトは lazy val を用いて Project 型の値を宣言することで定義される。例として、以下のようなものがプロジェクトだ:
lazy val util = (project in file("util"))
lazy val core = (project in file("core"))
val で定義された名前はプロジェクトの ID 及びベースディレクトリの名前になる。 ID は sbt シェルからプロジェクトを指定する時に用いられる。
ベースディレクトリ名が ID と同じ名前であるときは省略することができる。
lazy val util = project
lazy val core = project
複数プロジェクトに共通なセッティングをくくり出す場合、
セッティングを ThisBuild
にスコープ付けする。
ただし、右辺値には純粋な値か Global
もしくは ThisBuild
にスコープ付けされたセッティングしか置くことができない、
またサブプロジェクトにスコープ付けされたセッティングがデフォルトで存在しない必要があるというという制約がある。
(スコープ参照)
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val core = (project in file("core"))
.settings(
// other settings
)
lazy val util = (project in file("util"))
.settings(
// other settings
)
これで version
を一箇所で変更すれば、再読み込み後に全サブプロジェクトに反映されるようになる。
複数プロジェクトに共通なセッティングをくくり出す場合、
commonSettings
という名前のセッティングの Seq を作って、
それを引数として各プロジェクトの settings
メソッドを呼び出せばよい。
lazy val commonSettings = Seq(
target := { baseDirectory.value / "target2" }
)
lazy val core = (project in file("core"))
.settings(
commonSettings,
// other settings
)
lazy val util = (project in file("util"))
.settings(
commonSettings,
// other settings
)
一つのビルドの中の個々のプロジェクトはお互いに完全に独立した状態であってもよいが、
普通、何らかの形で依存関係を持っているだろう。
ここでは集約(aggregate
)とクラスパス(classpath
)という二種類の依存関係がある。
集約とは、集約する側のプロジェクトであるタスクを実行するとき、集約される側の複数のプロジェクトでも同じタスクを実行するという関係を意味する。例えば、
lazy val root = (project in file("."))
.aggregate(util, core)
lazy val util = (project in file("util"))
lazy val core = (project in file("core"))
上の例では、root
プロジェクトが util
と core
を集約している。
この状態で sbt を起動してコンパイルしてみよう。
3 つのプロジェクトが全てコンパイルされることが分かると思う。
集約プロジェクト内で(この場合は root
プロジェクトで)、
タスクごとに集約をコントロールすることができる。
例えば、update
タスクの集約を以下のようにして回避できる:
lazy val root = (project in file("."))
.aggregate(util, core)
.settings(
aggregate in update := false
)
[...]
aggregate in update
は、update
タスクにスコープ付けされた aggregate
キーだ
(スコープ参照)。
注意: 集約は、集約されるタスクを順不同に並列実行する。
あるプロジェクトが、他のプロジェクトにあるコードに依存させたい場合、
dependsOn
メソッドを呼び出して実現すればよい。
例えば、core
に util
のクラスパスが必要な場合は core
の定義を次のように書く:
lazy val core = project.dependsOn(util)
これで core
内のコードから util
の class を利用することができるようになった。
また、これにより core
がコンパイルされる前に util
の update
と compile
が実行されている必要があるので
プロジェクト間でコンパイル実行が順序付けられることになる。
複数のプロジェクトに依存するには、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 は全てのファイルを監視したり、大量に発生するディスクやシステム I/O によって高性能とは言えない反応になるかもしれない。
一つの対策として sbt は compile
を呼び出した時に依存するサブプロジェクトのコンパイルを
行うかどうかを制御する trackInternalDependencies
と exportToInternal
というセッティングがある。両者とも
TrackLevel.NoTracking
、TrackLevel.TrackIfMissing
、TrackLevel.TrackAlways
という 3つの値を取ることができる。デフォルトは両方とも TrackLevel.TrackAlways
だ。
trackInternalDependencies
が TrackLevel.TrackIfMissing
に設定されると、sbt は *.class
ファイル
(exportJars
が true
の場合は JAR ファイル)
が一切無い場合を除き自動的に内部 (サブプロジェクト) 依存性をコンパイルすることを止める。
TrackLevel.NoTracking
に設定すると内部依存性のコンパイルは無視される。
ただし、クラスパスは通常どおり追加されるため、依存性グラフは依存性だと表示する。
この動機は開発時に大量のサブプロジェクトの変更の確認に伴う I/O
オーバーヘッドを回避することにある。全てのサブプロジェクトを TrackIfMissing
に設定する方法を以下に示す。
ThisBuild / trackInternalDependencies := TrackLevel.TrackIfMissing
ThisBuild / exportJars := true
lazy val root = (project in file("."))
.aggregate(....)
exportToInternal
セッティングは依存された側から内部トラッキングをオプトアウトすることを可能にして、
これを使うことでほとんどのサブプロジェクトは追跡したいが、一部を抜きたいという時に使える。
trackInternalDependencies
と exportToInternal
の交叉が実際の追跡レベルを決定する。
以下が 1つのサブプロジェクトをオプトアウトさせる例だ:
lazy val dontTrackMe = (project in file("dontTrackMe"))
.settings(
exportToInternal := TrackLevel.NoTracking
)
もしプロジェクトがルートディレクトリに定義されてなかったら、 sbt はビルド時に他のプロジェクトを集約するデフォルトプロジェクトを勝手に生成する。
プロジェクト hello-foo
は、base = file("foo")
と共に定義されているため、
サブディレクトリ foo
に置かれる。
そのソースは、foo/Foo.scala
のように foo
の直下に置かれるか、
foo/src/main/scala
内に置かれる。
ビルド定義ファイルを除いては、通常の sbt ディレクトリ構造が foo
以下に適用される。
sbt インタラクティブプロンプトから、projects
と入力することでプロジェクトの全リストが表示され、
project <プロジェクト名>
で、カレントプロジェクトを選択できる。
compile
のようなタスクを実行すると、それはカレントプロジェクトに対して実行される。
これにより、ルートプロジェクトをコンパイルせずに、サブプロジェクトのみをコンパイルすることができる。
また subProjectID/compile
のように、プロジェクト ID を明示的に指定することで、そのプロジェクトのタスクを実行することもできる。
.sbt
ファイルで定義された値は、他の .sbt
ファイルからは見えない。 .sbt
ファイル間でコードを共有するためには、 ベースディレクトリにある project/
配下に Scala ファイルを用意すればよい。
詳細はビルドの整理を参照。
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
とも同じビルド定義の一部だ。
スタイルの選択:
*.sbt
ファイル内で宣言することができる。その場合、build.sbt
は lazy val foo = (project in file("foo"))
といった形で最小の project 宣言のみを行いセッティングは書かない。
build.sbt
に書けば全てのビルド定義を 1つのファイルにまとめることができるので、その方法を推奨する。ただし、これは好みの問題だから、好きにやっていい。
注意: サブプロジェクトは、project
サブディレクトリや、project/*.scala
ファイルを持つことができない。
foo/project/Build.scala
は無視される。