forked from scala/scala3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathClassfileWriters.scala
293 lines (251 loc) · 12.7 KB
/
ClassfileWriters.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package dotty.tools.backend.jvm
import java.io.{DataOutputStream, File, IOException, BufferedOutputStream, FileOutputStream}
import java.nio.ByteBuffer
import java.nio.channels.{ClosedByInterruptException, FileChannel}
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file._
import java.nio.file.attribute.FileAttribute
import java.util
import java.util.concurrent.ConcurrentHashMap
import java.util.zip.{CRC32, Deflater, ZipEntry, ZipOutputStream}
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Decorators.em
import dotty.tools.dotc.util.chaining.*
import dotty.tools.io.{AbstractFile, PlainFile, VirtualFile}
import dotty.tools.io.PlainFile.toPlainFile
import BTypes.InternalName
import dotty.tools.io.JarArchive
import scala.language.unsafeNulls
class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
import frontendAccess.{compilerSettings, backendReporting}
sealed trait TastyWriter {
def writeTasty(name: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): Unit
}
/**
* The interface to writing classfiles. GeneratedClassHandler calls these methods to generate the
* directory and files that are created, and eventually calls `close` when the writing is complete.
*
* The companion object is responsible for constructing a appropriate and optimal implementation for
* the supplied settings.
*
* Operations are threadsafe.
*/
sealed trait ClassfileWriter extends TastyWriter {
/**
* Write a classfile
*/
def writeClass(name: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile
/**
* Close the writer. Behavior is undefined after a call to `close`.
*/
def close(): Unit
protected def classRelativePath(className: InternalName, suffix: String = ".class"): String =
className.replace('.', '/').nn + suffix
}
object ClassfileWriter {
private def getDirectory(dir: String): Path = Paths.get(dir).nn
def apply(): ClassfileWriter = {
val jarManifestMainClass: Option[String] = compilerSettings.mainClass.orElse {
frontendAccess.getEntryPoints match {
case List(name) => Some(name)
case es =>
if es.isEmpty then backendReporting.log("No Main-Class designated or discovered.")
else backendReporting.log(s"No Main-Class due to multiple entry points:\n ${es.mkString("\n ")}")
None
}
}
// In Scala 2 depenening on cardinality of distinct output dirs MultiClassWriter could have been used
// In Dotty we always use single output directory
val basicClassWriter = new SingleClassWriter(
FileWriter(compilerSettings.outputDirectory, jarManifestMainClass)
)
val withAdditionalFormats =
compilerSettings.dumpClassesDirectory
.map(getDirectory)
.filter{path => Files.exists(path).tap{ok => if !ok then backendReporting.error(em"Output dir does not exist: ${path.toString}")}}
.map(out => FileWriter(out.toPlainFile, None))
.fold[ClassfileWriter](basicClassWriter)(new DebugClassWriter(basicClassWriter, _))
// val enableStats = settings.areStatisticsEnabled && settings.YaddBackendThreads.value == 1
// if (enableStats) new WithStatsWriter(withAdditionalFormats) else
withAdditionalFormats
}
private final class SingleClassWriter(underlying: FileWriter) extends ClassfileWriter {
override def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile = {
underlying.writeFile(classRelativePath(className), bytes)
}
override def writeTasty(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): Unit = {
underlying.writeFile(classRelativePath(className, ".tasty"), bytes)
}
override def close(): Unit = underlying.close()
}
private final class DebugClassWriter(basic: ClassfileWriter, dump: FileWriter) extends ClassfileWriter {
override def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile = {
val outFile = basic.writeClass(className, bytes, sourceFile)
dump.writeFile(classRelativePath(className), bytes)
outFile
}
override def writeTasty(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): Unit = {
basic.writeTasty(className, bytes, sourceFile)
}
override def close(): Unit = {
basic.close()
dump.close()
}
}
}
sealed trait FileWriter {
def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile
def close(): Unit
}
object FileWriter {
def apply(file: AbstractFile, jarManifestMainClass: Option[String]): FileWriter =
if (file.isInstanceOf[JarArchive]) {
val jarCompressionLevel = compilerSettings.jarCompressionLevel
// Writing to non-empty JAR might be an undefined behaviour, e.g. in case if other files where
// created using `AbstractFile.bufferedOutputStream`instead of JarWriter
val jarFile = file.underlyingSource.getOrElse{
throw new IllegalStateException("No underlying source for jar")
}
assert(file.isEmpty, s"Unsafe writing to non-empty JAR: $jarFile")
new JarEntryWriter(jarFile, jarManifestMainClass, jarCompressionLevel)
}
else if (file.isVirtual) new VirtualFileWriter(file)
else if (file.isDirectory) new DirEntryWriter(file.file.toPath.nn)
else throw new IllegalStateException(s"don't know how to handle an output of $file [${file.getClass}]")
}
private final class JarEntryWriter(file: AbstractFile, mainClass: Option[String], compressionLevel: Int) extends FileWriter {
//keep these imports local - avoid confusion with scala naming
import java.util.jar.Attributes.Name.{MANIFEST_VERSION, MAIN_CLASS}
import java.util.jar.{JarOutputStream, Manifest}
val storeOnly = compressionLevel == Deflater.NO_COMPRESSION
val jarWriter: JarOutputStream = {
import scala.util.Properties._
val manifest = new Manifest
val attrs = manifest.getMainAttributes.nn
attrs.put(MANIFEST_VERSION, "1.0")
attrs.put(ScalaCompilerVersion, versionNumberString)
mainClass.foreach(c => attrs.put(MAIN_CLASS, c))
val jar = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(file.file), 64000), manifest)
jar.setLevel(compressionLevel)
if (storeOnly) jar.setMethod(ZipOutputStream.STORED)
jar
}
lazy val crc = new CRC32
override def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile = this.synchronized {
val entry = new ZipEntry(relativePath)
if (storeOnly) {
// When using compression method `STORED`, the ZIP spec requires the CRC and compressed/
// uncompressed sizes to be written before the data. The JarOutputStream could compute the
// values while writing the data, but not patch them into the stream after the fact. So we
// need to pre-compute them here. The compressed size is taken from size.
// https://stackoverflow.com/questions/1206970/how-to-create-uncompressed-zip-archive-in-java/5868403
// With compression method `DEFLATED` JarOutputStream computes and sets the values.
entry.setSize(bytes.length)
crc.reset()
crc.update(bytes)
entry.setCrc(crc.getValue)
}
jarWriter.putNextEntry(entry)
try jarWriter.write(bytes, 0, bytes.length)
finally jarWriter.flush()
// important detail here, even on Windows, Zinc expects the separator within the jar
// to be the system default, (even if in the actual jar file the entry always uses '/').
// see https://github.com/sbt/zinc/blob/dcddc1f9cfe542d738582c43f4840e17c053ce81/internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala#L47
val pathInJar =
if File.separatorChar == '/' then relativePath
else relativePath.replace('/', File.separatorChar)
PlainFile.toPlainFile(Paths.get(s"${file.absolutePath}!$pathInJar"))
}
override def close(): Unit = this.synchronized(jarWriter.close())
}
private final class DirEntryWriter(base: Path) extends FileWriter {
val builtPaths = new ConcurrentHashMap[Path, java.lang.Boolean]()
val noAttributes = Array.empty[FileAttribute[_]]
private val isWindows = scala.util.Properties.isWin
private def checkName(component: Path): Unit = if (isWindows) {
val specials = raw"(?i)CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]".r
val name = component.toString
def warnSpecial(): Unit = backendReporting.warning(em"path component is special Windows device: ${name}")
specials.findPrefixOf(name).foreach(prefix => if (prefix.length == name.length || name(prefix.length) == '.') warnSpecial())
}
def ensureDirForPath(baseDir: Path, filePath: Path): Unit = {
import java.lang.Boolean.TRUE
val parent = filePath.getParent
if (!builtPaths.containsKey(parent)) {
parent.iterator.forEachRemaining(checkName)
try Files.createDirectories(parent, noAttributes: _*)
catch {
case e: FileAlreadyExistsException =>
// `createDirectories` reports this exception if `parent` is an existing symlink to a directory
// but that's fine for us (and common enough, `scalac -d /tmp` on mac targets symlink).
if (!Files.isDirectory(parent))
throw new FileConflictException(s"Can't create directory $parent; there is an existing (non-directory) file in its path", e)
}
builtPaths.put(baseDir, TRUE)
var current = parent
while ((current ne null) && (null ne builtPaths.put(current, TRUE))) {
current = current.getParent
}
}
checkName(filePath.getFileName())
}
// the common case is that we are are creating a new file, and on MS Windows the create and truncate is expensive
// because there is not an options in the windows API that corresponds to this so the truncate is applied as a separate call
// even if the file is new.
// as this is rare, its best to always try to create a new file, and it that fails, then open with truncate if that fails
private val fastOpenOptions = util.EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
private val fallbackOpenOptions = util.EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
override def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile = {
val path = base.resolve(relativePath)
try {
ensureDirForPath(base, path)
val os = if (isWindows) {
try FileChannel.open(path, fastOpenOptions)
catch {
case _: FileAlreadyExistsException => FileChannel.open(path, fallbackOpenOptions)
}
} else FileChannel.open(path, fallbackOpenOptions)
try os.write(ByteBuffer.wrap(bytes), 0L)
catch {
case ex: ClosedByInterruptException =>
try Files.deleteIfExists(path) // don't leave a empty of half-written classfile around after an interrupt
catch { case _: Throwable => () }
throw ex
}
os.close()
} catch {
case e: FileConflictException =>
backendReporting.error(em"error writing ${path.toString}: ${e.getMessage}")
case e: java.nio.file.FileSystemException =>
if (compilerSettings.debug) e.printStackTrace()
backendReporting.error(em"error writing ${path.toString}: ${e.getClass.getName} ${e.getMessage}")
}
AbstractFile.getFile(path)
}
override def close(): Unit = ()
}
private final class VirtualFileWriter(base: AbstractFile) extends FileWriter {
private def getFile(base: AbstractFile, path: String): AbstractFile = {
def ensureDirectory(dir: AbstractFile): AbstractFile =
if (dir.isDirectory) dir
else throw new FileConflictException(s"${base.path}/${path}: ${dir.path} is not a directory")
val components = path.split('/')
var dir = base
for (i <- 0 until components.length - 1) dir = ensureDirectory(dir) subdirectoryNamed components(i).toString
ensureDirectory(dir) fileNamed components.last.toString
}
private def writeBytes(outFile: AbstractFile, bytes: Array[Byte]): Unit = {
val out = new DataOutputStream(outFile.bufferedOutput)
try out.write(bytes, 0, bytes.length)
finally out.close()
}
override def writeFile(relativePath: String, bytes: Array[Byte]): AbstractFile = {
val outFile = getFile(base, relativePath)
writeBytes(outFile, bytes)
outFile
}
override def close(): Unit = ()
}
/** Can't output a file due to the state of the file system. */
class FileConflictException(msg: String, cause: Throwable = null) extends IOException(msg, cause)
}