1. 插件最佳实践

插件最佳实践 

此页面主要面向 sbt 插件作者。 此页面假定您已阅读使用插件插件.

插件开发者应努力实现一致性和易用性。具体而言

  • 插件应该与其他插件配合良好。避免命名空间冲突(在 sbt 和 Scala 中)至关重要。
  • 插件应该遵循一致的约定。无论引入什么插件,sbt 用户的体验都应该一致。

以下是一些当前的插件最佳实践。

注意: 最佳实践不断发展,因此请经常查看。

键命名约定:使用前缀 

有时,您需要一个新的键,因为没有现有的 sbt 键。在这种情况下,请使用特定于插件的前缀。

package sbtassembly

import sbt._, Keys._

object AssemblyPlugin extends AutoPlugin {
  object autoImport {
    val assembly                  = taskKey[File]("Builds a deployable fat jar.")
    val assembleArtifact          = settingKey[Boolean]("Enables (true) or disables (false) assembling an artifact.")
    val assemblyOption            = taskKey[AssemblyOption]("Configuration for making a deployable fat jar.")
    val assembledMappings         = taskKey[Seq[MappingSet]]("Keeps track of jar origins for each source.")

    val assemblyPackageScala      = taskKey[File]("Produces the scala artifact.")
    val assemblyJarName           = taskKey[String]("name of the fat jar")
    val assemblyMergeStrategy     = settingKey[String => MergeStrategy]("mapping from archive member path to merge strategy")
  }

  import autoImport._

  ....
}

在这种方法中,每个 val 都以 assembly 开头。插件用户可以在 build.sbt 中像这样引用设置

assembly / assemblyJarName := "something.jar"

在 sbt shell 中,用户可以通过相同的方式引用设置

sbt:helloworld> show assembly/assemblyJarName
[info] helloworld-assembly-0.1.0-SNAPSHOT.jar

避免使用 sbt 0.12 风格的键名,其中键的 Scala 标识符和 shell 使用连字符大小写

  • 错误:val jarName = SettingKey[String]("assembly-jar-name")
  • 错误:val jarName = SettingKey[String]("jar-name")
  • 正确:val assemblyJarName = taskKey[String]("name of the fat jar")

因为在 build.sbt 和 sbt shell 中,键都有一个单一的命名空间,如果不同的插件使用通用键名,例如 jarNameexcludedFiles,它们会导致命名冲突。

工件命名约定 

使用 sbt-$projectname 方案为您的库和工件命名。具有统一命名约定的插件生态系统使用户更容易区分项目或依赖项是否为 SBT 插件。

如果项目的名称是 foobar,则以下内容成立

  • 错误:foobar
  • 错误:foobar-sbt
  • 错误:sbt-foobar-plugin
  • 正确:sbt-foobar

如果您的插件提供了明显的“主”任务,请考虑将其命名为 foobarfoobar... 以便更直观地探索 sbt shell 和制表符补全中插件的功能。

(可选) 插件命名约定 

将您的插件命名为 FooBarPlugin

不要使用默认包 

如果构建文件位于某个包中,则用户将无法使用您的插件,因为它是在默认(无名)包中定义的。

让您的插件为人所知 

确保人们可以找到您的插件。以下是一些推荐步骤

  1. 在您的公告中提及@scala_sbt,我们会转发它。
  2. sbt/website 发送拉取请求,并在插件列表中添加您的插件。

重用现有键 

sbt 有许多预定义键。尽可能在您的插件中重用它们。例如,不要定义

val sourceFiles = settingKey[Seq[File]]("Some source files")

相反,请重用 sbt 现有的 sources 键。

使用设置和任务。避免使用命令。 

您的插件应该自然地融入 sbt 生态系统。您可以做的第一件事是避免定义命令,而使用设置和任务以及任务范围(有关任务范围的更多信息,请参见下文)。sbt 中的大多数有趣内容(如 compiletestpublish)都是使用任务提供的。任务可以通过任务引擎利用重复消除和并行执行。通过ScopeFilter 等功能,以前需要命令的许多功能现在可以使用任务实现。

