Merge pull request #1130 from sbt/wip/auto-plugins

AUTO PLUGIN FEATURE!!!!
This commit is contained in:
Josh Suereth 2014-02-20 16:08:01 -05:00
commit a8acc11fb5
27 changed files with 1690 additions and 140 deletions

View File

@ -18,6 +18,7 @@ trait Build
* If None, the root project is the first project in the build's root directory or just the first project if none are in the root directory.*/ * If None, the root project is the first project in the build's root directory or just the first project if none are in the root directory.*/
def rootProject: Option[Project] = None def rootProject: Option[Project] = None
} }
// TODO 0.14.0: decide if Plugin should be deprecated in favor of AutoPlugin
trait Plugin trait Plugin
{ {
@deprecated("Override projectSettings or buildSettings instead.", "0.12.0") @deprecated("Override projectSettings or buildSettings instead.", "0.12.0")

View File

@ -30,23 +30,108 @@ final class StructureIndex(
val keyIndex: KeyIndex, val keyIndex: KeyIndex,
val aggregateKeyIndex: KeyIndex val aggregateKeyIndex: KeyIndex
) )
/** A resolved build unit. (`ResolvedBuildUnit` would be a better name to distinguish it from the loaded, but unresolved `BuildUnit`.)
* @param unit The loaded, but unresolved [[BuildUnit]] this was resolved from.
* @param defined The definitive map from project IDs to resolved projects.
* These projects have had [[Reference]]s resolved and [[AutoPlugin]]s evaluated.
* @param rootProjects The list of project IDs for the projects considered roots of this build.
* The first root project is used as the default in several situations where a project is not otherwise selected.
*/
final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, ResolvedProject], val rootProjects: Seq[String], val buildSettings: Seq[Setting[_]]) extends BuildUnitBase final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, ResolvedProject], val rootProjects: Seq[String], val buildSettings: Seq[Setting[_]]) extends BuildUnitBase
{ {
assert(!rootProjects.isEmpty, "No root projects defined for build unit " + unit) assert(!rootProjects.isEmpty, "No root projects defined for build unit " + unit)
/** The project to use as the default when one is not otherwise selected.
* [[LocalRootProject]] resolves to this from within the same build.*/
val root = rootProjects.head val root = rootProjects.head
/** The base directory of the build unit (not the build definition).*/
def localBase = unit.localBase def localBase = unit.localBase
/** The classpath to use when compiling against this build unit's publicly visible code.
* It includes build definition and plugin classes, but not classes for .sbt file statements and expressions. */
def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath
/** The class loader to use for this build unit's publicly visible code.
* It includes build definition and plugin classes, but not classes for .sbt file statements and expressions. */
def loader = unit.definitions.loader def loader = unit.definitions.loader
/** The imports to use for .sbt files, `consoleProject` and other contexts that use code from the build definition. */
def imports = BuildUtil.getImports(unit) def imports = BuildUtil.getImports(unit)
override def toString = unit.toString override def toString = unit.toString
} }
// TODO: figure out how to deprecate and drop buildNames
/** The built and loaded build definition, including loaded but unresolved [[Project]]s, for a build unit (for a single URI).
*
* @param base The base directory of the build definition, typically `<build base>/project/`.
* @param loader The ClassLoader containing all classes and plugins for the build definition project.
* Note that this does not include classes for .sbt files.
* @param builds The list of [[Build]]s for the build unit.
* In addition to auto-discovered [[Build]]s, this includes any auto-generated default [[Build]]s.
* @param projects The list of all [[Project]]s from all [[Build]]s.
* These projects have not yet been resolved, but they have had auto-plugins applied.
* In particular, each [[Project]]'s `autoPlugins` field is populated according to their configured `natures`
* and their `settings` and `configurations` updated as appropriate.
* @param buildNames No longer used and will be deprecated once feasible.
*/
final class LoadedDefinitions(val base: File, val target: Seq[File], val loader: ClassLoader, val builds: Seq[Build], val projects: Seq[Project], val buildNames: Seq[String]) final class LoadedDefinitions(val base: File, val target: Seq[File], val loader: ClassLoader, val builds: Seq[Build], val projects: Seq[Project], val buildNames: Seq[String])
final class LoadedPlugins(val base: File, val pluginData: PluginData, val loader: ClassLoader, val plugins: Seq[Plugin], val pluginNames: Seq[String])
/** Auto-detected top-level modules (as in `object X`) of type `T` paired with their source names. */
final class DetectedModules[T](val modules: Seq[(String, T)])
{ {
/** The source names of the modules. This is "X" in `object X`, as opposed to the implementation class name "X$".
* The names are returned in a stable order such that `names zip values` pairs a name with the actual module. */
def names: Seq[String] = modules.map(_._1)
/** The singleton value of the module.
* The values are returned in a stable order such that `names zip values` pairs a name with the actual module. */
def values: Seq[T] = modules.map(_._2)
}
/** Auto-discovered modules for the build definition project. These include modules defined in build definition sources
* as well as modules in binary dependencies.
*
* @param builds The [[Build]]s detected in the build definition. This does not include the default [[Build]] that sbt creates if none is defined.
*/
final class DetectedPlugins(val plugins: DetectedModules[Plugin], val autoImports: DetectedModules[AutoImport], val autoPlugins: DetectedModules[AutoPlugin], val builds: DetectedModules[Build])
{
/** Sequence of import expressions for the build definition. This includes the names of the [[Plugin]], [[Build]], and [[AutoImport]] modules, but not the [[AutoPlugin]] modules. */
lazy val imports: Seq[String] = BuildUtil.getImports(plugins.names ++ builds.names ++ autoImports.names)
/** A function to select the right [[AutoPlugin]]s from [[autoPlugins]] given the defined [[Natures]] for a [[Project]]. */
lazy val compileNatures: Natures => Seq[AutoPlugin] = Natures.compile(autoPlugins.values.toList)
}
/** The built and loaded build definition project.
* @param base The base directory for the build definition project (not the base of the project itself).
* @param pluginData Evaluated tasks/settings from the build definition for later use.
* This is necessary because the build definition project is discarded.
* @param loader The class loader for the build definition project, notably excluding classes used for .sbt files.
* @param detected Auto-detected modules in the build definition.
*/
final class LoadedPlugins(val base: File, val pluginData: PluginData, val loader: ClassLoader, val detected: DetectedPlugins)
{
@deprecated("Use the primary constructor.", "0.13.2")
def this(base: File, pluginData: PluginData, loader: ClassLoader, plugins: Seq[Plugin], pluginNames: Seq[String]) =
this(base, pluginData, loader,
new DetectedPlugins(new DetectedModules(pluginNames zip plugins), new DetectedModules(Nil), new DetectedModules(Nil), new DetectedModules(Nil))
)
@deprecated("Use detected.plugins.values.", "0.13.2")
val plugins: Seq[Plugin] = detected.plugins.values
@deprecated("Use detected.plugins.names.", "0.13.2")
val pluginNames: Seq[String] = detected.plugins.names
def fullClasspath: Seq[Attributed[File]] = pluginData.classpath def fullClasspath: Seq[Attributed[File]] = pluginData.classpath
def classpath = data(fullClasspath) def classpath = data(fullClasspath)
} }
/** The loaded, but unresolved build unit.
* @param uri The uniquely identifying URI for the build.
* @param localBase The working location of the build on the filesystem.
* For local URIs, this is the same as `uri`, but for remote URIs, this is the local copy or workspace allocated for the build.
*/
final class BuildUnit(val uri: URI, val localBase: File, val definitions: LoadedDefinitions, val plugins: LoadedPlugins) final class BuildUnit(val uri: URI, val localBase: File, val definitions: LoadedDefinitions, val plugins: LoadedPlugins)
{ {
override def toString = if(uri.getScheme == "file") localBase.toString else (uri + " (locally: " + localBase +")") override def toString = if(uri.getScheme == "file") localBase.toString else (uri + " (locally: " + localBase +")")
@ -57,6 +142,8 @@ final class LoadedBuild(val root: URI, val units: Map[URI, LoadedBuildUnit])
BuildUtil.checkCycles(units) BuildUtil.checkCycles(units)
def allProjectRefs: Seq[(ProjectRef, ResolvedProject)] = for( (uri, unit) <- units.toSeq; (id, proj) <- unit.defined ) yield ProjectRef(uri, id) -> proj def allProjectRefs: Seq[(ProjectRef, ResolvedProject)] = for( (uri, unit) <- units.toSeq; (id, proj) <- unit.defined ) yield ProjectRef(uri, id) -> proj
def extra(data: Settings[Scope])(keyIndex: KeyIndex): BuildUtil[ResolvedProject] = BuildUtil(root, units, keyIndex, data) def extra(data: Settings[Scope])(keyIndex: KeyIndex): BuildUtil[ResolvedProject] = BuildUtil(root, units, keyIndex, data)
private[sbt] def autos = GroupedAutoPlugins(units)
} }
final class PartBuild(val root: URI, val units: Map[URI, PartBuildUnit]) final class PartBuild(val root: URI, val units: Map[URI, PartBuildUnit])
sealed trait BuildUnitBase { def rootProjects: Seq[String]; def buildSettings: Seq[Setting[_]] } sealed trait BuildUnitBase { def rootProjects: Seq[String]; def buildSettings: Seq[Setting[_]] }

View File

@ -35,7 +35,7 @@ final class BuildUtil[Proj](
case _ => None case _ => None
} }
val configurationsForAxis: Option[ResolvedReference] => Seq[String] = val configurationsForAxis: Option[ResolvedReference] => Seq[String] =
refOpt => configurations(projectForAxis(refOpt)).map(_.name) refOpt => configurations(projectForAxis(refOpt)).map(_.name)
} }
object BuildUtil object BuildUtil
@ -48,6 +48,20 @@ object BuildUtil
new BuildUtil(keyIndex, data, root, Load getRootProject units, getp, configs, aggregates) new BuildUtil(keyIndex, data, root, Load getRootProject units, getp, configs, aggregates)
} }
def dependencies(units: Map[URI, LoadedBuildUnit]): BuildDependencies =
{
import collection.mutable.HashMap
val agg = new HashMap[ProjectRef, Seq[ProjectRef]]
val cp = new HashMap[ProjectRef, Seq[ClasspathDep[ProjectRef]]]
for(lbu <- units.values; rp <- lbu.defined.values)
{
val ref = ProjectRef(lbu.unit.uri, rp.id)
cp(ref) = rp.dependencies
agg(ref) = rp.aggregate
}
BuildDependencies(cp.toMap, agg.toMap)
}
def checkCycles(units: Map[URI, LoadedBuildUnit]) def checkCycles(units: Map[URI, LoadedBuildUnit])
{ {
def getRef(pref: ProjectRef) = units(pref.build).defined(pref.project) def getRef(pref: ProjectRef) = units(pref.build).defined(pref.project)
@ -60,8 +74,14 @@ object BuildUtil
} }
} }
def baseImports: Seq[String] = "import sbt._, Keys._" :: Nil def baseImports: Seq[String] = "import sbt._, Keys._" :: Nil
def getImports(unit: BuildUnit): Seq[String] = getImports(unit.plugins.pluginNames, unit.definitions.buildNames)
def getImports(pluginNames: Seq[String], buildNames: Seq[String]): Seq[String] = baseImports ++ importAllRoot(pluginNames ++ buildNames) def getImports(unit: BuildUnit): Seq[String] = unit.plugins.detected.imports
@deprecated("Use getImports(Seq[String]).", "0.13.2")
def getImports(pluginNames: Seq[String], buildNames: Seq[String]): Seq[String] = getImports(pluginNames ++ buildNames)
def getImports(names: Seq[String]): Seq[String] = baseImports ++ importAllRoot(names)
def importAll(values: Seq[String]): Seq[String] = if(values.isEmpty) Nil else values.map( _ + "._" ).mkString("import ", ", ", "") :: Nil def importAll(values: Seq[String]): Seq[String] = if(values.isEmpty) Nil else values.map( _ + "._" ).mkString("import ", ", ", "") :: Nil
def importAllRoot(values: Seq[String]): Seq[String] = importAll(values map rootedName) def importAllRoot(values: Seq[String]): Seq[String] = importAll(values map rootedName)
def rootedName(s: String): String = if(s contains '.') "_root_." + s else s def rootedName(s: String): String = if(s contains '.') "_root_." + s else s

View File

@ -49,6 +49,11 @@ $ShowCommand <task>
Evaluates the specified task and display the value returned by the task.""" Evaluates the specified task and display the value returned by the task."""
val PluginsCommand = "plugins"
val PluginCommand = "plugin"
def pluginsBrief = "Lists currently available plugins."
def pluginsDetailed = pluginsBrief // TODO: expand
val LastCommand = "last" val LastCommand = "last"
val LastGrepCommand = "last-grep" val LastGrepCommand = "last-grep"
val ExportCommand = "export" val ExportCommand = "export"

