diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala b/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala index 249951f2e94a..2a2ae0c272b7 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala @@ -24,12 +24,17 @@ trait BCodeIdiomatic { import bTypes._ import coreBTypes._ - lazy val classfileVersion: Int = ctx.settings.target.value match { - case "jvm-1.5" => asm.Opcodes.V1_5 - case "jvm-1.6" => asm.Opcodes.V1_6 - case "jvm-1.7" => asm.Opcodes.V1_7 - case "jvm-1.8" => asm.Opcodes.V1_8 - case "jvm-9" => asm.Opcodes.V9 + lazy val target = Option(ctx.settings.release.value).filter(_.nonEmpty).getOrElse(ctx.settings.Xtarget.value) + + lazy val classfileVersion: Int = target match { + case "8" => asm.Opcodes.V1_8 + case "9" => asm.Opcodes.V9 + case "10" => asm.Opcodes.V10 + case "11" => asm.Opcodes.V11 + case "12" => asm.Opcodes.V12 + case "13" => asm.Opcodes.V13 + case "14" => asm.Opcodes.V14 + case "15" => asm.Opcodes.V15 } lazy val majorVersion: Int = (classfileVersion & 0xFF) diff --git a/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala b/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala index 8bf8d9f12079..b7f848edf543 100644 --- a/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala +++ b/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala @@ -7,11 +7,14 @@ import java.io.{File => JFile} import java.net.URL import java.nio.file.{FileSystems, Files} -import dotty.tools.io.{AbstractFile, PlainFile, ClassPath, ClassRepresentation, EfficientClassPath} +import dotty.tools.dotc.classpath.PackageNameUtils.{packageContains, separatePkgAndClassNames} +import dotty.tools.io.{AbstractFile, PlainFile, ClassPath, ClassRepresentation, EfficientClassPath, JDK9Reflectors} import FileUtils._ +import PlainFile.toPlainFile import scala.collection.JavaConverters._ import scala.collection.immutable.ArraySeq +import scala.util.control.NonFatal /** * A trait allowing to look for classpath entries in directories. It provides common logic for @@ -111,7 +114,7 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo else Array() } protected def getName(f: JFile): String = f.getName - protected def toAbstractFile(f: JFile): AbstractFile = new PlainFile(new dotty.tools.io.File(f.toPath)) + protected def toAbstractFile(f: JFile): AbstractFile = f.toPath.toPlainFile protected def isPackage(f: JFile): Boolean = f.isPackage assert(dir != null, "Directory file in DirectoryFileLookup cannot be null") @@ -122,15 +125,33 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo object JrtClassPath { import java.nio.file._, java.net.URI - def apply(): Option[ClassPath] = - try { - val fs = FileSystems.getFileSystem(URI.create("jrt:/")) - Some(new JrtClassPath(fs)) - } - catch { - case _: ProviderNotFoundException | _: FileSystemNotFoundException => - None + def apply(release: Option[String]): Option[ClassPath] = { + import scala.util.Properties._ + if (!isJavaAtLeast("9")) None + else { + // Longer term we'd like an official API for this in the JDK + // Discussion: http://mail.openjdk.java.net/pipermail/compiler-dev/2018-March/thread.html#11738 + + val currentMajorVersion: Int = JDK9Reflectors.runtimeVersionMajor(JDK9Reflectors.runtimeVersion()).intValue() + release match { + case Some(v) if v.toInt < currentMajorVersion => + try { + val ctSym = Paths.get(javaHome).resolve("lib").resolve("ct.sym") + if (Files.notExists(ctSym)) None + else Some(new CtSymClassPath(ctSym, v.toInt)) + } catch { + case NonFatal(_) => None + } + case _ => + try { + val fs = FileSystems.getFileSystem(URI.create("jrt:/")) + Some(new JrtClassPath(fs)) + } catch { + case _: ProviderNotFoundException | _: FileSystemNotFoundException => None + } + } } + } } /** @@ -157,20 +178,15 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No /** Empty string represents root package */ override private[dotty] def hasPackage(pkg: PackageName): Boolean = packageToModuleBases.contains(pkg.dottedString) - override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = { - def matches(packageDottedName: String) = - if (packageDottedName.contains(".")) - packageOf(packageDottedName) == inPackage.dottedString - else inPackage.isRoot - packageToModuleBases.keysIterator.filter(matches).map(PackageEntryImpl(_)).toVector - } + override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = + packageToModuleBases.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = if (inPackage.isRoot) Nil else packageToModuleBases.getOrElse(inPackage.dottedString, Nil).flatMap(x => Files.list(x.resolve(inPackage.dirPathTrailingSlash)).iterator().asScala.filter(_.getFileName.toString.endsWith(".class"))).map(x => - ClassFileEntryImpl(new PlainFile(new dotty.tools.io.File(x)))).toVector + ClassFileEntryImpl(x.toPlainFile)).toVector override private[dotty] def list(inPackage: PackageName): ClassPathEntries = if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil) @@ -184,14 +200,75 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No def findClassFile(className: String): Option[AbstractFile] = if (!className.contains(".")) None else { - val inPackage = packageOf(className) - packageToModuleBases.getOrElse(inPackage, Nil).iterator.flatMap{x => + val (inPackage, _) = separatePkgAndClassNames(className) + packageToModuleBases.getOrElse(inPackage, Nil).iterator.flatMap{ x => val file = x.resolve(FileUtils.dirPath(className) + ".class") - if (Files.exists(file)) new PlainFile(new dotty.tools.io.File(file)) :: Nil else Nil + if (Files.exists(file)) file.toPlainFile :: Nil else Nil }.take(1).toList.headOption } - private def packageOf(dottedClassName: String): String = - dottedClassName.substring(0, dottedClassName.lastIndexOf(".")) +} + +/** + * Implementation `ClassPath` based on the \$JAVA_HOME/lib/ct.sym backing http://openjdk.java.net/jeps/247 + */ +final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends ClassPath with NoSourcePaths { + import java.nio.file.Path, java.nio.file._ + + private val fileSystem: FileSystem = FileSystems.newFileSystem(ctSym, null: ClassLoader) + private val root: Path = fileSystem.getRootDirectories.iterator.next + private val roots = Files.newDirectoryStream(root).iterator.asScala.toList + + // http://mail.openjdk.java.net/pipermail/compiler-dev/2018-March/011737.html + private def codeFor(major: Int): String = if (major < 10) major.toString else ('A' + (major - 10)).toChar.toString + + private val releaseCode: String = codeFor(release) + private def fileNameMatchesRelease(fileName: String) = !fileName.contains("-") && fileName.contains(releaseCode) // exclude `9-modules` + private val rootsForRelease: List[Path] = roots.filter(root => fileNameMatchesRelease(root.getFileName.toString)) + + // e.g. "java.lang" -> Seq(/876/java/lang, /87/java/lang, /8/java/lang)) + private val packageIndex: scala.collection.Map[String, scala.collection.Seq[Path]] = { + val index = collection.mutable.AnyRefMap[String, collection.mutable.ListBuffer[Path]]() + val isJava12OrHigher = scala.util.Properties.isJavaAtLeast("12") + rootsForRelease.foreach(root => Files.walk(root).iterator().asScala.filter(Files.isDirectory(_)).foreach { p => + val moduleNamePathElementCount = if (isJava12OrHigher) 1 else 0 + if (p.getNameCount > root.getNameCount + moduleNamePathElementCount) { + val packageDotted = p.subpath(moduleNamePathElementCount + root.getNameCount, p.getNameCount).toString.replace('/', '.') + index.getOrElseUpdate(packageDotted, new collection.mutable.ListBuffer) += p + } + }) + index + } + + /** Empty string represents root package */ + override private[dotty] def hasPackage(pkg: PackageName) = packageIndex.contains(pkg.dottedString) + override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = { + packageIndex.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector + } + private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = { + if (inPackage.isRoot) Nil + else { + val sigFiles = packageIndex.getOrElse(inPackage.dottedString, Nil).iterator.flatMap(p => + Files.list(p).iterator.asScala.filter(_.getFileName.toString.endsWith(".sig"))) + sigFiles.map(f => ClassFileEntryImpl(f.toPlainFile)).toVector + } + } + + override private[dotty] def list(inPackage: PackageName): ClassPathEntries = + if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil) + else ClassPathEntries(packages(inPackage), classes(inPackage)) + + def asURLs: Seq[URL] = Nil + def asClassPathStrings: Seq[String] = Nil + def findClassFile(className: String): Option[AbstractFile] = { + if (!className.contains(".")) None + else { + val (inPackage, classSimpleName) = separatePkgAndClassNames(className) + packageIndex.getOrElse(inPackage, Nil).iterator.flatMap { p => + val path = p.resolve(classSimpleName + ".sig") + if (Files.exists(path)) path.toPlainFile :: Nil else Nil + }.take(1).toList.headOption + } + } } case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFileEntryImpl] with NoSourcePaths { @@ -201,9 +278,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile val relativePath = FileUtils.dirPath(className) val classFile = new JFile(dir, relativePath + ".class") if (classFile.exists) { - val wrappedClassFile = new dotty.tools.io.File(classFile.toPath) - val abstractClassFile = new PlainFile(wrappedClassFile) - Some(abstractClassFile) + Some(classFile.toPath.toPlainFile) } else None } @@ -228,11 +303,7 @@ case class DirectorySourcePath(dir: JFile) extends JFileDirectoryLookup[SourceFi .map(ext => new JFile(dir, relativePath + "." + ext)) .collectFirst { case file if file.exists() => file } - sourceFile.map { file => - val wrappedSourceFile = new dotty.tools.io.File(file.toPath) - val abstractSourceFile = new PlainFile(wrappedSourceFile) - abstractSourceFile - } + sourceFile.map(_.toPath.toPlainFile) } private[dotty] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage) diff --git a/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala b/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala index 83813a9c4fb5..10ca579fc134 100644 --- a/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala +++ b/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala @@ -37,6 +37,11 @@ object FileUtils { // FIXME: drop last condition when we stop being compatible with Scala 2.11 } + private val SUFFIX_CLASS = ".class" + private val SUFFIX_SCALA = ".scala" + private val SUFFIX_JAVA = ".java" + private val SUFFIX_SIG = ".sig" + def stripSourceExtension(fileName: String): String = if (endsScala(fileName)) stripClassExtension(fileName) else if (endsJava(fileName)) stripJavaExtension(fileName) @@ -46,23 +51,25 @@ object FileUtils { def dirPathInJar(forPackage: String): String = forPackage.replace('.', '/') + inline private def ends (filename:String, suffix:String) = filename.endsWith(suffix) && filename.length > suffix.length + def endsClass(fileName: String): Boolean = - fileName.length > 6 && fileName.substring(fileName.length - 6) == ".class" + ends (fileName, SUFFIX_CLASS) || fileName.endsWith(SUFFIX_SIG) def endsScalaOrJava(fileName: String): Boolean = endsScala(fileName) || endsJava(fileName) def endsJava(fileName: String): Boolean = - fileName.length > 5 && fileName.substring(fileName.length - 5) == ".java" + ends (fileName, SUFFIX_JAVA) def endsScala(fileName: String): Boolean = - fileName.length > 6 && fileName.substring(fileName.length - 6) == ".scala" + ends (fileName, SUFFIX_SCALA) def stripClassExtension(fileName: String): String = - fileName.substring(0, fileName.length - 6) // equivalent of fileName.length - ".class".length + fileName.substring(0, fileName.lastIndexOf('.')) def stripJavaExtension(fileName: String): String = - fileName.substring(0, fileName.length - 5) + fileName.substring(0, fileName.length - 5) // equivalent of fileName.length - SUFFIX_JAVA.length // probably it should match a pattern like [a-z_]{1}[a-z0-9_]* but it cannot be changed // because then some tests in partest don't pass diff --git a/compiler/src/dotty/tools/dotc/classpath/PackageNameUtils.scala b/compiler/src/dotty/tools/dotc/classpath/PackageNameUtils.scala index 303f142b9e60..d90de7de2b6b 100644 --- a/compiler/src/dotty/tools/dotc/classpath/PackageNameUtils.scala +++ b/compiler/src/dotty/tools/dotc/classpath/PackageNameUtils.scala @@ -14,7 +14,7 @@ object PackageNameUtils { * @param fullClassName full class name with package * @return (package, simple class name) */ - def separatePkgAndClassNames(fullClassName: String): (String, String) = { + inline def separatePkgAndClassNames(fullClassName: String): (String, String) = { val lastDotIndex = fullClassName.lastIndexOf('.') if (lastDotIndex == -1) (RootPackage, fullClassName) @@ -23,4 +23,15 @@ object PackageNameUtils { } def packagePrefix(inPackage: String): String = if (inPackage == RootPackage) "" else inPackage + "." + + /** + * `true` if `packageDottedName` is a package directly nested in `inPackage`, for example: + * - `packageContains("scala", "scala.collection")` + * - `packageContains("", "scala")` + */ + def packageContains(inPackage: String, packageDottedName: String) = { + if (packageDottedName.contains(".")) + packageDottedName.startsWith(inPackage) && packageDottedName.lastIndexOf('.') == inPackage.length + else inPackage == "" + } } diff --git a/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala b/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala index eb872abb80f0..99ea4f25ff49 100644 --- a/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala +++ b/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala @@ -24,13 +24,14 @@ sealed trait ZipAndJarFileLookupFactory { private val cache = new FileBasedCache[ClassPath] def create(zipFile: AbstractFile)(using Context): ClassPath = - if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile) - else createUsingCache(zipFile) + val release = Option(ctx.settings.release.value).filter(_.nonEmpty) + if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile, release) + else createUsingCache(zipFile, release) - protected def createForZipFile(zipFile: AbstractFile): ClassPath + protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath - private def createUsingCache(zipFile: AbstractFile): ClassPath = - cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile)) + private def createUsingCache(zipFile: AbstractFile, release: Option[String]): ClassPath = + cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile, release)) } /** @@ -38,7 +39,7 @@ sealed trait ZipAndJarFileLookupFactory { * It should be the only way of creating them as it provides caching. */ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory { - private case class ZipArchiveClassPath(zipFile: File) + private case class ZipArchiveClassPath(zipFile: File, override val release: Option[String]) extends ZipArchiveFileLookup[ClassFileEntryImpl] with NoSourcePaths { @@ -141,9 +142,9 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory { case class PackageInfo(packageName: String, subpackages: List[AbstractFile]) } - override protected def createForZipFile(zipFile: AbstractFile): ClassPath = + override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath = if (zipFile.file == null) createWithoutUnderlyingFile(zipFile) - else ZipArchiveClassPath(zipFile.file) + else ZipArchiveClassPath(zipFile.file, release) private def createWithoutUnderlyingFile(zipFile: AbstractFile) = zipFile match { case manifestRes: ManifestResources => @@ -162,6 +163,8 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory { private case class ZipArchiveSourcePath(zipFile: File) extends ZipArchiveFileLookup[SourceFileEntryImpl] with NoClassPaths { + + def release: Option[String] = None override def asSourcePathString: String = asClassPathString @@ -171,7 +174,7 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory { override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource } - override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file) + override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath = ZipArchiveSourcePath(zipFile.file) } final class FileBasedCache[T] { diff --git a/compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala b/compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala index a06fc429876a..7cf21facf02b 100644 --- a/compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala +++ b/compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala @@ -17,13 +17,14 @@ import dotty.tools.io.{EfficientClassPath, ClassRepresentation} */ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends EfficientClassPath { val zipFile: File + def release: Option[String] assert(zipFile != null, "Zip file in ZipArchiveFileLookup cannot be null") override def asURLs: Seq[URL] = Seq(zipFile.toURI.toURL) override def asClassPathStrings: Seq[String] = Seq(zipFile.getPath) - private val archive = new FileZipArchive(zipFile.toPath) + private val archive = new FileZipArchive(zipFile.toPath, release) override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = { for { diff --git a/compiler/src/dotty/tools/dotc/config/PathResolver.scala b/compiler/src/dotty/tools/dotc/config/PathResolver.scala index 83400795a877..5cbb11739d37 100644 --- a/compiler/src/dotty/tools/dotc/config/PathResolver.scala +++ b/compiler/src/dotty/tools/dotc/config/PathResolver.scala @@ -206,16 +206,19 @@ class PathResolver(using c: Context) { import classPathFactory._ // Assemble the elements! - def basis: List[Traversable[ClassPath]] = List( - JrtClassPath.apply(), // 1. The Java 9 classpath (backed by the jrt:/ virtual system, if available) - classesInPath(javaBootClassPath), // 2. The Java bootstrap class path. - contentsOfDirsInPath(javaExtDirs), // 3. The Java extension class path. - classesInExpandedPath(javaUserClassPath), // 4. The Java application class path. - classesInPath(scalaBootClassPath), // 5. The Scala boot class path. - contentsOfDirsInPath(scalaExtDirs), // 6. The Scala extension class path. - classesInExpandedPath(userClassPath), // 7. The Scala application class path. - sourcesInPath(sourcePath) // 8. The Scala source path. - ) + def basis: List[Traversable[ClassPath]] = + val release = Option(ctx.settings.release.value).filter(_.nonEmpty) + + List( + JrtClassPath(release), // 1. The Java 9+ classpath (backed by the jrt:/ virtual system, if available) + classesInPath(javaBootClassPath), // 2. The Java bootstrap class path. + contentsOfDirsInPath(javaExtDirs), // 3. The Java extension class path. + classesInExpandedPath(javaUserClassPath), // 4. The Java application class path. + classesInPath(scalaBootClassPath), // 5. The Scala boot class path. + contentsOfDirsInPath(scalaExtDirs), // 6. The Scala extension class path. + classesInExpandedPath(userClassPath), // 7. The Scala application class path. + sourcesInPath(sourcePath) // 8. The Scala source path. + ) lazy val containers: List[ClassPath] = basis.flatten.distinct diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 14a1ad51c81e..a65c45748523 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -72,6 +72,19 @@ trait CommonScalaSettings { self: Settings.SettingGroup => } class ScalaSettings extends Settings.SettingGroup with CommonScalaSettings { + private val minTargetVersion = 8 + private val maxTargetVersion = 15 + + private def supportedTargetVersions: List[String] = + (minTargetVersion to maxTargetVersion).toList.map(_.toString) + + protected def supportedReleaseVersions: List[String] = + if scala.util.Properties.isJavaAtLeast("9") then + val jdkVersion = scala.util.Properties.javaSpecVersion.stripPrefix("1.").toInt + val maxVersion = Math.min(jdkVersion, maxTargetVersion) + (minTargetVersion to maxVersion).toList.map(_.toString) + else List() + /** Path related settings */ val semanticdbTarget: Setting[String] = PathSetting("-semanticdb-target", "Specify an alternative output directory for SemanticDB files.", "") @@ -80,8 +93,8 @@ class ScalaSettings extends Settings.SettingGroup with CommonScalaSettings { val explain: Setting[Boolean] = BooleanSetting("-explain", "Explain errors in more detail.") withAbbreviation "--explain" val feature: Setting[Boolean] = BooleanSetting("-feature", "Emit warning and location for usages of features that should be imported explicitly.") withAbbreviation "--feature" val help: Setting[Boolean] = BooleanSetting("-help", "Print a synopsis of standard options.") withAbbreviation "--help" + val release: Setting[String] = ChoiceSetting("-release", "release", "Compile code with classes specific to the given version of the Java platform available on the classpath and emit bytecode for this version.", supportedReleaseVersions, "").withAbbreviation("--release") val source: Setting[String] = ChoiceSetting("-source", "source version", "source version", List("3.0", "3.1", "3.0-migration", "3.1-migration"), "3.0").withAbbreviation("--source") - val target: Setting[String] = ChoiceSetting("-target", "target", "Target platform for object files.", List("jvm-1.8", "jvm-9"), "jvm-1.8") withAbbreviation "--target" val scalajs: Setting[Boolean] = BooleanSetting("-scalajs", "Compile in Scala.js mode (requires scalajs-library.jar on the classpath).") withAbbreviation "--scalajs" val unchecked: Setting[Boolean] = BooleanSetting("-unchecked", "Enable additional warnings where generated code depends on assumptions.") withAbbreviation "--unchecked" val uniqid: Setting[Boolean] = BooleanSetting("-uniqid", "Uniquely tag all identifiers in debugging output.") withAbbreviation "--unique-id" @@ -123,6 +136,7 @@ class ScalaSettings extends Settings.SettingGroup with CommonScalaSettings { val XignoreScala2Macros: Setting[Boolean] = BooleanSetting("-Xignore-scala2-macros", "Ignore errors when compiling code that calls Scala2 macros, these will fail at runtime.") val XimportSuggestionTimeout: Setting[Int] = IntSetting("-Ximport-suggestion-timeout", "Timeout (in ms) for searching for import suggestions when errors are reported.", 8000) val Xsemanticdb: Setting[Boolean] = BooleanSetting("-Xsemanticdb", "Store information in SemanticDB.").withAbbreviation("-Ysemanticdb") + val Xtarget: Setting[String] = ChoiceSetting("-Xtarget", "target", "Emit bytecode for the specified version of the Java platform. This setting is ignored if -release is specified.", supportedTargetVersions, supportedTargetVersions.head) withAbbreviation "--Xtarget" val XmixinForceForwarders = ChoiceSetting( name = "-Xmixin-force-forwarders", diff --git a/compiler/src/dotty/tools/io/JDK9Reflectors.java b/compiler/src/dotty/tools/io/JDK9Reflectors.java new file mode 100644 index 000000000000..1b0ce5deabab --- /dev/null +++ b/compiler/src/dotty/tools/io/JDK9Reflectors.java @@ -0,0 +1,106 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package dotty.tools.io; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.jar.JarFile; + +public final class JDK9Reflectors { + private static final MethodHandle RUNTIME_VERSION_PARSE; + private static final MethodHandle RUNTIME_VERSION; + private static final MethodHandle RUNTIME_VERSION_MAJOR; + private static final MethodHandle NEW_JAR_FILE; + + static { + RUNTIME_VERSION_PARSE = lookupRuntimeVersionParse(); + RUNTIME_VERSION = lookupRuntimeVersion(); + RUNTIME_VERSION_MAJOR = lookupRuntimeVersionMajor(); + NEW_JAR_FILE = lookupNewJarFile(); + } + + // Classes from java.lang.Runtime are not available in JDK 8 so using them explicitly would prevent this file from compiling with JDK 8 + // but these methods are not called in runtime when using this version of JDK + + public static /*java.lang.Runtime.Version*/ Object runtimeVersionParse(String string) { + try { + return RUNTIME_VERSION_PARSE == null ? null : RUNTIME_VERSION_PARSE.invoke(string); + } catch (Throwable t) { + return null; + } + } + + public static /*java.lang.Runtime.Version*/ Object runtimeVersion() { + try { + return RUNTIME_VERSION == null ? null : RUNTIME_VERSION.invoke(); + } catch (Throwable t) { + return null; + } + } + + public static /*java.lang.Runtime.Version*/ Integer runtimeVersionMajor(/*java.lang.Runtime.Version*/ Object version) { + try { + return RUNTIME_VERSION_MAJOR == null ? null : (Integer) (int) RUNTIME_VERSION_MAJOR.invoke(version); + } catch (Throwable t) { + return null; + } + } + + public static JarFile newJarFile(java.io.File file, boolean verify, int mode, /*java.lang.Runtime.Version*/ Object version) throws IOException { + try { + if (version == null) return new JarFile(file, verify, mode); + else { + return NEW_JAR_FILE == null ? null : (JarFile) NEW_JAR_FILE.invoke(file, verify, mode, version); + } + } catch (IOException | IllegalArgumentException | SecurityException ex) { + throw ex; + } catch (Throwable t) { + throw new RuntimeException(t); + } + + } + + private static MethodHandle lookupRuntimeVersionParse() { + try { + return MethodHandles.lookup().findStatic(runtimeVersionClass(), "parse", MethodType.methodType(runtimeVersionClass(), String.class)); + } catch (Throwable t) { + return null; + } + } + private static MethodHandle lookupRuntimeVersion() { + try { + return MethodHandles.lookup().findStatic(java.lang.Runtime.class, "version", MethodType.methodType(runtimeVersionClass())); + } catch (Throwable t) { + return null; + } + } + private static MethodHandle lookupRuntimeVersionMajor() { + try { + return MethodHandles.lookup().findVirtual(runtimeVersionClass(), "major", MethodType.methodType(Integer.TYPE)); + } catch (Throwable t) { + return null; + } + } + private static MethodHandle lookupNewJarFile() { + try { + return MethodHandles.lookup().findConstructor(java.util.jar.JarFile.class, MethodType.methodType(void.class, java.io.File.class, java.lang.Boolean.TYPE, Integer.TYPE, runtimeVersionClass())); + } catch (Throwable t) { + return null; + } + } + private static Class runtimeVersionClass() throws ClassNotFoundException { + return Class.forName("java.lang.Runtime$Version"); + } +} diff --git a/compiler/src/dotty/tools/io/PlainFile.scala b/compiler/src/dotty/tools/io/PlainFile.scala index 1815eecefa90..608e661fdd07 100644 --- a/compiler/src/dotty/tools/io/PlainFile.scala +++ b/compiler/src/dotty/tools/io/PlainFile.scala @@ -7,6 +7,7 @@ package dotty.tools package io import java.io.{InputStream, OutputStream} +import java.nio.file.{InvalidPathException, Paths} /** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */ class PlainDirectory(givenPath: Directory) extends PlainFile(givenPath) { @@ -25,7 +26,30 @@ class PlainFile(val givenPath: Path) extends AbstractFile { dotc.util.Stats.record("new PlainFile") def jpath: JPath = givenPath.jpath - override def underlyingSource: Some[PlainFile] = Some(this) + + override def underlyingSource = { + val fileSystem = jpath.getFileSystem + fileSystem.provider().getScheme match { + case "jar" => + val fileStores = fileSystem.getFileStores.iterator() + if (fileStores.hasNext) { + val jarPath = fileStores.next().name + try { + Some(new PlainFile(new Path(Paths.get(jarPath.stripSuffix(fileSystem.getSeparator))))) + } catch { + case _: InvalidPathException => + None + } + } else None + case "jrt" => + if (jpath.getNameCount > 2 && jpath.startsWith("/modules")) { + // TODO limit this to OpenJDK based JVMs? + val moduleName = jpath.getName(1) + Some(new PlainFile(new Path(Paths.get(System.getProperty("java.home"), "jmods", moduleName.toString + ".jmod")))) + } else None + case _ => None + } + } /** Returns the name of this abstract file. */ @@ -94,3 +118,8 @@ class PlainFile(val givenPath: Path) extends AbstractFile { def lookupNameUnchecked(name: String, directory: Boolean): AbstractFile = new PlainFile(givenPath / name) } + +object PlainFile { + extension (jPath: JPath) + def toPlainFile = new PlainFile(new Path(jPath)) +} diff --git a/compiler/src/dotty/tools/io/ZipArchive.scala b/compiler/src/dotty/tools/io/ZipArchive.scala index de22f50835c0..2736d471caec 100644 --- a/compiler/src/dotty/tools/io/ZipArchive.scala +++ b/compiler/src/dotty/tools/io/ZipArchive.scala @@ -52,7 +52,7 @@ object ZipArchive { } import ZipArchive._ /** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */ -abstract class ZipArchive(override val jpath: JPath) extends AbstractFile with Equals { +abstract class ZipArchive(override val jpath: JPath, release: Option[String]) extends AbstractFile with Equals { self => override def underlyingSource: Option[ZipArchive] = Some(this) @@ -112,9 +112,15 @@ abstract class ZipArchive(override val jpath: JPath) extends AbstractFile with E def close(): Unit } /** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */ -final class FileZipArchive(jpath: JPath) extends ZipArchive(jpath) { +final class FileZipArchive(jpath: JPath, release: Option[String] = None) extends ZipArchive(jpath, release) { private def openZipFile(): ZipFile = try { - new ZipFile(file) + release match { + case Some(r) if file.getName.endsWith(".jar") => + val releaseVersion = JDK9Reflectors.runtimeVersionParse(r) + JDK9Reflectors.newJarFile(file, true, ZipFile.OPEN_READ, releaseVersion) + case _ => + new ZipFile(file) + } } catch { case ioe: IOException => throw new IOException("Error accessing " + file.getPath, ioe) } @@ -128,7 +134,7 @@ final class FileZipArchive(jpath: JPath) extends ZipArchive(jpath) { override def lastModified: Long = time // could be stale override def input: InputStream = { val zipFile = openZipFile() - val entry = zipFile.getEntry(name) + val entry = zipFile.getEntry(name) // with `-release`, returns the correct version under META-INF/versions val `delegate` = zipFile.getInputStream(entry) new FilterInputStream(`delegate`) { override def close(): Unit = { zipFile.close() } @@ -160,20 +166,27 @@ final class FileZipArchive(jpath: JPath) extends ZipArchive(jpath) { try { while (entries.hasMoreElements) { val zipEntry = entries.nextElement - val dir = getDir(dirs, zipEntry) - if (!zipEntry.isDirectory) { - val f = - if (ZipArchive.closeZipFile) - new LazyEntry( - zipEntry.getName(), - zipEntry.getTime(), - zipEntry.getSize().toInt, - dir - ) - else - new LeakyEntry(zipFile, zipEntry, dir) - - dir.entries(f.name) = f + if (!zipEntry.getName.startsWith("META-INF/versions/")) { + val zipEntryVersioned = if (release.isDefined) { + // JARFile will return the entry for the corresponding release-dependent version here under META-INF/versions + zipFile.getEntry(zipEntry.getName) + } else zipEntry + + if (!zipEntry.isDirectory) { + val dir = getDir(dirs, zipEntry) + val f = + if (ZipArchive.closeZipFile) + new LazyEntry( + zipEntry.getName(), + zipEntry.getTime(), + zipEntry.getSize().toInt, + dir + ) + else + new LeakyEntry(zipFile, zipEntryVersioned, dir) + + dir.entries(f.name) = f + } } } } finally { @@ -205,7 +218,7 @@ final class FileZipArchive(jpath: JPath) extends ZipArchive(jpath) { } } -final class ManifestResources(val url: URL) extends ZipArchive(null) { +final class ManifestResources(val url: URL) extends ZipArchive(null, None) { def iterator(): Iterator[AbstractFile] = { val root = new DirEntry("/", null) val dirs = mutable.HashMap[String, DirEntry]("/" -> root) diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTest.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTest.scala index 2ceeec681295..228f657457b8 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTest.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTest.scala @@ -69,6 +69,28 @@ trait DottyBytecodeTest { checkOutput(ctx.settings.outputDir.value) } + def compileCode(scalaSources: List[String], javaSources: List[String] = Nil): AbstractFile = { + given Context = initCtx + + val compiler = new Compiler + val run = compiler.newRun + compiler.newRun.compileFromStrings(scalaSources, javaSources) + ctx.settings.outputDir.value + } + + def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { + import scala.collection.mutable.ListBuffer + def files(dir: AbstractFile): List[(String, Array[Byte])] = { + val res = ListBuffer.empty[(String, Array[Byte])] + for (f <- dir.iterator) { + if (!f.isDirectory) res += ((f.name, f.toByteArray)) + else if (f.name != "." && f.name != "..") res ++= files(f) + } + res.toList + } + files(outDir) + } + protected def loadClassNode(input: InputStream, skipDebugInfo: Boolean = true): ClassNode = { val cr = new ClassReader(input) val cn = new ClassNode() diff --git a/compiler/test/dotty/tools/dotc/classpath/JrtClassPathTest.scala b/compiler/test/dotty/tools/dotc/classpath/JrtClassPathTest.scala new file mode 100644 index 000000000000..b676bb100320 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/classpath/JrtClassPathTest.scala @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ + +package dotty.tools.dotc.classpath + +import dotty.tools.io.ClassPath +import dotty.tools.backend.jvm.AsmUtils + +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +import dotty.tools.dotc.config.PathResolver +import dotty.tools.dotc.core.Contexts.{Context, ContextBase} +import dotty.tools.dotc.classpath.ClassPathFactory + +@RunWith(classOf[JUnit4]) +class JrtClassPathTest { + + @Test def lookupJavaClasses(): Unit = { + given Context = new ContextBase().initialCtx + val specVersion = scala.util.Properties.javaSpecVersion + // Run the test using the JDK8 or 9 provider for rt.jar depending on the platform the test is running on. + val cp: ClassPath = + if (specVersion == "" || specVersion == "1.8") { + val resolver = new PathResolver + val elements = (new ClassPathFactory).classesInPath(resolver.Calculated.javaBootClassPath) + AggregateClassPath(elements) + } + else JrtClassPath(None).get + + assertEquals(Nil, cp.classes("")) + assertTrue(cp.packages("java").toString, cp.packages("java").exists(_.name == "java.lang")) + assertTrue(cp.classes("java.lang").exists(_.name == "Object")) + val jl_Object = cp.classes("java.lang").find(_.name == "Object").get + assertEquals("java/lang/Object", AsmUtils.readClass(jl_Object.file.toByteArray).name) + assertTrue(cp.list("java.lang").packages.exists(_.name == "java.lang.annotation")) + assertTrue(cp.list("java.lang").classesAndSources.exists(_.name == "Object")) + assertTrue(cp.findClass("java.lang.Object").isDefined) + assertTrue(cp.findClassFile("java.lang.Object").isDefined) + } +} diff --git a/compiler/test/dotty/tools/dotc/classpath/MultiReleaseJarTest.scala b/compiler/test/dotty/tools/dotc/classpath/MultiReleaseJarTest.scala new file mode 100644 index 000000000000..2bbc9bf23edf --- /dev/null +++ b/compiler/test/dotty/tools/dotc/classpath/MultiReleaseJarTest.scala @@ -0,0 +1,116 @@ +package dotty.tools.dotc.classpath + +import dotty.tools.dotc.core.Contexts.Context + +import java.io.ByteArrayOutputStream +import java.nio.file.{FileSystems, Files, Path} +import java.util.jar.Attributes +import java.util.jar.Attributes.Name + +import org.junit.Test +import org.junit.Assert._ + +import scala.util.Properties +import scala.collection.JavaConverters._ + +class MultiReleaseJarTest extends dotty.tools.backend.jvm.DottyBytecodeTest { + @Test + def mrJar(): Unit = { + if (!Properties.isJavaAtLeast("9")) { println("skipping mrJar() on old JDK"); return } + + // The test fails if the same jar file gets reused. This might be a caching problem in our classpath implementation + + val jar1 = Files.createTempFile("mr-jar-test-", ".jar") + val jar3 = Files.createTempFile("mr-jar-test-", ".jar") + val jar2 = Files.createTempFile("mr-jar-test-", ".jar") + + def classBytes(code: String): Array[Byte] = + val outDir = compileCode(code :: Nil) + getGeneratedClassfiles(outDir).head._2 + + val defaultFooDef = "package p1; abstract class Foo { def foo1: Int }" + val defaultBarDef = "package p2; abstract class Bar { def bar1: Int }" + val java9FooDef = "package p1; abstract class Foo { def foo1: Int; def foo2: Int }" + val java10BarDef = "package p2; abstract class Bar { def bar1: Int; def bar2: Int }" + + def apiMethods(jarPath: Path, release: String): Set[String] = { + given ctx as Context = initCtx.fresh + ctx.settings.usejavacp.update(true) + ctx.settings.classpath.update(jarPath.toAbsolutePath.toString) + ctx.settings.release.update(release) + ctx.initialize() + val classNames = Seq("p1.Foo", "p2.Bar") + val classFiles = classNames.flatMap(ctx.platform.classPath.findClassFile) + val classNodes = classFiles.map(classFile => loadClassNode(classFile.input)) + val methodNames = classNodes.flatMap(_.methods.asScala).map(_.name) + methodNames.filter(_ != "").toSet + } + + try { + List(jar1, jar2, jar3).foreach(temp => createZip(temp, List( + "/p1/Foo.class" -> classBytes(defaultFooDef), + "/p2/Bar.class" -> classBytes(defaultBarDef), + "/META-INF/versions/9/p1/Foo.class" -> classBytes(java9FooDef), + "/META-INF/versions/10/p2/Bar.class" -> classBytes(java10BarDef), + "/META-INF/MANIFEST.MF" -> createManifest) + )) + + assertEquals(Set("foo1", "bar1"), apiMethods(jar1, "8")) + assertEquals(Set("foo1", "foo2", "bar1"), apiMethods(jar2, "9")) + + if Properties.isJavaAtLeast("10") then + assertEquals(Set("foo1", "foo2", "bar1", "bar2"), apiMethods(jar3, "10")) + } finally + List(jar1, jar2, jar3).foreach(Files.deleteIfExists) + } + + @Test + def ctSymTest(): Unit = { + if (!Properties.isJavaAtLeast("9")) { println("skipping mrJar() on old JDK"); return } + + def classExists(className: String, release: String): Boolean = { + given ctx as Context = initCtx.fresh + ctx.settings.usejavacp.update(true) + ctx.settings.release.update(release) + ctx.initialize() + val classFile = ctx.platform.classPath.findClassFile(className) + classFile.isDefined + } + + assertFalse(classExists("java.lang.invoke.LambdaMetafactory", "7")) + assertTrue(classExists("java.lang.invoke.LambdaMetafactory", "8")) + assertTrue(classExists("java.lang.invoke.LambdaMetafactory", "9")) + } + + + private def createManifest = { + val manifest = new java.util.jar.Manifest() + manifest.getMainAttributes.put(Name.MANIFEST_VERSION, "1.0") + manifest.getMainAttributes.put(new Attributes.Name("Multi-Release"), String.valueOf(true)) + val os = new ByteArrayOutputStream() + manifest.write(os) + val manifestBytes = os.toByteArray + manifestBytes + } + private def createZip(zipLocation: Path, content: List[(String, Array[Byte])]): Unit = { + val env = new java.util.HashMap[String, String]() + Files.deleteIfExists(zipLocation) + env.put("create", String.valueOf(true)) + val fileUri = zipLocation.toUri + val zipUri = new java.net.URI("jar:" + fileUri.getScheme, fileUri.getPath, null) + val zipfs = FileSystems.newFileSystem(zipUri, env) + try { + try { + for ((internalPath, contentBytes) <- content) { + val internalTargetPath = zipfs.getPath(internalPath) + Files.createDirectories(internalTargetPath.getParent) + Files.write(internalTargetPath, contentBytes) + } + } finally { + if (zipfs != null) zipfs.close() + } + } finally { + zipfs.close() + } + } +} diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index b73c3756fd89..8d1c9fa5cd86 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -9,7 +9,7 @@ object TestConfiguration { val noCheckOptions = Array( "-pagewidth", "120", "-color:never", - "-target", defaultTarget + "-Xtarget", defaultTarget ) val checkOptions = Array( @@ -89,6 +89,6 @@ object TestConfiguration { private def defaultTarget: String = { import scala.util.Properties.isJavaAtLeast - if isJavaAtLeast("9") then "jvm-9" else "jvm-1.8" + if isJavaAtLeast("9") then "9" else "8" } }