1. カスタム設定とタスク

カスタム設定とタスク 

このページでは、独自のsettingとtaskの作成方法について説明します。

このページを理解するには、入門ガイドの前のページ、特にbuild.sbtタスクグラフを読んでください。

キーの定義 

Keysには、キーの定義方法を示す多くの例が含まれています。ほとんどのキーはDefaultsに実装されています。

キーには3つの型のいずれかがあります。SettingKeyTaskKeyについては.sbtビルド定義で説明されています。入力タスクのページでInputKeyについて説明します。

Keysからの例

val scalaVersion = settingKey[String]("The version of Scala used for building.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")

キーコンストラクタには、キーの名前("scalaVersion")とドキュメント文字列("ビルドに使用されるscalaのバージョン。")の2つの文字列パラメータがあります。

.sbtビルド定義から、SettingKey[T]の型パラメータTは設定の値の型を示し、TaskKey[T]Tはタスクの結果の型を示していることを思い出してください。また、.sbtビルド定義から、設定はプロジェクトのリロードまで固定された値を持ち、タスクは毎回「タスク実行」(sbtインタラクティブプロンプトまたはバッチモードでコマンドが入力されるたびに)再計算されることを思い出してください。

キーは.sbtファイル.scalaファイル、または自動プラグインで定義できます。有効な自動プラグインのautoImportオブジェクトの下にあるすべてのvalは、.sbtファイルに自動的にインポートされます。

タスクの実装 

タスクのキーを定義したら、タスク定義でそれを完成させる必要があります。独自のタスクを定義することも、既存のタスクを再定義することもできます。どちらの場合も見た目は同じです。:=を使用して、タスクキーにコードを関連付けます。

val sampleStringTask = taskKey[String]("A sample string task.")
val sampleIntTask = taskKey[Int]("A sample int task.")

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"

lazy val library = (project in file("library"))
  .settings(
    sampleStringTask := System.getProperty("user.home"),
    sampleIntTask := {
      val sum = 1 + 2
      println("sum: " + sum)
      sum
    }
  )

タスクに依存関係がある場合、タスクグラフで説明されているように、valueを使用してその値を参照します。

タスクの実装で最も難しい部分は、多くの場合、sbt固有のものではありません。タスクは単なるScalaコードです。難しい部分は、実行しようとしていることを行うタスクの「本体」を記述することです。たとえば、HTMLをフォーマットしようとしている場合、HTMLライブラリを使用したい場合があります(ビルド定義にライブラリ依存関係を追加し、おそらくHTMLライブラリに基づいてコードを記述します)。

sbtにはいくつかのユーティリティライブラリと便利な関数があり、特にIOの便利なAPIを使用してファイルやディレクトリを操作できます。

タスクの実行セマンティクス 

valueを使用してカスタムタスクから他のタスクに依存する場合、重要な詳細としてタスクの実行セマンティクスがあります。実行セマンティクスとは、これらのタスクが正確にいつ評価されるかということです。

例えばsampleIntTaskの場合、タスクの本体の各行は厳密に順番に評価される必要があります。つまり、シーケンシャルセマンティクスです。

sampleIntTask := {
  val sum = 1 + 2        // first
  println("sum: " + sum) // second
  sum                    // third
}

実際には、JVMはsum3にインライン化することがありますが、タスクの観測可能な効果は、各行が順番に実行された場合と同じままです。

今度は、さらに2つのカスタムタスクstartServerstopServerを定義し、sampleIntTaskを次のように変更してみます。

val startServer = taskKey[Unit]("start server")
val stopServer = taskKey[Unit]("stop server")
val sampleIntTask = taskKey[Int]("A sample int task.")
val sampleStringTask = taskKey[String]("A sample string task.")

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"

lazy val library = (project in file("library"))
  .settings(
    startServer := {
      println("starting...")
      Thread.sleep(500)
    },
    stopServer := {
      println("stopping...")
      Thread.sleep(500)
    },
    sampleIntTask := {
      startServer.value
      val sum = 1 + 2
      println("sum: " + sum)
      stopServer.value // THIS WON'T WORK
      sum
    },
    sampleStringTask := {
      startServer.value
      val s = sampleIntTask.value.toString
      println("s: " + s)
      s
    }
  )