View File

@ -179,7 +179,7 @@ object Defaults extends BuildCommon
unmanagedResources <<= collectFiles(unmanagedResourceDirectories, includeFilter in unmanagedResources, excludeFilter in unmanagedResources), unmanagedResources <<= collectFiles(unmanagedResourceDirectories, includeFilter in unmanagedResources, excludeFilter in unmanagedResources),
watchSources in ConfigGlobal ++= unmanagedResources.value, watchSources in ConfigGlobal ++= unmanagedResources.value,
resourceGenerators :== Nil, resourceGenerators :== Nil,
resourceGenerators <+= (definedSbtPlugins, resourceManaged) map writePluginsDescriptor, resourceGenerators <+= (discoveredSbtPlugins, resourceManaged) map PluginDiscovery.writeDescriptors,
managedResources <<= generate(resourceGenerators), managedResources <<= generate(resourceGenerators),
resources <<= Classpaths.concat(managedResources, unmanagedResources) resources <<= Classpaths.concat(managedResources, unmanagedResources)
) )
@ -233,6 +233,7 @@ object Defaults extends BuildCommon
consoleQuick <<= consoleQuickTask, consoleQuick <<= consoleQuickTask,
discoveredMainClasses <<= compile map discoverMainClasses storeAs discoveredMainClasses triggeredBy compile, discoveredMainClasses <<= compile map discoverMainClasses storeAs discoveredMainClasses triggeredBy compile,
definedSbtPlugins <<= discoverPlugins, definedSbtPlugins <<= discoverPlugins,
discoveredSbtPlugins <<= discoverSbtPluginNames,
inTask(run)(runnerTask :: Nil).head, inTask(run)(runnerTask :: Nil).head,
selectMainClass := mainClass.value orElse selectRunMain(discoveredMainClasses.value), selectMainClass := mainClass.value orElse selectRunMain(discoveredMainClasses.value),
mainClass in run := (selectMainClass in run).value, mainClass in run := (selectMainClass in run).value,
@ -764,27 +765,21 @@ object Defaults extends BuildCommon
def sbtPluginExtra(m: ModuleID, sbtV: String, scalaV: String): ModuleID = def sbtPluginExtra(m: ModuleID, sbtV: String, scalaV: String): ModuleID =
m.extra(CustomPomParser.SbtVersionKey -> sbtV, CustomPomParser.ScalaVersionKey -> scalaV).copy(crossVersion = CrossVersion.Disabled) m.extra(CustomPomParser.SbtVersionKey -> sbtV, CustomPomParser.ScalaVersionKey -> scalaV).copy(crossVersion = CrossVersion.Disabled)
@deprecated("Use PluginDiscovery.writeDescriptor.", "0.13.2")
def writePluginsDescriptor(plugins: Set[String], dir: File): Seq[File] = def writePluginsDescriptor(plugins: Set[String], dir: File): Seq[File] =
{ PluginDiscovery.writeDescriptor(plugins.toSeq, dir, PluginDiscovery.Paths.Plugins).toList
val descriptor: File = dir / "sbt" / "sbt.plugins"
if(plugins.isEmpty) def discoverSbtPluginNames: Initialize[Task[PluginDiscovery.DiscoveredNames]] = Def.task {
{ if(sbtPlugin.value) PluginDiscovery.discoverSourceAll(compile.value) else PluginDiscovery.emptyDiscoveredNames
IO.delete(descriptor)
Nil
}
else
{
IO.writeLines(descriptor, plugins.toSeq.sorted)
descriptor :: Nil
}
} }
@deprecated("Use discoverSbtPluginNames.", "0.13.2")
def discoverPlugins: Initialize[Task[Set[String]]] = (compile, sbtPlugin, streams) map { (analysis, isPlugin, s) => if(isPlugin) discoverSbtPlugins(analysis, s.log) else Set.empty } def discoverPlugins: Initialize[Task[Set[String]]] = (compile, sbtPlugin, streams) map { (analysis, isPlugin, s) => if(isPlugin) discoverSbtPlugins(analysis, s.log) else Set.empty }
@deprecated("Use PluginDiscovery.sourceModuleNames[Plugin].", "0.13.2")
def discoverSbtPlugins(analysis: inc.Analysis, log: Logger): Set[String] = def discoverSbtPlugins(analysis: inc.Analysis, log: Logger): Set[String] =
{ PluginDiscovery.sourceModuleNames(analysis, classOf[Plugin].getName).toSet
val pluginClass = classOf[Plugin].getName
val discovery = Discovery(Set(pluginClass), Set.empty)( Tests allDefs analysis )
discovery collect { case (df, disc) if (disc.baseClasses contains pluginClass) && disc.isModule => df.name } toSet;
}
def copyResourcesTask = def copyResourcesTask =
(classDirectory, resources, resourceDirectories, streams) map { (target, resrcs, dirs, s) => (classDirectory, resources, resourceDirectories, streams) map { (target, resrcs, dirs, s) =>
@ -1262,19 +1257,8 @@ object Classpaths
if(useJars) Seq(pkgTask).join else psTask if(useJars) Seq(pkgTask).join else psTask
} }
def constructBuildDependencies: Initialize[BuildDependencies] = def constructBuildDependencies: Initialize[BuildDependencies] = loadedBuild(lb => BuildUtil.dependencies(lb.units))
loadedBuild { lb =>
import collection.mutable.HashMap
val agg = new HashMap[ProjectRef, Seq[ProjectRef]]
val cp = new HashMap[ProjectRef, Seq[ClasspathDep[ProjectRef]]]
for(lbu <- lb.units.values; rp <- lbu.defined.values)
{
val ref = ProjectRef(lbu.unit.uri, rp.id)
cp(ref) = rp.dependencies
agg(ref) = rp.aggregate
}
BuildDependencies(cp.toMap, agg.toMap)
}
def internalDependencies: Initialize[Task[Classpath]] = def internalDependencies: Initialize[Task[Classpath]] =
(thisProjectRef, classpathConfiguration, configuration, settingsData, buildDependencies) flatMap internalDependencies0 (thisProjectRef, classpathConfiguration, configuration, settingsData, buildDependencies) flatMap internalDependencies0
def unmanagedDependencies: Initialize[Task[Classpath]] = def unmanagedDependencies: Initialize[Task[Classpath]] =

View File

@ -0,0 +1,20 @@
package sbt
import Def.Setting
import java.net.URI
private[sbt] final class GroupedAutoPlugins(val all: Seq[AutoPlugin], val byBuild: Map[URI, Seq[AutoPlugin]])
{
def globalSettings: Seq[Setting[_]] = all.flatMap(_.globalSettings)
def buildSettings(uri: URI): Seq[Setting[_]] = byBuild.getOrElse(uri, Nil).flatMap(_.buildSettings)
}
private[sbt] object GroupedAutoPlugins
{
private[sbt] def apply(units: Map[URI, LoadedBuildUnit]): GroupedAutoPlugins =
{
val byBuild: Map[URI, Seq[AutoPlugin]] = units.mapValues(unit => unit.defined.values.flatMap(_.autoPlugins).toSeq.distinct).toMap
val all: Seq[AutoPlugin] = byBuild.values.toSeq.flatten.distinct
new GroupedAutoPlugins(all, byBuild)
}
}

View File

@ -131,6 +131,7 @@ object Keys
val crossVersion = SettingKey[CrossVersion]("cross-version", "Configures handling of the Scala version when cross-building.", CSetting) val crossVersion = SettingKey[CrossVersion]("cross-version", "Configures handling of the Scala version when cross-building.", CSetting)
val classpathOptions = SettingKey[ClasspathOptions]("classpath-options", "Configures handling of Scala classpaths.", DSetting) val classpathOptions = SettingKey[ClasspathOptions]("classpath-options", "Configures handling of Scala classpaths.", DSetting)
val definedSbtPlugins = TaskKey[Set[String]]("defined-sbt-plugins", "The set of names of Plugin implementations defined by this project.", CTask) val definedSbtPlugins = TaskKey[Set[String]]("defined-sbt-plugins", "The set of names of Plugin implementations defined by this project.", CTask)
val discoveredSbtPlugins = TaskKey[PluginDiscovery.DiscoveredNames]("discovered-sbt-plugins", "The names of sbt plugin-related modules (modules that extend Build, Plugin, AutoImport, AutoPlugin) defined by this project.", CTask)
val sbtPlugin = SettingKey[Boolean]("sbt-plugin", "If true, enables adding sbt as a dependency and auto-generation of the plugin descriptor file.", BMinusSetting) val sbtPlugin = SettingKey[Boolean]("sbt-plugin", "If true, enables adding sbt as a dependency and auto-generation of the plugin descriptor file.", BMinusSetting)
val printWarnings = TaskKey[Unit]("print-warnings", "Shows warnings from compilation, including ones that weren't printed initially.", BPlusTask) val printWarnings = TaskKey[Unit]("print-warnings", "Shows warnings from compilation, including ones that weren't printed initially.", BPlusTask)
val fileInputOptions = SettingKey[Seq[String]]("file-input-options", "Options that take file input, which may invalidate the cache.", CSetting) val fileInputOptions = SettingKey[Seq[String]]("file-input-options", "Options that take file input, which may invalidate the cache.", CSetting)
@ -348,7 +349,7 @@ object Keys
// Experimental in sbt 0.13.2 to enable grabing semantic compile failures. // Experimental in sbt 0.13.2 to enable grabing semantic compile failures.
private[sbt] val compilerReporter = TaskKey[Option[xsbti.Reporter]]("compilerReporter", "Experimental hook to listen (or send) compilation failure messages.", DTask) private[sbt] val compilerReporter = TaskKey[Option[xsbti.Reporter]]("compilerReporter", "Experimental hook to listen (or send) compilation failure messages.", DTask)
val triggeredBy = Def.triggeredBy val triggeredBy = Def.triggeredBy
val runBefore = Def.runBefore val runBefore = Def.runBefore

View File

