diff --git a/.travis.yml b/.travis.yml index edc99e178..d681f774b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,7 @@ install: script: # It doesn't need that much memory because compile and run are forked - - sbt -Dsbt.version=1.2.6 -Dsbt.ci=true -J-XX:ReservedCodeCacheSize=128m -J-Xmx800M -J-Xms800M -J-server "$SBT_CMD" + - sbt -Dsbt.ci=true -J-XX:ReservedCodeCacheSize=128m -J-Xmx800M -J-Xms800M -J-server "$SBT_CMD" before_cache: - find $HOME/.ivy2 -name "ivydata-*.properties" -delete diff --git a/build.sbt b/build.sbt index 20d9bade1..9507d71ac 100644 --- a/build.sbt +++ b/build.sbt @@ -656,6 +656,7 @@ lazy val mainProj = (project in file("main")) sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", testOptions in Test += Tests .Argument(TestFrameworks.ScalaCheck, "-minSuccessfulTests", "1000"), + Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat, // Delete this after 1.3.0-RC2. mimaSettings, mimaBinaryIssueFilters ++= Vector( // New and changed methods on KeyIndex. internal. @@ -895,6 +896,7 @@ def otherRootSettings = scripted := scriptedTask.evaluated, scriptedUnpublished := scriptedUnpublishedTask.evaluated, scriptedSource := (sourceDirectory in sbtProj).value / "sbt-test", + watchTriggers in scripted += scriptedSource.value.toGlob / **, scriptedLaunchOpts := List("-Xmx1500M", "-Xms512M", "-server"), publishAll := { val _ = (publishLocal).all(ScopeFilter(inAnyProject)).value }, publishLocalBinAll := { val _ = (publishLocalBin).all(ScopeFilter(inAnyProject)).value }, diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index 251423049..6b7b85e0c 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -9,7 +9,8 @@ package sbt import java.io.File -import sbt.internal.inc.classpath.ClassLoaderCache +import sbt.internal.inc.classpath.{ ClassLoaderCache => IncClassLoaderCache } +import sbt.internal.classpath.ClassLoaderCache import sbt.internal.server.ServerHandler import sbt.internal.util.AttributeKey import sbt.librarymanagement.ModuleID @@ -82,11 +83,16 @@ object BasicKeys { "True if commands are currently being entered from an interactive environment.", 10 ) - private[sbt] val classLoaderCache = AttributeKey[ClassLoaderCache]( + private[sbt] val classLoaderCache = AttributeKey[IncClassLoaderCache]( "class-loader-cache", "Caches class loaders based on the classpath entries and last modified times.", 10 ) + private[sbt] val extendedClassLoaderCache = AttributeKey[ClassLoaderCache]( + "extended-class-loader-cache", + "Caches class loaders based on the classpath entries and last modified times.", + 10 + ) private[sbt] val OnFailureStack = AttributeKey[List[Option[Exec]]]( "on-failure-stack", "Stack that remembers on-failure handlers.", diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index 0f38eaf57..f62a1aa75 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -9,6 +9,9 @@ package sbt import java.io.File import java.util.concurrent.Callable + +import sbt.internal.classpath.ClassLoaderCache +import sbt.internal.inc.classpath.{ ClassLoaderCache => IncClassLoaderCache } import sbt.util.Logger import sbt.internal.util.{ AttributeKey, @@ -19,7 +22,6 @@ import sbt.internal.util.{ GlobalLogging } import sbt.internal.util.complete.{ HistoryCommands, Parser } -import sbt.internal.inc.classpath.ClassLoaderCache /** * Data structure representing all command execution information. @@ -193,7 +195,7 @@ trait StateOps extends Any { def setInteractive(flag: Boolean): State /** Get the class loader cache for the application.*/ - def classLoaderCache: ClassLoaderCache + def classLoaderCache: IncClassLoaderCache /** Create and register a class loader cache. This should be called once at the application entry-point.*/ def initializeClassLoaderCache: State @@ -221,6 +223,7 @@ object State { /** * Provides a list of recently executed commands. The commands are stored as processed instead of as entered by the user. + * * @param executed the list of the most recently executed commands, with the most recent command first. * @param maxSize the maximum number of commands to keep, or 0 to keep an unlimited number. */ @@ -334,11 +337,18 @@ object State { def interactive = getBoolean(s, BasicKeys.interactive, false) def setInteractive(i: Boolean) = s.put(BasicKeys.interactive, i) - def classLoaderCache: ClassLoaderCache = - s get BasicKeys.classLoaderCache getOrElse newClassLoaderCache - def initializeClassLoaderCache = s.put(BasicKeys.classLoaderCache, newClassLoaderCache) + def classLoaderCache: IncClassLoaderCache = + s get BasicKeys.classLoaderCache getOrElse (throw new IllegalStateException( + "Tried to get classloader cache for uninitialized state." + )) + def initializeClassLoaderCache: State = { + s.get(BasicKeys.extendedClassLoaderCache).foreach(_.close()) + val cache = newClassLoaderCache + s.put(BasicKeys.extendedClassLoaderCache, cache) + .put(BasicKeys.classLoaderCache, new IncClassLoaderCache(cache)) + } private[this] def newClassLoaderCache = - new ClassLoaderCache(s.configuration.provider.scalaProvider.launcher.topLoader) + new ClassLoaderCache(s.configuration.provider.scalaProvider) } import ExceptionCategory._ diff --git a/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala b/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala new file mode 100644 index 000000000..dee8e88c4 --- /dev/null +++ b/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala @@ -0,0 +1,252 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.classpath + +import java.io.File +import java.lang.management.ManagementFactory +import java.lang.ref.{ Reference, ReferenceQueue, SoftReference } +import java.net.{ URL, URLClassLoader } +import java.util.concurrent.atomic.AtomicInteger + +import sbt.internal.inc.classpath.{ + AbstractClassLoaderCache, + ClassLoaderCache => IncClassLoaderCache +} +import sbt.internal.inc.{ AnalyzingCompiler, ZincUtil } +import sbt.io.IO +import xsbti.ScalaProvider +import xsbti.compile.{ ClasspathOptions, ScalaInstance } + +import scala.annotation.tailrec +import scala.collection.JavaConverters._ +import scala.util.control.NonFatal + +private object ClassLoaderCache { + private def threadID = new AtomicInteger(0) +} +private[sbt] class ClassLoaderCache( + override val commonParent: ClassLoader, + private val miniProvider: Option[(File, ClassLoader)] +) extends AbstractClassLoaderCache { + def this(commonParent: ClassLoader) = this(commonParent, None) + def this(scalaProvider: ScalaProvider) = + this(scalaProvider.launcher.topLoader, { + scalaProvider.jars.find(_.getName == "scala-library.jar").flatMap { lib => + val clazz = scalaProvider.getClass + try { + val loader = clazz.getDeclaredMethod("libraryLoaderOnly").invoke(scalaProvider) + Some(lib -> loader.asInstanceOf[ClassLoader]) + } catch { case NonFatal(_) => None } + } + }) + private val scalaProviderKey = miniProvider.map { + case (f, cl) => + new Key((f -> IO.getModifiedTimeOrZero(f)) :: Nil, commonParent) { + override def toClassLoader: ClassLoader = cl + } + } + private class Key(val fileStamps: Seq[(File, Long)], val parent: ClassLoader) { + def this(files: List[File]) = + this(files.map(f => f -> IO.getModifiedTimeOrZero(f)), commonParent) + lazy val files: Seq[File] = fileStamps.map(_._1) + lazy val maxStamp: Long = fileStamps.maxBy(_._2)._2 + class CachedClassLoader + extends URLClassLoader(fileStamps.map(_._1.toURI.toURL).toArray, parent) { + override def toString: String = + s"CachedClassloader {\n parent: $parent\n urls:\n" + getURLs.mkString(" ", "\n", "\n}") + } + def toClassLoader: ClassLoader = new CachedClassLoader + override def equals(o: Any): Boolean = o match { + case that: Key => this.fileStamps == that.fileStamps && this.parent == that.parent + } + override def hashCode(): Int = (fileStamps.hashCode * 31) ^ parent.hashCode + override def toString: String = s"Key(${fileStamps mkString ","}, $parent)" + } + private[this] val delegate = + new java.util.concurrent.ConcurrentHashMap[Key, Reference[ClassLoader]]() + private[this] val referenceQueue = new ReferenceQueue[ClassLoader] + + private[this] def closeExpiredLoaders(): Unit = { + val toClose = lock.synchronized(delegate.asScala.groupBy(_._1.files.toSet).flatMap { + case (_, pairs) if pairs.size > 1 => + val max = pairs.maxBy(_._1.maxStamp)._1 + pairs.filterNot(_._1 == max).flatMap { + case (k, v) => + delegate.remove(k) + Option(v.get) + } + case _ => Nil + }) + toClose.foreach(close) + } + private[this] class CleanupThread(private[this] val id: Int) + extends Thread(s"classloader-cache-cleanup-$id") { + setDaemon(true) + start() + @tailrec + override final def run(): Unit = { + val stop = try { + referenceQueue.remove(1000) match { + case ClassLoaderReference(key, classLoader) => + close(classLoader) + delegate.remove(key) + case _ => + } + closeExpiredLoaders() + false + } catch { + case _: InterruptedException => true + } + if (!stop) run() + } + } + + /* + * We need to manage the cache differently depending on whether or not sbt is started up with + * -XX:MaxMetaspaceSize=XXX. The reason is that when the metaspace limit is reached, the jvm + * will run a few Full GCs that will clear SoftReferences so that it can cleanup any classes + * that only softly reachable. If the GC during this phase is able to collect a classloader, it + * will free the metaspace (or at least some of it) previously occupied by the loader. This can + * prevent sbt from crashing with an OOM: Metaspace. The issue with this is that when a loader + * is collected in this way, it will leak handles to its url classpath. To prevent the resource + * leak, we can store a reference to a wrapper loader. That reference, in turn, holds a + * strong reference to the underlying loader. Under heap memory pressure, the jvm will clear the + * soft reference for the wrapped loader and add it to the reference queue. We add a thread + * that reads from the reference queue and closes the underlying URLClassLoader, preventing the + * resource leak. When the system is under heap memory pressure, this eviction approach works + * well. The problem is that we cannot prevent OOM: MetaSpace because the jvm doesn't give us + * a long enough window to clear the ClassLoader references. The wrapper class will get cleared + * during the Metaspace Full GC window, but, even though we quickly clear the strong reference + * to the underlying classloader and close it, the jvm gives up and crashes with an OOM. + * + * To avoid these crashes, if the user starts with a limit on metaspace size via + * -XX:MetaSpaceSize=XXX, we will just store direct soft references to the URLClassLoader and + * leak url classpath handles when loaders are evicted by garbage collection. This is consistent + * with the behavior of sbt versions < 1.3.0. In general, these leaks are probably not a big deal + * except on windows where they prevent any files for which the leaked class loader has an open + * handle from being modified. On linux and mac, we probably leak some file descriptors but it's + * fairly uncommon for sbt to run out of file descriptors. + * + */ + private[this] val metaspaceIsLimited = + ManagementFactory.getMemoryPoolMXBeans.asScala + .exists(b => (b.getName == "Metaspace") && (b.getUsage.getMax > 0)) + private[this] val mkReference: (Key, ClassLoader) => Reference[ClassLoader] = + if (metaspaceIsLimited)(_, cl) => new SoftReference(cl, referenceQueue) + else ClassLoaderReference.apply + private[this] val cleanupThread = new CleanupThread(ClassLoaderCache.threadID.getAndIncrement()) + private[this] val lock = new Object + + private class WrappedLoader(parent: ClassLoader) extends URLClassLoader(Array.empty, parent) { + // This is to make dotty work which extracts the URLs from the loader + override def getURLs: Array[URL] = parent match { + case u: URLClassLoader => u.getURLs + case _ => Array.empty + } + override def toString: String = s"WrappedLoader($parent)" + } + private def close(classLoader: ClassLoader): Unit = classLoader match { + case a: AutoCloseable => a.close() + case _ => + } + private case class ClassLoaderReference(key: Key, classLoader: ClassLoader) + extends SoftReference[ClassLoader]( + new WrappedLoader(classLoader), + referenceQueue + ) + def apply( + files: List[(File, Long)], + parent: ClassLoader, + mkLoader: () => ClassLoader + ): ClassLoader = { + val key = new Key(files, parent) + get(key, mkLoader) + } + override def apply(files: List[File]): ClassLoader = { + val key = new Key(files) + get(key, () => key.toClassLoader) + } + override def cachedCustomClassloader( + files: List[File], + mkLoader: () => ClassLoader + ): ClassLoader = { + val key = new Key(files) + get(key, mkLoader) + } + private[this] def get(key: Key, f: () => ClassLoader): ClassLoader = { + scalaProviderKey match { + case Some(k) if k == key => k.toClassLoader + case _ => + def addLoader(): ClassLoader = { + val ref = mkReference(key, f()) + val loader = ref.get + delegate.put(key, ref) + closeExpiredLoaders() + loader + } + lock.synchronized { + delegate.get(key) match { + case null => addLoader() + case ref => + ref.get match { + case null => addLoader() + case l => l + } + } + } + } + } + private def clear(lock: Object): Unit = { + delegate.forEach { + case (_, ClassLoaderReference(_, classLoader)) => close(classLoader) + case (_, r: Reference[ClassLoader]) => + r.get match { + case null => + case classLoader => close(classLoader) + } + case (_, _) => + } + delegate.clear() + } + + /** + * Clears any ClassLoader instances from the internal cache and closes them. Calling this + * method will not stop the cleanup thread. Call [[close]] to fully clean up this cache. + */ + def clear(): Unit = lock.synchronized(clear(lock)) + + /** + * Completely shuts down this cache. It stops the background thread for cleaning up classloaders + * + * Clears any ClassLoader instances from the internal cache and closes them. It also + * method will not stop the cleanup thread. Call [[close]] to fully clean up this cache. + */ + override def close(): Unit = lock.synchronized { + cleanupThread.interrupt() + cleanupThread.join() + clear(lock) + } +} + +private[sbt] object AlternativeZincUtil { + def scalaCompiler( + scalaInstance: ScalaInstance, + compilerBridgeJar: File, + classpathOptions: ClasspathOptions, + classLoaderCache: Option[IncClassLoaderCache] + ): AnalyzingCompiler = { + val bridgeProvider = ZincUtil.constantBridgeProvider(scalaInstance, compilerBridgeJar) + new AnalyzingCompiler( + scalaInstance, + bridgeProvider, + classpathOptions, + _ => (), + classLoaderCache + ) + } +} diff --git a/main/src/test/scala/sbt/internal/ClassLoaderCacheTest.scala b/main-command/src/test/scala/sbt/internal/ClassLoaderCacheTest.scala similarity index 58% rename from main/src/test/scala/sbt/internal/ClassLoaderCacheTest.scala rename to main-command/src/test/scala/sbt/internal/ClassLoaderCacheTest.scala index 201964666..eec114f40 100644 --- a/main/src/test/scala/sbt/internal/ClassLoaderCacheTest.scala +++ b/main-command/src/test/scala/sbt/internal/ClassLoaderCacheTest.scala @@ -11,56 +11,43 @@ import java.io.File import java.nio.file.Files import org.scalatest.{ FlatSpec, Matchers } +import sbt.internal.classpath.ClassLoaderCache import sbt.io.IO object ClassLoaderCacheTest { - private val initLoader = this.getClass.getClassLoader implicit class CacheOps(val c: ClassLoaderCache) { - def get(classpath: Seq[File]): ClassLoader = - c.get((classpath, initLoader, Map.empty, new File("/dev/null"))) + def get(classpath: Seq[File]): ClassLoader = c(classpath.toList) } } class ClassLoaderCacheTest extends FlatSpec with Matchers { import ClassLoaderCacheTest._ - def withCache[R](size: Int)(f: CacheOps => R): R = { - val cache = ClassLoaderCache(size) - try f(new CacheOps(cache)) + private def withCache[R](f: ClassLoaderCache => R): R = { + val cache = new ClassLoaderCache(ClassLoader.getSystemClassLoader) + try f(cache) finally cache.close() } - "ClassLoaderCache.get" should "make a new loader when full" in withCache(0) { cache => + "ClassLoaderCache" should "make a new loader when full" in withCache { cache => val classPath = Seq.empty[File] val firstLoader = cache.get(classPath) + cache.clear() val secondLoader = cache.get(classPath) assert(firstLoader != secondLoader) } - it should "not make a new loader when it already exists" in withCache(1) { cache => + it should "not make a new loader when it already exists" in withCache { cache => val classPath = Seq.empty[File] val firstLoader = cache.get(classPath) val secondLoader = cache.get(classPath) assert(firstLoader == secondLoader) } - it should "evict loaders" in withCache(2) { cache => - val firstClassPath = Seq.empty[File] - val secondClassPath = new File("foo") :: Nil - val thirdClassPath = new File("foo") :: new File("bar") :: Nil - val firstLoader = cache.get(firstClassPath) - val secondLoader = cache.get(secondClassPath) - val thirdLoader = cache.get(thirdClassPath) - assert(cache.get(thirdClassPath) == thirdLoader) - assert(cache.get(secondClassPath) == secondLoader) - assert(cache.get(firstClassPath) != firstLoader) - assert(cache.get(thirdClassPath) != thirdLoader) - } "Snapshots" should "be invalidated" in IO.withTemporaryDirectory { dir => val snapshotJar = Files.createFile(dir.toPath.resolve("foo-SNAPSHOT.jar")).toFile val regularJar = Files.createFile(dir.toPath.resolve("regular.jar")).toFile - withCache(1) { cache => + withCache { cache => val jarClassPath = snapshotJar :: regularJar :: Nil val initLoader = cache.get(jarClassPath) IO.setModifiedTimeOrFalse(snapshotJar, System.currentTimeMillis + 5000L) val secondLoader = cache.get(jarClassPath) assert(initLoader != secondLoader) - assert(initLoader.getParent == secondLoader.getParent) assert(cache.get(jarClassPath) == secondLoader) assert(cache.get(jarClassPath) != initLoader) } diff --git a/main/src/main/scala/sbt/ClassLoaderLayeringStrategy.scala b/main/src/main/scala/sbt/ClassLoaderLayeringStrategy.scala index b32eb44fd..483900728 100644 --- a/main/src/main/scala/sbt/ClassLoaderLayeringStrategy.scala +++ b/main/src/main/scala/sbt/ClassLoaderLayeringStrategy.scala @@ -89,36 +89,12 @@ object ClassLoaderLayeringStrategy { case object ScalaLibrary extends ScalaLibrary /** - * Add a layer on top of the ScalaLibrary layer for the runtime jar dependencies. + * Add a layer on top of the ScalaLibrary layer for all of the task jar dependencies. */ - sealed trait RuntimeDependencies extends ScalaLibrary + sealed trait AllLibraryJars extends ScalaLibrary /** - * Add a layer on top of the ScalaLibrary layer for the runtime jar dependencies. + * Add a layer on top of the ScalaLibrary layer for all of the jar dependencies. */ - case object RuntimeDependencies extends ScalaLibrary with RuntimeDependencies - - /** - * Add a layer on top of the ScalaLibrary layer for the test jar dependencies. - */ - sealed trait TestDependencies extends ScalaLibrary - - /** - * Add a layer on top of the ScalaInstance layer for the test jar dependencies. - */ - case object TestDependencies extends ScalaLibrary with TestDependencies - - /** - * Add the TestDependencies layer on top of the RuntimeDependencies layer on top of the - * ScalaLibrary layer. This differs from TestDependencies in that it will not reload the - * runtime classpath. The drawback to using this is that if the test dependencies evict - * classes provided in the runtime layer, then tests can fail. In order for sharing the runtime - * layer to work, it is necessary to set [[Keys.bgCopyClasspath]] to false. Otherwise the - * runtime and test classpaths are completely different. - */ - case object ShareRuntimeDependenciesLayerWithTestDependencies - extends ScalaLibrary - with RuntimeDependencies - with TestDependencies - + object AllLibraryJars extends AllLibraryJars } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 93ab73c9c..ad4025b81 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -28,6 +28,7 @@ import sbt.Project.{ import sbt.Scope.{ GlobalScope, ThisScope, fillTaskAxis } import sbt.internal.CommandStrings.ExportStream import sbt.internal._ +import sbt.internal.classpath.AlternativeZincUtil import sbt.internal.inc.JavaInterfaceUtil._ import sbt.internal.inc.classpath.ClasspathFilter import sbt.internal.inc.{ ZincLmUtil, ZincUtil } @@ -148,7 +149,6 @@ object Defaults extends BuildCommon { defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq( excludeFilter :== HiddenFileFilter, pathToFileStamp :== sbt.nio.FileStamp.hash, - classLoaderCache := ClassLoaderCache(4), fileInputs :== Nil, inputFileStamper :== sbt.nio.FileStamper.Hash, outputFileStamper :== sbt.nio.FileStamper.LastModified, @@ -162,15 +162,13 @@ object Defaults extends BuildCommon { .get(sbt.nio.Keys.persistentFileStampCache) .getOrElse(new sbt.nio.FileStamp.Cache) }, - ) ++ TaskRepository - .proxy(GlobalScope / classLoaderCache, ClassLoaderCache(4)) ++ globalIvyCore ++ globalJvmCore + ) ++ globalIvyCore ++ globalJvmCore ) ++ globalSbtCore private[sbt] lazy val globalJvmCore: Seq[Setting[_]] = Seq( compilerCache := state.value get Keys.stateCompilerCache getOrElse CompilerCache.fresh, - classLoaderLayeringStrategy :== ClassLoaderLayeringStrategy.RuntimeDependencies, - classLoaderLayeringStrategy in Test :== ClassLoaderLayeringStrategy.TestDependencies, + classLoaderLayeringStrategy :== ClassLoaderLayeringStrategy.AllLibraryJars, sourcesInBase :== true, autoAPIMappings := false, apiMappings := Map.empty, @@ -550,10 +548,11 @@ object Defaults extends BuildCommon { val scalac = scalaCompilerBridgeBinaryJar.value match { case Some(jar) => - ZincUtil.scalaCompiler( + AlternativeZincUtil.scalaCompiler( scalaInstance = scalaInstance.value, classpathOptions = classpathOptions.value, - compilerBridgeJar = jar + compilerBridgeJar = jar, + classLoaderCache = st.get(BasicKeys.classLoaderCache) ) case _ => ZincLmUtil.scalaCompiler( @@ -565,6 +564,7 @@ object Defaults extends BuildCommon { dependencyResolution = dr, compilerBridgeSource = scalaCompilerBridgeSource.value, scalaJarsTarget = zincDir, + classLoaderCache = st.get(BasicKeys.classLoaderCache), log = streams.value.log ) } @@ -594,7 +594,7 @@ object Defaults extends BuildCommon { compileInputsSettings ) ++ configGlobal ++ defaultCompileSettings ++ compileAnalysisSettings ++ Seq( clean := Clean.task(ThisScope, full = false).value, - fileOutputs := Seq(Glob(classDirectory.value, RecursiveGlob / "*.class")), + fileOutputs in compile := Seq(Glob(classDirectory.value, RecursiveGlob / "*.class")), compile := compileTask.value, internalDependencyConfigurations := InternalDependencies.configurations.value, manipulateBytecode := compileIncremental.value, @@ -710,8 +710,16 @@ object Defaults extends BuildCommon { val scalaProvider = appConfiguration.value.provider.scalaProvider val version = scalaVersion.value if (version == scalaProvider.version) // use the same class loader as the Scala classes used by sbt - Def.task(ScalaInstance(version, scalaProvider)) - else + Def.task { + val allJars = scalaProvider.jars + val libraryJars = allJars.filter(_.getName == "scala-library.jar") + allJars.filter(_.getName == "scala-compiler.jar") match { + case Array(compilerJar) if libraryJars.nonEmpty => + val cache = state.value.classLoaderCache + mkScalaInstance(version, allJars, libraryJars, compilerJar, cache) + case _ => ScalaInstance(version, scalaProvider) + } + } else scalaInstanceFromUpdate } } @@ -745,20 +753,51 @@ object Defaults extends BuildCommon { val allJars = toolReport.modules.flatMap(_.artifacts.map(_._2)) val libraryJar = file(ScalaArtifacts.LibraryID) val compilerJar = file(ScalaArtifacts.CompilerID) - new ScalaInstance( + mkScalaInstance( scalaVersion.value, - makeClassLoader(state.value)(allJars.toList), - makeClassLoader(state.value)(List(libraryJar)), - libraryJar, + allJars, + Array(libraryJar), + compilerJar, + state.value.classLoaderCache + ) + } + private[this] def mkScalaInstance( + version: String, + allJars: Seq[File], + libraryJars: Array[File], + compilerJar: File, + classLoaderCache: sbt.internal.inc.classpath.ClassLoaderCache + ): ScalaInstance = { + val libraryLoader = classLoaderCache(libraryJars.toList) + class ScalaLoader extends URLClassLoader(allJars.map(_.toURI.toURL).toArray, libraryLoader) + val fullLoader = classLoaderCache.cachedCustomClassloader( + allJars.toList, + () => new ScalaLoader + ) + new ScalaInstance( + version, + fullLoader, + libraryLoader, + libraryJars, compilerJar, allJars.toArray, - None + Some(version) ) } def scalaInstanceFromHome(dir: File): Initialize[Task[ScalaInstance]] = Def.task { - ScalaInstance(dir)(makeClassLoader(state.value)) + val dummy = ScalaInstance(dir)(state.value.classLoaderCache.apply) + Seq(dummy.loader, dummy.loaderLibraryOnly).foreach { + case a: AutoCloseable => a.close() + case cl => + } + mkScalaInstance( + dummy.version, + dummy.allJars, + dummy.libraryJars, + dummy.compilerJar, + state.value.classLoaderCache + ) } - private[this] def makeClassLoader(state: State) = state.classLoaderCache.apply _ private[this] def testDefaults = Defaults.globalDefaults( @@ -1019,7 +1058,7 @@ object Defaults extends BuildCommon { cp, forkedParallelExecution = false, javaOptions = Nil, - strategy = ClassLoaderLayeringStrategy.TestDependencies, + strategy = ClassLoaderLayeringStrategy.AllLibraryJars, projectId = "", ) } @@ -1042,7 +1081,7 @@ object Defaults extends BuildCommon { cp, forkedParallelExecution, javaOptions = Nil, - strategy = ClassLoaderLayeringStrategy.TestDependencies, + strategy = ClassLoaderLayeringStrategy.AllLibraryJars, projectId = "", ) } @@ -1823,34 +1862,9 @@ object Defaults extends BuildCommon { Classpaths.compilerPluginConfig ++ deprecationSettings lazy val compileSettings: Seq[Setting[_]] = - configSettings ++ (mainBgRunMainTask +: mainBgRunTask) ++ - Classpaths.addUnmanagedLibrary ++ - Vector( - TaskRepository.proxy( - Compile / classLoaderCache, - // We need a cache of size four so that the subset of the runtime dependencies that are used - // by the test task layers may be cached without evicting the runtime classloader layers. The - // cache size should be a multiple of two to support snapshot layers. - ClassLoaderCache(4) - ), - bgCopyClasspath in bgRun := { - val old = (bgCopyClasspath in bgRun).value - old && (Test / classLoaderLayeringStrategy).value != ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies - }, - bgCopyClasspath in bgRunMain := { - val old = (bgCopyClasspath in bgRunMain).value - old && (Test / classLoaderLayeringStrategy).value != ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies - }, - ) + configSettings ++ (mainBgRunMainTask +: mainBgRunTask) ++ Classpaths.addUnmanagedLibrary - lazy val testSettings: Seq[Setting[_]] = configSettings ++ testTasks ++ - Vector( - TaskRepository.proxy( - Test / classLoaderCache, - // We need a cache of size two for the test dependency layers (regular and snapshot). - ClassLoaderCache(2) - ) - ) + lazy val testSettings: Seq[Setting[_]] = configSettings ++ testTasks lazy val itSettings: Seq[Setting[_]] = inConfig(IntegrationTest) { testSettings @@ -1959,7 +1973,8 @@ object Classpaths { includeFilter in unmanagedJars value, excludeFilter in unmanagedJars value ) - ).map(exportClasspath) + ).map(exportClasspath) :+ + (sbt.nio.Keys.classpathFiles := data(fullClasspath.value).map(_.toPath)) private[this] def exportClasspath(s: Setting[Task[Classpath]]): Setting[Task[Classpath]] = s.mapInitialize(init => Def.task { exportClasspath(streams.value, init.value) }) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index fcf162d7a..467af30ad 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -485,8 +485,6 @@ object Keys { val resolvedScoped = Def.resolvedScoped val pluginData = taskKey[PluginData]("Information from the plugin build needed in the main build definition.").withRank(DTask) val globalPluginUpdate = taskKey[UpdateReport]("A hook to get the UpdateReport of the global plugin.").withRank(DTask) - private[sbt] val classLoaderCache = taskKey[internal.ClassLoaderCache]("The cache of ClassLoaders to be used for layering in tasks that invoke other java code").withRank(DTask) - private[sbt] val taskRepository = AttributeKey[TaskRepository.Repr]("task-repository", "A repository that can be used to cache arbitrary values for a given task key that can be read or filled during task evaluation.", 10000) private[sbt] val taskCancelStrategy = settingKey[State => TaskCancellationStrategy]("Experimental task cancellation handler.").withRank(DTask) // Experimental in sbt 0.13.2 to enable grabbing semantic compile failures. diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 3d350fc64..54b8b5216 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -8,7 +8,6 @@ package sbt import java.io.{ File, IOException } -import java.lang.reflect.InvocationTargetException import java.net.URI import java.util.concurrent.atomic.AtomicBoolean import java.util.{ Locale, Properties } @@ -27,7 +26,6 @@ import sbt.io._ import sbt.io.syntax._ import sbt.util.{ Level, Logger, Show } import xsbti.compile.CompilerCache -import xsbti.{ AppMain, AppProvider, ComponentProvider, Launcher, ScalaProvider } import scala.annotation.tailrec import scala.concurrent.ExecutionContext @@ -35,67 +33,8 @@ import scala.util.control.NonFatal /** This class is the entry point for sbt. */ final class xMain extends xsbti.AppMain { - def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = { - val modifiedConfiguration = new ModifiedConfiguration(configuration) - val loader = modifiedConfiguration.provider.loader - // No need to memoize the old class loader. It is reset by the launcher anyway. - Thread.currentThread.setContextClassLoader(loader) - val clazz = loader.loadClass("sbt.xMainImpl$") - val instance = clazz.getField("MODULE$").get(null) - val runMethod = clazz.getMethod("run", classOf[xsbti.AppConfiguration]) - try { - new Thread("sbt-load-global-instance") { - setDaemon(true) - override def run(): Unit = { - // This preloads the scala.tools.nsc.Global as a performance optimization" - loader.loadClass("sbt.internal.parser.SbtParser$").getField("MODULE$").get(null) - () - } - }.start() - runMethod.invoke(instance, modifiedConfiguration).asInstanceOf[xsbti.MainResult] - } catch { - case e: InvocationTargetException => - // This propogates xsbti.FullReload to the launcher - throw e.getCause - } finally { - loader match { - case a: AutoCloseable => a.close() - case _ => - } - } - } - /* - * Replaces the AppProvider.loader method with a new loader that puts the sbt test interface - * jar ahead of the rest of the sbt classpath in the classloading hierarchy. - */ - private class ModifiedConfiguration(val configuration: xsbti.AppConfiguration) - extends xsbti.AppConfiguration { - private[this] val metaLoader: ClassLoader = SbtMetaBuildClassLoader(configuration.provider) - - private class ModifiedAppProvider(val appProvider: AppProvider) extends AppProvider { - override def scalaProvider(): ScalaProvider = new ScalaProvider { - val delegate = configuration.provider.scalaProvider - override def launcher(): Launcher = delegate.launcher - override def version(): String = delegate.version - override def loader(): ClassLoader = metaLoader.getParent - override def jars(): Array[File] = delegate.jars - override def libraryJar(): File = delegate.libraryJar - override def compilerJar(): File = delegate.compilerJar - override def app(id: xsbti.ApplicationID): AppProvider = delegate.app(id) - } - override def id(): xsbti.ApplicationID = appProvider.id() - override def loader(): ClassLoader = metaLoader - @deprecated("Implements deprecated api", "1.3.0") - override def mainClass(): Class[_ <: AppMain] = appProvider.mainClass() - override def entryPoint(): Class[_] = appProvider.entryPoint() - override def newMain(): AppMain = appProvider.newMain() - override def mainClasspath(): Array[File] = appProvider.mainClasspath() - override def components(): ComponentProvider = appProvider.components() - } - override def arguments(): Array[String] = configuration.arguments - override def baseDirectory(): File = configuration.baseDirectory - override def provider(): AppProvider = new ModifiedAppProvider(configuration.provider) - } + def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = + new XMainConfiguration().runXMain(configuration) } private[sbt] object xMainImpl { private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = @@ -889,7 +828,8 @@ object BuiltinCommands { val session = Load.initialSession(structure, eval, s0) SessionSettings.checkSession(session, s) - registerGlobalCaches(Project.setProject(session, structure, s)) + Project + .setProject(session, structure, s) .put(sbt.nio.Keys.hasCheckedMetaBuild, new AtomicBoolean(false)) } @@ -909,23 +849,11 @@ object BuiltinCommands { } s.put(Keys.stateCompilerCache, cache) } - private[sbt] def registerGlobalCaches(s: State): State = - try { - val cleanedUp = new AtomicBoolean(false) - def cleanup(): Unit = { - s.get(Keys.taskRepository).foreach(_.close()) - () - } - cleanup() - s.addExitHook(if (cleanedUp.compareAndSet(false, true)) cleanup()) - .put(Keys.taskRepository, new TaskRepository.Repr) - } catch { - case NonFatal(_) => s - } def clearCaches: Command = { val help = Help.more(ClearCaches, ClearCachesDetailed) - Command.command(ClearCaches, help)(registerGlobalCaches _ andThen registerCompilerCache) + val f: State => State = registerCompilerCache _ andThen (_.initializeClassLoaderCache) + Command.command(ClearCaches, help)(f) } def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 => diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index e4d4aac28..623782346 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -135,14 +135,44 @@ object MainLoop { } def next(state: State): State = - ErrorHandling.wideConvert { state.process(processCommand) } match { - case Right(s) => s - case Left(t: xsbti.FullReload) => throw t - case Left(t: RebootCurrent) => throw t - case Left(Reload) => - val remaining = state.currentCommand.toList ::: state.remainingCommands - state.copy(remainingCommands = Exec("reload", None, None) :: remaining) - case Left(t) => state.handleError(t) + try { + ErrorHandling.wideConvert { + state.process(processCommand) + } match { + case Right(s) => s + case Left(t: xsbti.FullReload) => throw t + case Left(t: RebootCurrent) => throw t + case Left(Reload) => + val remaining = state.currentCommand.toList ::: state.remainingCommands + state.copy(remainingCommands = Exec("reload", None, None) :: remaining) + case Left(t) => state.handleError(t) + } + } catch { + case oom: OutOfMemoryError if oom.getMessage.contains("Metaspace") => + System.gc() // Since we're under memory pressure, see if more can be freed with a manual gc. + val isTestOrRun = state.remainingCommands.headOption.exists { exec => + val cmd = exec.commandLine + cmd.contains("test") || cmd.contains("run") + } + val isConsole = state.remainingCommands.exists(_.commandLine == "shell") || + (state.remainingCommands.last.commandLine == "iflast shell") + val testOrRunMessage = + if (!isTestOrRun) "" + else + " If this error occurred during a test or run evaluation, it can be caused by the " + + "choice of ClassLoaderLayeringStrategy. Of the available strategies, " + + "ClassLoaderLayeringStrategy.ScalaLibrary will typically use the least metaspace. " + + (if (isConsole) + " To change the layering strategy for this session, run:\n\n" + + "set ThisBuild / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy." + + "ScalaLibrary" + else "") + val msg: String = + s"Caught $oom\nTo best utilize classloader caching and to prevent file handle leaks, we" + + s"recommend running sbt without a MaxMetaspaceSize limit. $testOrRunMessage" + state.log.error(msg) + state.log.error("\n") + state.handleError(oom) } /** This is the main function State transfer function of the sbt command processing. */ diff --git a/main/src/main/scala/sbt/internal/ClassLoaderCache.scala b/main/src/main/scala/sbt/internal/ClassLoaderCache.scala deleted file mode 100644 index 90f4ea53b..000000000 --- a/main/src/main/scala/sbt/internal/ClassLoaderCache.scala +++ /dev/null @@ -1,93 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal - -import java.io.File -import java.nio.file.Files - -import sbt.internal.util.TypeFunctions.Id - -import scala.annotation.tailrec - -private[sbt] sealed trait ClassLoaderCache - extends Repository[Id, (Seq[File], ClassLoader, Map[String, String], File), ClassLoader] - -private[sbt] object ClassLoaderCache { - private type Resources = Map[String, String] - private sealed trait CachedClassLoader extends ClassLoader { - def close(): Unit - } - private sealed trait StableClassLoader extends CachedClassLoader - private sealed trait SnapshotClassLoader extends CachedClassLoader - def apply(maxSize: Int): ClassLoaderCache = { - new ClassLoaderCache { - private final def mktmp(tmp: File): File = - if (maxSize > 0) Files.createTempDirectory("sbt-jni").toFile else tmp - private[this] val lruCache = - LRUCache[(JarClassPath, ClassLoader), (JarClassPath, CachedClassLoader)]( - maxSize = maxSize, - onExpire = - (_: (JarClassPath, ClassLoader), v: (JarClassPath, CachedClassLoader)) => close(v._2) - ) - override def get(info: (Seq[File], ClassLoader, Resources, File)): ClassLoader = - synchronized { - val (paths, parent, resources, tmp) = info - val key @ (keyJCP, _) = (new JarClassPath(paths), parent) - def addLoader(base: Option[StableClassLoader] = None): CachedClassLoader = { - val baseLoader = base.getOrElse { - if (keyJCP.regularJars.isEmpty) new ClassLoader(parent) with StableClassLoader { - override def close(): Unit = parent match { - case s: StableClassLoader => s.close() - case _ => () - } - override def toString: String = parent.toString - } else - new LayeredClassLoader(keyJCP.regularJars, parent, resources, mktmp(tmp)) - with StableClassLoader - } - val loader: CachedClassLoader = - if (keyJCP.snapshotJars.isEmpty) baseLoader - else - new LayeredClassLoader(keyJCP.snapshotJars, baseLoader, resources, mktmp(tmp)) - with SnapshotClassLoader - lruCache.put(key, keyJCP -> loader) - loader - } - lruCache.get(key) match { - case Some((jcp, cl)) if keyJCP.strictEquals(jcp) => cl - case Some((_, cl: SnapshotClassLoader)) => - cl.close() - cl.getParent match { - case p: StableClassLoader => addLoader(Some(p)) - case _ => addLoader() - } - case None => addLoader() - } - } - override def close(): Unit = synchronized(lruCache.close()) - override def toString: String = { - import PrettyPrint.indent - val cacheElements = lruCache.entries.map { - case ((jcp, parent), (_, l)) => - s"(\n${indent(jcp, 4)},\n${indent(parent, 4)}\n) =>\n $l" - } - s"ClassLoaderCache(\n size = $maxSize,\n elements =\n${indent(cacheElements, 4)}\n)" - } - - // Close the ClassLoader and all of it's closeable parents. - @tailrec - private def close(loader: CachedClassLoader): Unit = { - loader.close() - loader.getParent match { - case c: CachedClassLoader => close(c) - case _ => () - } - } - } - } -} diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala index d56290d1e..37ace0f72 100644 --- a/main/src/main/scala/sbt/internal/ClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala @@ -9,18 +9,20 @@ package sbt package internal import java.io.File -import java.net.{ URL, URLClassLoader } +import java.net.URLClassLoader +import java.nio.file.Path import sbt.ClassLoaderLayeringStrategy._ import sbt.Keys._ -import sbt.SlashSyntax0._ +import sbt.internal.classpath.ClassLoaderCache import sbt.internal.inc.ScalaInstance import sbt.internal.inc.classpath.ClasspathUtilities import sbt.internal.util.Attributed import sbt.internal.util.Attributed.data import sbt.io.IO -import sbt.librarymanagement.Configurations.{ Runtime, Test } -import xsbti.AppProvider +import sbt.nio.FileStamp +import sbt.nio.FileStamp.LastModified +import sbt.nio.Keys._ private[sbt] object ClassLoaders { private[this] val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader @@ -29,20 +31,20 @@ private[sbt] object ClassLoaders { */ private[sbt] def testTask: Def.Initialize[Task[ClassLoader]] = Def.task { val si = scalaInstance.value - val rawCP = data(fullClasspath.value) - val fullCP = if (si.isManagedVersion) rawCP else List(si.libraryJar) ++ rawCP - val exclude = dependencyJars(exportedProducts).value.toSet ++ Set(si.libraryJar) + val rawCP = modifiedTimes((outputFileStamps in classpathFiles).value) + val fullCP = + if (si.isManagedVersion) rawCP + else si.libraryJars.map(j => j -> IO.getModifiedTimeOrZero(j)).toSeq ++ rawCP + val exclude = dependencyJars(exportedProducts).value.toSet ++ si.libraryJars + val resourceCP = modifiedTimes((outputFileStamps in resources).value) buildLayers( strategy = classLoaderLayeringStrategy.value, si = si, fullCP = fullCP, - rawRuntimeDependencies = - dependencyJars(Runtime / dependencyClasspath).value.filterNot(exclude), + resourceCP = resourceCP, allDependencies = dependencyJars(dependencyClasspath).value.filterNot(exclude), - globalCache = (Scope.GlobalScope / classLoaderCache).value, - runtimeCache = (Runtime / classLoaderCache).value, - testCache = (Test / classLoaderCache).value, - resources = ClasspathUtilities.createClasspathResources(fullCP, si), + cache = extendedClassLoaderCache.value, + resources = ClasspathUtilities.createClasspathResources(fullCP.map(_._1), si), tmp = IO.createUniqueDirectory(taskTemporaryDirectory.value), scope = resolvedScoped.value.scope ) @@ -54,6 +56,7 @@ private[sbt] object ClassLoaders { val s = streams.value val opts = forkOptions.value val options = javaOptions.value + val resourceCP = modifiedTimes((outputFileStamps in resources).value) if (fork.value) { s.log.debug(s"javaOptions: $options") Def.task(new ForkRun(opts)) @@ -73,23 +76,17 @@ private[sbt] object ClassLoaders { ) s.log.warn(s"$showJavaOptions will be ignored, $showFork is set to false") } - val globalCache = (Scope.GlobalScope / classLoaderCache).value - val runtimeCache = (Runtime / classLoaderCache).value - val testCache = (Test / classLoaderCache).value val exclude = dependencyJars(exportedProducts).value.toSet ++ instance.allJars - val runtimeDeps = dependencyJars(Runtime / dependencyClasspath).value.filterNot(exclude) val allDeps = dependencyJars(dependencyClasspath).value.filterNot(exclude) val newLoader = (classpath: Seq[File]) => { buildLayers( strategy = classLoaderLayeringStrategy.value: @sbtUnchecked, si = instance, - fullCP = classpath, - rawRuntimeDependencies = runtimeDeps, + fullCP = classpath.map(f => f -> IO.getModifiedTimeOrZero(f)), + resourceCP = resourceCP, allDependencies = allDeps, - globalCache = globalCache, - runtimeCache = runtimeCache, - testCache = testCache, + cache = extendedClassLoaderCache.value: @sbtUnchecked, resources = ClasspathUtilities.createClasspathResources(classpath, instance), tmp = taskTemporaryDirectory.value: @sbtUnchecked, scope = resolvedScope @@ -100,77 +97,63 @@ private[sbt] object ClassLoaders { } } + private[this] def extendedClassLoaderCache: Def.Initialize[Task[ClassLoaderCache]] = Def.task { + val errorMessage = "Tried to extract classloader cache for uninitialized state." + state.value + .get(BasicKeys.extendedClassLoaderCache) + .getOrElse(throw new IllegalStateException(errorMessage)) + } /* * Create a layered classloader. There are up to four layers: * 1) the scala instance class loader - * 2) the runtime dependencies - * 3) the test dependencies + * 2) the resource layer + * 3) the dependency jars * 4) the rest of the classpath - * The first two layers may be optionally cached to reduce memory usage and improve + * The first three layers may be optionally cached to reduce memory usage and improve * start up latency. Because there may be mutually incompatible libraries in the runtime * and test dependencies, it's important to be able to configure which layers are used. */ private def buildLayers( strategy: ClassLoaderLayeringStrategy, si: ScalaInstance, - fullCP: Seq[File], - rawRuntimeDependencies: Seq[File], + fullCP: Seq[(File, Long)], + resourceCP: Seq[(File, Long)], allDependencies: Seq[File], - globalCache: ClassLoaderCache, - runtimeCache: ClassLoaderCache, - testCache: ClassLoaderCache, + cache: ClassLoaderCache, resources: Map[String, String], tmp: File, scope: Scope ): ClassLoader = { - val isTest = scope.config.toOption.map(_.name) == Option("test") + val cpFiles = fullCP.map(_._1) val raw = strategy match { - case Flat => flatLoader(fullCP, interfaceLoader) + case Flat => flatLoader(cpFiles, interfaceLoader) case _ => - val (layerDependencies, layerTestDependencies) = strategy match { - case ShareRuntimeDependenciesLayerWithTestDependencies if isTest => (true, true) - case ScalaLibrary => (false, false) - case RuntimeDependencies => (true, false) - case TestDependencies if isTest => (false, true) - case badStrategy => - val msg = s"Layering strategy $badStrategy is not valid for the classloader in " + - s"$scope. Valid options are: ClassLoaderLayeringStrategy.{ " + - "Flat, ScalaInstance, RuntimeDependencies }" - throw new IllegalArgumentException(msg) + val layerDependencies = strategy match { + case _: AllLibraryJars => true + case _ => false } val allDependenciesSet = allDependencies.toSet - // The raw declarations are to avoid having to make a dynamic task. The - // allDependencies and allTestDependencies create a mutually exclusive list of jar - // dependencies for layers 2 and 3. Note that in the Runtime or Compile configs, it - // should always be the case that allTestDependencies == Nil - val allTestDependencies = if (layerTestDependencies) allDependenciesSet else Set.empty[File] - val allRuntimeDependencies = (if (layerDependencies) rawRuntimeDependencies else Nil).toSet + val scalaLibraryLayer = layer(si.libraryJars, interfaceLoader, cache, resources, tmp) + val cpFiles = fullCP.map(_._1) - val scalaLibrarySet = Set(si.libraryJar) - val scalaLibraryLayer = - globalCache.get((scalaLibrarySet.toList, interfaceLoader, resources, tmp)) - // layer 2 - val runtimeDependencySet = allDependenciesSet intersect allRuntimeDependencies - val runtimeDependencies = rawRuntimeDependencies.filter(runtimeDependencySet) - lazy val runtimeLayer = + // layer 2 (resources) + val resourceLayer = if (layerDependencies) - layer(runtimeDependencies, scalaLibraryLayer, runtimeCache, resources, tmp) + getResourceLayer(cpFiles, resourceCP, scalaLibraryLayer, cache, resources) else scalaLibraryLayer - // layer 3 (optional if testDependencies are empty) - val testDependencySet = allTestDependencies diff runtimeDependencySet - val testDependencies = allDependencies.filter(testDependencySet) - val testLayer = layer(testDependencies, runtimeLayer, testCache, resources, tmp) + // layer 3 (optional if in the test config and the runtime layer is not shared) + val dependencyLayer = + if (layerDependencies) layer(allDependencies, resourceLayer, cache, resources, tmp) + else resourceLayer // layer 4 - val dynamicClasspath = - fullCP.filterNot(testDependencySet ++ runtimeDependencies ++ scalaLibrarySet) - if (dynamicClasspath.nonEmpty) - new LayeredClassLoader(dynamicClasspath, testLayer, resources, tmp) - else testLayer + val dynamicClasspath = cpFiles.filterNot(allDependenciesSet ++ si.libraryJars) + new LayeredClassLoader(dynamicClasspath, dependencyLayer, resources, tmp) } - ClasspathUtilities.filterByClasspath(fullCP, raw) + ClasspathUtilities.filterByClasspath(cpFiles, raw) } + private def dependencyJars( key: sbt.TaskKey[Seq[Attributed[File]]] ): Def.Initialize[Task[Seq[File]]] = Def.task(data(key.value).filter(_.getName.endsWith(".jar"))) @@ -187,39 +170,60 @@ private[sbt] object ClassLoaders { resources: Map[String, String], tmp: File ): ClassLoader = { - val (snapshots, jars) = classpath.partition(_.toString.contains("-SNAPSHOT")) - val jarLoader = if (jars.isEmpty) parent else cache.get((jars, parent, resources, tmp)) - if (snapshots.isEmpty) jarLoader else cache.get((snapshots, jarLoader, resources, tmp)) + if (classpath.nonEmpty) { + cache( + classpath.toList.map(f => f -> IO.getModifiedTimeOrZero(f)), + parent, + () => new LayeredClassLoader(classpath, parent, resources, tmp) + ) + } else parent } + private class ResourceLoader( + classpath: Seq[File], + parent: ClassLoader, + resources: Map[String, String] + ) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) { + override def loadClass(name: String, resolve: Boolean): Class[_] = { + val clazz = parent.loadClass(name) + if (resolve) resolveClass(clazz) + clazz + } + override def toString: String = "ResourceLoader" + } + // Creates a one or two layered classloader for the provided classpaths depending on whether + // or not the classpath contains any snapshots. If it does, the snapshots are placed in a layer + // above the regular jar layer. This allows the snapshot layer to be invalidated without + // invalidating the regular jar layer. If the classpath is empty, it just returns the parent + // loader. + private def getResourceLayer( + classpath: Seq[File], + resources: Seq[(File, Long)], + parent: ClassLoader, + cache: ClassLoaderCache, + resourceMap: Map[String, String] + ): ClassLoader = { + if (resources.nonEmpty) { + cache( + resources.toList, + parent, + () => new ResourceLoader(classpath, parent, resourceMap) + ) + } else parent + } + + private[this] class FlatLoader(classpath: Seq[File], parent: ClassLoader) + extends URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent) { + override def toString: String = + s"FlatClassLoader(parent = $interfaceLoader, jars =\n${classpath.mkString("\n")}\n)" + } // helper methods private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader = - new URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent) { - override def toString: String = - s"FlatClassLoader(parent = $interfaceLoader, jars =\n${classpath.mkString("\n")}\n)" - } - -} - -private[sbt] object SbtMetaBuildClassLoader { - def apply(appProvider: AppProvider): ClassLoader = { - val interfaceFilter: URL => Boolean = _.getFile.endsWith("test-interface-1.0.jar") - def urls(jars: Array[File]): Array[URL] = jars.map(_.toURI.toURL) - val (interfaceURL, rest) = urls(appProvider.mainClasspath).partition(interfaceFilter) - val scalaProvider = appProvider.scalaProvider - val interfaceLoader = new URLClassLoader(interfaceURL, scalaProvider.launcher.topLoader) { - override def toString: String = s"SbtTestInterfaceClassLoader(${getURLs.head})" - } - val updatedLibraryLoader = new URLClassLoader(urls(scalaProvider.jars), interfaceLoader) { - override def toString: String = s"ScalaClassLoader(jars = {${getURLs.mkString(", ")}}" - } - new URLClassLoader(rest, updatedLibraryLoader) { - override def toString: String = s"SbtMetaBuildClassLoader" - override def close(): Unit = { - super.close() - updatedLibraryLoader.close() - interfaceLoader.close() - } - } + new FlatLoader(classpath, parent) + private[this] def modifiedTimes(stamps: Seq[(Path, FileStamp)]): Seq[(File, Long)] = stamps.map { + case (p, LastModified(lm)) => p.toFile -> lm + case (p, _) => + val f = p.toFile + f -> IO.getModifiedTimeOrZero(f) } } diff --git a/main/src/main/scala/sbt/internal/ConsoleProject.scala b/main/src/main/scala/sbt/internal/ConsoleProject.scala index fe565ce8b..737bd8609 100644 --- a/main/src/main/scala/sbt/internal/ConsoleProject.scala +++ b/main/src/main/scala/sbt/internal/ConsoleProject.scala @@ -8,9 +8,10 @@ package sbt package internal -import sbt.util.Logger +import sbt.internal.classpath.AlternativeZincUtil +import sbt.internal.inc.{ ScalaInstance, ZincLmUtil } import sbt.internal.util.JLine -import sbt.internal.inc.{ ScalaInstance, ZincLmUtil, ZincUtil } +import sbt.util.Logger import xsbti.compile.ClasspathOptionsUtil object ConsoleProject { @@ -35,10 +36,11 @@ object ConsoleProject { val launcher = app.provider.scalaProvider.launcher val compiler = scalaCompilerBridgeBinaryJar match { case Some(jar) => - ZincUtil.scalaCompiler( + AlternativeZincUtil.scalaCompiler( scalaInstance = scalaInstance, classpathOptions = ClasspathOptionsUtil.repl, - compilerBridgeJar = jar + compilerBridgeJar = jar, + classLoaderCache = state1.get(BasicKeys.classLoaderCache) ) case None => ZincLmUtil.scalaCompiler( @@ -50,6 +52,7 @@ object ConsoleProject { dependencyResolution = dependencyResolution, compilerBridgeSource = extracted.get(Keys.scalaCompilerBridgeSource), scalaJarsTarget = zincDir, + classLoaderCache = state1.get(BasicKeys.classLoaderCache), log = log ) } diff --git a/main/src/main/scala/sbt/internal/LRUCache.scala b/main/src/main/scala/sbt/internal/LRUCache.scala deleted file mode 100644 index a80a2837a..000000000 --- a/main/src/main/scala/sbt/internal/LRUCache.scala +++ /dev/null @@ -1,113 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal - -import java.util.concurrent.atomic.AtomicInteger - -import scala.annotation.tailrec - -private[sbt] sealed trait LRUCache[K, V] extends AutoCloseable { - def get(key: K): Option[V] - def entries: Seq[(K, V)] - def maxSize: Int - def put(key: K, value: V): Option[V] - def remove(key: K): Option[V] - def size: Int -} - -private[sbt] object LRUCache { - private[this] class impl[K, V](override val maxSize: Int, onExpire: Option[((K, V)) => Unit]) - extends LRUCache[K, V] { - private[this] val elementsSortedByAccess: Array[(K, V)] = new Array[(K, V)](maxSize) - private[this] val lastIndex: AtomicInteger = new AtomicInteger(-1) - - override def close(): Unit = this.synchronized { - val f = onExpire.getOrElse((_: (K, V)) => Unit) - 0 until maxSize foreach { i => - elementsSortedByAccess(i) match { - case null => - case el => f(el) - } - elementsSortedByAccess(i) = null - } - lastIndex.set(-1) - } - override def entries: Seq[(K, V)] = this.synchronized { - (0 to lastIndex.get()).map(elementsSortedByAccess) - } - override def get(key: K): Option[V] = this.synchronized { - indexOf(key) match { - case -1 => None - case i => replace(i, key, elementsSortedByAccess(i)._2) - } - } - override def put(key: K, value: V): Option[V] = this.synchronized { - indexOf(key) match { - case -1 => - append(key, value) - None - case i => replace(i, key, value) - } - } - override def remove(key: K): Option[V] = this.synchronized { - indexOf(key) match { - case -1 => None - case i => remove(i, lastIndex.get, expire = false) - } - } - override def size: Int = lastIndex.get + 1 - override def toString: String = { - val values = 0 to lastIndex.get() map { i => - val (key, value) = elementsSortedByAccess(i) - s"$key -> $value" - } - s"LRUCache(${values mkString ", "})" - } - - private def indexOf(key: K): Int = - elementsSortedByAccess.view.take(lastIndex.get() + 1).indexWhere(_._1 == key) - private def replace(index: Int, key: K, value: V): Option[V] = { - val prev = remove(index, lastIndex.get(), expire = false) - append(key, value) - prev - } - private def append(key: K, value: V): Unit = { - while (lastIndex.get() >= maxSize - 1) { - remove(0, lastIndex.get(), expire = true) - } - val index = lastIndex.incrementAndGet() - elementsSortedByAccess(index) = (key, value) - } - private def remove(index: Int, endIndex: Int, expire: Boolean): Option[V] = { - @tailrec - def shift(i: Int): Unit = if (i < endIndex) { - elementsSortedByAccess(i) = elementsSortedByAccess(i + 1) - shift(i + 1) - } - val prev = elementsSortedByAccess(index) - shift(index) - lastIndex.set(endIndex - 1) - if (expire) onExpire.foreach(f => f(prev)) - Some(prev._2) - } - } - private def emptyCache[K, V]: LRUCache[K, V] = new LRUCache[K, V] { - override def get(key: K): Option[V] = None - override def entries: Seq[(K, V)] = Nil - override def maxSize: Int = 0 - override def put(key: K, value: V): Option[V] = None - override def remove(key: K): Option[V] = None - override def size: Int = 0 - override def close(): Unit = {} - override def toString = "EmptyLRUCache" - } - def apply[K, V](maxSize: Int): LRUCache[K, V] = - if (maxSize > 0) new impl(maxSize, None) else emptyCache - def apply[K, V](maxSize: Int, onExpire: (K, V) => Unit): LRUCache[K, V] = - if (maxSize > 0) new impl(maxSize, Some(onExpire.tupled)) else emptyCache -} diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 743fa4a64..76741d579 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -87,6 +87,7 @@ private[sbt] object Load { dependencyResolution = dependencyResolution, compilerBridgeSource = ZincLmUtil.getDefaultBridgeModule(scalaProvider.version), scalaJarsTarget = zincDir, + state.get(BasicKeys.classLoaderCache), log = log ) val compilers = ZincUtil.compilers( @@ -374,8 +375,9 @@ private[sbt] object Load { val projectSettings = build.defined flatMap { case (id, project) => val ref = ProjectRef(uri, id) - val defineConfig: Seq[Setting[_]] = for (c <- project.configurations) - yield ((configuration in (ref, ConfigKey(c.name))) :== c) + val defineConfig: Seq[Setting[_]] = + for (c <- project.configurations) + yield ((configuration in (ref, ConfigKey(c.name))) :== c) val builtin: Seq[Setting[_]] = (thisProject :== project) +: (thisProjectRef :== ref) +: defineConfig val settings = builtin ++ project.settings ++ injectSettings.project diff --git a/main/src/main/scala/sbt/internal/LogManager.scala b/main/src/main/scala/sbt/internal/LogManager.scala index 12678337f..95ebcab7a 100644 --- a/main/src/main/scala/sbt/internal/LogManager.scala +++ b/main/src/main/scala/sbt/internal/LogManager.scala @@ -174,7 +174,7 @@ object LogManager { key: ScopedKey[_], state: State ): SuppressedTraceContext => Option[String] = { - lazy val display = Project.showContextKey(state) + val display = Project.showContextKey(state) def commandBase = "last " + display.show(unwrapStreamsKey(key)) def command(useFormat: Boolean) = if (useFormat) BLUE + commandBase + RESET else s"'$commandBase'" diff --git a/main/src/main/scala/sbt/internal/Repository.scala b/main/src/main/scala/sbt/internal/Repository.scala deleted file mode 100644 index 7d0b0f82f..000000000 --- a/main/src/main/scala/sbt/internal/Repository.scala +++ /dev/null @@ -1,59 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal - -import java.util.concurrent.ConcurrentHashMap - -import scala.collection.JavaConverters._ - -/** - * Represents an abstract cache of values, accessible by a key. The interface is deliberately - * minimal to give maximum flexibility to the implementation classes. For example, one can construct - * a cache from a `sbt.io.FileTreeRepository` that automatically registers the paths with the - * cache (but does not clear the cache on close): - * {{{ - * val repository = sbt.io.FileTreeRepository.default(_.getPath) - * val fileCache = new Repository[Seq, (Path, Boolean), TypedPath] { - * override def get(key: (Path, Boolean)): Seq[TypedPath] = { - * val (path, recursive) = key - * val depth = if (recursive) Int.MaxValue else 0 - * repository.register(path, depth) - * repository.list(path, depth, AllPass) - * } - * override def close(): Unit = {} - * } - * }}} - * - * @tparam M the container type of the cache. This will most commonly be `Option` or `Seq`. - * @tparam K the key type - * @tparam V the value type - */ -private[sbt] trait Repository[M[_], K, V] extends AutoCloseable { - def get(key: K): M[V] - override def close(): Unit = {} -} - -private[sbt] final class MutableRepository[K, V] extends Repository[Option, K, V] { - private[this] val map = new ConcurrentHashMap[K, V].asScala - override def get(key: K): Option[V] = map.get(key) - def put(key: K, value: V): Unit = this.synchronized { - map.put(key, value) - () - } - def remove(key: K): Unit = this.synchronized { - map.remove(key) - () - } - override def close(): Unit = this.synchronized { - map.foreach { - case (_, v: AutoCloseable) => v.close() - case _ => - } - map.clear() - } -} diff --git a/main/src/main/scala/sbt/internal/TaskRepository.scala b/main/src/main/scala/sbt/internal/TaskRepository.scala deleted file mode 100644 index ada63172d..000000000 --- a/main/src/main/scala/sbt/internal/TaskRepository.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal - -import sbt.Keys.state -import sbt._ - -private[sbt] object TaskRepository { - private[sbt] type Repr = MutableRepository[TaskKey[_], Any] - private[sbt] def proxy[T: Manifest](taskKey: TaskKey[T], task: => T): Def.Setting[Task[T]] = - proxy(taskKey, Def.task(task)) - private[sbt] def proxy[T: Manifest]( - taskKey: TaskKey[T], - task: Def.Initialize[Task[T]] - ): Def.Setting[Task[T]] = - taskKey := Def.taskDyn { - val taskRepository = state.value - .get(Keys.taskRepository) - .getOrElse { - val msg = "TaskRepository.proxy called before state was initialized" - throw new IllegalStateException(msg) - } - taskRepository.get(taskKey) match { - case Some(value: T) => Def.task(value) - case _ => - Def.task { - val value = task.value - taskRepository.put(taskKey, value) - value - } - } - }.value -} diff --git a/main/src/main/scala/sbt/internal/XMainConfiguration.scala b/main/src/main/scala/sbt/internal/XMainConfiguration.scala new file mode 100644 index 000000000..e0a6482e8 --- /dev/null +++ b/main/src/main/scala/sbt/internal/XMainConfiguration.scala @@ -0,0 +1,236 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal + +import java.io.File +import java.lang.reflect.InvocationTargetException +import java.net.{ URL, URLClassLoader } +import java.util.regex.Pattern + +import xsbti._ + +/** + * Generates a new app configuration and invokes xMainImpl.run. For AppConfigurations generated + * by recent launchers, it is unnecessary to modify the original configuration, but configurations + * generated by older launchers need to be modified to place the test interface jar higher in + * the class hierarchy. The methods this object are implemented without using the scala library + * so that we can avoid loading any classes from the old scala provider. Verified as of + * sbt 1.3.0 that there are no references to the scala standard library in any of the methods + * in this file. + */ +private[sbt] class XMainConfiguration { + private def close(classLoader: ClassLoader): Unit = classLoader match { + case a: AutoCloseable => a.close() + case _ => + } + def runXMain(configuration: xsbti.AppConfiguration): xsbti.MainResult = { + val updatedConfiguration = + if (configuration.provider.scalaProvider.launcher.topLoader.getClass.getCanonicalName + .contains("TestInterfaceLoader")) { + configuration + } else { + makeConfiguration(configuration) + } + val loader = updatedConfiguration.provider.loader + Thread.currentThread.setContextClassLoader(loader) + val clazz = loader.loadClass("sbt.xMainImpl$") + val instance = clazz.getField("MODULE$").get(null) + val runMethod = clazz.getMethod("run", classOf[xsbti.AppConfiguration]) + try { + loader.loadClass("sbt.internal.parser.SbtParserInit").getConstructor().newInstance() + runMethod.invoke(instance, updatedConfiguration).asInstanceOf[xsbti.MainResult] + } catch { + case e: InvocationTargetException => + // This propogates xsbti.FullReload to the launcher + throw e.getCause + } + } + + private def makeConfiguration(configuration: xsbti.AppConfiguration): xsbti.AppConfiguration = { + val baseLoader = classOf[XMainConfiguration].getClassLoader + val url = baseLoader.getResource("sbt/internal/XMainConfiguration.class") + val urlArray = new Array[URL](1) + urlArray(0) = new URL(url.getPath.replaceAll("[!][^!]*class", "")) + val topLoader = configuration.provider.scalaProvider.launcher.topLoader + // This loader doesn't have the scala library in it so it's critical that none of the code + // in this file use the scala library. + val modifiedLoader = new URLClassLoader(urlArray, topLoader) { + override def loadClass(name: String, resolve: Boolean): Class[_] = { + if (name.startsWith("sbt.internal.XMainConfiguration")) { + val clazz = findClass(name) + if (resolve) resolveClass(clazz) + clazz + } else { + super.loadClass(name, resolve) + } + } + } + val xMainConfigurationClass = modifiedLoader.loadClass("sbt.internal.XMainConfiguration") + val instance: AnyRef = + xMainConfigurationClass.getConstructor().newInstance().asInstanceOf[AnyRef] + + val method = xMainConfigurationClass.getMethod("makeLoader", classOf[AppProvider]) + val modifiedConfigurationClass = + modifiedLoader.loadClass("sbt.internal.XMainConfiguration$ModifiedConfiguration") + + val loader = method.invoke(instance, configuration.provider).asInstanceOf[ClassLoader] + + Thread.currentThread.setContextClassLoader(loader) + val cons = modifiedConfigurationClass.getConstructors()(0) + close(configuration.provider.loader) + val scalaProvider = configuration.provider.scalaProvider + val providerClass = scalaProvider.getClass + val _ = try { + val method = providerClass.getMethod("loaderLibraryOnly") + close(method.invoke(scalaProvider).asInstanceOf[ClassLoader]) + 1 + } catch { case _: NoSuchMethodException => 1 } + close(scalaProvider.loader) + close(configuration.provider.loader) + cons.newInstance(instance, configuration, loader).asInstanceOf[xsbti.AppConfiguration] + } + + /* + * Replaces the AppProvider.loader method with a new loader that puts the sbt test interface + * jar ahead of the rest of the sbt classpath in the classloading hierarchy. + */ + private[sbt] class ModifiedConfiguration( + val configuration: xsbti.AppConfiguration, + val metaLoader: ClassLoader + ) extends xsbti.AppConfiguration { + + private class ModifiedAppProvider(val appProvider: AppProvider) extends AppProvider { + private val delegate = configuration.provider.scalaProvider + object ModifiedScalaProvider extends ScalaProvider { + override def launcher(): Launcher = new Launcher { + private val delegateLauncher = delegate.launcher + private val interfaceLoader = metaLoader.loadClass("sbt.testing.Framework").getClassLoader + override def getScala(version: String): ScalaProvider = getScala(version, "") + override def getScala(version: String, reason: String): ScalaProvider = + getScala(version, reason, "org.scala-lang") + override def getScala(version: String, reason: String, scalaOrg: String): ScalaProvider = + delegateLauncher.getScala(version, reason, scalaOrg) + override def app(id: xsbti.ApplicationID, version: String): AppProvider = + delegateLauncher.app(id, version) + override def topLoader(): ClassLoader = interfaceLoader + override def globalLock(): GlobalLock = delegateLauncher.globalLock() + override def bootDirectory(): File = delegateLauncher.bootDirectory() + override def ivyRepositories(): Array[xsbti.Repository] = + delegateLauncher.ivyRepositories() + override def appRepositories(): Array[xsbti.Repository] = + delegateLauncher.appRepositories() + override def isOverrideRepositories: Boolean = delegateLauncher.isOverrideRepositories + override def ivyHome(): File = delegateLauncher.ivyHome() + override def checksums(): Array[String] = delegateLauncher.checksums() + } + override def version(): String = delegate.version + override def loader(): ClassLoader = metaLoader.getParent + override def jars(): Array[File] = delegate.jars + @deprecated("Implements deprecated api", "1.3.0") + override def libraryJar(): File = delegate.libraryJar + @deprecated("Implements deprecated api", "1.3.0") + override def compilerJar(): File = delegate.compilerJar + override def app(id: xsbti.ApplicationID): AppProvider = delegate.app(id) + def loaderLibraryOnly(): ClassLoader = metaLoader.getParent.getParent + } + override def scalaProvider(): ModifiedScalaProvider.type = ModifiedScalaProvider + override def id(): xsbti.ApplicationID = appProvider.id() + override def loader(): ClassLoader = metaLoader + @deprecated("Implements deprecated api", "1.3.0") + override def mainClass(): Class[_ <: AppMain] = appProvider.mainClass() + override def entryPoint(): Class[_] = appProvider.entryPoint() + override def newMain(): AppMain = appProvider.newMain() + override def mainClasspath(): Array[File] = appProvider.mainClasspath() + override def components(): ComponentProvider = appProvider.components() + } + override def arguments(): Array[String] = configuration.arguments + override def baseDirectory(): File = configuration.baseDirectory + override def provider(): AppProvider = new ModifiedAppProvider(configuration.provider) + } + + /** + * Rearrange the classloaders so that test-interface is above the scala library. Implemented + * without using the scala standard library to minimize classloading. + * @param appProvider the appProvider that needs to be modified + * @return a ClassLoader with a URLClassLoader for the test-interface-1.0.jar above the + * scala library. + */ + private[sbt] def makeLoader(appProvider: AppProvider): ClassLoader = { + val pattern = Pattern.compile("test-interface-[0-9.]+\\.jar") + val cp = appProvider.mainClasspath + val interfaceURL = new Array[URL](1) + val rest = new Array[URL](cp.length - 1) + + { + var i = 0 + var j = 0 // index into rest + while (i < cp.length) { + val file = cp(i) + if (pattern.matcher(file.getName).find()) { + interfaceURL(0) = file.toURI.toURL + } else { + rest(j) = file.toURI.toURL + j += 1 + } + i += 1 + } + } + val scalaProvider = appProvider.scalaProvider + val topLoader = scalaProvider.launcher.topLoader + class InterfaceLoader extends URLClassLoader(interfaceURL, topLoader) { + override def toString: String = "SbtTestInterfaceClassLoader(" + interfaceURL(0) + ")" + } + val interfaceLoader = new InterfaceLoader + val siJars = scalaProvider.jars + val lib = new Array[URL](1) + val scalaRest = new Array[URL](siJars.length - 1) + + { + var i = 0 + var j = 0 // index into scalaRest + while (i < siJars.length) { + val file = siJars(i) + if (file.getName.equals("scala-library.jar")) { + lib(0) = file.toURI.toURL + } else { + scalaRest(j) = file.toURI.toURL + j += 1 + } + i += 1 + } + } + class LibraryLoader extends URLClassLoader(lib, interfaceLoader) { + override def toString: String = "ScalaLibraryLoader( " + lib(0) + ")" + } + val libraryLoader = new LibraryLoader + class FullLoader extends URLClassLoader(scalaRest, libraryLoader) { + private val jarString: String = { + val res = new java.lang.StringBuilder + var i = 0 + while (i < scalaRest.length) { + res.append(scalaRest(i).getPath) + res.append(", ") + i += 1 + } + res.toString + } + override def toString: String = "ScalaClassLoader(jars = " + jarString + ")" + } + val fullLoader = new FullLoader + class MetaBuildLoader extends URLClassLoader(rest, fullLoader) { + override def toString: String = "SbtMetaBuildClassLoader" + override def close(): Unit = { + super.close() + libraryLoader.close() + fullLoader.close() + interfaceLoader.close() + } + } + new MetaBuildLoader + } +} diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index c6d0ac2ca..e05d81671 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -163,6 +163,16 @@ private[sbt] object SbtParser { } } +private class SbtParserInit { + new Thread("sbt-parser-init-thread") { + setDaemon(true) + start() + override def run(): Unit = { + val _ = SbtParser.defaultGlobalForParser + } + } +} + /** * This method solely exists to add scaladoc to members in SbtParser which * are defined using pattern matching. diff --git a/main/src/main/scala/sbt/nio/Keys.scala b/main/src/main/scala/sbt/nio/Keys.scala index 314528543..1d10d0dbe 100644 --- a/main/src/main/scala/sbt/nio/Keys.scala +++ b/main/src/main/scala/sbt/nio/Keys.scala @@ -148,6 +148,8 @@ object Keys { private[sbt] val pathToFileStamp = taskKey[Path => Option[FileStamp]]( "A function that computes a file stamp for a path. It may have the side effect of updating a cache." ).withRank(Invisible) + private[sbt] val classpathFiles = + taskKey[Seq[Path]]("The classpath for a task.").withRank(Invisible) private[this] val hasCheckedMetaBuildMsg = "Indicates whether or not we have called the checkBuildSources task. This is to avoid warning " + diff --git a/main/src/test/scala/sbt/internal/LRUCacheTest.scala b/main/src/test/scala/sbt/internal/LRUCacheTest.scala deleted file mode 100644 index 06add1271..000000000 --- a/main/src/test/scala/sbt/internal/LRUCacheTest.scala +++ /dev/null @@ -1,78 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt -package internal - -import java.util.concurrent.atomic.AtomicInteger - -import org.scalatest.{ FlatSpec, Matchers } - -class LRUCacheTest extends FlatSpec with Matchers { - "LRUCache" should "flush entries when full" in { - val cache = LRUCache[Int, Int](2) - cache.put(1, 1) - cache.put(2, 2) - cache.put(3, 3) - assert(cache.get(1).isEmpty) - assert(cache.get(2).contains(2)) - assert(cache.get(3).contains(3)) - - assert(cache.get(2).contains(2)) - cache.put(1, 1) - assert(cache.get(3).isEmpty) - assert(cache.get(2).contains(2)) - assert(cache.get(1).contains(1)) - } - it should "remove entries" in { - val cache = LRUCache[Int, Int](2) - cache.put(1, 1) - cache.put(2, 2) - assert(cache.get(1).contains(1)) - assert(cache.get(2).contains(2)) - - assert(cache.remove(1).getOrElse(-1) == 1) - assert(cache.get(1).isEmpty) - assert(cache.get(2).contains(2)) - } - it should "clear entries on close" in { - val cache = LRUCache[Int, Int](2) - cache.put(1, 1) - assert(cache.get(1).contains(1)) - cache.close() - assert(cache.get(1).isEmpty) - } - it should "call onExpire in close" in { - val count = new AtomicInteger(0) - val cache = - LRUCache[Int, Int]( - maxSize = 3, - onExpire = (_: Int, _: Int) => { count.getAndIncrement(); () } - ) - cache.put(1, 1) - cache.put(2, 2) - cache.put(3, 3) - cache.put(4, 4) - assert(count.get == 1) - cache.close() - assert(count.get == 4) - } - it should "apply on remove function" in { - val value = new AtomicInteger(0) - val cache = LRUCache[Int, Int](1, (k: Int, v: Int) => value.set(k + v)) - cache.put(1, 3) - cache.put(2, 2) - assert(value.get() == 4) - assert(cache.get(2).contains(2)) - } - it should "print sorted entries in toString" in { - val cache = LRUCache[Int, Int](2) - cache.put(2, 2) - cache.put(1, 1) - assert(cache.toString == s"LRUCache(2 -> 2, 1 -> 1)") - } -} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 4632346ba..dbe9d6a6d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -17,7 +17,7 @@ object Dependencies { case Some(version) => version case _ => nightlyVersion.getOrElse("1.3.0-M3") } - val zincVersion = nightlyVersion.getOrElse("1.3.0-M5") + val zincVersion = nightlyVersion.getOrElse("1.3.0-M6") private val sbtIO = "org.scala-sbt" %% "io" % ioVersion @@ -32,7 +32,7 @@ object Dependencies { private val libraryManagementCore = "org.scala-sbt" %% "librarymanagement-core" % lmVersion private val libraryManagementIvy = "org.scala-sbt" %% "librarymanagement-ivy" % lmVersion - val launcherVersion = "1.0.4" + val launcherVersion = "1.1.0" val launcherInterface = "org.scala-sbt" % "launcher-interface" % launcherVersion val rawLauncher = "org.scala-sbt" % "launcher" % launcherVersion val testInterface = "org.scala-sbt" % "test-interface" % "1.0" diff --git a/sbt/src/sbt-test/classloader-cache/akka-actor-system/test b/sbt/src/sbt-test/classloader-cache/akka-actor-system/test index abcb2b542..b14110376 100644 --- a/sbt/src/sbt-test/classloader-cache/akka-actor-system/test +++ b/sbt/src/sbt-test/classloader-cache/akka-actor-system/test @@ -1,4 +1,4 @@ -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies +> run > run diff --git a/sbt/src/sbt-test/classloader-cache/library-mismatch/test b/sbt/src/sbt-test/classloader-cache/library-mismatch/test index abdaa10fa..d737a4d50 100644 --- a/sbt/src/sbt-test/classloader-cache/library-mismatch/test +++ b/sbt/src/sbt-test/classloader-cache/library-mismatch/test @@ -1,15 +1,3 @@ -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies - -> run - -# This fails because the runtime layer includes an old version of the foo-lib library that doesn't -# have the sbt.foo.Foo.y method defined. -> test - -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies - > run > test - -> test diff --git a/sbt/src/sbt-test/classloader-cache/resources/build.sbt b/sbt/src/sbt-test/classloader-cache/resources/build.sbt new file mode 100644 index 000000000..fbc057785 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/build.sbt @@ -0,0 +1,5 @@ +resolvers += "Local Maven" at (baseDirectory.value / "libraries" / "foo" / "ivy").toURI.toURL.toString + +libraryDependencies += "sbt" %% "foo-lib" % "0.1.0" + +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test" diff --git a/sbt/src/sbt-test/classloader-cache/resources/changes/UpdatedResourceTest.scala b/sbt/src/sbt-test/classloader-cache/resources/changes/UpdatedResourceTest.scala new file mode 100644 index 000000000..b1b13145e --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/changes/UpdatedResourceTest.scala @@ -0,0 +1,9 @@ +package scripted + +import org.scalatest.FlatSpec + +class ResourceTest extends FlatSpec { + "test resources" should "load" in { + Main.main(Array("bar.txt", "updated-test")) + } +} diff --git a/sbt/src/sbt-test/classloader-cache/resources/changes/updated-main.txt b/sbt/src/sbt-test/classloader-cache/resources/changes/updated-main.txt new file mode 100644 index 000000000..d53f0341f --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/changes/updated-main.txt @@ -0,0 +1 @@ +updated-main \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/changes/updated-test.txt b/sbt/src/sbt-test/classloader-cache/resources/changes/updated-test.txt new file mode 100644 index 000000000..e0f0eebab --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/changes/updated-test.txt @@ -0,0 +1 @@ +updated-test \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/build.sbt b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/build.sbt new file mode 100644 index 000000000..860c958ab --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/build.sbt @@ -0,0 +1,11 @@ +name := "foo-lib" + +organization := "sbt" + +publishTo := Some(Resolver.file("test-resolver", file("").getCanonicalFile / "ivy")) + +version := "0.1.0" + +classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars + +Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar new file mode 100644 index 000000000..276995bb9 Binary files /dev/null and b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar differ diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar.md5 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar.md5 new file mode 100644 index 000000000..a1d5ac6db --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar.md5 @@ -0,0 +1 @@ +9bab3dca3e061fdbbff2419ef0eee90d \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar.sha1 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar.sha1 new file mode 100644 index 000000000..4ee36a1b4 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-javadoc.jar.sha1 @@ -0,0 +1 @@ +11081018ca6824226893b153fe439d3a94e23286 \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar new file mode 100644 index 000000000..e445add15 Binary files /dev/null and b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar differ diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar.md5 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar.md5 new file mode 100644 index 000000000..3fad0add4 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar.md5 @@ -0,0 +1 @@ +afba811024a110dc47734c6d9ec0eb5b \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar.sha1 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar.sha1 new file mode 100644 index 000000000..b2a820350 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0-sources.jar.sha1 @@ -0,0 +1 @@ +e4e1b37d77060a77ab20c508df90cdae6125f3bc \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar new file mode 100644 index 000000000..e2441bf60 Binary files /dev/null and b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar differ diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar.md5 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar.md5 new file mode 100644 index 000000000..7acfc2da3 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar.md5 @@ -0,0 +1 @@ +6de1cfe8548a007ac08763ab5f051e08 \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar.sha1 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar.sha1 new file mode 100644 index 000000000..e60d8daff --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.jar.sha1 @@ -0,0 +1 @@ +f25267dfe760e734ea4f8a00b8dc6ace477589d6 \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom new file mode 100644 index 000000000..a340da7e4 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom @@ -0,0 +1,20 @@ + + + 4.0.0 + sbt + foo-lib_2.12 + jar + foo-lib + 0.1.0 + foo-lib + + sbt + + + + org.scala-lang + scala-library + 2.12.8 + + + \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom.md5 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom.md5 new file mode 100644 index 000000000..70f55bc4e --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom.md5 @@ -0,0 +1 @@ +b83994a1d5ecfc3d6ff71021649e342d \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom.sha1 b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom.sha1 new file mode 100644 index 000000000..b005d6129 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/ivy/sbt/foo-lib_2.12/0.1.0/foo-lib_2.12-0.1.0.pom.sha1 @@ -0,0 +1 @@ +6b2c0b4d69ed8e10e0221db841e0f1fdeef12780 \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/project/build.properties b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/project/build.properties new file mode 100644 index 000000000..c66f82ead --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.0-RC1 diff --git a/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/src/main/scala/resource/Resource.scala b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/src/main/scala/resource/Resource.scala new file mode 100644 index 000000000..9793cd8a0 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/libraries/foo/src/main/scala/resource/Resource.scala @@ -0,0 +1,19 @@ +package resource + +import java.net.URL +import java.nio.file._ +import java.util.Collections + +object Resource { + def readFile(url: URL): String = { + val uri = url.toURI + val fileSystem = + if (uri.getScheme != "jar") None + else Some(FileSystems.newFileSystem(uri, Collections.emptyMap[String, Object])) + try new String(Files.readAllBytes(Paths.get(uri))) + finally fileSystem.foreach(_.close()) + } + def getStringResource(name: String): String = { + readFile(Resource.getClass.getClassLoader.getResource(name)) + } +} diff --git a/sbt/src/sbt-test/classloader-cache/resources/src/main/resources/foo.txt b/sbt/src/sbt-test/classloader-cache/resources/src/main/resources/foo.txt new file mode 100644 index 000000000..88d050b19 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/src/main/resources/foo.txt @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/src/main/scala/scripted/Main.scala b/sbt/src/sbt-test/classloader-cache/resources/src/main/scala/scripted/Main.scala new file mode 100644 index 000000000..61b2fb6c9 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/src/main/scala/scripted/Main.scala @@ -0,0 +1,10 @@ +package scripted + +import resource.Resource + +object Main { + def main(args: Array[String]): Unit = { + val Array(resource, expected) = args + assert(Resource.getStringResource(resource) == expected) + } +} diff --git a/sbt/src/sbt-test/classloader-cache/resources/src/test/resources/bar.txt b/sbt/src/sbt-test/classloader-cache/resources/src/test/resources/bar.txt new file mode 100644 index 000000000..30d74d258 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/src/test/resources/bar.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/resources/src/test/scala/scripted/ResourceTest.scala b/sbt/src/sbt-test/classloader-cache/resources/src/test/scala/scripted/ResourceTest.scala new file mode 100644 index 000000000..980fd37bc --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/src/test/scala/scripted/ResourceTest.scala @@ -0,0 +1,9 @@ +package scripted + +import org.scalatest.FlatSpec + +class ResourceTest extends FlatSpec { + "test resources" should "load" in { + Main.main(Array("bar.txt", "test")) + } +} diff --git a/sbt/src/sbt-test/classloader-cache/resources/test b/sbt/src/sbt-test/classloader-cache/resources/test new file mode 100644 index 000000000..57e1f61f5 --- /dev/null +++ b/sbt/src/sbt-test/classloader-cache/resources/test @@ -0,0 +1,15 @@ +> run foo.txt main + +$ copy-file changes/updated-main.txt src/main/resources/foo.txt + +> run foo.txt updated-main + +> test + +$ copy-file changes/updated-test.txt src/test/resources/bar.txt + +-> test + +$ copy-file changes/UpdatedResourceTest.scala src/test/scala/scripted/ResourceTest.scala + +> test \ No newline at end of file diff --git a/sbt/src/sbt-test/classloader-cache/runtime-layers/test b/sbt/src/sbt-test/classloader-cache/runtime-layers/test index 8ba3debaa..c6234883b 100644 --- a/sbt/src/sbt-test/classloader-cache/runtime-layers/test +++ b/sbt/src/sbt-test/classloader-cache/runtime-layers/test @@ -8,14 +8,10 @@ > run -> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies +> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars > run -> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies - --> run - > set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat > Test / runMain sbt.scripted.TestAkkaTest @@ -24,14 +20,6 @@ > Test / runMain sbt.scripted.TestAkkaTest -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies - -> Test / runMain sbt.scripted.TestAkkaTest - -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies - -> Test / runMain sbt.scripted.TestAkkaTest - -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies +> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars > Test / runMain sbt.scripted.TestAkkaTest diff --git a/sbt/src/sbt-test/classloader-cache/scalatest/test b/sbt/src/sbt-test/classloader-cache/scalatest/test index 2fb0d98cd..9b34bf4a6 100644 --- a/sbt/src/sbt-test/classloader-cache/scalatest/test +++ b/sbt/src/sbt-test/classloader-cache/scalatest/test @@ -1,9 +1,5 @@ > test -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies - -> test - > set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat > test @@ -12,7 +8,7 @@ > test -> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies +> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars $ copy-file changes/bad.scala src/test/scala/sbt/ScalatestTest.scala diff --git a/tasks/src/main/scala/sbt/Execute.scala b/tasks/src/main/scala/sbt/Execute.scala index 2332441d6..e9cfb471e 100644 --- a/tasks/src/main/scala/sbt/Execute.scala +++ b/tasks/src/main/scala/sbt/Execute.scala @@ -7,6 +7,8 @@ package sbt +import java.util.concurrent.ExecutionException + import sbt.internal.util.ErrorHandling.wideConvert import sbt.internal.util.{ DelegatingPMap, IDSet, PMap, RMap, ~> } import sbt.internal.util.Types._ @@ -109,7 +111,15 @@ private[sbt] final class Execute[F[_] <: AnyRef]( } } - (strategy.take()).process() + try { + strategy.take().process() + } catch { + case e: ExecutionException => + e.getCause match { + case oom: OutOfMemoryError => throw oom + case _ => throw e + } + } if (reverse.nonEmpty) next() } next() diff --git a/testing/src/main/scala/sbt/TestFramework.scala b/testing/src/main/scala/sbt/TestFramework.scala index dcaa6c717..71fe4790d 100644 --- a/testing/src/main/scala/sbt/TestFramework.scala +++ b/testing/src/main/scala/sbt/TestFramework.scala @@ -52,7 +52,7 @@ final class TestFramework(val implClassNames: String*) extends Serializable { + " using a layered class loader that cannot reach the sbt.testing.Framework class." + " The most likely cause is that your project has a runtime dependency on your" + " test framework, e.g. scalatest. To fix this, you can try to set\n" - + "Test / classLoaderLayeringStrategy := new ClassLoaderLayeringStrategy.Test(false, true)\nor\n" + + "Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ScalaLibrary\nor\n" + "Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat" ) None @@ -138,8 +138,9 @@ final class TestRunner( val nestedTasks = try testTask.execute(handler, loggers.map(_.log).toArray) catch { - case NonFatal(e) => errorEvents(e) - case e: IllegalAccessError => errorEvents(e) + case e: NoClassDefFoundError => errorEvents(e) + case NonFatal(e) => errorEvents(e) + case e: IllegalAccessError => errorEvents(e) } finally { loggers.foreach(_.flush()) } diff --git a/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincLmUtil.scala b/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincLmUtil.scala index 16d5dac76..e0d0d4722 100644 --- a/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincLmUtil.scala +++ b/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincLmUtil.scala @@ -8,10 +8,9 @@ package sbt.internal.inc import java.io.File -import java.net.URLClassLoader -import sbt.librarymanagement.{ DependencyResolution, ModuleID } import sbt.internal.inc.classpath.ClassLoaderCache +import sbt.librarymanagement.{ DependencyResolution, ModuleID } import xsbti._ import xsbti.compile._ @@ -32,6 +31,7 @@ object ZincLmUtil { dependencyResolution: DependencyResolution, compilerBridgeSource: ModuleID, scalaJarsTarget: File, + classLoaderCache: Option[ClassLoaderCache], log: Logger ): AnalyzingCompiler = { val compilerBridgeProvider = ZincComponentCompiler.interfaceProvider( @@ -40,8 +40,13 @@ object ZincLmUtil { dependencyResolution, scalaJarsTarget, ) - val loader = Some(new ClassLoaderCache(new URLClassLoader(new Array(0)))) - new AnalyzingCompiler(scalaInstance, compilerBridgeProvider, classpathOptions, _ => (), loader) + new AnalyzingCompiler( + scalaInstance, + compilerBridgeProvider, + classpathOptions, + _ => (), + classLoaderCache + ) } def getDefaultBridgeModule(scalaVersion: String): ModuleID =