SBT Pluginの作り方


SBT Pluginの作り方を調べてみても断片的な情報で、ちょっと苦労したのでまとめてみました。

# 1.基本形

単独のPluginとして動作する最低限の形で説明します。他のPluginを利用したりするケースもありますが、それらの説明は参考リンク先に委ねることにします。
ベーシックなサンプルとしては、[sbt-clover](https://github.com/shanbin/sbt-clover)が一番分かりやすかったです。
ただ、後述のテストまで含めると[xsbt-web-plugin](https://github.com/earldouglas/xsbt-web-plugin)の方が読みやすいかもしれません。

## プロジェクト設定

SBTのバージョンは2016/1/1時点で0.13.9が最新ですが、0.13.8あたりを指定しておけば良さそうです。

```project/build.properties
sbt.version=0.13.8
```

build.sbtで大事なのは`sbtPlugin := true`の部分です。それ以外は通常のSBTプロジェクトと変わりません。

```build.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](http://www.scala-sbt.org/0.13/docs/Tasks.html)/[inputKey](http://www.scala-sbt.org/0.13/docs/Input-Tasks.html)の3種類があります。

- settingKey  
属性のようなものを定義するときに使う。タスクの中で使いたい可変の属性など。
- taskKey  
ただの値ではなく動作を伴うタスクを定義するときに使う。
- inputKey  
taskKeyで実行時引数（Arguments）を受け取れるようにしたいときに使う。

なので、まずはKeyを定義しましょう。

```src/main/scala/your/organization/YourKeys.scala
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](http://www.scala-sbt.org/release/docs/Parsing-Input.html)が必要になりますが、大抵は[Built-inされたParser](http://www.scala-sbt.org/release/docs/Parsing-Input.html#Built-in+parsers)で十分でしょう。TaskもInputTaskも`sbt.Def`のメソッドから定義します。これはTaskを分割して書く方法で、本当に小さなPluginの場合などは後述のPlugin定義の中で`hello3times := { ... }`のように直接定義することも可能です。
その他、[Command](http://www.scala-sbt.org/release/docs/Commands.html)というものもあるようですが、公式ドキュメントのベストプラクティスを参照すると[Use settings and tasks. Avoid commands.](http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html#Use+settings+and+tasks.+Avoid+commands.)とあるので、ここでは触れないことにした方が恐らく幸せでしょう...

```src/main/scala/your/organization/YourTasks.scala
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](http://eed3si9n.com/ja/sbt-preview-auto-plugins)
規模の大きなSBT Pluginでは、Settingsをトレイトに切り出して整理したりもしていますが、今回は小さいのでPluginにそのまま書いてしまいます。

```src/main/scala/your/organization/YourPlugin.scala
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を使うことは[推奨されていません](http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html#Configuration+advices)。*
もちろん、`sbt hello3times`のようにすることも可能で、以下のように設定します。

```src/main/scala/your/organization/YourPlugin.scala
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](https://github.com/chatwork/sbt-docker)というPluginでは以下のようにtaskKeyを使ってスコープを作っています。これだと`hoge::hello3times`のようにコロン2つで呼び出すような形で、configとは違ってTaskなので、こちらの方が良いのかも？という気がしていますがどうなのでしょう。

```
val hoge = taskKey[Unit]("hoge task")
// ...
hello3times in hoge <<= helloTask
```

**追記の追記の追記** [SBT ベストプラクティス](http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html#You+probably+won%E2%80%99t+need+your+own+configuration)をよくよく読むと、
>  Instead, reuse an existing one or scope by the main task

とあるので、すでにあるKeyを再利用するか、メインタスクのスコープを使いましょう、と書いてありました・・・。ので、今回のような単に名前空間的にKeyを扱いたいときは、それを利用するメインタスクのスコープに入れる、が良さそうです。[サンプルコード](http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html#Provide+raw+settings+and+configured+settings)


## 利用してみる

作ったPluginを利用できるようにしてみましょう。
後述のテストの布石的に`src/sbt-test/your-plugin/simple`というディレクトリを作って、ここにPluginを利用できるようにSBTプロジェクトを設定してみます。

```src/sbt-test/your-plugin/simple/project/plugins.sbt
addSbtPlugin("your.organization" % "your-plugin" % "0.0.1-SNAPSHOT")
```

```src/sbt-test/your-plugin/simple/build.sbt
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 プラグインをテストする](http://eed3si9n.com/ja/testing-sbt-plugins)
手動で`sbt publishLocal`して試すこともできるんですが、これはよくローカルIvyのキャッシュが上手くクリアされずにハマることが多いので、素直にscripted-pluginを使いましょう。（私もPluginのlibraryDependenciesを足したらNoClassDefFoundErrorが発生するようになってハマりました...）
※このテストはあくまでもPluginが動作するか？という点のテストなので、ロジックのテストは別途Specs2なりを使ってテストしましょう。

**追記** よくよく試すと、scriptedでもpublishLocalすることには変わらないようです？ので、build.sbtに ~~`isSnapshot := true`を設定してあげて、~~ ライブラリ依存の追加をした場合は大人しくversionを上げた方が良いかもしれません。
**追記の追記** xuwei_kさんにコメント頂いたので訂正
> isSnapshot は通常は明示的に指定しない(どちらかといえば参照するためにある)というか、デフォルトではversionの文字列が-SNAPSHOTで終わっているかどうかによって変わったり します。いずれにせよ、isSnapshotを指定したからといって、今回触れているキャッシュの問題が解決するか微妙だと思うので、設定しないほうがいいと思います。 (というか、まず、開発中は必ずversionを-SNAPSHOTで終わらせておくようにしておけばいいはず)

## プロジェクト設定

まずは、scriptedを使えるようにします。

```project/plugins.sbt
// scripted for plugin testing
libraryDependencies <+= sbtVersion(v => "org.scala-sbt" % "scripted-plugin" % v)
```

次に、build.sbtでscriptedを有効化して、`-Dplugin.version`をscripted起動時に渡すように設定しておきます。（上記参考リンクの"ステップ 3: src/sbt-test"参照）

```build.sbt
// 追記
// scripted-plugin
ScriptedPlugin.scriptedSettings
scriptedBufferLog  := false
scriptedLaunchOpts <+= version { "-Dplugin.version=" + _ }
watchSources       <++= sourceDirectory map { path => (path ** "*").get }
```

そして、sbt-test側の設定も一部修正します。

```src/sbt-test/your-plugin/simple/project/plugins.sbt
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 プラグインをテストする](http://eed3si9n.com/ja/testing-sbt-plugins)の"ステップ 4: スクリプトを書く"を参照してください。
ただ、そこまで詳細なテストはできないので、もう少し踏み込んだテストは"ステップ 6: カスタムアサーション"の辺りを参照してScalaコードを書くような感じになるようです。

```src/sbt-test/your-plugin/simple/test
> hoge:hello3times
> hoge:echo3times a b c
```

これで以下のようにテストを実行できるようになります。
scriptedでは、実行の度にpublishからテストプロジェクトのコピーからが行われるようで、テストが実行されるディレクトリはどこかの一次ディレクトリで実行されるようです。なので、一部ファイルを書き換えてテストを実行する、みたいなことも前後処理などを書かなくても可能なはず（未検証）。

```
$ sbt scripted
```


# 参考

- [sbt テクノロジ・プリビュー : auto plugin](http://eed3si9n.com/ja/sbt-preview-auto-plugins)
- [sbt プラグインをテストする](http://eed3si9n.com/ja/testing-sbt-plugins)
- [ベストプラクティス](http://www.scala-sbt.org/0.13/docs/Plugins-Best-Practices.html)
- [sbt-clover](https://github.com/shanbin/sbt-clover)
- [xsbt-web-plugin](https://github.com/earldouglas/xsbt-web-plugin)
- [sbt-docker - marcuslonnberg](https://github.com/marcuslonnberg/sbt-docker)
- [sbt-docker - chatwork](https://github.com/chatwork/sbt-docker)
- [sbtプラグインを作る - おみブロZ](http://akiomik.hatenablog.jp/entry/2013/11/10/020732)