Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

sbtの依存性定義でたまに見るtestやprovidedとは何か--コンフィギュレーションについて学ぼう

sbtはかつてsimple build toolと呼ばれていた(今は酢豚の略とされている)。しかしビルドツールというのは本質的に難しい。複数の依存性を解決し、コンパイラ達が吐き出す成果物を統合し、1つあるいは2つ以上の最終成果物にしなければならないのだから、複雑性は必然だ。そして、複雑性があるところ常に難しさがひそんでいる。

www.scala-sbt.org

そして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

最も一般的なコンフィギュレーション。何も指定しなかった場合に実質的に利用されるのがこれ。このコンフィギュレーションで読み込まれた依存性はどの状況(テスト、配布、・・・)からも読み込める。また、この依存性はこのモジュールに依存するライブラリに波及する。つまり、このモジュールACompileコンフィギュレーションで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とほぼ同じ概念である。

docs.npmjs.com

Test

テストコードのコンパイルと実行時にのみ要求される依存性に対して使われるコンフィギュレーション。典型例がテストフレームワークである。通常の実行時には使われない、テスト時にのみ必要なライブラリがあればこのコンフィギュレーションに設定することでJARファイルのサイスを削減できる。

Optional

モジュールを何らかの事情で分割できないときに、必ずしも使われない依存性を分離するために使われるコンフィギュレーション。ライセンス上の都合で同梱できない場合にも使われるようだ。

Optionalで依存性を定義しても依存関係はCompileと同様だが、依存性の波及が起こらなくなる。すなわち、このモジュールAOptionalコンフィギュレーションでBに依存しているとき、Aに依存する別のモジュールCBに依存しなくなる(必要な場合は手動で依存させなければならない)。

かなりトリッキーなコンフィギュレーションであり、避けられるなら避けたほうが良い。

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で動作するときはTestCompileで解決される依存性が欲しい、自分が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で訊いている)

参考文献

ここまで読み進めた読者は、コンフィギュレーションについてある程度理解しているはずだ。より進んだ学習のために、以下の文献をおすすめする。

www.scala-sbt.org

ant.apache.org

ant.apache.org

ant.apache.org

maven.apache.org

maven.apache.org

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?