From fa36d0e2908f6787a2ea3db5e9dbf86ba6624459 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 9 Sep 2014 08:54:43 -0400 Subject: [PATCH 1/4] Add missing `configs` method from Project to the build.sbt DSL. * Create `configs` method for the sbt DSL * Add ProjectManipulation for this method to implement. --- main/src/main/scala/sbt/dsl/package.scala | 4 +++- main/src/main/scala/sbt/internals/DslAst.scala | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/dsl/package.scala b/main/src/main/scala/sbt/dsl/package.scala index 4ee17980e..d3b553373 100644 --- a/main/src/main/scala/sbt/dsl/package.scala +++ b/main/src/main/scala/sbt/dsl/package.scala @@ -2,7 +2,7 @@ package sbt import internals.{ DslEntry, - DslSetting, + DslConfigs, DslEnablePlugins, DslDisablePlugins } @@ -10,4 +10,6 @@ import internals.{ package object dsl { def enablePlugins(ps: AutoPlugin*): DslEntry = DslEnablePlugins(ps) def disablePlugins(ps: AutoPlugin*): DslEntry = DslDisablePlugins(ps) + def configs(cs: Configuration*): DslEntry = DslConfigs(cs) + } \ No newline at end of file diff --git a/main/src/main/scala/sbt/internals/DslAst.scala b/main/src/main/scala/sbt/internals/DslAst.scala index 2476cf7a5..9cb5cfea7 100644 --- a/main/src/main/scala/sbt/internals/DslAst.scala +++ b/main/src/main/scala/sbt/internals/DslAst.scala @@ -54,4 +54,8 @@ case class DslEnablePlugins(plugins: Seq[AutoPlugin]) extends ProjectManipulatio case class DslDisablePlugins(plugins: Seq[AutoPlugin]) extends ProjectManipulation { override val toFunction: Project => Project = _.disablePlugins(plugins: _*) } +/** Represents registering a set of configurations with the current project. */ +case class DslConfigs(cs: Seq[Configuration]) extends ProjectManipulation { + override val toFunction: Project => Project = _.configs(cs: _*) +} From 50460fad285e459da095de967f70d5b638c76f69 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 9 Sep 2014 08:56:20 -0400 Subject: [PATCH 2/4] Add toString methods to classloaders, for debugging. * Add toString methods to all classloader classes. --- .../main/scala/sbt/classpath/ClassLoaders.scala | 14 +++++++++++++- .../scala/sbt/classpath/ClasspathUtilities.scala | 7 +++++++ .../src/main/scala/sbt/classpath/DualLoader.scala | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala b/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala index b50d84883..2c15bdcc8 100644 --- a/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala +++ b/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala @@ -46,14 +46,26 @@ final class SelfFirstLoader(classpath: Seq[URL], parent: ClassLoader) extends Lo /** Doesn't load any classes itself, but instead verifies that all classes loaded through `parent` either come from `root` or `classpath`.*/ final class ClasspathFilter(parent: ClassLoader, root: ClassLoader, classpath: Set[File]) extends ClassLoader(parent) { + override def toString = + s"""|ClasspathFilter( + | parent = $parent + | root = $root + | cp = $classpath + |)""".stripMargin + private[this] val directories: Seq[File] = classpath.toSeq.filter(_.isDirectory) override def loadClass(className: String, resolve: Boolean): Class[_] = { val c = super.loadClass(className, resolve) if (includeLoader(c.getClassLoader, root) || fromClasspath(c)) c - else + else { + System.err.println(s"DEBUGME: Failing to load class $className because it was not in expected parent chain.") + System.err.println(s"DEBUGME: Found: ${c.getClassLoader}") + System.err.println(s"DEBUGME: Expected: ${root}") + System.err.println(s"DEBUGME: onClasspath: ${fromClasspath(c)}") throw new ClassNotFoundException(className) + } } private[this] def fromClasspath(c: Class[_]): Boolean = { diff --git a/util/classpath/src/main/scala/sbt/classpath/ClasspathUtilities.scala b/util/classpath/src/main/scala/sbt/classpath/ClasspathUtilities.scala index 2f7fa1d35..dd29d7f77 100644 --- a/util/classpath/src/main/scala/sbt/classpath/ClasspathUtilities.scala +++ b/util/classpath/src/main/scala/sbt/classpath/ClasspathUtilities.scala @@ -26,6 +26,13 @@ object ClasspathUtilities { new URLClassLoader(Path.toURLs(paths), parent) with RawResources with NativeCopyLoader { override def resources = resourceMap override val config = new NativeCopyConfig(nativeTemp, paths, javaLibraryPaths) + override def toString = + s"""|URLClassLoader with NativeCopyLoader with RawResources( + | urls = $paths, + | parent = $parent, + | resourceMap = ${resourceMap.keySet}, + | nativeTemp = $nativeTemp + |)""".stripMargin } def javaLibraryPaths: Seq[File] = IO.parseClasspath(System.getProperty("java.library.path")) diff --git a/util/classpath/src/main/scala/sbt/classpath/DualLoader.scala b/util/classpath/src/main/scala/sbt/classpath/DualLoader.scala index b51a403ea..e1d7ff67e 100644 --- a/util/classpath/src/main/scala/sbt/classpath/DualLoader.scala +++ b/util/classpath/src/main/scala/sbt/classpath/DualLoader.scala @@ -12,6 +12,7 @@ final class NullLoader extends ClassLoader { override final def loadClass(className: String, resolve: Boolean): Class[_] = throw new ClassNotFoundException("No classes can be loaded from the null loader") override def getResource(name: String): URL = null override def getResources(name: String): Enumeration[URL] = null + override def toString = "NullLoader" } /** Exception thrown when `loaderA` and `loaderB` load a different Class for the same name. */ @@ -84,6 +85,8 @@ class DualLoader(parentA: ClassLoader, aOnlyClasses: String => Boolean, aOnlyRes new DualEnumeration(urlsA, urlsB) } } + + override def toString = s"DualLoader(a = $parentA, b = $parentB)" } /** Concatenates `a` and `b` into a single `Enumeration`.*/ From d2950da9dd59b0eaaedc7c10f31f2cad3f3cd207 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 9 Sep 2014 08:56:51 -0400 Subject: [PATCH 3/4] Fix issue where ScalaInstance broke the thread-context-classloader for all scala classes. The issue is that when you manually set a ScalaInstance, i.e. not one from Ivy, the classpath which is returned for any given configuration ONLY uses Ivy. This means that the legitimate Scala JAR files that need to be on the classpath are missing from the list. For some reason, the way we instantiate tests uses an unfiltered classloader against the ScalaInstance, *BUT* the thread-context-classloader DOES use a filtered instance by classpath. This add the hook into the TestFramework runner creation so that the classpath accurately reflects the jars needed. cc @rkuhn --- testing/src/main/scala/sbt/TestFramework.scala | 7 ++++++- util/classpath/src/main/scala/sbt/ScalaInstance.scala | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/testing/src/main/scala/sbt/TestFramework.scala b/testing/src/main/scala/sbt/TestFramework.scala index 6595fd472..0bbab2058 100644 --- a/testing/src/main/scala/sbt/TestFramework.scala +++ b/testing/src/main/scala/sbt/TestFramework.scala @@ -191,7 +191,12 @@ object TestFramework { val notInterfaceFilter = (name: String) => !interfaceFilter(name) val dual = new DualLoader(scalaInstance.loader, notInterfaceFilter, x => true, getClass.getClassLoader, interfaceFilter, x => false) val main = ClasspathUtilities.makeLoader(classpath, dual, scalaInstance, tempDir) - ClasspathUtilities.filterByClasspath(interfaceJar +: classpath, main) + // TODO - There's actually an issue with the classpath facility such that unmanagedScalaInstances are not added + // to the classpath correctly. We have a temporary workaround here. + val cp: Seq[File] = + if (scalaInstance.isManagedVersion) interfaceJar +: classpath + else scalaInstance.allJars ++ (interfaceJar +: classpath) + ClasspathUtilities.filterByClasspath(cp, main) } def createTestFunction(loader: ClassLoader, taskDef: TaskDef, runner: TestRunner, testTask: TestTask): TestFunction = new TestFunction(taskDef, runner, (r: TestRunner) => withContextLoader(loader) { r.run(taskDef, testTask) }) { def tags = testTask.tags } diff --git a/util/classpath/src/main/scala/sbt/ScalaInstance.scala b/util/classpath/src/main/scala/sbt/ScalaInstance.scala index c0a207887..b2bf5bc3d 100644 --- a/util/classpath/src/main/scala/sbt/ScalaInstance.scala +++ b/util/classpath/src/main/scala/sbt/ScalaInstance.scala @@ -19,6 +19,12 @@ final class ScalaInstance(val version: String, val loader: ClassLoader, val libr @deprecated("Only `allJars` and `jars` can be reliably provided for modularized Scala.", "0.13.0") val compilerJar: File, @deprecated("Only `allJars` and `jars` can be reliably provided for modularized Scala.", "0.13.0") val extraJars: Seq[File], val explicitActual: Option[String]) extends xsbti.compile.ScalaInstance { + /** + * This tells us if the scalaInstance is from a managed (i.e. ivy-resolved) scala *or* + * if it's a free-floating ScalaInstance, in which case we need to do tricks to the classpaths we find + * because it won't be on them. + */ + final def isManagedVersion = explicitActual.isDefined // These are to implement xsbti.ScalaInstance @deprecated("Only `allJars` and `jars` can be reliably provided for modularized Scala.", "0.13.0") def otherJars: Array[File] = extraJars.toArray From da1fc33b521d64cd2448d42a2ea682ce3bd69585 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 9 Sep 2014 09:16:10 -0400 Subject: [PATCH 4/4] Removing printlns and adding the test, DOH. --- .../scala-instance-classloader/build.sbt | 29 +++++++++++++++++++ .../src/test/scala/Test.scala | 21 ++++++++++++++ .../tests/scala-instance-classloader/test | 1 + .../scala/sbt/classpath/ClassLoaders.scala | 7 +---- 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 sbt/src/sbt-test/tests/scala-instance-classloader/build.sbt create mode 100644 sbt/src/sbt-test/tests/scala-instance-classloader/src/test/scala/Test.scala create mode 100644 sbt/src/sbt-test/tests/scala-instance-classloader/test diff --git a/sbt/src/sbt-test/tests/scala-instance-classloader/build.sbt b/sbt/src/sbt-test/tests/scala-instance-classloader/build.sbt new file mode 100644 index 000000000..65dabf892 --- /dev/null +++ b/sbt/src/sbt-test/tests/scala-instance-classloader/build.sbt @@ -0,0 +1,29 @@ + +lazy val OtherScala = config("other-scala").hide + +configs(OtherScala) + +libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.11.1" % OtherScala.name + +managedClasspath in OtherScala := Classpaths.managedJars(OtherScala, classpathTypes.value, update.value) + +// Hack in the scala instance +scalaInstance := { + val rawJars = (managedClasspath in OtherScala).value.map(_.data) + val scalaHome = (target.value / "scala-home") + def removeVersion(name: String): String = + name.replaceAll("\\-2.11.1", "") + for(jar <- rawJars) { + val tjar = scalaHome / s"lib/${removeVersion(jar.getName)}" + IO.copyFile(jar, tjar) + } + IO.listFiles(scalaHome).foreach(f => System.err.println(s" * $f}")) + ScalaInstance(scalaHome, appConfiguration.value.provider.scalaProvider.launcher) +} + + +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test" + +libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.3" % "test" + +scalaVersion := "2.11.0" diff --git a/sbt/src/sbt-test/tests/scala-instance-classloader/src/test/scala/Test.scala b/sbt/src/sbt-test/tests/scala-instance-classloader/src/test/scala/Test.scala new file mode 100644 index 000000000..6006e8257 --- /dev/null +++ b/sbt/src/sbt-test/tests/scala-instance-classloader/src/test/scala/Test.scala @@ -0,0 +1,21 @@ +package akka.actor + +import org.junit._ + +class BadTest { + + @Test + def testCpIssue(): Unit = { + // TODO - This is merely the laziest way to run the test. What we want to do: + // * Load something from our own classloader that's INSIDE the scala library + // * Try to load that same something from the THREAD CONTEXT classloader. + // * Ensure we can do both, i.e. the second used to be filtered and broken. + val current = Thread.currentThread.getContextClassLoader + val mine = this.getClass.getClassLoader + val system = ActorSystem() + def evilGetThreadExectionContextName = + system.asInstanceOf[ActorSystemImpl].internalCallingThreadExecutionContext.getClass.getName + val expected = "scala.concurrent.Future$InternalCallbackExecutor$" + Assert.assertEquals("Failed to grab appropriate Akka name", expected, evilGetThreadExectionContextName) + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/scala-instance-classloader/test b/sbt/src/sbt-test/tests/scala-instance-classloader/test new file mode 100644 index 000000000..a270b7b48 --- /dev/null +++ b/sbt/src/sbt-test/tests/scala-instance-classloader/test @@ -0,0 +1 @@ +> test \ No newline at end of file diff --git a/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala b/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala index 2c15bdcc8..2a1362f7e 100644 --- a/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala +++ b/util/classpath/src/main/scala/sbt/classpath/ClassLoaders.scala @@ -59,13 +59,8 @@ final class ClasspathFilter(parent: ClassLoader, root: ClassLoader, classpath: S val c = super.loadClass(className, resolve) if (includeLoader(c.getClassLoader, root) || fromClasspath(c)) c - else { - System.err.println(s"DEBUGME: Failing to load class $className because it was not in expected parent chain.") - System.err.println(s"DEBUGME: Found: ${c.getClassLoader}") - System.err.println(s"DEBUGME: Expected: ${root}") - System.err.println(s"DEBUGME: onClasspath: ${fromClasspath(c)}") + else throw new ClassNotFoundException(className) - } } private[this] def fromClasspath(c: Class[_]): Boolean = {