Skip to content

Commit

Permalink
Read plugin descriptor via Classloader.getResource
Browse files Browse the repository at this point in the history
This lets customized Global's deliver this file, and offloads
the scanning logic.
  • Loading branch information
retronym committed Oct 17, 2018
1 parent 01a9dbd commit bec5899
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 93 deletions.
40 changes: 14 additions & 26 deletions src/compiler/scala/tools/nsc/plugins/Plugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import scala.tools.nsc.io.Jar
import scala.reflect.internal.util.ScalaClassLoader
import scala.reflect.io.{Directory, File, Path}
import java.io.InputStream
import java.net.URL

import scala.collection.JavaConverters._
import scala.collection.mutable
import scala.tools.nsc.classpath.FileBasedCache
import scala.util.{Failure, Success, Try}
Expand Down Expand Up @@ -157,38 +159,24 @@ object Plugin {
ignoring: List[String],
findPluginClassloader: (Seq[Path] => ClassLoader)): List[Try[AnyClass]] =
{
// 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, findPluginClassloader(Seq(j))))
val fromLoaders = paths.map {path =>
val loader = findPluginClassloader(path)
loader.getResource(PluginXML) match {
case null => Failure(new MissingPluginException(path))
case url =>
val inputStream = url.openStream
try {
Try((PluginDescription.fromXML(inputStream), loader))
} finally {
inputStream.close()
}
}
}

// 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) orElse loop(rest)
else if (p.isFile) loadDescriptionFromJar(p.toFile) orElse loop(rest)
else loop(rest)
}
loop(ps)
}
val fromPaths: PDResults = paths map (p => (p, findDescriptor(p))) map {
case (p, Success(pd)) => Success((pd, findPluginClassloader(p)))
case (_, Failure(e)) => Failure(e)
}

