blob: 7127ad107be468c4958fdff4abcd8d332b07a987 [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.io.*
import java.nio.file.*
import java.nio.file.Paths.*
import java.nio.file.attribute.BasicFileAttributes
import java.util.*
import java.util.jar.Attributes
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
@Suppress("unused")
/**
* A class for creating Jar files. Allows normalization of Jar entries by setting their timestamp to
* the DOS epoch. All Jar entries are sorted alphabetically.
*/
class JarCreator(
path: Path,
normalize: Boolean = true,
verbose: Boolean = false
) : JarHelper(path, normalize, verbose) {
// Map from Jar entry names to files. Use TreeMap so we can establish a canonical order for the
// entries regardless in what order they get added.
private val jarEntries = TreeMap<String, Path>()
private var manifestFile: String? = null
private var mainClass: String? = null
private var targetLabel: String? = null
private var injectingRuleKind: String? = null
/**
* Adds an entry to the Jar file, normalizing the name.
*
* @param entryName the name of the entry in the Jar file
* @param path the path of the input for the entry
* @return true iff a new entry was added
*/
private fun addEntry(entryName: String, path: Path): Boolean {
var normalizedEntryName = entryName
if (normalizedEntryName.startsWith("/")) {
normalizedEntryName = normalizedEntryName.substring(1)
} else if (normalizedEntryName.length >= 3
&& Character.isLetter(normalizedEntryName[0])
&& normalizedEntryName[1] == ':'
&& (normalizedEntryName[2] == '\\' || normalizedEntryName[2] == '/')
) {
// Windows absolute path, e.g. "D:\foo" or "e:/blah".
// Windows paths are case-insensitive, and support both backslashes and forward slashes.
normalizedEntryName = normalizedEntryName.substring(3)
} else if (normalizedEntryName.startsWith("./")) {
normalizedEntryName = normalizedEntryName.substring(2)
}
return jarEntries.put(normalizedEntryName, path) == null
}
/**
* Adds an entry to the Jar file, normalizing the name.
*
* @param entryName the name of the entry in the Jar file
* @param fileName the name of the input file for the entry
* @return true iff a new entry was added
*/
fun addEntry(entryName: String, fileName: String): Boolean {
return addEntry(entryName, get(fileName))
}
/**
* Adds the contents of a directory to the Jar file. All files below this directory will be added
* to the Jar file using the name relative to the directory as the name for the Jar entry.
*
* @param directory the directory to add to the jar
*/
fun addDirectory(directory: Path) {
if (!Files.exists(directory)) {
throw IllegalArgumentException("directory does not exist: $directory")
}
try {
Files.walkFileTree(
directory,
object : SimpleFileVisitor<Path>() {
@Throws(IOException::class)
override fun preVisitDirectory(path: Path, attrs: BasicFileAttributes): FileVisitResult {
if (path != directory) {
// For consistency with legacy behaviour, include entries for directories except for
// the root.
addEntry(path, /* isDirectory= */ true)
}
return FileVisitResult.CONTINUE
}
@Throws(IOException::class)
override fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult {
addEntry(path, /* isDirectory= */ false)
return FileVisitResult.CONTINUE
}
fun addEntry(path: Path, isDirectory: Boolean) {
val sb = StringBuilder()
var first = true
for (entry in directory.relativize(path)) {
if (!first) {
// use `/` as the directory separator for jar paths, even on Windows
sb.append('/')
}
sb.append(entry.fileName)
first = false
}
if (isDirectory) {
sb.append('/')
}
jarEntries[sb.toString()] = path
}
})
} catch (e: IOException) {
throw UncheckedIOException(e)
}
}
/**
* Adds a collection of entries to the jar, each with a given source path, and with the resulting
* file in the root of the jar.
*
* <pre>
* some/long/path.foo => (path.foo, some/long/path.foo)
</pre> *
*/
fun addRootEntries(entries: Collection<String>) {
for (entry in entries) {
val path = get(entry)
jarEntries[path.fileName.toString()] = path
}
}
/**
* Sets the main.class entry for the manifest. A value of `null` (the default) will
* omit the entry.
*
* @param mainClass the fully qualified name of the main class
*/
fun setMainClass(mainClass: String) {
this.mainClass = mainClass
}
fun setJarOwner(targetLabel: String, injectingRuleKind: String) {
this.targetLabel = targetLabel
this.injectingRuleKind = injectingRuleKind
}
/**
* Sets filename for the manifest content. If this is set the manifest will be read from this file
* otherwise the manifest content will get generated on the fly.
*
* @param manifestFile the filename of the manifest file.
*/
fun setManifestFile(manifestFile: String) {
this.manifestFile = manifestFile
}
@Throws(IOException::class)
private fun manifestContent(): ByteArray {
if (manifestFile != null) {
FileInputStream(manifestFile!!).use { `in` -> return manifestContentImpl(Manifest(`in`)) }
} else {
return manifestContentImpl(Manifest())
}
}
@Throws(IOException::class)
private fun manifestContentImpl(manifest: Manifest): ByteArray {
val attributes = manifest.mainAttributes
attributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
val createdBy = Attributes.Name("Created-By")
if (attributes.getValue(createdBy) == null) {
attributes[createdBy] = "io.bazel.rules.kotlin"
}
if (mainClass != null) {
attributes[Attributes.Name.MAIN_CLASS] = mainClass
}
if (targetLabel != null) {
attributes[TARGET_LABEL] = targetLabel
}
if (injectingRuleKind != null) {
attributes[INJECTING_RULE_KIND] = injectingRuleKind
}
val out = ByteArrayOutputStream()
manifest.write(out)
return out.toByteArray()
}
/**
* Executes the creation of the Jar file.
*
* @throws IOException if the Jar cannot be written or any of the entries cannot be read.
*/
@Throws(IOException::class)
fun execute() {
Files.newOutputStream(jarPath).use { os ->
BufferedOutputStream(os).use { bos ->
JarOutputStream(bos).use { out ->
// Create the manifest entry in the Jar file
writeManifestEntry(out, manifestContent())
for ((key, value) in jarEntries) {
out.copyEntry(key, value)
}
}
}
}
}
}