blob: f6c20eef8b73da0075faf0497e3a46bd41bcea7a [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.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;
}
}