diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4097aa0a..01f6b82cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,7 +97,7 @@ jobs: if: ${{ matrix.jobtype == 2 }} shell: bash run: | - ./sbt -v "scripted actions/* apiinfo/* compiler-project/* ivy-deps-management/* reporter/* tests/* watch/* classloader-cache/* package/*" + ./sbt -v "scripted actions/* apiinfo/* compiler-project/* console/* ivy-deps-management/* reporter/* tests/* watch/* classloader-cache/* package/*" - name: Build and test (3) if: ${{ matrix.jobtype == 3 }} shell: bash diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index a651c1ff7..227b9e899 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2185,6 +2185,28 @@ object Defaults extends BuildCommon { def consoleTask: Initialize[Task[Unit]] = consoleTask(fullClasspath, console) def consoleQuickTask = consoleTask(externalDependencyClasspath, consoleQuick) + + // The ScalaInstance loader chain bottoms out at the launcher's loader, which does not + // delegate to the JDK platform classloader. None on JDK 8 (no platform loader). + private[this] lazy val platformLoader: Option[ClassLoader] = + try { + Option( + classOf[ClassLoader] + .getMethod("getPlatformClassLoader") + .invoke(null) + .asInstanceOf[ClassLoader] + ) + } catch { case NonFatal(_) => None } + + private[this] def platformFallbackLoader(parent: ClassLoader): ClassLoader = + platformLoader match { + case Some(platform) => + new ClassLoader(parent) { + override def findClass(name: String): Class[_] = platform.loadClass(name) + } + case None => parent + } + def consoleTask(classpath: TaskKey[Classpath], task: TaskKey[_]): Initialize[Task[Unit]] = Def.task { val si = (task / scalaInstance).value @@ -2192,7 +2214,8 @@ object Defaults extends BuildCommon { val cpFiles = data((task / classpath).value) val fullcp = (cpFiles ++ si.allJars).distinct val tempDir = IO.createUniqueDirectory((task / taskTemporaryDirectory).value).toPath - val loader = ClasspathUtil.makeLoader(fullcp.map(_.toPath), si, tempDir) + val loader = + platformFallbackLoader(ClasspathUtil.makeLoader(fullcp.map(_.toPath), si, tempDir)) val compiler = (task / compilers).value.scalac match { case ac: AnalyzingCompiler => ac.onArgs(exported(s, "scala")) diff --git a/sbt-app/src/sbt-test/console/jdk-platform-classes/build.sbt b/sbt-app/src/sbt-test/console/jdk-platform-classes/build.sbt new file mode 100644 index 000000000..eb03d8cf6 --- /dev/null +++ b/sbt-app/src/sbt-test/console/jdk-platform-classes/build.sbt @@ -0,0 +1,19 @@ +scalaVersion := "2.13.12" + +lazy val markerFile = settingKey[java.io.File]("marker file written by the console REPL when JDK platform classes load") + +markerFile := target.value / "console-jdk-platform-ok" + +console / initialCommands := { + val path = markerFile.value.getAbsolutePath.replace("\\", "\\\\") + // java.sql lives in a JDK platform module. Loading it through the console REPL's + // classloader chain threw SecurityException("Prohibited package name: java.sql") + // before the platform-loader fallback (#4328): `ts` covers the top-down request, + // `mts` covers supertype resolution for a class defined from the project classpath. + // The REPL traps exceptions, so the assertion is a marker file: it is only written + // if both classes actually load. + s"""val ts = new java.sql.Timestamp(0L) + |val mts = new MyTimestamp + |java.nio.file.Files.write(java.nio.file.Paths.get("$path"), (ts.getClass.getName + " " + mts.getClass.getName).getBytes("UTF-8")) + |""".stripMargin +} diff --git a/sbt-app/src/sbt-test/console/jdk-platform-classes/src/main/scala/MyTimestamp.scala b/sbt-app/src/sbt-test/console/jdk-platform-classes/src/main/scala/MyTimestamp.scala new file mode 100644 index 000000000..fd6ee4908 --- /dev/null +++ b/sbt-app/src/sbt-test/console/jdk-platform-classes/src/main/scala/MyTimestamp.scala @@ -0,0 +1,5 @@ +// Linkage through the defining loader: resolving the java.sql.Timestamp supertype of a +// project-classpath class exercises the classpath loader's parent chain, not a top-down +// REPL request. This is the shape of sbt/sbt#4328's original repro (a JDBC driver +// implementing java.sql.Driver). +class MyTimestamp extends java.sql.Timestamp(0L) diff --git a/sbt-app/src/sbt-test/console/jdk-platform-classes/test b/sbt-app/src/sbt-test/console/jdk-platform-classes/test new file mode 100644 index 000000000..a43ca7c35 --- /dev/null +++ b/sbt-app/src/sbt-test/console/jdk-platform-classes/test @@ -0,0 +1,2 @@ +> console +$ exists target/console-jdk-platform-ok