sbtはかつてsimple build toolと呼ばれていた(今は酢豚の略とされている)。しかしビルドツールというのは本質的に難しい。複数の依存性を解決し、コンパイラ達が吐き出す成果物を統合し、1つあるいは2つ以上の最終成果物にしなければならないのだから、複雑性は必然だ。そして、複雑性があるところ常に難しさがひそんでいる。
そしてlibraryDependencies
の難しさは初心者が最も先にハマるものの一つだ。うち、コンフィギュレーションconfigurationはその難しさの一角を占めている。コンフィギュレーションについて知識をつけることで、実行時に発生するよくある間違い(例えば、実行時にクラスが存在しないというエラーでJVMが起動しないなど)を回避できるようになる。
この記事ではsbtにおけるlibraryDependencies
のコンフィギュレーションの使い所について述べる。
コンフィギュレーション
まずは形から入ろう。コンフィギュレーションとはいかなる構文要素なのか?という点に着目したのち、これがどのような意味をビルド時に持つのかを見ていくことにする。
構文から見たコンフィギュレーション
build.sbt
の書式の面から見たコンフィギュレーションとは、libraryDependencies
に指定する書式のうち、バージョン番号の後に出現するConfiguration
型を持つ値(文字列を置くことも可能)のことだ:
libraryDependencies += groupID % artifactID % revision % configuration
具体例を示す:
// 型安全 libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % Test // 同じ意味 libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % "test"
コンフィギュレーションを設定しない場合は"default"
か"compile"
を指定したのと同じだ*1。
コンフィギュレーションはどのような意味を持つか
コンフィギュレーションは依存性を使い分けるための手段だ。 元々コンフィギュレーションはJava向け依存性管理ツールであるIvy上の概念だが、sbtは内部的にIvyを利用しているため、sbtはコンフィギュレーションの概念をそのまま継承している。 どうしてコンフィギュレーションが必要なのか、これから解説する。
開発をしているとよくあることだが、同じモジュール(sbtではこれをプロジェクトと呼ぶ)の中であるにもかかわらず、状況により異なる依存性を切り替えて利用したいことがある。例えばテスト時にのみ必要な依存性が存在する(具体例で言えば、テストフレームワークだ)し、常に必要なものもある。配布するJARファイルにテストフレームワークが混ざっていては不経済だし、そうするべきではない。
Ivyはこれらのユースケースが異なる依存性を区別し切り替える手段を定義している。Ivyでは、同一モジュール内における区別された依存性のセットのことをコンフィギュレーションと呼ぶ。例えばテストフレームワークの依存性を"test"
というコンフィギュレーションに結び付けて定義し、テスト時には"test"
コンフィギュレーションでモジュールをビルドすることで、テスト時にのみテストフレームワークがロードされるようにできる。モジュールを構成する依存性をどう組み立てるかを定義する概念なので、コンフィギュレーションと呼ばれているというわけだ。
より実践的に言い換えると、グループID・アーティファクトID・リビジョンは何の依存性を導入するかを決定するために使われるのに対し、コンフィギュレーションはどう依存性を導入するかを決定するために使われる。
sbtで利用可能なコンフィギュレーション
実際に利用可能なコンフィギュレーションのうち、主なものをここで紹介する。
Default
Compile
Provided
Test
Optional
Runtime
Default
何も指定しなければこれが設定される、とIvyのドキュメントにある。実際にDefault
をexplicitに指定するとうまくコンパイルしないことがあるので、プレースホルダに近いかもしれない。
Compile
最も一般的なコンフィギュレーション。何も指定しなかった場合に実質的に利用されるのがこれ。このコンフィギュレーションで読み込まれた依存性はどの状況(テスト、配布、・・・)からも読み込める。また、この依存性はこのモジュールに依存するライブラリに波及する。つまり、このモジュールA
がCompile
コンフィギュレーションでB
に依存しているとき、A
に依存する別のモジュールC
は自動的にB
に依存する(おそらく、最も望ましい挙動だ)。
Provided
ほぼCompile
と同じだが、実行時環境(例えばJDKやサーブレットコンテナ)が依存性を埋めてくれることを期待する。最たる例がJavaxのServlet APIである。
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api libraryDependencies += "javax.servlet" % "javax.servlet-api" % "4.0.1" % "provided"
サーブレットの開発やコンパイルにはAPIが必要である。しかしAPIの実体はデプロイ先のウェブコンテナが提供するため、配布するJARファイルに含める必要はない。Provided
コンフィギュレーションに設定することで、コンパイルとテストではクラスパスに含まれるが、実行時にはウェブコンテナにクラスを提供してもらうようにできる。
これはnpmにおけるpeerDependencies
とほぼ同じ概念である。
Test
テストコードのコンパイルと実行時にのみ要求される依存性に対して使われるコンフィギュレーション。典型例がテストフレームワークである。通常の実行時には使われない、テスト時にのみ必要なライブラリがあればこのコンフィギュレーションに設定することでJARファイルのサイスを削減できる。
Optional
モジュールを何らかの事情で分割できないときに、必ずしも使われない依存性を分離するために使われるコンフィギュレーション。ライセンス上の都合で同梱できない場合にも使われるようだ。
Optional
で依存性を定義しても依存関係はCompile
と同様だが、依存性の波及が起こらなくなる。すなわち、このモジュールA
がOptional
コンフィギュレーションでB
に依存しているとき、A
に依存する別のモジュールC
はB
に依存しなくなる(必要な場合は手動で依存させなければならない)。
かなりトリッキーなコンフィギュレーションであり、避けられるなら避けたほうが良い。
https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html
Runtime
コンパイル時には読み込まれず、テストや実行時にのみ利用できるようになるコンフィギュレーション。動的なロードを行うときに使うかもしれない。これもかなりトリッキーで、Scalaで使うことはあまりないかもしれない。
主なコンフィギュレーションは以上だ。これらを適切に使い分けて依存性を管理してほしい。
コンフィギュレーションのマッピング
ここでは、マッピングという概念について解説する。マッピングは時としてマルチプロジェクトの困難さを軽減する。
以下のような形式のコンフィギュレーションを見たことがあるかもしれない:
foo %% bar % buzz % "test->test;test->compile;compile->compile"
一見して意味不明なコンフィギュレーションだが、これはマッピングと呼ばれるコンフィギュレーションの指定方法である。
マッピングは以下のような文法で定義される:
myConfiguration->targetConfiguration
また、複数のマッピングを同時に定義する際にはセミコロン;
で分割できる。
マッピングは、自分が動作中のコンフィギュレーションに対して、依存先のモジュールのうちどのコンフィギュレーションを採用するかを手動で割り当てるためのものである。初出の例では、「自分がTest
で動作するときはTest
とCompile
で解決される依存性が欲しい、自分がCompile
で動作するときはCompile
のコンフィギュレーションで解決してほしい」という意味合いになる。
- 自分が
Test
のときbar
モジュールを読み込む。その際、(通常ならばCompile
だけが依存性に波及するが、これに加えて)bar
モジュールのTest
コンフィギュレーションで定義される依存性もいっしょに欲しい
- 自分が
Compile
のときbar
モジュールを読み込む。その際、bar
モジュールのCompile
コンフィギュレーションで定義される依存性もいっしょに欲しい(通常の動作)
ここで、->
は「uses configuration of target's」と読み替えると分かりやすいかもしれない。
test uses configuration of target's test; test uses configuration of target's compile; compile uses configuration of target's compile
このテクニックは、プロジェクトを複数に分割し、テストコードもある程度分割されているとき、ヘルパー関数が1つのプロジェクトにまとめて定義されている、というシチュエーションで遭遇することになる。
ちなみに何もマッピングを指定しない場合(e.g. test
など)では、コンフィギュレーションはCompile
へとマッピングされる(つまり、test->compile
を指定したのと同じ)。これにより、依存先のCompile
コンフィギュレーションによる依存性がロードされる結果となる。
Q&A
以下のような疑問に遭遇したことがあるので、簡潔にまとめておいた:
- 実行環境で既にクラスが用意されているので、
sbt-assembly
が生成するJARファイルからこのライブラリを除きたいProvided
使ってください
sbt-assembly
はどのコンフィギュレーションで動くのCompile
で動作するはず(確信がないのでDiscordで訊いている)
参考文献
ここまで読み進めた読者は、コンフィギュレーションについてある程度理解しているはずだ。より進んだ学習のために、以下の文献をおすすめする。