このページではスコープの説明をする。君が、前のページの .sbt ビルド定義を読んで理解したことを前提とする。
前のページでは、あたかも name のようなキーは単一の sbt の Map のキー・値ペアの項目に対応するかのように説明をしてきた。
しかし、それは実際よりも物事を単純化している。
実のところ、全てのキーは「スコープ」と呼ばれる文脈に関連付けられた値を複数もつことができる。
以下に具体例で説明する:
compile キーは別の値をとることができる。
packageOption キーはクラスファイルのパッケージ(packageBin)とソースコードのパッケージ(packageSrc)で異なる値をとることができる。
スコープによって値が異なる可能性があるため、あるキーへの単一の値は存在しない。
しかし、スコープ付きキーには単一の値が存在する。
これまで見てきたように sbt がプロジェクトを記述するキーと値のマップを生成するためのセッティングキーのリストを処理していると考えるなら、
そのキーと値の Map におけるキーとは、実はスコープ付きキーである。
また、(build.sbt などの)ビルド定義内のセッティングもまたスコープ付きキーである。
スコープは、暗黙に存在していたり、デフォルトのものがあったりするが、
もしそのデフォルトが適切でなければ build.sbt で必要なスコープを指定する必要があるだろう。
スコープ軸(scope axis)は、型であり、そのインスタンスは独自のスコープを定義する (つまり、各インスタンスはキーの独自の値を持つことができる)。
スコープ軸は三つある:
一つのビルドに複数のプロジェクトを入れる場合、それぞれのプロジェクトにセッティングが必要だ。 つまり、キーはプロジェクトによりスコープ付けされる。
プロジェクト軸は「ビルド全体」に設定することもでき、その場合はセッティングは単一のプロジェクトではなくビルド全体に適用される。 ビルドレベルでのセッティングは、プロジェクトが特定のセッティングを定義しない場合のフォールバックとして使われることがよくある。
コンフィギュレーション(configuration)は、ビルドの種類を定義し、独自のクラスパス、ソース、生成パッケージなどをもつことができる。 コンフィギュレーションの概念は、sbt が マネージ依存性 に使っている Ivy と、MavenScopes に由来する。
sbt で使われるコンフィギュレーションには以下のものがある:
Compile は、メインのビルド(src/main/scala)を定義する。
Test は、テスト(src/test/scala)のビルド方法を定義する。
Runtime は、run タスクのクラスパスを定義する。
デフォルトでは、コンパイル、パッケージ化と実行に関するキーの全てはコンフィグレーションにスコープ付けされているため、
コンフィギュレーションごとに異なる動作をする可能性がある。
その最たる例が compile、package と run のタスクキーだが、
(sourceDirectories や scalacOptions や fullClasspath など)それらのキーに影響を及ぼす全てのキーもコンフィグレーションにスコープ付けされている。
セッティングはタスクの動作に影響を与えることもできる。例えば、packageSrc は packageOptions セッティングの影響を受ける。
これをサポートするため、(packageSrc のような)タスクキーは、(packageOption のような)別のキーのスコープとなりえる。
パッケージを構築するさまざまなタスク(packageSrc、packageBin、packageDoc)は、artifactName や packageOption などのパッケージ関連のキーを共有することができる。これらのキーはそれぞれのパッケージタスクに対して独自の値を取ることができる。
それぞれのスコープ軸は、その軸の型のインスタンスを代入する(例えば、タスク軸にはタスクを代入する)か、
もしくは、Global という特殊な値を代入することができる。
Global はご想像通りのもので、その軸の全てのインスタンスに対して適用されるセッティングの値だ。
例えば、タスク軸が Global ならば、全てのタスクに適用される。
スコープ付きキーは、そのスコープに関連付けられた値がなければ未定義であることもできる。
全てのスコープに対して、sbt には他のスコープからなるフォールバック検索パス(fallback search path)がある。
通常は、より特定のスコープに関連付けられた値が見つからなければ、sbt は、Global や、ビルド全体スコープなど、より一般的なスコープから値を見つけ出そうとする。
この機能により、より一般的なスコープで一度値を代入することで、複数のより特定なスコープがその値を継承することを可能とする。
以下に、inspect を使ったキーのフォールバック検索パス、別名「委譲」(delegate)の探し方を説明する。
コマンドラインとインタラクティブモードにおいて、sbt はスコープ付きキーを以下のように表示する(そして、パースする):
{<ビルド-uri>}<プロジェクト-id>/コンフィギュレーション:タスクキー::キー
{<ビルド-uri>}<プロジェクト-id> は、プロジェクト軸を特定する。<プロジェクト-id> がなければ、プロジェクト軸は「ビルド全体」スコープとなる。
コンフィギュレーション は、コンフィギュレーション軸を特定する。
タスクキー は、タスク軸を特定する。
キー は、スコープ付けされるキーを特定する。
全ての軸において、* を使って Global スコープを表すことができる。
スコープ付きキーの一部を省略すると、以下の手順で推論される:
Global タスクが使われる。
さらに詳しくは、Interacting with the Configuration System 参照。
fullClasspath はキーのみを指定し、デフォルトスコープを用いる。ここでは、カレントプロジェクト、キーに依存したコンフィギュレーション、グローバルタスクスコープとなる。
test:fullClasspath はコンフィギュレーションを指定する。つまりプロジェクト軸とタスク軸はデフォルトを用いつつも testコンフィギュレーションにおける fullClasspath というキーを表す。
*:fullClasspath はデフォルトコンフィギュレーションを用いずに Global コンフィギュレーションを用いる事を明示している。
doc::fullClasspath はプロジェクト軸とコンフィギュレーション軸はデフォルトを用いつつ、 doc タスクスコープにおける fullClasspath というキーを表す。
{file:/home/hp/checkout/hello/}default-aea33a/test:fullClasspath は {file:/home/hp/checkout/hello/} をルートディレクトリにビルドした際に含まれる default-aea33a というプロジェクトを指定している。さらにこのプロジェクト内の test コンフィギュレーションを用いる事も明示している。
{file:/home/hp/checkout/hello/}/test:fullClasspath は {file:/home/hp/checkout/hello/} のビルド全体をプロジェクトの軸とする。
{.}/test:fullClasspath は {.} で指定されたルートディレクトリのビルド全体をプロジェクト軸に取る。{.} は Scala code において ThisBuild と記述できる。
{file:/home/hp/checkout/hello/}/compile:doc::fullClasspath は3つのスコープ軸全てを指定している。
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 は以下を検索する:
runtime:full-classpath と compile:full-classpath)。
これらのスコープ付きキーは、プロジェクトは特定されていないため「現プロジェクト」で、タスクも特定されていない Global だ。
Global に設定されたコンフィギュレーション (*:full-classpath)。プロジェクトはまだ特定されていないため「現プロジェクト」で、タスクもまだ特定されていないため Global だ。
{.} 別名 ThisBuild に設定されたプロジェクト(つまり、特定のプロジェクトではなく、ビルド全体)。
Global に設定されたプロジェクト軸(*/test:full-classpath)(プロジェクトが特定されていない場合は、現プロジェクトを意味するため、Global を検索することは新しく、* と「プロジェクトが未表示」はプロジェクト軸に対して異なる値を持ち、*/test:full-classpath と test:full-classpath は等価ではない。)
Global を設定する(*/*:full-classpath)(特定されていないタスクは Global であるため、*/*:full-classpath は三つの軸全てが Global を取る。)
今度は、(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 タスクにスコープ付けされる:
lazy val root = (project in file(".")).
settings(
name := "hello"
)
sbt を実行して、inspect name と入力して、キーが {file:/home/hp/checkout/hello/}default-aea33a/*:name により提供されていることを確認しよう。つまり、プロジェクトは、{file:/home/hp/checkout/hello/}default-aea33a で、コンフィギュレーションは * で、タスクは表示されていない(グローバルを指す)ということだ。
キーにはオーバーロードされた in メソッドがあり、それによりスコープを設定できる。
in への引数として、どのスコープ軸のインスタンスでも渡すことができる。
これをやる意味は全くないけど、例として Compile コンフィギュレーションでスコープ付けされた name の設定を以下に示す:
name in Compile := "hello"
また、packageBin タスクでスコープ付けされた 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 タスクとなる)。