プラグイン

Note

このページでは中級者向けにプラグインの詳細を解説する。 プラグインシステムの概要はプラグインの基本を参照。

説明

プラグインはビルド定義を拡張する手段であり、組み込みのセッティングやタスクではできない機能を追加するのに使われる。例としては Docker コンテナの構築、Scala.JS のサポート、GitHub Actions の YAML 生成などがある。

プラグインは sbt のセッティングの列を定義し、すべてのプロジェクトに自動的に追加するか、選択したプロジェクトに明示的に宣言する。プラグインは新しいコマンドも定義できる。sbt 1.x および 2.x のプラグインは sbt.AutoPlugin トレイトを継承し、依存プラグイン間のセッティングの順序を自動化する。

プラグインの作成

概念的には、sbt プラグインは「メタビルド (現在は Scala 3.8.2) 上にある何の変哲もないライブラリ」と考えられる。しかし実際には、現代の sbt プラグインは auto plugin(sbt.AutoPlugin)を中心に構築され、正しい順序でタスクとセッティングを提供する。詳細はプラグイン依存性の節を参照。

build.sbt の設定

auto plugin を作るには、プロジェクトを作成し SbtPlugin を有効にする。

build.sbt

version := "0.1.0-SNAPSHOT"
organization := "com.example"
homepage := Some(url("https://github.com/sbt/sbt-hello"))

lazy val root = rootProject
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-hello",
    pluginCrossBuild / sbtVersion := {
      scalaBinaryVersion.value match
        case "3"    => "2.0.0-RC10" // set minimum sbt version
    }
  )

Note

  • sbt 2.x のプラグインは Scala 3.x でコンパイルする必要がある。scalaVersion を指定しない場合、sbt はプラグインに適した Scala バージョンをデフォルトとする。
  • pluginCrossBuild / sbtVersion は、プラグインを より古い sbt バージョン向けにコンパイルするためのセッティングであり、プラグイン利用者が sbt のバージョン範囲から選べるようにする。

projectSettings

適切な名前空間で、sbt.AutoPlugin を継承して auto plugin オブジェクトを定義する。auto plugin では、セッティングはプラグインが projectSettings メソッドを通じて直接提供する。サブプロジェクトに hello という名前のタスクを追加する例を示す:

src/main/sbthello/HelloPlugin.scala

package sbthello

import sbt.*
import Keys.*

object HelloPlugin extends AutoPlugin:
  override def trigger = allRequirements

  object autoImport:
    val helloGreeting = settingKey[String]("greeting")
    val hello = taskKey[Unit]("say hello")
  end autoImport

  import autoImport.*
  override lazy val globalSettings: Seq[Setting[?]] = Seq(
    helloGreeting := "hi",
  )

  override lazy val projectSettings: Seq[Setting[?]] = Seq(
    hello := {
      val s = streams.value
      val g = helloGreeting.value
      s.log.info(g)
    }
  )
end HelloPlugin

def vs lazy val

Scala では、def メソッドは統一アクセス原理と呼ばれる仕組みで lazy val によりオーバーライドできる。 projectSettings には lazy val を使うことを推奨する。セッティング定義は不変であることが多いためである。

globalSettings

globalSettings はグローバルなセッティングに一度だけ追加される(例: Global / helloGreeting)。これによりプラグインは新しい機能や新しいデフォルトを提供できる。主な用途の一つは、IDE プラグインのようにコマンドをグローバルに追加することである。

override lazy val globalSettings: Seq[Setting[?]] = Nil

可能な限り、プラグインは globalSettings でデフォルト値を定義すべきである。値を最も広いスコープで提供し、最も狭いスコープで使用すると、ユーザーがセッティングを変更できる自由度が最大になる。

プラグイン依存性の実装

次にプラグイン依存性を定義する。

package sbtless

import sbt.*
import Keys.*
object SbtLessPlugin extends AutoPlugin:
  override def requires = SbtJsTaskPlugin
  override lazy val projectSettings = ...
end SbtLessPlugin

requires メソッドは Plugins 型の値を返し、依存リストを構築する DSL である。requires メソッドには通常、次のいずれかが含まれる:

  • empty(プラグインなし)
  • 他の auto plugin
  • && 演算子(複数の依存を定義する)

ルートプラグインとトリガー付きプラグイン

一部のプラグインは常にサブプロジェクトで明示的に有効化される必要がある。これらをルートプラグインと呼ぶ。プラグイン依存グラフの「ルート」ノードとなるからだ。

