スコープ
このページではスコープの説明をする。君が、前のページの .sbt ビルド定義を読んで理解したことを前提とする。
キーに関する本当の話
これまでは、あたかも name
のようなキーは単一の sbt のマップのキー・値ペアの項目に対応するフリをして話を進めてきた。
それは単純化した話だ。
実のところは、全てのキーは、「スコープ」と呼ばれる文脈に関連付けられた値を複数もつことができる。
以下に具体例で説明する:
- ビルド定義に複数のプロジェクトがあれば、それぞれのプロジェクトにおいて同じキーが別の値を取ることができる。
- メインのソースとテストとのソースが異なるようにコンパイルしたければ、
compile
キーは別の値をとることができる。 - (jar パッケージの作成のオプションを表す)
package-option
キーはクラスファイルのパッケージ(package-bin
)とソースコードのパッケージ(package-src
)で異なる値をとることができる。
スコープによって値が異なる可能性があるため、あるキーへの単一の値は存在しない。
しかし、スコープ付きキーには単一の値が存在する。
これまで見てきたように、sbt が、プロジェクトを記述するキー・値のマップを生成するためにセッティングのリストを処理していくことを考えると、このキー・値マップ内のキーは、スコープ付きキーであることが分かる。
また、(build.sbt
などの)ビルド定義内の、セッティングもスコープ付きキーに適用されるものだ。
スコープは、デフォルトがあったり、暗示されていたりするが、デフォルトが間違っていれば build.sbt
にてスコープを指定しなければいけない。
スコープ軸
スコープ軸(scope axis)は、型であり、そのインスタンスは独自のスコープを定義する (つまり、各インスタンスはキーの独自の値を持つことができる)。
スコープ軸は三つある:
- プロジェクト
- コンフィギュレーション
- タスク
プロジェクト軸によるスコープ付け
一つのビルドに複数のプロジェクトを入れる場合、それぞれのプロジェクトにセッティングが必要だ。 つまり、キーはプロジェクトによりスコープ付けされる。
プロジェクト軸は「ビルド全体」に設定することもでき、その場合はセッティングは単一のプロジェクトではなくビルド全体に適用される。 ビルドレベルでのセッティングは、プロジェクトが特定のセッティングを定義しない場合のフォールバックとして使われることがよくある。
コンフィギュレーション軸によるスコープ付け
コンフィギュレーション(configuration)は、ビルドの種類を定義し、独自のクラスパス、ソース、生成パッケージなどをもつことができる。 コンフィギュレーションの概念は、sbt が マネージ依存性 に使っている Ivy と、MavenScopes に由来する。
sbt で使われるコンフィギュレーションには以下のものがある:
Compile
は、メインのビルド(src/main/scala
)を定義する。Test
は、テスト(src/test/scala
)のビルド方法を定義する。Runtime
は、run
タスクのクラスパスを定義する。
デフォルトでは、コンパイル、パッケージ化、と実行に関するキーの全てはコンフィグレーションにスコープ付けされているため、
コンフィギュレーションごとに異なる動作をする可能性がある。
その最たる例が compile
、package
と run
のタスクキーだが、
(source-directories
や scalac-options
や full-classpath
など)それらのキーに影響を及ぼす全てのキーもコンフィグレーションにスコープ付けされている。
タスク軸によるスコープ付け
セッティングはタスクの動作に影響を与えることもできる。例えば、pakcage-src
は package-options
セッティングの影響を受ける。
これをサポートするため、(package-src
のような)タスクキーは、(package-option
のような)別のキーのスコープとなりえる。
パッケージを構築するさまざまなタスク(package-src
、package-bin
、package-duc
)は、artifact-name
や package-option
などのパッケージ関連のキーを共有することができる。これらのキーはそれぞれのパッケージタスクに対して独自の値を取ることができる。
グローバルスコープ
それぞれのスコープ軸は、その軸の型のインスタンスを代入する(例えば、タスク軸にはタスクを代入する)か、
もしくは、Global
という特殊な値を代入することができる。
Global
は、予想通りのもので、その軸の全てのインスタンスに対して適用されるセッティングの値だ。
例えば、タスク軸が Global
ならば、全てのタスクに適用される。
委譲
スコープ付きキーは、そのスコープに関連付けられた値がなければ未定義であることもできる。
全てのスコープに対して、sbt には他のスコープからなるフォールバック検索パス(fallback search path)がある。
通常は、より特定のスコープに関連付けられた値が見つからなければ、
sbt は、Global
や、ビルド全体スコープなど、より一般的なスコープから値を見つけ出そうとする。
この機能により、より一般的なスコープで一度値を代入することで、複数のより特定なスコープがその値を継承することを可能とする。
以下に、inspect
を使ったキーのフォールバック検索パス、別名「委譲」(delegate)の探し方を説明する。
sbt 実行中のスコープ付きキーの参照方法
コマンドラインとインタラクティブモードにおいて、sbt はスコープ付きキーを以下のように表示し(パースする):
{<ビルド-uri>}<プロジェクト-id>/コンフィギュレーション:キー(for タスクキー)
{<ビルド-uri>}<プロジェクト-id>
は、プロジェクト軸を特定する。<プロジェクト-id> がなければ、プロジェクト軸は「ビルド全体」スコープとなる。コンフィギュレーション
は、コンフィギュレーション軸を特定する。(for タスクキー)
は、タスク軸を特定する。キー
は、スコープ付けされるキーを特定する。
全ての軸において、*
を使って Global
スコープを表すことができる。
スコープ付きキーの一部を省略すると、以下の手順で推論される:
- プロジェクトを省略した場合は、現在のプロジェクトが使われる。
- コンフィグレーションを省略した場合は、キーに依存したコンフィギュレーションが自動検知される。
- タスクを省略した場合は、
Global
タスクが使われる。
さらに詳しくは、[[Inspecting Settings]] 参照。
スコープの検査
sbt のインタラクティブモード内で inspect
コマンドを使ってキーとそのスコープを理解することができる。
例えば、inspect test:full-classpath
と試してみよう:
$ sbt > inspect test:full-classpath [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:full-classpath [info] Dependencies: [info] test:exported-products [info] test:dependency-classpath [info] Reverse dependencies: [info] test:run-main [info] test:run [info] test:test-loader [info] test:console [info] Delegates: [info] test:full-classpath [info] runtime:full-classpath [info] compile:full-classpath [info] *:full-classpath [info] {.}/test:full-classpath [info] {.}/runtime:full-classpath [info] {.}/compile:full-classpath [info] {.}/*:full-classpath [info] */test:full-classpath [info] */runtime:full-classpath [info] */compile:full-classpath [info] */*:full-classpath [info] Related: [info] compile:full-classpath [info] compile:full-classpath(for doc) [info] test:full-classpath(for doc) [info] runtime:full-classpath
一行目からこれが([[.sbt ビルド定義|Basic Def]] で説明されているとおり、セッティングではなく)タスクであることが分かる。
このタスクの戻り値は 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" は、まだ意味不明だろうけど、[[次のページ|More About Settings]]まで待ってて。
ここで委譲も見ることができ、もし値が定義されていなければ、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
コンフィギュレーションには定義されていない。
より詳しくは、[[Inspecting Settings]] 参照。
ビルド定義からスコープを参照する
build.sbt
で裸のキーを使ってセッティングを作った場合は、現プロジェクト、Global
コンフィグレーション、Global
タスクにスコープ付けされる:
name := "hello"
sbt を実行して、inspect name
と入力して、キーが {file:/home/hp/checkout/hello/}default-aea33a/*:name
により提供されていることを確認しよう。つまり、プロジェクトは、{file:/home/hp/checkout/hello/}default-aea33a
で、コンフィギュレーションは *
で、タスクは表示されていない(グローバルを指す)ということだ。
build.sbt
は常に単一のプロジェクトのセッティングを定義するため、「現プロジェクト」は今 build.sbt
で定義しているプロジェクトを指す。
([[マルチプロジェクトビルド|Multi-Project]]の場合は、プロジェクトごとに build.sbt
がある。)
キーにはオーバーロードされた in
メソッドがあり、それによりスコープを設定できる。
in
への引数として、どのスコープ軸のインスタンスでも渡すことができる。
これをやる意味は全くないけど、例として Compile
コンフィギュレーションでスコープ付けされた name
の設定を以下に示す:
name in Compile := "hello"
また、package-bin
タスクでスコープ付けされた 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
タスクとなる)。
続いて
スコープを理解したから、セッティングについてさらに深く理解することができる。