mirror of https://github.com/sbt/sbt.git
Merge pull request #1203 from eed3si9n/topic/requiredAutoPlugin
Unifies AutoPlugin and RootPlugin
This commit is contained in:
commit
4315049337
|
|
@ -100,7 +100,7 @@ final class DetectedPlugins(val plugins: DetectedModules[Plugin], val autoImport
|
|||
lazy val imports: Seq[String] = BuildUtil.getImports(plugins.names ++ builds.names ++ autoImports.names)
|
||||
|
||||
/** A function to select the right [[AutoPlugin]]s from [[autoPlugins]] for a [[Project]]. */
|
||||
lazy val compilePlugins: Plugins => Seq[AutoPlugin] = Plugins.compile(autoPlugins.values.toList)
|
||||
lazy val deducePlugins: (Plugins, Logger) => Seq[AutoPlugin] = Plugins.deducer(autoPlugins.values.toList)
|
||||
}
|
||||
|
||||
/** The built and loaded build definition project.
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ object Load
|
|||
val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase))
|
||||
|
||||
val memoSettings = new mutable.HashMap[File, LoadedSbtFile]
|
||||
def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings)
|
||||
def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings, config.log)
|
||||
val loadedProjectsRaw = loadProjects(initialProjects)
|
||||
val hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined)
|
||||
val (loadedProjects, defaultBuildIfNone) =
|
||||
|
|
@ -457,13 +457,14 @@ object Load
|
|||
private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] =
|
||||
b.projectDefinitions(base).map(resolveBase(base))
|
||||
|
||||
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings, acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile]): Seq[Project] =
|
||||
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings,
|
||||
acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile], log: Logger): Seq[Project] =
|
||||
{
|
||||
def loadSbtFiles(auto: AddSettings, base: File, autoPlugins: Seq[AutoPlugin], projectSettings: Seq[Setting[_]]): LoadedSbtFile =
|
||||
loadSettings(auto, base, plugins, eval, injectSettings, memoSettings, autoPlugins, projectSettings)
|
||||
def loadForProjects = newProjects map { project =>
|
||||
val autoPlugins =
|
||||
try plugins.detected.compilePlugins(project.plugins)
|
||||
try plugins.detected.deducePlugins(project.plugins, log)
|
||||
catch { case e: AutoPluginException => throw translateAutoPluginException(e, project) }
|
||||
val autoConfigs = autoPlugins.flatMap(_.projectConfigurations)
|
||||
val loadedSbtFiles = loadSbtFiles(project.auto, project.base, autoPlugins, project.settings)
|
||||
|
|
@ -483,7 +484,7 @@ object Load
|
|||
if(nextProjects.isEmpty)
|
||||
loadedProjects
|
||||
else
|
||||
loadTransitive(nextProjects, buildBase, plugins, eval, injectSettings, loadedProjects, memoSettings)
|
||||
loadTransitive(nextProjects, buildBase, plugins, eval, injectSettings, loadedProjects, memoSettings, log)
|
||||
}
|
||||
private[this] def translateAutoPluginException(e: AutoPluginException, project: Project): AutoPluginException =
|
||||
e.withPrefix(s"Error determining plugins for project '${project.id}' in ${project.base}:\n")
|
||||
|
|
|
|||
|
|
@ -9,23 +9,26 @@ TODO:
|
|||
import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException}
|
||||
import Def.Setting
|
||||
import Plugins._
|
||||
import annotation.tailrec
|
||||
|
||||
/** Marks a top-level object so that sbt will wildcard import it for .sbt files, `consoleProject`, and `set`. */
|
||||
trait AutoImport
|
||||
|
||||
/**
|
||||
An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation").
|
||||
The `select` method defines the conditions and a method like `projectSettings` defines the settings to add.
|
||||
The `requires` and `trigger` methods together define the conditions, and a method like `projectSettings` defines the settings to add.
|
||||
|
||||
Steps for plugin authors:
|
||||
1. Determine the [[AutoPlugins]]s that, when present (or absent), activate the AutoPlugin.
|
||||
2. Determine the settings/configurations to automatically inject when activated.
|
||||
1. Determine if the AutoPlugin should automatically be activated when all requirements are met, or should be opt-in.
|
||||
2. Determine the [[AutoPlugins]]s that, when present (or absent), act as the requirements for the AutoPlugin.
|
||||
3. Determine the settings/configurations to that the AutoPlugin injects when activated.
|
||||
|
||||
For example, the following will automatically add the settings in `projectSettings`
|
||||
to a project that has both the `Web` and `Javascript` plugins enabled.
|
||||
|
||||
object MyPlugin extends AutoPlugin {
|
||||
def select = Web && Javascript
|
||||
def requires = Web && Javascript
|
||||
def trigger = allRequirements
|
||||
override def projectSettings = Seq(...)
|
||||
}
|
||||
|
||||
|
|
@ -44,17 +47,25 @@ will activate `MyPlugin` defined above and have its settings automatically added
|
|||
|
||||
then the `MyPlugin` settings (and anything that activates only when `MyPlugin` is activated) will not be added.
|
||||
*/
|
||||
abstract class AutoPlugin extends Plugins.Basic
|
||||
abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions
|
||||
{
|
||||
/** This AutoPlugin will be activated for a project when the [[Plugins]] matcher returned by this method matches that project's plugins
|
||||
* AND the user does not explicitly exclude the Plugin returned by `provides`.
|
||||
*
|
||||
* For example, if this method returns `Web && Javascript`, this plugin instance will only be added
|
||||
* if the `Web` and `Javascript` plugins are enabled. */
|
||||
def select: Plugins
|
||||
/** Determines whether this AutoPlugin will be activated for this project when the `requires` clause is satisfied.
|
||||
*
|
||||
* When this method returns `allRequirements`, and `requires` method returns `Web && Javascript`, this plugin
|
||||
* instance will be added automatically if the `Web` and `Javascript` plugins are enbled.
|
||||
*
|
||||
* When this method returns `noTrigger`, and `requires` method returns `Web && Javascript`, this plugin
|
||||
* instance will be added only if the build user enables it, but it will automatically add both `Web` and `Javascript`. */
|
||||
def trigger: PluginTrigger
|
||||
|
||||
/** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]].
|
||||
*/
|
||||
def requires: Plugins
|
||||
|
||||
val label: String = getClass.getName.stripSuffix("$")
|
||||
|
||||
override def toString: String = label
|
||||
|
||||
/** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/
|
||||
def projectConfigurations: Seq[Configuration] = Nil
|
||||
|
||||
|
|
@ -71,25 +82,19 @@ abstract class AutoPlugin extends Plugins.Basic
|
|||
|
||||
// TODO?: def commands: Seq[Command]
|
||||
|
||||
def unary_! : Exclude = Exclude(this)
|
||||
private[sbt] def unary_! : Exclude = Exclude(this)
|
||||
|
||||
|
||||
/** If this plugin requries itself to be included, it means we're actually a nature,
|
||||
* not a normal plugin. The user must specifically enable this plugin
|
||||
* but other plugins can rely on its existence.
|
||||
*/
|
||||
final def isRoot: Boolean =
|
||||
this match {
|
||||
case _: RootAutoPlugin => true
|
||||
/** If this plugin does not have any requirements, it means it is actually a root plugin. */
|
||||
private[sbt] final def isRoot: Boolean =
|
||||
requires match {
|
||||
case Empty => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A root AutoPlugin is a plugin which must be explicitly enabled by users in their `addPlugins` method
|
||||
* on a project. However, RootAutoPlugins represent the "root" of a tree of dependent auto-plugins.
|
||||
*/
|
||||
abstract class RootAutoPlugin extends AutoPlugin {
|
||||
final def select: Plugins = this
|
||||
|
||||
/** If this plugin does not have any requirements, it means it is actually a root plugin. */
|
||||
private[sbt] final def isAlwaysEnabled: Boolean =
|
||||
isRoot && (trigger == AllRequirements)
|
||||
}
|
||||
|
||||
/** An error that occurs when auto-plugins aren't configured properly.
|
||||
|
|
@ -105,39 +110,91 @@ object AutoPluginException
|
|||
def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Plugins.translateMessage(origin), Some(origin))
|
||||
}
|
||||
|
||||
sealed trait PluginTrigger
|
||||
case object AllRequirements extends PluginTrigger
|
||||
case object NoTrigger extends PluginTrigger
|
||||
|
||||
/** An expression that matches `AutoPlugin`s. */
|
||||
sealed trait Plugins {
|
||||
def && (o: Basic): Plugins
|
||||
}
|
||||
|
||||
object Plugins
|
||||
|
||||
sealed trait PluginsFunctions
|
||||
{
|
||||
/** [[Plugins]] instance that doesn't require any [[Plugins]]s. */
|
||||
def empty: Plugins = Plugins.Empty
|
||||
|
||||
/** This plugin is activated when all required plugins are present. */
|
||||
def allRequirements: PluginTrigger = AllRequirements
|
||||
/** This plugin is activated only when it is manually activated. */
|
||||
def noTrigger: PluginTrigger = NoTrigger
|
||||
}
|
||||
|
||||
object Plugins extends PluginsFunctions
|
||||
{
|
||||
/** Given the available auto plugins `defined`, returns a function that selects [[AutoPlugin]]s for the provided [[AutoPlugin]]s.
|
||||
* The [[AutoPlugin]]s are topologically sorted so that a selected [[AutoPlugin]] comes before its selecting [[AutoPlugin]].*/
|
||||
def compile(defined: List[AutoPlugin]): Plugins => Seq[AutoPlugin] =
|
||||
if(defined.isEmpty)
|
||||
Types.const(Nil)
|
||||
* The [[AutoPlugin]]s are topologically sorted so that a required [[AutoPlugin]] comes before its requiring [[AutoPlugin]].*/
|
||||
def deducer(defined0: List[AutoPlugin]): (Plugins, Logger) => Seq[AutoPlugin] =
|
||||
if(defined0.isEmpty) (_, _) => Nil
|
||||
else
|
||||
{
|
||||
val byAtom = defined.map(x => (Atom(x.label), x))
|
||||
// TODO: defined should return all the plugins
|
||||
val allReqs = (defined0 flatMap { asRequirements }).toSet
|
||||
val diff = allReqs diff defined0.toSet
|
||||
val defined = if (!diff.isEmpty) diff.toList ::: defined0
|
||||
else defined0
|
||||
|
||||
val byAtom = defined map { x => (Atom(x.label), x) }
|
||||
val byAtomMap = byAtom.toMap
|
||||
if(byAtom.size != byAtomMap.size) duplicateProvidesError(byAtom)
|
||||
// Ignore clauses for plugins that just require themselves be specified.
|
||||
// Ignore clauses for plugins that does not require anything else.
|
||||
// Avoids the requirement for pure Nature strings *and* possible
|
||||
// circular dependencies in the logic.
|
||||
val clauses = Clauses( defined.filterNot(_.isRoot).map(d => asClause(d)) )
|
||||
requestedPlugins =>
|
||||
Logic.reduce(clauses, flattenConvert(requestedPlugins).toSet) match {
|
||||
val allRequirementsClause = defined.filterNot(_.isRoot).flatMap(d => asRequirementsClauses(d))
|
||||
val allEnabledByClause = defined.filterNot(_.isRoot).flatMap(d => asEnabledByClauses(d))
|
||||
(requestedPlugins, log) => {
|
||||
val alwaysEnabled: List[AutoPlugin] = defined.filter(_.isAlwaysEnabled)
|
||||
val knowlege0: Set[Atom] = ((flatten(requestedPlugins) ++ alwaysEnabled) collect {
|
||||
case x: AutoPlugin => Atom(x.label)
|
||||
}).toSet
|
||||
val clauses = Clauses((allRequirementsClause ::: allEnabledByClause) filterNot { _.head subsetOf knowlege0 })
|
||||
log.debug(s"deducing auto plugins based on known facts ${knowlege0.toString} and clauses ${clauses.toString}")
|
||||
Logic.reduce(clauses, (flattenConvert(requestedPlugins) ++ convertAll(alwaysEnabled)).toSet) match {
|
||||
case Left(problem) => throw AutoPluginException(problem)
|
||||
case Right(results) =>
|
||||
// results includes the originally requested (positive) atoms,
|
||||
// which won't have a corresponding AutoPlugin to map back to
|
||||
results.ordered.flatMap(a => byAtomMap.get(a).toList)
|
||||
log.debug(s" :: deduced result: ${results}")
|
||||
val selectedAtoms: List[Atom] = results.ordered
|
||||
val selectedPlugins = selectedAtoms map { a =>
|
||||
byAtomMap.getOrElse(a, throw AutoPluginException(s"${a} was not found in atom map."))
|
||||
}
|
||||
val forbidden: Set[AutoPlugin] = (selectedPlugins flatMap { Plugins.asExclusions }).toSet
|
||||
val c = selectedPlugins.toSet & forbidden
|
||||
if (!c.isEmpty) {
|
||||
exlusionConflictError(requestedPlugins, selectedPlugins, c.toSeq sortBy {_.label})
|
||||
}
|
||||
val retval = topologicalSort(selectedPlugins, log)
|
||||
log.debug(s" :: sorted deduced result: ${retval.toString}")
|
||||
retval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] def topologicalSort(ns: List[AutoPlugin], log: Logger): List[AutoPlugin] = {
|
||||
log.debug(s"sorting: ns: ${ns.toString}")
|
||||
@tailrec def doSort(found0: List[AutoPlugin], notFound0: List[AutoPlugin], limit0: Int): List[AutoPlugin] = {
|
||||
log.debug(s" :: sorting:: found: ${found0.toString} not found ${notFound0.toString}")
|
||||
if (limit0 < 0) throw AutoPluginException(s"Failed to sort ${ns} topologically")
|
||||
else if (notFound0.isEmpty) found0
|
||||
else {
|
||||
val (found1, notFound1) = notFound0 partition { n => asRequirements(n).toSet subsetOf found0.toSet }
|
||||
doSort(found0 ::: found1, notFound1, limit0 - 1)
|
||||
}
|
||||
}
|
||||
val (roots, nonRoots) = ns partition (_.isRoot)
|
||||
doSort(roots, nonRoots, ns.size * ns.size + 1)
|
||||
}
|
||||
private[sbt] def translateMessage(e: LogicException) = e match {
|
||||
case ic: InitialContradictions => s"Contradiction in selected plugins. These plguins were both included and excluded: ${literalsString(ic.literals.toSeq)}"
|
||||
case ic: InitialContradictions => s"Contradiction in selected plugins. These plugins were both included and excluded: ${literalsString(ic.literals.toSeq)}"
|
||||
case io: InitialOverlap => s"Cannot directly enable plugins. Plugins are enabled when their required plugins are satisifed. The directly selected plugins were: ${literalsString(io.literals.toSeq)}"
|
||||
case cn: CyclicNegation => s"Cycles in plugin requirements cannot involve excludes. The problematic cycle is: ${literalsString(cn.cycle)}"
|
||||
}
|
||||
|
|
@ -152,9 +209,30 @@ object Plugins
|
|||
val message = s"Plugin$ns provided by multiple AutoPlugins:$nl${dupStrings.mkString(nl)}"
|
||||
throw AutoPluginException(message)
|
||||
}
|
||||
private[this] def exlusionConflictError(requested: Plugins, selected: Seq[AutoPlugin], conflicting: Seq[AutoPlugin]) {
|
||||
def listConflicts(ns: Seq[AutoPlugin]) = (ns map { c =>
|
||||
val reasons = (if (flatten(requested) contains c) List("requested")
|
||||
else Nil) ++
|
||||
(if (c.requires != empty && c.trigger == allRequirements) List(s"enabled by ${c.requires.toString}")
|
||||
else Nil) ++
|
||||
{
|
||||
val reqs = selected filter { x => asRequirements(x) contains c }
|
||||
if (!reqs.isEmpty) List(s"""required by ${reqs.mkString(", ")}""")
|
||||
else Nil
|
||||
} ++
|
||||
{
|
||||
val exs = selected filter { x => asExclusions(x) contains c }
|
||||
if (!exs.isEmpty) List(s"""excluded by ${exs.mkString(", ")}""")
|
||||
else Nil
|
||||
}
|
||||
s""" - conflict: ${c.label} is ${reasons.mkString("; ")}"""
|
||||
}).mkString("\n")
|
||||
throw AutoPluginException(s"""Contradiction in enabled plugins:
|
||||
- requested: ${requested.toString}
|
||||
- enabled: ${selected.mkString(", ")}
|
||||
${listConflicts(conflicting)}""")
|
||||
}
|
||||
|
||||
/** [[Plugins]] instance that doesn't require any [[Plugins]]s. */
|
||||
def empty: Plugins = Empty
|
||||
private[sbt] final object Empty extends Plugins {
|
||||
def &&(o: Basic): Plugins = o
|
||||
override def toString = "<none>"
|
||||
|
|
@ -171,7 +249,7 @@ object Plugins
|
|||
}
|
||||
private[sbt] final case class And(plugins: List[Basic]) extends Plugins {
|
||||
def &&(o: Basic): Plugins = And(o :: plugins)
|
||||
override def toString = plugins.mkString(", ")
|
||||
override def toString = plugins.mkString(" && ")
|
||||
}
|
||||
private[sbt] def and(a: Plugins, b: Plugins) = b match {
|
||||
case Empty => a
|
||||
|
|
@ -186,10 +264,21 @@ object Plugins
|
|||
if(removed.isEmpty) Empty else And(removed)
|
||||
}
|
||||
|
||||
/** Defines a clause for `ap` such that the [[AutoPlugin]] provided by `ap` is the head and the selector for `ap` is the body. */
|
||||
private[sbt] def asClause(ap: AutoPlugin): Clause =
|
||||
Clause( convert(ap.select), Set(Atom(ap.label)) )
|
||||
|
||||
/** Defines enabled-by clauses for `ap`. */
|
||||
private[sbt] def asEnabledByClauses(ap: AutoPlugin): List[Clause] =
|
||||
// `ap` is the head and the required plugins for `ap` is the body.
|
||||
if (ap.trigger == AllRequirements) Clause( convert(ap.requires), Set(Atom(ap.label)) ) :: Nil
|
||||
else Nil
|
||||
/** Defines requirements clauses for `ap`. */
|
||||
private[sbt] def asRequirementsClauses(ap: AutoPlugin): List[Clause] =
|
||||
// required plugin is the head and `ap` is the body.
|
||||
asRequirements(ap) map { x => Clause( convert(ap), Set(Atom(x.label)) ) }
|
||||
private[sbt] def asRequirements(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
|
||||
case x: AutoPlugin => x
|
||||
}
|
||||
private[sbt] def asExclusions(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
|
||||
case Exclude(x) => x
|
||||
}
|
||||
private[this] def flattenConvert(n: Plugins): Seq[Literal] = n match {
|
||||
case And(ns) => convertAll(ns)
|
||||
case b: Basic => convertBasic(b) :: Nil
|
||||
|
|
@ -212,7 +301,7 @@ object Plugins
|
|||
}
|
||||
private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic
|
||||
|
||||
/** True if the select clause `n` is satisifed by `model`. */
|
||||
/** True if the trigger clause `n` is satisifed by `model`. */
|
||||
def satisfied(n: Plugins, model: Set[AutoPlugin]): Boolean =
|
||||
flatten(n) forall {
|
||||
case Exclude(a) => !model(a)
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ private[sbt] object PluginsDebug
|
|||
def projectForRef(ref: ProjectRef): ResolvedProject = get(Keys.thisProject in ref)
|
||||
val perBuild: Map[URI, Set[AutoPlugin]] = structure.units.mapValues(unit => availableAutoPlugins(unit).toSet)
|
||||
val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList
|
||||
lazy val context = Context(currentProject.plugins, currentProject.autoPlugins, Plugins.compile(pluginsThisBuild), pluginsThisBuild)
|
||||
lazy val context = Context(currentProject.plugins, currentProject.autoPlugins, Plugins.deducer(pluginsThisBuild), pluginsThisBuild, s.log)
|
||||
lazy val debug = PluginsDebug(context.available)
|
||||
if(!pluginsThisBuild.contains(plugin)) {
|
||||
val availableInBuilds: List[URI] = perBuild.toList.filter(_._2(plugin)).map(_._1)
|
||||
|
|
@ -152,9 +152,9 @@ private[sbt] object PluginsDebug
|
|||
/** The context for debugging a plugin (de)activation.
|
||||
* @param initial The initially defined [[AutoPlugin]]s.
|
||||
* @param enabled The resulting model.
|
||||
* @param compile The function used to compute the model.
|
||||
* @param deducePlugin The function used to compute the model.
|
||||
* @param available All [[AutoPlugin]]s available for consideration. */
|
||||
final case class Context(initial: Plugins, enabled: Seq[AutoPlugin], compile: Plugins => Seq[AutoPlugin], available: List[AutoPlugin])
|
||||
final case class Context(initial: Plugins, enabled: Seq[AutoPlugin], deducePlugin: (Plugins, Logger) => Seq[AutoPlugin], available: List[AutoPlugin], log: Logger)
|
||||
|
||||
/** Describes the steps to activate a plugin in some context. */
|
||||
sealed abstract class PluginEnable
|
||||
|
|
@ -236,10 +236,10 @@ private[sbt] object PluginsDebug
|
|||
// The model that results when the minimal plugins are enabled and the minimal plugins are excluded.
|
||||
// This can include more plugins than just `minRequiredPlugins` because the plguins required for `plugin`
|
||||
// might activate other plugins as well.
|
||||
val modelForMin = context.compile(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)))
|
||||
val modelForMin = context.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log)
|
||||
|
||||
val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins))
|
||||
val incrementalModel = context.compile(incrementalInputs).toSet
|
||||
val incrementalModel = context.deducePlugin(incrementalInputs, context.log).toSet
|
||||
|
||||
// Plugins that are newly enabled as a result of selecting the plugins needed for `plugin`, but aren't strictly required for `plugin`.
|
||||
// These could be excluded and `plugin` and the user's current plugins would still be activated.
|
||||
|
|
@ -252,7 +252,7 @@ private[sbt] object PluginsDebug
|
|||
// If both A and B must be deactivated, but A transitively depends on B, deactivating B will deactivate A.
|
||||
// If A must be deactivated, but one if its (transitively) required plugins isn't present, it won't be activated.
|
||||
// So, in either of these cases, A doesn't need to be considered further and won't be included in this set.
|
||||
val minDeactivate = minAbsentPlugins.filter(p => Plugins.satisfied(p.select, incrementalModel))
|
||||
val minDeactivate = minAbsentPlugins.filter(p => Plugins.satisfied(p.requires, incrementalModel))
|
||||
|
||||
val deactivate = for(d <- minDeactivate.toList) yield {
|
||||
// removing any one of these plugins will deactivate `d`. TODO: This is not an especially efficient implementation.
|
||||
|
|
@ -280,7 +280,7 @@ private[sbt] object PluginsDebug
|
|||
// The actual model might be larger, since other plugins might be enabled by the selected plugins.
|
||||
private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) {
|
||||
case _: Exclude => Nil
|
||||
case ap: AutoPlugin => Plugins.flatten(ap.select)
|
||||
case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin
|
||||
}
|
||||
|
||||
/** String representation of [[PluginEnable]], intended for end users. */
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import Def.Setting
|
|||
* Can control task-level paralleism, logging, etc.
|
||||
*/
|
||||
object GlobalModule extends AutoPlugin {
|
||||
// We must be explicitly enabled
|
||||
def select = Plugins.empty
|
||||
// This is included by default
|
||||
def requires = empty
|
||||
def trigger = allRequirements
|
||||
|
||||
override lazy val projectSettings: Seq[Setting[_]] =
|
||||
Defaults.coreDefaultSettings
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import Def.Setting
|
|||
object IvyModule extends AutoPlugin {
|
||||
// We are automatically included on everything that has the global module,
|
||||
// which is automatically included on everything.
|
||||
def select = GlobalModule
|
||||
def requires = GlobalModule
|
||||
def trigger = allRequirements
|
||||
|
||||
override lazy val projectSettings: Seq[Setting[_]] =
|
||||
Classpaths.ivyPublishSettings ++ Classpaths.ivyBaseSettings
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import Def.Setting
|
|||
object JvmModule extends AutoPlugin {
|
||||
// We are automatically enabled for any IvyModule project. We also require its settings
|
||||
// for ours to work.
|
||||
def select = IvyModule
|
||||
def requires = IvyModule
|
||||
def trigger = allRequirements
|
||||
|
||||
override lazy val projectSettings: Seq[Setting[_]] =
|
||||
Defaults.runnerSettings ++
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import org.specs2._
|
||||
import mutable.Specification
|
||||
|
||||
object PluginsTest extends Specification
|
||||
{
|
||||
import AI._
|
||||
|
||||
"Auto plugin" should {
|
||||
"enable plugins with trigger=allRequirements AND requirements met" in {
|
||||
deducePlugin(A && B, log) must contain(Q)
|
||||
}
|
||||
"enable transive plugins with trigger=allRequirements AND requirements met" in {
|
||||
deducePlugin(A && B, log) must contain(R)
|
||||
}
|
||||
"order enable plugins after required plugins" in {
|
||||
val ns = deducePlugin(A && B, log)
|
||||
( (ns indexOf Q) must beGreaterThan(ns indexOf A) ) and
|
||||
( (ns indexOf Q) must beGreaterThan(ns indexOf B) ) and
|
||||
( (ns indexOf R) must beGreaterThan(ns indexOf A) ) and
|
||||
( (ns indexOf R) must beGreaterThan(ns indexOf B) ) and
|
||||
( (ns indexOf R) must beGreaterThan(ns indexOf Q) )
|
||||
}
|
||||
"not enable plugins with trigger=allRequirements but conflicting requirements" in {
|
||||
deducePlugin(A && B, log) must not contain(S)
|
||||
}
|
||||
"enable plugins that are required by the requested plugins" in {
|
||||
val ns = deducePlugin(Q, log)
|
||||
(ns must contain(A)) and
|
||||
(ns must contain(B))
|
||||
}
|
||||
"throw an AutoPluginException on conflicting requirements" in {
|
||||
deducePlugin(S, log) must throwAn[AutoPluginException](message = """Contradiction in enabled plugins:
|
||||
- requested: sbt.AI\$S
|
||||
- enabled: sbt.AI\$S, sbt.AI\$Q, sbt.AI\$R, sbt.AI\$B, sbt.AI\$A
|
||||
- conflict: sbt.AI\$R is enabled by sbt.AI\$Q; excluded by sbt.AI\$S""")
|
||||
}
|
||||
"generates a detailed report on conflicting requirements" in {
|
||||
deducePlugin(T && U, log) must throwAn[AutoPluginException](message = """Contradiction in enabled plugins:
|
||||
- requested: sbt.AI\$T && sbt.AI\$U
|
||||
- enabled: sbt.AI\$U, sbt.AI\$T, sbt.AI\$A, sbt.AI\$Q, sbt.AI\$R, sbt.AI\$B
|
||||
- conflict: sbt.AI\$Q is enabled by sbt.AI\$A && sbt.AI\$B; required by sbt.AI\$T, sbt.AI\$R; excluded by sbt.AI\$U
|
||||
- conflict: sbt.AI\$R is enabled by sbt.AI\$Q; excluded by sbt.AI\$T""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AI
|
||||
{
|
||||
lazy val allPlugins: List[AutoPlugin] = List(A, B, Q, R, S, T, U)
|
||||
lazy val deducePlugin = Plugins.deducer(allPlugins)
|
||||
lazy val log = Logger.Null
|
||||
|
||||
trait EmptyAutoPlugin extends AutoPlugin {
|
||||
def requires = empty
|
||||
def trigger = noTrigger
|
||||
}
|
||||
object A extends EmptyAutoPlugin
|
||||
object B extends EmptyAutoPlugin
|
||||
|
||||
object Q extends AutoPlugin
|
||||
{
|
||||
def requires: Plugins = A && B
|
||||
def trigger = allRequirements
|
||||
}
|
||||
|
||||
object R extends AutoPlugin
|
||||
{
|
||||
def requires = Q
|
||||
def trigger = allRequirements
|
||||
}
|
||||
|
||||
object S extends AutoPlugin
|
||||
{
|
||||
def requires = Q && !R
|
||||
def trigger = allRequirements
|
||||
}
|
||||
|
||||
// This is an opt-in plugin with a requirement
|
||||
// Unless explicitly loaded by the build user, this will not be activated.
|
||||
object T extends AutoPlugin
|
||||
{
|
||||
def requires = Q && !R
|
||||
def trigger = noTrigger
|
||||
}
|
||||
|
||||
// This is an opt-in plugin with a requirement
|
||||
// Unless explicitly loaded by the build user, this will not be activated.
|
||||
object U extends AutoPlugin
|
||||
{
|
||||
def requires = A && !Q
|
||||
def trigger = noTrigger
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,38 @@
|
|||
// excludePlugins(C) will prevent C, and thus D, from being auto-added
|
||||
lazy val a = project.addPlugins(A, B).disablePlugins(Q)
|
||||
// disablePlugins(Q) will prevent R from being auto-added
|
||||
lazy val projA = project.addPlugins(A, B).disablePlugins(Q)
|
||||
|
||||
// without B, C is not added
|
||||
lazy val b = project.addPlugins(A)
|
||||
// without B, Q is not added
|
||||
lazy val projB = project.addPlugins(A)
|
||||
|
||||
// with both A and B, C is selected, which in turn selects D
|
||||
lazy val c = project.addPlugins(A, B)
|
||||
// with both A and B, Q is selected, which in turn selects R, but not S
|
||||
lazy val projC = project.addPlugins(A, B)
|
||||
|
||||
// with no natures defined, nothing is auto-added
|
||||
lazy val d = project
|
||||
lazy val projD = project
|
||||
|
||||
// with S selected, Q is loaded automatically, which in turn selects R
|
||||
lazy val projE = project.addPlugins(S)
|
||||
|
||||
check := {
|
||||
val ddel = (del in d).?.value // should be None
|
||||
same(ddel, None, "del in d")
|
||||
val bdel = (del in b).?.value // should be None
|
||||
same(bdel, None, "del in b")
|
||||
val adel = (del in a).?.value // should be None
|
||||
same(adel, None, "del in a")
|
||||
val adel = (del in projA).?.value // should be None
|
||||
same(adel, None, "del in projA")
|
||||
val bdel = (del in projB).?.value // should be None
|
||||
same(bdel, None, "del in projB")
|
||||
val ddel = (del in projD).?.value // should be None
|
||||
same(ddel, None, "del in projD")
|
||||
//
|
||||
val buildValue = (demo in ThisBuild).value
|
||||
same(buildValue, "build 0", "demo in ThisBuild")
|
||||
val globalValue = (demo in Global).value
|
||||
same(globalValue, "global 0", "demo in Global")
|
||||
val projValue = (demo in c).value
|
||||
same(projValue, "project c Q R", "demo in c")
|
||||
val qValue = (del in c in q).value
|
||||
same(qValue, " Q R", "del in c in q")
|
||||
val projValue = (demo in projC).value
|
||||
same(projValue, "project projC Q R", "demo in projC")
|
||||
val qValue = (del in projC in q).value
|
||||
same(qValue, " Q R", "del in projC in q")
|
||||
val optInValue = (del in projE in q).value
|
||||
same(optInValue, " Q S R", "del in projE in q")
|
||||
}
|
||||
|
||||
def same[T](actual: T, expected: T, label: String) {
|
||||
assert(actual == expected, s"Expected '$expected' for `$label`, got '$actual'")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
object AI extends AutoImport
|
||||
{
|
||||
trait EmptyAutoPlugin extends AutoPlugin {
|
||||
def select = Plugins.empty
|
||||
def requires = empty
|
||||
def trigger = noTrigger
|
||||
}
|
||||
object A extends EmptyAutoPlugin
|
||||
object B extends EmptyAutoPlugin
|
||||
|
|
@ -23,12 +24,14 @@ object AI extends AutoImport
|
|||
import AI._
|
||||
|
||||
object D extends AutoPlugin {
|
||||
def select: Plugins = E
|
||||
def requires: Plugins = E
|
||||
def trigger = allRequirements
|
||||
}
|
||||
|
||||
object Q extends AutoPlugin
|
||||
{
|
||||
def select: Plugins = A && B
|
||||
def requires: Plugins = A && B
|
||||
def trigger = allRequirements
|
||||
|
||||
override def projectConfigurations: Seq[Configuration] =
|
||||
p ::
|
||||
|
|
@ -56,12 +59,25 @@ object Q extends AutoPlugin
|
|||
object R extends AutoPlugin
|
||||
{
|
||||
// NOTE - Only plugins themselves support exclusions...
|
||||
def select = Q && !D
|
||||
def requires = Q
|
||||
def trigger = allRequirements
|
||||
|
||||
override def projectSettings = Seq(
|
||||
// tests proper ordering: R requires C, so C settings should come first
|
||||
// tests proper ordering: R requires Q, so Q settings should come first
|
||||
del in q += " R",
|
||||
// tests that configurations are properly registered, enabling delegation from p to q
|
||||
demo += (del in p).value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// This is an opt-in plugin with a requirement
|
||||
// Unless explicitly loaded by the build user, this will not be activated.
|
||||
object S extends AutoPlugin
|
||||
{
|
||||
def requires = Q
|
||||
def trigger = noTrigger
|
||||
|
||||
override def projectSettings = Seq(
|
||||
del in q += " S"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import Keys._
|
|||
|
||||
object C extends AutoImport {
|
||||
object bN extends AutoPlugin {
|
||||
def select = Plugins.empty
|
||||
def requires = empty
|
||||
def trigger = allRequirements
|
||||
}
|
||||
lazy val check = taskKey[Unit]("Checks that the AutoPlugin and Build are automatically added.")
|
||||
}
|
||||
|
|
@ -12,7 +13,8 @@ object C extends AutoImport {
|
|||
import C._
|
||||
|
||||
object A extends AutoPlugin {
|
||||
override def select = bN
|
||||
def requires = bN
|
||||
def trigger = allRequirements
|
||||
override def projectSettings = Seq(
|
||||
check := {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -234,10 +234,12 @@ core methods without requiring an import or qualification.
|
|||
In addition, a plugin can implement the `AutoPlugin` class. This has additoinal features, such as
|
||||
|
||||
* Specifying plugin dependencies.
|
||||
* Automatically activating itself when all dependencies are present.
|
||||
* Specifying `projectSettings`, `buildSettings`, and `globalSettings` as appropriate.
|
||||
|
||||
The AutoPlugin's `projectSettings` is automatically appended to each project's settings, when its dependencies also exist on that project
|
||||
The `select` method defines the conditions by which this plugin's settings are automatically imported.
|
||||
The `requires` method defines the dependencies to other plugins.
|
||||
The `trigger` method defines the conditions by which this plugin's settings are automatically activated.
|
||||
The `buildSettings` is appended to each build's settings (that is, `in ThisBuild`).
|
||||
The `globalSettings` is appended once to the global settings (`in Global`).
|
||||
These allow a plugin to automatically provide new functionality or new defaults.
|
||||
|
|
@ -268,8 +270,9 @@ An example of a typical plugin:
|
|||
object MyPlugin extends AutoPlugin
|
||||
{
|
||||
// Only enable this plugin for projects which are JvmModules.
|
||||
def select = sbt.plugins.JvmModule
|
||||
|
||||
def trigger = allRequirements
|
||||
def requires = sbt.plugins.JvmModule
|
||||
|
||||
// configuration points, like the built in `version`, `libraryDependencies`, or `compile`
|
||||
// by implementing Plugin, these are automatically imported in a user's `build.sbt`
|
||||
val newTask = taskKey[Unit]("A new task.")
|
||||
|
|
@ -302,11 +305,8 @@ A build definition that uses the plugin might look like:
|
|||
Root Plugins
|
||||
------------
|
||||
|
||||
Some plugins should always be explicitly enabled on projects. Sbt calls these "RootPlugins", i.e. plugins
|
||||
that are "root" nodes in the plugin depdendency graph. To define a root plugin, just extend the `sbt.RootPlugin`
|
||||
interface. This interface is exactly like the `AutoPlugin` interface except that a `select` method is not
|
||||
needed.
|
||||
|
||||
Some plugins should always be explicitly enabled on projects. Sbt calls these root plugins, i.e. plugins
|
||||
that are "root" nodes in the plugin depdendency graph. To define a root plugin, set the `trigger` method to `noTrigger` and the `requires` method to `empty`.
|
||||
|
||||
Example command root plugin
|
||||
----------------------
|
||||
|
|
@ -329,8 +329,11 @@ A basic plugin that adds commands looks like:
|
|||
|
||||
import sbt._
|
||||
import Keys._
|
||||
object MyPlugin extends RootPlugin
|
||||
object MyPlugin extends AutoPlugin
|
||||
{
|
||||
def trigger = noTrigger
|
||||
def requires = empty
|
||||
|
||||
override lazy val projectSettings = Seq(commands += myCommand)
|
||||
|
||||
lazy val myCommand =
|
||||
|
|
|
|||
|
|
@ -119,11 +119,12 @@ To create an sbt plugin,
|
|||
|
||||
1. Create a new project for the plugin.
|
||||
2. Set `sbtPlugin := true` for the project in `build.sbt`. This adds a dependency on sbt and will detect and record Plugins that you define.
|
||||
3. Define an `object` that extends `AutoPlugin` or `RootPlugin`. The contents of this object will be automatically imported in `.sbt` files, so ensure it only contains important API definitions and types.
|
||||
4. Define any custom tasks or settings (see the next section :doc:`Custom-Settings`).
|
||||
5. Collect the default settings to apply to a project in a list for the user to add. Optionally override one or more of `AutoPlugin`'s methods to have settings automatically added to user projects.
|
||||
6. (Optional) For non-root plguins, declare dependencies on other plugins by overriding the `select` method.
|
||||
6. Publish the project. There is a :doc:`community repository </Community/Community-Plugins>` available for open source plugins.
|
||||
3. Define another `object` that extends `AutoImport`. The contents of this object will be automatically imported in `.sbt` files, so ensure it only contains important API definitions and types.
|
||||
4. Define an `object` that extends `AutoPlugin`.
|
||||
5. Declare dependencies on other plugins by defining the `requires` method.
|
||||
5. Define any custom tasks or settings (see the next section :doc:`Custom-Settings`).
|
||||
6. Collect the default settings to apply to a project in a list for the user to add. Optionally override one or more of `AutoPlugin`'s methods to have settings automatically added to user projects.
|
||||
8. Publish the project. There is a :doc:`community repository </Community/Community-Plugins>` available for open source plugins.
|
||||
|
||||
For more details, including ways of developing plugins, see :doc:`/Extending/Plugins`.
|
||||
For best practices, see :doc:`/Extending/Plugins-Best-Practices`.
|
||||
|
|
|
|||
Loading…
Reference in New Issue