.scala ビルド定義 

このページは、このガイドのこれまでのページ、特に .sbt ビルド定義他の種類のセッティングを読んでいることを前提とする。

sbt は再帰的だ 

build.sbt は単純化しすぎていて、sbt の実際の動作を隠蔽している。 sbt のビルドは、Scala コードにより定義されている。そのコード自身もビルドされなければいけない。 当然これも sbt でビルドされる。

project ディレクトリは君のプロジェクトのビルド方法を記述したプロジェクトの中のプロジェクトだproject 内のプロジェクトは、他のプロジェクトができる全てのことを(理論的には)こなすことができる。 つまり、ビルド定義は sbt プロジェクトであるということだ_。

この入れ子構造は永遠に続く。project/project ディレクトリを作ることで ビルド定義のビルド定義プロジェクトをカスタム化することができる。

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

hello/                  # プロジェクトのベースディレクトリ

    Hello.scala         # プロジェクトのソースファイル  
                        # (src/main/scala に入れることもできる)

    build.sbt           # build.sbt は、project/ 内のビルド定義プロジェクトの
                        # 一部となる

    project/            # ビルド定義プロジェクトのベースディレクトリ
	   
        Build.scala     # project/ プロジェクトのソースファイル、
                        # つまり、ビルド定義のソースファイル

        build.sbt       # これは、project/project 内のビルド定義プロジェクトの
		                    # 一部となり、ビルド定義のビルド定義となる
						   
        project/        # ビルド定義プロジェクトのためのビルド定義プロジェクトの
                        # ベースディレクトリ

            Build.scala # project/project/ プロジェクトのソースファイル

普通はこういうことをする必要は全く無いので、安心してほしい! だけど、原理を理解すると役立つことがある。

ちなみに、.scala.sbt で終わる全てのファイルが用いられ、 build.sbtBuild.scala と命名するのは慣例にすぎない。 これは複数のファイルを使っていいということも意味する。

ビルド定義プロジェクトにおける .scala ソースファイル 

.sbt ファイルは、その兄弟の project ディレクトリにマージされる。 プロジェクトの構造をもう一度見てみる:

hello/                  # プロジェクトのベースディレクトリ

    build.sbt           # build.sbt は、project/ 内のビルド定義プロジェクトの
                        # 一部となる

    project/            # ビルド定義プロジェクトのベースディレクトリ

        Build.scala     # project/ プロジェクトのソースファイル、
                        # つまり、ビルド定義のソースファイル

build.sbt 内の Scala 式は別々にコンパイルされ、 Build.scala(もしくは、project/ ディレクトリ内の他の .scala ファイル) に編入される。

ベースディレクトリの .sbt ファイルは、 ベースディレクトリ直下の project 内のビルド定義プロジェクトの一部となる。

つまり、.sbt ファイルは、ビルド定義プロジェクトにセッティングを追加するための、 便利な略記法ということだ。

build.sbtBuild.scala の関係 

ビルド定義の中で、.sbt.scala を混ぜて使うには、両者の関係を理解する必要がある。

以下に、具体例で説明する。プロジェクトが hello にあるとすると、 hello/project/Build.scala を以下のように作る:

import sbt._
import Keys._

object HelloBuild extends Build {
  val sampleKeyA = settingKey[String]("demo key A")
  val sampleKeyB = settingKey[String]("demo key B")
  val sampleKeyC = settingKey[String]("demo key C")
  val sampleKeyD = settingKey[String]("demo key D")

  override lazy val settings = super.settings ++
    Seq(
      sampleKeyA := "A: in Build.settings in Build.scala",
      resolvers := Seq()
    )

  lazy val root = Project(id = "hello",
    base = file("."),
    settings = Seq(
      sampleKeyB := "B: in the root project settings in Build.scala"
    ))
}

次に、hello/build.sbt を以下のように書く:

sampleKeyC in ThisBuild := "C: in build.sbt scoped to ThisBuild"

sampleKeyD := "D: in build.sbt"

sbt のインタラクティブプロンプトを起動する。inspect sample-a と打ち込むと、以下のように表示されるはず(一部抜粋):

