From d1aed7012af7439181c4696fb33f5f4337b83684 Mon Sep 17 00:00:00 2001 From: Lex Spoon Date: Thu, 7 Jun 2007 09:11:46 +0000 Subject: 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. --- src/compiler/scala/tools/nsc/Global.scala | 29 +++- src/compiler/scala/tools/nsc/InterpreterLoop.scala | 3 + src/compiler/scala/tools/nsc/Main.scala | 44 ++++-- .../scala/tools/nsc/MainGenericRunner.scala | 22 ++- src/compiler/scala/tools/nsc/Settings.scala | 39 ++++- src/compiler/scala/tools/nsc/plugins/Plugin.scala | 138 ++++++++++++++++++ .../scala/tools/nsc/plugins/PluginComponent.scala | 11 ++ .../tools/nsc/plugins/PluginDescription.scala | 68 +++++++++ .../tools/nsc/plugins/PluginLoadException.scala | 5 + src/compiler/scala/tools/nsc/plugins/Plugins.scala | 161 +++++++++++++++++++++ 10 files changed, 497 insertions(+), 23 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/plugins/Plugin.scala create mode 100644 src/compiler/scala/tools/nsc/plugins/PluginComponent.scala create mode 100644 src/compiler/scala/tools/nsc/plugins/PluginDescription.scala create mode 100644 src/compiler/scala/tools/nsc/plugins/PluginLoadException.scala create mode 100644 src/compiler/scala/tools/nsc/plugins/Plugins.scala 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::" + } 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 choices, * (default 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 = { + + {name} + {classname} + + } +} + + + +/** 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 {_*} => () + 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 + } +} -- cgit v1.2.3