Skip to content

Commit 181b742

Browse files
committed
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
1 parent a34130f commit 181b742

15 files changed

+531
-86
lines changed

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

+11-6
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

+102-31
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import java.io.{File => JFile}
77
import java.net.URL
88
import java.nio.file.{FileSystems, Files}
99

10-
import dotty.tools.io.{AbstractFile, PlainFile, ClassPath, ClassRepresentation, EfficientClassPath}
10+
import dotty.tools.dotc.classpath.PackageNameUtils.{packageContains, separatePkgAndClassNames}
11+
import dotty.tools.io.{AbstractFile, PlainFile, ClassPath, ClassRepresentation, EfficientClassPath, JDK9Reflectors}
1112
import FileUtils._
13+
import PlainFile.toPlainFile
1214

1315
import scala.collection.JavaConverters._
1416
import scala.collection.immutable.ArraySeq
17+
import scala.util.control.NonFatal
1518

1619
/**
1720
* 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
111114
else Array()
112115
}
113116
protected def getName(f: JFile): String = f.getName
114-
protected def toAbstractFile(f: JFile): AbstractFile = new PlainFile(new dotty.tools.io.File(f.toPath))
117+
protected def toAbstractFile(f: JFile): AbstractFile = f.toPath.toPlainFile
115118
protected def isPackage(f: JFile): Boolean = f.isPackage
116119

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

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

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

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-
}
181+
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] =
182+
packageToModuleBases.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
167183

168184
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] =
169185
if (inPackage.isRoot) Nil
170186
else
171187
packageToModuleBases.getOrElse(inPackage.dottedString, Nil).flatMap(x =>
172188
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
189+
ClassFileEntryImpl(x.toPlainFile)).toVector
174190

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

197274
case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFileEntryImpl] with NoSourcePaths {
@@ -201,9 +278,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile
201278
val relativePath = FileUtils.dirPath(className)
202279
val classFile = new JFile(dir, relativePath + ".class")
203280
if (classFile.exists) {
204-
val wrappedClassFile = new dotty.tools.io.File(classFile.toPath)
205-
val abstractClassFile = new PlainFile(wrappedClassFile)
206-
Some(abstractClassFile)
281+
Some(classFile.toPath.toPlainFile)
207282
}
208283
else None
209284
}
@@ -228,11 +303,7 @@ case class DirectorySourcePath(dir: JFile) extends JFileDirectoryLookup[SourceFi
228303
.map(ext => new JFile(dir, relativePath + "." + ext))
229304
.collectFirst { case file if file.exists() => file }
230305

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

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

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

+12-5
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

+12-1
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
}

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

+12-9
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,22 @@ sealed trait ZipAndJarFileLookupFactory {
2424
private val cache = new FileBasedCache[ClassPath]
2525

2626
def create(zipFile: AbstractFile)(using Context): ClassPath =
27-
if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile)
28-
else createUsingCache(zipFile)
27+
val release = Option(ctx.settings.release.value).filter(_.nonEmpty)
28+
if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile, release)
29+
else createUsingCache(zipFile, release)
2930

30-
protected def createForZipFile(zipFile: AbstractFile): ClassPath
31+
protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath
3132

32-
private def createUsingCache(zipFile: AbstractFile): ClassPath =
33-
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile))
33+
private def createUsingCache(zipFile: AbstractFile, release: Option[String]): ClassPath =
34+
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile, release))
3435
}
3536

3637
/**
3738
* Manages creation of classpath for class files placed in zip and jar files.
3839
* It should be the only way of creating them as it provides caching.
3940
*/
4041
object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
41-
private case class ZipArchiveClassPath(zipFile: File)
42+
private case class ZipArchiveClassPath(zipFile: File, override val release: Option[String])
4243
extends ZipArchiveFileLookup[ClassFileEntryImpl]
4344
with NoSourcePaths {
4445

@@ -141,9 +142,9 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
141142
case class PackageInfo(packageName: String, subpackages: List[AbstractFile])
142143
}
143144