设置可以从其他设置和任务中组合起来。任务可以从其他任务和输入任务中组合起来。另一方面,命令不能从以上任何一种中组合起来。一般来说,请使用您需要的最小东西。命令的一个合法用途可能是使用插件访问构建定义本身,而不是代码。sbt-inspectr 在成为 inspect tree 之前是使用命令实现的。

在普通的 Scala 对象中提供核心功能 

例如,sbt 的 package 任务的核心功能是在sbt.Package 中实现的,可以通过其 apply 方法调用。这使得可以从其他插件(如 sbt-assembly)中更多地重用该功能,sbt-assembly 反过来又实现了 sbtassembly.Assembly 对象来实现其核心功能。

请遵循他们的做法,并在普通的 Scala 对象中提供核心功能。

配置建议 

如果您的插件引入了新的源代码或其自己的库依赖项,那么您才需要自己的配置。

您可能不需要自己的配置 

配置应该用于为插件命名空间键。如果您只是添加任务和设置,请不要定义您自己的配置。相反,请重用现有配置按主任务范围(参见下文)。

package sbtwhatever

import sbt._, Keys._

object WhateverPlugin extends sbt.AutoPlugin {
  override def requires = plugins.JvmPlugin
  override def trigger = allRequirements

  object autoImport {
    // BAD sample
    lazy val Whatever = config("whatever") extend(Compile)
    lazy val specificKey = settingKey[String]("A plugin specific key")
  }
  import autoImport._
  override lazy val projectSettings = Seq(
    Whatever / specificKey := "another opinion" // DON'T DO THIS
  )
}

何时定义您自己的配置 

如果您的插件引入了新的源代码或其自己的库依赖项,那么您才需要自己的配置。例如,假设您构建了一个插件,该插件执行模糊测试,需要自己的模糊测试库和模糊测试源代码。可以像 CompileTest 配置一样重用 scalaSource 键,但 scalaSource 针对 Fuzz 配置的范围(表示为 scalaSource in Fuzz)可以指向 src/fuzz/scala,使其与其他 Scala 源目录不同。因此,这三个定义使用相同的,但它们代表不同的。因此,在用户的 build.sbt 中,我们可能会看到

Fuzz / scalaSource := baseDirectory.value / "source" / "fuzz" / "scala"

Compile / scalaSource := baseDirectory.value / "source" / "main" / "scala"

在模糊测试插件中,这是通过 inConfig 定义实现的

package sbtfuzz

import sbt._, Keys._

object FuzzPlugin extends sbt.AutoPlugin {
  override def requires = plugins.JvmPlugin
  override def trigger = allRequirements

  object autoImport {
    lazy val Fuzz = config("fuzz") extend(Compile)
  }
  import autoImport._

  lazy val baseFuzzSettings: Seq[Def.Setting[_]] = Seq(
    test := {
      println("fuzz test")
    }
  )
  override lazy val projectSettings = inConfig(Fuzz)(baseFuzzSettings)
}

定义新类型配置时,例如

lazy val Fuzz = config("fuzz") extend(Compile)

应该用于创建配置。配置实际上与依赖项解析(使用 Ivy)相关联,并且可以更改生成的 pom 文件。

与配置配合使用 

无论您是否附带配置,插件都应努力支持多种配置,包括构建用户创建的配置。某些与特定配置绑定的任务可以在其他配置中重复使用。虽然您可能不会立即在您的插件中看到这种需求,但一些项目可能会要求您具有这种灵活性。

提供原始设置和配置设置 

按配置轴将您的设置拆分为以下部分

package sbtobfuscate

import sbt._, Keys._

object ObfuscatePlugin extends sbt.AutoPlugin {
  override def requires = plugins.JvmPlugin
  override def trigger = allRequirements

