diff options
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.java | 401 |
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); + } + } +} |