@ -6,19 +6,16 @@ package sbt
import java.io.File import java.io.File
import java.net.{URI,URL} import java.net.{URI,URL}
import compiler.{Eval,EvalImports} import compiler.{Eval,EvalImports}
import xsbt.api.{Discovered,Discovery}
import xsbti.compile.CompileOrder
import classpath.ClasspathUtilities import classpath.ClasspathUtilities
import scala.annotation.tailrec import scala.annotation.tailrec
import collection.mutable import collection.mutable
import Compiler.{Compilers,Inputs} import Compiler.Compilers
import inc.{FileValueCache, Locate} import inc.{FileValueCache, Locate}
import Project.{inScope,makeSettings} import Project.{inScope,makeSettings}
import Def.{isDummy, ScopedKey, ScopeLocal, Setting} import Def.{isDummy, ScopedKey, ScopeLocal, Setting}
import Keys.{appConfiguration, baseDirectory, configuration, fullResolvers, fullClasspath, pluginData, streams, thisProject, thisProjectRef, update} import Keys.{appConfiguration, baseDirectory, configuration, fullResolvers, fullClasspath, pluginData, streams, thisProject, thisProjectRef, update}
import Keys.{exportedProducts, loadedBuild, onLoadMessage, resolvedScoped, sbtPlugin, scalacOptions, taskDefinitionKey} import Keys.{exportedProducts, loadedBuild, onLoadMessage, resolvedScoped, sbtPlugin, scalacOptions, taskDefinitionKey}
import tools.nsc.reporters.ConsoleReporter import tools.nsc.reporters.ConsoleReporter
import Build.analyzed
import Attributed.data import Attributed.data
import Scope.{GlobalScope, ThisScope} import Scope.{GlobalScope, ThisScope}
import Types.const import Types.const
@ -180,7 +177,7 @@ object Load
val keys = Index.allKeys(settings) val keys = Index.allKeys(settings)
val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key) val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key)
val scopedKeys = keys ++ data.allKeys( (s,k) => ScopedKey(s,k)) val scopedKeys = keys ++ data.allKeys( (s,k) => ScopedKey(s,k))
val projectsMap = projects.mapValues(_.defined.keySet) val projectsMap = projects.mapValues(_.defined.keySet).toMap
val keyIndex = KeyIndex(scopedKeys, projectsMap) val keyIndex = KeyIndex(scopedKeys, projectsMap)
val aggIndex = KeyIndex.aggregate(scopedKeys, extra(keyIndex), projectsMap) val aggIndex = KeyIndex.aggregate(scopedKeys, extra(keyIndex), projectsMap)
new sbt.StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex) new sbt.StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex)
@ -201,10 +198,10 @@ object Load
{ {
((loadedBuild in GlobalScope :== loaded) +: ((loadedBuild in GlobalScope :== loaded) +:
transformProjectOnly(loaded.root, rootProject, injectSettings.global)) ++ transformProjectOnly(loaded.root, rootProject, injectSettings.global)) ++
inScope(GlobalScope)( pluginGlobalSettings(loaded) ) ++ inScope(GlobalScope)( pluginGlobalSettings(loaded) ++ loaded.autos.globalSettings ) ++
loaded.units.toSeq.flatMap { case (uri, build) => loaded.units.toSeq.flatMap { case (uri, build) =>
val plugins = build.unit.plugins.plugins val plugins = build.unit.plugins.detected.plugins.values
val pluginBuildSettings = plugins.flatMap(_.buildSettings) val pluginBuildSettings = plugins.flatMap(_.buildSettings) ++ loaded.autos.buildSettings(uri)
val pluginNotThis = plugins.flatMap(_.settings) filterNot isProjectThis val pluginNotThis = plugins.flatMap(_.settings) filterNot isProjectThis
val projectSettings = build.defined flatMap { case (id, project) => val projectSettings = build.defined flatMap { case (id, project) =>
val ref = ProjectRef(uri, id) val ref = ProjectRef(uri, id)
@ -220,9 +217,10 @@ object Load
buildSettings ++ projectSettings buildSettings ++ projectSettings
} }
} }
@deprecated("Does not account for AutoPlugins and will be made private.", "0.13.2")
def pluginGlobalSettings(loaded: sbt.LoadedBuild): Seq[Setting[_]] = def pluginGlobalSettings(loaded: sbt.LoadedBuild): Seq[Setting[_]] =
loaded.units.toSeq flatMap { case (_, build) => loaded.units.toSeq flatMap { case (_, build) =>
build.unit.plugins.plugins flatMap { _.globalSettings } build.unit.plugins.detected.plugins.values flatMap { _.globalSettings }
} }
@deprecated("No longer used.", "0.13.0") @deprecated("No longer used.", "0.13.0")
@ -368,10 +366,11 @@ object Load
def resolveProjects(loaded: sbt.PartBuild): sbt.LoadedBuild = def resolveProjects(loaded: sbt.PartBuild): sbt.LoadedBuild =
{ {
val rootProject = getRootProject(loaded.units) val rootProject = getRootProject(loaded.units)
new sbt.LoadedBuild(loaded.root, loaded.units map { case (uri, unit) => val units = loaded.units map { case (uri, unit) =>
IO.assertAbsolute(uri) IO.assertAbsolute(uri)
(uri, resolveProjects(uri, unit, rootProject)) (uri, resolveProjects(uri, unit, rootProject))
}) }
new sbt.LoadedBuild(loaded.root, units)
} }
def resolveProjects(uri: URI, unit: sbt.PartBuildUnit, rootProject: URI => String): sbt.LoadedBuildUnit = def resolveProjects(uri: URI, unit: sbt.PartBuildUnit, rootProject: URI => String): sbt.LoadedBuildUnit =
{ {
@ -399,10 +398,10 @@ object Load
def getBuild[T](map: Map[URI, T], uri: URI): T = def getBuild[T](map: Map[URI, T], uri: URI): T =
map.getOrElse(uri, noBuild(uri)) map.getOrElse(uri, noBuild(uri))
def emptyBuild(uri: URI) = sys.error("No root project defined for build unit '" + uri + "'") def emptyBuild(uri: URI) = sys.error(s"No root project defined for build unit '$uri'")
def noBuild(uri: URI) = sys.error("Build unit '" + uri + "' not defined.") def noBuild(uri: URI) = sys.error(s"Build unit '$uri' not defined.")
def noProject(uri: URI, id: String) = sys.error("No project '" + id + "' defined in '" + uri + "'.") def noProject(uri: URI, id: String) = sys.error(s"No project '$id' defined in '$uri'.")
def noConfiguration(uri: URI, id: String, conf: String) = sys.error("No configuration '" + conf + "' defined in project '" + id + "' in '" + uri +"'") def noConfiguration(uri: URI, id: String, conf: String) = sys.error(s"No configuration '$conf' defined in project '$id' in '$uri'")
def loadUnit(uri: URI, localBase: File, s: State, config: sbt.LoadBuildConfiguration): sbt.BuildUnit = def loadUnit(uri: URI, localBase: File, s: State, config: sbt.LoadBuildConfiguration): sbt.BuildUnit =
{ {
@ -410,15 +409,13 @@ object Load
val defDir = projectStandard(normBase) val defDir = projectStandard(normBase)
val plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin)) val plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin))
val defNames = analyzed(plugs.fullClasspath) flatMap findDefinitions val defsScala = plugs.detected.builds.values
val defsScala = if(defNames.isEmpty) Nil else loadDefinitions(plugs.loader, defNames)
val imports = BuildUtil.getImports(plugs.pluginNames, defNames)
lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions) lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions)
val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase))
val memoSettings = new mutable.HashMap[File, LoadedSbtFile] val memoSettings = new mutable.HashMap[File, LoadedSbtFile]
def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, imports, plugs, () => eval, config.injectSettings, Nil, memoSettings) def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings)
val loadedProjectsRaw = loadProjects(initialProjects) val loadedProjectsRaw = loadProjects(initialProjects)
val hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined) val hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined)
val (loadedProjects, defaultBuildIfNone) = val (loadedProjects, defaultBuildIfNone) =
@ -434,7 +431,7 @@ object Load
} }
val defs = if(defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala val defs = if(defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala
val loadedDefs = new sbt.LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, defNames) val loadedDefs = new sbt.LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, plugs.detected.builds.names)
new sbt.BuildUnit(uri, normBase, loadedDefs, plugs) new sbt.BuildUnit(uri, normBase, loadedDefs, plugs)
} }
@ -460,16 +457,22 @@ object Load
private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] = private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] =
b.projectDefinitions(base).map(resolveBase(base)) b.projectDefinitions(base).map(resolveBase(base))
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, imports: Seq[String], plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings, acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile]): Seq[Project] = private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings, acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile]): Seq[Project] =
{ {
def loadSbtFiles(auto: AddSettings, base: File): LoadedSbtFile = def loadSbtFiles(auto: AddSettings, base: File, autoPlugins: Seq[AutoPlugin]): LoadedSbtFile =
loadSettings(auto, base, imports, plugins, eval, injectSettings, memoSettings) loadSettings(auto, base, plugins, eval, injectSettings, memoSettings, autoPlugins)
def loadForProjects = newProjects map { project => def loadForProjects = newProjects map { project =>
val loadedSbtFiles = loadSbtFiles(project.auto, project.base) val autoPlugins =
val transformed = project.copy(settings = (project.settings: Seq[Setting[_]]) ++ loadedSbtFiles.settings) try plugins.detected.compileNatures(project.natures)
catch { case e: AutoPluginException => throw translateAutoPluginException(e, project) }
val autoConfigs = autoPlugins.flatMap(_.projectConfigurations)
val loadedSbtFiles = loadSbtFiles(project.auto, project.base, autoPlugins)
val newSettings = (project.settings: Seq[Setting[_]]) ++ loadedSbtFiles.settings
// add the automatically selected settings, record the selected AutoPlugins, and register the automatically selected configurations
val transformed = project.copy(settings = newSettings).setAutoPlugins(autoPlugins).overrideConfigs(autoConfigs : _*)
(transformed, loadedSbtFiles.projects) (transformed, loadedSbtFiles.projects)
} }
def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase).projects def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase, Nil).projects
val (nextProjects, loadedProjects) = val (nextProjects, loadedProjects) =
if(newProjects.isEmpty) // load the .sbt files in the root directory to look for Projects if(newProjects.isEmpty) // load the .sbt files in the root directory to look for Projects
(defaultLoad, acc) (defaultLoad, acc)
@ -481,10 +484,12 @@ object Load
if(nextProjects.isEmpty) if(nextProjects.isEmpty)
loadedProjects loadedProjects
else else
loadTransitive(nextProjects, buildBase, imports, plugins, eval, injectSettings, loadedProjects, memoSettings) loadTransitive(nextProjects, buildBase, plugins, eval, injectSettings, loadedProjects, memoSettings)
} }
private[this] def translateAutoPluginException(e: AutoPluginException, project: Project): AutoPluginException =
e.withPrefix(s"Error determining plugins for project '${project.id}' in ${project.base}:\n")
private[this] def loadSettings(auto: AddSettings, projectBase: File, buildImports: Seq[String], loadedPlugins: sbt.LoadedPlugins, eval: ()=>Eval, injectSettings: InjectSettings, memoSettings: mutable.Map[File, LoadedSbtFile]): LoadedSbtFile = private[this] def loadSettings(auto: AddSettings, projectBase: File, loadedPlugins: sbt.LoadedPlugins, eval: ()=>Eval, injectSettings: InjectSettings, memoSettings: mutable.Map[File, LoadedSbtFile], autoPlugins: Seq[AutoPlugin]): LoadedSbtFile =
{ {
lazy val defaultSbtFiles = configurationSources(projectBase) lazy val defaultSbtFiles = configurationSources(projectBase)
def settings(ss: Seq[Setting[_]]) = new LoadedSbtFile(ss, Nil, Nil) def settings(ss: Seq[Setting[_]]) = new LoadedSbtFile(ss, Nil, Nil)
@ -499,14 +504,20 @@ object Load
lf lf
} }
def loadSettingsFile(src: File): LoadedSbtFile = def loadSettingsFile(src: File): LoadedSbtFile =
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), buildImports, 0)(loader) EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader)
import AddSettings.{User,SbtFiles,DefaultSbtFiles,Plugins,Sequence} import AddSettings.{User,SbtFiles,DefaultSbtFiles,Plugins,Sequence}
def pluginSettings(f: Plugins) = {
val included = loadedPlugins.detected.plugins.values.filter(f.include) // don't apply the filter to AutoPlugins, only Plugins
val oldStyle = included.flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings)
val autoStyle = autoPlugins.flatMap(_.projectSettings)
oldStyle ++ autoStyle
}
def expand(auto: AddSettings): LoadedSbtFile = auto match { def expand(auto: AddSettings): LoadedSbtFile = auto match {
case User => settings(injectSettings.projectLoaded(loader)) case User => settings(injectSettings.projectLoaded(loader))
case sf: SbtFiles => loadSettings( sf.files.map(f => IO.resolve(projectBase, f))) case sf: SbtFiles => loadSettings( sf.files.map(f => IO.resolve(projectBase, f)))
case sf: DefaultSbtFiles => loadSettings( defaultSbtFiles.filter(sf.include)) case sf: DefaultSbtFiles => loadSettings( defaultSbtFiles.filter(sf.include))
case f: Plugins => settings(loadedPlugins.plugins.filter(f.include).flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings)) case p: Plugins => settings(pluginSettings(p))
case q: Sequence => (LoadedSbtFile.empty /: q.sequence) { (b,add) => b.merge( expand(add) ) } case q: Sequence => (LoadedSbtFile.empty /: q.sequence) { (b,add) => b.merge( expand(add) ) }
} }
expand(auto) expand(auto)
@ -599,67 +610,46 @@ object Load
config.evalPluginDef(pluginDef, pluginState) config.evalPluginDef(pluginDef, pluginState)
} }
@deprecated("Use ModuleUtilities.getCheckedObjects[Build].", "0.13.2")
def loadDefinitions(loader: ClassLoader, defs: Seq[String]): Seq[Build] = def loadDefinitions(loader: ClassLoader, defs: Seq[String]): Seq[Build] =
defs map { definition => loadDefinition(loader, definition) } defs map { definition => loadDefinition(loader, definition) }
@deprecated("Use ModuleUtilities.getCheckedObject[Build].", "0.13.2")
def loadDefinition(loader: ClassLoader, definition: String): Build = def loadDefinition(loader: ClassLoader, definition: String): Build =
ModuleUtilities.getObject(definition, loader).asInstanceOf[Build] ModuleUtilities.getObject(definition, loader).asInstanceOf[Build]
def loadPlugins(dir: File, data: PluginData, loader: ClassLoader): sbt.LoadedPlugins = def loadPlugins(dir: File, data: PluginData, loader: ClassLoader): sbt.LoadedPlugins =
{ new sbt.LoadedPlugins(dir, data, loader, PluginDiscovery.discoverAll(data, loader))
val (pluginNames, plugins) = if(data.classpath.isEmpty) (Nil, Nil) else {
val names = getPluginNames(data.classpath, loader) @deprecated("Replaced by the more general PluginDiscovery.binarySourceModuleNames and will be made private.", "0.13.2")
val loaded =
try loadPlugins(loader, names)
catch {
case e: ExceptionInInitializerError =>
val cause = e.getCause
if(cause eq null) throw e else throw cause
case e: LinkageError => incompatiblePlugins(data, e)
}
(names, loaded)
}
new sbt.LoadedPlugins(dir, data, loader, plugins, pluginNames)
}
private[this] def incompatiblePlugins(data: PluginData, t: LinkageError): Nothing =
{
val evicted = data.report.toList.flatMap(_.configurations.flatMap(_.evicted))
val evictedModules = evicted map { id => (id.organization, id.name) } distinct ;
val evictedStrings = evictedModules map { case (o,n) => o + ":" + n }
val msgBase = "Binary incompatibility in plugins detected."
val msgExtra = if(evictedStrings.isEmpty) "" else "\nNote that conflicts were resolved for some dependencies:\n\t" + evictedStrings.mkString("\n\t")
throw new IncompatiblePluginsException(msgBase + msgExtra, t)
}
def getPluginNames(classpath: Seq[Attributed[File]], loader: ClassLoader): Seq[String] = def getPluginNames(classpath: Seq[Attributed[File]], loader: ClassLoader): Seq[String] =
( binaryPlugins(data(classpath), loader) ++ (analyzed(classpath) flatMap findPlugins) ).distinct PluginDiscovery.binarySourceModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins, classOf[Plugin].getName)
@deprecated("Use PluginDiscovery.binaryModuleNames.", "0.13.2")
def binaryPlugins(classpath: Seq[File], loader: ClassLoader): Seq[String] = def binaryPlugins(classpath: Seq[File], loader: ClassLoader): Seq[String] =
{ PluginDiscovery.binaryModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins)
import collection.JavaConversions._
loader.getResources("sbt/sbt.plugins").toSeq.filter(onClasspath(classpath)) flatMap { u => @deprecated("Use PluginDiscovery.onClasspath", "0.13.2")
IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty)
}
}
def onClasspath(classpath: Seq[File])(url: URL): Boolean = def onClasspath(classpath: Seq[File])(url: URL): Boolean =
IO.urlAsFile(url) exists (classpath.contains _) PluginDiscovery.onClasspath(classpath)(url)
@deprecated("Use ModuleUtilities.getCheckedObjects[Plugin].", "0.13.2")
def loadPlugins(loader: ClassLoader, pluginNames: Seq[String]): Seq[Plugin] = def loadPlugins(loader: ClassLoader, pluginNames: Seq[String]): Seq[Plugin] =
pluginNames.map(pluginName => loadPlugin(pluginName, loader)) ModuleUtilities.getCheckedObjects[Plugin](pluginNames, loader).map(_._2)
@deprecated("Use ModuleUtilities.getCheckedObject[Plugin].", "0.13.2")
def loadPlugin(pluginName: String, loader: ClassLoader): Plugin = def loadPlugin(pluginName: String, loader: ClassLoader): Plugin =
ModuleUtilities.getObject(pluginName, loader).asInstanceOf[Plugin] ModuleUtilities.getCheckedObject[Plugin](pluginName, loader)
@deprecated("No longer used.", "0.13.2")
def findPlugins(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Plugin") def findPlugins(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Plugin")
@deprecated("No longer used.", "0.13.2")
def findDefinitions(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Build") def findDefinitions(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Build")
@deprecated("Use PluginDiscovery.sourceModuleNames", "0.13.2")
def discover(analysis: inc.Analysis, subclasses: String*): Seq[String] = def discover(analysis: inc.Analysis, subclasses: String*): Seq[String] =
{ PluginDiscovery.sourceModuleNames(analysis, subclasses : _*)
val subclassSet = subclasses.toSet
val ds = Discovery(subclassSet, Set.empty)(Tests.allDefs(analysis))
ds.flatMap {
case (definition, Discovered(subs,_,_,true)) =>
if((subs & subclassSet).isEmpty) Nil else definition.name :: Nil
case _ => Nil
}
}
def initialSession(structure: sbt.BuildStructure, rootEval: () => Eval, s: State): SessionSettings = { def initialSession(structure: sbt.BuildStructure, rootEval: () => Eval, s: State): SessionSettings = {
val session = s get Keys.sessionSettings val session = s get Keys.sessionSettings

View File

@ -89,7 +89,7 @@ object BuiltinCommands
def ScriptCommands: Seq[Command] = Seq(ignore, exit, Script.command, setLogLevel, early, act, nop) def ScriptCommands: Seq[Command] = Seq(ignore, exit, Script.command, setLogLevel, early, act, nop)
def DefaultCommands: Seq[Command] = Seq(ignore, help, completionsCommand, about, tasks, settingsCommand, loadProject, def DefaultCommands: Seq[Command] = Seq(ignore, help, completionsCommand, about, tasks, settingsCommand, loadProject,
projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion, projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++ ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++
compatCommands compatCommands
def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil
@ -125,7 +125,8 @@ object BuiltinCommands
def aboutPlugins(e: Extracted): String = def aboutPlugins(e: Extracted): String =
{ {
val allPluginNames = e.structure.units.values.flatMap(_.unit.plugins.pluginNames).toSeq.distinct 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: ", ", ", "") if(allPluginNames.isEmpty) "" else allPluginNames.mkString("Available Plugins: ", ", ", "")
} }
def aboutScala(s: State, e: Extracted): String = def aboutScala(s: State, e: Extracted): String =
@ -374,6 +375,20 @@ object BuiltinCommands
Help.detailOnly(taskDetail(allTaskAndSettingKeys(s))) Help.detailOnly(taskDetail(allTaskAndSettingKeys(s)))
else else
Help.empty Help.empty
def plugins = Command.command(PluginsCommand, pluginsBrief, pluginsDetailed) { s =>
val helpString = NaturesDebug.helpAll(s)
System.out.println(helpString)
s
}
val pluginParser: State => Parser[AutoPlugin] = s => {
val autoPlugins: Map[String, AutoPlugin] = NaturesDebug.autoPluginMap(s)
token(Space) ~> Act.knownIDParser(autoPlugins, "plugin")
}
def plugin = Command(PluginCommand)(pluginParser) { (s, plugin) =>
val helpString = NaturesDebug.help(plugin, s)
System.out.println(helpString)
s
}
def projects = Command(ProjectsCommand, (ProjectsCommand, projectsBrief), projectsDetailed )(s => projectsParser(s).?) { def projects = Command(ProjectsCommand, (ProjectsCommand, projectsBrief), projectsDetailed )(s => projectsParser(s).?) {
case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds) case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds)

View File

@ -0,0 +1,207 @@
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 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.
For example, the following will automatically add the settings in `projectSettings`
to a project that has both the `Web` and `Javascript` natures enabled.
object MyPlugin extends AutoPlugin {
def select = Web && Javascript
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.
3. Exclude plugins, if desired.
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 && !MyPlugin)
then the `MyPlugin` settings (and anything that activates only when `MyPlugin` 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]
def unary_! : Exclude = Exclude(this)
}
/** 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
}
/** Represents a feature or conceptual group of settings.
* `label` is the unique ID for this nature. */
final case class Nature(label: String) extends Basic {
/** Constructs a Natures matcher that excludes this Nature. */
override def toString = label
}
object Natures
{
/** Given the available auto plugins `defined`, returns a function that selects [[AutoPlugin]]s for the provided [[Nature]]s.
* The [[AutoPlugin]]s are topologically sorted so that a selected [[AutoPlugin]] comes before its selecting [[AutoPlugin]].*/
def compile(defined: List[AutoPlugin]): Natures => Seq[AutoPlugin] =
if(defined.isEmpty)
Types.const(Nil)
else
{
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)) )
requestedNatures =>
Logic.reduce(clauses, flattenConvert(requestedNatures).toSet) match {
case Left(problem) => throw AutoPluginException(problem)
case Right(results) =>
// results includes the originally requested (positive) atoms,
// which won't have a corresponding AutoPlugin to map back to
results.ordered.flatMap(a => byAtomMap.get(a).toList)
}
}
private[sbt] def translateMessage(e: LogicException) = e match {
case ic: InitialContradictions => s"Contradiction in selected natures. These natures were both included and excluded: ${literalsString(ic.literals.toSeq)}"
case io: InitialOverlap => s"Cannot directly enable plugins. Plugins are enabled when their required natures 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"Nature$ns provided by multiple AutoPlugins:$nl${dupStrings.mkString(nl)}"
throw AutoPluginException(message)
}
/** [[Natures]] instance that doesn't require any [[Nature]]s. */
def empty: Natures = Empty
private[sbt] final object Empty extends Natures {
def &&(o: Basic): Natures = o
override def toString = "<none>"
}
/** An included or excluded Nature. TODO: better name than Basic. */
sealed abstract class Basic extends Natures {
def &&(o: Basic): Natures = 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(natures: List[Basic]) extends Natures {
def &&(o: Basic): Natures = And(o :: natures)
override def toString = natures.mkString(", ")
}
private[sbt] def and(a: Natures, b: Natures) = b match {
case Empty => a
case And(ns) => (a /: ns)(_ && _)
case b: Basic => a && b
}
private[sbt] def remove(a: Natures, del: Set[Basic]): Natures = 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 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.label)) )
private[this] def flattenConvert(n: Natures): Seq[Literal] = n match {
case And(ns) => convertAll(ns)
case b: Basic => convertBasic(b) :: Nil
case Empty => Nil
}
private[sbt] def flatten(n: Natures): Seq[Basic] = n match {
case And(ns) => ns
case b: Basic => b :: Nil
case Empty => Nil
}
private[this] def convert(n: Natures): 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 Nature(s) => Atom(s)
case a: AutoPlugin => Atom(a.label)
}
private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic
/** True if the select clause `n` is satisifed by `model`. */
def satisfied(n: Natures, model: Set[AutoPlugin], natures: Set[Nature]): Boolean =
flatten(n) forall {
case Exclude(a) => !model(a)
case n: Nature => natures(n)
case ap: AutoPlugin => model(ap)
}
}

