スコープ 

このページではスコープの説明をする。君が、前のページの .sbt ビルド定義タスク・グラフ を読んで理解したことを前提とする。

キーに関する本当の話 

前のページでは、あたかも name のようなキーは単一の sbt の Map のキー・値ペアの項目に対応するかのように説明をしてきた。 しかし、それは実際よりも物事を単純化している。

実のところ、全てのキーは「スコープ」と呼ばれる文脈に関連付けられた値を複数もつことができる。

以下に具体例で説明する:

スコープによって値が異なる可能性があるため、あるキーへの単一の値は存在しない

しかし、スコープ付きキーには単一の値が存在する。

これまで見てきたように sbt がプロジェクトを記述するキーと値のマップを生成するためのセッティングキーのリストを処理していると考えるなら、 そのキーと値の Map におけるキーとは、実はスコープ付きキーである。 また、(build.sbt などの)ビルド定義内のセッティングもまたスコープ付きキーである。

スコープは、暗黙に存在していたり、デフォルトのものがあったりするが、 もしそのデフォルトが適切でなければ build.sbt で必要なスコープを指定する必要があるだろう。

スコープ軸 

スコープ軸(scope axis)は、型であり、そのインスタンスは独自のスコープを定義する (つまり、各インスタンスはキーの独自の値を持つことができる)。

スコープ軸は三つある:

サブプロジェクト軸によるスコープ付け 

一つのビルドに複数のプロジェクトを入れる場合、それぞれのプロジェクトにセッティングが必要だ。 つまり、キーはプロジェクトによりスコープ付けされる。

プロジェクト軸は「ビルド全体」に設定することもでき、その場合はセッティングは単一のプロジェクトではなくビルド全体に適用される。 ビルドレベルでのセッティングは、プロジェクトが特定のセッティングを定義しない場合のフォールバックとして使われることがよくある。

依存性コンフィギュレーション軸によるスコープ付け 

依存性コンフィギュレーション(dependency configuration)は、ライブラリ依存性のグラフを定義し、独自のクラスパス、ソース、生成パッケージなどをもつことができる。 コンフィギュレーションの概念は、sbt が マネージ依存性 に使っている Ivy と、MavenScopes に由来する。

sbt で使われる代表的なコンフィギュレーションには以下のものがある:

デフォルトでは、コンパイル、パッケージ化と実行に関するキーの全ては依存性コンフィグレーションにスコープ付けされているため、 依存性コンフィギュレーションごとに異なる動作をする可能性がある。 その最たる例が compilepackagerun のタスクキーだが、 (sourceDirectoriesscalacOptionsfullClasspath など)それらのキーに影響を及ぼす全てのキーもコンフィグレーションにスコープ付けされている。

タスク軸によるスコープ付け 

セッティングはタスクの動作に影響を与えることもできる。例えば、packageSrcpackageOptions セッティングの影響を受ける。

これをサポートするため、(packageSrc のような)タスクキーは、(packageOption のような)別のキーのスコープとなりえる。

パッケージを構築するさまざまなタスク(packageSrcpackageBinpackageDoc)は、artifactNamepackageOption などのパッケージ関連のキーを共有することができる。これらのキーはそれぞれのパッケージタスクに対して独自の値を取ることができる。

グローバルスコープ 

それぞれのスコープ軸は、その軸の型のインスタンスを代入する(例えば、タスク軸にはタスクを代入する)か、 もしくは、Global という特殊な値を代入することができる。

Global はご想像通りのもので、その軸の全てのインスタンスに対して適用されるセッティングの値だ。 例えば、タスク軸が Global ならば、全てのタスクに適用される。

委譲 

スコープ付きキーは、そのスコープに関連付けられた値がなければ未定義であることもできる。

全てのスコープに対して、sbt には他のスコープからなるフォールバック検索パス(fallback search path)がある。 通常は、より特定のスコープに関連付けられた値が見つからなければ、sbt は、Global や、ビルド全体スコープなど、より一般的なスコープから値を見つけ出そうとする。

