mirror of https://github.com/sbt/sbt.git
Merge pull request #1130 from sbt/wip/auto-plugins
AUTO PLUGIN FEATURE!!!!
This commit is contained in:
commit
a8acc11fb5
|
|
@ -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.*/
|
||||
def rootProject: Option[Project] = None
|
||||
}
|
||||
// TODO 0.14.0: decide if Plugin should be deprecated in favor of AutoPlugin
|
||||
trait Plugin
|
||||
{
|
||||
@deprecated("Override projectSettings or buildSettings instead.", "0.12.0")
|
||||
|
|
|
|||
|
|
@ -30,23 +30,108 @@ final class StructureIndex(
|
|||
val keyIndex: 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
|
||||
{
|
||||
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
|
||||
|
||||
/** The base directory of the build unit (not the build definition).*/
|
||||
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
|
||||
|
||||
/** 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
|
||||
|
||||
/** The imports to use for .sbt files, `consoleProject` and other contexts that use code from the build definition. */
|
||||
def imports = BuildUtil.getImports(unit)
|
||||
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 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 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)
|
||||
{
|
||||
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)
|
||||
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)
|
||||
|
||||
private[sbt] def autos = GroupedAutoPlugins(units)
|
||||
}
|
||||
final class PartBuild(val root: URI, val units: Map[URI, PartBuildUnit])
|
||||
sealed trait BuildUnitBase { def rootProjects: Seq[String]; def buildSettings: Seq[Setting[_]] }
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ final class BuildUtil[Proj](
|
|||
case _ => None
|
||||
}
|
||||
|
||||
val configurationsForAxis: Option[ResolvedReference] => Seq[String] =
|
||||
val configurationsForAxis: Option[ResolvedReference] => Seq[String] =
|
||||
refOpt => configurations(projectForAxis(refOpt)).map(_.name)
|
||||
}
|
||||
object BuildUtil
|
||||
|
|
@ -48,6 +48,20 @@ object BuildUtil
|
|||
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 getRef(pref: ProjectRef) = units(pref.build).defined(pref.project)
|
||||
|
|
@ -60,8 +74,14 @@ object BuildUtil
|
|||
}
|
||||
}
|
||||
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 importAllRoot(values: Seq[String]): Seq[String] = importAll(values map rootedName)
|
||||
def rootedName(s: String): String = if(s contains '.') "_root_." + s else s
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ $ShowCommand <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 LastGrepCommand = "last-grep"
|
||||
val ExportCommand = "export"
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ object Defaults extends BuildCommon
|
|||
unmanagedResources <<= collectFiles(unmanagedResourceDirectories, includeFilter in unmanagedResources, excludeFilter in unmanagedResources),
|
||||
watchSources in ConfigGlobal ++= unmanagedResources.value,
|
||||
resourceGenerators :== Nil,
|
||||
resourceGenerators <+= (definedSbtPlugins, resourceManaged) map writePluginsDescriptor,
|
||||
resourceGenerators <+= (discoveredSbtPlugins, resourceManaged) map PluginDiscovery.writeDescriptors,
|
||||
managedResources <<= generate(resourceGenerators),
|
||||
resources <<= Classpaths.concat(managedResources, unmanagedResources)
|
||||
)
|
||||
|
|
@ -233,6 +233,7 @@ object Defaults extends BuildCommon
|
|||
consoleQuick <<= consoleQuickTask,
|
||||
discoveredMainClasses <<= compile map discoverMainClasses storeAs discoveredMainClasses triggeredBy compile,
|
||||
definedSbtPlugins <<= discoverPlugins,
|
||||
discoveredSbtPlugins <<= discoverSbtPluginNames,
|
||||
inTask(run)(runnerTask :: Nil).head,
|
||||
selectMainClass := mainClass.value orElse selectRunMain(discoveredMainClasses.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 =
|
||||
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] =
|
||||
{
|
||||
val descriptor: File = dir / "sbt" / "sbt.plugins"
|
||||
if(plugins.isEmpty)
|
||||
{
|
||||
IO.delete(descriptor)
|
||||
Nil
|
||||
}
|
||||
else
|
||||
{
|
||||
IO.writeLines(descriptor, plugins.toSeq.sorted)
|
||||
descriptor :: Nil
|
||||
}
|
||||
PluginDiscovery.writeDescriptor(plugins.toSeq, dir, PluginDiscovery.Paths.Plugins).toList
|
||||
|
||||
def discoverSbtPluginNames: Initialize[Task[PluginDiscovery.DiscoveredNames]] = Def.task {
|
||||
if(sbtPlugin.value) PluginDiscovery.discoverSourceAll(compile.value) else PluginDiscovery.emptyDiscoveredNames
|
||||
}
|
||||
|
||||
@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 }
|
||||
|
||||
@deprecated("Use PluginDiscovery.sourceModuleNames[Plugin].", "0.13.2")
|
||||
def discoverSbtPlugins(analysis: inc.Analysis, log: Logger): Set[String] =
|
||||
{
|
||||
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;
|
||||
}
|
||||
PluginDiscovery.sourceModuleNames(analysis, classOf[Plugin].getName).toSet
|
||||
|
||||
def copyResourcesTask =
|
||||
(classDirectory, resources, resourceDirectories, streams) map { (target, resrcs, dirs, s) =>
|
||||
|
|
@ -1262,19 +1257,8 @@ object Classpaths
|
|||
if(useJars) Seq(pkgTask).join else psTask
|
||||
}
|
||||
|
||||
def constructBuildDependencies: Initialize[BuildDependencies] =
|
||||
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 constructBuildDependencies: Initialize[BuildDependencies] = loadedBuild(lb => BuildUtil.dependencies(lb.units))
|
||||
|
||||
def internalDependencies: Initialize[Task[Classpath]] =
|
||||
(thisProjectRef, classpathConfiguration, configuration, settingsData, buildDependencies) flatMap internalDependencies0
|
||||
def unmanagedDependencies: Initialize[Task[Classpath]] =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -131,6 +131,7 @@ object Keys
|
|||
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 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 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)
|
||||
|
|
@ -348,7 +349,7 @@ object Keys
|
|||
|
||||
// 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)
|
||||
|
||||
|
||||
val triggeredBy = Def.triggeredBy
|
||||
val runBefore = Def.runBefore
|
||||
|
||||
|
|
|
|||
|
|
@ -6,19 +6,16 @@ package sbt
|
|||
import java.io.File
|
||||
import java.net.{URI,URL}
|
||||
import compiler.{Eval,EvalImports}
|
||||
import xsbt.api.{Discovered,Discovery}
|
||||
import xsbti.compile.CompileOrder
|
||||
import classpath.ClasspathUtilities
|
||||
import scala.annotation.tailrec
|
||||
import collection.mutable
|
||||
import Compiler.{Compilers,Inputs}
|
||||
import Compiler.Compilers
|
||||
import inc.{FileValueCache, Locate}
|
||||
import Project.{inScope,makeSettings}
|
||||
import Def.{isDummy, ScopedKey, ScopeLocal, Setting}
|
||||
import Keys.{appConfiguration, baseDirectory, configuration, fullResolvers, fullClasspath, pluginData, streams, thisProject, thisProjectRef, update}
|
||||
import Keys.{exportedProducts, loadedBuild, onLoadMessage, resolvedScoped, sbtPlugin, scalacOptions, taskDefinitionKey}
|
||||
import tools.nsc.reporters.ConsoleReporter
|
||||
import Build.analyzed
|
||||
import Attributed.data
|
||||
import Scope.{GlobalScope, ThisScope}
|
||||
import Types.const
|
||||
|
|
@ -180,7 +177,7 @@ object Load
|
|||
val keys = Index.allKeys(settings)
|
||||
val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key)
|
||||
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 aggIndex = KeyIndex.aggregate(scopedKeys, extra(keyIndex), projectsMap)
|
||||
new sbt.StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex)
|
||||
|
|
@ -201,10 +198,10 @@ object Load
|
|||
{
|
||||
((loadedBuild in GlobalScope :== loaded) +:
|
||||
transformProjectOnly(loaded.root, rootProject, injectSettings.global)) ++
|
||||
inScope(GlobalScope)( pluginGlobalSettings(loaded) ) ++
|
||||
inScope(GlobalScope)( pluginGlobalSettings(loaded) ++ loaded.autos.globalSettings ) ++
|
||||
loaded.units.toSeq.flatMap { case (uri, build) =>
|
||||
val plugins = build.unit.plugins.plugins
|
||||
val pluginBuildSettings = plugins.flatMap(_.buildSettings)
|
||||
val plugins = build.unit.plugins.detected.plugins.values
|
||||
val pluginBuildSettings = plugins.flatMap(_.buildSettings) ++ loaded.autos.buildSettings(uri)
|
||||
val pluginNotThis = plugins.flatMap(_.settings) filterNot isProjectThis
|
||||
val projectSettings = build.defined flatMap { case (id, project) =>
|
||||
val ref = ProjectRef(uri, id)
|
||||
|
|
@ -220,9 +217,10 @@ object Load
|
|||
buildSettings ++ projectSettings
|
||||
}
|
||||
}
|
||||
@deprecated("Does not account for AutoPlugins and will be made private.", "0.13.2")
|
||||
def pluginGlobalSettings(loaded: sbt.LoadedBuild): Seq[Setting[_]] =
|
||||
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")
|
||||
|
|
@ -368,10 +366,11 @@ object Load
|
|||
def resolveProjects(loaded: sbt.PartBuild): sbt.LoadedBuild =
|
||||
{
|
||||
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)
|
||||
(uri, resolveProjects(uri, unit, rootProject))
|
||||
})
|
||||
}
|
||||
new sbt.LoadedBuild(loaded.root, units)
|
||||
}
|
||||
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 =
|
||||
map.getOrElse(uri, noBuild(uri))
|
||||
|
||||
def emptyBuild(uri: URI) = sys.error("No root project defined for build unit '" + uri + "'")
|
||||
def noBuild(uri: URI) = sys.error("Build unit '" + uri + "' not defined.")
|
||||
def noProject(uri: URI, id: String) = sys.error("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 emptyBuild(uri: URI) = sys.error(s"No root project defined for build unit '$uri'")
|
||||
def noBuild(uri: URI) = sys.error(s"Build unit '$uri' not defined.")
|
||||
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(s"No configuration '$conf' defined in project '$id' in '$uri'")
|
||||
|
||||
def loadUnit(uri: URI, localBase: File, s: State, config: sbt.LoadBuildConfiguration): sbt.BuildUnit =
|
||||
{
|
||||
|
|
@ -410,15 +409,13 @@ object Load
|
|||
val defDir = projectStandard(normBase)
|
||||
|
||||
val plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin))
|
||||
val defNames = analyzed(plugs.fullClasspath) flatMap findDefinitions
|
||||
val defsScala = if(defNames.isEmpty) Nil else loadDefinitions(plugs.loader, defNames)
|
||||
val imports = BuildUtil.getImports(plugs.pluginNames, defNames)
|
||||
val defsScala = plugs.detected.builds.values
|
||||
|
||||
lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions)
|
||||
val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase))
|
||||
|
||||
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 hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined)
|
||||
val (loadedProjects, defaultBuildIfNone) =
|
||||
|
|
@ -434,7 +431,7 @@ object Load
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -460,16 +457,22 @@ object Load
|
|||
private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] =
|
||||
b.projectDefinitions(base).map(resolveBase(base))
|
||||
|
||||
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, 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 =
|
||||
loadSettings(auto, base, imports, plugins, eval, injectSettings, memoSettings)
|
||||
def loadSbtFiles(auto: AddSettings, base: File, autoPlugins: Seq[AutoPlugin]): LoadedSbtFile =
|
||||
loadSettings(auto, base, plugins, eval, injectSettings, memoSettings, autoPlugins)
|
||||
def loadForProjects = newProjects map { project =>
|
||||
val loadedSbtFiles = loadSbtFiles(project.auto, project.base)
|
||||
val transformed = project.copy(settings = (project.settings: Seq[Setting[_]]) ++ loadedSbtFiles.settings)
|
||||
val autoPlugins =
|
||||
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)
|
||||
}
|
||||
def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase).projects
|
||||
def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase, Nil).projects
|
||||
val (nextProjects, loadedProjects) =
|
||||
if(newProjects.isEmpty) // load the .sbt files in the root directory to look for Projects
|
||||
(defaultLoad, acc)
|
||||
|
|
@ -481,10 +484,12 @@ object Load
|
|||
if(nextProjects.isEmpty)
|
||||
loadedProjects
|
||||
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)
|
||||
def settings(ss: Seq[Setting[_]]) = new LoadedSbtFile(ss, Nil, Nil)
|
||||
|
|
@ -499,14 +504,20 @@ object Load
|
|||
lf
|
||||
}
|
||||
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}
|
||||
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 {
|
||||
case User => settings(injectSettings.projectLoaded(loader))
|
||||
case sf: SbtFiles => loadSettings( sf.files.map(f => IO.resolve(projectBase, f)))
|
||||
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) ) }
|
||||
}
|
||||
expand(auto)
|
||||
|
|
@ -599,67 +610,46 @@ object Load
|
|||
config.evalPluginDef(pluginDef, pluginState)
|
||||
}
|
||||
|
||||
@deprecated("Use ModuleUtilities.getCheckedObjects[Build].", "0.13.2")
|
||||
def loadDefinitions(loader: ClassLoader, defs: Seq[String]): Seq[Build] =
|
||||
defs map { definition => loadDefinition(loader, definition) }
|
||||
|
||||
@deprecated("Use ModuleUtilities.getCheckedObject[Build].", "0.13.2")
|
||||
def loadDefinition(loader: ClassLoader, definition: String): Build =
|
||||
ModuleUtilities.getObject(definition, loader).asInstanceOf[Build]
|
||||
|
||||
def loadPlugins(dir: File, data: PluginData, loader: ClassLoader): sbt.LoadedPlugins =
|
||||
{
|
||||
val (pluginNames, plugins) = if(data.classpath.isEmpty) (Nil, Nil) else {
|
||||
val names = getPluginNames(data.classpath, loader)
|
||||
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)
|
||||
}
|
||||
new sbt.LoadedPlugins(dir, data, loader, PluginDiscovery.discoverAll(data, loader))
|
||||
|
||||
@deprecated("Replaced by the more general PluginDiscovery.binarySourceModuleNames and will be made private.", "0.13.2")
|
||||
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] =
|
||||
{
|
||||
import collection.JavaConversions._
|
||||
loader.getResources("sbt/sbt.plugins").toSeq.filter(onClasspath(classpath)) flatMap { u =>
|
||||
IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty)
|
||||
}
|
||||
}
|
||||
PluginDiscovery.binaryModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins)
|
||||
|
||||
@deprecated("Use PluginDiscovery.onClasspath", "0.13.2")
|
||||
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] =
|
||||
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 =
|
||||
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")
|
||||
|
||||
@deprecated("No longer used.", "0.13.2")
|
||||
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] =
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
PluginDiscovery.sourceModuleNames(analysis, subclasses : _*)
|
||||
|
||||
def initialSession(structure: sbt.BuildStructure, rootEval: () => Eval, s: State): SessionSettings = {
|
||||
val session = s get Keys.sessionSettings
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ object BuiltinCommands
|
|||
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,
|
||||
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) ++
|
||||
compatCommands
|
||||
def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil
|
||||
|
|
@ -125,7 +125,8 @@ object BuiltinCommands
|
|||
|
||||
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: ", ", ", "")
|
||||
}
|
||||
def aboutScala(s: State, e: Extracted): String =
|
||||
|
|
@ -374,6 +375,20 @@ object BuiltinCommands
|
|||
Help.detailOnly(taskDetail(allTaskAndSettingKeys(s)))
|
||||
else
|
||||
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).?) {
|
||||
case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -50,33 +50,52 @@ sealed trait ProjectDefinition[PR <: ProjectReference]
|
|||
/** Configures the sources of automatically appended settings.*/
|
||||
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 equals(o: Any) = o match {
|
||||
case p: ProjectDefinition[_] => p.getClass == this.getClass && p.id == id && p.base == base
|
||||
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]
|
||||
{
|
||||
// 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,
|
||||
delegates: => Seq[ProjectReference] = delegates, settings: => Seq[Setting[_]] = settings, configurations: Seq[Configuration] = configurations,
|
||||
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 resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
|
||||
def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep
|
||||
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 resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
|
||||
def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep
|
||||
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.
|
||||
|
|
@ -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.
|
||||
* Any configured .sbt files are removed from this project's list.*/
|
||||
def setSbtFiles(files: File*): Project = copy(auto = AddSettings.append( AddSettings.clearSbtFiles(auto), AddSettings.sbtFiles(files: _*)) )
|
||||
|
||||
/** Sets the [[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] }
|
||||
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)
|
||||
|
||||
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 dependencies = dependencies0
|
||||
lazy val delegates = delegates0
|
||||
lazy val settings = settings0
|
||||
|
||||
|
||||
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,
|
||||
delegates: => Seq[ProjectReference] = Nil, settings: => Seq[Def.Setting[_]] = defaultSettings, configurations: Seq[Configuration] = Configurations.default,
|
||||
auto: AddSettings = AddSettings.allDefaults): Project =
|
||||
{
|
||||
validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg))
|
||||
new ProjectDef[ProjectReference](id, base, aggregate, dependencies, delegates, settings, configurations, auto) with Project
|
||||
}
|
||||
unresolved(id, base, aggregate, dependencies, delegates, settings, configurations, auto, Natures.empty, Nil)
|
||||
|
||||
/** 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
|
||||
|
|
@ -185,9 +225,23 @@ object Project extends ProjectExtra
|
|||
* This is a best effort implementation, since valid characters are not documented or consistent.*/
|
||||
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],
|
||||
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
|
||||
|
||||
|
|
@ -307,7 +361,7 @@ object Project extends ProjectExtra
|
|||
def details(structure: BuildStructure, actual: Boolean, scope: Scope, key: AttributeKey[_])(implicit display: Show[ScopedKey[_]]): String =
|
||||
{
|
||||
val scoped = ScopedKey(scope,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 => "" }
|
||||
|
||||
|
|
@ -413,7 +467,7 @@ object Project extends ProjectExtra
|
|||
import DefaultParsers._
|
||||
|
||||
val loadActionParser = token(Space ~> ("plugins" ^^^ Plugins | "return" ^^^ Return)) ?? Current
|
||||
|
||||
|
||||
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 inPluginProject(s: State): Boolean = projectReturn(s).toList.length > 1
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ object Sbt extends Build
|
|||
lazy val datatypeSub = baseProject(utilPath /"datatype", "Datatype Generator") dependsOn(ioSub)
|
||||
// cross versioning
|
||||
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 **** */
|
||||
|
||||
|
|
@ -130,7 +132,7 @@ object Sbt extends Build
|
|||
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.
|
||||
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
|
||||
// technically, we need a dependency on all of mainSub's dependencies, but we don't do that since this is strictly an integration project
|
||||
|
|
|
|||
|
|
@ -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'")
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
> check
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
sbtPlugin := true
|
||||
|
||||
name := "demo-plugin"
|
||||
|
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("org.example" % "demo-plugin" % "3.4")
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
organization in ThisBuild := "org.example"
|
||||
|
||||
version in ThisBuild := "3.4"
|
||||
|
||||
lazy val define = project
|
||||
|
||||
lazy val use = project
|
||||
|
|
@ -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
|
||||
|
|
@ -6,7 +6,7 @@ package sbt
|
|||
object ModuleUtilities
|
||||
{
|
||||
/** 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. */
|
||||
def getObject(className: String, loader: ClassLoader): AnyRef =
|
||||
{
|
||||
|
|
@ -14,4 +14,10 @@ object ModuleUtilities
|
|||
val singletonField = obj.getField("MODULE$")
|
||||
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)))
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ object Dag
|
|||
import JavaConverters.asScalaSetConverter
|
||||
|
||||
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] =
|
||||
{
|
||||
val discovered = new mutable.HashSet[T]
|
||||
|
|
@ -24,7 +24,7 @@ object Dag
|
|||
def visitAll(nodes: Iterable[T]) = nodes foreach visit
|
||||
def visit(node : T){
|
||||
if (!discovered(node)) {
|
||||
discovered(node) = true;
|
||||
discovered(node) = true;
|
||||
try { visitAll(dependencies(node)); } catch { case c: Cyclic => throw node :: c }
|
||||
finished += node;
|
||||
}
|
||||
|
|
@ -33,11 +33,13 @@ object Dag
|
|||
}
|
||||
|
||||
visitAll(nodes);
|
||||
|
||||
|
||||
finished.toList;
|
||||
}
|
||||
// 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]
|
||||
var finished: List[T] = Nil
|
||||
|
|
@ -45,23 +47,23 @@ object Dag
|
|||
def visitAll(nodes: Iterable[T]) = nodes foreach visit
|
||||
def visit(node : T){
|
||||
if (!discovered(node)) {
|
||||
discovered(node) = true;
|
||||
discovered(node) = true;
|
||||
visitAll(dependencies(node))
|
||||
finished ::= node;
|
||||
}
|
||||
}
|
||||
|
||||
visit(node);
|
||||
visitAll(nodes);
|
||||
finished;
|
||||
}
|
||||
final class Cyclic(val value: Any, val all: List[Any], val complete: Boolean)
|
||||
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)
|
||||
override def toString = getMessage
|
||||
def ::(a: Any): Cyclic =
|
||||
def ::(a: Any): Cyclic =
|
||||
if(complete)
|
||||
this
|
||||
else if(a == value)
|
||||
|
|
@ -69,5 +71,62 @@ object Dag
|
|||
else
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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] 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. */
|
||||
|
|
@ -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 reverseMap = rev
|
||||
|
||||
|
||||
def forward(t: A) = get(fwd, 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 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 +(pair: (A,B)) = this + (pair._1, Set(pair._2))
|
||||
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) })
|
||||
|
||||
def ++(rs: Traversable[(A,B)]) = ((this: Relation[A,B]) /: rs) { _ + _ }
|
||||
|
|
|
|||
Loading…
Reference in New Issue