auto plugin は、依存が満たされるサブプロジェクトに自動的に発動される方法も提供する。これらをトリガー付きプラグインと呼び、trigger メソッドをオーバーライドして作成する。

例えば、ビルドにコマンドを自動的に追加できるトリガー付きプラグインを作りたい場合がある。そうするには、requires メソッドが empty を返すようにし、trigger メソッドを allRequirements でオーバーライドする。

package sbthello

import sbt.*
import Keys.*

object HelloPlugin2 extends AutoPlugin:
  override def trigger = allRequirements

  ....
end HelloPlugin2

ビルドユーザーは、このプラグインを project/plugins.sbt に含める必要があるが、build.sbt に含める必要はなくなる。この機能はプラグイン依存性と組み合わせると真価を発揮する。SbtLessPlugin を別のプラグインに依存するよう修正してみる:

package sbtless

import sbt.*
import Keys.*

object SbtLessPlugin extends AutoPlugin:
  override def trigger = allRequirements
  override def requires = SbtJsTaskPlugin
  override lazy val projectSettings = ...
end SbtLessPlugin

実際、PlayScala プラグイン(Play フレームワークは sbt プラグインである)は SbtJsTaskPlugin をプラグイン依存性の 1つとして列挙している。したがって、次のような build.sbt を定義すると:

lazy val root = rootProject
  .enablePlugins(PlayScala)

SbtLessPlugin からのセッティング列が、PlayScala のセッティングの後のどこかに自動的に追加される。

これによりプラグインは、既存のプラグインを透過的に、かつ正しく、機能拡張することができる。またユーザーから手動でセッティング列の順序付けをする負担を取り除くことで、プラグイン作者は安心して機能拡張を提供することができる。

autoImport によるインポートの制御

auto plugin が autoImport という名前の vallazy val、または object を提供すると、そのフィールドの内容は seteval、および *.sbt ファイルでワイルドカードインポートされる。

package sbthello

import sbt.*
import Keys.*

object HelloPlugin3 extends AutoPlugin:
  object autoImport:
    val greeting = settingKey[String]("greeting")
    val hello = taskKey[Unit]("say hello")
  end autoImport

  import autoImport.*
  override def trigger = allRequirements
  ....
end HelloPlugin3

通常、autoImport は新しいキー(SettingKeyTaskKey、または InputKey)や、インポートや修飾なしで使えるコアメソッドを提供するために使われる。

プラグインの例

典型的なプラグインの例:

Plugin build.sbt

version := "0.1.0-SNAPSHOT"
organization := "com.example"
homepage := Some(url("https://github.com/sbt/sbt-obfuscate"))

lazy val root = rootProject
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-obfuscate",
    pluginCrossBuild / sbtVersion := {
      scalaBinaryVersion.value match
        case "3" => "2.0.0-RC10" // set minimum sbt version
    }
  )

src/main/scala/sbtobfuscate/ObfuscatePlugin.scala

package sbtobfuscate

import sbt.*
import sbt.Keys.*
import sbt.CacheImplicits.given
import xsbti.HashedVirtualFileRef

object ObfuscatePlugin extends AutoPlugin:
  // by defining autoImport, the settings are automatically
  // imported into user's `*.sbt`
  object autoImport:
    val obfuscate = taskKey[Seq[HashedVirtualFileRef]]("Obfuscates files.")

    // configuration points, like built-in `version` and `libraryDependencies`
    val obfuscateLiterals = settingKey[Boolean]("Obfuscate literals.")
  end autoImport

  import autoImport.*

  // This plugin is automatically enabled for projects which are JvmPlugin.
  override def trigger = allRequirements

  // default values for the settings
  override lazy val globalSettings: Seq[Def.Setting[?]] = Seq(
    obfuscateLiterals := false,
  )

  // default implementations for the tasks
  val baseObfuscateSettings: Seq[Def.Setting[?]] = Seq(
    obfuscate := {
      Obfuscate(sourcesVF.value, (obfuscate / obfuscateLiterals).value)
    },
  )

  // a group of settings that are automatically added to projects.
  override lazy val projectSettings: Seq[Def.Setting[?]] =
    inConfig(Compile)(baseObfuscateSettings) ++
    inConfig(Test)(baseObfuscateSettings)
end ObfuscatePlugin

