blob: a0a7c95938a54984bcec1b31f3ac5b1ab2432693 [file] [log] [blame]
/*
* Copyright 2018 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.bazel.kotlin.builder.utils.jars
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.regex.Pattern
import java.util.stream.Stream
/**
* Source jar packager for JavaLike files. The placement is discovered from the package entry.
*/
class SourceJarCreator(
path: Path,
verbose: Boolean = false
) : JarHelper(path, normalize = true, verbose = verbose) {
companion object {
private const val BL = """\p{Blank}*"""
private const val COM_BL = """$BL(?:/\*[^\n]*\*/$BL)*"""
private val PKG_PATTERN: Pattern =
Pattern.compile("""^${COM_BL}package$COM_BL([a-zA-Z1-9.]+)$COM_BL(?:;?.*)$""")
@JvmStatic
fun extractPackage(line: String): String? = PKG_PATTERN.matcher(line).takeIf { it.matches() }?.group(1)
private fun isJavaSourceLike(name: String): Boolean {
return name.endsWith(".kt") || name.endsWith(".java")
}
}
sealed class Entry {
class File(val path: Path, val content: ByteArray) : Entry() {
override fun toString(): String = "File $path"
}
object Directory : Entry() {
override fun toString(): String = "Directory"
}
}
private class JarFilenameHelper {
/**
* A map from the directories in the underlying filesystem to jar directory names.
*/
private val directoryToPackageMap = mutableMapOf<Path, String>()
/**
* Entries for which packages could not be located.
*/
private val deferredEntries = mutableMapOf<Path, ByteArray>()
/**
* Locate the directory name for the file as it should appear in the jar.
*
* If the directory could not be located add it to the deferred list and return null.
*
* Files like `package-info.java` could end up getting deferred if they have an annotation embedded on the same
* line or files that have entries such as `/* weird comment */package lala`
*/
fun getFilename(sourceFile: Path, bytes: ByteArray): String? {
return directoryToPackageMap[sourceFile.parent].let { existingPackageName ->
existingPackageName ?: locatePackageLineInBody(bytes).also {
if (it == null) {
deferredEntries[sourceFile] = bytes
}
}?.let { "$it/${sourceFile.fileName}" }
}
}
/**
* Visit any deferred entries.
*
* @param block the visitor, the second param is the package name and may still be null.
*/
fun visitDeferredEntries(block: (Path, String?, ByteArray) -> Unit) {
deferredEntries.forEach { sourceFile, bytes ->
block(sourceFile, directoryToPackageMap[sourceFile.parent], bytes)
}
}
private fun locatePackageLineInBody(bytes: ByteArray): String? =
bytes.inputStream().bufferedReader().use {
var res = it.readLine()
while (res != null) {
extractPackage(res)?.replace('.', '/')?.also {
return@use it
}
res = it.readLine()
}
null
}
}
private val filenameHelper = JarFilenameHelper()
private val entries = TreeMap<String, Entry>()
/**
* Consume a stream of sources, this should contain valid source files and srcjar.
*/
fun addSources(sources: Stream<Path>) {
sources.forEach { path ->
val fileName = path.fileName.toString()
when {
isJavaSourceLike(fileName) -> addJavaLikeSourceFile(path)
fileName.endsWith(".srcjar") -> addSourceJar(path)
}
}
}
/**
* Add a single source jar
*/
private fun addSourceJar(path: Path) {
if(verbose) {
System.err.println("adding source jar: $path")
}
JarFile(path.toFile()).use { jar ->
for (entry in jar.entries()) {
if (!entry.isDirectory) {
if (entry.name.endsWith(".kt") or entry.name.endsWith(".java")) {
jar.getInputStream(entry).readBytes(entry.size.toInt()).also {
addEntry(entry.name, path, it)
}
}
}
}
}
}
/**
* Add a single source file. This method uses the [JarFilenameHelper] so it should be used when jar filename
* correction is desired. It should only be used for Java-Like source files.
*/
private fun addJavaLikeSourceFile(sourceFile: Path) {
val bytes = Files.readAllBytes(sourceFile)
filenameHelper.getFilename(sourceFile, bytes)?.also {
addEntry(it, sourceFile, bytes)
}
}
fun execute() {
if (verbose) {
System.err.println("creating source jar file: $jarPath")
}
filenameHelper.visitDeferredEntries { path, jarFilename, bytes ->
if (jarFilename == null) {
System.err.println("could not determine jar entry name for $path. Body:\n${bytes.toString(Charset.defaultCharset())}}")
} else {
System.err.println("adding deferred source file $path -> $jarFilename")
addEntry(jarFilename, path, bytes)
}
}
Files.newOutputStream(jarPath).use {
JarOutputStream(it).use { out ->
for ((key, value) in entries) {
try {
when (value) {
is Entry.File -> out.copyEntry(key, value.path, value.content)
is Entry.Directory -> out.copyEntry(key)
}
} catch (throwable: Throwable) {
throw RuntimeException("could not copy JarEntry $key $value", throwable)
}
}
}
}
}
private fun addEntry(name: String, path: Path, bytes: ByteArray) {
name.split("/").also {
if (it.size >= 2) {
for (i in ((it.size - 1) downTo 1)) {
val dirName = it.subList(0, i).joinToString("/", postfix = "/")
if (entries.putIfAbsent(dirName, Entry.Directory) != null) {
break
} else if (verbose) {
System.err.println("adding directory: $dirName")
}
}
}
}
val result = entries.putIfAbsent(name, Entry.File(path, bytes))
require(result as? Entry.Directory != null || result == null) {
"source entry jarName: $name from: $path collides with entry from: ${(result as Entry.File).path}"
}
}
}