SBT Pluginの作り方を調べてみても断片的な情報で、ちょっと苦労したのでまとめてみました。
1.基本形
単独のPluginとして動作する最低限の形で説明します。他のPluginを利用したりするケースもありますが、それらの説明は参考リンク先に委ねることにします。
ベーシックなサンプルとしては、sbt-cloverが一番分かりやすかったです。
ただ、後述のテストまで含めるとxsbt-web-pluginの方が読みやすいかもしれません。
プロジェクト設定
SBTのバージョンは2016/1/1時点で0.13.9が最新ですが、0.13.8あたりを指定しておけば良さそうです。
sbt.version=0.13.8
build.sbtで大事なのはsbtPlugin := true
の部分です。それ以外は通常のSBTプロジェクトと変わりません。
// general
organization := "your.organization"
name := "your-plugin"
version := "0.0.1-SNAPSHOT"
sbtPlugin := true
scalacOptions ++= Seq("-feature", "-deprecation")
追記 ↑xuwei_kさんからコメント頂いたのでscalaVersion
の記述を削除しました。
sbtの0.13.xは Scala 2.10.x で作られているので、 scalaVersion に 2.11.x を指定したらダメです。基本的に指定しないほうがいいです(指定しなければデフォルトのsbtで使われてるものと同じになるので)
コード
SBT PluginはKey
の定義をして、xxxSettings
の中でそのKeyに対して振る舞いをセットする、みたいな形で作っていきます。KeyはsettingKey
/taskKey/inputKeyの3種類があります。
- settingKey
属性のようなものを定義するときに使う。タスクの中で使いたい可変の属性など。 - taskKey
ただの値ではなく動作を伴うタスクを定義するときに使う。 - inputKey
taskKeyで実行時引数(Arguments)を受け取れるようにしたいときに使う。
なので、まずはKeyを定義しましょう。
import sbt._
object YourKeys {
val message = settingKey[String]("表示メッセージ") // ジェネリックにはKeyの型を、引数にはKeyの説明を指定
val hello3times = taskKey[Unit]("messageを3回表示する")
val echo3times = inputKey[Unit]("タスク起動時に渡された文字列を3回表示する")
}
次に、Taskの振る舞いを定義します。
出力先はprintln
でもテストには十分なのですが、sbt.Keys.streams.value.log
で取得できるロガーを使ってみます。
inputTaskの方は、Parserが必要になりますが、大抵はBuilt-inされたParserで十分でしょう。TaskもInputTaskもsbt.Def
のメソッドから定義します。これはTaskを分割して書く方法で、本当に小さなPluginの場合などは後述のPlugin定義の中でhello3times := { ... }
のように直接定義することも可能です。
その他、Commandというものもあるようですが、公式ドキュメントのベストプラクティスを参照するとUse settings and tasks. Avoid commands.とあるので、ここでは触れないことにした方が恐らく幸せでしょう...
import sbt._
import sbt.Keys._
import YourKeys._
object YourTasks extends HelloTask with EchoTask
trait HelloTask {
lazy val helloTask = Def.task {
val log = streams.value.log
for (i <- 0 to 2) {
log.info(s"$i:${message.value}")
}
}
}
trait EchoTask {
// Argumentsを受け取るにはParserが必要
import sbt.complete.DefaultParsers._
lazy val echoTask = Def.inputTask {
val args: Seq[String] = spaceDelimited("<arg>").parsed
val log = streams.value.log
for (i <- 0 to 2) {
args.foreach(arg => log.info(s"$i:$arg"))
}
}
}
最後にPluginとして、これらを組み合わせます。
Pluginの作成には、AutoPlugin
という仕組みを利用することが一番お手軽です。(SBT0.13.5以降で使用可能)
参考:sbt テクノロジ・プリビュー : auto plugin
規模の大きなSBT Pluginでは、Settingsをトレイトに切り出して整理したりもしていますが、今回は小さいのでPluginにそのまま書いてしまいます。
import sbt._
import sbt.Keys._
object YourPlugin extends AutoPlugin {
val autoImport = YourKeys
import autoImport._
val config = config("hoge") // ※1
import YourTasks._
override def projectSettings: Seq[Def.Setting[_]] = inConfig(config)(
Seq(
message := "hello world", // 初期値的な
hello3times <<= helloTask,
echo3times <<= echoTask
)
)
}
※1 config?
このサンプルではconfig("hoge")
と用意して、projectSettingsにはinConfig(config) { ... }
と指定しています。こうすることで、hogeスコープのKeyとして定義することができます。例えばsbt hoge:hello3times
のようにconfigで指定した名前の子として使えるようになる感じです。 ただし、SBT Pluginのベストプラクティスでは、このような単に名前空間としてconfigを使うことは推奨されていません。
もちろん、sbt hello3times
のようにすることも可能で、以下のように設定します。
override def projectSettings: Seq[Def.Setting[_]] = Seq(
message := "hello world",
hello3times <<= helloTask,
echo3times <<= echoTask
)
追記 choplinさんからコメント頂いたので訂正
configurationは依存関係のセットを指定するのに使用するもので名前空間として使わないほうがよい、とオフィシャルのPlugins Best Practicesにあります。この例だと名前空間として利用しているので、Plugins Best Practicesではこう言われていると触れた方がいいかもしれません。http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html#Configuration+advices
追記の追記 xuwei_kさんからコメント頂いて検討したので追記
名前空間的なものを用意したい場合はどうするのがベターなのでしょう?
http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html#Avoid+namespace+clashes
によると、プリフィックスを付ける、という感じですがダサい・・・
それで済むならそうするという、ある意味ダサい慣習が現状はデフォルトだと思います。いずれにせよ、scopeを単に名前空間的に使っても、scope名が衝突したら同じですし
名前の衝突は完全には避けることができないものの、他のSBT Pluginを見ている感じだと、
- sourceDirectoryなどKeyを共有する(?)ような場合はconfigなどでscopeをつくる
- そうでない場合はプリフィックスを付ける
というところなのかなと思います。(正解のない話なので個人的見解。これがベスト!があれば教えて下さい。)
ちなみにですが、sbt-dockerというPluginでは以下のようにtaskKeyを使ってスコープを作っています。これだとhoge::hello3times
のようにコロン2つで呼び出すような形で、configとは違ってTaskなので、こちらの方が良いのかも?という気がしていますがどうなのでしょう。
val hoge = taskKey[Unit]("hoge task")
// ...
hello3times in hoge <<= helloTask
追記の追記の追記 SBT ベストプラクティスをよくよく読むと、
Instead, reuse an existing one or scope by the main task
とあるので、すでにあるKeyを再利用するか、メインタスクのスコープを使いましょう、と書いてありました・・・。ので、今回のような単に名前空間的にKeyを扱いたいときは、それを利用するメインタスクのスコープに入れる、が良さそうです。サンプルコード
利用してみる
作ったPluginを利用できるようにしてみましょう。
後述のテストの布石的にsrc/sbt-test/your-plugin/simple
というディレクトリを作って、ここにPluginを利用できるようにSBTプロジェクトを設定してみます。
addSbtPlugin("your.organization" % "your-plugin" % "0.0.1-SNAPSHOT")
name := "sbt-test"
version := "0.0.1-SNAPSHOT"
enablePlugins(YourPlugin)
これで、以下のようにPluginのタスクを実行できるようになります。
※後述のテスト項にも書いていますが、手動でpublishLocalするこの方法はハマりやすいので非推奨です。
$ cd your-plugins-directory
$ sbt compile
$ sbt publishLocal
$ cd your-plugins-directory/src/sbt-test/your-plugin/simple
$ sbt hoge:hello3times
2.テスト
scripted-pluginがsbtには付属していて、これを利用するのが事故が少なそうです。
参考:sbt プラグインをテストする
手動でsbt publishLocal
して試すこともできるんですが、これはよくローカルIvyのキャッシュが上手くクリアされずにハマることが多いので、素直にscripted-pluginを使いましょう。(私もPluginのlibraryDependenciesを足したらNoClassDefFoundErrorが発生するようになってハマりました...)
※このテストはあくまでもPluginが動作するか?という点のテストなので、ロジックのテストは別途Specs2なりを使ってテストしましょう。
追記 よくよく試すと、scriptedでもpublishLocalすることには変わらないようです?ので、build.sbtに ライブラリ依存の追加をした場合は大人しくversionを上げた方が良いかもしれません。isSnapshot := true
を設定してあげて、
追記の追記 xuwei_kさんにコメント頂いたので訂正
isSnapshot は通常は明示的に指定しない(どちらかといえば参照するためにある)というか、デフォルトではversionの文字列が-SNAPSHOTで終わっているかどうかによって変わったり します。いずれにせよ、isSnapshotを指定したからといって、今回触れているキャッシュの問題が解決するか微妙だと思うので、設定しないほうがいいと思います。 (というか、まず、開発中は必ずversionを-SNAPSHOTで終わらせておくようにしておけばいいはず)
プロジェクト設定
まずは、scriptedを使えるようにします。
// scripted for plugin testing
libraryDependencies <+= sbtVersion(v => "org.scala-sbt" % "scripted-plugin" % v)
次に、build.sbtでscriptedを有効化して、-Dplugin.version
をscripted起動時に渡すように設定しておきます。(上記参考リンクの"ステップ 3: src/sbt-test"参照)
// 追記
// scripted-plugin
ScriptedPlugin.scriptedSettings
scriptedBufferLog := false
scriptedLaunchOpts <+= version { "-Dplugin.version=" + _ }
watchSources <++= sourceDirectory map { path => (path ** "*").get }
そして、sbt-test側の設定も一部修正します。
Option(System.getProperty("plugin.version")) match {
case None =>
throw new RuntimeException(
"""|The system property 'plugin.version' is not defined.
|Please specify this property using the SBT flag -D.""".stripMargin)
case Some(pluginVersion) =>
addSbtPlugin("your.organization" % "your-plugin" % pluginVersion)
}
テストコード
scriptedによるテストではtest
というファイルを用意して、そこにSBTコマンドを記述するような形で定義します。これも詳しくはsbt プラグインをテストするの"ステップ 4: スクリプトを書く"を参照してください。
ただ、そこまで詳細なテストはできないので、もう少し踏み込んだテストは"ステップ 6: カスタムアサーション"の辺りを参照してScalaコードを書くような感じになるようです。
> hoge:hello3times
> hoge:echo3times a b c
これで以下のようにテストを実行できるようになります。
scriptedでは、実行の度にpublishからテストプロジェクトのコピーからが行われるようで、テストが実行されるディレクトリはどこかの一次ディレクトリで実行されるようです。なので、一部ファイルを書き換えてテストを実行する、みたいなことも前後処理などを書かなくても可能なはず(未検証)。
$ sbt scripted