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]("fat jar 的名稱")

因為 build.sbt 和 sbt Shell 中都有單一金鑰命名空間,如果不同的外掛使用類似 jarNameexcludedFiles 的通用金鑰名稱,將會造成名稱衝突。

成品命名慣例 

使用 sbt-$專案名稱 方案來命名您的函式庫和成品。具有一致命名慣例的外掛生態系統讓使用者更容易判斷專案或依賴是否為 SBT 外掛。

如果專案名稱為 foobar,則以下成立

  • 錯誤:foobar
  • 錯誤:foobar-sbt
  • 錯誤:sbt-foobar-plugin
  • 正確:sbt-foobar

如果您的外掛提供明顯的「主要」任務,請考慮將其命名為 foobarfoobar...,讓使用者更容易在 sbt Shell 和 Tab 鍵自動完成中探索您外掛的功能。

(選用)外掛命名慣例 

將您的外掛命名為 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
  )
}

何時定義自己的組態 

如果您的外掛引入一組新的原始程式碼或其自己的函式庫依賴,那麼您才需要自己的組態。例如,假設您建立了一個外掛,可以執行模糊測試,這需要其自己的模糊測試函式庫和模糊測試原始程式碼。scalaSource 金鑰可以重複使用,類似於 CompileTest 組態,但範圍設定為 Fuzz 組態(表示為 scalaSource in Fuzz)的 scalaSource 可以指向 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) 的形式使用。這讓使用者可以在特定子專案層級設定 obfuscate / obfuscateOption,或者將作用域設為 ThisBuild 以影響所有子專案。

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

在全域作用域中為鍵提供預設值時,必須知道用於定義該鍵的每個鍵(如果有)也必須定義在全域作用域中,否則將在載入時失敗。

為設定使用「主要」任務作用域 

有時候,您希望為外掛程式中的特定「主要」任務定義一些設定。在這種情況下,您可以使用任務本身來設定設定的作用域。請參閱 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 ...
    }
  )
}