Skip to content

Commit b2ba80a

Browse files
committed
Merge pull request scala#4051 from heathermiller/repl-cp-fix2
SI-6502 Reenables loading jars into the running REPL (regression in 2.10)
2 parents 965d7b9 + 24a2ef9 commit b2ba80a

File tree

8 files changed

+354
-18
lines changed

8 files changed

+354
-18
lines changed

src/compiler/scala/tools/nsc/Global.scala

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ package tools
88
package nsc
99

1010
import java.io.{ File, FileOutputStream, PrintWriter, IOException, FileNotFoundException }
11+
import java.net.URL
1112
import java.nio.charset.{ Charset, CharsetDecoder, IllegalCharsetNameException, UnsupportedCharsetException }
1213
import scala.compat.Platform.currentTime
1314
import scala.collection.{ mutable, immutable }
1415
import io.{ SourceReader, AbstractFile, Path }
1516
import reporters.{ Reporter, ConsoleReporter }
16-
import util.{ ClassPath, StatisticsInfo, returning, stackTraceString }
17+
import util.{ ClassPath, MergedClassPath, StatisticsInfo, returning, stackTraceString }
1718
import scala.reflect.ClassTag
1819
import scala.reflect.internal.util.{ OffsetPosition, SourceFile, NoSourceFile, BatchSourceFile, ScriptSourceFile }
1920
import scala.reflect.internal.pickling.{ PickleBuffer, PickleFormat }
@@ -841,6 +842,150 @@ class Global(var currentSettings: Settings, var reporter: Reporter)
841842
} reverse
842843
}
843844

