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 シェル内では、ユーザーは設定を同じ方法で参照できます

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

キーの Scala 識別子とシェルがケバブケースを使用する sbt 0.12 スタイルのキー名は避けてください

  • 悪い例: `val jarName = SettingKey[String]("assembly-jar-name")`
  • 悪い例: `val jarName = SettingKey[String]("jar-name")`
  • 良い例: `val assemblyJarName = taskKey[String]("fat jar の名前")`

`build.sbt` と sbt シェルの両方でキーに単一のネームスペースがあるため、異なるプラグインが `jarName` や `excludedFiles` のような汎用的なキー名を使用すると、名前の競合が発生します。

アーティファクト命名規則 

ライブラリとアーティファクトに名前を付けるには、`sbt-$projectname` スキームを使用します。一貫した命名規則を持つプラグインエコシステムにより、ユーザーはプロジェクトまたは依存関係が SBT プラグインかどうかを簡単に判断できます。

プロジェクトの名前が `foobar` の場合、以下が成り立ちます

  • 悪い例: `foobar`
  • 悪い例: `foobar-sbt`
  • 悪い例: `sbt-foobar-plugin`
  • 良い例: `sbt-foobar`

プラグインが明白な「メイン」タスクを提供する場合、sbt シェルとタブ補完内でプラグインの機能をより直感的に探求できるように、`foobar` または `foobar...` という名前を付けることを検討してください。

(オプション) プラグイン命名規則 

プラグインに `FooBarPlugin` という名前を付けます。

デフォルトパッケージを使用しない 

ビルドファイルが何らかのパッケージにあるユーザーは、デフォルト (名前なし) パッケージで定義されている場合、プラグインを使用できません。

プラグインを周知させる 

人々があなたのプラグインを見つけられるようにしてください。推奨される手順を以下に示します

  1. アナウンスで @scala_sbt について言及してください。RT します。
  2. sbt/website にプルリクエストを送信し、プラグインリストにプラグインを追加します。

既存のキーを再利用する 

sbt には多数の定義済みキーがあります。可能な場合は、プラグインでそれらを再利用してください。たとえば、以下を定義しないでください

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

代わりに、sbt の既存の `sources` キーを再利用します。

設定とタスクを使用する. コマンドは避ける。 

プラグインは、sbt エコシステムの他の部分に自然に適合する必要があります。最初にできることは、コマンドの定義を避け、代わりに設定とタスク、およびタスクスコープを使用することです (タスクスコープの詳細については以下を参照)。`compile`、`test`、`publish` など、sbt のほとんどの興味深いものはタスクを使用して提供されます。タスクは、タスクエンジンによる重複削減と並列実行を活用できます。ScopeFilterなどの機能により、以前はコマンドが必要だった多くの機能がタスクを使用して可能になりました。

設定は他の設定とタスクから構成できます。タスクは他のタスクと入力タスクから構成できます。一方、コマンドは上記のいずれからも構成できません. 一般に、必要な最小限のものを使用してください。コマンドの正当な使用法の 1 つは、コードではなくビルド定義自体にアクセスするためのプラグインを使用することです。 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` キーは `Compile` および `Test` 設定と同様に再利用できますが、`Fuzz` 設定にスコープが設定された `scalaSource` ( `scalaSource in Fuzz` と表記) は `src/fuzz/scala` を指すことができるため、他の Scala ソースとは異なりますディレクトリ。したがって、これら 3 つの定義は同じ*キー*を使用しますが、異なる*値*を表します。したがって、ユーザーの `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` でデフォルト値を提供する 

設定またはタスクのデフォルト値がプロジェクトレベルの設定 (`baseDirectory`、`compile` など) に推移的に依存していない場合は、`globalSettings` で定義します.

例えば、sbt.Defaults において、`licenses`、`developers`、`scmInfo` などの公開関連のキーはすべて `Global` スコープで定義されており、通常は `Nil` や `None` などの空の値に設定されています。

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
  }
}

上記では、`obfuscateOption` は `globalSettings` でデフォルトの仮の値が設定されていますが、`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 ...
    }
  )
}