144-
override protected def createForZipFile(zipFile: AbstractFile): ClassPath =
145+
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath =
145146
if (zipFile.file == null) createWithoutUnderlyingFile(zipFile)
146-
else ZipArchiveClassPath(zipFile.file)
147+
else ZipArchiveClassPath(zipFile.file, release)
147148

148149
private def createWithoutUnderlyingFile(zipFile: AbstractFile) = zipFile match {
149150
case manifestRes: ManifestResources =>
@@ -162,6 +163,8 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
162163
private case class ZipArchiveSourcePath(zipFile: File)
163164
extends ZipArchiveFileLookup[SourceFileEntryImpl]
164165
with NoClassPaths {
166+
167+
def release: Option[String] = None
165168

166169
override def asSourcePathString: String = asClassPathString
167170

@@ -171,7 +174,7 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
171174
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource
172175
}
173176

174-
override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file)
177+
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath = ZipArchiveSourcePath(zipFile.file)
175178
}
176179

177180
final class FileBasedCache[T] {

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ import dotty.tools.io.{EfficientClassPath, ClassRepresentation}
1717
*/
1818
trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends EfficientClassPath {
1919
val zipFile: File
20+
def release: Option[String]
2021

2122
assert(zipFile != null, "Zip file in ZipArchiveFileLookup cannot be null")
2223

2324
override def asURLs: Seq[URL] = Seq(zipFile.toURI.toURL)
2425
override def asClassPathStrings: Seq[String] = Seq(zipFile.getPath)
2526

26-
private val archive = new FileZipArchive(zipFile.toPath)
27+
private val archive = new FileZipArchive(zipFile.toPath, release)
2728

2829
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = {
2930
for {

compiler/src/dotty/tools/dotc/config/PathResolver.scala

+13-10
Original file line numberDiff line numberDiff line change
@@ -206,16 +206,19 @@ class PathResolver(using c: Context) {
206206
import classPathFactory._
207207

208208
// Assemble the elements!
209-
def basis: List[Traversable[ClassPath]] = List(
210-
JrtClassPath.apply(), // 1. The Java 9 classpath (backed by the jrt:/ virtual system, if available)
211-
classesInPath(javaBootClassPath), // 2. The Java bootstrap class path.
212-
contentsOfDirsInPath(javaExtDirs), // 3. The Java extension class path.
213-
classesInExpandedPath(javaUserClassPath), // 4. The Java application class path.
214-
classesInPath(scalaBootClassPath), // 5. The Scala boot class path.
215-
contentsOfDirsInPath(scalaExtDirs), // 6. The Scala extension class path.
216-
classesInExpandedPath(userClassPath), // 7. The Scala application class path.
217-
sourcesInPath(sourcePath) // 8. The Scala source path.
218-
)
209+
def basis: List[Traversable[ClassPath]] =
210+
val release = Option(ctx.settings.release.value).filter(_.nonEmpty)
211+
212+
List(
213+
JrtClassPath(release), // 1. The Java 9+ classpath (backed by the jrt:/ virtual system, if available)
214+
classesInPath(javaBootClassPath), // 2. The Java bootstrap class path.
215+
contentsOfDirsInPath(javaExtDirs), // 3. The Java extension class path.
216+
classesInExpandedPath(javaUserClassPath), // 4. The Java application class path.
217+
classesInPath(scalaBootClassPath), // 5. The Scala boot class path.
218+
contentsOfDirsInPath(scalaExtDirs), // 6. The Scala extension class path.
219+
classesInExpandedPath(userClassPath), // 7. The Scala application class path.
220+
sourcesInPath(sourcePath) // 8. The Scala source path.
221+
)
219222

220223
lazy val containers: List[ClassPath] = basis.flatten.distinct
221224

0 commit comments

Comments
 (0)