Restructure Natures/AutoPlugin types

- remove AutoPlugin.provides
 * name comes from module name
 * AutoPlugin is Nature-like via Basic
- Project.addNatures only accepts varags of Nature values
 * enforces that a user cannot explicitly enable an AutoPlugin
 * drops need for && and - combinators
- Project.excludeNatures accepts varags of AutoPlugin values
 * enforces that only AutoPlugins can be excluded
 * drops need for && and - combinators
This commit is contained in:
Mark Harrah 2014-02-08 10:23:31 -05:00
parent e037731d81
commit 49bf842b3d
7 changed files with 106 additions and 118 deletions

View File

@ -1,97 +0,0 @@
/*
TODO:
- Natured type contains AutoPlugin and Nature
- atoms of AutoPlugin.select are Natured
- atoms of Project.natures are Nature
- no more AutoPlugin.provides: name comes from module name
- index all available AutoPlugins to get the tasks that will be added
- 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
*/
package sbt
import Def.Setting
import logic.Logic.LogicException
/** Marks a top-level object so that sbt will wildcard import it for .sbt files, `consoleProject`, and `set`. */
trait AutoImport
/**
An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation").
The `select` method defines the conditions,
`provides` defines an identifier for the AutoPlugin,
and a method like `projectSettings` defines the settings to add.
Steps for plugin authors:
1. Determine the [[Nature]]s that, when present (or absent), activate the AutoPlugin.
2. Determine the settings/configurations to automatically inject when activated.
3. Define a new, unique identifying [[Nature]] associated with the AutoPlugin, where a Nature is essentially a String ID.
For example, the following will automatically add the settings in `projectSettings`
to a project that has both the `Web` and `Javascript` natures enabled. It will itself
define the `MyStuff` nature. This nature can be explicitly disabled by the user to
prevent the plugin from activating.
object MyPlugin extends AutoPlugin {
def select = Web && Javascript
def provides = MyStuff
override def projectSettings = Seq(...)
}
Steps for users:
1. add dependencies on plugins as usual with addSbtPlugin
2. add Natures to Projects, which will automatically select the plugin settings to add for those Projects.
For example, given natures Web and Javascript (perhaps provided by plugins added with addSbtPlugin),
<Project>.natures( Web && Javascript )
will activate `MyPlugin` defined above and have its settings automatically added. If the user instead defines
<Project>.natures( Web && Javascript && !MyStuff)
then the `MyPlugin` settings (and anything that activates only when `MyStuff` is activated) will not be added.
*/
abstract class AutoPlugin
{
/** This AutoPlugin will be activated for a project when the [[Natures]] matcher returned by this method matches that project's natures
* AND the user does not explicitly exclude the Nature returned by `provides`.
*
* For example, if this method returns `Web && Javascript`, this plugin instance will only be added
* if the `Web` and `Javascript` natures are enabled. */
def select: Natures
/** The unique [[Nature]] for this AutoPlugin instance. This has two purposes:
* 1. The user can explicitly disable this AutoPlugin.
* 2. Other plugins can activate based on whether this AutoPlugin was activated.
*/
def provides: Nature
/** 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 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
// TODO?: def commands: Seq[Command]
}
/** 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)
}
object AutoPluginException
{
def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None)
def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Natures.translateMessage(origin), Some(origin))
}

View File

@ -125,7 +125,7 @@ object BuiltinCommands
def aboutPlugins(e: Extracted): String =
{
def list(b: BuildUnit) = b.plugins.detected.autoPlugins.values.map(_.provides) ++ b.plugins.detected.plugins.names
def list(b: BuildUnit) = b.plugins.detected.autoPlugins.values.map(_.label) ++ b.plugins.detected.plugins.names
val allPluginNames = e.structure.units.values.flatMap(u => list(u.unit)).toSeq.distinct
if(allPluginNames.isEmpty) "" else allPluginNames.mkString("Available Plugins: ", ", ", "")
}

View File

@ -1,9 +1,95 @@
package sbt
/*
TODO:
- index all available AutoPlugins to get the tasks that will be added
- 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 Natures._
/** Marks a top-level object so that sbt will wildcard import it for .sbt files, `consoleProject`, and `set`. */
trait AutoImport
/**
An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation").
The `select` method defines the conditions,
`provides` defines an identifier for the AutoPlugin,
and a method like `projectSettings` defines the settings to add.
Steps for plugin authors:
1. Determine the [[Nature]]s that, when present (or absent), activate the AutoPlugin.
2. Determine the settings/configurations to automatically inject when activated.
3. Define a new, unique identifying [[Nature]] associated with the AutoPlugin, where a Nature is essentially a String ID.
For example, the following will automatically add the settings in `projectSettings`
to a project that has both the `Web` and `Javascript` natures enabled. It will itself
define the `MyStuff` nature. This nature can be explicitly disabled by the user to
prevent the plugin from activating.
object MyPlugin extends AutoPlugin {
def select = Web && Javascript
def provides = MyStuff
override def projectSettings = Seq(...)
}
Steps for users:
1. add dependencies on plugins as usual with addSbtPlugin
2. add Natures to Projects, which will automatically select the plugin settings to add for those Projects.
For example, given natures Web and Javascript (perhaps provided by plugins added with addSbtPlugin),
<Project>.natures( Web && Javascript )
will activate `MyPlugin` defined above and have its settings automatically added. If the user instead defines
<Project>.natures( Web && Javascript && !MyStuff)
then the `MyPlugin` settings (and anything that activates only when `MyStuff` is activated) will not be added.
*/
abstract class AutoPlugin extends Natures.Basic
{
/** This AutoPlugin will be activated for a project when the [[Natures]] matcher returned by this method matches that project's natures
* AND the user does not explicitly exclude the Nature returned by `provides`.
*
* For example, if this method returns `Web && Javascript`, this plugin instance will only be added
* if the `Web` and `Javascript` natures are enabled. */
def select: Natures
val label: String = getClass.getName.stripSuffix("$")
/** 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 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
// TODO?: def commands: Seq[Command]
}
/** 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)
}
object AutoPluginException
{
def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None)
def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Natures.translateMessage(origin), Some(origin))
}
/** An expression that matches `Nature`s. */
sealed trait Natures {
def && (o: Basic): Natures
@ -26,7 +112,7 @@ object Natures
Types.const(Nil)
else
{
val byAtom = defined.map(x => (Atom(x.provides.label), x))
val byAtom = defined.map(x => (Atom(x.label), x))
val byAtomMap = byAtom.toMap
if(byAtom.size != byAtomMap.size) duplicateProvidesError(byAtom)
val clauses = Clauses( defined.map(d => asClause(d)) )
@ -68,8 +154,8 @@ object Natures
sealed abstract class Basic extends Natures {
def &&(o: Basic): Natures = And(this :: o :: Nil)
}
private[sbt] final case class Exclude(n: Nature) extends Basic {
def unary_! : Nature = n
private[sbt] final case class Exclude(n: Basic) extends Basic {
def unary_! : Basic = n
override def toString = s"!$n"
}
private[sbt] final case class And(natures: List[Basic]) extends Natures {
@ -84,7 +170,7 @@ object Natures
/** Defines a clause for `ap` such that the [[Nature]] provided by `ap` is the head and the selector for `ap` is the body. */
private[sbt] def asClause(ap: AutoPlugin): Clause =
Clause( convert(ap.select), Set(Atom(ap.provides.label)) )
Clause( convert(ap.select), Set(Atom(ap.label)) )
private[this] def flatten(n: Natures): Seq[Literal] = n match {
case And(ns) => convertAll(ns)
@ -100,6 +186,7 @@ object Natures
private[this] def convertBasic(b: Basic): Literal = b match {
case Exclude(n) => !convertBasic(n)
case Nature(s) => Atom(s)
case a: AutoPlugin => Atom(a.label)
}
private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic
}

View File

@ -67,7 +67,7 @@ sealed trait ProjectDefinition[PR <: ProjectReference]
val agg = ifNonEmpty("aggregate", aggregate)
val dep = ifNonEmpty("dependencies", dependencies)
val conf = ifNonEmpty("configurations", configurations)
val autos = ifNonEmpty("autoPlugins", autoPlugins.map(_.provides))
val autos = ifNonEmpty("autoPlugins", autoPlugins.map(_.label))
val fields = s"id $id" :: s"base: $base" :: agg ::: dep ::: conf ::: (s"natures: List($natures)" :: autos)
s"Project(${fields.mkString(", ")})"
}
@ -136,11 +136,17 @@ sealed trait Project extends ProjectDefinition[ProjectReference]
* Any configured .sbt files are removed from this project's list.*/
def setSbtFiles(files: File*): Project = copy(auto = AddSettings.append( AddSettings.clearSbtFiles(auto), AddSettings.sbtFiles(files: _*)) )
/** Sets the [[Natures]] of this project.
/** Sets the [[Nature]]s of this project.
A [[Nature]] is a common label that is used by plugins to determine what settings, if any, to add to a project. */
def addNatures(ns: Natures): Project = {
def addNatures(ns: Nature*): Project = setNatures(Natures.and(natures, Natures.And(ns.toList)))
/** Disable the given plugins on this project. */
def disablePlugins(plugins: AutoPlugin*): Project =
setNatures(Natures.and(natures, Natures.And(plugins.map(p => Natures.Exclude(p)).toList)))
private[this] def setNatures(ns: Natures): Project = {
// TODO: for 0.14.0, use copy when it has the additional `natures` parameter
unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, Natures.and(natures, ns), autoPlugins)
unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, ns, autoPlugins)
}
/** Definitively set the [[AutoPlugin]]s for this project. */

View File

@ -1,11 +1,11 @@
// !C will exclude C, and thus D, from being auto-added
lazy val a = project.addNatures(A && B && !C)
// excludePlugins(C) will prevent C, and thus D, from being auto-added
lazy val a = project.addNatures(A, B).disablePlugins(Q)
// without B, C is not added
lazy val b = project.addNatures(A)
// with both A and B, C is selected, which in turn selects D
lazy val c = project.addNatures(A && B)
lazy val c = project.addNatures(A, B)
// with no natures defined, nothing is auto-added
lazy val d = project

View File

@ -6,9 +6,7 @@ object AI extends AutoImport
{
lazy val A = Nature("A")
lazy val B = Nature("B")
lazy val C = Nature("C")
lazy val D = Nature("D")
lazy val E = Nature("E")
lazy val q = config("q")
lazy val p = config("p").extend(q)
@ -25,8 +23,6 @@ object Q extends AutoPlugin
{
def select: Natures = A && B
def provides = C
override def projectConfigurations: Seq[Configuration] =
p ::
q ::
@ -52,9 +48,7 @@ object Q extends AutoPlugin
object R extends AutoPlugin
{
def select = C && !D
def provides = E
def select = Q && !D
override def projectSettings = Seq(
// tests proper ordering: R requires C, so C settings should come first

View File

@ -3,7 +3,6 @@ import Keys._
object C extends AutoImport {
lazy val aN = Nature("A")
lazy val bN = Nature("B")
lazy val check = taskKey[Unit]("Checks that the AutoPlugin and Build are automatically added.")
}
@ -11,7 +10,6 @@ object C extends AutoImport {
import C._
object A extends AutoPlugin {
override def provides = aN
override def select = bN
override def projectSettings = Seq(
check := {}