[info] Setting: java.lang.String = A: in Build.settings in Build.scala
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}/*:sampleKeyA

次に、inspect sample-c と打ち込むと、以下のように表示される:

[info] Setting: java.lang.String = C: in build.sbt scoped to ThisBuild
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}/*:sampleKeyC

二つの値とも、“Provided by” は同じスコープを表示していることに注意してほしい。 つまり、.sbt ファイルの sampleKeyC in ThisBuild は、 .scala ファイルの Build.settings リストにセッティングを追加するのと等価とういうことだ。 sbt は、ビルド全体にスコープ付けされたセッティングを両者から取り込んでビルド定義を作成する。

次は、inspect sample-b:

[info] Setting: java.lang.String = B: in the root project settings in Build.scala
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}hello/*:sampleKeyB

sample-b は、 ビルド全体({file:/home/hp/checkout/hello/})ではなく、 特定のプロジェクト({file:/home/hp/checkout/hello/}hello) にスコープ付けされいることに注意してほしい。

もうお分かりだと思うが、inspect sample-dsample-b に対応する:

[info] Setting: java.lang.String = D: in build.sbt
[info] Provided by:
[info]  {file:/home/hp/checkout/hello/}hello/*:sampleKeyD

sbt は .sbt ファイルからのセッティングを Build.settingsProject.settings追加するため、 これは .sbt 内のセッティングの優先順位が高いことを意味する。 Build.scala を変更して、build.sbt でも設定されている sample-csample-d キーを設定してみよう。 build.sbt 内のセッティングが、Build.scala 内のそれに「勝つ」はずだ。

もう一つ気づいたかもしれないが、sampleCsampleDbuild.sbt でそのまま使うことができる。 これは、sbt が Build オブジェクトのコンテンツを自動的に .sbt ファイルにインポートすることにより実現されている。 具体的には、build.sbt ファイル内で import HelloBuild._ が暗黙に呼ばれている。

まとめてみると:

いつ .scala ファイルを使うか 

.scala ファイルでは、セッティング式の羅列に限定されない。 valobject やメソッド定義など、Scala コードを自由に書ける。

推奨される方法の一つとしては、.scala ファイルは valobject やメソッド定義を くくり出すのに使用して、セッティングの定義は .sbt で行うことだ。

.sbt 形式は、単一の式のみが許されているので、式の間でコードを共有する方法を持たない。 コードを共有したければ、共通の変数やメソッドの定義ができるように .scala ファイルが必要になる。 (sbt 0.13.0 からbuild.sbtでも、val、lazy val、メソッド定義は可能になりました。classやobjectが必要な場合は引き続きbuild.scalaが必要です)

.sbt ファイルと .scala ファイルの両方がコンパイルされ、一つのビルド定義が作られる。

.scala ファイルは、単一のビルド内で複数のプロジェクトを定義する場合にも必須だ。 これに関しては、マルチプロジェクトで後ほど説明する。

マルチプロジェクト.sbt ファイルを使うことの欠点は、 .sbt ファイルが異なるディレクトリに散らばってしまうことだ。 そのため、サブプロジェクトがある場合は、セッティングを .scala に置くことを好む人もいる。 これは、マルチプロジェクトのふるまいを理解すると、すぐ分かるようになる。)

インタラクティブモードにおけるビルド定義 

sbt のインタラクティブプロンプトの現プロジェクトを project/ 内のビルド定義プロジェクトに切り替えることができる。 reload plugins と打ち込むことで切り替わる:

> reload plugins
[info] Set current project to default-a0e8e4 (in build file:/home/hp/checkout/hello/project/)
> show sources
[info] ArrayBuffer(/home/hp/checkout/hello/project/Build.scala)
> reload return
[info] Loading project definition from /home/hp/checkout/hello/project
[info] Set current project to hello (in build file:/home/hp/checkout/hello/)
> show sources
[info] ArrayBuffer(/home/hp/checkout/hello/hw.scala)
>

上記にあるとおり、reload return を使ってビルド定義プロジェクトから普通のプロジェクトに戻る。

注意: 全て immutable だ 

build.sbt 内のセッティングが、BuildProject オブジェクトの settings フィールドに 追加されると考えるのは間違っている。 そうじゃなくて、BuildProject のセッティングリストと build.sbt のセッティングが 連結されて別の不変リストになって、それが sbt に使われるというのが正しい。 BuildProject オブジェクトは、immutable なコンフィギュレーションであり、 ビルド定義の全体からすると、たった一部にすぎない。

事実、セッティングには他にも出どころがある。具体的には、以下の順で追加される:

後続のセッティングは古いものをオーバーライドする。このリスト全体でビルド定義が構成される。