Magnolia Tech

いつもコードのことばかり考えている人のために。

sbt 1.0時代のPlugin事情

sbt-scalatraというsbt pluginを1.0対応させるためにやったことをまとめておきます。

sbt 1.0対応作業一覧

sbt 1.0対応のpluginをゼロから作るときの詳しい解説は、公式ドキュメントにきちんと書かれているので、まずはそれを読む。

sbt Reference Manual — Plugins

ここでは既にsbt 0.13系のPluginをアップデートしてsbt 1.0対応したときのことを書く。ざっと以下のような作業を行った。

  1. AutoPlugin化
  2. project/pluginをアップデートする
  3. 複数のsbtのバージョンに対応させる
  4. plugin間の依存関係の定義
  5. pluginを自動で有効にする
  6. settingsの対応
  7. deprecatedなメソッドの書き換え

AutoPlugin化

sbt 1.0以降はAutoPluginしかサポートされなくなるので、従来Plugin classを継承していた箇所をAutoPlugin classの継承へ書き換える。

単純に書き換えるだけでOK。

これを…

object ScalatraPlugin extends Plugin {

こう書き換える。

object ScalatraPlugin extends AutoPlugin {

AutoPlugin自体の詳しい解説はAPI Documentや、AutoPlugin登場時の解説記事を読むと分かりやすい。

http://www.scala-sbt.org/1.0.2/api/sbt/AutoPlugin.html

http://eed3si9n.com/ja/sbt-preview-auto-plugins

project/plugin.sbtをアップデートする

project/plugin.sbtに書かれているsbt pluginのバージョンをsbt 1.0対応したものへアップデートする。

sbt 1.0への対応状況はsbtのWikiを参照すると良い。きちんとアップデートされている。

github.com

ここで一つでも使用しているsbt pluginがsbt 1.0対応できていなければ1.0対応のコンパイルが通らないため、先にそちらのpluginをアップデートするか、issue上げて直してもらうか、PR送るか、という3択となる(もしくは使うのを止める)。

plugin間の依存関係の定義

AutoPluginでは、Plugin同士の依存関係を定義できる。

例えばsbt-scalatraのScalatraPluginは、xsbt-web-pluginJettyPluginに依存していて、ScalatraPluginを有効にすれば自動的にJettyPluginも有効になる(AutoPlugin化される前は明示的に別々に有効にする必要が有った)。

依存するPluginは、project/plugin.sbtではなくbuild.sbtに定義する。その際addSbtPluginではなく、libraryDependenciesを使う。

sbtの複数バージョンに対応させるため、build.sbtに以下のように定義する(xsbt-web-pluginに依存する場合の指定例です)。sbtとScalaのバージョンがターゲットのsbtのバージョンによって自動的に選択される。

libraryDependencies += {
  Defaults.sbtPluginExtra(
    "com.earldouglas" % "xsbt-web-plugin" % "4.0.0",
    (sbtBinaryVersion in pluginCrossBuild).value,
    (scalaBinaryVersion in pluginCrossBuild).value
  )
},

なぜかsbtのドキュメントにはsbtPluginExtraのことが書かれていないが、以下のブログを読むと構造が分かる。

始める sbt - プラグインの使用

実際のplugin間の依存関係は、以下のようにrequiresをオーバーライドすることで定義する。

import sbt._
object ScalatraPlugin extends AutoPlugin {
  override def requires = JettyPlugin

複数のsbtのバージョンに対応させる

当面sbt 0.13も利用され続けると思われるので、0.13系と1.0系の両方に対応させるためには、build.sbtにcrossSbtVersionsを定義し、クロスビルドを有効にする。

例えば以下のように記述すれば、0.13.16と、1.0.2に対応したバイナリを作成する。

crossSbtVersions := Seq("0.13.16", "1.0.2"),

普通にcompileするとproject/build.propertiesのバージョンを参照するので、^comile^scriptedのように先頭に'^'を付けてコマンドを実行する。

project/build.propertiesに指定するバージョンは、crossSbtVersionsに指定したバージョンのうち、どちらかと必ず合わせた方が良い。そうしないと無駄に色々なバージョンのsbtをダウンロードすることになる。

pluginを自動で有効にする

下記のように定義すると、Pluginは自動的に有効になる。

override def trigger = allRequirements

triggerをオーバライドしない場合、build.sbtの中で明示的にpluginを有効にする。

enablePlugins(ScalatraPlugin)

例えば一つのbuild.sbtに複数のサブプロジェクトが有り、特定のサブプロジェクトでしか有効にする必要が無いPluginであれば明示的に有効させるべきだし、ソースコードフォーマッターのようにプロジェクト横断的に使うものであれば明示的に有効にさせる必要は無いかもしれない。その辺りはユースケース次第だが、どちらにしてもドキュメントには明示した方が良い。

settingsの対応

AutoPluginより前はpluginごとに色々な設定名が使われていたが、AutoPlugin化以降は、MyPlugin.projectSettingsみたいにpluginのclass名に続けてprojectSettings又はbuildSettingsという名称で統一された。

互換性のため、既存のsettingsの名称を変える必要は無いが、以下のようにprojectSettingsに代入しておくことでprojectSettings形式で使えるようにする方が良い。

override lazy val projectSettings = scalatraSettings

またsettingsを使って有効にしたいtaskKeyやsettingKeyはautoImportに定義する。

deprecatedなメソッドの書き換え

sbt 0.13でdeprecatedになっているメソッドはsbt 1.0では削除されている。そのため下記のドキュメントを参考にソースコードを書き換える。

sbt Reference Manual — Migrating from sbt 0.12.x

互換性の関係からどうしても一つのソースコードにまとめられない場合は、それぞれのsbtのバージョンに対応したディレクトリを用意することで、sbtのバージョン別ソースコードを用意することができる(sbtのAPIが変わったものや、Scala 2.10と2.12の非互換など)。

src/main/scala-sbt-0.13/

src/main/scala-sbt-1.0/

具体的なコード例は、sbt-scalatraの例を参考にすると良い。

github.com

おわりに

一つ一つの作業は単純だけど、特にsbt 1.0で内部構造が大きく変わっているものなど、クラスの階層構造が変わっていたり、別リポジトリへ移っていたりと、意外と移動先を探すのが大変だった。

そこはひたすらsbt自体のソースを見るしかない。あとsbt自体のドキュメントがかなりの量あるので、それを読み込むのがけっこう大変だった。

とりあえずsbt 1.0にアップデートしないとJava9で動作しないので、前に進むしかない。ソフトウェアエンジニアに後退する、という選択肢は無いので進もう!