summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLex Spoon <lex@lexspoon.org>2007-06-07 09:11:46 +0000
committerLex Spoon <lex@lexspoon.org>2007-06-07 09:11:46 +0000
commitd1aed7012af7439181c4696fb33f5f4337b83684 (patch)
tree5f57aa3c6860ada20b5e4ef00debe18acf5884c1
parent6739cacb9dbc1cbc3c459b87b8ba97923d687fbe (diff)
downloadscala-d1aed7012af7439181c4696fb33f5f4337b83684.tar.gz
scala-d1aed7012af7439181c4696fb33f5f4337b83684.tar.bz2
scala-d1aed7012af7439181c4696fb33f5f4337b83684.zip
Final merge from the plugins branch. The compiler
can now have plugins loaded at runtime via jars, and thus compiler components can be distributed indepedently of the central compiler.
-rw-r--r--src/compiler/scala/tools/nsc/Global.scala29
-rw-r--r--src/compiler/scala/tools/nsc/InterpreterLoop.scala3
-rw-r--r--src/compiler/scala/tools/nsc/Main.scala44
-rw-r--r--src/compiler/scala/tools/nsc/MainGenericRunner.scala22
-rw-r--r--src/compiler/scala/tools/nsc/Settings.scala39
-rw-r--r--src/compiler/scala/tools/nsc/plugins/Plugin.scala138
-rw-r--r--src/compiler/scala/tools/nsc/plugins/PluginComponent.scala11
-rw-r--r--src/compiler/scala/tools/nsc/plugins/PluginDescription.scala68
-rw-r--r--src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala5
-rw-r--r--src/compiler/scala/tools/nsc/plugins/Plugins.scala161
10 files changed, 497 insertions, 23 deletions
diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala
index abb588970c..afedea0b1a 100644
--- a/src/compiler/scala/tools/nsc/Global.scala
+++ b/src/compiler/scala/tools/nsc/Global.scala
@@ -19,6 +19,7 @@ import scala.collection.mutable.{HashSet, HashMap, ListBuffer}
import symtab._
import symtab.classfile.{PickleBuffer, Pickler, ICodeReader}
import util.Statistics
+import plugins.Plugins
import ast._
import ast.parser._
import typechecker._
@@ -34,6 +35,7 @@ import backend.icode.analysis._
class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable
with Trees
with CompilationUnits
+ with Plugins
{
// alternate constructors ------------------------------------------
def this(reporter: Reporter) =
@@ -367,7 +369,10 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable
object typer extends analyzer.Typer(
analyzer.NoContext.make(EmptyTree, Global.this.definitions.RootClass, newScope))
- def phaseDescriptors: List[SubComponent] = List(
+ /** The built-in components. The full list of components, including
+ * plugins, is computed in the Plugins trait.
+ */
+ protected def builtInPhaseDescriptors: List[SubComponent] = List(
analyzer.namerFactory: SubComponent, // note: types are there because otherwise
analyzer.typerFactory: SubComponent, // consistency check after refchecks would fail.
superAccessors, // add super accessors
@@ -393,6 +398,24 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable
if (forMSIL) genMSIL else genJVM, // generate .class files
sampleTransform)
+
+ private var phasesCache: Option[List[SubComponent]] = None
+
+ def phaseDescriptors = {
+ if (phasesCache.isEmpty)
+ phasesCache = Some(computePhaseDescriptors)
+ phasesCache.get
+ }
+
+ /** A description of the phases that will run */
+ def phaseDescriptions: String = {
+ val messages =
+ for (phase <- phaseDescriptors)
+ yield phase.phaseName //todo: + " - " + phase.description
+ messages.mkString("\n")
+ }
+
+
protected def insertBefore(c: SubComponent, cs: List[SubComponent], before: SubComponent): List[SubComponent] = cs match {
case List() => List(c)
case c1 :: cs1 => if (c1 == before) c :: cs else c1 :: insertBefore(c, cs1, before)
@@ -649,4 +672,8 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable
def onlyPresentation = settings.doc.value
// used to disable caching in lampion IDE.
def inIDE = false
+
+
+ // force some initialization
+ new Run
}
diff --git a/src/compiler/scala/tools/nsc/InterpreterLoop.scala b/src/compiler/scala/tools/nsc/InterpreterLoop.scala
index f37044be8e..b58aa4cf64 100644
--- a/src/compiler/scala/tools/nsc/InterpreterLoop.scala
+++ b/src/compiler/scala/tools/nsc/InterpreterLoop.scala
@@ -263,6 +263,9 @@ class InterpreterLoop(in0: BufferedReader, out: PrintWriter) {
createInterpreter()
try {
+ if (interpreter.reporter.hasErrors) {
+ return // it is broken on startup; go ahead and exit
+ }
printWelcome
repl
} finally {
diff --git a/src/compiler/scala/tools/nsc/Main.scala b/src/compiler/scala/tools/nsc/Main.scala
index 66d4eb3c1a..fd2fe4cc82 100644
--- a/src/compiler/scala/tools/nsc/Main.scala
+++ b/src/compiler/scala/tools/nsc/Main.scala
@@ -44,29 +44,42 @@ object Main extends AnyRef with EvalLoop {
val command = new CompilerCommand(List.fromArray(args), settings, error, false)
if (command.settings.version.value)
reporter.info(null, versionMsg, true)
- else if (command.settings.help.value || command.settings.Xhelp.value) {
- if (command.settings.help.value) reporter.info(null, command.usageMsg, true)
- if (command.settings.Xhelp.value) reporter.info(null, command.xusageMsg, true)
- } else {
+ else {
try {
object compiler extends Global(command.settings, reporter)
if (reporter.hasErrors) {
reporter.flush()
return
}
- if (command.settings.resident.value)
- resident(compiler)
- else if (command.files.isEmpty)
- reporter.info(null, command.usageMsg, true)
+
+ if (command.settings.help.value || command.settings.Xhelp.value) {
+ if (command.settings.help.value) {
+ reporter.info(null, command.usageMsg, true)
+ reporter.info(null, compiler.pluginOptionsHelp, true)
+ }
+ if (command.settings.Xhelp.value)
+ reporter.info(null, command.xusageMsg, true)
+ } else if (command.settings.showPlugins.value)
+ reporter.info(null, compiler.pluginDescriptions, true)
+ else if (command.settings.showPhases.value)
+ reporter.info(null, compiler.phaseDescriptions, true)
else {
- val run = new compiler.Run
- run compile command.files
- if (command.settings.doc.value) {
- object generator extends DocGenerator {
- val global: compiler.type = compiler
- def settings = command.settings
+ if (command.settings.resident.value)
+ resident(compiler)
+ else if (command.files.isEmpty) {
+ reporter.info(null, command.usageMsg, true)
+ reporter.info(null, compiler.pluginOptionsHelp, true)
+ } else {
+ val run = new compiler.Run
+ run compile command.files
+ if (command.settings.doc.value) {
+ object generator extends DocGenerator {
+ val global: compiler.type = compiler
+ def settings = command.settings
+ }
+ generator.process(run.units)
}
- generator.process(run.units)
+ reporter.printSummary()
}
}
} catch {
@@ -75,7 +88,6 @@ object Main extends AnyRef with EvalLoop {
ex.printStackTrace();
reporter.error(null, "fatal error: " + msg)
}
- reporter.printSummary()
}
}
diff --git a/src/compiler/scala/tools/nsc/MainGenericRunner.scala b/src/compiler/scala/tools/nsc/MainGenericRunner.scala
index c5b903066f..ac895cea6f 100644
--- a/src/compiler/scala/tools/nsc/MainGenericRunner.scala
+++ b/src/compiler/scala/tools/nsc/MainGenericRunner.scala
@@ -64,11 +64,6 @@ object MainGenericRunner {
settings.defines.applyToCurrentJVM
- if (settings.help.value || !command.ok) {
- Console.println(command.usageMessage)
- return
- }
-
if (settings.version.value) {
Console.println(
"Scala code runner " +
@@ -77,6 +72,23 @@ object MainGenericRunner {
return
}
+ if (settings.help.value || !command.ok) {
+ println(command.usageMessage)
+ return
+ }
+
+ def sampleCompiler = new Global(settings)
+
+ if (settings.showPhases.value) {
+ println(sampleCompiler.phaseDescriptions)
+ return
+ }
+
+ if (settings.showPlugins.value) {
+ println(sampleCompiler.pluginDescriptions)
+ return
+ }
+
def paths0(str: String): List[String] =
str.split(File.pathSeparator).toList
diff --git a/src/compiler/scala/tools/nsc/Settings.scala b/src/compiler/scala/tools/nsc/Settings.scala
index 88752e80ca..c60756916b 100644
--- a/src/compiler/scala/tools/nsc/Settings.scala
+++ b/src/compiler/scala/tools/nsc/Settings.scala
@@ -83,6 +83,12 @@ class Settings(error: String => Unit) {
val sourcepath = StringSetting ("-sourcepath", "path", "Specify where to find input source files", "")
val bootclasspath = StringSetting ("-bootclasspath", "path", "Override location of bootstrap class files", bootclasspathDefault)
val extdirs = StringSetting ("-extdirs", "dirs", "Override location of installed extensions", extdirsDefault)
+ val plugin = MultiStringSetting("-plugin", "file", "Load a plugin from a file")
+ val disable = MultiStringSetting("-disable", "plugin", "Disable a plugin")
+ val require = MultiStringSetting("-require", "plugin", "Abort unless a plugin is available")
+ val pluginOptions = new MultiStringSetting("-P", "plugin:opt", "Pass an option to a plugin") {
+ override def helpSyntax = "-P:<plugin>:<opt>"
+ }
val outdir = StringSetting ("-d", "directory", "Specify where to place generated class files", ".")
val encoding = new StringSetting ("-encoding", "encoding", "Specify character encoding used by source files", encodingDefault) { override def hiddenToIDE = false }
val target = ChoiceSetting ("-target", "Specify which backend to use", List("jvm-1.5", "jvm-1.4", "msil", "cldc"), "jvm-1.4")
@@ -114,7 +120,8 @@ class Settings(error: String => Unit) {
val version = new BooleanSetting("-version", "Print product version and exit") { override def hiddenToIDE = true }
val help = new BooleanSetting("-help", "Print a synopsis of standard options") { override def hiddenToIDE = true }
val nouescape = new BooleanSetting("-nouescape", "disables handling of \\u unicode escapes")
-// val showPhases = BooleanSetting("-showphases", "Print a synopsis of compiler phases")
+ val showPhases = BooleanSetting("-showphases", "Print a synopsis of compiler phases")
+ val showPlugins = BooleanSetting("-showplugins", "Print a synopsis of loaded plugins")
val inline = BooleanSetting("-Xinline", "Perform inlining when possible")
@@ -289,6 +296,36 @@ class Settings(error: String => Unit) {
if (value == default) Nil else List(name, value)
}
+ /** A setting that accumulates all strings supplied to it */
+ case class MultiStringSetting(name: String, arg: String, descr: String)
+ extends Setting(descr) {
+ override def hiddenToIDE = true
+ protected var v: List[String] = Nil
+ def value = v
+ def appendToValue(str: String) { v = v ::: List(str) }
+
+ protected val nameColon = name + ":"
+ def tryToSet(args: List[String]): List[String] = args match {
+ case arg :: rest if (arg.startsWith(nameColon)) =>
+ val toadd = arg.substring(nameColon.length())
+ if (toadd.length == 0) {
+ error("empty argument to " + nameColon)
+ args
+ } else {
+ appendToValue(toadd)
+ rest
+ }
+ case _ => args
+ }
+
+ override def helpSyntax = name + ":<" + arg + ">"
+
+ def unparse: List[String] =
+ for (opt <- value)
+ yield nameColon+opt
+ }
+
+
/** A setting represented by a string in a given set of <code>choices</code>,
* (<code>default</code> unless set).
*/
diff --git a/src/compiler/scala/tools/nsc/plugins/Plugin.scala b/src/compiler/scala/tools/nsc/plugins/Plugin.scala
new file mode 100644
index 0000000000..73ebb66134
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/plugins/Plugin.scala
@@ -0,0 +1,138 @@
+package scala.tools.nsc.plugins
+import java.io.File
+import java.util.jar.JarFile
+import java.util.zip.ZipException
+import scala.xml.XML
+import java.net.URLClassLoader
+import scala.collection.mutable
+import mutable.ListBuffer
+
+/** Information about a plugin loaded from a jar file.
+ *
+ * The concrete subclass must have a one-argument constructor
+ * that accepts an instance of Global.
+ *
+ * (val global: Global)
+ */
+abstract class Plugin {
+ /** The name of this plugin */
+ val name: String
+
+ /** The components that this phase defines */
+ val components: List[PluginComponent]
+
+ /** A one-line description of the plugin */
+ val description: String
+
+ /** The compiler that this plugin uses. This is normally equated
+ * to a constructor parameter in the concrete subclass. */
+ val global: Global
+
+ /** Handle any plugin-specific options. The -P:plugname: part
+ * will not be present. */
+ def processOptions(options: List[String], error: String=>Unit) {
+ if (!options.isEmpty)
+ error("Error: " + name + " has no options")
+ }
+
+ /** A description of this plugin's options, suitable as a response
+ * to the -help command-line option. Conventionally, the
+ * options should be listed with the -P:plugname: part included.
+ */
+ val optionsHelp: Option[String] = None
+}
+
+object Plugin {
+ /** Create a class loader with the specified file plus
+ * the loader that loaded the Scala compiler.
+ */
+ private def loaderFor(jarfiles: Seq[File]): ClassLoader = {
+ val compilerLoader = classOf[Plugin].getClassLoader
+ val jarurls = jarfiles.map(.toURL).toArray
+ new URLClassLoader(jarurls, compilerLoader)
+ }
+
+
+
+ /** Try to load a plugin description from the specified
+ * file, returning None if it does not work. */
+ private def loadDescription(jarfile: File): Option[PluginDescription] = {
+ if (!jarfile.exists) return None
+
+ try {
+ val jar = new JarFile(jarfile)
+ try {
+ val ent = jar.getEntry("scalac-plugin.xml")
+ if(ent == null) return None
+
+ val inBytes = jar.getInputStream(ent)
+ val packXML = XML.load(inBytes)
+ inBytes.close()
+
+ PluginDescription.fromXML(packXML)
+ } finally {
+ jar.close()
+ }
+ } catch {
+ case _:ZipException => None
+ }
+ }
+
+
+
+ /** Loads a plugin class from the named jar file. Returns None
+ * if the jar file has no plugin in it or if the plugin
+ * is badly formed. */
+ def loadFrom(jarfile: File,
+ loader: ClassLoader): Option[Class] =
+ {
+ val pluginInfo = loadDescription(jarfile).get
+
+ try {
+ Some(loader.loadClass(pluginInfo.classname))
+ } catch {
+ case _:ClassNotFoundException =>
+ println("Warning: class not found for plugin in " + jarfile +
+ " (" + pluginInfo.classname + ")")
+ None
+ }
+ }
+
+
+
+ /** Load all plugins found in the argument list, bot hin
+ * the jar files explicitly listed, and in the jar files in
+ * the directories specified. Skips all plugins in `ignoring'.
+ * A single classloader is created and used to load all of them. */
+ def loadAllFrom(jars: List[File],
+ dirs: List[File],
+ ignoring: List[String]): List[Class] =
+ {
+ val alljars = new ListBuffer[File]
+
+ alljars ++= jars
+
+ for {
+ dir <- dirs
+ entries = dir.listFiles.toList
+ sorted = entries.sort((f1,f2)=>f1.getName <= f2.getName)
+ ent <- sorted
+ if ent.toString.toLowerCase.endsWith(".jar")
+ pdesc <- loadDescription(ent)
+ if !(ignoring contains pdesc.name)
+ } alljars += ent
+
+ val loader = loaderFor(alljars.toList)
+ alljars.toList.map(f => loadFrom(f,loader)).flatMap(x => x)
+ }
+
+
+
+ /** Instantiate a plugin class, given the class and
+ * the compiler it is to be used in.
+ */
+ def instantiate(clazz: Class, global: Global): Plugin = {
+ val constructor = clazz.getConstructor(Array(classOf[Global]))
+ constructor.newInstance(Array(global)).asInstanceOf[Plugin]
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/plugins/PluginComponent.scala b/src/compiler/scala/tools/nsc/plugins/PluginComponent.scala
new file mode 100644
index 0000000000..8a4cee80f7
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/plugins/PluginComponent.scala
@@ -0,0 +1,11 @@
+package scala.tools.nsc.plugins
+
+/** A component that is part of a Plugin.
+ *
+ * @version 1.0
+ * @author Lex Spoon, 2007/5/29
+ */
+abstract class PluginComponent extends SubComponent {
+ /** the phase this plugin wants to run after */
+ val runsAfter: String
+}
diff --git a/src/compiler/scala/tools/nsc/plugins/PluginDescription.scala b/src/compiler/scala/tools/nsc/plugins/PluginDescription.scala
new file mode 100644
index 0000000000..f7abda65aa
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/plugins/PluginDescription.scala
@@ -0,0 +1,68 @@
+package scala.tools.nsc.plugins
+import scala.xml.{Node,NodeSeq}
+import java.io.File
+
+/** A description of a compiler plugin, suitable for serialization
+ * to XML for inclusion in the plugin's .jar file.
+ *
+ * @author Lex Spoon
+ * @version 1.0, 2007-5-21
+ */
+abstract class PluginDescription {
+ /** A short name of the compiler, used to identify it in
+ * various contexts. The phase defined by the plugin
+ * should have the same name. */
+ val name: String
+
+
+ /** The name of the main class for the plugin */
+ val classname: String
+
+ /** An XML representation of this description. It can be
+ * read back using PluginDescription.fromXML . It should
+ * be stored inside the jor. */
+ def toXML: Node = {
+ <plugin>
+ <name>{name}</name>
+ <classname>{classname}</classname>
+ </plugin>
+ }
+}
+
+
+
+/** Utilities for the PluginDescription class.
+ *
+ * @author Lex Spoon
+ * @version 1.0, 2007-5-21
+ */
+object PluginDescription {
+ def fromXML(xml: Node): Option[PluginDescription] = {
+ // check the top-level tag
+ xml match {
+ case <plugin>{_*}</plugin> => ()
+ case _ => return None
+ }
+
+ /** Extract one field */
+ def getField(field: String): Option[String] = {
+ val text = (xml \\ field).text.trim
+ if (text == "") None else Some(text)
+ }
+
+ // extract the required fields
+ val name1 = getField("name") match {
+ case None => return None
+ case Some(str) => str
+ }
+ val classname1 = getField("classname") match {
+ case None => return None
+ case Some(str) => str
+ }
+
+ Some(new PluginDescription {
+ val name = name1
+ val classname = classname1
+ })
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala b/src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala
new file mode 100644
index 0000000000..d69a5e098c
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala
@@ -0,0 +1,5 @@
+package scala.tools.nsc.plugins
+
+
+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
new file mode 100644
index 0000000000..b8d618a3fc
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/plugins/Plugins.scala
@@ -0,0 +1,161 @@
+package scala.tools.nsc.plugins
+import java.io.File
+
+/** Support for run-time loading of compiler plugins */
+trait Plugins { self: Global =>
+
+ /** Load all available plugin. Skips plugins that
+ * either have the same name as another one, or which
+ * define a phase name that another one does.
+ */
+ protected def loadPlugins: List[Plugin] = {
+ // load all the plugins
+ val jars = settings.plugin.value.map(new File(_))
+ val dirs =
+ for (name <- settings.extdirs.value.split(File.pathSeparator).toList)
+ yield new File(name)
+
+ val initPlugins =
+ for (plugClass <- Plugin.loadAllFrom(jars, dirs, settings.disable.value))
+ yield Plugin.instantiate(plugClass, this)
+
+ // remove any with conflicting names or subcomponent names
+ def pick(
+ plugins: List[Plugin],
+ plugNames: Set[String],
+ phaseNames: Set[String]): List[Plugin] =
+ {
+ plugins match {
+ case Nil => Nil
+ case plug :: rest =>
+ val plugPhaseNames = Set.empty ++ plug.components.map(.phaseName)
+ def withoutPlug = pick(rest, plugNames, plugPhaseNames)
+ def withPlug =
+ (plug ::
+ pick(rest,
+ plugNames+plug.name,
+ phaseNames++plugPhaseNames))
+
+ if (plugNames.contains(plug.name)) {
+ if (settings.verbose.value)
+ inform("[skipping a repeated plugin: " + plug.name + "]")
+ withoutPlug
+ } else if (settings.disable.value contains(plug.name)) {
+ if (settings.verbose.value)
+ inform("[disabling plugin: " + plug.name + "]")
+ withoutPlug
+ } else {
+ val commonPhases = phaseNames.intersect(plugPhaseNames)
+ if (!commonPhases.isEmpty) {
+ if (settings.verbose.value)
+ inform("[skipping plugin " + plug.name +
+ "because it repeats phase names: " +
+ commonPhases.mkString(", ") + "]")
+ withoutPlug
+ } else {
+ if (settings.verbose.value)
+ inform("[loaded plugin " + plug.name + "]")
+ withPlug
+ }
+ }
+ }
+ }
+
+ val plugs =
+ pick(initPlugins,
+ Set.empty,
+ Set.empty ++ builtInPhaseDescriptors.map(.phaseName))
+
+ for (req <- settings.require.value; if !plugs.exists(p => p.name==req))
+ error("Missing required plugin: " + req)
+
+
+ for (plug <- plugs) {
+ val nameColon = plug.name + ":"
+ val opts = for {
+ raw <- settings.pluginOptions.value
+ if raw.startsWith(nameColon)
+ } yield raw.substring(nameColon.length)
+
+ if (!opts.isEmpty)
+ plug.processOptions(opts, error)
+ }
+
+ for {
+ opt <- settings.pluginOptions.value
+ if !plugs.exists(p => opt.startsWith(p.name + ":"))
+ } error("bad option: -P:" + opt)
+
+ plugs
+ }
+
+
+ private var pluginsCache: Option[List[Plugin]] = None
+
+ def plugins: List[Plugin] = {
+ if (pluginsCache.isEmpty)
+ pluginsCache = Some(loadPlugins)
+ pluginsCache.get
+ }
+
+
+ /** A description of all the plugins that are loaded */
+ def pluginDescriptions: String = {
+ val messages =
+ for (plugin <- plugins)
+ yield plugin.name + " - " + plugin.description
+ messages.mkString("\n")
+ }
+
+
+ /** Compute a full list of phase descriptors, including
+ * both built-in phases and those coming from plugins. */
+ protected def computePhaseDescriptors: List[SubComponent] = {
+ def insert(descs: List[SubComponent], component: PluginComponent)
+ :List[SubComponent] =
+ {
+ descs match {
+ case Nil => assert(false); Nil
+ case hd::rest if hd.phaseName == component.runsAfter =>
+ hd :: component :: rest
+ case hd :: rest =>
+ hd :: (insert(rest, component))
+ }
+ }
+
+ var descriptors = builtInPhaseDescriptors
+ var plugsLeft = plugins.flatMap(.components)
+
+ // Insert all the plugins, one by one. Note that
+ // plugins are allowed to depend on each other, thus
+ // complicating the algorithm.
+
+ while (!plugsLeft.isEmpty) {
+ val nextPlug = plugsLeft.find(plug =>
+ descriptors.exists(d => d.phaseName == plug.runsAfter))
+ nextPlug match {
+ case None =>
+ error("Failed to load plugin phases")
+ for (plug <- plugsLeft)
+ error (plug.phaseName + " depends on " + plug.runsAfter)
+ return descriptors
+ case Some(nextPlug) =>
+ descriptors = insert(descriptors, nextPlug)
+ plugsLeft = plugsLeft.filter(p => !(p eq nextPlug))
+ }
+ }
+
+ descriptors
+ }
+
+ /** Summary of the options for all loaded plugins */
+ def pluginOptionsHelp: String = {
+ val buf = new StringBuffer
+ for (plug <- plugins; help <- plug.optionsHelp) {
+ buf append ("Options for plugin " + plug.name + ":\n")
+ buf append help
+ buf append "\n"
+ }
+ buf.toString
+ }
+}