Merge pull request #1516 from sbt/wip/fix-1455

Allow root plugins to be disabled.
This commit is contained in:
eugene yokota 2014-08-12 11:12:18 -04:00
commit 3d59b18aef
4 changed files with 585 additions and 569 deletions

View File

@ -5,11 +5,11 @@ TODO:
- error message when a task doesn't exist that it would be provided by plugin x, enabled by natures y,z, blocked by a, b - error message when a task doesn't exist that it would be provided by plugin x, enabled by natures y,z, blocked by a, b
*/ */
import logic.{Atom, Clause, Clauses, Formula, Literal, Logic, Negated} import logic.{Atom, Clause, Clauses, Formula, Literal, Logic, Negated}
import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException} import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException}
import Def.Setting import Def.Setting
import Plugins._ import Plugins._
import annotation.tailrec import annotation.tailrec
/** /**
An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation"). An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation").
@ -30,7 +30,7 @@ For example, the following will automatically add the settings in `projectSettin
override def projectSettings = Seq(...) override def projectSettings = Seq(...)
object autoImport { object autoImport {
lazy val obfuscate = taskKey[Seq[File]]("Obfuscates the source.") lazy val obfuscate = taskKey[Seq[File]]("Obfuscates the source.")
} }
} }
@ -50,278 +50,294 @@ 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. then the `MyPlugin` settings (and anything that activates only when `MyPlugin` is activated) will not be added.
*/ */
abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions {
{ /** Determines whether this AutoPlugin will be activated for this project when the `requires` clause is satisfied.
/** 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
* 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 enabled.
* instance will be added automatically if the `Web` and `Javascript` plugins are enabled. *
* * When this method returns `noTrigger`, and `requires` method returns `Web && Javascript`, this plugin
* 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`. */
* instance will be added only if the build user enables it, but it will automatically add both `Web` and `Javascript`. */ def trigger: PluginTrigger = noTrigger
def trigger: PluginTrigger = noTrigger
/** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]]. /** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]].
*/ */
def requires: Plugins = empty def requires: Plugins = empty
val label: String = getClass.getName.stripSuffix("$") val label: String = getClass.getName.stripSuffix("$")
override def toString: String = label override def toString: String = label
/** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/ /** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/
def projectConfigurations: Seq[Configuration] = Nil def projectConfigurations: Seq[Configuration] = Nil
/** The [[Setting]]s to add in the scope of each project that activates this AutoPlugin. */ /** The [[Setting]]s to add in the scope of each project that activates this AutoPlugin. */
def projectSettings: Seq[Setting[_]] = Nil def projectSettings: Seq[Setting[_]] = Nil
/** The [[Setting]]s to add to the build scope for each project that activates this AutoPlugin. /** The [[Setting]]s to add to the build scope for each project that activates this AutoPlugin.
* The settings returned here are guaranteed to be added to a given build scope only once * The settings returned here are guaranteed to be added to a given build scope only once
* regardless of how many projects for that build activate this AutoPlugin. */ * regardless of how many projects for that build activate this AutoPlugin. */
def buildSettings: Seq[Setting[_]] = Nil def buildSettings: Seq[Setting[_]] = Nil
/** The [[Setting]]s to add to the global scope exactly once if any project activates this AutoPlugin. */ /** The [[Setting]]s to add to the global scope exactly once if any project activates this AutoPlugin. */
def globalSettings: Seq[Setting[_]] = Nil def globalSettings: Seq[Setting[_]] = Nil
// TODO?: def commands: Seq[Command] // TODO?: def commands: Seq[Command]
private[sbt] def unary_! : Exclude = Exclude(this) private[sbt] def unary_! : Exclude = Exclude(this)
/** If this plugin does not have any requirements, it means it is actually a root plugin. */ /** If this plugin does not have any requirements, it means it is actually a root plugin. */
private[sbt] final def isRoot: Boolean = private[sbt] final def isRoot: Boolean =
requires match { requires match {
case Empty => true case Empty => true
case _ => false case _ => false
} }
/** If this plugin does not have any requirements, it means it is actually a root plugin. */ /** If this plugin does not have any requirements, it means it is actually a root plugin. */
private[sbt] final def isAlwaysEnabled: Boolean = private[sbt] final def isAlwaysEnabled: Boolean =
isRoot && (trigger == AllRequirements) isRoot && (trigger == AllRequirements)
} }
/** An error that occurs when auto-plugins aren't configured properly. /** An error that occurs when auto-plugins aren't configured properly.
* It translates the error from the underlying logic system to be targeted at end users. */ * It translates the error from the underlying logic system to be targeted at end users. */
final class AutoPluginException private(val message: String, val origin: Option[LogicException]) extends RuntimeException(message) final class AutoPluginException private(val message: String, val origin: Option[LogicException]) extends RuntimeException(message) {
{ /** Prepends `p` to the error message derived from `origin`. */
/** Prepends `p` to the error message derived from `origin`. */ def withPrefix(p: String) = new AutoPluginException(p + message, origin)
def withPrefix(p: String) = new AutoPluginException(p + message, origin)
} }
object AutoPluginException object AutoPluginException {
{ def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None)
def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None) def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Plugins.translateMessage(origin), Some(origin))
def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Plugins.translateMessage(origin), Some(origin))
} }
sealed trait PluginTrigger sealed trait PluginTrigger
case object AllRequirements extends PluginTrigger case object AllRequirements extends PluginTrigger
case object NoTrigger extends PluginTrigger case object NoTrigger extends PluginTrigger
/** An expression that matches `AutoPlugin`s. */ /** An expression that matches `AutoPlugin`s. */
sealed trait Plugins { sealed trait Plugins {
def && (o: Basic): Plugins def && (o: Basic): Plugins
} }
sealed trait PluginsFunctions sealed trait PluginsFunctions {
{ /** [[Plugins]] instance that doesn't require any [[Plugins]]s. */
/** [[Plugins]] instance that doesn't require any [[Plugins]]s. */ def empty: Plugins = Plugins.Empty
def empty: Plugins = Plugins.Empty
/** This plugin is activated when all required plugins are present. */ /** This plugin is activated when all required plugins are present. */
def allRequirements: PluginTrigger = AllRequirements def allRequirements: PluginTrigger = AllRequirements
/** This plugin is activated only when it is manually activated. */ /** This plugin is activated only when it is manually activated. */
def noTrigger: PluginTrigger = NoTrigger def noTrigger: PluginTrigger = NoTrigger
} }
object Plugins extends PluginsFunctions object Plugins extends PluginsFunctions {
{ /** Given the available auto plugins `defined`, returns a function that selects [[AutoPlugin]]s for the provided [[AutoPlugin]]s.
/** 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 required [[AutoPlugin]] comes before its requiring [[AutoPlugin]].*/
* 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] =
def deducer(defined0: List[AutoPlugin]): (Plugins, Logger) => Seq[AutoPlugin] = if(defined0.isEmpty) (_, _) => Nil
if(defined0.isEmpty) (_, _) => Nil else {
else // TODO: defined should return all the plugins
{ val allReqs = (defined0 flatMap { asRequirements }).toSet
// TODO: defined should return all the plugins val diff = allReqs diff defined0.toSet
val allReqs = (defined0 flatMap { asRequirements }).toSet val defined = if (!diff.isEmpty) diff.toList ::: defined0
val diff = allReqs diff defined0.toSet else defined0
val defined = if (!diff.isEmpty) diff.toList ::: defined0
else defined0
val byAtom = defined map { x => (Atom(x.label), x) } val byAtom = defined map { x => (Atom(x.label), x) }
val byAtomMap = byAtom.toMap val byAtomMap = byAtom.toMap
if(byAtom.size != byAtomMap.size) duplicateProvidesError(byAtom) if(byAtom.size != byAtomMap.size) duplicateProvidesError(byAtom)
// Ignore clauses for plugins that does not require anything else. // Ignore clauses for plugins that does not require anything else.
// Avoids the requirement for pure Nature strings *and* possible // Avoids the requirement for pure Nature strings *and* possible
// circular dependencies in the logic. // circular dependencies in the logic.
val allRequirementsClause = defined.filterNot(_.isRoot).flatMap(d => asRequirementsClauses(d)) val allRequirementsClause = defined.filterNot(_.isRoot).flatMap(d => asRequirementsClauses(d))
val allEnabledByClause = defined.filterNot(_.isRoot).flatMap(d => asEnabledByClauses(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) =>
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 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)}"
}
private[this] def literalsString(lits: Seq[Literal]): String =
lits map { case Atom(l) => l; case Negated(Atom(l)) => l } mkString(", ")
private[this] def duplicateProvidesError(byAtom: Seq[(Atom, AutoPlugin)]) { // Note: Here is where the function begins. We're given a list of plugins now.
val dupsByAtom = byAtom.groupBy(_._1).mapValues(_.map(_._2)) (requestedPlugins, log) => {
val dupStrings = for( (atom, dups) <- dupsByAtom if dups.size > 1 ) yield def explicitlyDisabled(p: AutoPlugin): Boolean = hasExclude(requestedPlugins, p)
s"${atom.label} by ${dups.mkString(", ")}" val alwaysEnabled: List[AutoPlugin] = defined.filter(_.isAlwaysEnabled).filterNot(explicitlyDisabled)
val (ns, nl) = if(dupStrings.size > 1) ("s", "\n\t") else ("", " ") val knowlege0: Set[Atom] = ((flatten(requestedPlugins) ++ alwaysEnabled) collect {
val message = s"Plugin$ns provided by multiple AutoPlugins:$nl${dupStrings.mkString(nl)}" case x: AutoPlugin => Atom(x.label)
throw AutoPluginException(message) }).toSet
} val clauses = Clauses((allRequirementsClause ::: allEnabledByClause) filterNot { _.head subsetOf knowlege0 })
private[this] def exlusionConflictError(requested: Plugins, selected: Seq[AutoPlugin], conflicting: Seq[AutoPlugin]) { log.debug(s"deducing auto plugins based on known facts ${knowlege0.toString} and clauses ${clauses.toString}")
def listConflicts(ns: Seq[AutoPlugin]) = (ns map { c => Logic.reduce(clauses, (flattenConvert(requestedPlugins) ++ convertAll(alwaysEnabled)).toSet) match {
val reasons = (if (flatten(requested) contains c) List("requested") case Left(problem) => throw AutoPluginException(problem)
else Nil) ++ case Right(results) =>
(if (c.requires != empty && c.trigger == allRequirements) List(s"enabled by ${c.requires.toString}") log.debug(s" :: deduced result: ${results}")
else Nil) ++ val selectedAtoms: List[Atom] = results.ordered
{ val selectedPlugins = selectedAtoms map { a =>
val reqs = selected filter { x => asRequirements(x) contains c } byAtomMap.getOrElse(a, throw AutoPluginException(s"${a} was not found in atom map."))
if (!reqs.isEmpty) List(s"""required by ${reqs.mkString(", ")}""") }
else Nil val forbidden: Set[AutoPlugin] = (selectedPlugins flatMap { Plugins.asExclusions }).toSet
} ++ val c = selectedPlugins.toSet & forbidden
{ if (!c.isEmpty) {
val exs = selected filter { x => asExclusions(x) contains c } exlusionConflictError(requestedPlugins, selectedPlugins, c.toSeq sortBy {_.label})
if (!exs.isEmpty) List(s"""excluded by ${exs.mkString(", ")}""") }
else Nil val retval = topologicalSort(selectedPlugins, log)
} log.debug(s" :: sorted deduced result: ${retval.toString}")
s""" - conflict: ${c.label} is ${reasons.mkString("; ")}""" retval
}).mkString("\n") }
throw AutoPluginException(s"""Contradiction in enabled plugins: }
}
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 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)}"
}
private[this] def literalsString(lits: Seq[Literal]): String =
lits map { case Atom(l) => l; case Negated(Atom(l)) => l } mkString(", ")
private[this] def duplicateProvidesError(byAtom: Seq[(Atom, AutoPlugin)]) {
val dupsByAtom = byAtom.groupBy(_._1).mapValues(_.map(_._2))
val dupStrings = for( (atom, dups) <- dupsByAtom if dups.size > 1 ) yield
s"${atom.label} by ${dups.mkString(", ")}"
val (ns, nl) = if(dupStrings.size > 1) ("s", "\n\t") else ("", " ")
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} - requested: ${requested.toString}
- enabled: ${selected.mkString(", ")} - enabled: ${selected.mkString(", ")}
${listConflicts(conflicting)}""") ${listConflicts(conflicting)}""")
} }
private[sbt] final object Empty extends Plugins { private[sbt] final object Empty extends Plugins {
def &&(o: Basic): Plugins = o def &&(o: Basic): Plugins = o
override def toString = "<none>" override def toString = "<none>"
} }
/** An included or excluded Nature/Plugin. TODO: better name than Basic. Also, can we dump /** An included or excluded Nature/Plugin. TODO: better name than Basic. Also, can we dump
* this class. * this class.
*/ */
sealed abstract class Basic extends Plugins { sealed abstract class Basic extends Plugins {
def &&(o: Basic): Plugins = And(this :: o :: Nil) def &&(o: Basic): Plugins = And(this :: o :: Nil)
} }
private[sbt] final case class Exclude(n: AutoPlugin) extends Basic { private[sbt] final case class Exclude(n: AutoPlugin) extends Basic {
override def toString = s"!$n" override def toString = s"!$n"
} }
private[sbt] final case class And(plugins: List[Basic]) extends Plugins { private[sbt] final case class And(plugins: List[Basic]) extends Plugins {
def &&(o: Basic): Plugins = And(o :: 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 { private[sbt] def and(a: Plugins, b: Plugins) = b match {
case Empty => a case Empty => a
case And(ns) => (a /: ns)(_ && _) case And(ns) => (a /: ns)(_ && _)
case b: Basic => a && b case b: Basic => a && b
} }
private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match { private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match {
case b: Basic => if(del(b)) Empty else b case b: Basic => if(del(b)) Empty else b
case Empty => Empty case Empty => Empty
case And(ns) => case And(ns) =>
val removed = ns.filterNot(del) val removed = ns.filterNot(del)
if(removed.isEmpty) Empty else And(removed) if(removed.isEmpty) Empty else And(removed)
} }
/** Defines enabled-by clauses for `ap`. */ /** Defines enabled-by clauses for `ap`. */
private[sbt] def asEnabledByClauses(ap: AutoPlugin): List[Clause] = private[sbt] def asEnabledByClauses(ap: AutoPlugin): List[Clause] =
// `ap` is the head and the required plugins for `ap` is the body. // `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 if (ap.trigger == AllRequirements) Clause( convert(ap.requires), Set(Atom(ap.label)) ) :: Nil
else Nil else Nil
/** Defines requirements clauses for `ap`. */ /** Defines requirements clauses for `ap`. */
private[sbt] def asRequirementsClauses(ap: AutoPlugin): List[Clause] = private[sbt] def asRequirementsClauses(ap: AutoPlugin): List[Clause] =
// required plugin is the head and `ap` is the body. // required plugin is the head and `ap` is the body.
asRequirements(ap) map { x => Clause( convert(ap), Set(Atom(x.label)) ) } asRequirements(ap) map { x => Clause( convert(ap), Set(Atom(x.label)) ) }
private[sbt] def asRequirements(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect { private[sbt] def asRequirements(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
case x: AutoPlugin => x case x: AutoPlugin => x
} }
private[sbt] def asExclusions(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect { private[sbt] def asExclusions(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
case Exclude(x) => x case Exclude(x) => x
} }
private[this] def flattenConvert(n: Plugins): Seq[Literal] = n match { // TODO - This doesn't handle nested AND boolean logic...
case And(ns) => convertAll(ns) private[sbt] def hasExclude(n: Plugins, p: AutoPlugin): Boolean = n match {
case b: Basic => convertBasic(b) :: Nil case `p` => false
case Empty => Nil case Exclude(`p`) => true
} // TODO - This is stupidly advanced. We do a nested check through possible and-ed
private[sbt] def flatten(n: Plugins): Seq[Basic] = n match { // lists of plugins exclusions to see if the plugin ever winds up in an excluded=true case.
case And(ns) => ns // This would handle things like !!p or !(p && z)
case b: Basic => b :: Nil case Exclude(n) => hasInclude(n, p)
case Empty => Nil case And(ns) => ns.forall(n => hasExclude(n, p))
} case b: Basic => false
case Empty => false
}
private[sbt] def hasInclude(n: Plugins, p: AutoPlugin): Boolean = n match {
case `p` => true
case Exclude(n) => hasExclude(n, p)
case And(ns) => ns.forall(n => hasInclude(n, p))
case b: Basic => false
case Empty => false
}
private[this] def flattenConvert(n: Plugins): Seq[Literal] = n match {
case And(ns) => convertAll(ns)
case b: Basic => convertBasic(b) :: Nil
case Empty => Nil
}
private[sbt] def flatten(n: Plugins): Seq[Basic] = n match {
case And(ns) => ns
case b: Basic => b :: Nil
case Empty => Nil
}
private[this] def convert(n: Plugins): Formula = n match { private[this] def convert(n: Plugins): Formula = n match {
case And(ns) => convertAll(ns).reduce[Formula](_ && _) case And(ns) => convertAll(ns).reduce[Formula](_ && _)
case b: Basic => convertBasic(b) case b: Basic => convertBasic(b)
case Empty => Formula.True case Empty => Formula.True
} }
private[this] def convertBasic(b: Basic): Literal = b match { private[this] def convertBasic(b: Basic): Literal = b match {
case Exclude(n) => !convertBasic(n) case Exclude(n) => !convertBasic(n)
case a: AutoPlugin => Atom(a.label) case a: AutoPlugin => Atom(a.label)
} }
private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic
/** True if the trigger clause `n` is satisifed by `model`. */ /** True if the trigger clause `n` is satisifed by `model`. */
def satisfied(n: Plugins, model: Set[AutoPlugin]): Boolean = def satisfied(n: Plugins, model: Set[AutoPlugin]): Boolean =
flatten(n) forall { flatten(n) forall {
case Exclude(a) => !model(a) case Exclude(a) => !model(a)
case ap: AutoPlugin => model(ap) case ap: AutoPlugin => model(ap)
} }
private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = { private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = {
import reflect.runtime.{universe => ru} import reflect.runtime.{universe => ru}
import util.control.Exception.catching import util.control.Exception.catching
val m = ru.runtimeMirror(loader) val m = ru.runtimeMirror(loader)
val im = m.reflect(ap) val im = m.reflect(ap)
val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt { val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt {
im.symbol.asType.toType.declaration(ru.newTermName("autoImport")) match { im.symbol.asType.toType.declaration(ru.newTermName("autoImport")) match {
case ru.NoSymbol => false case ru.NoSymbol => false
case sym => sym.asTerm.isGetter || sym.asTerm.isModule case sym => sym.asTerm.isGetter || sym.asTerm.isModule
} }
} }
hasGetterOpt getOrElse false hasGetterOpt getOrElse false
} }
} }

View File

@ -1,382 +1,375 @@
package sbt package sbt
import Def.Setting import Def.Setting
import Plugins._ import Plugins._
import PluginsDebug._ import PluginsDebug._
import java.net.URI import java.net.URI
private[sbt] class PluginsDebug(val available: List[AutoPlugin], val nameToKey: Map[String, AttributeKey[_]], val provided: Relation[AutoPlugin, AttributeKey[_]]) private[sbt] class PluginsDebug(val available: List[AutoPlugin], val nameToKey: Map[String, AttributeKey[_]], val provided: Relation[AutoPlugin, AttributeKey[_]]) {
{ /** The set of [[AutoPlugin]]s that might define a key named `keyName`.
/** The set of [[AutoPlugin]]s that might define a key named `keyName`. * Because plugins can define keys in different scopes, this should only be used as a guideline. */
* Because plugins can define keys in different scopes, this should only be used as a guideline. */ def providers(keyName: String): Set[AutoPlugin] = nameToKey.get(keyName) match {
def providers(keyName: String): Set[AutoPlugin] = nameToKey.get(keyName) match { case None => Set.empty
case None => Set.empty case Some(key) => provided.reverse(key)
case Some(key) => provided.reverse(key) }
} /** Describes alternative approaches for defining key [[keyName]] in [[context]].*/
/** Describes alternative approaches for defining key [[keyName]] in [[context]].*/ def toEnable(keyName: String, context: Context): List[PluginEnable] =
def toEnable(keyName: String, context: Context): List[PluginEnable] = providers(keyName).toList.map(plugin => pluginEnable(context, plugin))
providers(keyName).toList.map(plugin => pluginEnable(context, plugin))
/** Provides text to suggest how [[notFoundKey]] can be defined in [[context]]. */ /** Provides text to suggest how [[notFoundKey]] can be defined in [[context]]. */
def debug(notFoundKey: String, context: Context): String = def debug(notFoundKey: String, context: Context): String =
{ {
val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) { val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) {
case pa: PluginActivated => Left(pa) case pa: PluginActivated => Left(pa)
case pd: EnableDeactivated => Right(pd) case pd: EnableDeactivated => Right(pd)
} }
val activePrefix = if(activated.nonEmpty) s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n" else "" val activePrefix = if(activated.nonEmpty) s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n" else ""
activePrefix + debugDeactivated(notFoundKey, deactivated) activePrefix + debugDeactivated(notFoundKey, deactivated)
} }
private[this] def debugDeactivated(notFoundKey: String, deactivated: Seq[EnableDeactivated]): String = private[this] def debugDeactivated(notFoundKey: String, deactivated: Seq[EnableDeactivated]): String =
{ {
val (impossible, possible) = Util.separate(deactivated) { val (impossible, possible) = Util.separate(deactivated) {
case pi: PluginImpossible => Left(pi) case pi: PluginImpossible => Left(pi)
case pr: PluginRequirements => Right(pr) case pr: PluginRequirements => Right(pr)
} }
if(possible.nonEmpty) { if(possible.nonEmpty) {
val explained = possible.map(explainPluginEnable) val explained = possible.map(explainPluginEnable)
val possibleString = val possibleString =
if(explained.size > 1) explained.zipWithIndex.map{case (s,i) => s"$i. $s"}.mkString("Multiple plugins are available that can provide $notFoundKey:\n", "\n", "") if(explained.size > 1) explained.zipWithIndex.map{case (s,i) => s"$i. $s"}.mkString("Multiple plugins are available that can provide $notFoundKey:\n", "\n", "")
else s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.mkString}" else s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.mkString}"
def impossiblePlugins = impossible.map(_.plugin.label).mkString(", ") def impossiblePlugins = impossible.map(_.plugin.label).mkString(", ")
val imPostfix = if(impossible.isEmpty) "" else s"\n\nThere are other available plugins that provide $notFoundKey, but they are impossible to add: $impossiblePlugins" val imPostfix = if(impossible.isEmpty) "" else s"\n\nThere are other available plugins that provide $notFoundKey, but they are impossible to add: $impossiblePlugins"
possibleString + imPostfix possibleString + imPostfix
} }
else if(impossible.isEmpty) else if(impossible.isEmpty)
s"No available plugin provides key $notFoundKey." s"No available plugin provides key $notFoundKey."
else { else {
val explanations = impossible.map(explainPluginEnable) val explanations = impossible.map(explainPluginEnable)
explanations.mkString(s"Plugins are available that could provide $notFoundKey, but they are impossible to add:\n\t", "\n\t", "") explanations.mkString(s"Plugins are available that could provide $notFoundKey, but they are impossible to add:\n\t", "\n\t", "")
} }
} }
/** Text that suggests how to activate [[plugin]] in [[context]] if possible and if it is not already activated.*/ /** Text that suggests how to activate [[plugin]] in [[context]] if possible and if it is not already activated.*/
def help(plugin: AutoPlugin, context: Context): String = def help(plugin: AutoPlugin, context: Context): String =
if(context.enabled.contains(plugin)) if (context.enabled.contains(plugin)) activatedHelp(plugin)
activatedHelp(plugin) else deactivatedHelp(plugin, context)
else private def activatedHelp(plugin: AutoPlugin): String =
deactivatedHelp(plugin, context) {
private def activatedHelp(plugin: AutoPlugin): String = val prefix = s"${plugin.label} is activated."
{ val keys = provided.forward(plugin)
val prefix = s"${plugin.label} is activated." val keysString = if(keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}"
val keys = provided.forward(plugin) val configs = plugin.projectConfigurations
val keysString = if(keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}" val confsString = if(configs.isEmpty) "" else s"\nIt defines these configurations: ${multi(configs.map(_.name))}"
val configs = plugin.projectConfigurations prefix + keysString + confsString
val confsString = if(configs.isEmpty) "" else s"\nIt defines these configurations: ${multi(configs.map(_.name))}" }
prefix + keysString + confsString private def deactivatedHelp(plugin: AutoPlugin, context: Context): String =
} {
private def deactivatedHelp(plugin: AutoPlugin, context: Context): String = val prefix = s"${plugin.label} is NOT activated."
{ val keys = provided.forward(plugin)
val prefix = s"${plugin.label} is NOT activated." val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}"
val keys = provided.forward(plugin) val configs = plugin.projectConfigurations
val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}" val confsString = if(configs.isEmpty) "" else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}"
val configs = plugin.projectConfigurations val toActivate = explainPluginEnable(pluginEnable(context, plugin))
val confsString = if(configs.isEmpty) "" else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}" s"$prefix$keysString$confsString\n$toActivate"
val toActivate = explainPluginEnable(pluginEnable(context, plugin)) }
s"$prefix$keysString$confsString\n$toActivate"
}
private[this] def multi(strs: Seq[String]): String = strs.mkString(if(strs.size > 4) "\n\t" else ", ") private[this] def multi(strs: Seq[String]): String = strs.mkString(if(strs.size > 4) "\n\t" else ", ")
} }
private[sbt] object PluginsDebug private[sbt] object PluginsDebug {
{ def helpAll(s: State): String =
def helpAll(s: State): String = if(Project.isProjectLoaded(s))
if(Project.isProjectLoaded(s)) {
{ val extracted = Project.extract(s)
val extracted = Project.extract(s) import extracted._
import extracted._ def helpBuild(uri: URI, build: LoadedBuildUnit): String =
def helpBuild(uri: URI, build: LoadedBuildUnit): String = {
{ val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield {
val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield { val activatedIn = build.defined.values.toList.filter(_.autoPlugins.contains(plugin)).map(_.id)
val activatedIn = build.defined.values.toList.filter(_.autoPlugins.contains(plugin)).map(_.id) val actString = if(activatedIn.nonEmpty) activatedIn.mkString(": enabled in ", ", ", "") else "" // TODO: deal with large builds
val actString = if(activatedIn.nonEmpty) activatedIn.mkString(": enabled in ", ", ", "") else "" // TODO: deal with large builds s"\n\t${plugin.label}$actString"
s"\n\t${plugin.label}$actString" }
} s"In $uri${pluginStrings.mkString}"
s"In $uri${pluginStrings.mkString}" }
} val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build)
val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build) buildStrings.mkString("\n")
buildStrings.mkString("\n") }
} else "No project is currently loaded."
else
"No project is currently loaded."
def autoPluginMap(s: State): Map[String, AutoPlugin] = def autoPluginMap(s: State): Map[String, AutoPlugin] =
{ {
val extracted = Project.extract(s) val extracted = Project.extract(s)
import extracted._ import extracted._
structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap
} }
private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] = private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] =
build.unit.plugins.detected.autoPlugins map {_.value} build.unit.plugins.detected.autoPlugins map {_.value}
def help(plugin: AutoPlugin, s: State): String = def help(plugin: AutoPlugin, s: State): String =
{ {
val extracted = Project.extract(s) val extracted = Project.extract(s)
import extracted._ import extracted._
def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin) def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin)
def projectForRef(ref: ProjectRef): ResolvedProject = get(Keys.thisProject in ref) def projectForRef(ref: ProjectRef): ResolvedProject = get(Keys.thisProject in ref)
val perBuild: Map[URI, Set[AutoPlugin]] = structure.units.mapValues(unit => availableAutoPlugins(unit).toSet) val perBuild: Map[URI, Set[AutoPlugin]] = structure.units.mapValues(unit => availableAutoPlugins(unit).toSet)
val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList
lazy val context = Context(currentProject.plugins, currentProject.autoPlugins, Plugins.deducer(pluginsThisBuild), pluginsThisBuild, s.log) lazy val context = Context(currentProject.plugins, currentProject.autoPlugins, Plugins.deducer(pluginsThisBuild), pluginsThisBuild, s.log)
lazy val debug = PluginsDebug(context.available) lazy val debug = PluginsDebug(context.available)
if(!pluginsThisBuild.contains(plugin)) { if(!pluginsThisBuild.contains(plugin)) {
val availableInBuilds: List[URI] = perBuild.toList.filter(_._2(plugin)).map(_._1) val availableInBuilds: List[URI] = perBuild.toList.filter(_._2(plugin)).map(_._1)
s"Plugin ${plugin.label} is only available in builds:\n\t${availableInBuilds.mkString("\n\t")}\nSwitch to a project in one of those builds using `project` and rerun this command for more information." s"Plugin ${plugin.label} is only available in builds:\n\t${availableInBuilds.mkString("\n\t")}\nSwitch to a project in one of those builds using `project` and rerun this command for more information."
} else if(definesPlugin(currentProject)) } else if(definesPlugin(currentProject))
debug.activatedHelp(plugin) debug.activatedHelp(plugin)
else { else {
val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil) val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil)
val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref))) val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref)))
if(definedInAggregated.nonEmpty) { if(definedInAggregated.nonEmpty) {
val projectNames = definedInAggregated.map(_.project) // TODO: usually in this build, but could technically require the build to be qualified val projectNames = definedInAggregated.map(_.project) // TODO: usually in this build, but could technically require the build to be qualified
s"Plugin ${plugin.label} is not activated on this project, but this project aggregates projects where it is activated:\n\t${projectNames.mkString("\n\t")}" s"Plugin ${plugin.label} is not activated on this project, but this project aggregates projects where it is activated:\n\t${projectNames.mkString("\n\t")}"
} else { } else {
val base = debug.deactivatedHelp(plugin, context) val base = debug.deactivatedHelp(plugin, context)
val aggNote = if(thisAggregated.nonEmpty) "Note: This project aggregates other projects and this" else "Note: This" val aggNote = if(thisAggregated.nonEmpty) "Note: This project aggregates other projects and this" else "Note: This"
val common = " information is for this project only." val common = " information is for this project only."
val helpOther = "To see how to activate this plugin for another project, change to the project using `project <name>` and rerun this command." val helpOther = "To see how to activate this plugin for another project, change to the project using `project <name>` and rerun this command."
s"$base\n$aggNote$common\n$helpOther" s"$base\n$aggNote$common\n$helpOther"
} }
} }
} }
/** Precomputes information for debugging plugins. */ /** Precomputes information for debugging plugins. */
def apply(available: List[AutoPlugin]): PluginsDebug = def apply(available: List[AutoPlugin]): PluginsDebug =
{ {
val keyR = definedKeys(available) val keyR = definedKeys(available)
val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap
new PluginsDebug(available, nameToKey, keyR) new PluginsDebug(available, nameToKey, keyR)
} }
/** The context for debugging a plugin (de)activation. /** The context for debugging a plugin (de)activation.
* @param initial The initially defined [[AutoPlugin]]s. * @param initial The initially defined [[AutoPlugin]]s.
* @param enabled The resulting model. * @param enabled The resulting model.
* @param deducePlugin The function used to compute the model. * @param deducePlugin The function used to compute the model.
* @param available All [[AutoPlugin]]s available for consideration. */ * @param available All [[AutoPlugin]]s available for consideration. */
final case class Context(initial: Plugins, enabled: Seq[AutoPlugin], deducePlugin: (Plugins, Logger) => Seq[AutoPlugin], available: List[AutoPlugin], log: Logger) 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. */ /** Describes the steps to activate a plugin in some context. */
sealed abstract class PluginEnable sealed abstract class PluginEnable
/** Describes a [[plugin]] that is already activated in the [[context]].*/ /** Describes a [[plugin]] that is already activated in the [[context]].*/
final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable
sealed abstract class EnableDeactivated extends PluginEnable sealed abstract class EnableDeactivated extends PluginEnable
/** Describes a [[plugin]] that cannot be activated in a [[context]] due to [[contradictions]] in requirements. */ /** Describes a [[plugin]] that cannot be activated in a [[context]] due to [[contradictions]] in requirements. */
final case class PluginImpossible(plugin: AutoPlugin, context: Context, contradictions: Set[AutoPlugin]) extends EnableDeactivated final case class PluginImpossible(plugin: AutoPlugin, context: Context, contradictions: Set[AutoPlugin]) extends EnableDeactivated
/** Describes the requirements for activating [[plugin]] in [[context]]. /** Describes the requirements for activating [[plugin]] in [[context]].
* @param context The base plugins, exclusions, and ultimately activated plugins * @param context The base plugins, exclusions, and ultimately activated plugins
* @param blockingExcludes Existing exclusions that prevent [[plugin]] from being activated and must be dropped * @param blockingExcludes Existing exclusions that prevent [[plugin]] from being activated and must be dropped
* @param enablingPlugins [[AutoPlugin]]s that are not currently enabled, but need to be enabled for [[plugin]] to activate * @param enablingPlugins [[AutoPlugin]]s that are not currently enabled, but need to be enabled for [[plugin]] to activate
* @param extraEnabledPlugins Plugins that will be enabled as a result of [[plugin]] activating, but are not required for [[plugin]] to activate * @param extraEnabledPlugins Plugins that will be enabled as a result of [[plugin]] activating, but are not required for [[plugin]] to activate
* @param willRemove Plugins that will be deactivated as a result of [[plugin]] activating * @param willRemove Plugins that will be deactivated as a result of [[plugin]] activating
* @param deactivate Describes plugins that must be deactivated for [[plugin]] to activate. These require an explicit exclusion or dropping a transitive [[AutoPlugin]].*/ * @param deactivate Describes plugins that must be deactivated for [[plugin]] to activate. These require an explicit exclusion or dropping a transitive [[AutoPlugin]].*/
final case class PluginRequirements(plugin: AutoPlugin, context: Context, blockingExcludes: Set[AutoPlugin], enablingPlugins: Set[AutoPlugin], extraEnabledPlugins: Set[AutoPlugin], willRemove: Set[AutoPlugin], deactivate: List[DeactivatePlugin]) extends EnableDeactivated final case class PluginRequirements(plugin: AutoPlugin, context: Context, blockingExcludes: Set[AutoPlugin], enablingPlugins: Set[AutoPlugin], extraEnabledPlugins: Set[AutoPlugin], willRemove: Set[AutoPlugin], deactivate: List[DeactivatePlugin]) extends EnableDeactivated
/** Describes a [[plugin]] that must be removed in order to activate another plugin in some context. /** Describes a [[plugin]] that must be removed in order to activate another plugin in some context.
* The [[plugin]] can always be directly, explicitly excluded. * The [[plugin]] can always be directly, explicitly excluded.
* @param removeOneOf If non-empty, removing one of these [[AutoPlugin]]s will deactivate [[plugin]] without affecting the other plugin. If empty, a direct exclusion is required. * @param removeOneOf If non-empty, removing one of these [[AutoPlugin]]s will deactivate [[plugin]] without affecting the other plugin. If empty, a direct exclusion is required.
* @param newlySelected If false, this plugin was selected in the original context. */ * @param newlySelected If false, this plugin was selected in the original context. */
final case class DeactivatePlugin(plugin: AutoPlugin, removeOneOf: Set[AutoPlugin], newlySelected: Boolean) final case class DeactivatePlugin(plugin: AutoPlugin, removeOneOf: Set[AutoPlugin], newlySelected: Boolean)
/** Determines how to enable [[plugin]] in [[context]]. */ /** Determines how to enable [[plugin]] in [[context]]. */
def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable = def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable =
if(context.enabled.contains(plugin)) if(context.enabled.contains(plugin))
PluginActivated(plugin, context) PluginActivated(plugin, context)
else else
enableDeactivated(context, plugin) enableDeactivated(context, plugin)
private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable = private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable = {
{ // deconstruct the context
// deconstruct the context val initialModel = context.enabled.toSet
val initialModel = context.enabled.toSet val initial = flatten(context.initial)
val initial = flatten(context.initial) val initialPlugins = plugins(initial)
val initialPlugins = plugins(initial) val initialExcludes = excludes(initial)
val initialExcludes = excludes(initial)
val minModel = minimalModel(plugin) val minModel = minimalModel(plugin)
/* example 1 /* example 1
A :- B, not C A :- B, not C
C :- D, E C :- D, E
initial: B, D, E initial: B, D, E
propose: drop D or E propose: drop D or E
initial: B, not A initial: B, not A
propose: drop 'not A' propose: drop 'not A'
example 2 example 2
A :- B, not C A :- B, not C
C :- B C :- B
initial: <empty> initial: <empty>
propose: B, exclude C propose: B, exclude C
*/ */
// `plugin` will only be activated when all of these plugins are activated // `plugin` will only be activated when all of these plugins are activated
// Deactivating any one of these would deactivate `plugin`. // Deactivating any one of these would deactivate `plugin`.
val minRequiredPlugins = plugins(minModel) val minRequiredPlugins = plugins(minModel)
// The presence of any one of these plugins would deactivate `plugin` // The presence of any one of these plugins would deactivate `plugin`
val minAbsentPlugins = excludes(minModel).toSet val minAbsentPlugins = excludes(minModel).toSet
// Plugins that must be both activated and deactivated for `plugin` to activate. // Plugins that must be both activated and deactivated for `plugin` to activate.
// A non-empty list here cannot be satisfied and is an error. // A non-empty list here cannot be satisfied and is an error.
val contradictions = minAbsentPlugins & minRequiredPlugins val contradictions = minAbsentPlugins & minRequiredPlugins
if(contradictions.nonEmpty) if(contradictions.nonEmpty) PluginImpossible(plugin, context, contradictions)
PluginImpossible(plugin, context, contradictions) else {
else // Plguins that the user has to add to the currently selected plugins in order to enable `plugin`.
{ val addToExistingPlugins = minRequiredPlugins -- initialPlugins
// Plguins that the user has to add to the currently selected plugins in order to enable `plugin`.
val addToExistingPlugins = minRequiredPlugins -- initialPlugins
// Plugins that are currently excluded that need to be allowed. // Plugins that are currently excluded that need to be allowed.
val blockingExcludes = initialExcludes & minRequiredPlugins val blockingExcludes = initialExcludes & minRequiredPlugins
// The model that results when the minimal plugins are enabled and the minimal plugins are excluded. // 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` // This can include more plugins than just `minRequiredPlugins` because the plguins required for `plugin`
// might activate other plugins as well. // might activate other plugins as well.
val modelForMin = context.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log) val modelForMin = context.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log)
val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins)) val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins))
val incrementalModel = context.deducePlugin(incrementalInputs, context.log).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`. // 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. // These could be excluded and `plugin` and the user's current plugins would still be activated.
val extraPlugins = incrementalModel.toSet -- minRequiredPlugins -- initialModel val extraPlugins = incrementalModel.toSet -- minRequiredPlugins -- initialModel
// Plugins that will no longer be enabled as a result of enabling `plugin`. // Plugins that will no longer be enabled as a result of enabling `plugin`.
val willRemove = initialModel -- incrementalModel val willRemove = initialModel -- incrementalModel
// Determine the plugins that must be independently deactivated. // Determine the plugins that must be independently deactivated.
// If both A and B must be deactivated, but A transitively depends on B, deactivating B will deactivate A. // 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. // 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. // 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.requires, incrementalModel)) val minDeactivate = minAbsentPlugins.filter(p => Plugins.satisfied(p.requires, incrementalModel))
val deactivate = for(d <- minDeactivate.toList) yield { val deactivate = for(d <- minDeactivate.toList) yield {
// removing any one of these plugins will deactivate `d`. TODO: This is not an especially efficient implementation. // removing any one of these plugins will deactivate `d`. TODO: This is not an especially efficient implementation.
val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins
val newlySelected = !initialModel(d) val newlySelected = !initialModel(d)
// a. suggest removing a plugin in removeOneToDeactivate to deactivate d // a. suggest removing a plugin in removeOneToDeactivate to deactivate d
// b. suggest excluding `d` to directly deactivate it in any case // b. suggest excluding `d` to directly deactivate it in any case
// c. note whether d was already activated (in context.enabled) or is newly selected // c. note whether d was already activated (in context.enabled) or is newly selected
DeactivatePlugin(d, removeToDeactivate, newlySelected) DeactivatePlugin(d, removeToDeactivate, newlySelected)
} }
PluginRequirements(plugin, context, blockingExcludes, addToExistingPlugins, extraPlugins, willRemove, deactivate) PluginRequirements(plugin, context, blockingExcludes, addToExistingPlugins, extraPlugins, willRemove, deactivate)
} }
} }
private[this] def includeAll[T <: Basic](basic: Set[T]): Plugins = And(basic.toList) private[this] def includeAll[T <: Basic](basic: Set[T]): Plugins = And(basic.toList)
private[this] def excludeAll(plugins: Set[AutoPlugin]): Plugins = And(plugins map (p => Exclude(p)) toList) private[this] def excludeAll(plugins: Set[AutoPlugin]): Plugins = And(plugins map (p => Exclude(p)) toList)
private[this] def excludes(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case Exclude(b) => b }.toSet private[this] def excludes(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case Exclude(b) => b }.toSet
private[this] def plugins(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case n: AutoPlugin => n }.toSet private[this] def plugins(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case n: AutoPlugin => n }.toSet
// If there is a model that includes `plugin`, it includes at least what is returned by this method. // If there is a model that includes `plugin`, it includes at least what is returned by this method.
// This is the list of plugins that must be included as well as list of plugins that must not be present. // This is the list of plugins that must be included as well as list of plugins that must not be present.
// It might not be valid, such as if there are contradictions or if there are cycles that are unsatisfiable. // It might not be valid, such as if there are contradictions or if there are cycles that are unsatisfiable.
// The actual model might be larger, since other plugins might be enabled by the selected plugins. // 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) { private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) {
case _: Exclude => Nil case _: Exclude => Nil
case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin
} }
/** String representation of [[PluginEnable]], intended for end users. */ /** String representation of [[PluginEnable]], intended for end users. */
def explainPluginEnable(ps: PluginEnable): String = def explainPluginEnable(ps: PluginEnable): String =
ps match { ps match {
case PluginRequirements(plugin, context, blockingExcludes, enablingPlugins, extraEnabledPlugins, toBeRemoved, deactivate) => case PluginRequirements(plugin, context, blockingExcludes, enablingPlugins, extraEnabledPlugins, toBeRemoved, deactivate) =>
def indent(str: String) = if(str.isEmpty) "" else s"\t$str" def indent(str: String) = if(str.isEmpty) "" else s"\t$str"
def note(str: String) = if(str.isEmpty) "" else s"Note: $str" def note(str: String) = if(str.isEmpty) "" else s"Note: $str"
val parts = val parts =
indent(excludedError(false /* TODO */, blockingExcludes.toList)) :: indent(excludedError(false /* TODO */, blockingExcludes.toList)) ::
indent(required(enablingPlugins.toList)) :: indent(required(enablingPlugins.toList)) ::
indent(needToDeactivate(deactivate)) :: indent(needToDeactivate(deactivate)) ::
note(willAdd(plugin, extraEnabledPlugins.toList)) :: note(willAdd(plugin, extraEnabledPlugins.toList)) ::
note(willRemove(plugin, toBeRemoved.toList)) :: note(willRemove(plugin, toBeRemoved.toList)) ::
Nil Nil
parts.filterNot(_.isEmpty).mkString("\n") parts.filterNot(_.isEmpty).mkString("\n")
case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions) case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions)
case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated." case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated."
} }
/** Provides a [[Relation]] between plugins and the keys they potentially define. /** Provides a [[Relation]] between plugins and the keys they potentially define.
* Because plugins can define keys in different scopes and keys can be overridden, this is not definitive.*/ * Because plugins can define keys in different scopes and keys can be overridden, this is not definitive.*/
def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] = def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] =
{ {
def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] = def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] =
ss.map(_.key.key) ss.map(_.key.key)
def allSettings(p: AutoPlugin): Seq[Setting[_]] = p.projectSettings ++ p.buildSettings ++ p.globalSettings def allSettings(p: AutoPlugin): Seq[Setting[_]] = p.projectSettings ++ p.buildSettings ++ p.globalSettings
val empty = Relation.empty[AutoPlugin, AttributeKey[_]] val empty = Relation.empty[AutoPlugin, AttributeKey[_]]
(empty /: available)( (r,p) => r + (p, extractDefinedKeys(allSettings(p))) ) (empty /: available)( (r,p) => r + (p, extractDefinedKeys(allSettings(p))) )
} }
private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String = private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String =
str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive)) str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive))
private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) = private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) =
s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded." s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded."
private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) = private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) =
s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies).mkString("\n\t")}" s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies).mkString("\n\t")}"
private[this] def transitiveString(transitive: Boolean) = private[this] def transitiveString(transitive: Boolean) =
if(transitive) "(transitive) " else "" if(transitive) "(transitive) " else ""
private[this] def required(plugins: List[AutoPlugin]): String = private[this] def required(plugins: List[AutoPlugin]): String =
str(plugins)(requiredPlugin, requiredPlugins) str(plugins)(requiredPlugin, requiredPlugins)
private[this] def requiredPlugin(plugin: AutoPlugin) = private[this] def requiredPlugin(plugin: AutoPlugin) =
s"Required plugin ${plugin.label} not present." s"Required plugin ${plugin.label} not present."
private[this] def requiredPlugins(plugins: List[AutoPlugin]) = private[this] def requiredPlugins(plugins: List[AutoPlugin]) =
s"Required plugins not present:\n\t${plugins.map(_.label).mkString("\n\t")}" s"Required plugins not present:\n\t${plugins.map(_.label).mkString("\n\t")}"
private[this] def str[A](list: List[A])(f: A => String, fs: List[A] => String): String = list match { private[this] def str[A](list: List[A])(f: A => String, fs: List[A] => String): String =
case Nil => "" list match {
case single :: Nil => f(single) case Nil => ""
case _ => fs(list) case single :: Nil => f(single)
} case _ => fs(list)
}
private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String = private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String =
str(plugins)(willAddPlugin(base), willAddPlugins(base)) str(plugins)(willAddPlugin(base), willAddPlugins(base))
private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) = private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) =
s"Enabling ${base.label} will also enable ${plugin.label}" s"Enabling ${base.label} will also enable ${plugin.label}"
private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) = private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}" s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}"
private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String = private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String =
str(plugins)(willRemovePlugin(base), willRemovePlugins(base)) str(plugins)(willRemovePlugin(base), willRemovePlugins(base))
private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) = private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) =
s"Enabling ${base.label} will disable ${plugin.label}" s"Enabling ${base.label} will disable ${plugin.label}"
private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) = private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}" s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}"
private[this] def labels(plugins: List[AutoPlugin]): List[String] = private[this] def labels(plugins: List[AutoPlugin]): List[String] =
plugins.map(_.label) plugins.map(_.label)
private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String = private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String =
str(deactivate)(deactivate1, deactivateN) str(deactivate)(deactivate1, deactivateN)
private[this] def deactivateN(plugins: List[DeactivatePlugin]): String = private[this] def deactivateN(plugins: List[DeactivatePlugin]): String =
plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "") plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "")
private[this] def deactivate1(deactivate: DeactivatePlugin): String = private[this] def deactivate1(deactivate: DeactivatePlugin): String =
s"Need to deactivate ${deactivateString(deactivate)}" s"Need to deactivate ${deactivateString(deactivate)}"
private[this] def deactivateString(d: DeactivatePlugin): String = private[this] def deactivateString(d: DeactivatePlugin): String =
{ {
val removePluginsString: String = val removePluginsString: String =
d.removeOneOf.toList match { d.removeOneOf.toList match {
case Nil => "" case Nil => ""
case x :: Nil => s" or no longer include $x" case x :: Nil => s" or no longer include $x"
case xs => s" or remove one of ${xs.mkString(", ")}" case xs => s" or remove one of ${xs.mkString(", ")}"
} }
s"${d.plugin.label}: directly exclude it${removePluginsString}" s"${d.plugin.label}: directly exclude it${removePluginsString}"
} }
private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String = private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String =
str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin)) str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin))
private[this] def pluginImpossible1(plugin: AutoPlugin)(contradiction: AutoPlugin): String = private[this] def pluginImpossible1(plugin: AutoPlugin)(contradiction: AutoPlugin): String =
s"There is no way to enable plugin ${plugin.label}. It (or its dependencies) requires plugin ${contradiction.label} to both be present and absent. Please report the problem to the plugin's author." s"There is no way to enable plugin ${plugin.label}. It (or its dependencies) requires plugin ${contradiction.label} to both be present and absent. Please report the problem to the plugin's author."
private[this] def pluginImpossibleN(plugin: AutoPlugin)(contradictions: List[AutoPlugin]): String = private[this] def pluginImpossibleN(plugin: AutoPlugin)(contradictions: List[AutoPlugin]): String =
s"There is no way to enable plugin ${plugin.label}. It (or its dependencies) requires these plugins to be both present and absent:\n\t${labels(contradictions).mkString("\n\t")}\nPlease report the problem to the plugin's author." s"There is no way to enable plugin ${plugin.label}. It (or its dependencies) requires these plugins to be both present and absent:\n\t${labels(contradictions).mkString("\n\t")}\nPlease report the problem to the plugin's author."
} }

View File

@ -44,6 +44,7 @@
[1488]: https://github.com/sbt/sbt/pull/1488 [1488]: https://github.com/sbt/sbt/pull/1488
[1489]: https://github.com/sbt/sbt/pull/1489 [1489]: https://github.com/sbt/sbt/pull/1489
[1494]: https://github.com/sbt/sbt/pull/1494 [1494]: https://github.com/sbt/sbt/pull/1494
[1516]: https://github.com/sbt/sbt/pull/1516
[@dansanduleac]: https://github.com/dansanduleac [@dansanduleac]: https://github.com/dansanduleac
[@2m]: https://github.com/2m [@2m]: https://github.com/2m
@ -87,6 +88,7 @@
- Allows keys defined inside `build.sbt` to be used from sbt shell. [#1059][1059]/[#1456][1456] - Allows keys defined inside `build.sbt` to be used from sbt shell. [#1059][1059]/[#1456][1456]
- Updates internal Ivy instance to cache the results of dependency exclusion rules. [#1476][1476] by [@eed3si9n][@eed3si9n] - Updates internal Ivy instance to cache the results of dependency exclusion rules. [#1476][1476] by [@eed3si9n][@eed3si9n]
- Adds `Resolver.jcenterRepo` and `Resolver.bintrayRepo(owner, repo)` to add Bintray easier. [#1405][1405] by [@evgeny-goldin][@evgeny-goldin] - Adds `Resolver.jcenterRepo` and `Resolver.bintrayRepo(owner, repo)` to add Bintray easier. [#1405][1405] by [@evgeny-goldin][@evgeny-goldin]
- AutoPlugins with no requirements enabled by allRequirements can now be disable dby the user. [#1516][1516] by [@jsuereth]
### Bug fixes ### Bug fixes
@ -103,6 +105,7 @@
- Fixes `Scope.parseScopedKey`. [#1384][1384] by [@eed3si9n][@eed3si9n] - Fixes `Scope.parseScopedKey`. [#1384][1384] by [@eed3si9n][@eed3si9n]
- Fixes `build.sbt` errors causing `ArrayIndexOutOfBoundsException` due to invalid source in position. [#1181][1181] by [@eed3si9n][@eed3si9n] - Fixes `build.sbt` errors causing `ArrayIndexOutOfBoundsException` due to invalid source in position. [#1181][1181] by [@eed3si9n][@eed3si9n]
### Maven Central Repository defaults to HTTPS ### Maven Central Repository defaults to HTTPS
Thanks to Sonatype, HTTPS access to Maven Central Repository is available to public. This is now enabled by default, but if HTTP is required for some reason the following system properties can be used: Thanks to Sonatype, HTTPS access to Maven Central Repository is available to public. This is now enabled by default, but if HTTP is required for some reason the following system properties can be used:

View File

@ -25,6 +25,10 @@ lazy val projH = project.enablePlugins(TopB)
lazy val projI = project.enablePlugins(TopC) lazy val projI = project.enablePlugins(TopC)
// Tests that we can disable an auto-enabled root plugin
lazy val disableAutoNoRequirePlugin = project.disablePlugins(OrgPlugin)
disablePlugins(plugins.IvyPlugin) disablePlugins(plugins.IvyPlugin)
check := { check := {