LoginSignup
10
9

More than 3 years have passed since last update.

sbt入門

Posted at

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 オプションを有効にする設定だ

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に紐づく
もちろん部分的に指定するのも可能だ

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に対して独自の設定を付与することができる
定義方法は以下だ

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を指定することも可能だ

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の一覧なども含まれる
以下のように定義する

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 を使うのも手だ)

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

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

Task

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

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に対するコンパイルとその結果を返すタスクだ
タスクの戻り値は以下のように他の新しいタスクから参照できる

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

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

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


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

10
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9