この機能により、より一般的なスコープで一度値を代入することで、複数のより特定なスコープがその値を継承することを可能とする。

以下に、inspect を使ったキーのフォールバック検索パス、別名「委譲」(delegate)の探し方を説明する。

sbt 実行中のスコープ付きキーの参照方法 

コマンドラインとインタラクティブモードにおいて、sbt はスコープ付きキーを以下のように表示する(そして、パースする):

{<ビルド-uri>}<プロジェクト-id>/コンフィギュレーション:タスクキー::キー

全ての軸において、* を使って Global スコープを表すことができる。

スコープ付きキーの一部を省略すると、以下の手順で推論される:

さらに詳しくは、Interacting with the Configuration System 参照。

スコープ付きキーの表記例 

スコープの検査 

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-classpathtest コンフィギュレーションと {file:/home/hp/checkout/hello/}default-aea33a プロジェクトにスコープ付けされた full-classpath キー)。

“Dependencies” に関しては、前のページで解説した。

ここで委譲も見ることができ、もし値が定義されていなければ、sbt は以下を検索する:

今度は、(inspect test:full-class のかわりに)inspect full-classpath を試してみて、違いをみてみよう。 コンフィグレーションが省略されたため、compile だと自動検知される。 そのため、inspect compile:full-classpathinspect 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 コンフィギュレーションの packageBinname など、複数のスコープ軸でスコープ付けする:

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 タスクは、デフォルトで CompileTest コンフィギュレーションにスコープ付けされているけど、 これらのスコープ外には存在しない。

そのため、compile キーに関連付けられた値を変更するには、compile in Compilecompile in Test のどちらかを書く必要がある。 素の compile を使うと、コンフィグレーションにスコープ付けされた標準のコンパイルタスクをオーバーライドするかわりに、カレントプロジェクトにスコープ付けされた新しいコンパイルタスクを定義してしまう。

“Reference to undefined setting“ のようなエラーに遭遇した場合は、スコープを指定していないか、間違ったスコープを指定したことによることが多い。 君が使っているキーは何か別のスコープの中で定義されている可能性がある。 エラーメッセージの一部として sbt は、君が意味したであろうものを推測してくれるから、“Did you mean compile:compile?” を探そう。

キーの名前はキーの一部であると考えることもできる。 実際の所は、全てのキーは名前と(三つの軸を持つ)スコープによって構成される。 つまり、packageOptions in (Compile, packageBin) という式全体でキー名だということだ。 単に packageOptions と言っただけでもキー名だけど、それは別のキーだ (in 無しのキーのスコープは暗黙で決定され、現プロジェクト、Global コンフィグレーション、Global タスクとなる)。

ビルドワイド・セッティング 

サブプロジェクト間に共通なセッティングを一度に定義するための上級テクニックとしてセッティングを ThisBuild にスコープ付けするという方法がある。

もし特定のサブプロジェクトにスコープ付けされたキーが見つから無かった場合、 sbt はフォールバックとして ThisBuild 内を探す。 この仕組みを利用して、 versionscalaVersionorganization といったよく使われるキーに対してビルドワイドなデフォルトのセッティングを定義することができる。

便宜のため、セッティング式のキーと本文の両方を ThisBuild にスコープ付けする inThisBuild(...) という関数が用意されている。 セッティング式を渡すと、それに in ThisBuild を可能な所に追加したのと同じものが得られる。

lazy val root = (project in file("."))
  .settings(
    inThisBuild(List(
      // Same as:
      // organization in ThisBuild := "com.example"
      organization := "com.example",
      scalaVersion := "2.12.1",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "Hello",
    publish := (),
    publishLocal := ()
  )

lazy val core = (project in file("core")).
  settings(
    // other settings
  )

lazy val util = (project in file("util")).
  settings(
    // other settings
  )