Skip to content

Commit b6e4894

Browse files
committed
Fix scala#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
1 parent 51ac3ae commit b6e4894

15 files changed

+539
-86
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package dotty.tools;
14+
15+
import java.io.IOException;
16+
import java.lang.invoke.MethodHandle;
17+
import java.lang.invoke.MethodHandles;
18+
import java.lang.invoke.MethodType;
19+
import java.util.jar.JarFile;
20+
21+
public final class JDK9Reflectors {
22+
private static final MethodHandle RUNTIME_VERSION_PARSE;
23+
private static final MethodHandle RUNTIME_VERSION;
24+
private static final MethodHandle RUNTIME_VERSION_MAJOR;
25+
private static final MethodHandle NEW_JAR_FILE;
26+
27+
static {
28+
RUNTIME_VERSION_PARSE = lookupRuntimeVersionParse();
29+
RUNTIME_VERSION = lookupRuntimeVersion();
30+
RUNTIME_VERSION_MAJOR = lookupRuntimeVersionMajor();
31+
NEW_JAR_FILE = lookupNewJarFile();
32+
}
33+
34+
// Classes from java.lang.Runtime are not available in JDK 8 so using them explicitly would prevent this file from compiling with JDK 8
35+
// but these methods are not called in runtime when using this version of JDK
36+
37+
public static /*java.lang.Runtime.Version*/ Object runtimeVersionParse(String string) {
38+
try {
39+
return RUNTIME_VERSION_PARSE == null ? null : RUNTIME_VERSION_PARSE.invoke(string);
40+
} catch (Throwable t) {
41+
return null;
42+
}
43+
}
44+
45+
public static /*java.lang.Runtime.Version*/ Object runtimeVersion() {
46+
try {
47+
return RUNTIME_VERSION == null ? null : RUNTIME_VERSION.invoke();
48+
} catch (Throwable t) {
49+
return null;
50+
}
51+
}
52+
53+
public static /*java.lang.Runtime.Version*/ Integer runtimeVersionMajor(/*java.lang.Runtime.Version*/ Object version) {
54+
try {
55+
return RUNTIME_VERSION_MAJOR == null ? null : (Integer) (int) RUNTIME_VERSION_MAJOR.invoke(version);
56+
} catch (Throwable t) {
57+
return null;
58+
}
59+
}
60+
61+
public static JarFile newJarFile(java.io.File file, boolean verify, int mode, /*java.lang.Runtime.Version*/ Object version) throws IOException {
62+
try {
63+
if (version == null) return new JarFile(file, verify, mode);
64+
else {
65+
return NEW_JAR_FILE == null ? null : (JarFile) NEW_JAR_FILE.invoke(file, verify, mode, version);
66+
}
67+
} catch (IOException | IllegalArgumentException | SecurityException ex) {
68+
throw ex;
69+
} catch (Throwable t) {
70+
throw new RuntimeException(t);
71+
}
72+
73+
}
74+
75+
private static MethodHandle lookupRuntimeVersionParse() {
76+
try {
77+
return MethodHandles.lookup().findStatic(runtimeVersionClass(), "parse", MethodType.methodType(runtimeVersionClass(), String.class));
78+
} catch (Throwable t) {
79+
return null;
80+
}
81+
}
82+
private static MethodHandle lookupRuntimeVersion() {
83+
try {
84+
return MethodHandles.lookup().findStatic(java.lang.Runtime.class, "version", MethodType.methodType(runtimeVersionClass()));
85+
} catch (Throwable t) {
86+
return null;
87+
}
88+
}
89+
private static MethodHandle lookupRuntimeVersionMajor() {
90+
try {
91+
return MethodHandles.lookup().findVirtual(runtimeVersionClass(), "major", MethodType.methodType(Integer.TYPE));
92+
} catch (Throwable t) {
93+
return null;
94+
}
95+
}
96+
private static MethodHandle lookupNewJarFile() {
97+
try {
98+
return MethodHandles.lookup().findConstructor(java.util.jar.JarFile.class, MethodType.methodType(void.class, java.io.File.class, java.lang.Boolean.TYPE, Integer.TYPE, runtimeVersionClass()));
99+
} catch (Throwable t) {
100+
return null;
101+
}
102+
}
103+
private static Class<?> runtimeVersionClass() throws ClassNotFoundException {
104+
return Class.forName("java.lang.Runtime$Version");
105+
}
106+
}

compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@ trait BCodeIdiomatic {
2424
import bTypes._
2525
import coreBTypes._
2626

27-
lazy val classfileVersion: Int = ctx.settings.target.value match {
28-
case "jvm-1.5" => asm.Opcodes.V1_5
29-
case "jvm-1.6" => asm.Opcodes.V1_6
30-
case "jvm-1.7" => asm.Opcodes.V1_7
31-
case "jvm-1.8" => asm.Opcodes.V1_8
32-
case "jvm-9" => asm.Opcodes.V9
27+
lazy val target = Option(ctx.settings.release.value).filter(_.nonEmpty).getOrElse(ctx.settings.Xtarget.value)
28+
29+
lazy val classfileVersion: Int = target match {
30+
case "8" => asm.Opcodes.V1_8
31+
case "9" => asm.Opcodes.V9
32+
case "10" => asm.Opcodes.V10
33+
case "11" => asm.Opcodes.V11
34+
case "12" => asm.Opcodes.V12
35+
case "13" => asm.Opcodes.V13
36+
case "14" => asm.Opcodes.V14
37+
case "15" => asm.Opcodes.V15
3338
}
3439

3540
lazy val majorVersion: Int = (classfileVersion & 0xFF)

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import java.io.{File => JFile}
77
import java.net.URL
88
import java.nio.file.{FileSystems, Files}
99

10+
import dotty.tools.JDK9Reflectors
11+
import dotty.tools.dotc.classpath.PackageNameUtils.{packageContains, separatePkgAndClassNames}
1012
import dotty.tools.io.{AbstractFile, PlainFile, ClassPath, ClassRepresentation, EfficientClassPath}
1113
import FileUtils._
14+
import PlainFile.toPlainFile
1215

1316
import scala.collection.JavaConverters._
1417
import scala.collection.immutable.ArraySeq
18+
import scala.util.control.NonFatal
1519

1620
/**
1721
* A trait allowing to look for classpath entries in directories. It provides common logic for
@@ -111,7 +115,7 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo
111115
else Array()
112116
}
113117
protected def getName(f: JFile): String = f.getName
114-
protected def toAbstractFile(f: JFile): AbstractFile = new PlainFile(new dotty.tools.io.File(f.toPath))
118+
protected def toAbstractFile(f: JFile): AbstractFile = f.toPath.toPlainFile
115119
protected def isPackage(f: JFile): Boolean = f.isPackage
116120

117121
assert(dir != null, "Directory file in DirectoryFileLookup cannot be null")
@@ -122,15 +126,33 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo
122126

123127
object JrtClassPath {
124128
import java.nio.file._, java.net.URI
125-
def apply(): Option[ClassPath] =
126-
try {
127-
val fs = FileSystems.getFileSystem(URI.create("jrt:/"))
128-
Some(new JrtClassPath(fs))
129-
}
130-
catch {
131-
case _: ProviderNotFoundException | _: FileSystemNotFoundException =>
132-
None
129+
def apply(release: Option[String]): Option[ClassPath] = {
130+
import scala.util.Properties._
131+
if (!isJavaAtLeast("9")) None
132+
else {
133+
// Longer term we'd like an official API for this in the JDK
134+
// Discussion: http://mail.openjdk.java.net/pipermail/compiler-dev/2018-March/thread.html#11738
135+
136+
val currentMajorVersion: Int = JDK9Reflectors.runtimeVersionMajor(JDK9Reflectors.runtimeVersion()).intValue()
137+
release match {
138+
case Some(v) if v.toInt < currentMajorVersion =>
139+
try {
140+
val ctSym = Paths.get(javaHome).resolve("lib").resolve("ct.sym")
141+
if (Files.notExists(ctSym)) None
142+
else Some(new CtSymClassPath(ctSym, v.toInt))
143+
} catch {
144+
case NonFatal(_) => None
145+
}
146+
case _ =>
147+
try {
148+
val fs = FileSystems.getFileSystem(URI.create("jrt:/"))
149+
Some(new JrtClassPath(fs))
150+
} catch {
151+
case _: ProviderNotFoundException | _: FileSystemNotFoundException => None
152+
}
153+
}
133154
}
155+
}
134156
}
135157

136158
/**
@@ -157,20 +179,15 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No
157179
/** Empty string represents root package */
158180
override private[dotty] def hasPackage(pkg: PackageName): Boolean = packageToModuleBases.contains(pkg.dottedString)
159181

160-
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = {
161-
def matches(packageDottedName: String) =
162-
if (packageDottedName.contains("."))
163-
packageOf(packageDottedName) == inPackage.dottedString
164-
else inPackage.isRoot
165-
packageToModuleBases.keysIterator.filter(matches).map(PackageEntryImpl(_)).toVector
166-
}
182+
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] =
183+
packageToModuleBases.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
167184

168185
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] =
169186
if (inPackage.isRoot) Nil
170187
else
171188
packageToModuleBases.getOrElse(inPackage.dottedString, Nil).flatMap(x =>
172189
Files.list(x.resolve(inPackage.dirPathTrailingSlash)).iterator().asScala.filter(_.getFileName.toString.endsWith(".class"))).map(x =>
173-
ClassFileEntryImpl(new PlainFile(new dotty.tools.io.File(x)))).toVector
190+
ClassFileEntryImpl(x.toPlainFile)).toVector
174191

