From 2465e1785de954dd80d6b37a1505c232044ab4d2 Mon Sep 17 00:00:00 2001 From: Stefan Zeiger Date: Thu, 4 Jul 2024 19:09:49 +0200 Subject: [PATCH] Add support for alternative system images to the compiler This PR adds a new `-system` / `--system` setting to scalac which mimics its counterpart in javac. This fixes the biggest problem (at least for us) of https://github.com/scala/bug/issues/13015. It is now possible to compile code against an older system image without enforcing strict module access. scalac generally does not enforce modules (https://github.com/scala/scala-dev/issues/529) but it does when using `-release` (with class lookup based on `ct.sym`) and there was no way to opt out of these restrictions. The usual opt-out in javac is `--add-exports` but it is not supported for system modules in combination with `--release` (https://bugs.openjdk.org/browse/JDK-8178152) so there is no expectation that scalac could support it. Instead the solution for javac is to replace `--release` with a combination of `-source`, `-target` and a system image for the target version via `--system`. This combination, unlike `--release`, can be used with `--add-exports`. If scalac adds full module support at a later time (with access restrictions enabled by default) it will also need to support `--add-exports` and allow its use in combination with `--system`. I am also un-deprecating `-target` (which was deprecated in favor of `-release`) because it now has a legitimate use in combination with an alternative system image (just like in javac where it is serves the same purpose and is not deprecated, either). --- src/compiler/scala/tools/nsc/Global.scala | 2 +- .../nsc/classpath/DirectoryClassPath.scala | 18 +++++++++++++----- .../nsc/settings/StandardScalaSettings.scala | 4 +++- .../scala/tools/util/PathResolver.scala | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index 9b48b7e33cc2..2d9abef5e1db 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -148,7 +148,7 @@ class Global(var currentSettings: Settings, reporter0: Reporter) def optimizerClassPath(base: ClassPath): ClassPath = base match { case AggregateClassPath(entries) if entries.head.isInstanceOf[CtSymClassPath] => - JrtClassPath(release = None, unsafe = None, closeableRegistry) match { + JrtClassPath(release = None, settings.systemPathValue, unsafe = None, closeableRegistry) match { case jrt :: Nil => AggregateClassPath(entries.drop(1).prepended(jrt)) case _ => base } diff --git a/src/compiler/scala/tools/nsc/classpath/DirectoryClassPath.scala b/src/compiler/scala/tools/nsc/classpath/DirectoryClassPath.scala index 15202e73b547..7aa1b23d3fd1 100644 --- a/src/compiler/scala/tools/nsc/classpath/DirectoryClassPath.scala +++ b/src/compiler/scala/tools/nsc/classpath/DirectoryClassPath.scala @@ -15,6 +15,7 @@ package scala.tools.nsc.classpath import java.io.{Closeable, File} import java.net.{URI, URL} import java.nio.file._ +import java.util.Collections import scala.jdk.CollectionConverters._ import scala.reflect.internal.JDK9Reflectors @@ -137,9 +138,10 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo } object JrtClassPath { + private val jrtFileSystemCache = new FileBasedCache[String, FileSystem]() private val jrtClassPathCache = new FileBasedCache[Unit, JrtClassPath]() private val ctSymClassPathCache = new FileBasedCache[String, CtSymClassPath]() - def apply(release: Option[String], unsafe: Option[List[String]], closeableRegistry: CloseableRegistry): List[ClassPath] = + def apply(release: Option[String], systemPath: Option[String], unsafe: Option[List[String]], closeableRegistry: CloseableRegistry): List[ClassPath] = if (!isJavaAtLeast("9")) Nil else { // TODO escalate errors once we're sure they are fatal @@ -155,14 +157,14 @@ object JrtClassPath { val ct = createCt(version, closeableRegistry) unsafe match { case Some(pkgs) if pkgs.nonEmpty => - createJrt(closeableRegistry) match { + createJrt(systemPath, closeableRegistry) match { case Nil => ct case jrts => ct.appended(new FilteringJrtClassPath(jrts.head, pkgs: _*)) } case _ => ct } case _ => - createJrt(closeableRegistry) + createJrt(systemPath, closeableRegistry) } } private def createCt(v: String, closeableRegistry: CloseableRegistry): List[ClassPath] = @@ -176,9 +178,15 @@ object JrtClassPath { } catch { case NonFatal(_) => Nil } - private def createJrt(closeableRegistry: CloseableRegistry): List[JrtClassPath] = + private def createJrt(systemPath: Option[String], closeableRegistry: CloseableRegistry): List[JrtClassPath] = try { - val fs = FileSystems.getFileSystem(URI.create("jrt:/")) + val fs = systemPath match { + case Some(javaHome) => + jrtFileSystemCache.getOrCreate(javaHome, Nil, + () => FileSystems.newFileSystem(URI.create("jrt:/"), Collections.singletonMap("java.home", javaHome)), + closeableRegistry, checkStamps = false) + case None => FileSystems.getFileSystem(URI.create("jrt:/")) + } val classPath = jrtClassPathCache.getOrCreate((), Nil, () => new JrtClassPath(fs), closeableRegistry, checkStamps = false) List(classPath) } catch { diff --git a/src/compiler/scala/tools/nsc/settings/StandardScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/StandardScalaSettings.scala index 044a6ca3dbd6..5d83538c512a 100644 --- a/src/compiler/scala/tools/nsc/settings/StandardScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/StandardScalaSettings.scala @@ -32,6 +32,7 @@ trait StandardScalaSettings { _: MutableSettings => val javaextdirs = PathSetting ("-javaextdirs", "Override java extdirs classpath.", Defaults.javaExtDirs) withAbbreviation "--java-extension-directories" val sourcepath = PathSetting ("-sourcepath", "Specify location(s) of source files.", "") withAbbreviation "--source-path" // Defaults.scalaSourcePath val rootdir = PathSetting ("-rootdir", "The absolute path of the project root directory, usually the git/scm checkout. Used by -Wconf.", "") withAbbreviation "--root-directory" + val systemPath = PathSetting ("-system", "Override location of Java system modules", "") withAbbreviation "--system" /** Other settings. */ @@ -75,11 +76,13 @@ trait StandardScalaSettings { _: MutableSettings => val current = setting.value.toInt if (!isJavaAtLeast("9") && current > 8) errorFn.apply("-release is only supported on JVM 9 and higher") if (target.valueSetByUser.map(_.toInt > current).getOrElse(false)) errorFn("-release cannot be less than -target") + if (systemPath.isSetByUser) errorFn("-release cannot be used with -system") //target.value = setting.value // this would trigger deprecation } .withAbbreviation("--release") .withAbbreviation("-java-output-version") def releaseValue: Option[String] = release.valueSetByUser + def systemPathValue: Option[String] = systemPath.valueSetByUser val target = ChoiceSetting("-target", "target", "Target platform for object files.", AllTargetVersions, "8") .withPreSetHook(normalizeTarget) @@ -90,7 +93,6 @@ trait StandardScalaSettings { _: MutableSettings => // .withAbbreviation("--Xtarget") // .withAbbreviation("-Xtarget") .withAbbreviation("-Xunchecked-java-output-version") - .withDeprecationMessage("Use -release instead to compile against the correct platform API.") def targetValue: String = target.valueSetByUser.orElse(releaseValue).getOrElse(target.value) val unchecked = BooleanSetting ("-unchecked", "Enable additional warnings where generated code depends on assumptions. See also -Wconf.") withAbbreviation "--unchecked" withPostSetHook { s => if (s.value) Wconf.tryToSet(List(s"cat=unchecked:w")) diff --git a/src/compiler/scala/tools/util/PathResolver.scala b/src/compiler/scala/tools/util/PathResolver.scala index e37ac5b74884..9a4be23f8c04 100644 --- a/src/compiler/scala/tools/util/PathResolver.scala +++ b/src/compiler/scala/tools/util/PathResolver.scala @@ -270,7 +270,7 @@ final class PathResolver(settings: Settings, closeableRegistry: CloseableRegistr sourcesInPath(sourcePath) // 7. The Scala source path. ) - private def jrt: List[ClassPath] = JrtClassPath.apply(settings.releaseValue, settings.unsafe.valueSetByUser, closeableRegistry) + private def jrt: List[ClassPath] = JrtClassPath.apply(settings.releaseValue, settings.systemPathValue, settings.unsafe.valueSetByUser, closeableRegistry) lazy val containers = basis.flatten.distinct