View File

@ -0,0 +1,386 @@
package sbt
import Def.Setting
import Natures._
import NaturesDebug._
import java.net.URI
private[sbt] class NaturesDebug(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", "")
}
}
/** 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[sbt] object NaturesDebug
{
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.values
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.natures, currentProject.autoPlugins, Natures.compile(pluginsThisBuild), pluginsThisBuild)
lazy val debug = NaturesDebug(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 <name>` and rerun this command."
s"$base\n$aggNote$common\n$helpOther"
}
}
}
/** Precomputes information for debugging natures and plugins. */
def apply(available: List[AutoPlugin]): NaturesDebug =
{
val keyR = definedKeys(available)
val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap
new NaturesDebug(available, nameToKey, keyR)
}
/** The context for debugging a plugin (de)activation.
* @param initial The initially defined [[Nature]]s.
* @param enabled The resulting model.
* @param compile The function used to compute the model.
* @param available All [[AutoPlugin]]s available for consideration. */
final case class Context(initial: Natures, enabled: Seq[AutoPlugin], compile: Natures => Seq[AutoPlugin], available: List[AutoPlugin])
/** 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 natures, exclusions, and ultimately activated plugins
* @param blockingExcludes Existing exclusions that prevent [[plugin]] from being activated and must be dropped
* @param enablingNatures [[Nature]]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 [[Nature]].*/
final case class PluginRequirements(plugin: AutoPlugin, context: Context, blockingExcludes: Set[AutoPlugin], enablingNatures: Set[Nature], 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 [[Nature]]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[Nature], 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)
private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable =
{
// deconstruct the context
val initialModel = context.enabled.toSet
val initial = flatten(context.initial)
val initialNatures = natures(initial)
val initialExcludes = excludes(initial)
val minModel = minimalModel(plugin)
/* 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'
example 2
A :- B, not C
C :- B
initial: <empty>
propose: B, exclude C
*/
// `plugin` will only be activated when all of these natures are activated
// Deactivating any one of these would deactivate `plugin`.
val minRequiredNatures = natures(minModel)
// `plugin` will only be activated when all of these plugins are activated
// Deactivating any one of these would deactivate `plugin`.
val minRequiredPlugins = minModel.collect{ case a: AutoPlugin => a }.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
if(contradictions.nonEmpty)
PluginImpossible(plugin, context, contradictions)
else
{
// Natures that the user has to add to the currently selected natures in order to enable `plugin`.
val addToExistingNatures = minRequiredNatures -- initialNatures
// Plugins that are currently excluded that need to be allowed.
val blockingExcludes = initialExcludes & minRequiredPlugins
// The model that results when the minimal natures are enabled and the minimal plugins are excluded.
// This can include more plugins than just `minRequiredPlugins` because the natures required for `plugin`
// might activate other plugins as well.
val modelForMin = context.compile(and(includeAll(minRequiredNatures), excludeAll(minAbsentPlugins)))
val incrementalInputs = and( includeAll(minRequiredNatures ++ initialNatures), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins))
val incrementalModel = context.compile(incrementalInputs).toSet
// Plugins that are newly enabled as a result of selecting the natures 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
// 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 natures 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 => Natures.satisfied(p.select, incrementalModel, natures(flatten(incrementalInputs))))
val deactivate = for(d <- minDeactivate.toList) yield {
// removing any one of these natures will deactivate `d`. TODO: This is not an especially efficient implementation.
val removeToDeactivate = natures(minimalModel(d)) -- minRequiredNatures
val newlySelected = !initialModel(d)
// a. suggest removing a nature 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, addToExistingNatures, extraPlugins, willRemove, deactivate)
}
}
private[this] def includeAll[T <: Basic](basic: Set[T]): Natures = And(basic.toList)
private[this] def excludeAll(plugins: Set[AutoPlugin]): Natures = 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 natures(bs: Seq[Basic]): Set[Nature] = bs.collect { case n: Nature => 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 natures and 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 natures.
private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) {
case _: Exclude | _: Nature => Nil
case ap: AutoPlugin => Natures.flatten(ap.select)
}
/** String representation of [[PluginEnable]], intended for end users. */
def explainPluginEnable(ps: PluginEnable): String =
ps match {
case PluginRequirements(plugin, context, blockingExcludes, enablingNatures, 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(enablingNatures.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))) )
}
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 required(natures: List[Nature]): String =
str(natures)(requiredNature, requiredNatures)
private[this] def requiredNature(nature: Nature) =
s"Required nature ${nature.label} not present."
private[this] def requiredNatures(natures: List[Nature]) =
s"Required natures not present:\n\t${natures.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 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 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 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 removeNaturesString: 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${removeNaturesString}"
}
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."
}

