summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/plugins
diff options
context:
space:
mode:
authorSom Snytt <som.snytt@gmail.com>2013-11-20 11:44:13 -0800
committerSom Snytt <som.snytt@gmail.com>2013-12-06 12:48:07 -0800
commit1d30ea86690db1b3b074190094d1f62d12e4efe1 (patch)
treed6ee4d131ec26f75083883ad0026e8b0c189aee6 /src/compiler/scala/tools/nsc/plugins
parent7d28c4966e5b32b46fda927f303aea4af20c4517 (diff)
downloadscala-1d30ea86690db1b3b074190094d1f62d12e4efe1.tar.gz
scala-1d30ea86690db1b3b074190094d1f62d12e4efe1.tar.bz2
scala-1d30ea86690db1b3b074190094d1f62d12e4efe1.zip
SI-4841 Plugins get a class path
Let -Xplugin specify a class path (or multiple of them). Each entry can be a jar or dir, and the first entry to supply a plugin descriptor defines the plugin to load. If no plugin is found on the path, then issue a warning if `-Xdev`. This honors the legacy silent treatment (which scala-ide tests for). In the proposed scheme, each plugin gets a class loader so that plugins are isolated from each other. Presumably, if compiler plugins were a rich ecosystem, in which shared dependencies were required in incompatible versions, this would have become a requirement by now. (Updated with a `DirectTest` that uses two plugins, but keeping the following as documentation.) Partest can't do multiple plugins yet, but this is what it looks like: ``` skalac -Xplugin:sample.jar:useful.jar:util,needful.jar:another.jar:util,needful.jar:util:exploded -Xplugin-require:sample,another,other foo.scala skalac -Xplugin:sample.jar:useful.jar:util,needful.jar:another.jar:util,needful.jar:util:exploded -Xplugin-require:sample,other -Xplugin-disable:another foo.scala skalac -Xplugin:sample.jar:useful.jar:util,sample.jar:useful.jar:util -Xplugin-require:sample foo.scala ``` The manual test shows three plugins with various permutations of jars and dirs. The manual test demonstrates that plugins only see classes on their class path: ``` Initializing test.plugins.SamplePlugin needful.Needful? Failure(java.lang.ClassNotFoundException: needful.Needful) useful.Useful? Success(class useful.Useful) Initializing more.plugins.AnotherPlugin needful.Needful? Success(class needful.Needful) useful.Useful? Failure(java.lang.ClassNotFoundException: useful.Useful) Initializing other.plugins.OtherPlugin ``` Disabling a plugin results in a message instead of silent suppression: ``` Disabling plugin another ``` The duplicate plugin class test must still be honored: ``` Ignoring duplicate plugin sample (test.plugins.SamplePlugin) Initializing test.plugins.SamplePlugin needful.Needful? Failure(java.lang.ClassNotFoundException: needful.Needful) useful.Useful? Success(class useful.Useful) ``` If the path is bad, then missing classes will report which plugin induced the error: ``` Error: class not found: util/Probe required by test.plugins.SamplePlugin Error: class not found: util/Probe required by more.plugins.AnotherPlugin Initializing other.plugins.OtherPlugin needful.Needful? Success(class needful.Needful) useful.Useful? Failure(java.lang.ClassNotFoundException: useful.Useful) error: Missing required plugin: sample error: Missing required plugin: another two errors found ```
Diffstat (limited to 'src/compiler/scala/tools/nsc/plugins')
-rw-r--r--src/compiler/scala/tools/nsc/plugins/Plugin.scala93
-rw-r--r--src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala15
-rw-r--r--src/compiler/scala/tools/nsc/plugins/Plugins.scala21
3 files changed, 77 insertions, 52 deletions
diff --git a/src/compiler/scala/tools/nsc/plugins/Plugin.scala b/src/compiler/scala/tools/nsc/plugins/Plugin.scala
index d194c095f8..183752d4a2 100644
--- a/src/compiler/scala/tools/nsc/plugins/Plugin.scala
+++ b/src/compiler/scala/tools/nsc/plugins/Plugin.scala
@@ -12,7 +12,8 @@ import scala.reflect.io.{ Directory, File, Path }
import java.io.InputStream
import java.util.zip.ZipException
-import scala.collection.mutable.ListBuffer
+import scala.collection.mutable
+import mutable.ListBuffer
import scala.util.{ Try, Success, Failure }
/** Information about a plugin loaded from a jar file.
@@ -99,7 +100,7 @@ object Plugin {
private def loadDescriptionFromJar(jarp: Path): Try[PluginDescription] = {
// XXX Return to this once we have more ARM support
def read(is: Option[InputStream]) = is match {
- case None => throw new RuntimeException(s"Missing $PluginXML in $jarp")
+ case None => throw new PluginLoadException(jarp.path, s"Missing $PluginXML in $jarp")
case Some(is) => PluginDescription.fromXML(is)
}
Try(new Jar(jarp.jfile).withEntryStream(PluginXML)(read))
@@ -113,11 +114,14 @@ object Plugin {
/** Use a class loader to load the plugin class.
*/
def load(classname: String, loader: ClassLoader): Try[AnyClass] = {
- Try[AnyClass] {
- loader loadClass classname
- } recoverWith {
- case _: Exception =>
- Failure(new RuntimeException(s"Warning: class not found: ${classname}"))
+ import scala.util.control.NonFatal
+ try {
+ Success[AnyClass](loader loadClass classname)
+ } catch {
+ case NonFatal(e) =>
+ Failure(new PluginLoadException(classname, s"Error: unable to load class: $classname"))
+ case e: NoClassDefFoundError =>
+ Failure(new PluginLoadException(classname, s"Error: class not found: ${e.getMessage} required by $classname"))
}
}
@@ -128,33 +132,54 @@ object Plugin {
* A single classloader is created and used to load all of them.
*/
def loadAllFrom(
- jars: List[Path],
+ paths: List[List[Path]],
dirs: List[Path],
ignoring: List[String]): List[Try[AnyClass]] =
{
- // List[(jar, Success(descriptor))] in dir
- def scan(d: Directory) = for {
- f <- d.files.toList sortBy (_.name)
- if Jar isJarOrZip f
- pd = loadDescriptionFromJar(f)
- if pd.isSuccess
- } yield (f, pd)
- // (dir, Try(descriptor))
- def explode(d: Directory) = d -> loadDescriptionFromFile(d / PluginXML)
- // (j, Try(descriptor))
- def required(j: Path) = j -> loadDescriptionFromJar(j)
-
- type Paired = Tuple2[Path, Try[PluginDescription]]
- val included: List[Paired] = (dirs flatMap (_ ifDirectory scan)).flatten
- val exploded: List[Paired] = jars flatMap (_ ifDirectory explode)
- val explicit: List[Paired] = jars flatMap (_ ifFile required)
- def ignored(p: Paired) = p match {
- case (path, Success(pd)) => ignoring contains pd.name
- case _ => false
+ // List[(jar, Try(descriptor))] in dir
+ def scan(d: Directory) =
+ d.files.toList sortBy (_.name) filter (Jar isJarOrZip _) map (j => (j, loadDescriptionFromJar(j)))
+
+ type PDResults = List[Try[(PluginDescription, ScalaClassLoader)]]
+
+ // scan plugin dirs for jars containing plugins, ignoring dirs with none and other jars
+ val fromDirs: PDResults = dirs filter (_.isDirectory) flatMap { d =>
+ scan(d.toDirectory) collect {
+ case (j, Success(pd)) => Success((pd, loaderFor(Seq(j))))
+ }
+ }
+
+ // scan jar paths for plugins, taking the first plugin you find.
+ // a path element can be either a plugin.jar or an exploded dir.
+ def findDescriptor(ps: List[Path]) = {
+ def loop(qs: List[Path]): Try[PluginDescription] = qs match {
+ case Nil => Failure(new MissingPluginException(ps))
+ case p :: rest =>
+ if (p.isDirectory) loadDescriptionFromFile(p.toDirectory / PluginXML)
+ else if (p.isFile) loadDescriptionFromJar(p.toFile)
+ else loop(rest)
+ }
+ loop(ps)
+ }
+ val fromPaths: PDResults = paths map (p => (p, findDescriptor(p))) map {
+ case (p, Success(pd)) => Success((pd, loaderFor(p)))
+ case (_, Failure(e)) => Failure(e)
}
- val (locs, pds) = ((explicit ::: exploded ::: included) filterNot ignored).unzip
- val loader = loaderFor(locs.distinct)
- (pds filter (_.isSuccess) map (_.get.classname)).distinct map (Plugin load (_, loader))
+
+ val seen = mutable.HashSet[String]()
+ val enabled = (fromPaths ::: fromDirs) map {
+ case Success((pd, loader)) if seen(pd.classname) =>
+ // a nod to SI-7494, take the plugin classes distinctly
+ Failure(new PluginLoadException(pd.name, s"Ignoring duplicate plugin ${pd.name} (${pd.classname})"))
+ case Success((pd, loader)) if ignoring contains pd.name =>
+ Failure(new PluginLoadException(pd.name, s"Disabling plugin ${pd.name}"))
+ case Success((pd, loader)) =>
+ seen += pd.classname
+ Plugin.load(pd.classname, loader)
+ case Failure(e) =>
+ Failure(e)
+ }
+ enabled // distinct and not disabled
}
/** Instantiate a plugin class, given the class and
@@ -164,3 +189,11 @@ object Plugin {
(clazz getConstructor classOf[Global] newInstance global).asInstanceOf[Plugin]
}
}
+
+class PluginLoadException(val path: String, message: String, cause: Exception) extends Exception(message, cause) {
+ def this(path: String, message: String) = this(path, message, null)
+}
+
+class MissingPluginException(path: String) extends PluginLoadException(path, s"No plugin in path $path") {
+ def this(paths: List[Path]) = this(paths mkString File.pathSeparator)
+}
diff --git a/src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala b/src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala
deleted file mode 100644
index c5da24993e..0000000000
--- a/src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-/* NSC -- new Scala compiler
- * Copyright 2007-2013 LAMP/EPFL
- * @author Lex Spoon
- */
-
-package scala.tools.nsc
-package plugins
-
-/** ...
- *
- * @author Lex Spoon
- * @version 1.0, 2007-5-21
- */
-class PluginLoadException(filename: String, cause: Exception)
-extends Exception(cause)
diff --git a/src/compiler/scala/tools/nsc/plugins/Plugins.scala b/src/compiler/scala/tools/nsc/plugins/Plugins.scala
index 4769705404..12f9aeba27 100644
--- a/src/compiler/scala/tools/nsc/plugins/Plugins.scala
+++ b/src/compiler/scala/tools/nsc/plugins/Plugins.scala
@@ -8,6 +8,7 @@ package scala.tools.nsc
package plugins
import scala.reflect.io.{ File, Path }
+import scala.tools.nsc.util.ClassPath
import scala.tools.util.PathResolver.Defaults
/** Support for run-time loading of compiler plugins.
@@ -16,8 +17,7 @@ import scala.tools.util.PathResolver.Defaults
* @version 1.1, 2009/1/2
* Updated 2009/1/2 by Anders Bach Nielsen: Added features to implement SIP 00002
*/
-trait Plugins {
- self: Global =>
+trait Plugins { global: Global =>
/** Load a rough list of the plugins. For speed, it
* does not instantiate a compiler run. Therefore it cannot
@@ -25,13 +25,20 @@ trait Plugins {
* filtered from the final list of plugins.
*/
protected def loadRoughPluginsList(): List[Plugin] = {
- val jars = settings.plugin.value map Path.apply
- def injectDefault(s: String) = if (s.isEmpty) Defaults.scalaPluginPath else s
- val dirs = (settings.pluginsDir.value split File.pathSeparator).toList map injectDefault map Path.apply
- val maybes = Plugin.loadAllFrom(jars, dirs, settings.disable.value)
+ def asPath(p: String) = ClassPath split p
+ val paths = settings.plugin.value filter (_ != "") map (s => asPath(s) map Path.apply)
+ val dirs = {
+ def injectDefault(s: String) = if (s.isEmpty) Defaults.scalaPluginPath else s
+ asPath(settings.pluginsDir.value) map injectDefault map Path.apply
+ }
+ val maybes = Plugin.loadAllFrom(paths, dirs, settings.disable.value)
val (goods, errors) = maybes partition (_.isSuccess)
// Explicit parameterization of recover to suppress -Xlint warning about inferred Any
- errors foreach (_.recover[Any] { case e: Exception => inform(e.getMessage) })
+ errors foreach (_.recover[Any] {
+ // legacy behavior ignores altogether, so at least warn devs
+ case e: MissingPluginException => if (global.isDeveloper) warning(e.getMessage)
+ case e: Exception => inform(e.getMessage)
+ })
val classes = goods map (_.get) // flatten
// Each plugin must only be instantiated once. A common pattern