Skip to content

Commit be60505

Browse files
Backport "Fix declaring product of straight-to-jar compilation" to LTS (#21164)
Backports #20592 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents eebc0f7 + 6e76dbe commit be60505

File tree

6 files changed

+111
-67
lines changed

6 files changed

+111
-67
lines changed

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package dotty.tools.backend.jvm
22

3-
import java.io.{DataOutputStream, IOException, BufferedOutputStream, FileOutputStream}
3+
import java.io.{DataOutputStream, File, IOException, BufferedOutputStream, FileOutputStream}
44
import java.nio.ByteBuffer
55
import java.nio.channels.{ClosedByInterruptException, FileChannel}
66
import java.nio.charset.StandardCharsets.UTF_8
@@ -12,7 +12,7 @@ import java.util.zip.{CRC32, Deflater, ZipEntry, ZipOutputStream}
1212

1313
import dotty.tools.dotc.core.Contexts.*
1414
import dotty.tools.dotc.core.Decorators.em
15-
import dotty.tools.io.{AbstractFile, PlainFile}
15+
import dotty.tools.io.{AbstractFile, PlainFile, VirtualFile}
1616
import dotty.tools.io.PlainFile.toPlainFile
1717
import BTypes.InternalName
1818
import scala.util.chaining._
@@ -22,7 +22,6 @@ import scala.language.unsafeNulls
2222

2323

2424
class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
25-
type NullableFile = AbstractFile | Null
2625
import frontendAccess.{compilerSettings, backendReporting}
2726

2827
sealed trait TastyWriter {
@@ -42,7 +41,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
4241
/**
4342
* Write a classfile
4443
*/
45-
def writeClass(name: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): NullableFile
44+
def writeClass(name: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile
4645

4746

4847
/**
@@ -87,7 +86,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
8786
}
8887

8988
private final class SingleClassWriter(underlying: FileWriter) extends ClassfileWriter {
90-
override def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): NullableFile = {
89+
override def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile = {
9190
underlying.writeFile(classRelativePath(className), bytes)
9291
}
9392
override def writeTasty(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): Unit = {
@@ -99,7 +98,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
9998
}
10099

101100
private final class DebugClassWriter(basic: ClassfileWriter, dump: FileWriter) extends ClassfileWriter {
102-
override def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): NullableFile = {
101+
override def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile = {
103102
val outFile = basic.writeClass(className, bytes, sourceFile)
104103
dump.writeFile(classRelativePath(className), bytes)
105104
outFile
@@ -117,7 +116,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
117116
}
118117

119118
sealed trait FileWriter {
120-
def writeFile(relativePath: String, bytes: Array[Byte]): NullableFile
119+
def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile
121120
def close(): Unit
122121
}
123122

@@ -161,7 +160,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
161160

162161
lazy val crc = new CRC32
163162

164-
override def writeFile(relativePath: String, bytes: Array[Byte]): NullableFile = this.synchronized {
163+
override def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile = this.synchronized {
165164
val entry = new ZipEntry(relativePath)
166165
if (storeOnly) {
167166
// When using compression method `STORED`, the ZIP spec requires the CRC and compressed/
@@ -178,7 +177,13 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
178177
jarWriter.putNextEntry(entry)
179178
try jarWriter.write(bytes, 0, bytes.length)
180179
finally jarWriter.flush()
181-
null
180+
// important detail here, even on Windows, Zinc expects the separator within the jar
181+
// to be the system default, (even if in the actual jar file the entry always uses '/').
182+
// see https://github.com/sbt/zinc/blob/dcddc1f9cfe542d738582c43f4840e17c053ce81/internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala#L47
183+
val pathInJar =
184+
if File.separatorChar == '/' then relativePath
185+
else relativePath.replace('/', File.separatorChar)
186+
PlainFile.toPlainFile(Paths.get(s"${file.absolutePath}!$pathInJar"))
182187
}
183188

184189
override def close(): Unit = this.synchronized(jarWriter.close())
@@ -226,7 +231,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
226231
private val fastOpenOptions = util.EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
227232
private val fallbackOpenOptions = util.EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
228233

229-
override def writeFile(relativePath: String, bytes: Array[Byte]): NullableFile = {
234+
override def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile = {
230235
val path = base.resolve(relativePath)
231236
try {
232237
ensureDirForPath(base, path)
@@ -275,7 +280,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
275280
finally out.close()
276281
}
277282

278-
override def writeFile(relativePath: String, bytes: Array[Byte]):NullableFile = {
283+
override def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile = {
279284
val outFile = getFile(base, relativePath)
280285
writeBytes(outFile, bytes)
281286
outFile

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class PostProcessor(val frontendAccess: PostProcessorFrontendAccess, val bTypes:
4848
if AsmUtils.traceSerializedClassEnabled && internalName.contains(AsmUtils.traceSerializedClassPattern) then
4949
AsmUtils.traceClass(bytes)
5050
val clsFile = classfileWriter.writeClass(internalName, bytes, sourceFile)
51-
if clsFile != null then clazz.onFileCreated(clsFile)
51+
clazz.onFileCreated(clsFile)
5252
}
5353

5454
def sendToDisk(tasty: GeneratedTasty, sourceFile: AbstractFile): Unit = {

compiler/src/dotty/tools/io/JarArchive.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import scala.jdk.CollectionConverters.*
1010
* This class implements an [[AbstractFile]] backed by a jar
1111
* that be can used as the compiler's output directory.
1212
*/
13-
class JarArchive private (root: Directory) extends PlainDirectory(root) {
14-
def close(): Unit = jpath.getFileSystem().close()
13+
class JarArchive private (val jarPath: Path, root: Directory) extends PlainDirectory(root) {
14+
def close(): Unit = this.synchronized(jpath.getFileSystem().close())
1515
def allFileNames(): Iterator[String] =
1616
java.nio.file.Files.walk(jpath).iterator().asScala.map(_.toString)
17+
18+
override def toString: String = jarPath.toString
1719
}
1820

1921
object JarArchive {
@@ -39,6 +41,6 @@ object JarArchive {
3941
}
4042
}
4143
val root = fs.getRootDirectories().iterator.next()
42-
new JarArchive(Directory(root))
44+
new JarArchive(path, Directory(root))
4345
}
4446
}

sbt-bridge/test/xsbt/ExtractUsedNamesSpecification.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package xsbt
22

33
import xsbti.UseScope
4-
import ScalaCompilerForUnitTesting.Callbacks
54

65
import org.junit.{ Test, Ignore }
76
import org.junit.Assert._
@@ -227,9 +226,9 @@ class ExtractUsedNamesSpecification {
227226

228227
def findPatMatUsages(in: String): Set[String] = {
229228
val compilerForTesting = new ScalaCompilerForUnitTesting
230-
val (_, Callbacks(callback, _)) =
229+
val output =
231230
compilerForTesting.compileSrcs(List(List(sealedClass, in)))
232-
val clientNames = callback.usedNamesAndScopes.view.filterKeys(!_.startsWith("base."))
231+
val clientNames = output.analysis.usedNamesAndScopes.view.filterKeys(!_.startsWith("base."))
233232

234233
val names: Set[String] = clientNames.flatMap {
235234
case (_, usages) =>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package xsbt
2+
3+
import org.junit.Assert.*
4+
import org.junit.Ignore
5+
import org.junit.Test
6+
7+
import java.io.File
8+
import java.nio.file.Path
9+
import java.nio.file.Paths
10+
11+
class ProductsSpecification {
12+
13+
@Test
14+
def extractProductsFromJar = {
15+
val src =
16+
"""package example
17+
|
18+
|class A {
19+
| class B
20+
| def foo =
21+
| class C
22+
|}""".stripMargin
23+
val output = compiler.compileSrcsToJar(src)
24+
val srcFile = output.srcFiles.head
25+
val products = output.analysis.productClassesToSources.filter(_._2 == srcFile).keys.toSet
26+
27+
def toPathInJar(className: String): Path =
28+
Paths.get(s"${output.classesOutput}!${className.replace('.', File.separatorChar)}.class")
29+
val expected = Set("example.A", "example.A$B", "example.A$C$1").map(toPathInJar)
30+
assertEquals(products, expected)
31+
}
32+
33+
private def compiler = new ScalaCompilerForUnitTesting
34+
}

sbt-bridge/test/xsbt/ScalaCompilerForUnitTesting.scala

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
/** Adapted from https://github.com/sbt/sbt/blob/0.13/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala */
22
package xsbt
33

4-
import xsbti.compile.{CompileProgress, SingleOutput}
5-
import java.io.File
6-
import xsbti._
7-
import sbt.io.IO
8-
import xsbti.api.{ ClassLike, Def, DependencyContext }
9-
import DependencyContext._
10-
import xsbt.api.SameAPI
11-
import sbt.internal.util.ConsoleLogger
12-
import dotty.tools.io.PlainFile.toPlainFile
134
import dotty.tools.xsbt.CompilerBridge
5+
import sbt.io.IO
6+
import xsbti.*
7+
import xsbti.api.ClassLike
8+
import xsbti.api.DependencyContext.*
9+
import xsbti.compile.SingleOutput
10+
11+
import java.io.File
12+
import java.nio.file.Path
1413

1514
import TestCallback.ExtractedClassDependencies
16-
import ScalaCompilerForUnitTesting.Callbacks
1715

18-
object ScalaCompilerForUnitTesting:
19-
case class Callbacks(analysis: TestCallback, progress: TestCompileProgress)
16+
case class CompileOutput(srcFiles: Seq[VirtualFileRef], classesOutput: Path, analysis: TestCallback, progress: TestCompileProgress)
2017

2118
/**
2219
* Provides common functionality needed for unit tests that require compiling
@@ -25,38 +22,33 @@ object ScalaCompilerForUnitTesting:
2522
class ScalaCompilerForUnitTesting {
2623

2724
def extractEnteredPhases(srcs: String*): Seq[List[String]] = {
28-
val (tempSrcFiles, Callbacks(_, testProgress)) = compileSrcs(srcs: _*)
29-
val run = testProgress.runs.head
30-
tempSrcFiles.map(src => run.unitPhases(src.id))
25+
val output = compileSrcs(srcs*)
26+
val run = output.progress.runs.head
27+
output.srcFiles.map(src => run.unitPhases(src.id))
3128
}
3229

33-
def extractTotal(srcs: String*)(extraSourcePath: String*): Int = {
34-
val (tempSrcFiles, Callbacks(_, testProgress)) = compileSrcs(List(srcs.toList), extraSourcePath.toList)
35-
val run = testProgress.runs.head
36-
run.total
37-
}
30+
def extractTotal(srcs: String*)(extraSourcePath: String*): Int =
31+
compileSrcs(List(srcs.toList), extraSourcePath.toList).progress.runs.head.total
3832

39-
def extractProgressPhases(srcs: String*): List[String] = {
40-
val (_, Callbacks(_, testProgress)) = compileSrcs(srcs: _*)
41-
testProgress.runs.head.phases
42-
}
33+
def extractProgressPhases(srcs: String*): List[String] =
34+
compileSrcs(srcs*).progress.runs.head.phases
4335

4436
/**
4537
* Compiles given source code using Scala compiler and returns API representation
4638
* extracted by ExtractAPI class.
4739
*/
4840
def extractApiFromSrc(src: String): Seq[ClassLike] = {
49-
val (Seq(tempSrcFile), Callbacks(analysisCallback, _)) = compileSrcs(src)
50-
analysisCallback.apis(tempSrcFile)
41+
val output = compileSrcs(src)
42+
output.analysis.apis(output.srcFiles.head)
5143
}
5244

5345
/**
5446
* Compiles given source code using Scala compiler and returns API representation
5547
* extracted by ExtractAPI class.
5648
*/
5749
def extractApisFromSrcs(srcs: List[String]*): Seq[Seq[ClassLike]] = {
58-
val (tempSrcFiles, Callbacks(analysisCallback, _)) = compileSrcs(srcs.toList)
59-
tempSrcFiles.map(analysisCallback.apis)
50+
val output = compileSrcs(srcs.toList)
51+
output.srcFiles.map(output.analysis.apis)
6052
}
6153

6254
/**
@@ -73,15 +65,16 @@ class ScalaCompilerForUnitTesting {
7365
assertDefaultScope: Boolean = true
7466
): Map[String, Set[String]] = {
7567
// we drop temp src file corresponding to the definition src file
76-
val (Seq(_, tempSrcFile), Callbacks(analysisCallback, _)) = compileSrcs(definitionSrc, actualSrc)
68+
val output = compileSrcs(definitionSrc, actualSrc)
69+
val analysis = output.analysis
7770

7871
if (assertDefaultScope) for {
79-
(className, used) <- analysisCallback.usedNamesAndScopes
80-
analysisCallback.TestUsedName(name, scopes) <- used
72+
(className, used) <- analysis.usedNamesAndScopes
73+
analysis.TestUsedName(name, scopes) <- used
8174
} assert(scopes.size() == 1 && scopes.contains(UseScope.Default), s"$className uses $name in $scopes")
8275

83-
val classesInActualSrc = analysisCallback.classNames(tempSrcFile).map(_._1)
84-
classesInActualSrc.map(className => className -> analysisCallback.usedNames(className)).toMap
76+
val classesInActualSrc = analysis.classNames(output.srcFiles.head).map(_._1)
77+
classesInActualSrc.map(className => className -> analysis.usedNames(className)).toMap
8578
}
8679

8780
/**
@@ -91,11 +84,11 @@ class ScalaCompilerForUnitTesting {
9184
* Only the names used in the last src file are returned.
9285
*/
9386
def extractUsedNamesFromSrc(sources: String*): Map[String, Set[String]] = {
94-
val (srcFiles, Callbacks(analysisCallback, _)) = compileSrcs(sources: _*)
95-
srcFiles
87+
val output = compileSrcs(sources*)
88+
output.srcFiles
9689
.map { srcFile =>
97-
val classesInSrc = analysisCallback.classNames(srcFile).map(_._1)
98-
classesInSrc.map(className => className -> analysisCallback.usedNames(className)).toMap
90+
val classesInSrc = output.analysis.classNames(srcFile).map(_._1)
91+
classesInSrc.map(className => className -> output.analysis.usedNames(className)).toMap
9992
}
10093
.reduce(_ ++ _)
10194
}
@@ -113,15 +106,15 @@ class ScalaCompilerForUnitTesting {
113106
* file system-independent way of testing dependencies between source code "files".
114107
*/
115108
def extractDependenciesFromSrcs(srcs: List[List[String]]): ExtractedClassDependencies = {
116-
val (_, Callbacks(testCallback, _)) = compileSrcs(srcs)
109+
val analysis = compileSrcs(srcs).analysis
117110

118-
val memberRefDeps = testCallback.classDependencies collect {
111+
val memberRefDeps = analysis.classDependencies collect {
119112
case (target, src, DependencyByMemberRef) => (src, target)
120113
}
121-
val inheritanceDeps = testCallback.classDependencies collect {
114+
val inheritanceDeps = analysis.classDependencies collect {
122115
case (target, src, DependencyByInheritance) => (src, target)
123116
}
124-
val localInheritanceDeps = testCallback.classDependencies collect {
117+
val localInheritanceDeps = analysis.classDependencies collect {
125118
case (target, src, LocalDependencyByInheritance) => (src, target)
126119
}
127120
ExtractedClassDependencies.fromPairs(memberRefDeps, inheritanceDeps, localInheritanceDeps)
@@ -142,12 +135,20 @@ class ScalaCompilerForUnitTesting {
142135
* The sequence of temporary files corresponding to passed snippets and analysis
143136
* callback is returned as a result.
144137
*/
145-
def compileSrcs(groupedSrcs: List[List[String]], sourcePath: List[String] = Nil): (Seq[VirtualFile], Callbacks) = {
138+
def compileSrcs(groupedSrcs: List[List[String]], sourcePath: List[String] = Nil, compileToJar: Boolean = false): CompileOutput = {
146139
val temp = IO.createTemporaryDirectory
147140
val analysisCallback = new TestCallback
148141
val testProgress = new TestCompileProgress
149-
val classesDir = new File(temp, "classes")
150-
classesDir.mkdir()
142+
val classesOutput =
143+
if (compileToJar) {
144+
val jar = new File(temp, "classes.jar")
145+
jar.createNewFile()
146+
jar
147+
} else {
148+
val dir = new File(temp, "classes")
149+
dir.mkdir()
150+
dir
151+
}
151152

152153
val bridge = new CompilerBridge
153154

@@ -164,16 +165,16 @@ class ScalaCompilerForUnitTesting {
164165
}
165166

166167
val virtualSrcFiles = srcFiles.toArray
167-
val classesDirPath = classesDir.getAbsolutePath.toString
168+
val classesOutputPath = classesOutput.getAbsolutePath()
168169
val output = new SingleOutput:
169-
def getOutputDirectory() = classesDir
170+
def getOutputDirectory() = classesOutput
170171

171172
val maybeSourcePath = if extraFiles.isEmpty then Nil else List("-sourcepath", temp.getAbsolutePath.toString)
172173

173174
bridge.run(
174175
virtualSrcFiles,
175176
new TestDependencyChanges,
176-
Array("-Yforce-sbt-phases", "-classpath", classesDirPath, "-usejavacp", "-d", classesDirPath) ++ maybeSourcePath,
177+
Array("-Yforce-sbt-phases", "-classpath", classesOutputPath, "-usejavacp", "-d", classesOutputPath) ++ maybeSourcePath,
177178
output,
178179
analysisCallback,
179180
new TestReporter,
@@ -185,13 +186,16 @@ class ScalaCompilerForUnitTesting {
185186

186187
srcFiles
187188
}
188-
(files.flatten.toSeq, Callbacks(analysisCallback, testProgress))
189+
CompileOutput(files.flatten.toSeq, classesOutput.toPath, analysisCallback, testProgress)
189190
}
190191

191-
def compileSrcs(srcs: String*): (Seq[VirtualFile], Callbacks) = {
192+
def compileSrcs(srcs: String*): CompileOutput = {
192193
compileSrcs(List(srcs.toList))
193194
}
194195

196+
def compileSrcsToJar(srcs: String*): CompileOutput =
197+
compileSrcs(List(srcs.toList), compileToJar = true)
198+
195199
private def prepareSrcFile(baseDir: File, fileName: String, src: String): VirtualFile = {
196200
val srcFile = new File(baseDir, fileName)
197201
IO.write(srcFile, src)

0 commit comments

Comments
 (0)