View File

@ -0,0 +1,135 @@
package sbt
import java.io.File
import java.net.URL
import Attributed.data
import Build.analyzed
import xsbt.api.{Discovered,Discovery}
object PluginDiscovery
{
/** Relative paths of resources that list top-level modules that are available.
* Normally, the classes for those modules will be in the same classpath entry as the resource. */
object Paths
{
final val AutoPlugins = "sbt/sbt.autoplugins"
final val Plugins = "sbt/sbt.plugins"
final val Builds = "sbt/sbt.builds"
final val AutoImports = "sbt/sbt.autoimports"
}
/** Names of top-level modules that subclass sbt plugin-related classes: [[Plugin]], [[AutoImport]], [[AutoPlugin]], and [[Build]]. */
final class DiscoveredNames(val plugins: Seq[String], val autoImports: Seq[String], val autoPlugins: Seq[String], val builds: Seq[String])
def emptyDiscoveredNames: DiscoveredNames = new DiscoveredNames(Nil, Nil, Nil, Nil)
/** Discovers and loads the sbt-plugin-related top-level modules from the classpath and source analysis in `data` and using the provided class `loader`. */
def discoverAll(data: PluginData, loader: ClassLoader): DetectedPlugins =
{
def discover[T](resource: String)(implicit mf: reflect.ClassManifest[T]) =
binarySourceModules[T](data, loader, resource)
import Paths._
new DetectedPlugins(discover[Plugin](Plugins), discover[AutoImport](AutoImports), discover[AutoPlugin](AutoPlugins), discover[Build](Builds))
}
/** Discovers the sbt-plugin-related top-level modules from the provided source `analysis`. */
def discoverSourceAll(analysis: inc.Analysis): DiscoveredNames =
{
def discover[T](implicit mf: reflect.ClassManifest[T]): Seq[String] =
sourceModuleNames(analysis, mf.erasure.getName)
new DiscoveredNames(discover[Plugin], discover[AutoImport], discover[AutoPlugin], discover[Build])
}
// TODO: for 0.14.0, consider consolidating into a single file, which would make the classpath search 4x faster
/** Writes discovered module `names` to zero or more files in `dir` as per [[writeDescriptor]] and returns the list of files written. */
def writeDescriptors(names: DiscoveredNames, dir: File): Seq[File] =
{
import Paths._
val files =
writeDescriptor(names.plugins, dir, Plugins) ::
writeDescriptor(names.autoPlugins, dir, AutoPlugins) ::
writeDescriptor(names.builds, dir, Builds) ::
writeDescriptor(names.autoImports, dir, AutoImports) ::
Nil
files.flatMap(_.toList)
}
/** Stores the module `names` in `dir / path`, one per line, unless `names` is empty and then the file is deleted and `None` returned. */
def writeDescriptor(names: Seq[String], dir: File, path: String): Option[File] =
{
val descriptor: File = new File(dir, path)
if(names.isEmpty)
{
IO.delete(descriptor)
None
}
else
{
IO.writeLines(descriptor, names.distinct.sorted)
Some(descriptor)
}
}
/** Discovers the names of top-level modules listed in resources named `resourceName` as per [[binaryModuleNames]] or
* available as analyzed source and extending from any of `subclasses` as per [[sourceModuleNames]]. */
def binarySourceModuleNames(classpath: Seq[Attributed[File]], loader: ClassLoader, resourceName: String, subclasses: String*): Seq[String] =
(
binaryModuleNames(data(classpath), loader, resourceName) ++
(analyzed(classpath) flatMap ( a => sourceModuleNames(a, subclasses : _*) ))
).distinct
/** Discovers top-level modules in `analysis` that inherit from any of `subclasses`. */
def sourceModuleNames(analysis: inc.Analysis, subclasses: String*): Seq[String] =
{
val subclassSet = subclasses.toSet
val ds = Discovery(subclassSet, Set.empty)(Tests.allDefs(analysis))
ds.flatMap {
case (definition, Discovered(subs,_,_,true)) =>
if((subs & subclassSet).isEmpty) Nil else definition.name :: Nil
case _ => Nil
}
}
/** Obtains the list of modules identified in all resource files `resourceName` from `loader` that are on `classpath`.
* `classpath` and `loader` are both required to ensure that `loader`
* doesn't bring in any resources outside of the intended `classpath`, such as from parent loaders. */
def binaryModuleNames(classpath: Seq[File], loader: ClassLoader, resourceName: String): Seq[String] =
{
import collection.JavaConversions._
loader.getResources(resourceName).toSeq.filter(onClasspath(classpath)) flatMap { u =>
IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty)
}
}
/** Returns `true` if `url` is an entry in `classpath`.*/
def onClasspath(classpath: Seq[File])(url: URL): Boolean =
IO.urlAsFile(url) exists (classpath.contains _)
private[sbt] def binarySourceModules[T](data: PluginData, loader: ClassLoader, resourceName: String)(implicit mf: reflect.ClassManifest[T]): DetectedModules[T] =
{
val classpath = data.classpath
val namesAndValues = if(classpath.isEmpty) Nil else {
val names = binarySourceModuleNames(classpath, loader, resourceName, mf.erasure.getName)
loadModules[T](data, names, loader)
}
new DetectedModules(namesAndValues)
}
private[this] def loadModules[T: ClassManifest](data: PluginData, names: Seq[String], loader: ClassLoader): Seq[(String,T)] =
try ModuleUtilities.getCheckedObjects[T](names, loader)
catch {
case e: ExceptionInInitializerError =>
val cause = e.getCause
if(cause eq null) throw e else throw cause
case e: LinkageError => incompatiblePlugins(data, e)
}
private[this] def incompatiblePlugins(data: PluginData, t: LinkageError): Nothing =
{
val evicted = data.report.toList.flatMap(_.configurations.flatMap(_.evicted))
val evictedModules = evicted map { id => (id.organization, id.name) } distinct ;
val evictedStrings = evictedModules map { case (o,n) => o + ":" + n }
val msgBase = "Binary incompatibility in plugins detected."
val msgExtra = if(evictedStrings.isEmpty) "" else "\nNote that conflicts were resolved for some dependencies:\n\t" + evictedStrings.mkString("\n\t")
throw new IncompatiblePluginsException(msgBase + msgExtra, t)
}
}

View File