val seen = mutable.HashSet[String]()
val enabled = (fromPaths ::: fromDirs) map {
val enabled = fromLoaders map {
case Success((pd, loader)) if seen(pd.classname) =>
// a nod to scala/bug#7494, take the plugin classes distinctly
Failure(new PluginLoadException(pd.name, s"Ignoring duplicate plugin ${pd.name} (${pd.classname})"))
Expand Down
50 changes: 50 additions & 0 deletions src/compiler/scala/tools/nsc/plugins/Plugins.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
package scala.tools.nsc
package plugins

import java.net.URL

import scala.reflect.internal.util.ScalaClassLoader
import scala.reflect.io.Path
import scala.tools.nsc
import scala.tools.nsc.typechecker.Macros
import scala.tools.nsc.util.ClassPath
import scala.tools.util.PathResolver.Defaults

Expand Down Expand Up @@ -127,4 +132,49 @@ trait Plugins { global: Global =>
(for (plug <- roughPluginsList ; help <- plug.optionsHelp) yield {
"\nOptions for plugin '%s':\n%s\n".format(plug.name, help)
}).mkString

/** Obtains a `ClassLoader` instance used for macro expansion.
*
* By default a new `ScalaClassLoader` is created using the classpath
* from global and the classloader of self as parent.
*
* Mirrors with runtime definitions (e.g. Repl) need to adjust this method.
*/
protected[scala] def findMacroClassLoader(): ClassLoader = {
val classpath: Seq[URL] = if (settings.YmacroClasspath.isSetByUser) {
for {
file <- scala.tools.nsc.util.ClassPath.expandPath(settings.YmacroClasspath.value, true)
af <- Option(nsc.io.AbstractFile getDirectory file)
} yield af.file.toURI.toURL
} else global.classPath.asURLs
def newLoader = () => {
analyzer.macroLogVerbose("macro classloader: initializing from -cp: %s".format(classpath))
ScalaClassLoader.fromURLs(classpath, getClass.getClassLoader)
}

val disableCache = settings.YcacheMacroClassLoader.value == settings.CachePolicy.None.name
if (disableCache) newLoader()
else {
import scala.tools.nsc.io.Jar
import scala.reflect.io.{AbstractFile, Path}

val urlsAndFiles = classpath.map(u => u -> AbstractFile.getURL(u))
val hasNullURL = urlsAndFiles.filter(_._2 eq null)
if (hasNullURL.nonEmpty) {
// TODO if the only null is jrt:// we can still cache
// TODO filter out classpath elements pointing to non-existing files before we get here, that's another source of null
analyzer.macroLogVerbose(s"macro classloader: caching is disabled because `AbstractFile.getURL` returned `null` for ${hasNullURL.map(_._1).mkString(", ")}.")
newLoader()
} else {
val locations = urlsAndFiles.map(t => Path(t._2.file))
val nonJarZips = locations.filterNot(Jar.isJarOrZip(_))
if (nonJarZips.nonEmpty) {
analyzer.macroLogVerbose(s"macro classloader: caching is disabled because the following paths are not supported: ${nonJarZips.mkString(",")}.")
newLoader()
} else {
Macros.macroClassLoadersCache.getOrCreate(locations.map(_.jfile.toPath()), newLoader)
}
}
}
}
}
45 changes: 0 additions & 45 deletions src/compiler/scala/tools/nsc/typechecker/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,51 +67,6 @@ trait Macros extends MacroRuntimes with Traces with Helpers {

def globalSettings = global.settings

/** Obtains a `ClassLoader` instance used for macro expansion.
*
* By default a new `ScalaClassLoader` is created using the classpath
* from global and the classloader of self as parent.
*
* Mirrors with runtime definitions (e.g. Repl) need to adjust this method.
*/
protected def findMacroClassLoader(): ClassLoader = {
val classpath: Seq[URL] = if (settings.YmacroClasspath.isSetByUser) {
for {
file <- scala.tools.nsc.util.ClassPath.expandPath(settings.YmacroClasspath.value, true)
af <- Option(AbstractFile getDirectory file)
} yield af.file.toURI.toURL
} else global.classPath.asURLs
def newLoader = () => {
macroLogVerbose("macro classloader: initializing from -cp: %s".format(classpath))
ScalaClassLoader.fromURLs(classpath, self.getClass.getClassLoader)
}

val disableCache = settings.YcacheMacroClassLoader.value == settings.CachePolicy.None.name
if (disableCache) newLoader()
else {
import scala.tools.nsc.io.Jar
import scala.reflect.io.{AbstractFile, Path}

val urlsAndFiles = classpath.map(u => u -> AbstractFile.getURL(u))
val hasNullURL = urlsAndFiles.filter(_._2 eq null)
if (hasNullURL.nonEmpty) {
// TODO if the only null is jrt:// we can still cache
// TODO filter out classpath elements pointing to non-existing files before we get here, that's another source of null
macroLogVerbose(s"macro classloader: caching is disabled because `AbstractFile.getURL` returned `null` for ${hasNullURL.map(_._1).mkString(", ")}.")
newLoader()
} else {
val locations = urlsAndFiles.map(t => Path(t._2.file))
val nonJarZips = locations.filterNot(Jar.isJarOrZip(_))
if (nonJarZips.nonEmpty) {
macroLogVerbose(s"macro classloader: caching is disabled because the following paths are not supported: ${nonJarZips.mkString(",")}.")
newLoader()
} else {
Macros.macroClassLoadersCache.getOrCreate(locations.map(_.jfile.toPath()), newLoader)
}
}
}
}

/** `MacroImplBinding` and its companion module are responsible for
* serialization/deserialization of macro def -> impl bindings.
*
Expand Down
20 changes: 8 additions & 12 deletions src/compiler/scala/tools/reflect/ReflectGlobal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,14 @@ import scala.tools.nsc.typechecker.Analyzer
class ReflectGlobal(currentSettings: Settings, reporter: Reporter, override val rootClassLoader: ClassLoader)
extends Global(currentSettings, reporter) with scala.tools.reflect.ReflectSetup with scala.reflect.runtime.SymbolTable {

override lazy val analyzer = new {
val global: ReflectGlobal.this.type = ReflectGlobal.this
} with Analyzer {
/** Obtains the classLoader used for runtime macro expansion.
*
* Macro expansion can use everything available in [[global.classPath]] or [[rootClassLoader]].
* The [[rootClassLoader]] is used to obtain runtime defined macros.
*/
override protected def findMacroClassLoader(): ClassLoader = {
val classpath = global.classPath.asURLs
ScalaClassLoader.fromURLs(classpath, rootClassLoader)
}
/** Obtains the classLoader used for runtime macro expansion.
*
* Macro expansion can use everything available in `global.classPath` or `rootClassLoader`.
* The `rootClassLoader` is used to obtain runtime defined macros.
*/
override protected[scala] def findMacroClassLoader(): ClassLoader = {
val classpath = classPath.asURLs
ScalaClassLoader.fromURLs(classpath, rootClassLoader)
}

override def transformedType(sym: Symbol) =
Expand Down
15 changes: 5 additions & 10 deletions src/repl/scala/tools/nsc/interpreter/ReplGlobal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,11 @@ trait ReplGlobal extends Global {
super.abort(msg)
}

override lazy val analyzer = new {
val global: ReplGlobal.this.type = ReplGlobal.this
} with Analyzer {

override protected def findMacroClassLoader(): ClassLoader = {
val loader = super.findMacroClassLoader
macroLogVerbose("macro classloader: initializing from a REPL classloader: %s".format(global.classPath.asURLs))
val virtualDirectory = globalSettings.outputDirs.getSingleOutput.get
new util.AbstractFileClassLoader(virtualDirectory, loader) {}
}
override protected[scala] def findMacroClassLoader(): ClassLoader = {
val loader = super.findMacroClassLoader
analyzer.macroLogVerbose("macro classloader: initializing from a REPL classloader: %s".format(classPath.asURLs))
val virtualDirectory = analyzer.globalSettings.outputDirs.getSingleOutput.get
new util.AbstractFileClassLoader(virtualDirectory, loader) {}
}

override def optimizerClassPath(base: ClassPath): ClassPath = {
Expand Down
71 changes: 71 additions & 0 deletions test/junit/scala/tools/nsc/GlobalCustomizeClassloaderTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package scala.tools.nsc

import org.junit.{Assert, Test}
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

import scala.reflect.internal.util.{AbstractFileClassLoader, NoSourceFile}
import scala.reflect.io.{Path, VirtualDirectory}
import scala.tools.nsc.plugins.{Plugin, PluginComponent}

@RunWith(classOf[JUnit4])
class GlobalCustomizeClassloaderTest {
// Demonstrate extension points to customise creation of the classloaders used to load compiler
// plugins and macro implementations.
//
// A use case could be for a build tool to take control of caching of these classloaders in a way
// that properly closes them before one of the elements needs to be overwritten.
@Test def test(): Unit = {
val g = new Global(new Settings) {
override protected[scala] def findMacroClassLoader(): ClassLoader = getClass.getClassLoader
override protected def findPluginClassLoader(classpath: Seq[Path]): ClassLoader = {
val d = new VirtualDirectory("", None)
val xml = d.fileNamed("scalac-plugin.xml")
val out = xml.bufferedOutput
out.write(
s"""<plugin>
|<name>sample-plugin</name>
|<classname>${classOf[SamplePlugin].getName}</classname>
|</plugin>
|""".stripMargin.getBytes())
out.close()
new AbstractFileClassLoader(d, getClass.getClassLoader)
}
}
g.settings.usejavacp.value = true
g.settings.plugin.value = List("sample")
new g.Run
assert(g.settings.log.value == List("typer"))

val unit = new g.CompilationUnit(NoSourceFile)
val context = g.analyzer.rootContext(unit)
val typer = g.analyzer.newTyper(context)
import g._
SampleMacro.data = "in this classloader"
val typed = typer.typed(q"scala.tools.nsc.SampleMacro.m")
assert(!reporter.hasErrors)
typed match {
case Typed(Literal(Constant(s: String)), _) => Assert.assertEquals(SampleMacro.data, s)
case _ => Assert.fail()
}
}
}

object SampleMacro {
var data: String = _
import language.experimental.macros
import scala.reflect.macros.blackbox.Context
def m: String = macro impl
def impl(c: Context): c.Tree = c.universe.Literal(c.universe.Constant(data))
}

class SamplePlugin(val global: Global) extends Plugin {
override val name: String = "sample"
override val description: String = "sample"
override val components: List[PluginComponent] = Nil
override def init(options: List[String], error: String => Unit): Boolean = {
val result = super.init(options, error)
global.settings.log.value = List("typer")
result
}
}

0 comments on commit bec5899

Please sign in to comment.