845+
// ------------ REPL utilities ---------------------------------
846+
847+
/** Extend classpath of `platform` and rescan updated packages. */
848+
def extendCompilerClassPath(urls: URL*): Unit = {
849+
val newClassPath = platform.classPath.mergeUrlsIntoClassPath(urls: _*)
850+
platform.currentClassPath = Some(newClassPath)
851+
// Reload all specified jars into this compiler instance
852+
invalidateClassPathEntries(urls.map(_.getPath): _*)
853+
}
854+
855+
// ------------ Invalidations ---------------------------------
856+
857+
/** Is given package class a system package class that cannot be invalidated?
858+
*/
859+
private def isSystemPackageClass(pkg: Symbol) =
860+
pkg == RootClass || (pkg.hasTransOwner(definitions.ScalaPackageClass) && !pkg.hasTransOwner(this.rootMirror.staticPackage("scala.tools").moduleClass.asClass))
861+
862+
/** Invalidates packages that contain classes defined in a classpath entry, and
863+
* rescans that entry.
864+
*
865+
* First, the classpath entry referred to by one of the `paths` is rescanned,
866+
* so that any new files or changes in subpackages are picked up.
867+
* Second, any packages for which one of the following conditions is met is invalidated:
868+
* - the classpath entry contained during the last compilation run now contains classfiles
869+
* that represent a member in the package;
870+
* - the classpath entry now contains classfiles that represent a member in the package;
871+
* - the set of subpackages has changed.
872+
*
873+
* The invalidated packages are reset in their entirety; all member classes and member packages
874+
* are re-accessed using the new classpath.
875+
*
876+
* System packages that the compiler needs to access as part of standard definitions
877+
* are not invalidated. A system package is:
878+
* Any package rooted in "scala", with the exception of packages rooted in "scala.tools".
879+
*
880+
* @param paths Fully-qualified names that refer to directories or jar files that are
881+
* entries on the classpath.
882+
*/
883+
def invalidateClassPathEntries(paths: String*): Unit = {
884+
implicit object ClassPathOrdering extends Ordering[PlatformClassPath] {
885+
def compare(a:PlatformClassPath, b:PlatformClassPath) = a.asClasspathString compare b.asClasspathString
886+
}
887+
val invalidated, failed = new mutable.ListBuffer[ClassSymbol]
888+
classPath match {
889+
case cp: MergedClassPath[_] =>
890+
def assoc(path: String): List[(PlatformClassPath, PlatformClassPath)] = {
891+
val dir = AbstractFile.getDirectory(path)
892+
val canonical = dir.canonicalPath
893+
def matchesCanonical(e: ClassPath[_]) = e.origin match {
894+
case Some(opath) =>
895+
AbstractFile.getDirectory(opath).canonicalPath == canonical
896+
case None =>
897+
false
898+
}
899+
cp.entries find matchesCanonical match {
900+
case Some(oldEntry) =>
901+
List(oldEntry -> cp.context.newClassPath(dir))
902+
case None =>
903+
error(s"Error adding entry to classpath. During invalidation, no entry named $path in classpath $classPath")
904+
List()
905+
}
906+
}
907+
val subst = immutable.TreeMap(paths flatMap assoc: _*)
908+
if (subst.nonEmpty) {
909+
platform updateClassPath subst
910+
informProgress(s"classpath updated on entries [${subst.keys mkString ","}]")
911+
def mkClassPath(elems: Iterable[PlatformClassPath]): PlatformClassPath =
912+
if (elems.size == 1) elems.head
913+
else new MergedClassPath(elems, classPath.context)
914+
val oldEntries = mkClassPath(subst.keys)
915+
val newEntries = mkClassPath(subst.values)
916+
mergeNewEntries(newEntries, RootClass, Some(classPath), Some(oldEntries), invalidated, failed)
917+
}
918+
}
919+
def show(msg: String, syms: scala.collection.Traversable[Symbol]) =
920+
if (syms.nonEmpty)
921+
informProgress(s"$msg: ${syms map (_.fullName) mkString ","}")
922+
show("invalidated packages", invalidated)
923+
show("could not invalidate system packages", failed)
924+
}
925+
926+
/** Merges new classpath entries into the symbol table
927+
*
928+
* @param newEntries The new classpath entries
929+
* @param root The root symbol to be resynced (a package class)
930+
* @param allEntries Optionally, the corresponding package in the complete current classpath
931+
* @param oldEntries Optionally, the corresponding package in the old classpath entries
932+
* @param invalidated A listbuffer collecting the invalidated package classes
933+
* @param failed A listbuffer collecting system package classes which could not be invalidated
934+
*
935+
* The merging strategy is determined by the absence or presence of classes and packages.
936+
*
937+
* If either oldEntries or newEntries contains classes, root is invalidated provided that a corresponding package
938+
* exists in allEntries. Otherwise it is removed.
939+
* Otherwise, the action is determined by the following matrix, with columns:
940+
*
941+
* old sym action
942+
* + + recurse into all child packages of newEntries
943+
* - + invalidate root
944+
* - - create and enter root
945+
*
946+
* Here, old means classpath, and sym means symboltable. + is presence of an entry in its column, - is absence.
947+
*/
948+
private def mergeNewEntries(newEntries: PlatformClassPath, root: ClassSymbol,
949+
allEntries: OptClassPath, oldEntries: OptClassPath,
950+
invalidated: mutable.ListBuffer[ClassSymbol], failed: mutable.ListBuffer[ClassSymbol]) {
951+
ifDebug(informProgress(s"syncing $root, $oldEntries -> $newEntries"))
952+
953+
val getName: ClassPath[AbstractFile] => String = (_.name)
954+
def hasClasses(cp: OptClassPath) = cp.isDefined && cp.get.classes.nonEmpty
955+
def invalidateOrRemove(root: ClassSymbol) = {
956+
allEntries match {
957+
case Some(cp) => root setInfo new loaders.PackageLoader(cp)
958+
case None => root.owner.info.decls unlink root.sourceModule
959+
}
960+
invalidated += root
961+
}
962+
def subPackage(cp: PlatformClassPath, name: String): OptClassPath =
963+
cp.packages find (cp1 => getName(cp1) == name)
964+
965+
val classesFound = hasClasses(oldEntries) || newEntries.classes.nonEmpty
966+
if (classesFound && !isSystemPackageClass(root)) {
967+
invalidateOrRemove(root)
968+
} else {
969+
if (classesFound) {
970+
if (root.isRoot) invalidateOrRemove(EmptyPackageClass)
971+
else failed += root
972+
}
973+
if (!oldEntries.isDefined) invalidateOrRemove(root)
974+
else
975+
for (pstr <- newEntries.packages.map(getName)) {
976+
val pname = newTermName(pstr)
977+
val pkg = (root.info decl pname) orElse {
978+
// package does not exist in symbol table, create symbol to track it
979+
assert(!subPackage(oldEntries.get, pstr).isDefined)
980+
loaders.enterPackage(root, pstr, new loaders.PackageLoader(allEntries.get))
981+
}
982+
mergeNewEntries(subPackage(newEntries, pstr).get, pkg.moduleClass.asClass,
983+
subPackage(allEntries.get, pstr), subPackage(oldEntries.get, pstr),
984+
invalidated, failed)
985+
}
986+
}
987+
}
988+
844989
// ----------- Runs ---------------------------------------
845990