object Obfuscate:
  def apply(
    sources: Seq[HashedVirtualFileRef],
    obfuscateLiterals: Boolean
  ): Seq[HashedVirtualFileRef] =
    // TODO obfuscate stuff!
    sources
end Obfuscate

使用例

プラグインを使うビルド定義は、例えば次のような build.sbt になる:

obfuscateLiterals := true

auto plugin の利用

よくあるのは、リポジトリに公開されたバイナリプラグインを使う場合である。必要な sbt プラグイン、一般的な依存、必要なリポジトリをすべて含む project/plugins.sbt を作成できる:

addSbtPlugin("org.example" % "plugin" % "1.0")
addSbtPlugin("org.example" % "another-plugin" % "2.0")

// ビルド定義で使う通常のライブラリ(sbt プラグインではない)
libraryDependencies += "org.example" % "utilities" % "1.3"

resolvers += "Example Plugin Repository" at "https://example.org/repo/"

多くの auto plugin はプロジェクトにセッティングを自動追加するが、明示的な有効化が必要なものもある。例を示す:

lazy val util = (project in file("util"))
  .enablePlugins(FooPlugin, BarPlugin)
  .disablePlugins(plugins.IvyPlugin)
  .settings(
    name := "hello-util"
  )

メタビルド

メタビルドは project/ フォルダ下のプロジェクトである。このプロジェクトのクラスパスは、project/ 内のビルド定義およびプロジェクトのベースディレクトリにある .sbt ファイルに使われる。eval および set コマンドにも使われる。

具体的には、

  1. メタビルドが宣言したマネージ依存性は取得され、通常のプロジェクトと同様にビルド定義のクラスパスで利用できる。
  2. project/lib/ のアンマネージ依存性は、通常のプロジェクトと同様にビルド定義で利用できる。
  3. project/ プロジェクト内のソースはビルド定義ファイルであり、マネージ依存性とアンマネージ依存性から構築したクラスパスでコンパイルされる。
  4. サブプロジェクト依存は project/plugins.sbt に宣言できる(通常プロジェクトの build.sbt と同様)し、ビルド定義から利用できる。

ビルド定義のクラスパスから sbt/sbt.autoplugins 記述子ファイルが検索され、sbt.AutoPlugin 実装の名前が含まれる。

reload plugins コマンドは現在のビルドを(ルート)プロジェクトのメタビルド定義に切り替える。これによりメタビルドプロジェクトを通常のサブプロジェクトのように操作できる。reload return は元のビルドに戻る。メタビルドプロジェクトのセッションで保存されていないセッティングは破棄される。

auto plugin は、サブプロジェクトに自動注入するセッティングを定義するモジュールである。さらに auto plugin は次の機能を提供する:

  • 選択した名前を .sbt ファイルおよび evalset コマンドに自動インポートする。
  • 他の auto plugin へのプラグイン依存を指定する。
  • すべての依存が揃うと自動的に自身を有効化する。
  • 必要に応じて projectSettingsbuildSettingsglobalSettings を指定する。

プラグイン依存性

sbt 0.13.5 で AutoPlugin システムが導入される前は、あるプラグインが別のプラグインのセッティングやタスクに依存する場合、ビルド利用者が正しい順序でプラグインを含める必要があった。アプリケーション内のプラグイン数が増えると複雑でエラーが起きやすくなった。

auto plugin は他の auto plugin に依存でき、これらの依存セッティングが先にロードされるようにできる。

SbtLessPlugin と、さらに SbtJsTaskPluginSbtWebPluginJvmPlugin に依存する SbtCoffeeScriptPlugin があるとする。これらすべてを手動で有効化する代わりに、プロジェクトは次のように SbtLessPluginSbtCoffeeScriptPlugin だけを有効化すればよい:

lazy val root = (project in file("."))
  .enablePlugins(SbtLessPlugin, SbtCoffeeScriptPlugin)

これにより、正しい順序でプラグインから適切なセッティング列が引き込まれる。

Maven 公開の慣例

sbt 2.x のプラグインは、成果物名に _sbt2_3 サフィックスを付けて Maven レイアウトのリポジトリに公開される。例えば sbt-ci-release 1.11.2 は Central Repo に https://repo1.maven.org/maven2/com/github/sbt/sbt-ci-release_sbt2_3/1.11.2/ として公開される。このサフィックスは sbt が自動的に付加するため、build.sbtnamesbt-ci-release などとだけ書けばよい。