/*
* 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.jvm.java;
import com.facebook.buck.event.api.BuckTracing;
import com.facebook.buck.jvm.java.abi.SourceBasedAbiStubber;
import com.facebook.buck.jvm.java.abi.StubGenerator;
import com.facebook.buck.jvm.java.abi.source.api.BootClasspathOracle;
import com.facebook.buck.jvm.java.abi.source.api.FrontendOnlyJavacTaskProxy;
import com.facebook.buck.jvm.java.plugin.PluginLoader;
import com.facebook.buck.jvm.java.plugin.api.BuckJavacTaskListener;
import com.facebook.buck.jvm.java.plugin.api.BuckJavacTaskProxy;
import com.facebook.buck.jvm.java.plugin.api.PluginClassLoader;
import com.facebook.buck.jvm.java.plugin.api.PluginClassLoaderFactory;
import com.facebook.buck.jvm.java.tracing.JavacPhaseEventLogger;
import com.facebook.buck.jvm.java.tracing.TracingTaskListener;
import com.facebook.buck.jvm.java.tracing.TranslatingJavacPhaseTracer;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.zip.CustomJarOutputStream;
import com.facebook.buck.zip.CustomZipOutputStream;
import com.facebook.buck.zip.ZipOutputStreams;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintWriter; // NOPMD required by API
import java.io.Writer;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.Nullable;
import javax.lang.model.SourceVersion;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
/** Command used to compile java libraries with a variety of ways to handle dependencies. */
public abstract class Jsr199Javac implements Javac {
private static final Logger LOG = Logger.get(Jsr199Javac.class);
private static final JavacVersion VERSION = JavacVersion.of("in memory");
@Override
public JavacVersion getVersion() {
return VERSION;
}
@Override
public String getDescription(
ImmutableList<String> options,
ImmutableSortedSet<Path> javaSourceFilePaths,
Path pathToSrcsList) {
StringBuilder builder = new StringBuilder("javac ");
Joiner.on(" ").appendTo(builder, options);
builder.append(" ");
builder.append("@").append(pathToSrcsList);
return builder.toString();
}
@Override
public String getShortName() {
return "javac";
}
@Override
public ImmutableList<String> getCommandPrefix(SourcePathResolver resolver) {
throw new UnsupportedOperationException("In memory javac may not be used externally");
}
@Override
public ImmutableMap<String, String> getEnvironment(SourcePathResolver resolver) {
throw new UnsupportedOperationException("In memory javac may not be used externally");
}
protected abstract JavaCompiler createCompiler(JavacExecutionContext context);
@Override
public int buildWithClasspath(
JavacExecutionContext context,
BuildTarget invokingRule,
ImmutableList<String> options,
ImmutableList<JavacPluginJsr199Fields> pluginFields,
ImmutableSortedSet<Path> javaSourceFilePaths,
Path pathToSrcsList,
Optional<Path> workingDirectory,
CompilationMode compilationMode) {
JavaCompiler compiler = createCompiler(context);
CustomJarOutputStream jarOutputStream = null;
StandardJavaFileManager fileManager = null;
JavaInMemoryFileManager inMemoryFileManager = null;
Path directToJarPath = null;
try {
fileManager = compiler.getStandardFileManager(null, null, null);
if (context.getDirectToJarOutputSettings().isPresent()) {
directToJarPath =
context
.getProjectFilesystem()
.getPathForRelativePath(
context.getDirectToJarOutputSettings().get().getDirectToJarOutputPath());
inMemoryFileManager =
new JavaInMemoryFileManager(
fileManager,
directToJarPath,
context.getDirectToJarOutputSettings().get().getClassesToRemoveFromJar());
fileManager = inMemoryFileManager;
}
Iterable<? extends JavaFileObject> compilationUnits;
try {
compilationUnits =
createCompilationUnits(
fileManager, context.getProjectFilesystem()::resolve, javaSourceFilePaths);
} catch (IOException e) {
LOG.warn(e, "Error building compilation units");
return 1;
}
try {
int result =
buildWithClasspath(
context,
invokingRule,
options,
pluginFields,
javaSourceFilePaths,
pathToSrcsList,
compiler,
fileManager,
compilationUnits,
compilationMode);
if (result != 0 || !context.getDirectToJarOutputSettings().isPresent()) {
return result;
}
jarOutputStream =
ZipOutputStreams.newJarOutputStream(
Preconditions.checkNotNull(directToJarPath),
ZipOutputStreams.HandleDuplicates.APPEND_TO_ZIP);
if (compilationMode == CompilationMode.ABI) {
jarOutputStream.setEntryHashingEnabled(true);
}
return new JarBuilder(context.getProjectFilesystem())
.setObserver(new LoggingJarBuilderObserver(context.getEventSink()))
.setEntriesToJar(context.getDirectToJarOutputSettings().get().getEntriesToJar())
.setAlreadyAddedEntries(
Preconditions.checkNotNull(inMemoryFileManager).writeToJar(jarOutputStream))
.setMainClass(context.getDirectToJarOutputSettings().get().getMainClass().orElse(null))
.setManifestFile(
context.getDirectToJarOutputSettings().get().getManifestFile().orElse(null))
.setShouldMergeManifests(true)
.setEntryPatternBlacklist(ImmutableSet.of())
.appendToJarFile(
context
.getProjectFilesystem()
.resolve(
context.getDirectToJarOutputSettings().get().getDirectToJarOutputPath()),
Preconditions.checkNotNull(jarOutputStream));
} finally {
close(compilationUnits);
}
} catch (IOException e) {
LOG.warn(e, "Unable to create jarOutputStream");
} finally {
closeResources(fileManager, inMemoryFileManager, jarOutputStream);
}
return 1;
}
private void closeResources(
@Nullable StandardJavaFileManager fileManager,
@Nullable JavaInMemoryFileManager inMemoryFileManager,
@Nullable CustomZipOutputStream jarOutputStream) {
try {
if (jarOutputStream != null) {
jarOutputStream.close();
}
} catch (IOException e) {
LOG.warn(e, "Unable to close jarOutputStream. We may be leaking memory.");
}
try {
if (inMemoryFileManager != null) {
inMemoryFileManager.close();
} else if (fileManager != null) {
fileManager.close();
}
} catch (IOException e) {
LOG.warn(e, "Unable to close fileManager. We may be leaking memory.");
}
}
private int buildWithClasspath(
JavacExecutionContext context,
BuildTarget invokingRule,
ImmutableList<String> options,
ImmutableList<JavacPluginJsr199Fields> pluginFields,
ImmutableSortedSet<Path> javaSourceFilePaths,
Path pathToSrcsList,
JavaCompiler compiler,
StandardJavaFileManager fileManager,
Iterable<? extends JavaFileObject> compilationUnits,
CompilationMode compilationMode) {
// write javaSourceFilePaths to classes file
// for buck user to have a list of all .java files to be compiled
// since we do not print them out to console in case of error
try {
context
.getProjectFilesystem()
.writeLinesToPath(
FluentIterable.from(javaSourceFilePaths)
.transform(Object::toString)
.transform(ARGFILES_ESCAPER),
pathToSrcsList);
} catch (IOException e) {
context
.getEventSink()
.reportThrowable(
e,
"Cannot write list of .java files to compile to %s file! Terminating compilation.",
pathToSrcsList);
return 1;
}
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
List<String> classNamesForAnnotationProcessing = ImmutableList.of();
Writer compilerOutputWriter = new PrintWriter(context.getStdErr()); // NOPMD required by API
PluginClassLoaderFactory loaderFactory = PluginLoader.newFactory(context.getClassLoaderCache());
BuckJavacTaskProxy javacTask;
if (compilationMode != CompilationMode.ABI) {
javacTask =
BuckJavacTaskProxy.getTask(
loaderFactory,
compiler,
compilerOutputWriter,
context.getUsedClassesFileWriter().wrapFileManager(fileManager),
diagnostics,
options,
classNamesForAnnotationProcessing,
compilationUnits);
} else {
javacTask =
FrontendOnlyJavacTaskProxy.getTask(
loaderFactory,
compiler,
compilerOutputWriter,
context.getUsedClassesFileWriter().wrapFileManager(fileManager),
diagnostics,
options,
classNamesForAnnotationProcessing,
compilationUnits);
javacTask.addPostEnterCallback(
topLevelTypes -> {
StubGenerator stubGenerator =
new StubGenerator(
getTargetVersion(options),
javacTask.getElements(),
fileManager,
context.getEventSink().getEventBus());
stubGenerator.generate(topLevelTypes);
});
}
PluginClassLoader pluginLoader = loaderFactory.getPluginClassLoader(javacTask);
boolean isSuccess = false;
BuckTracing.setCurrentThreadTracingInterfaceFromJsr199Javac(
new Jsr199TracingBridge(context.getEventSink(), invokingRule));
BuckJavacTaskListener taskListener = null;
if (EnumSet.of(
CompilationMode.FULL_CHECKING_REFERENCES, CompilationMode.FULL_ENFORCING_REFERENCES)
.contains(compilationMode)) {
taskListener =
SourceBasedAbiStubber.newValidatingTaskListener(
pluginLoader,
javacTask,
new FileManagerBootClasspathOracle(fileManager),
compilationMode == CompilationMode.FULL_ENFORCING_REFERENCES
? Diagnostic.Kind.ERROR
: Diagnostic.Kind.WARNING);
}
try {
try (
// TranslatingJavacPhaseTracer is AutoCloseable so that it can detect the end of tracing
// in some unusual situations
TranslatingJavacPhaseTracer tracer =
new TranslatingJavacPhaseTracer(
new JavacPhaseEventLogger(invokingRule, context.getEventSink()));
// Ensure annotation processors are loaded from their own classloader. If we don't do
// this, then the evidence suggests that they get one polluted with Buck's own classpath,
// which means that libraries that have dependencies on different versions of Buck's deps
// may choke with novel errors that don't occur on the command line.
AnnotationProcessorFactory processorFactory =
new AnnotationProcessorFactory(
context.getEventSink(),
compiler.getClass().getClassLoader(),
context.getClassLoaderCache(),
invokingRule)) {
taskListener = new TracingTaskListener(tracer, taskListener);
javacTask.setTaskListener(taskListener);
javacTask.setProcessors(processorFactory.createProcessors(pluginFields));
// Invoke the compilation and inspect the result.
isSuccess = javacTask.call();
} catch (IOException e) {
LOG.warn(e, "Unable to close annotation processor class loader. We may be leaking memory.");
}
} finally {
// Clear the tracing interface so we have no chance of leaking it to code that shouldn't
// be using it.
BuckTracing.clearCurrentThreadTracingInterfaceFromJsr199Javac();
}
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
LOG.debug("javac: %s", DiagnosticPrettyPrinter.format(diagnostic));
}
List<Diagnostic<? extends JavaFileObject>> cleanDiagnostics =
DiagnosticCleaner.clean(diagnostics.getDiagnostics());
if (isSuccess) {
context.getUsedClassesFileWriter().writeFile(context.getProjectFilesystem());
return 0;
} else {
if (context.getVerbosity().shouldPrintStandardInformation()) {
int numErrors = 0;
int numWarnings = 0;
for (Diagnostic<? extends JavaFileObject> diagnostic : cleanDiagnostics) {
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(DiagnosticPrettyPrinter.format(diagnostic));
}
if (numErrors > 0 || numWarnings > 0) {
context.getStdErr().printf("Errors: %d. Warnings: %d.\n", numErrors, numWarnings);
}
}
return 1;
}
}
private static SourceVersion getTargetVersion(Iterable<String> options) {
boolean foundTarget = false;
for (String option : options) {
if (option.equals("-target")) {
foundTarget = true;
} else if (foundTarget) {
switch (option) {
case "1.3":
return SourceVersion.RELEASE_3;
case "1.4":
return SourceVersion.RELEASE_4;
case "1.5":
case "5":
return SourceVersion.RELEASE_5;
case "1.6":
case "6":
return SourceVersion.RELEASE_6;
case "1.7":
case "7":
return SourceVersion.RELEASE_7;
case "1.8":
case "8":
return SourceVersion.RELEASE_8;
default:
throw new HumanReadableException("target %s not supported", option);
}
}
}
throw new AssertionError("Unreachable code");
}
private void close(Iterable<? extends JavaFileObject> compilationUnits) {
for (JavaFileObject unit : compilationUnits) {
if (unit instanceof Closeable) {
try {
((Closeable) unit).close();
} catch (IOException e) {
LOG.warn(e, "Unable to close zipfile. We may be leaking memory.");
}
}
}
}
private Iterable<? extends JavaFileObject> createCompilationUnits(
StandardJavaFileManager fileManager,
Function<Path, Path> absolutifier,
Set<Path> javaSourceFilePaths)
throws IOException {
List<JavaFileObject> compilationUnits = new ArrayList<>();
for (Path path : javaSourceFilePaths) {
String pathString = path.toString();
if (pathString.endsWith(".java")) {
// For an ordinary .java file, create a corresponding JavaFileObject.
Iterable<? extends JavaFileObject> javaFileObjects =
fileManager.getJavaFileObjects(absolutifier.apply(path).toFile());
compilationUnits.add(Iterables.getOnlyElement(javaFileObjects));
} else if (pathString.endsWith(SRC_ZIP) || pathString.endsWith(SRC_JAR)) {
// For a Zip of .java files, create a JavaFileObject for each .java entry.
ZipFile zipFile = new ZipFile(absolutifier.apply(path).toFile());
boolean hasZipFileBeenUsed = false;
for (Enumeration<? extends ZipEntry> entries = zipFile.entries();
entries.hasMoreElements();
) {
ZipEntry entry = entries.nextElement();
if (!entry.getName().endsWith(".java")) {
continue;
}
hasZipFileBeenUsed = true;
compilationUnits.add(new ZipEntryJavaFileObject(zipFile, entry));
}
if (!hasZipFileBeenUsed) {
zipFile.close();
}
}
}
return compilationUnits;
}
private static class FileManagerBootClasspathOracle implements BootClasspathOracle {
private final JavaFileManager fileManager;
private final Map<String, Set<String>> packagesContents = new HashMap<>();
private FileManagerBootClasspathOracle(JavaFileManager fileManager) {
this.fileManager = fileManager;
}
@Override
public boolean isOnBootClasspath(String binaryName) {
String packageName = getPackageName(binaryName);
Set<String> packageContents = getPackageContents(packageName);
return packageContents.contains(binaryName);
}
private Set<String> getPackageContents(String packageName) {
Set<String> packageContents = packagesContents.get(packageName);
if (packageContents == null) {
packageContents = new HashSet<>();
try {
for (JavaFileObject javaFileObject :
this.fileManager.list(
StandardLocation.PLATFORM_CLASS_PATH,
packageName,
EnumSet.of(JavaFileObject.Kind.CLASS),
true)) {
packageContents.add(
fileManager.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, javaFileObject));
}
} catch (IOException e) {
throw new HumanReadableException(e, "Failed to list boot classpath contents.");
// Do nothing
}
packagesContents.put(packageName, packageContents);
}
return packageContents;
}
private String getPackageName(String binaryName) {
int lastDot = binaryName.lastIndexOf('.');
if (lastDot < 0) {
return "";
}
return binaryName.substring(0, lastDot);
}
}
}