| /* |
| * 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 paths could not be located during processing. |
| */ |
| 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 getFilenameOrDefer(sourceFile: Path, body: ByteArray): String? = |
| directoryToPackageMap[sourceFile.parent] ?: locatePackagePathOrDefer(sourceFile, body)?.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 locatePackagePathOrDefer(sourceFile: Path, body: ByteArray): String? = |
| body.inputStream().bufferedReader().useLines { |
| it.mapNotNull(::extractPackage).firstOrNull()?.replace('.', '/') |
| }.also { |
| if (it == null) { |
| deferredEntries[sourceFile] = body |
| } |
| } |
| } |
| |
| 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 (isJavaSourceLike(entry.name)) { |
| 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.getFilenameOrDefer(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) { |
| if (verbose) { |
| val body = bytes.toString(Charset.defaultCharset()) |
| System.err.println("""could not determine jar entry name for $path. Body:\n$body}""") |
| } else { |
| // if not verbose silently add files at the root. |
| addEntry(path.fileName.toString(), path, bytes) |
| } |
| } 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}" |
| } |
| } |
| } |