From b8619f4aaecedd3bac6a6975f997239105ceb7aa Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Tue, 14 Jan 2014 12:38:06 -0500 Subject: [PATCH] Main part of integrating natures into project loading. --- main/src/main/scala/sbt/AutoPlugin.scala | 43 ++++-- main/src/main/scala/sbt/Build.scala | 1 + main/src/main/scala/sbt/BuildStructure.scala | 27 +++- main/src/main/scala/sbt/BuildUtil.scala | 12 +- .../main/scala/sbt/GroupedAutoPlugins.scala | 20 +++ main/src/main/scala/sbt/Load.scala | 146 ++++++++++++------ main/src/main/scala/sbt/Main.scala | 3 +- main/src/main/scala/sbt/Project.scala | 76 +++++++-- project/Sbt.scala | 2 +- .../sbt-test/project/auto-plugins/build.sbt | 34 ++++ .../project/auto-plugins/project/Q.scala | 65 ++++++++ sbt/src/sbt-test/project/auto-plugins/test | 1 + .../src/main/scala/sbt/ModuleUtilities.scala | 8 +- 13 files changed, 363 insertions(+), 75 deletions(-) create mode 100644 main/src/main/scala/sbt/GroupedAutoPlugins.scala create mode 100644 sbt/src/sbt-test/project/auto-plugins/build.sbt create mode 100644 sbt/src/sbt-test/project/auto-plugins/project/Q.scala create mode 100644 sbt/src/sbt-test/project/auto-plugins/test diff --git a/main/src/main/scala/sbt/AutoPlugin.scala b/main/src/main/scala/sbt/AutoPlugin.scala index cbf8d8bc5..9f11a6771 100644 --- a/main/src/main/scala/sbt/AutoPlugin.scala +++ b/main/src/main/scala/sbt/AutoPlugin.scala @@ -4,6 +4,9 @@ package sbt 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 that the settings are automatically added to a build (called "activation"). The `select` method defines the conditions, @@ -59,15 +62,15 @@ abstract class 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 + 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 + 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 + def globalSettings: Seq[Setting[_]] = Nil // TODO?: def commands: Seq[Command] } @@ -82,19 +85,30 @@ sealed trait Natures { final case class Nature(label: String) extends Basic { /** Constructs a Natures matcher that excludes this Nature. */ def unary_! : Basic = Exclude(this) + override def toString = label } object Natures { // TODO: allow multiple AutoPlugins to provide the same Nature? // TODO: translate error messages - /** Select the AutoPlugins to include according to the user-specified natures in `requested` and all discovered AutoPlugins in `defined`.*/ - def evaluate(requested: Natures, defined: List[AutoPlugin]): Seq[AutoPlugin] = - { - val byAtom = defined.map(x => (Atom(x.provides.label), x)).toMap - val clauses = Clauses( defined.map(d => asClause(d)) ) - val results = Logic.reduce(clauses, flatten(requested).toSet) - results.ordered.map(byAtom) + def compile(defined: List[AutoPlugin]): Natures => Seq[AutoPlugin] = + if(defined.isEmpty) + Types.const(Nil) + else + { + val byAtom = defined.map(x => (Atom(x.provides.label), x)).toMap + val clauses = Clauses( defined.map(d => asClause(d)) ) + requestedNatures => { + val results = Logic.reduce(clauses, flatten(requestedNatures).toSet) + results.ordered.flatMap(a => byAtom.get(a).toList) + } + } + + def empty: Natures = Empty + private[sbt] final object Empty extends Natures { + def &&(o: Basic): Natures = o + override def toString = "" } /** An included or excluded Nature. TODO: better name than Basic. */ @@ -103,9 +117,16 @@ object Natures } private[sbt] final case class Exclude(n: Nature) extends Basic { def unary_! : Nature = n + 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 asClause(ap: AutoPlugin): Clause = @@ -114,11 +135,13 @@ object Natures private[this] def flatten(n: Natures): Seq[Literal] = n match { case And(ns) => convertAll(ns) case b: Basic => convertBasic(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) diff --git a/main/src/main/scala/sbt/Build.scala b/main/src/main/scala/sbt/Build.scala index 0501f9259..7bcb704ec 100644 --- a/main/src/main/scala/sbt/Build.scala +++ b/main/src/main/scala/sbt/Build.scala @@ -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") diff --git a/main/src/main/scala/sbt/BuildStructure.scala b/main/src/main/scala/sbt/BuildStructure.scala index 9db86a90f..7b35d348d 100644 --- a/main/src/main/scala/sbt/BuildStructure.scala +++ b/main/src/main/scala/sbt/BuildStructure.scala @@ -41,11 +41,35 @@ final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, Resolv override def toString = unit.toString } +// TODO: figure out how to deprecate and drop buildNames 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]) + +final class DetectedModules[T](val modules: Seq[(String, T)]) { + def names: Seq[String] = modules.map(_._1) + def values: Seq[T] = modules.map(_._2) +} + +final class DetectedPlugins(val plugins: DetectedModules[Plugin], val autoImports: DetectedModules[AutoImport], val autoPlugins: DetectedModules[AutoPlugin], val builds: DetectedModules[Build]) { + lazy val imports: Seq[String] = BuildUtil.getImports(plugins.names ++ builds.names ++ autoImports.names) + lazy val compileNatures: Natures => Seq[AutoPlugin] = Natures.compile(autoPlugins.values.toList) +} +final class LoadedPlugins(val base: File, val pluginData: PluginData, val loader: ClassLoader, val detected: DetectedPlugins) +{ +/* + // TODO: uncomment before COMMIT for compatibility + @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, DetectedPlugins(DetectedModules(pluginNames zip plugins), DetectedModules(Nil), DetectedModules(Nil), DetectedModules(Nil))) + @deprecated("Use detected.plugins.values.", "0.13.2") + val plugins = detected.plugins.values + @deprecated("Use detected.plugins.names.", "0.13.2") + val pluginNames = detected.plugins.names +*/ + def fullClasspath: Seq[Attributed[File]] = pluginData.classpath def classpath = data(fullClasspath) + } final class BuildUnit(val uri: URI, val localBase: File, val definitions: LoadedDefinitions, val plugins: LoadedPlugins) { @@ -57,6 +81,7 @@ 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[_]] } diff --git a/main/src/main/scala/sbt/BuildUtil.scala b/main/src/main/scala/sbt/BuildUtil.scala index df57581bd..c48e721f7 100644 --- a/main/src/main/scala/sbt/BuildUtil.scala +++ b/main/src/main/scala/sbt/BuildUtil.scala @@ -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 @@ -60,8 +60,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 diff --git a/main/src/main/scala/sbt/GroupedAutoPlugins.scala b/main/src/main/scala/sbt/GroupedAutoPlugins.scala new file mode 100644 index 000000000..2c99b2d85 --- /dev/null +++ b/main/src/main/scala/sbt/GroupedAutoPlugins.scala @@ -0,0 +1,20 @@ +package sbt + + import Def.Setting + import java.net.URI + +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) +} + +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) + } +} \ No newline at end of file diff --git a/main/src/main/scala/sbt/Load.scala b/main/src/main/scala/sbt/Load.scala index 8b7f3465a..f68ae1878 100755 --- a/main/src/main/scala/sbt/Load.scala +++ b/main/src/main/scala/sbt/Load.scala @@ -180,7 +180,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 +201,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 +220,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 +369,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 +401,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 +412,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 +434,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 +460,19 @@ 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 = plugins.detected.compileNatures(project.natures) + val autoConfigs = autoPlugins.flatMap(_.projectConfigurations) + val loadedSbtFiles = loadSbtFiles(project.auto, project.base, autoPlugins) + val newSettings = (project.settings: Seq[Setting[_]]) ++ loadedSbtFiles.settings + 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,10 @@ 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 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 +502,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,27 +608,48 @@ object Load config.evalPluginDef(pluginDef, pluginState) } + +/* +// TODO: UNCOMMENT BEFORE COMMIT + @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) + // TODO: binary detection for builds, autoImports, autoPlugins + import AutoBinaryResource._ + val plugins = detectModules[Plugin](data, loader, Plugins) + val builds = detectModules[Build](data, loader, Builds) + val autoImports = detectModules[AutoImport](data, loader, AutoImports) + val autoPlugins = detectModules[AutoPlugin](data, loader, AutoPlugins) + val detected = new DetectedPlugins(plugins, autoImports, autoPlugins, builds) + new sbt.LoadedPlugins(dir, data, loader, detected) } + private[this] def detectModules[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 = discoverModuleNames(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)) @@ -629,26 +659,54 @@ object Load val msgExtra = if(evictedStrings.isEmpty) "" else "\nNote that conflicts were resolved for some dependencies:\n\t" + evictedStrings.mkString("\n\t") throw new IncompatiblePluginsException(msgBase + msgExtra, t) } - def getPluginNames(classpath: Seq[Attributed[File]], loader: ClassLoader): Seq[String] = - ( binaryPlugins(data(classpath), loader) ++ (analyzed(classpath) flatMap findPlugins) ).distinct + def discoverModuleNames(classpath: Seq[Attributed[File]], loader: ClassLoader, resourceName: String, moduleTypes: String*): Seq[String] = + ( + binaryPlugins(data(classpath), loader, resourceName) ++ + (analyzed(classpath) flatMap (a => discover(a, moduleTypes : _*))) + ).distinct + + @deprecated("Replaced by the more general discoverModuleNames and will be made private.", "0.13.2") + def getPluginNames(classpath: Seq[Attributed[File]], loader: ClassLoader): Seq[String] = + discoverModuleNames(classpath, loader, AutoBinaryResource.Plugins, classOf[Plugin].getName) + +/* +TODO: UNCOMMENT BEFORE COMMIT + @deprecated("Explicitly specify the resource name.", "0.13.2") def binaryPlugins(classpath: Seq[File], loader: ClassLoader): Seq[String] = + binaryPlugins(classpath, loader, AutoBinaryResource.Plugins) +*/ + + object AutoBinaryResource { + final val AutoPlugins = "sbt/sbt.autoplugins" + final val Plugins = "sbt/sbt.plugins" + final val Builds = "sbt/sbt.builds" + final val AutoImports = "sbt/sbt.autoimports" + } + def binaryPlugins(classpath: Seq[File], loader: ClassLoader, resourceName: String): Seq[String] = { import collection.JavaConversions._ - loader.getResources("sbt/sbt.plugins").toSeq.filter(onClasspath(classpath)) flatMap { u => + loader.getResources(resourceName).toSeq.filter(onClasspath(classpath)) flatMap { u => IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty) } } def onClasspath(classpath: Seq[File])(url: URL): Boolean = IO.urlAsFile(url) exists (classpath.contains _) +/* +// TODO: UNCOMMENT BEFORE COMMIT + @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](loader, pluginNames) + @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") +*/ + def findDefinitions(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Build") def discover(analysis: inc.Analysis, subclasses: String*): Seq[String] = { diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 295ffca33..462d5a49b 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -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(_.provides) ++ 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 = diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index db0705299..8baa06997 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -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(_.provides)) + 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,24 @@ 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 [[Natures]] of this project. + A [[Nature]] is a common label that is used by plugins to determine what settings, if any, to add to a project. */ + def addNatures(ns: Natures): Project = { + // TODO: for 0.14.0, use copy when it has the additional `natures` parameter + unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, Natures.and(natures, ns), autoPlugins) + } + + /** 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 +185,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 +219,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 +355,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 +461,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 diff --git a/project/Sbt.scala b/project/Sbt.scala index e6fdf84ca..3fb03a64a 100644 --- a/project/Sbt.scala +++ b/project/Sbt.scala @@ -73,7 +73,7 @@ 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 monotonic logic that includes restricted negation as failure + // A logic with restricted negation as failure for a unique, stable model lazy val logicSub = baseProject(utilPath / "logic", "Logic").dependsOn(collectionSub, relationSub) /* **** Intermediate-level Modules **** */ diff --git a/sbt/src/sbt-test/project/auto-plugins/build.sbt b/sbt/src/sbt-test/project/auto-plugins/build.sbt new file mode 100644 index 000000000..d9543939b --- /dev/null +++ b/sbt/src/sbt-test/project/auto-plugins/build.sbt @@ -0,0 +1,34 @@ +// !C will exclude C, and thus D, from being auto-added +lazy val a = project.addNatures(A && B && !C) + +// 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'") +} \ No newline at end of file diff --git a/sbt/src/sbt-test/project/auto-plugins/project/Q.scala b/sbt/src/sbt-test/project/auto-plugins/project/Q.scala new file mode 100644 index 000000000..73dd5211b --- /dev/null +++ b/sbt/src/sbt-test/project/auto-plugins/project/Q.scala @@ -0,0 +1,65 @@ + 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 C = Nature("C") + lazy val D = Nature("D") + lazy val E = Nature("E") + + lazy val q = config("q") + lazy val p = config("p").extend(q) + + 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 + + def provides = C + + 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 = C && !D + + def provides = E + + 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 + ) +} \ No newline at end of file diff --git a/sbt/src/sbt-test/project/auto-plugins/test b/sbt/src/sbt-test/project/auto-plugins/test new file mode 100644 index 000000000..15675b169 --- /dev/null +++ b/sbt/src/sbt-test/project/auto-plugins/test @@ -0,0 +1 @@ +> check diff --git a/util/classpath/src/main/scala/sbt/ModuleUtilities.scala b/util/classpath/src/main/scala/sbt/ModuleUtilities.scala index d939c040b..69dfa31dc 100644 --- a/util/classpath/src/main/scala/sbt/ModuleUtilities.scala +++ b/util/classpath/src/main/scala/sbt/ModuleUtilities.scala @@ -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))) } \ No newline at end of file