175192
override private[dotty] def list(inPackage: PackageName): ClassPathEntries =
176193
if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil)
@@ -184,14 +201,75 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No
184201
def findClassFile(className: String): Option[AbstractFile] =
185202
if (!className.contains(".")) None
186203
else {
187-
val inPackage = packageOf(className)
188-
packageToModuleBases.getOrElse(inPackage, Nil).iterator.flatMap{x =>
204+
val (inPackage, _) = separatePkgAndClassNames(className)
205+
packageToModuleBases.getOrElse(inPackage, Nil).iterator.flatMap{ x =>
189206
val file = x.resolve(FileUtils.dirPath(className) + ".class")
190-
if (Files.exists(file)) new PlainFile(new dotty.tools.io.File(file)) :: Nil else Nil
207+
if (Files.exists(file)) file.toPlainFile :: Nil else Nil
191208
}.take(1).toList.headOption
192209
}
193-
private def packageOf(dottedClassName: String): String =
194-
dottedClassName.substring(0, dottedClassName.lastIndexOf("."))
210+
}
211+
212+
/**
213+
* Implementation `ClassPath` based on the \$JAVA_HOME/lib/ct.sym backing http://openjdk.java.net/jeps/247
214+
*/
215+
final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends ClassPath with NoSourcePaths {
216+
import java.nio.file.Path, java.nio.file._
217+
218+
private val fileSystem: FileSystem = FileSystems.newFileSystem(ctSym, null: ClassLoader)
219+
private val root: Path = fileSystem.getRootDirectories.iterator.next
220+
private val roots = Files.newDirectoryStream(root).iterator.asScala.toList
221+
222+
// http://mail.openjdk.java.net/pipermail/compiler-dev/2018-March/011737.html
223+
private def codeFor(major: Int): String = if (major < 10) major.toString else ('A' + (major - 10)).toChar.toString
224+
225+
private val releaseCode: String = codeFor(release)
226+
private def fileNameMatchesRelease(fileName: String) = !fileName.contains("-") && fileName.contains(releaseCode) // exclude `9-modules`
227+
private val rootsForRelease: List[Path] = roots.filter(root => fileNameMatchesRelease(root.getFileName.toString))
228+
229+
// e.g. "java.lang" -> Seq(/876/java/lang, /87/java/lang, /8/java/lang))
230+
private val packageIndex: scala.collection.Map[String, scala.collection.Seq[Path]] = {
231+
val index = collection.mutable.AnyRefMap[String, collection.mutable.ListBuffer[Path]]()
232+
val isJava12OrHigher = scala.util.Properties.isJavaAtLeast("12")
233+
rootsForRelease.foreach(root => Files.walk(root).iterator().asScala.filter(Files.isDirectory(_)).foreach { p =>
234+
val moduleNamePathElementCount = if (isJava12OrHigher) 1 else 0
235+
if (p.getNameCount > root.getNameCount + moduleNamePathElementCount) {
236+
val packageDotted = p.subpath(moduleNamePathElementCount + root.getNameCount, p.getNameCount).toString.replace('/', '.')
237+
index.getOrElseUpdate(packageDotted, new collection.mutable.ListBuffer) += p
238+
}
239+
})
240+
index
241+
}
242+
243+
/** Empty string represents root package */
244+
override private[dotty] def hasPackage(pkg: PackageName) = packageIndex.contains(pkg.dottedString)
245+
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = {
246+
packageIndex.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
247+
}
248+
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = {
249+
if (inPackage.isRoot) Nil
250+
else {
251+
val sigFiles = packageIndex.getOrElse(inPackage.dottedString, Nil).iterator.flatMap(p =>
252+
Files.list(p).iterator.asScala.filter(_.getFileName.toString.endsWith(".sig")))
253+
sigFiles.map(f => ClassFileEntryImpl(f.toPlainFile)).toVector
254+
}
255+
}
256+
257+
override private[dotty] def list(inPackage: PackageName): ClassPathEntries =
258+
if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil)
259+
else ClassPathEntries(packages(inPackage), classes(inPackage))
260+
261+
def asURLs: Seq[URL] = Nil
262+
def asClassPathStrings: Seq[String] = Nil
263+
def findClassFile(className: String): Option[AbstractFile] = {
264+
if (!className.contains(".")) None
265+
else {
266+
val (inPackage, classSimpleName) = separatePkgAndClassNames(className)
267+
packageIndex.getOrElse(inPackage, Nil).iterator.flatMap { p =>
268+
val path = p.resolve(classSimpleName + ".sig")
269+
if (Files.exists(path)) path.toPlainFile :: Nil else Nil
270+
}.take(1).toList.headOption
271+
}
272+
}
195273
}
196274