@ -50,33 +50,52 @@ sealed trait ProjectDefinition[PR <: ProjectReference]
/** Configures the sources of automatically appended settings.*/ /** Configures the sources of automatically appended settings.*/
def auto: AddSettings def auto: AddSettings
/** The [[Natures]] associated with 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 natures: Natures
/** The [[AutoPlugin]]s enabled for this project. This value is only available on a loaded Project. */
private[sbt] def autoPlugins: Seq[AutoPlugin]
override final def hashCode: Int = id.hashCode ^ base.hashCode ^ getClass.hashCode override final def hashCode: Int = id.hashCode ^ base.hashCode ^ getClass.hashCode
override final def equals(o: Any) = o match { override final def equals(o: Any) = o match {
case p: ProjectDefinition[_] => p.getClass == this.getClass && p.id == id && p.base == base case p: ProjectDefinition[_] => p.getClass == this.getClass && p.id == id && p.base == base
case _ => false case _ => false
} }
override def toString = "Project(id: " + id + ", base: " + base + ", aggregate: " + aggregate + ", dependencies: " + dependencies + ", configurations: " + configurations + ")" override def toString =
{
val agg = ifNonEmpty("aggregate", aggregate)
val dep = ifNonEmpty("dependencies", dependencies)
val conf = ifNonEmpty("configurations", configurations)
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(", ")})"
}
private[this] def ifNonEmpty[T](label: String, ts: Iterable[T]): List[String] = if(ts.isEmpty) Nil else s"$label: $ts" :: Nil
} }
sealed trait Project extends ProjectDefinition[ProjectReference] sealed trait Project extends ProjectDefinition[ProjectReference]
{ {
// TODO: add parameters for natures and autoPlugins in 0.14.0 (not reasonable to do in a binary compatible way in 0.13)
def copy(id: String = id, base: File = base, aggregate: => Seq[ProjectReference] = aggregate, dependencies: => Seq[ClasspathDep[ProjectReference]] = dependencies, def copy(id: String = id, base: File = base, aggregate: => Seq[ProjectReference] = aggregate, dependencies: => Seq[ClasspathDep[ProjectReference]] = dependencies,
delegates: => Seq[ProjectReference] = delegates, settings: => Seq[Setting[_]] = settings, configurations: Seq[Configuration] = configurations, delegates: => Seq[ProjectReference] = delegates, settings: => Seq[Setting[_]] = settings, configurations: Seq[Configuration] = configurations,
auto: AddSettings = auto): Project = auto: AddSettings = auto): Project =
Project(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto) unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, natures, autoPlugins)
def resolve(resolveRef: ProjectReference => ProjectRef): ResolvedProject = def resolve(resolveRef: ProjectReference => ProjectRef): ResolvedProject =
{ {
def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep
def resolveDep(d: ClasspathDep[ProjectReference]) = ResolvedClasspathDependency(resolveRef(d.project), d.configuration) def resolveDep(d: ClasspathDep[ProjectReference]) = ResolvedClasspathDependency(resolveRef(d.project), d.configuration)
resolved(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates), settings, configurations, auto) resolved(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates),
settings, configurations, auto, natures, autoPlugins)
} }
def resolveBuild(resolveRef: ProjectReference => ProjectReference): Project = def resolveBuild(resolveRef: ProjectReference => ProjectReference): Project =
{ {
def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep
def resolveDep(d: ClasspathDep[ProjectReference]) = ClasspathDependency(resolveRef(d.project), d.configuration) def resolveDep(d: ClasspathDep[ProjectReference]) = ClasspathDependency(resolveRef(d.project), d.configuration)
apply(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates), settings, configurations, auto) unresolved(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates),
settings, configurations, auto, natures, autoPlugins)
} }
/** Applies the given functions to this Project. /** Applies the given functions to this Project.
@ -116,8 +135,30 @@ sealed trait Project extends ProjectDefinition[ProjectReference]
/** Sets the list of .sbt files to parse for settings to be appended to this project's settings. /** Sets the list of .sbt files to parse for settings to be appended to this project's settings.
* Any configured .sbt files are removed from this project's list.*/ * 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: _*)) ) def setSbtFiles(files: File*): Project = copy(auto = AddSettings.append( AddSettings.clearSbtFiles(auto), AddSettings.sbtFiles(files: _*)) )
/** 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: 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, ns, autoPlugins)
}
/** Definitively set the [[AutoPlugin]]s for this project. */
private[sbt] def setAutoPlugins(autos: Seq[AutoPlugin]): Project = {
// TODO: for 0.14.0, use copy when it has the additional `autoPlugins` parameter
unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, natures, autos)
}
}
sealed trait ResolvedProject extends ProjectDefinition[ProjectRef] {
/** The [[AutoPlugin]]s enabled for this project as computed from [[natures]].*/
def autoPlugins: Seq[AutoPlugin]
} }
sealed trait ResolvedProject extends ProjectDefinition[ProjectRef]
sealed trait ClasspathDep[PR <: ProjectReference] { def project: PR; def configuration: Option[String] } sealed trait ClasspathDep[PR <: ProjectReference] { def project: PR; def configuration: Option[String] }
final case class ResolvedClasspathDependency(project: ProjectRef, configuration: Option[String]) extends ClasspathDep[ProjectRef] final case class ResolvedClasspathDependency(project: ProjectRef, configuration: Option[String]) extends ClasspathDep[ProjectRef]
@ -150,23 +191,22 @@ object Project extends ProjectExtra
Def.showRelativeKey( ProjectRef(loaded.root, loaded.units(loaded.root).rootProjects.head), loaded.allProjectRefs.size > 1, keyNameColor) Def.showRelativeKey( ProjectRef(loaded.root, loaded.units(loaded.root).rootProjects.head), loaded.allProjectRefs.size > 1, keyNameColor)
private abstract class ProjectDef[PR <: ProjectReference](val id: String, val base: File, aggregate0: => Seq[PR], dependencies0: => Seq[ClasspathDep[PR]], private abstract class ProjectDef[PR <: ProjectReference](val id: String, val base: File, aggregate0: => Seq[PR], dependencies0: => Seq[ClasspathDep[PR]],
delegates0: => Seq[PR], settings0: => Seq[Def.Setting[_]], val configurations: Seq[Configuration], val auto: AddSettings) extends ProjectDefinition[PR] delegates0: => Seq[PR], settings0: => Seq[Def.Setting[_]], val configurations: Seq[Configuration], val auto: AddSettings,
val natures: Natures, val autoPlugins: Seq[AutoPlugin]) extends ProjectDefinition[PR]
{ {
lazy val aggregate = aggregate0 lazy val aggregate = aggregate0
lazy val dependencies = dependencies0 lazy val dependencies = dependencies0
lazy val delegates = delegates0 lazy val delegates = delegates0
lazy val settings = settings0 lazy val settings = settings0
Dag.topologicalSort(configurations)(_.extendsConfigs) // checks for cyclic references here instead of having to do it in Scope.delegates Dag.topologicalSort(configurations)(_.extendsConfigs) // checks for cyclic references here instead of having to do it in Scope.delegates
} }
// TODO: add parameter for natures in 0.14.0
def apply(id: String, base: File, aggregate: => Seq[ProjectReference] = Nil, dependencies: => Seq[ClasspathDep[ProjectReference]] = Nil, def apply(id: String, base: File, aggregate: => Seq[ProjectReference] = Nil, dependencies: => Seq[ClasspathDep[ProjectReference]] = Nil,
delegates: => Seq[ProjectReference] = Nil, settings: => Seq[Def.Setting[_]] = defaultSettings, configurations: Seq[Configuration] = Configurations.default, delegates: => Seq[ProjectReference] = Nil, settings: => Seq[Def.Setting[_]] = defaultSettings, configurations: Seq[Configuration] = Configurations.default,
auto: AddSettings = AddSettings.allDefaults): Project = auto: AddSettings = AddSettings.allDefaults): Project =
{ unresolved(id, base, aggregate, dependencies, delegates, settings, configurations, auto, Natures.empty, Nil)
validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg))
new ProjectDef[ProjectReference](id, base, aggregate, dependencies, delegates, settings, configurations, auto) with Project
}
/** Returns None if `id` is a valid Project ID or Some containing the parser error message if it is not.*/ /** Returns None if `id` is a valid Project ID or Some containing the parser error message if it is not.*/
def validProjectID(id: String): Option[String] = DefaultParsers.parse(id, DefaultParsers.ID).left.toOption def validProjectID(id: String): Option[String] = DefaultParsers.parse(id, DefaultParsers.ID).left.toOption
@ -185,9 +225,23 @@ object Project extends ProjectExtra
* This is a best effort implementation, since valid characters are not documented or consistent.*/ * This is a best effort implementation, since valid characters are not documented or consistent.*/
def normalizeModuleID(id: String): String = normalizeBase(id) def normalizeModuleID(id: String): String = normalizeBase(id)
@deprecated("Will be removed.", "0.13.2")
def resolved(id: String, base: File, aggregate: => Seq[ProjectRef], dependencies: => Seq[ResolvedClasspathDependency], delegates: => Seq[ProjectRef], def resolved(id: String, base: File, aggregate: => Seq[ProjectRef], dependencies: => Seq[ResolvedClasspathDependency], delegates: => Seq[ProjectRef],
settings: Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings): ResolvedProject = settings: Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings): ResolvedProject =
new ProjectDef[ProjectRef](id, base, aggregate, dependencies, delegates, settings, configurations, auto) with ResolvedProject resolved(id, base, aggregate, dependencies, delegates, settings, configurations, auto, Natures.empty, Nil)
private def resolved(id: String, base: File, aggregate: => Seq[ProjectRef], dependencies: => Seq[ClasspathDep[ProjectRef]],
delegates: => Seq[ProjectRef], settings: Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings,
natures: Natures, autoPlugins: Seq[AutoPlugin]): ResolvedProject =
new ProjectDef[ProjectRef](id, base, aggregate, dependencies, delegates, settings, configurations, auto, natures, autoPlugins) with ResolvedProject
private def unresolved(id: String, base: File, aggregate: => Seq[ProjectReference], dependencies: => Seq[ClasspathDep[ProjectReference]],
delegates: => Seq[ProjectReference], settings: => Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings,
natures: Natures, autoPlugins: Seq[AutoPlugin]): Project =
{
validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg))
new ProjectDef[ProjectReference](id, base, aggregate, dependencies, delegates, settings, configurations, auto, natures, autoPlugins) with Project
}
def defaultSettings: Seq[Def.Setting[_]] = Defaults.defaultSettings def defaultSettings: Seq[Def.Setting[_]] = Defaults.defaultSettings
@ -307,7 +361,7 @@ object Project extends ProjectExtra
def details(structure: BuildStructure, actual: Boolean, scope: Scope, key: AttributeKey[_])(implicit display: Show[ScopedKey[_]]): String = def details(structure: BuildStructure, actual: Boolean, scope: Scope, key: AttributeKey[_])(implicit display: Show[ScopedKey[_]]): String =
{ {
val scoped = ScopedKey(scope,key) val scoped = ScopedKey(scope,key)
val data = scopedKeyData(structure, scope, key) map {_.description} getOrElse {"No entry for key."} val data = scopedKeyData(structure, scope, key) map {_.description} getOrElse {"No entry for key."}
val description = key.description match { case Some(desc) => "Description:\n\t" + desc + "\n"; case None => "" } val description = key.description match { case Some(desc) => "Description:\n\t" + desc + "\n"; case None => "" }
@ -413,7 +467,7 @@ object Project extends ProjectExtra
import DefaultParsers._ import DefaultParsers._
val loadActionParser = token(Space ~> ("plugins" ^^^ Plugins | "return" ^^^ Return)) ?? Current val loadActionParser = token(Space ~> ("plugins" ^^^ Plugins | "return" ^^^ Return)) ?? Current
val ProjectReturn = AttributeKey[List[File]]("project-return", "Maintains a stack of builds visited using reload.") val ProjectReturn = AttributeKey[List[File]]("project-return", "Maintains a stack of builds visited using reload.")
def projectReturn(s: State): List[File] = getOrNil(s, ProjectReturn) def projectReturn(s: State): List[File] = getOrNil(s, ProjectReturn)
def inPluginProject(s: State): Boolean = projectReturn(s).toList.length > 1 def inPluginProject(s: State): Boolean = projectReturn(s).toList.length > 1

View File

@ -73,6 +73,8 @@ object Sbt extends Build
lazy val datatypeSub = baseProject(utilPath /"datatype", "Datatype Generator") dependsOn(ioSub) lazy val datatypeSub = baseProject(utilPath /"datatype", "Datatype Generator") dependsOn(ioSub)
// cross versioning // cross versioning
lazy val crossSub = baseProject(utilPath / "cross", "Cross") settings(inConfig(Compile)(Transform.crossGenSettings): _*) lazy val crossSub = baseProject(utilPath / "cross", "Cross") settings(inConfig(Compile)(Transform.crossGenSettings): _*)
// A logic with restricted negation as failure for a unique, stable model
lazy val logicSub = testedBaseProject(utilPath / "logic", "Logic").dependsOn(collectionSub, relationSub)
/* **** Intermediate-level Modules **** */ /* **** Intermediate-level Modules **** */
@ -130,7 +132,7 @@ object Sbt extends Build
completeSub, classpathSub, stdTaskSub, processSub) settings( sbinary ) completeSub, classpathSub, stdTaskSub, processSub) settings( sbinary )
// The main integration project for sbt. It brings all of the subsystems together, configures them, and provides for overriding conventions. // The main integration project for sbt. It brings all of the subsystems together, configures them, and provides for overriding conventions.
lazy val mainSub = testedBaseProject(mainPath, "Main") dependsOn(actionsSub, mainSettingsSub, interfaceSub, ioSub, ivySub, launchInterfaceSub, logSub, processSub, runSub, commandSub) settings(scalaXml) lazy val mainSub = testedBaseProject(mainPath, "Main") dependsOn(actionsSub, mainSettingsSub, interfaceSub, ioSub, ivySub, launchInterfaceSub, logSub, logicSub, processSub, runSub, commandSub) settings(scalaXml)
// Strictly for bringing implicits and aliases from subsystems into the top-level sbt namespace through a single package object // Strictly for bringing implicits and aliases from subsystems into the top-level sbt namespace through a single package object
// technically, we need a dependency on all of mainSub's dependencies, but we don't do that since this is strictly an integration project // technically, we need a dependency on all of mainSub's dependencies, but we don't do that since this is strictly an integration project

View File

@ -0,0 +1,34 @@
// 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)
// with no natures defined, nothing is auto-added
lazy val d = project
check := {
val ddel = (del in d).?.value // should be None
same(ddel, None, "del in d")
val bdel = (del in b).?.value // should be None
same(bdel, None, "del in b")
val adel = (del in a).?.value // should be None
same(adel, None, "del in a")
//
val buildValue = (demo in ThisBuild).value
same(buildValue, "build 0", "demo in ThisBuild")
val globalValue = (demo in Global).value
same(globalValue, "global 0", "demo in Global")
val projValue = (demo in c).value
same(projValue, "project c Q R", "demo in c")
val qValue = (del in c in q).value
same(qValue, " Q R", "del in c in q")
}
def same[T](actual: T, expected: T, label: String) {
assert(actual == expected, s"Expected '$expected' for `$label`, got '$actual'")
}

View File

@ -0,0 +1,59 @@
import sbt._
import sbt.Keys.{name, resolvedScoped}
import java.util.concurrent.atomic.{AtomicInteger => AInt}
object AI extends AutoImport
{
lazy val A = Nature("A")
lazy val B = Nature("B")
lazy val D = Nature("D")
lazy val q = config("q")
lazy val p = config("p").extend(q)
lazy val demo = settingKey[String]("A demo setting.")
lazy val del = settingKey[String]("Another demo setting.")
lazy val check = settingKey[Unit]("Verifies settings are as they should be.")
}
import AI._
object Q extends AutoPlugin
{
def select: Natures = A && B
override def projectConfigurations: Seq[Configuration] =
p ::
q ::
Nil
override def projectSettings: Seq[Setting[_]] =
(demo := s"project ${name.value}") ::
(del in q := " Q") ::
Nil
override def buildSettings: Seq[Setting[_]] =
(demo := s"build ${buildCount.getAndIncrement}") ::
Nil
override def globalSettings: Seq[Setting[_]] =
(demo := s"global ${globalCount.getAndIncrement}") ::
Nil
// used to ensure the build-level and global settings are only added once
private[this] val buildCount = new AInt(0)
private[this] val globalCount = new AInt(0)
}
object R extends AutoPlugin
{
def select = Q && !D
override def projectSettings = Seq(
// tests proper ordering: R requires C, so C settings should come first
del in q += " R",
// tests that configurations are properly registered, enabling delegation from p to q
demo += (del in p).value
)
}

View File

@ -0,0 +1 @@
> check

View File

@ -0,0 +1,21 @@
import sbt._
import Keys._
object C extends AutoImport {
lazy val bN = Nature("B")
lazy val check = taskKey[Unit]("Checks that the AutoPlugin and Build are automatically added.")
}
import C._
object A extends AutoPlugin {
override def select = bN
override def projectSettings = Seq(
check := {}
)
}
object B extends Build {
lazy val extra = project.addNatures(bN)
}

View File

@ -0,0 +1,3 @@
sbtPlugin := true
name := "demo-plugin"

View File

@ -0,0 +1 @@
addSbtPlugin("org.example" % "demo-plugin" % "3.4")

View File

@ -0,0 +1,7 @@
organization in ThisBuild := "org.example"
version in ThisBuild := "3.4"
lazy val define = project
lazy val use = project

View File

@ -0,0 +1,10 @@
$ copy-file changes/define/build.sbt build.sbt
$ copy-file changes/define/A.scala A.scala
# reload implied
> publishLocal
$ delete build.sbt A.scala
$ copy-file changes/use/plugins.sbt project/plugins.sbt
> reload
> extra/check