  object autoImport {
    lazy val obfuscate = taskKey[Seq[File]]("obfuscate the source")
    lazy val obfuscateStylesheet = settingKey[File]("obfuscate stylesheet")
  }
  import autoImport._
  lazy val baseObfuscateSettings: Seq[Def.Setting[_]] = Seq(
    obfuscate := Obfuscate((obfuscate / sources).value),
    obfuscate / sources := sources.value
  )
  override lazy val projectSettings = inConfig(Compile)(baseObfuscateSettings)
}

// core feature implemented here
object Obfuscate {
  def apply(sources: Seq[File]): Seq[File] = {
    sources
  }
}

baseObfuscateSettings 值为插件的任务提供基本配置。如果项目需要,可以在其他配置中重复使用。obfuscateSettings 值为项目提供默认的 Compile 范围设置,可以直接使用。这为使用插件提供的功能提供了最大的灵活性。以下是原始设置的重复使用方式

import sbtobfuscate.ObfuscatePlugin

lazy val app = (project in file("app"))
  .settings(inConfig(Test)(ObfuscatePlugin.baseObfuscateSettings))

范围建议 

一般来说,如果插件使用最宽的范围提供键(设置和任务),并使用最窄的范围引用它们,它将为构建用户提供最大的灵活性。

globalSettings 中提供默认值 

如果您的设置或任务的默认值不依赖于项目级设置(例如 baseDirectorycompile 等),请在 globalSettings 中定义它。

例如,在 sbt.Defaults 中,与发布相关的键,例如 licensesdevelopersscmInfo,都在 Global 范围内定义,通常为 NilNone 之类的空值。

package sbtobfuscate

import sbt._, Keys._

object ObfuscatePlugin extends sbt.AutoPlugin {
  override def requires = plugins.JvmPlugin
  override def trigger = allRequirements

  object autoImport {
    lazy val obfuscate = taskKey[Seq[File]]("obfuscate the source")
    lazy val obfuscateOption = settingKey[ObfuscateOption]("options to configure obfuscate")
  }
  import autoImport._
  override lazy val globalSettings = Seq(
    obfuscateOption := ObfuscateOption()
  )

  override lazy val projectSettings = inConfig(Compile)(
    obfuscate := {
      Obfuscate(
        (obfuscate / sources).value,
        (obfuscate / obfuscateOption).value
      )
    },
    obfuscate / sources := sources.value
  )
}

// core feature implemented here
object Obfuscate {
  def apply(sources: Seq[File], opt: ObfuscateOption): Seq[File] = {
    sources
  }
}

在上面的示例中,obfuscateOptionglobalSettings 中被设置为一个默认的虚构值;但在 projectSettings 中被用作 (obfuscate / obfuscateOption)。这允许用户在特定子项目级别或作用域为 ThisBuild(影响所有子项目)的情况下设置 obfuscate / obfuscateOption

ThisBuild / obfuscate / obfuscateOption := ObfuscateOption().withX(true)

在全局范围内为键设置默认值需要知道,用于定义该键的每个键(如果有)也必须在全局范围内定义,否则它将在加载时失败。

使用“main”任务范围进行设置 

有时您想为插件中的特定“main”任务定义一些设置。在这种情况下,您可以使用任务本身对设置进行作用域。请参阅 baseObfuscateSettings

  lazy val baseObfuscateSettings: Seq[Def.Setting[_]] = Seq(
    obfuscate := Obfuscate((obfuscate / sources).value),
    obfuscate / sources := sources.value
  )

在上面的示例中,obfuscate / sources 在主任务 obfuscate 下进行作用域。

globalSettings 中重新连接现有键 

有时您可能需要在 globalSettings 中重新连接现有键。一般规则是小心您所触碰的内容

应注意确保来自其他插件的先前设置不被忽略。例如,在创建新的 onLoad 处理程序时,请确保先前的 onLoad 处理程序未被删除。

package sbtsomething

import sbt._, Keys._

object MyPlugin extends AutoPlugin {
  override def requires = plugins.JvmPlugin
  override def trigger = allRequirements

  override val globalSettings: Seq[Def.Setting[_]] = Seq(
    Global / onLoad := (Global / onLoad).value andThen { state =>
      ... return new state ...
    }
  )
}