197275
case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFileEntryImpl] with NoSourcePaths {
@@ -201,9 +279,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile
201279
val relativePath = FileUtils.dirPath(className)
202280
val classFile = new JFile(dir, relativePath + ".class")
203281
if (classFile.exists) {
204-
val wrappedClassFile = new dotty.tools.io.File(classFile.toPath)
205-
val abstractClassFile = new PlainFile(wrappedClassFile)
206-
Some(abstractClassFile)
282+
Some(classFile.toPath.toPlainFile)
207283
}
208284
else None
209285
}
@@ -228,11 +304,7 @@ case class DirectorySourcePath(dir: JFile) extends JFileDirectoryLookup[SourceFi
228304
.map(ext => new JFile(dir, relativePath + "." + ext))
229305
.collectFirst { case file if file.exists() => file }
230306

231-
sourceFile.map { file =>
232-
val wrappedSourceFile = new dotty.tools.io.File(file.toPath)
233-
val abstractSourceFile = new PlainFile(wrappedSourceFile)
234-
abstractSourceFile
235-
}
307+
sourceFile.map(_.toPath.toPlainFile)
236308
}
237309

238310
private[dotty] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage)

compiler/src/dotty/tools/dotc/classpath/FileUtils.scala

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ object FileUtils {
3737
// FIXME: drop last condition when we stop being compatible with Scala 2.11
3838
}
3939

40+
private val SUFFIX_CLASS = ".class"
41+
private val SUFFIX_SCALA = ".scala"
42+
private val SUFFIX_JAVA = ".java"
43+
private val SUFFIX_SIG = ".sig"
44+
4045
def stripSourceExtension(fileName: String): String =
4146
if (endsScala(fileName)) stripClassExtension(fileName)
4247
else if (endsJava(fileName)) stripJavaExtension(fileName)
@@ -46,23 +51,25 @@ object FileUtils {
4651

4752
def dirPathInJar(forPackage: String): String = forPackage.replace('.', '/')
4853

54+
inline private def ends (filename:String, suffix:String) = filename.endsWith(suffix) && filename.length > suffix.length
55+
4956
def endsClass(fileName: String): Boolean =
50-
fileName.length > 6 && fileName.substring(fileName.length - 6) == ".class"
57+
ends (fileName, SUFFIX_CLASS) || fileName.endsWith(SUFFIX_SIG)
5158

5259
def endsScalaOrJava(fileName: String): Boolean =
5360
endsScala(fileName) || endsJava(fileName)
5461

5562
def endsJava(fileName: String): Boolean =
56-
fileName.length > 5 && fileName.substring(fileName.length - 5) == ".java"
63+
ends (fileName, SUFFIX_JAVA)
5764

5865
def endsScala(fileName: String): Boolean =
59-
fileName.length > 6 && fileName.substring(fileName.length - 6) == ".scala"
66+
ends (fileName, SUFFIX_SCALA)
6067

6168
def stripClassExtension(fileName: String): String =
62-
fileName.substring(0, fileName.length - 6) // equivalent of fileName.length - ".class".length
69+
fileName.substring(0, fileName.lastIndexOf('.'))
6370

6471
def stripJavaExtension(fileName: String): String =
65-
fileName.substring(0, fileName.length - 5)
72+
fileName.substring(0, fileName.length - 5) // equivalent of fileName.length - SUFFIX_JAVA.length
6673

6774
// probably it should match a pattern like [a-z_]{1}[a-z0-9_]* but it cannot be changed
6875
// because then some tests in partest don't pass

compiler/src/dotty/tools/dotc/classpath/PackageNameUtils.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ object PackageNameUtils {
1414
* @param fullClassName full class name with package
1515
* @return (package, simple class name)
1616
*/
17-
def separatePkgAndClassNames(fullClassName: String): (String, String) = {
17+
inline def separatePkgAndClassNames(fullClassName: String): (String, String) = {
1818
val lastDotIndex = fullClassName.lastIndexOf('.')
1919
if (lastDotIndex == -1)
2020
(RootPackage, fullClassName)
@@ -23,4 +23,15 @@ object PackageNameUtils {
2323
}
2424

2525
def packagePrefix(inPackage: String): String = if (inPackage == RootPackage) "" else inPackage + "."
26+
27+
/**
28+
* `true` if `packageDottedName` is a package directly nested in `inPackage`, for example:
29+
* - `packageContains("scala", "scala.collection")`
30+
* - `packageContains("", "scala")`
31+
*/
32+
def packageContains(inPackage: String, packageDottedName: String) = {
33+
if (packageDottedName.contains("."))
34+
packageDottedName.startsWith(inPackage) && packageDottedName.lastIndexOf('.') == inPackage.length
35+
else inPackage == ""
36+
}
2637
}

0 commit comments

Comments
 (0)