View File

@ -6,7 +6,7 @@ package sbt
object ModuleUtilities object ModuleUtilities
{ {
/** Reflectively loads and returns the companion object for top-level class `className` from `loader`. /** Reflectively loads and returns the companion object for top-level class `className` from `loader`.
* The class name should not include the `$` that scalac appends to the underlying jvm class for * The class name should not include the `$` that scalac appends to the underlying jvm class for
* a companion object. */ * a companion object. */
def getObject(className: String, loader: ClassLoader): AnyRef = def getObject(className: String, loader: ClassLoader): AnyRef =
{ {
@ -14,4 +14,10 @@ object ModuleUtilities
val singletonField = obj.getField("MODULE$") val singletonField = obj.getField("MODULE$")
singletonField.get(null) singletonField.get(null)
} }
def getCheckedObject[T](className: String, loader: ClassLoader)(implicit mf: reflect.ClassManifest[T]): T =
mf.erasure.cast(getObject(className, loader)).asInstanceOf[T]
def getCheckedObjects[T](classNames: Seq[String], loader: ClassLoader)(implicit mf: reflect.ClassManifest[T]): Seq[(String,T)] =
classNames.map(name => (name, getCheckedObject(name, loader)))
} }

View File

@ -15,7 +15,7 @@ object Dag
import JavaConverters.asScalaSetConverter import JavaConverters.asScalaSetConverter
def topologicalSort[T](root: T)(dependencies: T => Iterable[T]): List[T] = topologicalSort(root :: Nil)(dependencies) def topologicalSort[T](root: T)(dependencies: T => Iterable[T]): List[T] = topologicalSort(root :: Nil)(dependencies)
def topologicalSort[T](nodes: Iterable[T])(dependencies: T => Iterable[T]): List[T] = def topologicalSort[T](nodes: Iterable[T])(dependencies: T => Iterable[T]): List[T] =
{ {
val discovered = new mutable.HashSet[T] val discovered = new mutable.HashSet[T]
@ -24,7 +24,7 @@ object Dag
def visitAll(nodes: Iterable[T]) = nodes foreach visit def visitAll(nodes: Iterable[T]) = nodes foreach visit
def visit(node : T){ def visit(node : T){
if (!discovered(node)) { if (!discovered(node)) {
discovered(node) = true; discovered(node) = true;
try { visitAll(dependencies(node)); } catch { case c: Cyclic => throw node :: c } try { visitAll(dependencies(node)); } catch { case c: Cyclic => throw node :: c }
finished += node; finished += node;
} }
@ -33,11 +33,13 @@ object Dag
} }
visitAll(nodes); visitAll(nodes);
finished.toList; finished.toList;
} }
// doesn't check for cycles // doesn't check for cycles
def topologicalSortUnchecked[T](node: T)(dependencies: T => Iterable[T]): List[T] = def topologicalSortUnchecked[T](node: T)(dependencies: T => Iterable[T]): List[T] = topologicalSortUnchecked(node :: Nil)(dependencies)
def topologicalSortUnchecked[T](nodes: Iterable[T])(dependencies: T => Iterable[T]): List[T] =
{ {
val discovered = new mutable.HashSet[T] val discovered = new mutable.HashSet[T]
var finished: List[T] = Nil var finished: List[T] = Nil
@ -45,23 +47,23 @@ object Dag
def visitAll(nodes: Iterable[T]) = nodes foreach visit def visitAll(nodes: Iterable[T]) = nodes foreach visit
def visit(node : T){ def visit(node : T){
if (!discovered(node)) { if (!discovered(node)) {
discovered(node) = true; discovered(node) = true;
visitAll(dependencies(node)) visitAll(dependencies(node))
finished ::= node; finished ::= node;
} }
} }
visit(node); visitAll(nodes);
finished; finished;
} }
final class Cyclic(val value: Any, val all: List[Any], val complete: Boolean) final class Cyclic(val value: Any, val all: List[Any], val complete: Boolean)
extends Exception( "Cyclic reference involving " + extends Exception( "Cyclic reference involving " +
(if(complete) all.mkString("\n ", "\n ", "") else value) (if(complete) all.mkString("\n ", "\n ", "") else value)
) )
{ {
def this(value: Any) = this(value, value :: Nil, false) def this(value: Any) = this(value, value :: Nil, false)
override def toString = getMessage override def toString = getMessage
def ::(a: Any): Cyclic = def ::(a: Any): Cyclic =
if(complete) if(complete)
this this
else if(a == value) else if(a == value)
@ -69,5 +71,62 @@ object Dag
else else
new Cyclic(value, a :: all, false) new Cyclic(value, a :: all, false)
} }
}
/** A directed graph with edges labeled positive or negative. */
private[sbt] trait DirectedSignedGraph[Node]
{
/** Directed edge type that tracks the sign and target (head) vertex.
* The sign can be obtained via [[isNegative]] and the target vertex via [[head]]. */
type Arrow
/** List of initial nodes. */
def nodes: List[Arrow]
/** Outgoing edges for `n`. */
def dependencies(n: Node): List[Arrow]
/** `true` if the edge `a` is "negative", false if it is "positive". */
def isNegative(a: Arrow): Boolean
/** The target of the directed edge `a`. */
def head(a: Arrow): Node
}
/** Traverses a directed graph defined by `graph` looking for a cycle that includes a "negative" edge.
* The directed edges are weighted by the caller as "positive" or "negative".
* If a cycle containing a "negative" edge is detected, its member edges are returned in order.
* Otherwise, the empty list is returned. */
private[sbt] def findNegativeCycle[Node](graph: DirectedSignedGraph[Node]): List[graph.Arrow] =
{
import scala.annotation.tailrec
import graph._
val finished = new mutable.HashSet[Node]
val visited = new mutable.HashSet[Node]
def visit(edges: List[Arrow], stack: List[Arrow]): List[Arrow] = edges match {
case Nil => Nil
case edge :: tail =>
val node = head(edge)
if(!visited(node))
{
visited += node
visit(dependencies(node), edge :: stack) match {
case Nil =>
finished += node
visit(tail, stack)
case cycle => cycle
}
}
else if(!finished(node))
{
// cycle. If a negative edge is involved, it is an error.
val between = edge :: stack.takeWhile(f => head(f) != node)
if(between exists isNegative)
between
else
visit(tail, stack)
}
else
visit(tail, stack)
}
visit(graph.nodes, Nil)
}
}

View File

