// 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.buildjar.jarhelper; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; import java.util.Map; import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; /** * 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. */ public class JarCreator extends JarHelper { // 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 final TreeMap<String, Path> jarEntries = new TreeMap<>(); private String manifestFile; private String mainClass; public JarCreator(String fileName) { super(fileName); } /** * 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 */ public boolean addEntry(String entryName, Path path) { if (entryName.startsWith("/")) { entryName = entryName.substring(1); } else if (entryName.length() >= 3 && Character.isLetter(entryName.charAt(0)) && entryName.charAt(1) == ':' && (entryName.charAt(2) == '\\' || entryName.charAt(2) == '/')) { // Windows absolute path, e.g. "D:\foo" or "e:/blah". // Windows paths are case-insensitive, and support both backslashes and forward slashes. entryName = entryName.substring(3); } else if (entryName.startsWith("./")) { entryName = entryName.substring(2); } return jarEntries.put(entryName, 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 */ public boolean addEntry(String entryName, String fileName) { return addEntry(entryName, Paths.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 */ public void addDirectory(String directory) { addDirectory(null, new File(directory)); } /** * Adds the contents of a directory to the Jar file. All files below this directory will be added * to the Jar file using the prefix and the name relative to the directory as the name for the Jar * entry. Always uses '/' as the separator char for the Jar entries. * * @param prefix the prefix to prepend to every Jar entry name found below the directory * @param directory the directory to add to the Jar */ private void addDirectory(String prefix, File directory) { File[] files = directory.listFiles(); if (files != null) { for (File file : files) { String entryName = prefix != null ? prefix + "/" + file.getName() : file.getName(); jarEntries.put(entryName, file.toPath()); if (file.isDirectory()) { addDirectory(entryName, file); } } } } /** * 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> */ public void addRootEntries(Collection<String> entries) { for (String entry : entries) { Path path = Paths.get(entry); jarEntries.put(path.getFileName().toString(), path); } } /** * Sets the main.class entry for the manifest. A value of <code>null</code> (the default) will * omit the entry. * * @param mainClass the fully qualified name of the main class */ public void setMainClass(String mainClass) { this.mainClass = mainClass; } /** * 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. */ public void setManifestFile(String manifestFile) { this.manifestFile = manifestFile; } private byte[] manifestContent() throws IOException { Manifest manifest; if (manifestFile != null) { FileInputStream in = new FileInputStream(manifestFile); manifest = new Manifest(in); } else { manifest = new Manifest(); } Attributes attributes = manifest.getMainAttributes(); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); Attributes.Name createdBy = new Attributes.Name("Created-By"); if (attributes.getValue(createdBy) == null) { attributes.put(createdBy, "blaze"); } if (mainClass != null) { attributes.put(Attributes.Name.MAIN_CLASS, mainClass); } ByteArrayOutputStream out = new 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. */ public void execute() throws IOException { try (OutputStream os = new FileOutputStream(jarFile); BufferedOutputStream bos = new BufferedOutputStream(os); JarOutputStream out = new JarOutputStream(bos)) { // Create the manifest entry in the Jar file writeManifestEntry(out, manifestContent()); for (Map.Entry<String, Path> entry : jarEntries.entrySet()) { copyEntry(out, entry.getKey(), entry.getValue()); } } } /** A simple way to create Jar file using the JarCreator class. */ public static void main(String[] args) { if (args.length < 1) { System.err.println("usage: CreateJar output [root directories]"); System.exit(1); } String output = args[0]; JarCreator createJar = new JarCreator(output); for (int i = 1; i < args.length; i++) { createJar.addDirectory(args[i]); } createJar.setCompression(true); createJar.setNormalize(true); createJar.setVerbose(true); long start = System.currentTimeMillis(); try { createJar.execute(); } catch (IOException e) { e.printStackTrace(); System.exit(1); } long stop = System.currentTimeMillis(); System.err.println((stop - start) + "ms."); } }