diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 9310c44f7..5ca5a6356 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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) => diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 17f64e0b3..ceb7813ed 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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 diff --git a/main/src/main/scala/sbt/Load.scala b/main/src/main/scala/sbt/Load.scala index 2e9e1504f..b822a96cb 100755 --- a/main/src/main/scala/sbt/Load.scala +++ b/main/src/main/scala/sbt/Load.scala @@ -6,7 +6,6 @@ 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 @@ -18,7 +17,6 @@ package sbt 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 @@ -618,82 +616,23 @@ object Load ModuleUtilities.getObject(definition, loader).asInstanceOf[Build] def loadPlugins(dir: File, data: PluginData, loader: ClassLoader): sbt.LoadedPlugins = - new sbt.LoadedPlugins(dir, data, loader, autoDetect(data, loader)) + new sbt.LoadedPlugins(dir, data, loader, PluginDiscovery.discoverAll(data, loader)) - private[this] def autoDetect(data: PluginData, loader: ClassLoader): DetectedPlugins = - { - // 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) - new DetectedPlugins(plugins, autoImports, autoPlugins, builds) - } - 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)) - val evictedModules = evicted map { id => (id.organization, id.name) } distinct ; - val evictedStrings = evictedModules map { case (o,n) => o + ":" + n } - val msgBase = "Binary incompatibility in plugins detected." - val msgExtra = if(evictedStrings.isEmpty) "" else "\nNote that conflicts were resolved for some dependencies:\n\t" + evictedStrings.mkString("\n\t") - throw new IncompatiblePluginsException(msgBase + msgExtra, t) - } - - def 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") + @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] = - discoverModuleNames(classpath, loader, AutoBinaryResource.Plugins, classOf[Plugin].getName) + PluginDiscovery.binarySourceModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins, classOf[Plugin].getName) - @deprecated("Explicitly specify the resource name.", "0.13.2") + @deprecated("Use PluginDiscovery.binaryModuleNames.", "0.13.2") def binaryPlugins(classpath: Seq[File], loader: ClassLoader): Seq[String] = - binaryPlugins(classpath, loader, AutoBinaryResource.Plugins) + PluginDiscovery.binaryModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins) - /** 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 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(resourceName).toSeq.filter(onClasspath(classpath)) flatMap { u => - IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty) - } - } + @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] = - ModuleUtilities.getCheckedObjects[Plugin](loader, pluginNames) + ModuleUtilities.getCheckedObjects[Plugin](pluginNames, loader).map(_._2) @deprecated("Use ModuleUtilities.getCheckedObject[Plugin].", "0.13.2") def loadPlugin(pluginName: String, loader: ClassLoader): Plugin = @@ -702,17 +641,12 @@ object Load @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 diff --git a/main/src/main/scala/sbt/PluginDiscovery.scala b/main/src/main/scala/sbt/PluginDiscovery.scala new file mode 100644 index 000000000..351debfb8 --- /dev/null +++ b/main/src/main/scala/sbt/PluginDiscovery.scala @@ -0,0 +1,121 @@ +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" + } + 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) + + 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)) + } + 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 + 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) + } + + 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) + } + } + + + 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 + + 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 + } + } + + 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) + } + } + 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) + } +} \ No newline at end of file