插件

Note

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

描述

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

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

创建插件

概念上,可将 sbt 插件视为「metabuild 上的普通库」,当前 metabuild 使用 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 与 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.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

常见情况是使用发布到仓库的二进制插件。您可创建 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 等即可。