blob: fb1b99714b64469963dd52580adca9c17ef2c58e [file] [log] [blame]
// Copyright 2014 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 com.google.devtools.build.singlejar;
import static com.google.devtools.build.singlejar.ZipCombiner.DOS_EPOCH;
import com.google.devtools.build.singlejar.DefaultJarEntryFilter.PathFilter;
import com.google.devtools.build.singlejar.ZipCombiner.OutputMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.annotation.concurrent.NotThreadSafe;
/**
* An application that emulates the existing SingleJar tool, using the {@link
* ZipCombiner} class.
*/
@NotThreadSafe
public class SingleJar {
private static final byte NEWLINE_BYTE = (byte) '\n';
private static final String MANIFEST_FILENAME = JarFile.MANIFEST_NAME;
private static final String BUILD_DATA_FILENAME = "build-data.properties";
private final SimpleFileSystem fileSystem;
/** The input jar files we want to combine into the output jar. */
private final List<String> inputJars = new ArrayList<>();
/** Additional resources to be added to the output jar. */
private final List<String> resources = new ArrayList<>();
/** Additional class path resources to be added to the output jar. */
private final List<String> classpathResources = new ArrayList<>();
/** The name of the output Jar file. */
private String outputJar;
/** A filter for what jar entries to include */
private PathFilter allowedPaths = DefaultJarEntryFilter.ANY_PATH;
/** Extra manifest contents. */
private String extraManifestContent;
/** The main class - this is put into the manifest and also into the build info. */
private String mainClass;
/**
* Warn about duplicate resource files, and skip them. Default behavior is to
* give an error message.
*/
private boolean warnDuplicateFiles = false;
/** Indicates whether to set all timestamps to a fixed value. */
private boolean normalize = false;
private OutputMode outputMode = OutputMode.FORCE_STORED;
/** Whether to include build-data.properties file */
protected boolean includeBuildData = true;
/** List of build information properties files */
protected List<String> buildInformationFiles = new ArrayList<>();
/** Extraneous build informations (key=value) */
protected List<String> buildInformations = new ArrayList<>();
/** The (optional) native executable that will be prepended to this JAR. */
private String launcherBin = null;
// Only visible for testing.
protected SingleJar(SimpleFileSystem fileSystem) {
this.fileSystem = fileSystem;
}
/**
* Creates a manifest and returns an input stream for its contents.
*/
private InputStream createManifest() throws IOException {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
attributes.put(new Attributes.Name("Created-By"), "blaze-singlejar");
if (mainClass != null) {
attributes.put(Attributes.Name.MAIN_CLASS, mainClass);
}
if (extraManifestContent != null) {
ByteArrayInputStream in = new ByteArrayInputStream(extraManifestContent.getBytes("UTF8"));
manifest.read(in);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
manifest.write(out);
return new ByteArrayInputStream(out.toByteArray());
}
private InputStream createBuildData() throws IOException {
Properties properties = mergeBuildData();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
properties.store(outputStream, null);
byte[] output = outputStream.toByteArray();
// Properties#store() adds a timestamp comment as first line, delete it.
output = stripFirstLine(output);
return new ByteArrayInputStream(output);
}
static byte[] stripFirstLine(byte[] output) {
int i = 0;
while (i < output.length && output[i] != NEWLINE_BYTE) {
i++;
}
if (i < output.length) {
output = Arrays.copyOfRange(output, i + 1, output.length);
} else {
output = new byte[0];
}
return output;
}
private Properties mergeBuildData() throws IOException {
Properties properties = new Properties();
for (String fileName : buildInformationFiles) {
InputStream file = fileSystem.getInputStream(fileName);
if (file != null) {
properties.load(file);
}
}
// extra properties
for (String info : buildInformations) {
String[] split = info.split("=", 2);
String key = split[0];
String value = "";
if (split.length > 1) {
value = split[1];
}
properties.put(key, value);
}
// finally add generic information
// TODO(bazel-team) do we need to resolve the path to be absolute or canonical?
properties.put("build.target", outputJar);
if (mainClass != null) {
properties.put("main.class", mainClass);
}
return properties;
}
private String getName(String filename) {
int index = filename.lastIndexOf('/');
return index < 0 ? filename : filename.substring(index + 1);
}
// Only visible for testing.
protected int run(List<String> args) throws IOException {
List<String> expandedArgs = new OptionFileExpander(fileSystem).expandArguments(args);
processCommandlineArgs(expandedArgs);
InputStream buildInfo = createBuildData();
ZipCombiner combiner = null;
try {
combiner = new ZipCombiner(outputMode, createEntryFilter(normalize, allowedPaths),
fileSystem.getOutputStream(outputJar));
if (launcherBin != null) {
combiner.prependExecutable(fileSystem.getInputStream(launcherBin));
}
Date date = normalize ? ZipCombiner.DOS_EPOCH : null;
// Add a manifest file.
JarUtils.addMetaInf(combiner, date);
combiner.addFile(MANIFEST_FILENAME, date, createManifest());
if (includeBuildData) {
// Add the build data file.
combiner.addFile(BUILD_DATA_FILENAME, date, buildInfo);
}
// Copy the resources to the top level of the jar file.
for (String classpathResource : classpathResources) {
String entryName = getName(classpathResource);
if (warnDuplicateFiles && combiner.containsFile(entryName)) {
System.err.println("File " + entryName + " clashes with a previous file");
continue;
}
combiner.addFile(entryName, date, fileSystem.getInputStream(classpathResource));
}
// Copy the resources into the jar file.
for (String resource : resources) {
String from;
String to;
int i = resource.indexOf(':');
if (i < 0) {
to = from = resource;
} else {
from = resource.substring(0, i);
to = resource.substring(i + 1);
}
if (warnDuplicateFiles && combiner.containsFile(to)) {
System.err.println("File " + from + " at " + to + " clashes with a previous file");
continue;
}
// Add parent directory entries.
int idx = to.indexOf('/');
while (idx != -1) {
String dir = to.substring(0, idx + 1);
if (!combiner.containsFile(dir)) {
combiner.addDirectory(dir, DOS_EPOCH);
}
idx = to.indexOf('/', idx + 1);
}
combiner.addFile(to, date, fileSystem.getInputStream(from));
}
// Copy the jars into the jar file.
for (String inputJar : inputJars) {
File jar = fileSystem.getFile(inputJar);
combiner.addZip(jar);
}
// Close the output file. If something goes wrong here, delete the file.
combiner.close();
combiner = null;
} finally {
// This part is only executed if an exception occurred.
if (combiner != null) {
try {
// We may end up calling close twice, but that's ok.
combiner.close();
} catch (IOException e) {
// There's already an exception in progress - this won't add any
// additional information.
}
// Ignore return value - there's already an exception in progress.
fileSystem.delete(outputJar);
}
}
return 0;
}
protected ZipEntryFilter createEntryFilter(boolean normalize, PathFilter allowedPaths) {
return new DefaultJarEntryFilter(normalize, allowedPaths);
}
/**
* Collects the arguments for a command line flag until it finds a flag that
* starts with the terminatorPrefix.
*
* @param args
* @param startIndex the start index in the args to collect the flag arguments
* from
* @param flagArguments the collected flag arguments
* @param terminatorPrefix the terminator prefix to stop collecting of
* argument flags
* @return the index of the first argument that started with the
* terminatorPrefix
*/
private static int collectFlagArguments(List<String> args, int startIndex,
List<String> flagArguments, String terminatorPrefix) {
startIndex++;
while (startIndex < args.size()) {
String name = args.get(startIndex);
if (name.startsWith(terminatorPrefix)) {
return startIndex - 1;
}
flagArguments.add(name);
startIndex++;
}
return startIndex;
}
/**
* Returns a single argument for a command line option.
*
* @throws IOException if no more arguments are available
*/
private static String getArgument(List<String> args, int i, String arg) throws IOException {
if (i + 1 < args.size()) {
return args.get(i + 1);
}
throw new IOException(arg + ": missing argument");
}
/**
* Processes the command line arguments.
*
* @throws IOException if one of the files containing options cannot be read
*/
protected void processCommandlineArgs(List<String> args) throws IOException {
List<String> manifestLines = new ArrayList<>();
List<String> prefixes = new ArrayList<>();
for (int i = 0; i < args.size(); i++) {
String arg = args.get(i);
if (arg.equals("--sources")) {
i = collectFlagArguments(args, i, inputJars, "--");
} else if (arg.equals("--resources")) {
i = collectFlagArguments(args, i, resources, "--");
} else if (arg.equals("--classpath_resources")) {
i = collectFlagArguments(args, i, classpathResources, "--");
} else if (arg.equals("--deploy_manifest_lines")) {
i = collectFlagArguments(args, i, manifestLines, "--");
} else if (arg.equals("--build_info_file")) {
buildInformationFiles.add(getArgument(args, i, arg));
i++;
} else if (arg.equals("--extra_build_info")) {
buildInformations.add(getArgument(args, i, arg));
i++;
} else if (arg.equals("--main_class")) {
mainClass = getArgument(args, i, arg);
i++;
} else if (arg.equals("--output")) {
outputJar = getArgument(args, i, arg);
i++;
} else if (arg.equals("--compression")) {
outputMode = OutputMode.FORCE_DEFLATE;
} else if (arg.equals("--dont_change_compression")) {
outputMode = OutputMode.DONT_CARE;
} else if (arg.equals("--normalize")) {
normalize = true;
} else if (arg.equals("--include_prefixes")) {
i = collectFlagArguments(args, i, prefixes, "--");
} else if (arg.equals("--exclude_build_data")) {
includeBuildData = false;
} else if (arg.equals("--warn_duplicate_resources")) {
warnDuplicateFiles = true;
} else if (arg.equals("--java_launcher")) {
launcherBin = getArgument(args, i, arg);
i++;
} else {
throw new IOException("unknown option : '" + arg + "'");
}
}
if (!manifestLines.isEmpty()) {
setExtraManifestContent(joinWithNewlines(manifestLines));
}
if (!prefixes.isEmpty()) {
setPathPrefixes(prefixes);
}
}
private String joinWithNewlines(Iterable<String> lines) {
StringBuilder result = new StringBuilder();
Iterator<String> it = lines.iterator();
if (it.hasNext()) {
result.append(it.next());
}
while (it.hasNext()) {
result.append('\n');
result.append(it.next());
}
return result.toString();
}
private void setExtraManifestContent(String extraManifestContent) {
// The manifest content has to be terminated with a newline character
if (!extraManifestContent.endsWith("\n")) {
extraManifestContent = extraManifestContent + '\n';
}
this.extraManifestContent = extraManifestContent;
}
private void setPathPrefixes(List<String> prefixes) throws IOException {
if (prefixes.isEmpty()) {
throw new IOException(
"Empty set of path prefixes; cowardly refusing to emit an empty jar file");
}
allowedPaths = new PrefixListPathFilter(prefixes);
}
static int singleRun(String[] args) throws IOException {
SingleJar singlejar = new SingleJar(new JavaIoFileSystem());
return singlejar.run(Arrays.asList(args));
}
public static void main(String[] args) {
if (shouldRunInWorker(args)) {
if (!canRunInWorker()) {
System.err.println("Asked to run in a worker, but no worker support");
System.exit(1);
}
try {
runWorker(args);
} catch (Exception e) {
System.err.println("Error running worker : " + e.getMessage());
System.exit(1);
}
return;
}
try {
System.exit(singleRun(args));
} catch (IOException e) {
System.err.println("SingleJar threw exception : " + e.getMessage());
System.exit(1);
}
}
private static void runWorker(String[] args) throws Exception {
// Invocation is done through reflection so that this code will work in bazel open source
// as well. SingleJar is used for bootstrap and thus can not depend on protos (used in
// SingleJarWorker).
Class<?> workerClass = Class.forName("com.google.devtools.build.singlejar.SingleJarWorker");
workerClass.getMethod("main", String[].class).invoke(null, (Object) args);
}
protected static boolean shouldRunInWorker(String[] args) {
return Arrays.asList(args).contains("--persistent_worker");
}
private static boolean canRunInWorker() {
try {
Class.forName("com.google.devtools.build.singlejar.SingleJarWorker");
return true;
} catch (ClassNotFoundException e1) {
return false;
}
}
}