sbt入門

# SBT
https://www.scala-sbt.org/index.html
言わずもがなscalaで作られたbuildツール
scala以外の言語のプロジェクトにも使えるようだが、当記事ではscalaのbuildに使うことを前提に進めていく
`version 1.3.6` を前提に進めていく
入門記事だが、対象読者はsbtを一通りの触ってみたぐらいのユーザを想定している(サブプロジェクトの定義ができればベストだ)
記事の目標としては読者が独自のTaskやCommandを定義できることを目指した

# DSL
sbtのbuild設定はprojectルートにある `build.sbt` ファイルと `project/*.scala` `project/*.sbt`  
に分割して記載することができる
`build.sbt` 含め `*.sbt` は自動でロードされ、 `*.scala` (厳密に書くと, そこに記載されている `class, object` )は `*.sbt` ファイルより呼び出されることでロードされる
その他の違いについては以下で記載する

## `*.sbt` と `*.scala` の違い
- `*.scala` の特徴  
`*.scala` は普通のscalaファイルだ、ライブラリ依存を除き、通常のscalaとして行えることは(おそらく)全て行える
`*.scala` にbuild設定を記載する場合は `import sbt._; import Keys._` をすればよい、これらは `*.sbt` の場合、暗黙的にimportされているものになる
余談だが、過去に存在していた `Build.scala` は deprecated になったらしい
- `*.sbt` の特徴  
`*.sbt` はトップレベルにsetting式( `:=` や `+=` を使ったsettingKeyの変更やtaskの定義など)を記述することができる, 後述するが各KeyにはScopeというものが存在しており、Scope指定をせずトップレベルに記載された場合、任意のScopeに対して値が紐づいていない場合のフォールバック値として利用される(つまり、デフォルト値のような挙動を持たすことができる)  
`build.sbt` は特殊な `*.sbt` ファイルで必ず一番最後にロードされる、root projectの定義やglobalなtaskの定義などはここ書くといい

## Scope
sbtのbuild設定は、簡単に言うとScope(+settingKey)に紐づく値の集合だ
Scopeには三つの軸( `project` `configuration` `task` )が存在しており、これはTuple3で表現される `e.g. (root, Compile, compile)` (わかりにくいが、Compileとcompileは別のものを指しており、前者は Compile configuration, 後者はcompile taskだ)
scopeに対する値の紐付けはsetting式で行える
例えば以下の場合Test configuration, すなわち、 `test:compile` を行った時のみ `-Xfatal-warnings` オプションを有効にする設定だ

```scala
scalacOptions in (yourProject, Test, compile) += "-Xfatal-warnings"
// yourProject / Test / compile / scalacOptions += "-Xfatal-warnings" と書いても同じだ
```

Scopeの記述は省略可能で省略した場合は、sbtまたはsbt plugin側で自動で設定される
たとえば `lazy val core = (project in file("./core")).settings(...)` の中でScopeを省略した場合は、自動でそのprojectに紐づく
もちろん部分的に指定するのも可能だ

```scala
lazy val core = (project in file("core"))
  .settings(
    scalacOptions in Test += "-Xfatal-warnings"
)
```

`*.sbt` でScope指定なしでglobalに宣言したものはデフォルト値のように働く、ただし、明示的にデフォルト値として使いたい場合は `ThisBuild` Scopeに紐づけておくほうがいい、 `ThisBuild` はフォールバックを前提に用意されているScopeだからだ
https://www.scala-sbt.org/1.x/docs/ja/Scopes.html#%E3%83%93%E3%83%AB%E3%83%89%E3%83%AC%E3%83%99%E3%83%AB%E3%83%BB%E3%82%BB%E3%83%83%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

## SettingKey
ユーザは基本的にはsbt apiやpluginに存在するSettingKey(+Scope)に値をマッピングするだけだが
settingKeyを定義することで、projectに対して独自の設定を付与することができる
定義方法は以下だ

```scala
lazy val strictBuild = settingKey[Boolean]("add cats library use base settings")
lazy val baseSettings = Def.settings(
  scalacOptions ++= (if (strictBuild.value) Seq("-Xfatal-warnings") else Nil)
)

lazy val proj1 = (project in file("./proj1"))
  .settings(
    strictBuild := true
  )
  .settings(baseSettings)

lazy val proj2 = (project in file("./proj2"))
  .settings(
    strictBuild := false
  )
  .settings(baseSettings)
```

