From 54125cef646713903a0599524342e4a5ccf6888b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pa=C5=82ka?= Date: Thu, 10 Dec 2020 16:24:43 +0100 Subject: [PATCH] Fix #8634: Support -release option * A port of https://github.com/scala/scala/pull/6362/files with some improvements * When running scalac on JDK 9+ the -release option assures that code is compiled with classes specific to the release available on the classpath. This applies to classes from the JDK itself and from external jars. If the compilation succeeds, bytecode for the specified release is produced. * -target option gets renamed to -Xtarget. Using -release instead is preferred since -Xtarget sets the bytecode version without any checks so this might lead to producing code that breaks at runime * Fix parsing annotations in classfiles --- .../tools/backend/jvm/BCodeIdiomatic.scala | 30 +++- .../dotc/classpath/DirectoryClassPath.scala | 133 ++++++++++++++---- .../tools/dotc/classpath/FileUtils.scala | 17 ++- .../dotc/classpath/PackageNameUtils.scala | 13 +- .../ZipAndJarFileLookupFactory.scala | 21 +-- .../dotc/classpath/ZipArchiveFileLookup.scala | 3 +- .../tools/dotc/config/PathResolver.scala | 23 +-- .../tools/dotc/config/ScalaSettings.scala | 18 ++- .../dotc/core/classfile/ClassfileParser.scala | 26 ++-- .../src/dotty/tools/io/JDK9Reflectors.java | 106 ++++++++++++++ compiler/src/dotty/tools/io/PlainFile.scala | 31 +++- compiler/src/dotty/tools/io/ZipArchive.scala | 53 ++++--- .../test/dotty/tools/AnnotationsTests.scala | 14 +- .../tools/backend/jvm/DottyBytecodeTest.scala | 22 +++ .../dotc/classpath/JrtClassPathTest.scala | 44 ++++++ .../dotc/classpath/MultiReleaseJarTest.scala | 116 +++++++++++++++ .../test/dotty/tools/io/ZipArchiveTest.scala | 4 +- .../tools/vulpix/TestConfiguration.scala | 4 +- 18 files changed, 567 insertions(+), 111 deletions(-) create mode 100644 compiler/src/dotty/tools/io/JDK9Reflectors.java create mode 100644 compiler/test/dotty/tools/dotc/classpath/JrtClassPathTest.scala create mode 100644 compiler/test/dotty/tools/dotc/classpath/MultiReleaseJarTest.scala diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala b/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala index 249951f2e94a..607af504e1f5 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala @@ -24,12 +24,30 @@ 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 = + val releaseValue = Option(ctx.settings.release.value).filter(_.nonEmpty) + val targetValue = Option(ctx.settings.Xtarget.value).filter(_.nonEmpty) + val defaultTarget = "8" + (releaseValue, targetValue) match + case (Some(release), None) => release + case (None, Some(target)) => target + case (Some(release), Some(_)) => + report.warning(s"The value of ${ctx.settings.Xtarget.name} was overriden by ${ctx.settings.release.name}") + release + case (None, None) => "8" // least supported version by default + + + 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..68db2f5743e5 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -2,7 +2,7 @@ package dotty.tools.dotc package config import dotty.tools.dotc.core.Contexts._ -import dotty.tools.io.{ Directory, PlainDirectory, AbstractFile } +import dotty.tools.io.{ Directory, PlainDirectory, AbstractFile, JDK9Reflectors } import PathResolver.Defaults import rewrites.Rewrites import Settings.Setting @@ -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 = JDK9Reflectors.runtimeVersionMajor(JDK9Reflectors.runtimeVersion()).intValue() + 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 might produce bytecode that will break at runtime. When on JDK 9+, consider -release as a safer alternative.", supportedTargetVersions, "") withAbbreviation "--Xtarget" val XmixinForceForwarders = ChoiceSetting( name = "-Xmixin-force-forwarders", diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 6413c47681ae..6b38ede16ab9 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -57,8 +57,6 @@ class ClassfileParser( classRoot: ClassDenotation, moduleRoot: ClassDenotation)(ictx: Context) { - //println(s"parsing ${classRoot.name.debugString} ${moduleRoot.name.debugString}") - import ClassfileConstants._ import ClassfileParser._ @@ -600,17 +598,8 @@ class ClassfileParser( * return None. */ def parseAnnotation(attrNameIndex: Char, skip: Boolean = false)(using ctx: Context, in: DataReader): Option[ClassfileAnnotation] = try { - val attrType = pool.getType(attrNameIndex) - attrType match - case tp: TypeRef => - // Silently ignore missing annotation classes like javac - if tp.denot.infoOrCompleter.isInstanceOf[StubInfo] then - if ctx.debug then - report.warning(i"Error while parsing annotations in ${classfile}: annotation class $tp not present on classpath") - return None - case _ => - - val nargs = in.nextChar + val attrType = pool.getType(attrNameIndex.toInt) + val nargs = in.nextChar.toInt val argbuf = new ListBuffer[(NameOrString, untpd.Tree | EnumTag)] var hasError = false for (i <- 0 until nargs) { @@ -623,8 +612,15 @@ class ClassfileParser( hasError = !skip } } - if (hasError || skip) None - else Some(ClassfileAnnotation(attrType, argbuf.toList)) + attrType match + case tp: TypeRef if tp.denot.infoOrCompleter.isInstanceOf[StubInfo] => + // Silently ignore missing annotation classes like javac + if ctx.debug then + report.warning(i"Error while parsing annotations in ${classfile}: annotation class $tp not present on classpath") + None + case _ => + if (hasError || skip) None + else Some(ClassfileAnnotation(attrType, argbuf.toList)) } catch { case f: FatalError => throw f // don't eat fatal errors, they mean a class was not found 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..4bdc4c9d4057 100644 --- a/compiler/src/dotty/tools/io/ZipArchive.scala +++ b/compiler/src/dotty/tools/io/ZipArchive.scala @@ -31,7 +31,7 @@ object ZipArchive { * @return A ZipArchive if `file` is a readable zip file, otherwise null. */ def fromFile(file: File): FileZipArchive = fromPath(file.jpath) - def fromPath(jpath: JPath): FileZipArchive = new FileZipArchive(jpath) + def fromPath(jpath: JPath): FileZipArchive = new FileZipArchive(jpath, release = None) def fromManifestURL(url: URL): AbstractFile = new ManifestResources(url) @@ -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]) 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/AnnotationsTests.scala b/compiler/test/dotty/tools/AnnotationsTests.scala index 61d84ac9ab0f..a197ca4bc94e 100644 --- a/compiler/test/dotty/tools/AnnotationsTests.scala +++ b/compiler/test/dotty/tools/AnnotationsTests.scala @@ -43,16 +43,18 @@ class AnnotationsTest: @Test def surviveMissingAnnot: Unit = withJavaCompiled( - VirtualJavaSource("Annot.java", - "public @interface Annot {}"), + VirtualJavaSource("Annot1.java", + "public @interface Annot1 {}"), + VirtualJavaSource("Annot2.java", + "public @interface Annot2 {}"), VirtualJavaSource("A.java", - "@Annot() public class A {}")) { javaOutputDir => - Files.delete(javaOutputDir.resolve("Annot.class")) + "@Annot1() @Annot2() public class A {}")) { javaOutputDir => + Files.delete(javaOutputDir.resolve("Annot1.class")) inCompilerContext(javaOutputDir.toString + File.pathSeparator + TestConfiguration.basicClasspath) { val cls = requiredClass("A") val annots = cls.annotations.map(_.tree) - assert(annots == Nil, - s"class A should have no visible annotations since Annot is not on the classpath, but found: $annots") + assert(annots.length == 1, + s"class A should have only one visible annotation since Annot is not on the classpath, but found: $annots") assert(!ctx.reporter.hasErrors && !ctx.reporter.hasWarnings, s"A missing annotation while parsing a Java class should be silently ignored but: ${ctx.reporter.summary}") } 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..b79a34e1f2ab --- /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: 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: 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/io/ZipArchiveTest.scala b/compiler/test/dotty/tools/io/ZipArchiveTest.scala index 47aa509fe300..d10027070c69 100644 --- a/compiler/test/dotty/tools/io/ZipArchiveTest.scala +++ b/compiler/test/dotty/tools/io/ZipArchiveTest.scala @@ -17,7 +17,7 @@ class ZipArchiveTest { @Test def corruptZip(): Unit = { val f = Files.createTempFile("test", ".jar") - val fza = new FileZipArchive(f) + val fza = new FileZipArchive(f, release = None) try { fza.iterator assert(false) @@ -33,7 +33,7 @@ class ZipArchiveTest { @Test def missingFile(): Unit = { val f = Paths.get("xxx.does.not.exist") - val fza = new FileZipArchive(f) + val fza = new FileZipArchive(f, release = None) try { fza.iterator assert(false) 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" } }