@ -0,0 +1,325 @@
package sbt
package logic
import scala.annotation.tailrec
import Formula.{And, True}
/*
Defines a propositional logic with negation as failure and only allows stratified rule sets (negation must be acyclic) in order to have a unique minimal model.
For example, this is not allowed:
+ p :- not q
+ q :- not p
but this is:
+ p :- q
+ q :- p
as is this:
+ p :- q
+ q := not r
Some useful links:
+ https://en.wikipedia.org/wiki/Nonmonotonic_logic
+ https://en.wikipedia.org/wiki/Negation_as_failure
+ https://en.wikipedia.org/wiki/Propositional_logic
+ https://en.wikipedia.org/wiki/Stable_model_semantics
+ http://www.w3.org/2005/rules/wg/wiki/negation
*/
/** Disjunction (or) of the list of clauses. */
final case class Clauses(clauses: List[Clause]) {
assert(clauses.nonEmpty, "At least one clause is required.")
}
/** When the `body` Formula succeeds, atoms in `head` are true. */
final case class Clause(body: Formula, head: Set[Atom])
/** A literal is an [[Atom]] or its [[negation|Negated]]. */
sealed abstract class Literal extends Formula {
/** The underlying (positive) atom. */
def atom: Atom
/** Negates this literal.*/
def unary_! : Literal
}
/** A variable with name `label`. */
final case class Atom(label: String) extends Literal {
def atom = this
def unary_! : Negated = Negated(this)
}
/** A negated atom, in the sense of negation as failure, not logical negation.
* That is, it is true if `atom` is not known/defined. */
final case class Negated(atom: Atom) extends Literal {
def unary_! : Atom = atom
}
/** A formula consists of variables, negation, and conjunction (and).
* (Disjunction is not currently included- it is modeled at the level of a sequence of clauses.
* This is less convenient when defining clauses, but is not less powerful.) */
sealed abstract class Formula {
/** Constructs a clause that proves `atoms` when this formula is true. */
def proves(atom: Atom, atoms: Atom*): Clause = Clause(this, (atom +: atoms).toSet)
/** Constructs a formula that is true iff this formula and `f` are both true.*/
def && (f: Formula): Formula = (this, f) match {
case (True, x) => x
case (x, True) => x
case (And(as), And(bs)) => And(as ++ bs)
case (And(as), b: Literal) => And(as + b)
case (a: Literal, And(bs)) => And(bs + a)
case (a: Literal, b: Literal) => And( Set(a,b) )
}
}
object Formula {
/** A conjunction of literals. */
final case class And(literals: Set[Literal]) extends Formula {
assert(literals.nonEmpty, "'And' requires at least one literal.")
}
final case object True extends Formula
}
object Logic
{
def reduceAll(clauses: List[Clause], initialFacts: Set[Literal]): Either[LogicException, Matched] =
reduce(Clauses(clauses), initialFacts)
/** Computes the variables in the unique stable model for the program represented by `clauses` and `initialFacts`.
* `clause` may not have any negative feedback (that is, negation is acyclic)
* and `initialFacts` cannot be in the head of any clauses in `clause`.
* These restrictions ensure that the logic program has a unique minimal model. */
def reduce(clauses: Clauses, initialFacts: Set[Literal]): Either[LogicException, Matched] =
{
val (posSeq, negSeq) = separate(initialFacts.toSeq)
val (pos, neg) = (posSeq.toSet, negSeq.toSet)
val problem =
checkContradictions(pos, neg) orElse
checkOverlap(clauses, pos) orElse
checkAcyclic(clauses)
problem.toLeft(
reduce0(clauses, initialFacts, Matched.empty)
)
}
/** Verifies `initialFacts` are not in the head of any `clauses`.
* This avoids the situation where an atom is proved but no clauses prove it.
* This isn't necessarily a problem, but the main sbt use cases expects
* a proven atom to have at least one clause satisfied. */
private[this] def checkOverlap(clauses: Clauses, initialFacts: Set[Atom]): Option[InitialOverlap] = {
val as = atoms(clauses)
val initialOverlap = initialFacts.filter(as.inHead)
if(initialOverlap.nonEmpty) Some(new InitialOverlap(initialOverlap)) else None
}
private[this] def checkContradictions(pos: Set[Atom], neg: Set[Atom]): Option[InitialContradictions] = {
val contradictions = pos intersect neg
if(contradictions.nonEmpty) Some(new InitialContradictions(contradictions)) else None
}
private[this] def checkAcyclic(clauses: Clauses): Option[CyclicNegation] = {
val deps = dependencyMap(clauses)
val cycle = Dag.findNegativeCycle(graph(deps))
if(cycle.nonEmpty) Some(new CyclicNegation(cycle)) else None
}
private[this] def graph(deps: Map[Atom, Set[Literal]]) = new Dag.DirectedSignedGraph[Atom] {
type Arrow = Literal
def nodes = deps.keys.toList
def dependencies(a: Atom) = deps.getOrElse(a, Set.empty).toList
def isNegative(b: Literal) = b match {
case Negated(_) => true
case Atom(_) => false
}
def head(b: Literal) = b.atom
}
private[this] def dependencyMap(clauses: Clauses): Map[Atom, Set[Literal]] =
(Map.empty[Atom, Set[Literal]] /: clauses.clauses) {
case (m, Clause(formula, heads)) =>
val deps = literals(formula)
(m /: heads) { (n, head) => n.updated(head, n.getOrElse(head, Set.empty) ++ deps) }
}
sealed abstract class LogicException(override val toString: String)
final class InitialContradictions(val literals: Set[Atom]) extends LogicException("Initial facts cannot be both true and false:\n\t" + literals.mkString("\n\t"))
final class InitialOverlap(val literals: Set[Atom]) extends LogicException("Initial positive facts cannot be implied by any clauses:\n\t" + literals.mkString("\n\t"))
final class CyclicNegation(val cycle: List[Literal]) extends LogicException("Negation may not be involved in a cycle:\n\t" + cycle.mkString("\n\t"))
/** Tracks proven atoms in the reverse order they were proved. */
final class Matched private(val provenSet: Set[Atom], reverseOrdered: List[Atom]) {
def add(atoms: Set[Atom]): Matched = add(atoms.toList)
def add(atoms: List[Atom]): Matched = {
val newOnly = atoms.filterNot(provenSet)
new Matched(provenSet ++ newOnly, newOnly ::: reverseOrdered)
}
def ordered: List[Atom] = reverseOrdered.reverse
override def toString = ordered.map(_.label).mkString("Matched(", ",", ")")
}
object Matched {
val empty = new Matched(Set.empty, Nil)
}
/** Separates a sequence of literals into `(pos, neg)` atom sequences. */
private[this] def separate(lits: Seq[Literal]): (Seq[Atom], Seq[Atom]) = Util.separate(lits) {
case a: Atom => Left(a)
case Negated(n) => Right(n)
}
/** Finds clauses that have no body and thus prove their head.
* Returns `(<proven atoms>, <remaining unproven clauses>)`. */
private[this] def findProven(c: Clauses): (Set[Atom], List[Clause]) =
{
val (proven, unproven) = c.clauses.partition(_.body == True)
(proven.flatMap(_.head).toSet, unproven)
}
private[this] def keepPositive(lits: Set[Literal]): Set[Atom] =
lits.collect{ case a: Atom => a}.toSet
// precondition: factsToProcess contains no contradictions
@tailrec
private[this] def reduce0(clauses: Clauses, factsToProcess: Set[Literal], state: Matched): Matched =
applyAll(clauses, factsToProcess) match {
case None => // all of the remaining clauses failed on the new facts
state
case Some(applied) =>
val (proven, unprovenClauses) = findProven(applied)
val processedFacts = state add keepPositive(factsToProcess)
val newlyProven = proven -- processedFacts.provenSet
val newState = processedFacts add newlyProven
if(unprovenClauses.isEmpty)
newState // no remaining clauses, done.
else {
val unproven = Clauses(unprovenClauses)
val nextFacts: Set[Literal] = if(newlyProven.nonEmpty) newlyProven.toSet else inferFailure(unproven)
reduce0(unproven, nextFacts, newState)
}
}
/** Finds negated atoms under the negation as failure rule and returns them.
* This should be called only after there are no more known atoms to be substituted. */
private[this] def inferFailure(clauses: Clauses): Set[Literal] =
{
/* At this point, there is at least one clause and one of the following is the case as the result of the acyclic negation rule:
i. there is at least one variable that occurs in a clause body but not in the head of a clause
ii. there is at least one variable that occurs in the head of a clause and does not transitively depend on a negated variable
In either case, each such variable x cannot be proven true and therefore proves 'not x' (negation as failure, !x in the code).
*/
val allAtoms = atoms(clauses)
val newFacts: Set[Literal] = negated(allAtoms.triviallyFalse)
if(newFacts.nonEmpty)
newFacts
else {
val possiblyTrue = hasNegatedDependency(clauses.clauses, Relation.empty, Relation.empty)
val newlyFalse: Set[Literal] = negated(allAtoms.inHead -- possiblyTrue)
if(newlyFalse.nonEmpty)
newlyFalse
else // should never happen due to the acyclic negation rule
error(s"No progress:\n\tclauses: $clauses\n\tpossibly true: $possiblyTrue")
}
}
private[this] def negated(atoms: Set[Atom]): Set[Literal] = atoms.map(a => Negated(a))
/** Computes the set of atoms in `clauses` that directly or transitively take a negated atom as input.
* For example, for the following clauses, this method would return `List(a, d)` :
* a :- b, not c
* d :- a
*/
@tailrec
def hasNegatedDependency(clauses: Seq[Clause], posDeps: Relation[Atom, Atom], negDeps: Relation[Atom, Atom]): List[Atom] =
clauses match {
case Seq() =>
// because cycles between positive literals are allowed, this isn't strictly a topological sort
Dag.topologicalSortUnchecked(negDeps._1s)(posDeps.reverse)
case Clause(formula, head) +: tail =>
// collect direct positive and negative literals and track them in separate graphs
val (pos, neg) = directDeps(formula)
val (newPos, newNeg) = ( (posDeps, negDeps) /: head) { case ( (pdeps, ndeps), d) =>
(pdeps + (d, pos), ndeps + (d, neg) )
}
hasNegatedDependency(tail, newPos, newNeg)
}
/** Computes the `(positive, negative)` literals in `formula`. */
private[this] def directDeps(formula: Formula): (Seq[Atom], Seq[Atom]) =
Util.separate(literals(formula).toSeq) {
case Negated(a) => Right(a)
case a: Atom => Left(a)
}
private[this] def literals(formula: Formula): Set[Literal] = formula match {
case And(lits) => lits
case l: Literal => Set(l)
case True => Set.empty
}
/** Computes the atoms in the heads and bodies of the clauses in `clause`. */
def atoms(cs: Clauses): Atoms = cs.clauses.map(c => Atoms(c.head, atoms(c.body))).reduce(_ ++ _)
/** Computes the set of all atoms in `formula`. */
def atoms(formula: Formula): Set[Atom] = formula match {
case And(lits) => lits.map(_.atom)
case Negated(lit) => Set(lit)
case a: Atom => Set(a)
case True => Set()
}
/** Represents the set of atoms in the heads of clauses and in the bodies (formulas) of clauses. */
final case class Atoms(val inHead: Set[Atom], val inFormula: Set[Atom]) {
/** Concatenates this with `as`. */
def ++ (as: Atoms): Atoms = Atoms(inHead ++ as.inHead, inFormula ++ as.inFormula)
/** Atoms that cannot be true because they do not occur in a head. */
def triviallyFalse: Set[Atom] = inFormula -- inHead
}
/** Applies known facts to `clause`s, deriving a new, possibly empty list of clauses.
* 1. If a fact is in the body of a clause, the derived clause has that fact removed from the body.
* 2. If the negation of a fact is in a body of a clause, that clause fails and is removed.
* 3. If a fact or its negation is in the head of a clause, the derived clause has that fact (or its negation) removed from the head.
* 4. If a head is empty, the clause proves nothing and is removed.
*
* NOTE: empty bodies do not cause a clause to succeed yet.
* All known facts must be applied before this can be done in order to avoid inconsistencies.
* Precondition: no contradictions in `facts`
* Postcondition: no atom in `facts` is present in the result
* Postcondition: No clauses have an empty head
* */
def applyAll(cs: Clauses, facts: Set[Literal]): Option[Clauses] =
{
val newClauses =
if(facts.isEmpty)
cs.clauses.filter(_.head.nonEmpty) // still need to drop clauses with an empty head
else
cs.clauses.map(c => applyAll(c, facts)).flatMap(_.toList)
if(newClauses.isEmpty) None else Some(Clauses(newClauses))
}
def applyAll(c: Clause, facts: Set[Literal]): Option[Clause] =
{
val atoms = facts.map(_.atom)
val newHead = c.head -- atoms // 3.
if(newHead.isEmpty) // 4. empty head
None
else
substitute(c.body, facts).map( f => Clause(f, newHead) ) // 1, 2
}
/** Derives the formula that results from substituting `facts` into `formula`. */
@tailrec
def substitute(formula: Formula, facts: Set[Literal]): Option[Formula] = formula match {
case And(lits) =>
def negated(lits: Set[Literal]): Set[Literal] = lits.map(a => !a)
if( lits.exists( negated(facts) ) ) // 2.
None
else {
val newLits = lits -- facts
val newF = if(newLits.isEmpty) True else And(newLits)
Some(newF) // 1.
}
case True => Some(True)
case lit: Literal => // define in terms of And
substitute(And(Set(lit)), facts)
}
}

View File

@ -0,0 +1,117 @@
package sbt
package logic
import org.scalacheck._
import Prop.secure
import Logic.{LogicException, Matched}
object LogicTest extends Properties("Logic")
{
import TestClauses._
property("Handles trivial resolution.") = secure( expect(trivial, Set(A) ) )
property("Handles less trivial resolution.") = secure( expect(lessTrivial, Set(B,A,D)) )
property("Handles cycles without negation") = secure( expect(cycles, Set(F,A,B)) )
property("Handles basic exclusion.") = secure( expect(excludedPos, Set()) )
property("Handles exclusion of head proved by negation.") = secure( expect(excludedNeg, Set()) )
// TODO: actually check ordering, probably as part of a check that dependencies are satisifed
property("Properly orders results.") = secure( expect(ordering, Set(B,A,C,E,F)))
property("Detects cyclic negation") = secure(
Logic.reduceAll(badClauses, Set()) match {
case Right(res) => false
case Left(err: Logic.CyclicNegation) => true
case Left(err) => error(s"Expected cyclic error, got: $err")
}
)
def expect(result: Either[LogicException, Matched], expected: Set[Atom]) = result match {
case Left(err) => false
case Right(res) =>
val actual = res.provenSet
(actual == expected) || error(s"Expected to prove $expected, but actually proved $actual")
}
}
object TestClauses
{
val A = Atom("A")
val B = Atom("B")
val C = Atom("C")
val D = Atom("D")
val E = Atom("E")
val F = Atom("F")
val G = Atom("G")
val clauses =
A.proves(B) ::
A.proves(F) ::
B.proves(F) ::
F.proves(A) ::
(!C).proves(F) ::
D.proves(C) ::
C.proves(D) ::
Nil
val cycles = Logic.reduceAll(clauses, Set())
val badClauses =
A.proves(D) ::
clauses
val excludedNeg = {
val cs =
(!A).proves(B) ::
Nil
val init =
(!A) ::
(!B) ::
Nil
Logic.reduceAll(cs, init.toSet)
}
val excludedPos = {
val cs =
A.proves(B) ::
Nil
val init =
A ::
(!B) ::
Nil
Logic.reduceAll(cs, init.toSet)
}
val trivial = {
val cs =
Formula.True.proves(A) ::
Nil
Logic.reduceAll(cs, Set.empty)
}
val lessTrivial = {
val cs =
Formula.True.proves(A) ::
Formula.True.proves(B) ::
(A && B && (!C)).proves(D) ::
Nil
Logic.reduceAll(cs, Set())
}
val ordering = {
val cs =
E.proves(F) ::
(C && !D).proves(E) ::
(A && B).proves(C) ::
Nil
Logic.reduceAll(cs, Set(A,B))
}
def all {
println(s"Cycles: $cycles")
println(s"xNeg: $excludedNeg")
println(s"xPos: $excludedPos")
println(s"trivial: $trivial")
println(s"lessTrivial: $lessTrivial")
println(s"ordering: $ordering")
}
}

View File

@ -40,7 +40,7 @@ object Relation
private[sbt] def get[X,Y](map: M[X,Y], t: X): Set[Y] = map.getOrElse(t, Set.empty[Y]) private[sbt] def get[X,Y](map: M[X,Y], t: X): Set[Y] = map.getOrElse(t, Set.empty[Y])
private[sbt] type M[X,Y] = Map[X, Set[Y]] private[sbt] type M[X,Y] = Map[X, Set[Y]]
} }
/** Binary relation between A and B. It is a set of pairs (_1, _2) for _1 in A, _2 in B. */ /** Binary relation between A and B. It is a set of pairs (_1, _2) for _1 in A, _2 in B. */
@ -111,7 +111,7 @@ private final class MRelation[A,B](fwd: Map[A, Set[B]], rev: Map[B, Set[A]]) ext
{ {
def forwardMap = fwd def forwardMap = fwd
def reverseMap = rev def reverseMap = rev
def forward(t: A) = get(fwd, t) def forward(t: A) = get(fwd, t)
def reverse(t: B) = get(rev, t) def reverse(t: B) = get(rev, t)
@ -119,12 +119,12 @@ private final class MRelation[A,B](fwd: Map[A, Set[B]], rev: Map[B, Set[A]]) ext
def _2s = rev.keySet def _2s = rev.keySet
def size = (fwd.valuesIterator map { _.size }).foldLeft(0)(_ + _) def size = (fwd.valuesIterator map { _.size }).foldLeft(0)(_ + _)
def all: Traversable[(A,B)] = fwd.iterator.flatMap { case (a, bs) => bs.iterator.map( b => (a,b) ) }.toTraversable def all: Traversable[(A,B)] = fwd.iterator.flatMap { case (a, bs) => bs.iterator.map( b => (a,b) ) }.toTraversable
def +(pair: (A,B)) = this + (pair._1, Set(pair._2)) def +(pair: (A,B)) = this + (pair._1, Set(pair._2))
def +(from: A, to: B) = this + (from, to :: Nil) def +(from: A, to: B) = this + (from, to :: Nil)
def +(from: A, to: Traversable[B]) = def +(from: A, to: Traversable[B]) = if(to.isEmpty) this else
new MRelation( add(fwd, from, to), (rev /: to) { (map, t) => add(map, t, from :: Nil) }) new MRelation( add(fwd, from, to), (rev /: to) { (map, t) => add(map, t, from :: Nil) })
def ++(rs: Traversable[(A,B)]) = ((this: Relation[A,B]) /: rs) { _ + _ } def ++(rs: Traversable[(A,B)]) = ((this: Relation[A,B]) /: rs) { _ + _ }