846991
private var curRun: Run = null

src/compiler/scala/tools/nsc/backend/JavaPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ trait JavaPlatform extends Platform {
1616
import global._
1717
import definitions._
1818

19-
private var currentClassPath: Option[MergedClassPath[AbstractFile]] = None
19+
private[nsc] var currentClassPath: Option[MergedClassPath[AbstractFile]] = None
2020

2121
def classPath: ClassPath[AbstractFile] = {
2222
if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings).result)

src/compiler/scala/tools/nsc/util/ClassPath.scala

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ abstract class ClassPath[T] {
197197
def packages: IndexedSeq[ClassPath[T]]
198198
def sourcepaths: IndexedSeq[AbstractFile]
199199

200+
/** The entries this classpath is composed of. In class `ClassPath` it's just the singleton list containing `this`.
201+
* Subclasses such as `MergedClassPath` typically return lists with more elements.
202+
*/
203+
def entries: IndexedSeq[ClassPath[T]] = IndexedSeq(this)
204+
205+
/** Merge classpath of `platform` and `urls` into merged classpath */
206+
def mergeUrlsIntoClassPath(urls: URL*): MergedClassPath[T] = {
207+
// Collect our new jars/directories and add them to the existing set of classpaths
208+
val allEntries =
209+
(entries ++
210+
urls.map(url => context.newClassPath(io.AbstractFile.getURL(url)))
211+
).distinct
212+
213+
// Combine all of our classpaths (old and new) into one merged classpath
214+
new MergedClassPath(allEntries, context)
215+
}
216+
200217
/**
201218
* Represents classes which can be loaded with a ClassfileLoader and/or SourcefileLoader.
202219
*/
@@ -322,7 +339,7 @@ extends MergedClassPath[T](original.entries map (e => subst getOrElse (e, e)), o
322339
* A classpath unifying multiple class- and sourcepath entries.
323340
*/
324341
class MergedClassPath[T](
325-
val entries: IndexedSeq[ClassPath[T]],
342+
override val entries: IndexedSeq[ClassPath[T]],
326343
val context: ClassPathContext[T])
327344
extends ClassPath[T] {
328345
def this(entries: TraversableOnce[ClassPath[T]], context: ClassPathContext[T]) =

src/reflect/scala/reflect/io/AbstractFile.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,16 @@ object AbstractFile {
4848
else null
4949

5050
/**
51-
* If the specified URL exists and is a readable zip or jar archive,
52-
* returns an abstract directory backed by it. Otherwise, returns
53-
* `null`.
51+
* If the specified URL exists and is a regular file or a directory, returns an
52+
* abstract regular file or an abstract directory, respectively, backed by it.
53+
* Otherwise, returns `null`.
5454
*/
55-
def getURL(url: URL): AbstractFile = {
56-
if (url == null || !Path.isExtensionJarOrZip(url.getPath)) null
57-
else ZipArchive fromURL url
58-
}
55+
def getURL(url: URL): AbstractFile =
56+
if (url.getProtocol == "file") {
57+
val f = new java.io.File(url.getPath)
58+
if (f.isDirectory) getDirectory(f)
59+
else getFile(f)
60+
} else null
5961

6062
def getResources(url: URL): AbstractFile = ZipArchive fromManifestURL url
6163
}

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import scala.reflect.internal.util.{ BatchSourceFile, ScalaClassLoader }
1919
import ScalaClassLoader._
2020
import scala.reflect.io.{ File, Directory }
2121
import scala.tools.util._
22+
import io.AbstractFile
2223
import scala.collection.generic.Clearable
2324
import scala.concurrent.{ ExecutionContext, Await, Future, future }
2425
import ExecutionContext.Implicits._
@@ -221,7 +222,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
221222
nullary("power", "enable power user mode", powerCmd),
222223
nullary("quit", "exit the interpreter", () => Result(keepRunning = false, None)),
223224
cmd("replay", "[options]", "reset the repl and replay all previous commands", replayCommand),
224-
//cmd("require", "<path>", "add a jar or directory to the classpath", require), // TODO
225+
cmd("require", "<path>", "add a jar to the classpath", require),
225226
cmd("reset", "[options]", "reset the repl to its initial state, forgetting all session entries", resetCommand),
226227
cmd("save", "<path>", "save replayable session to a file", saveCommand),
227228
shCommand,
@@ -620,13 +621,57 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
620621
val f = File(arg).normalize
621622
if (f.exists) {
622623
addedClasspath = ClassPath.join(addedClasspath, f.path)
623-
val totalClasspath = ClassPath.join(settings.classpath.value, addedClasspath)
624-
echo("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, totalClasspath))
625-
replay()
624+
intp.addUrlsToClassPath(f.toURI.toURL)
625+
echo("Added '%s' to classpath.".format(f.path, intp.global.classPath.asClasspathString))
626+
repldbg("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, intp.global.classPath.asClasspathString))
626627
}
627628
else echo("The path '" + f + "' doesn't seem to exist.")
628629
}
629630

631+
/** Adds jar file to the current classpath. Jar will only be added if it
632+
* does not contain classes that already exist on the current classpath.
633+
*
634+
* Importantly, `require` adds jars to the classpath ''without'' resetting
635+
* the state of the interpreter. This is in contrast to `replay` which can
636+
* be used to add jars to the classpath and which creates a new instance of
637+
* the interpreter and replays all interpreter expressions.
638+
*/
639+
def require(arg: String): Unit = {
640+
class InfoClassLoader extends java.lang.ClassLoader {
641+
def classOf(arr: Array[Byte]): Class[_] =
642+
super.defineClass(null, arr, 0, arr.length)
643+
}
644+
645+
val f = File(arg).normalize
646+
647+
if (f.isDirectory) {
648+
echo("Adding directories to the classpath is not supported. Add a jar instead.")
649+
return
650+
}
651+
652+
val jarFile = AbstractFile.getDirectory(new java.io.File(arg))
653+
654+
def flatten(f: AbstractFile): Iterator[AbstractFile] =
655+
if (f.isClassContainer) f.iterator.flatMap(flatten)
656+
else Iterator(f)
657+
658+
val entries = flatten(jarFile)
659+
val cloader = new InfoClassLoader
660+
661+
def classNameOf(classFile: AbstractFile): String = cloader.classOf(classFile.toByteArray).getName
662+
def alreadyDefined(clsName: String) = intp.classLoader.tryToLoadClass(clsName).isDefined
663+
val exists = entries.filter(_.hasExtension("class")).map(classNameOf).exists(alreadyDefined)
664+
665+
if (!f.exists) echo(s"The path '$f' doesn't seem to exist.")
666+
else if (exists) echo(s"The path '$f' cannot be loaded, because existing classpath entries conflict.") // TODO tell me which one
667+
else {
668+
addedClasspath = ClassPath.join(addedClasspath, f.path)
669+
intp.addUrlsToClassPath(f.toURI.toURL)
670+
echo("Added '%s' to classpath.".format(f.path, intp.global.classPath.asClasspathString))
671+
repldbg("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, intp.global.classPath.asClasspathString))
672+
}
673+
}
674+
630675
def powerCmd(): Result = {
631676
if (isReplPower) "Already in power mode."
632677
else enablePowerMode(isDuringInit = false)

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import scala.reflect.internal.util.{ BatchSourceFile, SourceFile }
1818
import scala.tools.util.PathResolver
1919
import scala.tools.nsc.io.AbstractFile
2020
import scala.tools.nsc.typechecker.{ TypeStrings, StructuredTypeStrings }
21-
import scala.tools.nsc.util.{ ScalaClassLoader, stringFromReader, stringFromWriter, StackTraceOps }
21+
import scala.tools.nsc.util.{ ScalaClassLoader, stringFromReader, stringFromWriter, StackTraceOps, ClassPath, MergedClassPath }
22+
import ScalaClassLoader.URLClassLoader
2223
import scala.tools.nsc.util.Exceptional.unwrap
24+
import scala.tools.nsc.backend.JavaPlatform
2325
import javax.script.{AbstractScriptEngine, Bindings, ScriptContext, ScriptEngine, ScriptEngineFactory, ScriptException, CompiledScript, Compilable}
26+
import java.net.URL
27+
import java.io.File
2428

2529
/** An interpreter for Scala code.
2630
*
@@ -82,6 +86,8 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
8286
private var _classLoader: util.AbstractFileClassLoader = null // active classloader
8387
private val _compiler: ReplGlobal = newCompiler(settings, reporter) // our private compiler
8488

89+
private var _runtimeClassLoader: URLClassLoader = null // wrapper exposing addURL
90+
8591
def compilerClasspath: Seq[java.net.URL] = (
8692
if (isInitializeComplete) global.classPath.asURLs
8793
else new PathResolver(settings).result.asURLs // the compiler's classpath
@@ -237,6 +243,18 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
237243
new Global(settings, reporter) with ReplGlobal { override def toString: String = "<global>" }
238244
}
239245

246+
/**
247+
* Adds all specified jars to the compile and runtime classpaths.
248+
*
249+
* @note Currently only supports jars, not directories.
250+
* @param urls The list of items to add to the compile and runtime classpaths.
251+
*/
252+
def addUrlsToClassPath(urls: URL*): Unit = {
253+
new Run // force some initialization
254+
urls.foreach(_runtimeClassLoader.addURL) // Add jars to runtime classloader
255+
global.extendCompilerClassPath(urls: _*) // Add jars to compile-time classpath
256+
}
257+
240258
/** Parent classloader. Overridable. */
241259
protected def parentClassLoader: ClassLoader =
242260
settings.explicitParentLoader.getOrElse( this.getClass.getClassLoader() )
@@ -329,9 +347,9 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
329347
}
330348
}
331349
private def makeClassLoader(): util.AbstractFileClassLoader =
332-
new TranslatingClassLoader(parentClassLoader match {
333-
case null => ScalaClassLoader fromURLs compilerClasspath
334-
case p => new ScalaClassLoader.URLClassLoader(compilerClasspath, p)
350+
new TranslatingClassLoader({
351+
_runtimeClassLoader = new URLClassLoader(compilerClasspath, parentClassLoader)
352+
_runtimeClassLoader
335353
})
336354

337355
// Set the current Java "context" class loader to this interpreter's class loader

test/files/run/t6502.check

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
test1 res1: true
2+
test1 res2: true
3+
test2 res1: true
4+
test2 res2: true
5+
test3 res1: true
6+
test3 res2: true
7+
test4 res1: true
8+
test4 res2: true

0 commit comments

Comments
 (0)