aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java')
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java401
1 files changed, 401 insertions, 0 deletions
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java
new file mode 100644
index 0000000000..4551fd1813
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java
@@ -0,0 +1,401 @@
+// Copyright 2014 Google Inc. 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 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.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<String>();
+
+ /** Extraneous build informations (key=value) */
+ protected List<String> buildInformations = new ArrayList<String>();
+
+ /** 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, 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;
+ }
+ combiner.addFile(to, date, fileSystem.getInputStream(from));
+ }
+
+ // Copy the jars into the jar file.
+ for (String inputJar : inputJars) {
+ InputStream in = fileSystem.getInputStream(inputJar);
+ try {
+ combiner.addZip(inputJar, in);
+ InputStream inToClose = in;
+ in = null;
+ inToClose.close();
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // Preserve original exception.
+ }
+ }
+ }
+ }
+
+ // 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);
+ }
+
+ public static void main(String[] args) {
+ try {
+ SingleJar singlejar = new SingleJar(new JavaIoFileSystem());
+ System.exit(singlejar.run(Arrays.asList(args)));
+ } catch (IOException e) {
+ System.err.println("SingleJar threw exception : " + e.getMessage());
+ System.exit(1);
+ }
+ }
+}