diff --git a/main/src/main/scala/sbt/Plugins.scala b/main/src/main/scala/sbt/Plugins.scala index 52bcd288a..6d789ac18 100644 --- a/main/src/main/scala/sbt/Plugins.scala +++ b/main/src/main/scala/sbt/Plugins.scala @@ -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 */ - import logic.{Atom, Clause, Clauses, Formula, Literal, Logic, Negated} - import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException} - import Def.Setting - import Plugins._ - import annotation.tailrec +import logic.{Atom, Clause, Clauses, Formula, Literal, Logic, Negated} +import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException} +import Def.Setting +import Plugins._ +import annotation.tailrec /** 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(...) 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. */ -abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions -{ - /** 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 enabled. - * - * 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 = noTrigger +abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions { + /** 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 enabled. + * + * 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 = noTrigger - /** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]]. - */ - def requires: Plugins = empty + /** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]]. + */ + 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.*/ - def projectConfigurations: Seq[Configuration] = Nil + /** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/ + def projectConfigurations: Seq[Configuration] = Nil - /** The [[Setting]]s to add in the scope of each project that activates this AutoPlugin. */ - def projectSettings: Seq[Setting[_]] = Nil + /** The [[Setting]]s to add in the scope of each project that activates this AutoPlugin. */ + def projectSettings: Seq[Setting[_]] = Nil - /** 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 - * regardless of how many projects for that build activate this AutoPlugin. */ - def buildSettings: Seq[Setting[_]] = Nil + /** 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 + * regardless of how many projects for that build activate this AutoPlugin. */ + def buildSettings: Seq[Setting[_]] = Nil - /** The [[Setting]]s to add to the global scope exactly once if any project activates this AutoPlugin. */ - def globalSettings: Seq[Setting[_]] = Nil + /** The [[Setting]]s to add to the global scope exactly once if any project activates this AutoPlugin. */ + 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. */ - private[sbt] final def isRoot: Boolean = - requires match { - case Empty => true - case _ => false - } + /** 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 + } - /** 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) + /** 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. * 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) -{ - /** Prepends `p` to the error message derived from `origin`. */ - def withPrefix(p: String) = new AutoPluginException(p + message, origin) +final class AutoPluginException private(val message: String, val origin: Option[LogicException]) extends RuntimeException(message) { + /** Prepends `p` to the error message derived from `origin`. */ + def withPrefix(p: String) = new AutoPluginException(p + message, origin) } -object AutoPluginException -{ - def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None) - def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Plugins.translateMessage(origin), Some(origin)) +object AutoPluginException { + def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None) + 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 +case object NoTrigger extends PluginTrigger /** An expression that matches `AutoPlugin`s. */ sealed trait Plugins { - def && (o: Basic): Plugins + def && (o: Basic): Plugins } -sealed trait PluginsFunctions -{ - /** [[Plugins]] instance that doesn't require any [[Plugins]]s. */ - def empty: Plugins = Plugins.Empty +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 + /** 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 required [[AutoPlugin]] comes before its requiring [[AutoPlugin]].*/ - def deducer(defined0: List[AutoPlugin]): (Plugins, Logger) => Seq[AutoPlugin] = - if(defined0.isEmpty) (_, _) => Nil - else - { - // 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 +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 required [[AutoPlugin]] comes before its requiring [[AutoPlugin]].*/ + def deducer(defined0: List[AutoPlugin]): (Plugins, Logger) => Seq[AutoPlugin] = + if(defined0.isEmpty) (_, _) => Nil + else { + // 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 does not require anything else. - // Avoids the requirement for pure Nature strings *and* possible - // circular dependencies in the logic. - 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) => - 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(", ") + 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 does not require anything else. + // Avoids the requirement for pure Nature strings *and* possible + // circular dependencies in the logic. + val allRequirementsClause = defined.filterNot(_.isRoot).flatMap(d => asRequirementsClauses(d)) + val allEnabledByClause = defined.filterNot(_.isRoot).flatMap(d => asEnabledByClauses(d)) - 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: + // Note: Here is where the function begins. We're given a list of plugins now. + (requestedPlugins, log) => { + def explicitlyDisabled(p: AutoPlugin): Boolean = hasExclude(requestedPlugins, p) + val alwaysEnabled: List[AutoPlugin] = defined.filter(_.isAlwaysEnabled).filterNot(explicitlyDisabled) + 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)]) { + 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} - enabled: ${selected.mkString(", ")} ${listConflicts(conflicting)}""") - } + } - private[sbt] final object Empty extends Plugins { - def &&(o: Basic): Plugins = o - override def toString = "" - } + private[sbt] final object Empty extends Plugins { + def &&(o: Basic): Plugins = o + override def toString = "" + } - /** An included or excluded Nature/Plugin. TODO: better name than Basic. Also, can we dump - * this class. - */ - sealed abstract class Basic extends Plugins { - def &&(o: Basic): Plugins = And(this :: o :: Nil) - } - private[sbt] final case class Exclude(n: AutoPlugin) extends Basic { - override def toString = s"!$n" - } - private[sbt] final case class And(plugins: List[Basic]) extends Plugins { - def &&(o: Basic): Plugins = And(o :: plugins) - override def toString = plugins.mkString(" && ") - } - private[sbt] def and(a: Plugins, b: Plugins) = b match { - case Empty => a - case And(ns) => (a /: ns)(_ && _) - case b: Basic => a && b - } - private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match { - case b: Basic => if(del(b)) Empty else b - case Empty => Empty - case And(ns) => - val removed = ns.filterNot(del) - if(removed.isEmpty) Empty else And(removed) - } + /** An included or excluded Nature/Plugin. TODO: better name than Basic. Also, can we dump + * this class. + */ + sealed abstract class Basic extends Plugins { + def &&(o: Basic): Plugins = And(this :: o :: Nil) + } + private[sbt] final case class Exclude(n: AutoPlugin) extends Basic { + override def toString = s"!$n" + } + private[sbt] final case class And(plugins: List[Basic]) extends Plugins { + def &&(o: Basic): Plugins = And(o :: plugins) + override def toString = plugins.mkString(" && ") + } + private[sbt] def and(a: Plugins, b: Plugins) = b match { + case Empty => a + case And(ns) => (a /: ns)(_ && _) + case b: Basic => a && b + } + private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match { + case b: Basic => if(del(b)) Empty else b + case Empty => Empty + case And(ns) => + val removed = ns.filterNot(del) + if(removed.isEmpty) Empty else And(removed) + } - /** 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 - 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 - } + /** 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 + } + // TODO - This doesn't handle nested AND boolean logic... + private[sbt] def hasExclude(n: Plugins, p: AutoPlugin): Boolean = n match { + case `p` => false + case Exclude(`p`) => true + // TODO - This is stupidly advanced. We do a nested check through possible and-ed + // lists of plugins exclusions to see if the plugin ever winds up in an excluded=true case. + // This would handle things like !!p or !(p && z) + case Exclude(n) => hasInclude(n, p) + 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 { - case And(ns) => convertAll(ns).reduce[Formula](_ && _) - case b: Basic => convertBasic(b) - case Empty => Formula.True - } - private[this] def convertBasic(b: Basic): Literal = b match { - case Exclude(n) => !convertBasic(n) - case a: AutoPlugin => Atom(a.label) - } - private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic + private[this] def convert(n: Plugins): Formula = n match { + case And(ns) => convertAll(ns).reduce[Formula](_ && _) + case b: Basic => convertBasic(b) + case Empty => Formula.True + } + private[this] def convertBasic(b: Basic): Literal = b match { + case Exclude(n) => !convertBasic(n) + case a: AutoPlugin => Atom(a.label) + } + private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic - /** 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) - case ap: AutoPlugin => model(ap) - } + /** 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) + case ap: AutoPlugin => model(ap) + } - private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = { - import reflect.runtime.{universe => ru} - import util.control.Exception.catching - val m = ru.runtimeMirror(loader) - val im = m.reflect(ap) - val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt { - im.symbol.asType.toType.declaration(ru.newTermName("autoImport")) match { - case ru.NoSymbol => false - case sym => sym.asTerm.isGetter || sym.asTerm.isModule - } - } - hasGetterOpt getOrElse false - } + private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = { + import reflect.runtime.{universe => ru} + import util.control.Exception.catching + val m = ru.runtimeMirror(loader) + val im = m.reflect(ap) + val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt { + im.symbol.asType.toType.declaration(ru.newTermName("autoImport")) match { + case ru.NoSymbol => false + case sym => sym.asTerm.isGetter || sym.asTerm.isModule + } + } + hasGetterOpt getOrElse false + } } diff --git a/main/src/main/scala/sbt/PluginsDebug.scala b/main/src/main/scala/sbt/PluginsDebug.scala index bace7b52b..d15a238a8 100644 --- a/main/src/main/scala/sbt/PluginsDebug.scala +++ b/main/src/main/scala/sbt/PluginsDebug.scala @@ -1,382 +1,375 @@ package sbt - import Def.Setting - import Plugins._ - import PluginsDebug._ - import java.net.URI + import Def.Setting + import Plugins._ + import PluginsDebug._ + import java.net.URI -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`. - * 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 { - case None => Set.empty - case Some(key) => provided.reverse(key) - } - /** Describes alternative approaches for defining key [[keyName]] in [[context]].*/ - def toEnable(keyName: String, context: Context): List[PluginEnable] = - providers(keyName).toList.map(plugin => pluginEnable(context, plugin)) +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`. + * 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 { + case None => Set.empty + case Some(key) => provided.reverse(key) + } + /** Describes alternative approaches for defining key [[keyName]] in [[context]].*/ + def toEnable(keyName: String, context: Context): List[PluginEnable] = + providers(keyName).toList.map(plugin => pluginEnable(context, plugin)) - /** Provides text to suggest how [[notFoundKey]] can be defined in [[context]]. */ - def debug(notFoundKey: String, context: Context): String = - { - val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) { - case pa: PluginActivated => Left(pa) - case pd: EnableDeactivated => Right(pd) - } - val activePrefix = if(activated.nonEmpty) s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n" else "" - activePrefix + debugDeactivated(notFoundKey, deactivated) - } - private[this] def debugDeactivated(notFoundKey: String, deactivated: Seq[EnableDeactivated]): String = - { - val (impossible, possible) = Util.separate(deactivated) { - case pi: PluginImpossible => Left(pi) - case pr: PluginRequirements => Right(pr) - } - if(possible.nonEmpty) { - val explained = possible.map(explainPluginEnable) - 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", "") - else s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.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" - possibleString + imPostfix - } - else if(impossible.isEmpty) - s"No available plugin provides key $notFoundKey." - else { - 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", "") - } - } + /** Provides text to suggest how [[notFoundKey]] can be defined in [[context]]. */ + def debug(notFoundKey: String, context: Context): String = + { + val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) { + case pa: PluginActivated => Left(pa) + case pd: EnableDeactivated => Right(pd) + } + val activePrefix = if(activated.nonEmpty) s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n" else "" + activePrefix + debugDeactivated(notFoundKey, deactivated) + } + private[this] def debugDeactivated(notFoundKey: String, deactivated: Seq[EnableDeactivated]): String = + { + val (impossible, possible) = Util.separate(deactivated) { + case pi: PluginImpossible => Left(pi) + case pr: PluginRequirements => Right(pr) + } + if(possible.nonEmpty) { + val explained = possible.map(explainPluginEnable) + 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", "") + else s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.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" + possibleString + imPostfix + } + else if(impossible.isEmpty) + s"No available plugin provides key $notFoundKey." + else { + 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", "") + } + } - /** 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 = - if(context.enabled.contains(plugin)) - activatedHelp(plugin) - else - deactivatedHelp(plugin, context) - private def activatedHelp(plugin: AutoPlugin): String = - { - val prefix = s"${plugin.label} is activated." - val keys = provided.forward(plugin) - val keysString = if(keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}" - val configs = plugin.projectConfigurations - 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 = - { - val prefix = s"${plugin.label} is NOT activated." - val keys = provided.forward(plugin) - val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}" - val configs = plugin.projectConfigurations - val confsString = if(configs.isEmpty) "" else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}" - val toActivate = explainPluginEnable(pluginEnable(context, plugin)) - s"$prefix$keysString$confsString\n$toActivate" - } + /** 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 = + if (context.enabled.contains(plugin)) activatedHelp(plugin) + else deactivatedHelp(plugin, context) + private def activatedHelp(plugin: AutoPlugin): String = + { + val prefix = s"${plugin.label} is activated." + val keys = provided.forward(plugin) + val keysString = if(keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}" + val configs = plugin.projectConfigurations + 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 = + { + val prefix = s"${plugin.label} is NOT activated." + val keys = provided.forward(plugin) + val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}" + val configs = plugin.projectConfigurations + val confsString = if(configs.isEmpty) "" else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}" + 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 -{ - def helpAll(s: State): String = - if(Project.isProjectLoaded(s)) - { - val extracted = Project.extract(s) - import extracted._ - def helpBuild(uri: URI, build: LoadedBuildUnit): String = - { - val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield { - 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 - s"\n\t${plugin.label}$actString" - } - s"In $uri${pluginStrings.mkString}" - } - val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build) - buildStrings.mkString("\n") - } - else - "No project is currently loaded." +private[sbt] object PluginsDebug { + def helpAll(s: State): String = + if(Project.isProjectLoaded(s)) + { + val extracted = Project.extract(s) + import extracted._ + def helpBuild(uri: URI, build: LoadedBuildUnit): String = + { + val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield { + 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 + s"\n\t${plugin.label}$actString" + } + s"In $uri${pluginStrings.mkString}" + } + val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build) + buildStrings.mkString("\n") + } + else "No project is currently loaded." - def autoPluginMap(s: State): Map[String, AutoPlugin] = - { - val extracted = Project.extract(s) - import extracted._ - structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap - } - private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] = - build.unit.plugins.detected.autoPlugins map {_.value} + def autoPluginMap(s: State): Map[String, AutoPlugin] = + { + val extracted = Project.extract(s) + import extracted._ + structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap + } + private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] = + build.unit.plugins.detected.autoPlugins map {_.value} - def help(plugin: AutoPlugin, s: State): String = - { - val extracted = Project.extract(s) - import extracted._ - def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin) - 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.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) - 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)) - debug.activatedHelp(plugin) - else { - val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil) - val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref))) - if(definedInAggregated.nonEmpty) { - 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")}" - } else { - val base = debug.deactivatedHelp(plugin, context) - 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 helpOther = "To see how to activate this plugin for another project, change to the project using `project ` and rerun this command." - s"$base\n$aggNote$common\n$helpOther" - } - } - } + def help(plugin: AutoPlugin, s: State): String = + { + val extracted = Project.extract(s) + import extracted._ + def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin) + 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.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) + 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)) + debug.activatedHelp(plugin) + else { + val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil) + val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref))) + if(definedInAggregated.nonEmpty) { + 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")}" + } else { + val base = debug.deactivatedHelp(plugin, context) + 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 helpOther = "To see how to activate this plugin for another project, change to the project using `project ` and rerun this command." + s"$base\n$aggNote$common\n$helpOther" + } + } + } - /** Precomputes information for debugging plugins. */ - def apply(available: List[AutoPlugin]): PluginsDebug = - { - val keyR = definedKeys(available) - val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap - new PluginsDebug(available, nameToKey, keyR) - } + /** Precomputes information for debugging plugins. */ + def apply(available: List[AutoPlugin]): PluginsDebug = + { + val keyR = definedKeys(available) + val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap + new PluginsDebug(available, nameToKey, keyR) + } - /** The context for debugging a plugin (de)activation. - * @param initial The initially defined [[AutoPlugin]]s. - * @param enabled The resulting 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], deducePlugin: (Plugins, Logger) => Seq[AutoPlugin], available: List[AutoPlugin], log: Logger) + /** The context for debugging a plugin (de)activation. + * @param initial The initially defined [[AutoPlugin]]s. + * @param enabled The resulting 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], deducePlugin: (Plugins, Logger) => Seq[AutoPlugin], available: List[AutoPlugin], log: Logger) - /** Describes the steps to activate a plugin in some context. */ - sealed abstract class PluginEnable - /** Describes a [[plugin]] that is already activated in the [[context]].*/ - final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable - sealed abstract class EnableDeactivated extends PluginEnable - /** 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 + /** Describes the steps to activate a plugin in some context. */ + sealed abstract class PluginEnable + /** Describes a [[plugin]] that is already activated in the [[context]].*/ + final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable + sealed abstract class EnableDeactivated extends PluginEnable + /** 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 - /** Describes the requirements for activating [[plugin]] in [[context]]. - * @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 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 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]].*/ - 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 the requirements for activating [[plugin]] in [[context]]. + * @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 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 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]].*/ + 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. - * 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 newlySelected If false, this plugin was selected in the original context. */ - final case class DeactivatePlugin(plugin: AutoPlugin, removeOneOf: Set[AutoPlugin], newlySelected: Boolean) + /** Describes a [[plugin]] that must be removed in order to activate another plugin in some context. + * 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 newlySelected If false, this plugin was selected in the original context. */ + final case class DeactivatePlugin(plugin: AutoPlugin, removeOneOf: Set[AutoPlugin], newlySelected: Boolean) - /** Determines how to enable [[plugin]] in [[context]]. */ - def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable = - if(context.enabled.contains(plugin)) - PluginActivated(plugin, context) - else - enableDeactivated(context, plugin) + /** Determines how to enable [[plugin]] in [[context]]. */ + def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable = + if(context.enabled.contains(plugin)) + PluginActivated(plugin, context) + else + enableDeactivated(context, plugin) - private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable = - { - // deconstruct the context - val initialModel = context.enabled.toSet - val initial = flatten(context.initial) - val initialPlugins = plugins(initial) - val initialExcludes = excludes(initial) + private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable = { + // deconstruct the context + val initialModel = context.enabled.toSet + val initial = flatten(context.initial) + val initialPlugins = plugins(initial) + val initialExcludes = excludes(initial) - val minModel = minimalModel(plugin) + val minModel = minimalModel(plugin) - /* example 1 - A :- B, not C - C :- D, E - initial: B, D, E - propose: drop D or E + /* example 1 + A :- B, not C + C :- D, E + initial: B, D, E + propose: drop D or E - initial: B, not A - propose: drop 'not A' + initial: B, not A + propose: drop 'not A' - example 2 - A :- B, not C - C :- B - initial: - propose: B, exclude C - */ + example 2 + A :- B, not C + C :- B + initial: + propose: B, exclude C + */ - // `plugin` will only be activated when all of these plugins are activated - // Deactivating any one of these would deactivate `plugin`. - val minRequiredPlugins = plugins(minModel) + // `plugin` will only be activated when all of these plugins are activated + // Deactivating any one of these would deactivate `plugin`. + val minRequiredPlugins = plugins(minModel) - // The presence of any one of these plugins would deactivate `plugin` - val minAbsentPlugins = excludes(minModel).toSet + // The presence of any one of these plugins would deactivate `plugin` + val minAbsentPlugins = excludes(minModel).toSet - // Plugins that must be both activated and deactivated for `plugin` to activate. - // A non-empty list here cannot be satisfied and is an error. - val contradictions = minAbsentPlugins & minRequiredPlugins + // Plugins that must be both activated and deactivated for `plugin` to activate. + // A non-empty list here cannot be satisfied and is an error. + val contradictions = minAbsentPlugins & minRequiredPlugins - if(contradictions.nonEmpty) - PluginImpossible(plugin, context, contradictions) - else - { - // Plguins that the user has to add to the currently selected plugins in order to enable `plugin`. - val addToExistingPlugins = minRequiredPlugins -- initialPlugins + if(contradictions.nonEmpty) PluginImpossible(plugin, context, contradictions) + else { + // 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. - val blockingExcludes = initialExcludes & minRequiredPlugins + // Plugins that are currently excluded that need to be allowed. + val blockingExcludes = initialExcludes & minRequiredPlugins - // 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.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log) + // 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.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log) - val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins)) - val incrementalModel = context.deducePlugin(incrementalInputs, context.log).toSet + val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins)) + 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. - val extraPlugins = incrementalModel.toSet -- minRequiredPlugins -- initialModel + // 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. + val extraPlugins = incrementalModel.toSet -- minRequiredPlugins -- initialModel - // Plugins that will no longer be enabled as a result of enabling `plugin`. - val willRemove = initialModel -- incrementalModel + // Plugins that will no longer be enabled as a result of enabling `plugin`. + val willRemove = initialModel -- incrementalModel - // 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 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.requires, incrementalModel)) + // 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 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.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. - val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins - val newlySelected = !initialModel(d) - // a. suggest removing a plugin in removeOneToDeactivate to deactivate d - // 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 - DeactivatePlugin(d, removeToDeactivate, newlySelected) - } + val deactivate = for(d <- minDeactivate.toList) yield { + // removing any one of these plugins will deactivate `d`. TODO: This is not an especially efficient implementation. + val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins + val newlySelected = !initialModel(d) + // a. suggest removing a plugin in removeOneToDeactivate to deactivate d + // 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 + 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 excludeAll(plugins: Set[AutoPlugin]): Plugins = And(plugins map (p => Exclude(p)) 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 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 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 - // 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. - // 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. - private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) { - case _: Exclude => Nil - case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin - } + // 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. + // 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. + private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) { + case _: Exclude => Nil + case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin + } - /** String representation of [[PluginEnable]], intended for end users. */ - def explainPluginEnable(ps: PluginEnable): String = - ps match { - case PluginRequirements(plugin, context, blockingExcludes, enablingPlugins, extraEnabledPlugins, toBeRemoved, deactivate) => - def indent(str: String) = if(str.isEmpty) "" else s"\t$str" - def note(str: String) = if(str.isEmpty) "" else s"Note: $str" - val parts = - indent(excludedError(false /* TODO */, blockingExcludes.toList)) :: - indent(required(enablingPlugins.toList)) :: - indent(needToDeactivate(deactivate)) :: - note(willAdd(plugin, extraEnabledPlugins.toList)) :: - note(willRemove(plugin, toBeRemoved.toList)) :: - Nil - parts.filterNot(_.isEmpty).mkString("\n") - case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions) - case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated." - } + /** String representation of [[PluginEnable]], intended for end users. */ + def explainPluginEnable(ps: PluginEnable): String = + ps match { + case PluginRequirements(plugin, context, blockingExcludes, enablingPlugins, extraEnabledPlugins, toBeRemoved, deactivate) => + def indent(str: String) = if(str.isEmpty) "" else s"\t$str" + def note(str: String) = if(str.isEmpty) "" else s"Note: $str" + val parts = + indent(excludedError(false /* TODO */, blockingExcludes.toList)) :: + indent(required(enablingPlugins.toList)) :: + indent(needToDeactivate(deactivate)) :: + note(willAdd(plugin, extraEnabledPlugins.toList)) :: + note(willRemove(plugin, toBeRemoved.toList)) :: + Nil + parts.filterNot(_.isEmpty).mkString("\n") + case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions) + case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated." + } - /** 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.*/ - def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] = - { - def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] = - ss.map(_.key.key) - def allSettings(p: AutoPlugin): Seq[Setting[_]] = p.projectSettings ++ p.buildSettings ++ p.globalSettings - val empty = Relation.empty[AutoPlugin, AttributeKey[_]] - (empty /: available)( (r,p) => r + (p, extractDefinedKeys(allSettings(p))) ) - } + /** 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.*/ + def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] = + { + def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] = + ss.map(_.key.key) + def allSettings(p: AutoPlugin): Seq[Setting[_]] = p.projectSettings ++ p.buildSettings ++ p.globalSettings + val empty = Relation.empty[AutoPlugin, AttributeKey[_]] + (empty /: available)( (r,p) => r + (p, extractDefinedKeys(allSettings(p))) ) + } - private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String = - str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive)) + private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String = + str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive)) - private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) = - s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded." - private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) = - s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies).mkString("\n\t")}" - private[this] def transitiveString(transitive: Boolean) = - if(transitive) "(transitive) " else "" + private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) = + s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded." + private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) = + s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies).mkString("\n\t")}" + private[this] def transitiveString(transitive: Boolean) = + if(transitive) "(transitive) " else "" - private[this] def required(plugins: List[AutoPlugin]): String = - str(plugins)(requiredPlugin, requiredPlugins) + private[this] def required(plugins: List[AutoPlugin]): String = + str(plugins)(requiredPlugin, requiredPlugins) - private[this] def requiredPlugin(plugin: AutoPlugin) = - s"Required plugin ${plugin.label} not present." - private[this] def requiredPlugins(plugins: List[AutoPlugin]) = - s"Required plugins not present:\n\t${plugins.map(_.label).mkString("\n\t")}" + private[this] def requiredPlugin(plugin: AutoPlugin) = + s"Required plugin ${plugin.label} not present." + private[this] def requiredPlugins(plugins: List[AutoPlugin]) = + 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 { - case Nil => "" - case single :: Nil => f(single) - case _ => fs(list) - } + private[this] def str[A](list: List[A])(f: A => String, fs: List[A] => String): String = + list match { + case Nil => "" + case single :: Nil => f(single) + case _ => fs(list) + } - private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String = - str(plugins)(willAddPlugin(base), willAddPlugins(base)) + private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String = + str(plugins)(willAddPlugin(base), willAddPlugins(base)) - private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) = - s"Enabling ${base.label} will also enable ${plugin.label}" - private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) = - s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}" + private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) = + s"Enabling ${base.label} will also enable ${plugin.label}" + private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) = + s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}" - private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String = - str(plugins)(willRemovePlugin(base), willRemovePlugins(base)) + private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String = + str(plugins)(willRemovePlugin(base), willRemovePlugins(base)) - private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) = - s"Enabling ${base.label} will disable ${plugin.label}" - private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) = - s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}" + private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) = + s"Enabling ${base.label} will disable ${plugin.label}" + private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) = + s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}" - private[this] def labels(plugins: List[AutoPlugin]): List[String] = - plugins.map(_.label) + private[this] def labels(plugins: List[AutoPlugin]): List[String] = + plugins.map(_.label) - private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String = - str(deactivate)(deactivate1, deactivateN) - private[this] def deactivateN(plugins: List[DeactivatePlugin]): String = - plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "") - private[this] def deactivate1(deactivate: DeactivatePlugin): String = - s"Need to deactivate ${deactivateString(deactivate)}" - private[this] def deactivateString(d: DeactivatePlugin): String = - { - val removePluginsString: String = - d.removeOneOf.toList match { - case Nil => "" - case x :: Nil => s" or no longer include $x" - case xs => s" or remove one of ${xs.mkString(", ")}" - } - s"${d.plugin.label}: directly exclude it${removePluginsString}" - } + private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String = + str(deactivate)(deactivate1, deactivateN) + private[this] def deactivateN(plugins: List[DeactivatePlugin]): String = + plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "") + private[this] def deactivate1(deactivate: DeactivatePlugin): String = + s"Need to deactivate ${deactivateString(deactivate)}" + private[this] def deactivateString(d: DeactivatePlugin): String = + { + val removePluginsString: String = + d.removeOneOf.toList match { + case Nil => "" + case x :: Nil => s" or no longer include $x" + case xs => s" or remove one of ${xs.mkString(", ")}" + } + s"${d.plugin.label}: directly exclude it${removePluginsString}" + } - private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String = - str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin)) + private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String = + str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin)) - 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." - 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." + 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." + 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." } \ No newline at end of file diff --git a/notes/0.13.6.md b/notes/0.13.6.md index df703c94d..f04773615 100644 --- a/notes/0.13.6.md +++ b/notes/0.13.6.md @@ -44,6 +44,7 @@ [1488]: https://github.com/sbt/sbt/pull/1488 [1489]: https://github.com/sbt/sbt/pull/1489 [1494]: https://github.com/sbt/sbt/pull/1494 + [1516]: https://github.com/sbt/sbt/pull/1516 [@dansanduleac]: https://github.com/dansanduleac [@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] - 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] +- AutoPlugins with no requirements enabled by allRequirements can now be disable dby the user. [#1516][1516] by [@jsuereth] ### Bug fixes @@ -103,6 +105,7 @@ - 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] + ### 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: diff --git a/sbt/src/sbt-test/project/auto-plugins/build.sbt b/sbt/src/sbt-test/project/auto-plugins/build.sbt index 5fd1c156a..d8dc8d0db 100644 --- a/sbt/src/sbt-test/project/auto-plugins/build.sbt +++ b/sbt/src/sbt-test/project/auto-plugins/build.sbt @@ -25,6 +25,10 @@ lazy val projH = project.enablePlugins(TopB) 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) check := {