Skip to content

Commit e2a829f

Browse files
committed
Read plugin descriptor via Classloader.getResources
This lets customized Global's deliver this file.
1 parent 01a9dbd commit e2a829f

File tree

6 files changed

+150
-93
lines changed

6 files changed

+150
-93
lines changed

src/compiler/scala/tools/nsc/plugins/Plugin.scala

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import scala.tools.nsc.io.Jar
1717
import scala.reflect.internal.util.ScalaClassLoader
1818
import scala.reflect.io.{Directory, File, Path}
1919
import java.io.InputStream
20+
import java.net.URL
2021

22+
import scala.collection.JavaConverters._
2123
import scala.collection.mutable
2224
import scala.tools.nsc.classpath.FileBasedCache
2325
import scala.util.{Failure, Success, Try}
@@ -157,38 +159,26 @@ object Plugin {
157159
ignoring: List[String],
158160
findPluginClassloader: (Seq[Path] => ClassLoader)): List[Try[AnyClass]] =
159161
{
160-
// List[(jar, Try(descriptor))] in dir
161-
def scan(d: Directory) =
162-
d.files.toList sortBy (_.name) filter (Jar isJarOrZip _) map (j => (j, loadDescriptionFromJar(j)))
163-
164162
type PDResults = List[Try[(PluginDescription, ScalaClassLoader)]]
165163

166-
// scan plugin dirs for jars containing plugins, ignoring dirs with none and other jars
167-
val fromDirs: PDResults = dirs filter (_.isDirectory) flatMap { d =>
168-
scan(d.toDirectory) collect {
169-
case (j, Success(pd)) => Success((pd, findPluginClassloader(Seq(j))))
164+
val fromLoaders = paths.map {path =>
165+
val loader = findPluginClassloader(path)
166+
val list: Option[URL] = loader.getResources(PluginXML).asScala.take(1).toList.headOption
167+
list match {
168+
case Some(url) =>
169+
val inputStream = url.openStream
170+
try {
171+
Try((PluginDescription.fromXML(inputStream), loader))
172+
} finally {
173+
inputStream.close()
174+
}
175+
case None =>
176+
Failure(new MissingPluginException(path))
170177
}
171178
}
172179

173-
// scan jar paths for plugins, taking the first plugin you find.
174-
// a path element can be either a plugin.jar or an exploded dir.
175-
def findDescriptor(ps: List[Path]) = {
176-
def loop(qs: List[Path]): Try[PluginDescription] = qs match {
177-
case Nil => Failure(new MissingPluginException(ps))
178-
case p :: rest =>
179-
if (p.isDirectory) loadDescriptionFromFile(p.toDirectory / PluginXML) orElse loop(rest)
180-
else if (p.isFile) loadDescriptionFromJar(p.toFile) orElse loop(rest)
181-
else loop(rest)
182-
}
183-
loop(ps)
184-
}
185-
val fromPaths: PDResults = paths map (p => (p, findDescriptor(p))) map {
186-
case (p, Success(pd)) => Success((pd, findPluginClassloader(p)))
187-
case (_, Failure(e)) => Failure(e)
188-
}
189-
190180
val seen = mutable.HashSet[String]()
191-
val enabled = (fromPaths ::: fromDirs) map {
181+
val enabled = fromLoaders map {
192182
case Success((pd, loader)) if seen(pd.classname) =>
193183
// a nod to scala/bug#7494, take the plugin classes distinctly
194184
Failure(new PluginLoadException(pd.name, s"Ignoring duplicate plugin ${pd.name} (${pd.classname})"))

src/compiler/scala/tools/nsc/plugins/Plugins.scala

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
package scala.tools.nsc
1414
package plugins
1515

16+
import java.net.URL
17+
18+
import scala.reflect.internal.util.ScalaClassLoader
1619
import scala.reflect.io.Path
20+
import scala.tools.nsc
21+
import scala.tools.nsc.typechecker.Macros
1722
import scala.tools.nsc.util.ClassPath
1823
import scala.tools.util.PathResolver.Defaults
1924

@@ -127,4 +132,49 @@ trait Plugins { global: Global =>
127132
(for (plug <- roughPluginsList ; help <- plug.optionsHelp) yield {
128133
"\nOptions for plugin '%s':\n%s\n".format(plug.name, help)
129134
}).mkString
135+
136+
/** Obtains a `ClassLoader` instance used for macro expansion.
137+
*
138+
* By default a new `ScalaClassLoader` is created using the classpath
139+
* from global and the classloader of self as parent.
140+
*
141+
* Mirrors with runtime definitions (e.g. Repl) need to adjust this method.
142+
*/
143+
protected[scala] def findMacroClassLoader(): ClassLoader = {
144+
val classpath: Seq[URL] = if (settings.YmacroClasspath.isSetByUser) {
145+
for {
146+
file <- scala.tools.nsc.util.ClassPath.expandPath(settings.YmacroClasspath.value, true)
147+
af <- Option(nsc.io.AbstractFile getDirectory file)
148+
} yield af.file.toURI.toURL
149+
} else global.classPath.asURLs
150+
def newLoader = () => {
151+
analyzer.macroLogVerbose("macro classloader: initializing from -cp: %s".format(classpath))
152+
ScalaClassLoader.fromURLs(classpath, getClass.getClassLoader)
153+
}
154+
155+
val disableCache = settings.YcacheMacroClassLoader.value == settings.CachePolicy.None.name
156+
if (disableCache) newLoader()
157+
else {
158+
import scala.tools.nsc.io.Jar
159+
import scala.reflect.io.{AbstractFile, Path}
160+
161+
val urlsAndFiles = classpath.map(u => u -> AbstractFile.getURL(u))
162+
val hasNullURL = urlsAndFiles.filter(_._2 eq null)
163+
if (hasNullURL.nonEmpty) {
164+
// TODO if the only null is jrt:// we can still cache
165+
// TODO filter out classpath elements pointing to non-existing files before we get here, that's another source of null
166+
analyzer.macroLogVerbose(s"macro classloader: caching is disabled because `AbstractFile.getURL` returned `null` for ${hasNullURL.map(_._1).mkString(", ")}.")
167+
newLoader()
168+
} else {
169+
val locations = urlsAndFiles.map(t => Path(t._2.file))
170+
val nonJarZips = locations.filterNot(Jar.isJarOrZip(_))
171+
if (nonJarZips.nonEmpty) {
172+
analyzer.macroLogVerbose(s"macro classloader: caching is disabled because the following paths are not supported: ${nonJarZips.mkString(",")}.")
173+
newLoader()
174+
} else {
175+
Macros.macroClassLoadersCache.getOrCreate(locations.map(_.jfile.toPath()), newLoader)
176+
}
177+
}
178+
}
179+
}
130180
}

src/compiler/scala/tools/nsc/typechecker/Macros.scala

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -67,51 +67,6 @@ trait Macros extends MacroRuntimes with Traces with Helpers {
6767

6868
def globalSettings = global.settings
6969

70-
/** Obtains a `ClassLoader` instance used for macro expansion.
71-
*
72-
* By default a new `ScalaClassLoader` is created using the classpath
73-
* from global and the classloader of self as parent.
74-
*
75-
* Mirrors with runtime definitions (e.g. Repl) need to adjust this method.
76-
*/
77-
protected def findMacroClassLoader(): ClassLoader = {
78-
val classpath: Seq[URL] = if (settings.YmacroClasspath.isSetByUser) {
79-
for {
80-
file <- scala.tools.nsc.util.ClassPath.expandPath(settings.YmacroClasspath.value, true)
81-
af <- Option(AbstractFile getDirectory file)
82-
} yield af.file.toURI.toURL
83-
} else global.classPath.asURLs
84-
def newLoader = () => {
85-
macroLogVerbose("macro classloader: initializing from -cp: %s".format(classpath))
86-
ScalaClassLoader.fromURLs(classpath, self.getClass.getClassLoader)
87-
}
88-
89-
val disableCache = settings.YcacheMacroClassLoader.value == settings.CachePolicy.None.name
90-
if (disableCache) newLoader()
91-
else {
92-
import scala.tools.nsc.io.Jar
93-
import scala.reflect.io.{AbstractFile, Path}
94-
95-
val urlsAndFiles = classpath.map(u => u -> AbstractFile.getURL(u))
96-
val hasNullURL = urlsAndFiles.filter(_._2 eq null)
97-
if (hasNullURL.nonEmpty) {
98-
// TODO if the only null is jrt:// we can still cache
99-
// TODO filter out classpath elements pointing to non-existing files before we get here, that's another source of null
100-
macroLogVerbose(s"macro classloader: caching is disabled because `AbstractFile.getURL` returned `null` for ${hasNullURL.map(_._1).mkString(", ")}.")
101-
newLoader()
102-
} else {
103-
val locations = urlsAndFiles.map(t => Path(t._2.file))
104-
val nonJarZips = locations.filterNot(Jar.isJarOrZip(_))
105-
if (nonJarZips.nonEmpty) {
106-
macroLogVerbose(s"macro classloader: caching is disabled because the following paths are not supported: ${nonJarZips.mkString(",")}.")
107-
newLoader()
108-
} else {
109-
Macros.macroClassLoadersCache.getOrCreate(locations.map(_.jfile.toPath()), newLoader)
110-
}
111-
}
112-
}
113-
}
114-
11570
/** `MacroImplBinding` and its companion module are responsible for
11671
* serialization/deserialization of macro def -> impl bindings.
11772
*

src/compiler/scala/tools/reflect/ReflectGlobal.scala

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,14 @@ import scala.tools.nsc.typechecker.Analyzer
2525
class ReflectGlobal(currentSettings: Settings, reporter: Reporter, override val rootClassLoader: ClassLoader)
2626
extends Global(currentSettings, reporter) with scala.tools.reflect.ReflectSetup with scala.reflect.runtime.SymbolTable {
2727

28-
override lazy val analyzer = new {
29-
val global: ReflectGlobal.this.type = ReflectGlobal.this
30-
} with Analyzer {
31-
/** Obtains the classLoader used for runtime macro expansion.
32-
*
33-
* Macro expansion can use everything available in [[global.classPath]] or [[rootClassLoader]].
34-
* The [[rootClassLoader]] is used to obtain runtime defined macros.
35-
*/
36-
override protected def findMacroClassLoader(): ClassLoader = {
37-
val classpath = global.classPath.asURLs
38-
ScalaClassLoader.fromURLs(classpath, rootClassLoader)
39-
}
28+
/** Obtains the classLoader used for runtime macro expansion.
29+
*
30+
* Macro expansion can use everything available in `global.classPath` or `rootClassLoader`.
31+
* The `rootClassLoader` is used to obtain runtime defined macros.
32+
*/
33+
override protected[scala] def findMacroClassLoader(): ClassLoader = {
34+
val classpath = classPath.asURLs
35+
ScalaClassLoader.fromURLs(classpath, rootClassLoader)
4036
}
4137

4238
override def transformedType(sym: Symbol) =

src/repl/scala/tools/nsc/interpreter/ReplGlobal.scala

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,11 @@ trait ReplGlobal extends Global {
2525
super.abort(msg)
2626
}
2727

28-
override lazy val analyzer = new {
29-
val global: ReplGlobal.this.type = ReplGlobal.this
30-
} with Analyzer {
31-
32-
override protected def findMacroClassLoader(): ClassLoader = {
33-
val loader = super.findMacroClassLoader
34-
macroLogVerbose("macro classloader: initializing from a REPL classloader: %s".format(global.classPath.asURLs))
35-
val virtualDirectory = globalSettings.outputDirs.getSingleOutput.get
36-
new util.AbstractFileClassLoader(virtualDirectory, loader) {}
37-
}
28+
override protected[scala] def findMacroClassLoader(): ClassLoader = {
29+
val loader = super.findMacroClassLoader
30+
analyzer.macroLogVerbose("macro classloader: initializing from a REPL classloader: %s".format(classPath.asURLs))
31+
val virtualDirectory = analyzer.globalSettings.outputDirs.getSingleOutput.get
32+
new util.AbstractFileClassLoader(virtualDirectory, loader) {}
3833
}
3934

4035
override def optimizerClassPath(base: ClassPath): ClassPath = {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package scala.tools.nsc
2+
3+
import org.junit.{Assert, Test}
4+
import org.junit.runner.RunWith
5+
import org.junit.runners.JUnit4
6+
7+
import scala.reflect.internal.util.{AbstractFileClassLoader, NoSourceFile}
8+
import scala.reflect.io.{Path, VirtualDirectory}
9+
import scala.tools.nsc.plugins.{Plugin, PluginComponent}
10+
11+
@RunWith(classOf[JUnit4])
12+
class GlobalCustomizeClassloaderTest {
13+
// Demonstrate extension points to customise creation of the classloaders used to load compiler
14+
// plugins and macro implementations.
15+
//
16+
// A use case could be for a build tool to take control of caching of these classloaders in a way
17+
// that properly closes them before one of the elements needs to be overwritten.
18+
@Test def test(): Unit = {
19+
val g = new Global(new Settings) {
20+
override protected[scala] def findMacroClassLoader(): ClassLoader = getClass.getClassLoader
21+
override protected def findPluginClassLoader(classpath: Seq[Path]): ClassLoader = {
22+
val d = new VirtualDirectory("", None)
23+
val xml = d.fileNamed("scalac-plugin.xml")
24+
val out = xml.bufferedOutput
25+
out.write(
26+
s"""<plugin>
27+
|<name>sample-plugin</name>
28+
|<classname>${classOf[SamplePlugin].getName}</classname>
29+
|</plugin>
30+
|""".stripMargin.getBytes())
31+
out.close()
32+
new AbstractFileClassLoader(d, getClass.getClassLoader)
33+
}
34+
}
35+
g.settings.usejavacp.value = true
36+
g.settings.plugin.value = List("sample")
37+
new g.Run
38+
assert(g.settings.log.value == List("typer"))
39+
40+
val unit = new g.CompilationUnit(NoSourceFile)
41+
val context = g.analyzer.rootContext(unit)
42+
val typer = g.analyzer.newTyper(context)
43+
import g._
44+
SampleMacro.data = "in this classloader"
45+
val typed = typer.typed(q"scala.tools.nsc.SampleMacro.m")
46+
assert(!reporter.hasErrors)
47+
typed match {
48+
case Typed(Literal(Constant(s: String)), _) => Assert.assertEquals(SampleMacro.data, s)
49+
case _ => Assert.fail()
50+
}
51+
}
52+
}
53+
54+
object SampleMacro {
55+
var data: String = _
56+
import language.experimental.macros
57+
import scala.reflect.macros.blackbox.Context
58+
def m: String = macro impl
59+
def impl(c: Context): c.Tree = c.universe.Literal(c.universe.Constant(data))
60+
}
61+
62+
class SamplePlugin(val global: Global) extends Plugin {
63+
override val name: String = "sample"
64+
override val description: String = "sample"
65+
override val components: List[PluginComponent] = Nil
66+
override def init(options: List[String], error: String => Unit): Boolean = {
67+
val result = super.init(options, error)
68+
global.settings.log.value = List("typer")
69+
result
70+
}
71+
}

0 commit comments

Comments
 (0)