From 1d30ea86690db1b3b074190094d1f62d12e4efe1 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 20 Nov 2013 11:44:13 -0800 Subject: 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 ``` --- src/compiler/scala/tools/nsc/plugins/Plugin.scala | 93 +++++++++++++++------- .../tools/nsc/plugins/PluginLoadException.scala | 15 ---- src/compiler/scala/tools/nsc/plugins/Plugins.scala | 21 +++-- 3 files changed, 77 insertions(+), 52 deletions(-) delete mode 100644 src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala (limited to 'src/compiler/scala') 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 -- cgit v1.2.3