こんにちは、キャスレーコンサルティングID(システム・インテグレーション)部のfkmtです。

Java10では、「var」が使えるようになったそうですね。
Java8でLambdaが出てきた時にも思いましたが、もうScala使えばいいのに

というワケで、今回はScala、、、というかSBTについて、書こうと思います。
テーマは、SBTプラグインです。

SBTとは、Scala製のビルドツールで「Simple Build Tool」の略です(でした?)。

> バージョン0.13までは、SBTのソースコードのヘッダに「sbt — Simple Build Tool」と書いてありました。
> 1.xからは、「Simple Build Tool」の記載はなくなっています。

SBTについては、(いささか古いですが)このブログ↓でもご紹介しています。
【scala初心者向け】sbtを触ってみよう!

Hello World

まずは、定番「Hello World」を。

カスタムタスク

さっそくプラグインを作って、、、といきたいところですが、
まずは、build.sbtに直接カスタムタスクを書いて、挙動を確認します。

SBTではKeyとTaskを使ってカスタマイズします。
KeyにはSettingKey、TaskKey、InputKeyという種類があって、、、
と、詳しい説明をしていると眠くなるので、サクっと書いてしまいましょう。

build.sbt

// プロジェクトの基本設定
lazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.12.5",
    name := "hello-world"
  )

// Keyの定義
// SBTから`hello`で呼び出せるようになる。
val hello = taskKey[Unit]("hello world")

// helloの実装
hello := {
  println("hello world")
}

これだけで `sbt hello` と叩くと、以下のように表示されます。

sbt hello
#> hello world

引数を使ってみる

先の例では、「hello world」と表示するだけでしたが、
次は「world」の部分を、引数から取得するようにしてみます。

build.sbt

// Keyの定義
// SBTから`hello`で呼び出せるようになる。
val hello = inputKey[Unit]("hello world")

// helloの実装
hello := {
  val args: Seq[String] = complete.DefaultParsers.spaceDelimited("<arg>").parsed
  println("args.size=" + args.size)
  println(s"hello ${args.head}!!")
}

> プロジェクトの基本設定の部分は変わらないので省略

「hello」の定義を、「taskKey」から「inputKey」に変更しました。
引数を取って処理する場合は、「inputKey」を使います。

また、タスクの実装では引数を処理するために、パーサを使います。
デフォルトで用意されているパーサの中から、引数を空白で区切ってくれるパーサを選びました。

実行してみると、以下のようになります。

sbt "hello hoge"
#> args.size=1
#> hello hoge!!

sbt "hello hoge fuga"
#> args.size=2
#> hello hoge!!

プラグイン化してみる

基本的な挙動が分かったところで、これをプラグイン化してみます。

プラグインの基本的なことは、だいたいココ↓に書いてあります。

https://www.scala-sbt.org/1.x/docs/Plugins.html

モノスゴク要約すると、

  • 1. 普通にSBTプロジェクトを作る
  • 2. 「build.sbt」に `sbtPlugin := true` と書く
  • 3. sbt.AutoPluginをextendsした、Objectを作る

以上です。

考えるな、感じろ

ドキュメントを読んだだけでは、理解したつもりになるだけなので、実際に手を動かして感じましょう。

build.sbt

lazy val root = (project in file("."))
  .settings(
    organization := "jp.co.casleyconsulting",
    version := "0.1.0-SNAPSHOT",
    name := "hello-plugin",
    sbtPlugin := true
  )

注目点は、 `sbtPlugin := true` です。
これだけでプラグインになります。

HelloPlugin.scala

package jp.co.casleyconsulting.helloplugin

import sbt._

object HelloPlugin extends AutoPlugin {

  // これを書いておくと、プラグイン利用者が面倒なimportを書かなくてすむ
  object autoImport {
    val hello = inputKey[Unit]("hello")
  }

  import autoImport._

  // このプラグインが有効化されるトリガ
  // `allRequirements`はこのプラグインの依存性が全て解決されたら有効化される
  override def trigger = allRequirements

  // プロジェクトのビルドセッティングにこのプラグインを設定する
  override lazy val buildSettings = Seq(
    hello := helloTask.evaluated // 引数を取らないTaskのときは `helloTask.value` になる
  )

  // helloタスクの実装
  lazy val helloTask = Def.inputTask {
    val args: Seq[String] = complete.DefaultParsers.spaceDelimited("<arg>").parsed
    println(s"hello ${args.mkString(",")} !!")
  }

}

build.sbtに、直接カスタムタスクを書いたときよりも、少し記述量が増えます。
各コードが何をしているのかは、コード中のコメントをご参照ください。

カスタムタスクでは、定義したKeyに直接実装を書いていましたが
プラグイン化した今回は、行儀のよい(?)書き方にしてみました。

タスクの実装は、「Def.inputTask」を使って別途定義し、
定義したタスクの実装「evaluated」を呼んで、Keyにセットしています。

注意していただきたい点は、 `hello := helloTask.evaluated` で、
この「evaluated」はInputTaskの場合に使い、引数を取らないTaskの場合は「value」を使います。

また、ついでに今回は引数が複数ある場合にも、対応してみました。

使ってみる

さて、プラグインはできたので使ってみます。

まずは、作ったプラグインをローカルインストールします。
`sbt publishLoadl` と叩くと、ivyのローカルリポジトリ(~/.ivy2/local/)にプラグインが配置されます。

ローカルインストールが済んだら、適当なSBTプロジェクトを作ってプラグインをロードします。

sbt new sbt/scala-seed.g8 --name="sample"

cd sample

cat <<'EOF'> project/plugins.sbt
addSbtPlugin("jp.co.casleyconsulting" % "hello-plugin" % "0.1.0-SNAPSHOT")
EOF

sbt "hello hoge fuga piyo"
#> hello hoge,fuga,piyo !!

できましたー!

まとめ

今回は、SBTプラグインの入門的な内容をご紹介しました。

SBTの公式ドキュメントをなぞった程度ですが、文章を読むよりコードを見て触った方が理解が進むと思い
できるだけ説明は省いて、コード主体で書いてみました。

今回は触れていませんが、プラグインのテストする仕組み(scripted test framework)もあるようです。
自分も試したことがないので、機会があればまたご紹介できればと思います。