插件

Note

本页面向中级用户介绍插件细节。 插件系统的一般介绍请参阅插件基础

描述

插件用于扩展构建定义,常用来增加内置设置和任务之外的能力。示例用途包括构建 Docker 容器、支持 Scala.JS、生成 GitHub Actions YAML 等。

插件定义一系列 sbt 设置,可自动加入所有项目,或为选定项目显式声明。插件也可定义新命令。sbt 1.x 与 2.x 中的插件扩展 sbt.AutoPlugin trait,可自动处理依赖插件之间的设置顺序。

创建插件

Conceptually we can think of the sbt plugins to be any other libraries on the metabuild, which is currently on Scala 3.8.4. However, in practice modern sbt plugins are built around auto plugin (sbt.AutoPlugin), which provides tasks and settings in the correct order. See the Plugin dependencies section for more details.

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 与 lazy val

在 Scala 中,可用 lazy val 重写 def 方法,这称为统一访问原则。 建议对 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 列为其必需插件之一。因此,若定义如下 build.sbt

lazy val root = rootProject
  .enablePlugins(PlayScala)

SbtLessPlugin 的设置序列会自动追加在 PlayScala 的设置之后某处。

这使插件可在不打扰用户的前提下正确扩展现有插件。也有助于减轻用户的排序负担,让插件作者在向用户提供功能时有更大自由度。

使用 autoImport 控制导入

当 auto plugin 提供名为 autoImportvallazy valobject 时,该字段的内容会在 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 用于提供新的键——SettingKeyTaskKeyInputKey——或核心方法,而无需显式 import 或限定名。

示例插件

典型插件示例:

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.1" // 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

常见情况是使用发布到仓库的二进制插件。您可创建 project/plugins.sbt,在其中声明所需的 sbt 插件、一般依赖及必要的仓库:

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

// plain library (not an sbt plugin) for use in the build definition
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"
  )

Metabuild

metabuild 是 project/ 目录下的项目。该项目的 classpath 用作 project/ 中构建定义以及项目根目录下任意 .sbt 文件的 classpath,也用于 evalset 命令。

具体而言:

  1. metabuild 声明的托管依赖会被解析并出现在构建定义 classpath 上,与普通项目相同。
  2. project/lib/ 中的非托管依赖可用于构建定义,与普通项目相同。
  3. project/ 项目中的源码即构建定义文件,使用由托管与非托管依赖构成的 classpath 编译。
  4. 可在 project/plugins.sbt 中声明项目依赖(方式类似普通项目中的 build.sbt),这些依赖将对构建定义可用。

会在构建定义 classpath 上查找 sbt/sbt.autoplugins 描述文件,其中包含 sbt.AutoPlugin 实现类名。

reload plugins 命令将当前构建切换为(根)项目的 metabuild 定义,从而像普通子项目一样操作 metabuild。reload return 切换回原始构建。未保存的 metabuild 会话设置会被丢弃。

auto plugin 是定义可自动注入子项目的设置的模块。此外,auto plugin 还提供以下能力:

  • 将选定名称自动导入 .sbt 文件以及 evalset 命令。
  • 指定对其他 auto plugin 的依赖。
  • 在全部依赖满足时自动激活自身。
  • 按需指定 projectSettingsbuildSettingsglobalSettings

插件依赖

在 sbt 0.13.5 引入 AutoPlugin 系统之前,若插件依赖另一插件的设置和任务,构建用户必须按正确顺序包含插件。随着应用中插件增多,这变得复杂且易错。

auto plugin 可依赖其他 auto plugin,并确保这些依赖的设置先被加载。

假设有 SbtLessPluginSbtCoffeeScriptPlugin,后者又依赖 SbtJsTaskPluginSbtWebPluginJvmPlugin。无需手动激活所有这些插件,项目只需启用 SbtLessPluginSbtCoffeeScriptPlugin

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

这将按正确顺序从各插件拉入正确的设置序列。

Maven 发布约定

sbt 2.x 插件发布到 Maven 布局仓库时,工件名带有 _sbt2_3 后缀。例如 sbt-ci-release 1.11.2 发布至中央仓库 https://repo1.maven.org/maven2/com/github/sbt/sbt-ci-release_sbt2_3/1.11.2/。该后缀由 sbt 自动追加,因此在 build.sbtname 只需写 sbt-ci-release 等即可。