| // 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; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.io.ByteStreams; |
| import com.google.devtools.build.buildjar.jarhelper.JarCreator; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| /** |
| * Base class for java_library builders. |
| * |
| * <p>Implements common functionality like source files preparation and |
| * output jar creation. |
| */ |
| public abstract class AbstractLibraryBuilder extends CommonJavaLibraryProcessor { |
| |
| /** |
| * Prepares a compilation run. This involves cleaning up temporary dircectories and |
| * writing the classpath files. |
| */ |
| protected void prepareSourceCompilation(JavaLibraryBuildRequest build) throws IOException { |
| File classDirectory = new File(build.getClassDir()); |
| if (classDirectory.exists()) { |
| try { |
| // Necessary for local builds in order to discard previous outputs |
| cleanupOutputDirectory(classDirectory); |
| } catch (IOException e) { |
| throw new IOException("Cannot clean output directory '" + classDirectory + "'", e); |
| } |
| } |
| classDirectory.mkdirs(); |
| |
| setUpSourceJars(build); |
| } |
| |
| public void buildJar(JavaLibraryBuildRequest build) throws IOException { |
| JarCreator jar = new JarCreator(build.getOutputJar()); |
| jar.setNormalize(true); |
| jar.setCompression(build.compressJar()); |
| |
| // The easiest way to handle resource jars is to unpack them into the class directory, just |
| // before we start zipping it up. |
| for (String resourceJar : build.getResourceJars()) { |
| setUpSourceJar(new File(resourceJar), build.getClassDir(), |
| new ArrayList<SourceJarEntryListener>()); |
| } |
| |
| jar.addDirectory(build.getClassDir()); |
| |
| jar.addRootEntries(build.getRootResourceFiles()); |
| addResourceEntries(jar, build.getResourceFiles()); |
| addMessageEntries(jar, build.getMessageFiles()); |
| |
| jar.execute(); |
| } |
| |
| /** |
| * Adds a collection of resource entries. Each entry is a string composed of a |
| * pair of parts separated by a colon ':'. The name of the resource comes from |
| * the second part, and the path to the resource comes from the whole string |
| * with the colon replaced by a slash '/'. |
| * <pre> |
| * prefix:name => (name, prefix/name) |
| * </pre> |
| */ |
| private static void addResourceEntries(JarCreator jar, Collection<String> resources) |
| throws IOException { |
| for (String resource : resources) { |
| int colon = resource.indexOf(':'); |
| if (colon < 0) { |
| throw new IOException("" + resource + ": Illegal resource entry."); |
| } |
| String prefix = resource.substring(0, colon); |
| String name = resource.substring(colon + 1); |
| String path = colon > 0 ? prefix + "/" + name : name; |
| addEntryWithParents(jar, name, path); |
| } |
| } |
| |
| private static void addMessageEntries(JarCreator jar, List<String> messages) |
| throws IOException { |
| for (String message : messages) { |
| int colon = message.indexOf(':'); |
| if (colon < 0) { |
| throw new IOException("" + message + ": Illegal message entry."); |
| } |
| String prefix = message.substring(0, colon); |
| String name = message.substring(colon + 1); |
| String path = colon > 0 ? prefix + "/" + name : name; |
| File messageFile = new File(path); |
| // Ignore empty messages. They get written by the translation importer |
| // when there is no translation for a particular language. |
| if (messageFile.length() != 0L) { |
| addEntryWithParents(jar, name, path); |
| } |
| } |
| } |
| |
| /** |
| * Adds an entry to the jar, making sure that all the parent dirs up to the |
| * base of {@code entry} are also added. |
| * |
| * @param entry the PathFragment of the entry going into the Jar file |
| * @param file the PathFragment of the input file for the entry |
| */ |
| @VisibleForTesting |
| static void addEntryWithParents(JarCreator creator, String entry, String file) { |
| while ((entry != null) && creator.addEntry(entry, file)) { |
| entry = new File(entry).getParent(); |
| file = new File(file).getParent(); |
| } |
| } |
| |
| /** |
| * Internal interface which will listen on each entry of the source jar |
| * files during the source jar setup process. |
| */ |
| protected interface SourceJarEntryListener { |
| void onEntry(ZipEntry entry) throws IOException; |
| void finish() throws IOException; |
| } |
| |
| protected List<SourceJarEntryListener> getSourceJarEntryListeners(JavaLibraryBuildRequest build) { |
| List<SourceJarEntryListener> result = new ArrayList<>(); |
| result.add(new SourceJavaFileCollector(build)); |
| return result; |
| } |
| |
| /** |
| * A SourceJarEntryListener that collects a lists of source Java files from |
| * the source jar files. |
| */ |
| private static class SourceJavaFileCollector implements SourceJarEntryListener { |
| private final List<String> sources; |
| private final JavaLibraryBuildRequest build; |
| |
| public SourceJavaFileCollector(JavaLibraryBuildRequest build) { |
| this.sources = new ArrayList<>(); |
| this.build = build; |
| } |
| |
| @Override |
| public void onEntry(ZipEntry entry) { |
| String entryName = entry.getName(); |
| if (entryName.endsWith(".java")) { |
| sources.add(build.getTempDir() + File.separator + entryName); |
| } |
| } |
| |
| @Override |
| public void finish() { |
| build.getSourceFiles().addAll(sources); |
| } |
| } |
| |
| /** |
| * Extracts the all source jars from the build request into the temporary |
| * directory specified in the build request. Empties the temporary directory, |
| * if it exists. |
| */ |
| private void setUpSourceJars(JavaLibraryBuildRequest build) throws IOException { |
| String sourcesDir = build.getTempDir(); |
| |
| File sourceDirFile = new File(sourcesDir); |
| if (sourceDirFile.exists()) { |
| cleanupDirectory(sourceDirFile, true); |
| } |
| |
| if (build.getSourceJars().isEmpty()) { |
| return; |
| } |
| |
| List<SourceJarEntryListener> listeners = getSourceJarEntryListeners(build); |
| for (String sourceJar : build.getSourceJars()) { |
| setUpSourceJar(new File(sourceJar), sourcesDir, listeners); |
| } |
| for (SourceJarEntryListener listener : listeners) { |
| listener.finish(); |
| } |
| } |
| |
| /** |
| * Extracts the source jar into the directory sourceDir. Calls each of the |
| * SourceJarEntryListeners for each non-directory entry to do additional work. |
| */ |
| private void setUpSourceJar(File sourceJar, String sourceDir, |
| List<SourceJarEntryListener> listeners) |
| throws IOException { |
| try (ZipFile zipFile = new ZipFile(sourceJar)) { |
| Enumeration<? extends ZipEntry> zipEntries = zipFile.entries(); |
| while (zipEntries.hasMoreElements()) { |
| ZipEntry currentEntry = zipEntries.nextElement(); |
| String entryName = currentEntry.getName(); |
| File outputFile = new File(sourceDir, entryName); |
| |
| outputFile.getParentFile().mkdirs(); |
| |
| if (currentEntry.isDirectory()) { |
| outputFile.mkdir(); |
| } else { |
| // Copy the data from the zip file to the output file. |
| try (InputStream in = zipFile.getInputStream(currentEntry); |
| OutputStream out = new FileOutputStream(outputFile)) { |
| ByteStreams.copy(in, out); |
| } |
| |
| for (SourceJarEntryListener listener : listeners) { |
| listener.onEntry(currentEntry); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Recursively cleans up the files beneath the specified output directory. |
| * Does not follow symbolic links. Throws IOException if any deletion fails. |
| * |
| * Will delete all empty directories. |
| * |
| * @param dir the directory to clean up. |
| * @return true if the directory itself was removed as well. |
| */ |
| boolean cleanupOutputDirectory(File dir) throws IOException { |
| return cleanupDirectory(dir, false); |
| } |
| |
| /** |
| * Recursively cleans up the files beneath the specified output directory. |
| * Does not follow symbolic links. Throws IOException if any deletion fails. |
| * If removeEverything is false, keeps .class files if keepClassFilesDuringCleanup() |
| * returns true. If removeEverything is true, removes everything. Will delete all |
| * empty directories. |
| * |
| * @param dir the directory to clean up. |
| * @param removeEverything whether to remove all files, or keep flags.xml/.class files. |
| * @return true if the directory itself was removed as well. |
| */ |
| private boolean cleanupDirectory(File dir, boolean removeEverything) throws IOException { |
| boolean isEmpty = true; |
| File[] files = dir.listFiles(); |
| if (files == null) { return false; } // avoid race condition |
| for (File file : files) { |
| if (file.isDirectory()) { |
| isEmpty &= cleanupDirectory(file, removeEverything); |
| } else if (!removeEverything && keepClassFilesDuringCleanup() && |
| file.getName().endsWith(".class")) { |
| isEmpty = false; |
| } else { |
| file.delete(); |
| } |
| } |
| if (isEmpty) { |
| dir.delete(); |
| } |
| return isEmpty; |
| } |
| |
| /** |
| * Returns true if cleaning the output directory should remove all |
| * .class files in the output directory. |
| */ |
| protected boolean keepClassFilesDuringCleanup() { |
| return false; |
| } |
| |
| } |