SettingKeyの定義は `settingKey[T](description: String)` 関数で行い、型パラメタ `T` がそのキーに紐づけることのできる値の型だ
SettingKeyの値は `SettingKey#value` メソッドで取得することができる, ただしこれはsetting式の中でのみ使え、取得Scopeがコンテキストにより変動する(今回の例だと最終的にproject定義の中に入っているので各projectに紐づいた値が取得される)
もちろん `value` メソッドで明示的にScopeを指定することも可能だ

```scala
lazy val proj1 = (project in file("./proj1"))
  .settings(
    strictBuild := true
  )
  .settings(baseSettings)

lazy val proj2 = (project in file("./proj2"))
  .settings(
    strictBuild := false,
    scalacOptions ++= (if ((strictBuild in proj1).value) Seq("-Xfatal-warnings") else Nil)
  )
```

この場合、proj2もproj1のsetting値(つまりtrueだ)を拝借することができる, この例のように作るのは混乱を招くだけだが、Scopeを完全指定できること自体は役に立つので覚えておくといい

## Command
Commandを定義することで独自のsbtコマンドを定義することができる
これは逐次実行するコマンドの列などを定義するときに役に立つ
コマンドの本体は `State => State` のFunction1だ、Stateは実行時のsbtの状態を表し、settingの一覧なども含まれる
以下のように定義する

```scala
commands += Command.command("coreCompile") { st =>
  println("==+ switch core +==")
  val st1 = Command.process("project core", st)
  println("==+ compile +==")
  val st2 = Command.process("compile", st1)
  println("==+ switch root +==")
  val st3 = Command.process("project root", st2)
  st3
}
```

これは引数を取らないコマンドの定義だ、引数を一つとるものは `Command.single`, 複数とるものは `Command.args` で定義できる
`Command.process` で他のCommandを呼び出すことができる、他のコマンドも状態を返すのでそのStateをバケツリレーすることで逐次状態を変更している, sbtより外の世界に対する副作用のみに興味がある場合は、このバケツリレーは不要だ
`commands` は標準で用意してくれているSettingKeyのひとつだ、つまり、コマンドの一覧もScopeに紐づく値のひとつにすぎない
(余談だが、このような単純な逐次呼び出しであれば `addCommandAlias` を使うのも手だ)

```scala
addCommandAlias("coreCompile", ";project core ;compile ;project root")
``` 


次で紹介するTaskとCommandの大きな違いは呼び出し元を何に置いているか、という点だ、Commandはsbt shellからinteractiveに呼び出されることを想定している

## Task
TaskはCommandのようにshellからinteractiveに実行もできるし、他のTaskからも呼び出すこともできる
Taskは戻り値を持ち、以下のように定義できる

```scala
import xsbti.compile.CompileAnalysis // これはTaskの定義とは直接関係ないimport
lazy val aggregateCompile = taskKey[Seq[CompileAnalysis]]("compile with all project.")

aggregateCompile := {
  val currentState = state.value
  val extracted = Project.extract(currentState)
  import extracted._
  
  structure.allProjectPairs
    .filter { case (res, _) => res.aggregate.isEmpty }
    .map { case (_, ref) => runTask(compile in (ref, Compile), currentState)._2 }
}
```
これはaggregate projectを除く全てのsub projectに対するコンパイルとその結果を返すタスクだ
タスクの戻り値は以下のように他の新しいタスクから参照できる


```scala
lazy val dump = taskKey[Unit]("dump all compiled files")

dump := {
  aggregateCompile.value.map { an =>
    println(
      an.readSourceInfos().getAllSourceInfos.keySet()
    )
  }
}
```

Taskを利用することで他のTaskに依存した新しい独自タスクを再定義できるのが特徴だ


---

おそらくこれだけの基本的な知識があれば、独自タスクの定義ができると思う
sbt DSLは(主にスコープとマクロによって)記述箇所によって挙動が変わるため、初心者にとってあまり直感的ではない
ただこれはスコープの概念を理解すれば無駄な記述量を減らせる嬉しい機能なので是非とも覚えて使いこなして欲しい
(といっても筆者もここに書いたこと以上のことは知らないし、間違っている可能性もあるので、指摘いただけると助かります)