sbtインタラクティブプロンプトからsampleIntTaskを実行すると、次のようになります。

> sampleIntTask
stopping...
starting...
sum: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:00:00 PM

何が起こったのかを確認するために、sampleIntTaskの図解表記を見てみましょう。

task-dependency

単純なScalaメソッド呼び出しとは異なり、タスクに対してvalueメソッドを呼び出すと、厳密に評価されません。代わりに、sampleIntTaskstartServerstopServerタスクに依存していることを示すプレースホルダーとして機能します。あなたがsampleIntTaskを呼び出すと、sbtのタスクエンジンは

  • sampleIntTaskを評価するにタスク依存関係を評価します(部分的な順序付け)
  • 独立している場合、タスク依存関係を並列に評価しようとします(並列化)
  • 各タスク依存関係は、コマンド実行ごとに1回だけ評価されます(重複排除)

タスク依存関係の重複排除 

最後の点を示すために、sbtインタラクティブプロンプトからsampleStringTaskを実行できます。

> sampleStringTask
stopping...
starting...
sum: 3
s: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:30:00 PM

sampleStringTaskstartServersampleIntTaskタスクの両方に依存し、sampleIntTaskstartServerタスクに依存しているため、タスク依存関係として2回表示されます。これが単純なScalaメソッド呼び出しであれば2回評価されますが、valueは単にタスク依存関係を示しているため、1回だけ評価されます。以下は、sampleStringTaskの評価の図解表記です。

task-dependency

タスク依存関係を重複排除しないと、Test / testのタスク依存関係としてTest / compileが複数回表示されるため、testタスクが呼び出されたときにテストソースコードが何度もコンパイルされることになります。

クリーンアップタスク 

stopServerタスクをどのように実装すべきでしょうか?クリーンアップタスクの概念は、タスクの依存関係を追跡することに関することであるため、タスクの実行モデルには適合しません。最後の操作は、他の中間タスクに依存するタスクになるはずです。例えば、stopServersampleStringTaskに依存する必要があります。その時点で、stopServersampleStringTaskになります。

lazy val library = (project in file("library"))
  .settings(
    startServer := {
      println("starting...")
      Thread.sleep(500)
    },
    sampleIntTask := {
      startServer.value
      val sum = 1 + 2
      println("sum: " + sum)
      sum
    },
    sampleStringTask := {
      startServer.value
      val s = sampleIntTask.value.toString
      println("s: " + s)
      s
    },
    sampleStringTask := {
      val old = sampleStringTask.value
      println("stopping...")
      Thread.sleep(500)
      old
    }
  )

それが機能することを示すために、インタラクティブプロンプトからsampleStringTaskを実行します。

> sampleStringTask
starting...
sum: 3
s: 3
stopping...
[success] Total time: 1 s, completed Dec 22, 2014 6:00:00 PM

task-dependency

プレーンなScalaを使用する 

何かを別の何かが起こった後に確実に実行させるもう一つの方法は、Scalaを使用することです。例えば、project/ServerUtil.scalaに単純な関数を次のように実装できます。

sampleIntTask := {
  ServerUtil.startServer
  try {
    val sum = 1 + 2
    println("sum: " + sum)
  } finally {
    ServerUtil.stopServer
  }
  sum
}

プレーンなメソッド呼び出しはシーケンシャルセマンティクスに従うため、すべてが順序どおりに実行されます。重複排除がないため、その点に注意する必要があります。

プラグインに変換する 

多くのカスタムコードがある場合は、複数のビルドで再利用するためにプラグインに移動することを検討してください。

前に触れられていたように、そしてここで詳しく説明されているように、プラグインの作成は非常に簡単です。

このページは簡単な紹介でした。タスクページには、カスタムタスクについてさらに多くの情報があります。