blob: cab2500619024e01671be6f9ab93f62512a02e7d [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.java;
import com.facebook.buck.rules.BuildDependencies;
import com.facebook.buck.rules.Sha1HashCode;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.util.CapturingPrintStream;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.Nullable;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
/**
* Command used to compile java libraries with a variety of ways to handle dependencies.
* <p>
* If {@code buildDependencies} is set to {@link BuildDependencies#FIRST_ORDER_ONLY}, this class
* will invoke javac using {@code declaredClasspathEntries} for the classpath.
* If {@code buildDependencies} is set to {@link BuildDependencies#TRANSITIVE}, this class will
* invoke javac using {@code transitiveClasspathEntries} for the classpath.
* If {@code buildDependencies} is set to {@link BuildDependencies#WARN_ON_TRANSITIVE}, this class
* will first compile using {@code declaredClasspathEntries}, and should that fail fall back to
* {@code transitiveClasspathEntries} but warn the developer about which dependencies were in
* the transitive classpath but not in the declared classpath.
*/
public class JavacInMemoryStep implements Step {
private final String outputDirectory;
private final Set<String> javaSourceFilePaths;
private final JavacOptions javacOptions;
private final Optional<String> pathToOutputAbiFile;
@Nullable
private File abiKeyFile;
@Nullable
private Sha1HashCode abiKey;
private final ImmutableSet<String> transitiveClasspathEntries;
private final ImmutableSet<String> declaredClasspathEntries;
private final Optional<String> invokingRule;
private final BuildDependencies buildDependencies;
private final Optional<SuggestBuildRules> suggestBuildRules;
/**
* Will be {@code true} once {@link #buildWithClasspath(ExecutionContext, Set)} has been invoked.
*/
private AtomicBoolean isExecuted = new AtomicBoolean(false);
private static final Pattern IMPORT_FAILURE =
Pattern.compile("import ([\\w\\.\\*]*);");
private static final Pattern PACKAGE_FAILURE =
Pattern.compile(".*?package ([\\w\\.\\*]*) does not exist");
private static final Pattern ACCESS_FAILURE =
Pattern.compile(".*?error: cannot access ([\\w\\.\\*]*)");
private static final Pattern CLASS_NOT_FOUND =
Pattern.compile(".*?class file for ([\\w\\.\\*]*) not found");
private static final Pattern CLASS_SYMBOL_NOT_FOUND =
Pattern.compile(".*?symbol:\\s*class\\s*([\\w\\.\\*]*)");
private static final ImmutableList<Pattern> MISSING_IMPORT_PATTERNS =
ImmutableList.of(IMPORT_FAILURE,
PACKAGE_FAILURE,
ACCESS_FAILURE,
CLASS_NOT_FOUND,
CLASS_SYMBOL_NOT_FOUND);
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static interface SuggestBuildRules extends
Function<ImmutableSet<String>,ImmutableSet<String>> {}
public JavacInMemoryStep(
String outputDirectory,
Set<String> javaSourceFilePaths,
Set<String> transitiveClasspathEntries,
Set<String> declaredClasspathEntries,
JavacOptions javacOptions,
Optional<String> pathToOutputAbiFile,
Optional<String> invokingRule,
BuildDependencies buildDependencies,
Optional<SuggestBuildRules> suggestBuildRules) {
this.outputDirectory = Preconditions.checkNotNull(outputDirectory);
this.javaSourceFilePaths = ImmutableSet.copyOf(javaSourceFilePaths);
this.transitiveClasspathEntries = ImmutableSet.copyOf(transitiveClasspathEntries);
this.javacOptions = Preconditions.checkNotNull(javacOptions);
this.pathToOutputAbiFile = Preconditions.checkNotNull(pathToOutputAbiFile);
this.declaredClasspathEntries = ImmutableSet.copyOf(declaredClasspathEntries);
this.invokingRule = Preconditions.checkNotNull(invokingRule);
this.buildDependencies = Preconditions.checkNotNull(buildDependencies);
this.suggestBuildRules = Preconditions.checkNotNull(suggestBuildRules);
}
/**
* Returns a list of command-line options to pass to javac. These options reflect
* the configuration of this javac command.
*
* @param context the ExecutionContext with in which javac will run
* @return list of String command-line options.
*/
@VisibleForTesting
protected ImmutableList<String> getOptions(ExecutionContext context,
Set<String> buildClasspathEntries) {
ImmutableList.Builder<String> builder = ImmutableList.builder();
ProjectFilesystem filesystem = context.getProjectFilesystem();
AnnotationProcessingDataDecorator decorator;
if (pathToOutputAbiFile.isPresent()) {
abiKeyFile = filesystem.getFileForRelativePath(pathToOutputAbiFile.get());
decorator = new AbiWritingAnnotationProcessingDataDecorator(abiKeyFile);
} else {
decorator = AnnotationProcessingDataDecorators.identity();
}
javacOptions.appendOptionsToList(builder,
context.getProjectFilesystem().getPathRelativizer(),
decorator);
// verbose flag, if appropriate.
if (context.getVerbosity().shouldUseVerbosityFlagIfAvailable()) {
builder.add("-verbose");
}
// Specify the output directory.
Function<String, Path> pathRelativizer = filesystem.getPathRelativizer();
builder.add("-d").add(pathRelativizer.apply(outputDirectory).toString());
// Build up and set the classpath.
if (!buildClasspathEntries.isEmpty()) {
String classpath = Joiner.on(File.pathSeparator).join(
Iterables.transform(buildClasspathEntries, pathRelativizer));
builder.add("-classpath", classpath);
}
return builder.build();
}
@Override
public final int execute(ExecutionContext context) {
try {
return executeBuild(context);
} finally {
isExecuted.set(true);
}
}
public int executeBuild(ExecutionContext context) {
// Build up the compilation task.
if (buildDependencies == BuildDependencies.FIRST_ORDER_ONLY) {
return buildWithClasspath(context,
ImmutableSet.copyOf(declaredClasspathEntries));
} else if (buildDependencies == BuildDependencies.WARN_ON_TRANSITIVE) {
return tryBuildWithFirstOrderDeps(context);
} else {
return buildWithClasspath(context, getClasspathEntries());
}
}
private int tryBuildWithFirstOrderDeps(ExecutionContext context) {
CapturingPrintStream stdout = new CapturingPrintStream();
CapturingPrintStream stderr = new CapturingPrintStream();
ExecutionContext firstOrderContext = context.createSubContext(stdout, stderr);
int declaredDepsResult = buildWithClasspath(firstOrderContext,
ImmutableSet.copyOf(declaredClasspathEntries));
String firstOrderStdout = stdout.getContentsAsString(Charsets.UTF_8);
String firstOrderStderr = stderr.getContentsAsString(Charsets.UTF_8);
if (declaredDepsResult != 0) {
int transitiveResult = buildWithClasspath(context, getClasspathEntries());
if (transitiveResult == 0) {
ImmutableSet<String> failedImports = findFailedImports(firstOrderStderr);
context.getStdErr().println(String.format("Rule %s builds with its transitive " +
"dependencies but not with its first order dependencies.", invokingRule.or("")));
context.getStdErr().println("The following packages were missing:");
context.getStdErr().println(Joiner.on(LINE_SEPARATOR).join(failedImports));
if (suggestBuildRules.isPresent()) {
context.getStdErr().println("Try adding the following deps:");
context.getStdErr().println(Joiner.on(LINE_SEPARATOR)
.join(suggestBuildRules.get().apply(failedImports)));
}
context.getStdErr().println();
context.getStdErr().println();
}
return transitiveResult;
} else {
context.getStdOut().print(firstOrderStdout);
context.getStdErr().print(firstOrderStderr);
}
return declaredDepsResult;
}
@VisibleForTesting
static ImmutableSet<String> findFailedImports(String output) {
Iterable<String> lines = Splitter.on(LINE_SEPARATOR).split(output);
ImmutableSortedSet.Builder<String> failedImports = ImmutableSortedSet.naturalOrder();
for (String line : lines) {
for (Pattern missingImportPattern : MISSING_IMPORT_PATTERNS) {
Matcher lineMatch = missingImportPattern.matcher(line);
if (lineMatch.matches()) {
failedImports.add(lineMatch.group(1));
break;
}
}
}
return failedImports.build();
}
/**
* @return The classpath entries used to invoke javac.
*/
protected ImmutableSet<String> getClasspathEntries() {
if (buildDependencies == BuildDependencies.TRANSITIVE) {
return transitiveClasspathEntries;
} else {
return declaredClasspathEntries;
}
}
protected int buildWithClasspath(ExecutionContext context, Set<String> buildClasspathEntries) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
Preconditions.checkNotNull(compiler,
"If using JRE instead of JDK, ToolProvider.getSystemJavaCompiler() may be null.");
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationUnits;
try {
compilationUnits = createCompilationUnits(
fileManager, context.getProjectFilesystem().getPathRelativizer());
} catch (IOException e) {
e.printStackTrace(context.getStdErr());
return 1;
}
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
List<String> options = getOptions(context, buildClasspathEntries);
List<String> classNamesForAnnotationProcessing = ImmutableList.of();
Writer compilerOutputWriter = new PrintWriter(context.getStdErr());
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
compilerOutputWriter,
fileManager,
diagnostics,
options,
classNamesForAnnotationProcessing,
compilationUnits);
// Invoke the compilation and inspect the result.
boolean isSuccess = compilationTask.call();
if (isSuccess) {
if (abiKeyFile != null) {
try {
String firstLine = Files.readFirstLine(abiKeyFile, Charsets.UTF_8);
if (firstLine != null) {
abiKey = new Sha1HashCode(firstLine);
}
} catch (IOException e) {
e.printStackTrace(context.getStdErr());
return 1;
}
}
return 0;
} else {
if (context.getVerbosity().shouldPrintStandardInformation()) {
int numErrors = 0;
int numWarnings = 0;
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
Diagnostic.Kind kind = diagnostic.getKind();
if (kind == Diagnostic.Kind.ERROR) {
++numErrors;
} else if (kind == Diagnostic.Kind.WARNING || kind == Diagnostic.Kind.MANDATORY_WARNING) {
++numWarnings;
}
context.getStdErr().println(diagnostic);
}
if (numErrors > 0 || numWarnings > 0) {
context.getStdErr().printf("Errors: %d. Warnings: %d.\n", numErrors, numWarnings);
}
}
return 1;
}
}
private Iterable<? extends JavaFileObject> createCompilationUnits(
StandardJavaFileManager fileManager,
Function<String, Path> pathRelativizer) throws IOException {
List<JavaFileObject> compilationUnits = Lists.newArrayList();
for (String path : javaSourceFilePaths) {
if (path.endsWith(".java")) {
// For an ordinary .java file, create a corresponding JavaFileObject.
Iterable<? extends JavaFileObject> javaFileObjects = fileManager.getJavaFileObjects(
pathRelativizer.apply(path).toFile());
compilationUnits.add(Iterables.getOnlyElement(javaFileObjects));
} else if (path.endsWith(".src.zip")) {
// For a Zip of .java files, create a JavaFileObject for each .java entry.
ZipFile zipFile = new ZipFile(pathRelativizer.apply(path).toFile());
for (Enumeration<? extends ZipEntry> entries = zipFile.entries();
entries.hasMoreElements();
) {
ZipEntry entry = entries.nextElement();
if (!entry.getName().endsWith(".java")) {
continue;
}
compilationUnits.add(new ZipEntryJavaFileObject(zipFile, entry));
}
}
}
return compilationUnits;
}
@Override
public String getDescription(ExecutionContext context) {
StringBuilder builder = new StringBuilder("javac ");
Joiner.on(" ").appendTo(builder, getOptions(context, getClasspathEntries()));
builder.append(" ");
Joiner.on(" ").appendTo(builder, javaSourceFilePaths);
return builder.toString();
}
@Override
public String getShortName() {
return "javac";
}
@VisibleForTesting
Set<String> getSrcs() {
return javaSourceFilePaths;
}
/**
* Returns a SHA-1 hash for the ABI of the Java code compiled by this step.
* <p>
* In order for this method to return a non-null value, it must be invoked after
* {@link #buildWithClasspath(ExecutionContext, Set)}, which must have completed successfully
* (i.e., returned with an exit code of 0).
*/
@Nullable
public Sha1HashCode getAbiKey() {
Preconditions.checkState(isExecuted.get(), "Must execute step before requesting AbiKey.");
// Note that if the rule fails, isExecuted should still be set, but abiKey will be null.
return abiKey;
}
}