| /* |
| * 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.event.MissingSymbolEvent; |
| import com.facebook.buck.log.Logger; |
| import com.facebook.buck.model.BuildTarget; |
| import com.facebook.buck.rules.RuleKey; |
| import com.facebook.buck.step.ExecutionContext; |
| import com.facebook.buck.util.ClassLoaderCache; |
| import com.facebook.buck.util.HumanReadableException; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Function; |
| import com.google.common.base.Functions; |
| 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.FluentIterable; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.Writer; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.Enumeration; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| import javax.annotation.Nullable; |
| import javax.annotation.processing.Processor; |
| 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.ToolProvider; |
| |
| /** |
| * Command used to compile java libraries with a variety of ways to handle dependencies. |
| * <p> |
| * If {@code buildDependencies} is set to |
| * {@link com.facebook.buck.rules.BuildDependencies#FIRST_ORDER_ONLY}, this class will invoke javac |
| * using {@code declaredClasspathEntries} for the classpath. If {@code buildDependencies} is set to |
| * {@link com.facebook.buck.rules.BuildDependencies#TRANSITIVE}, this class will invoke javac using |
| * {@code transitiveClasspathEntries} for the classpath. If {@code buildDependencies} is set to |
| * {@link com.facebook.buck.rules.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 Jsr199Javac implements Javac { |
| |
| private static final Logger LOG = Logger.get(Jsr199Javac.class); |
| private static final JavacVersion VERSION = new JavacVersion() { |
| @Override |
| public String getVersionString() { |
| return "in memory"; |
| } |
| }; |
| |
| @Override |
| public JavacVersion getVersion() { |
| return VERSION; |
| } |
| |
| private Optional<Path> javacJar; |
| |
| /** |
| * @param javacJar If absent, use the system compiler. Otherwise, load the compiler from this |
| * path. |
| */ |
| Jsr199Javac(Optional<Path> javacJar) { |
| // XXX: maybe we can accept a Provider<JavaCompiler> or just a JavaCompiler instance. |
| this.javacJar = javacJar; |
| } |
| |
| @VisibleForTesting |
| public Optional<Path> getJavacJar() { |
| return javacJar; |
| } |
| |
| @Override |
| public String getDescription( |
| ExecutionContext context, |
| ImmutableList<String> options, |
| ImmutableSet<Path> javaSourceFilePaths, |
| Optional<Path> pathToSrcsList) { |
| StringBuilder builder = new StringBuilder("javac "); |
| Joiner.on(" ").appendTo(builder, options); |
| builder.append(" "); |
| |
| if (pathToSrcsList.isPresent()) { |
| builder.append("@").append(pathToSrcsList.get()); |
| } else { |
| Joiner.on(" ").appendTo(builder, javaSourceFilePaths); |
| } |
| |
| return builder.toString(); |
| } |
| |
| @Override |
| public String getShortName() { |
| return "javac"; |
| } |
| |
| @Override |
| public boolean isUsingWorkspace() { |
| return false; |
| } |
| |
| @Override |
| public RuleKey.Builder appendToRuleKey(RuleKey.Builder builder, String key) { |
| return builder.setReflectively(key + ".javac", "jsr199") |
| .setReflectively(key + ".javac.version", "in-memory") |
| .setReflectively(key + ".javacjar", javacJar); |
| } |
| |
| @Override |
| public int buildWithClasspath( |
| ExecutionContext context, |
| BuildTarget invokingRule, |
| ImmutableList<String> options, |
| ImmutableSet<Path> javaSourceFilePaths, |
| Optional<Path> pathToSrcsList, |
| Optional<Path> workingDirectory) { |
| JavaCompiler compiler; |
| |
| if (javacJar.isPresent()) { |
| ClassLoaderCache classLoaderCache = context.getClassLoaderCache(); |
| ClassLoader compilerClassLoader = classLoaderCache.getClassLoaderForClassPath( |
| ClassLoader.getSystemClassLoader(), |
| ImmutableList.of(javacJar.get())); |
| try { |
| compiler = (JavaCompiler) |
| compilerClassLoader.loadClass("com.sun.tools.javac.api.JavacTool") |
| .newInstance(); |
| } catch (ClassNotFoundException | IllegalAccessException | InstantiationException ex) { |
| throw new RuntimeException(ex); |
| } |
| } else { |
| synchronized (ToolProvider.class) { |
| // ToolProvider has no synchronization internally, so if we don't synchronize from the |
| // outside we could wind up loading the compiler classes multiple times from different |
| // class loaders. |
| compiler = ToolProvider.getSystemJavaCompiler(); |
| } |
| |
| if (compiler == null) { |
| throw new HumanReadableException( |
| "No system compiler found. Did you install the JRE instead of the JDK?"); |
| } |
| } |
| |
| StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); |
| Iterable<? extends JavaFileObject> compilationUnits = ImmutableSet.of(); |
| try { |
| compilationUnits = createCompilationUnits( |
| fileManager, |
| context.getProjectFilesystem().getAbsolutifier(), |
| javaSourceFilePaths); |
| } catch (IOException e) { |
| close(fileManager, compilationUnits); |
| e.printStackTrace(context.getStdErr()); |
| return 1; |
| } |
| |
| if (pathToSrcsList.isPresent()) { |
| // 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(Functions.toStringFunction()) |
| .transform(ARGFILES_ESCAPER), |
| pathToSrcsList.get()); |
| } catch (IOException e) { |
| close(fileManager, compilationUnits); |
| context.logError( |
| e, |
| "Cannot write list of .java files to compile to %s file! Terminating compilation.", |
| pathToSrcsList.get()); |
| return 1; |
| } |
| } |
| |
| DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); |
| List<String> classNamesForAnnotationProcessing = ImmutableList.of(); |
| Writer compilerOutputWriter = new PrintWriter(context.getStdErr()); |
| JavaCompiler.CompilationTask compilationTask = compiler.getTask( |
| compilerOutputWriter, |
| fileManager, |
| diagnostics, |
| options, |
| classNamesForAnnotationProcessing, |
| compilationUnits); |
| |
| // 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. |
| ProcessorBundle bundle = null; |
| boolean isSuccess; |
| |
| try { |
| bundle = prepareProcessors( |
| compiler.getClass().getClassLoader(), |
| invokingRule, |
| options); |
| compilationTask.setProcessors(bundle.processors); |
| |
| // Invoke the compilation and inspect the result. |
| isSuccess = compilationTask.call(); |
| } finally { |
| close(fileManager, compilationUnits); |
| } |
| |
| if (isSuccess) { |
| 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; |
| handleMissingSymbolError(invokingRule, diagnostic, context); |
| } 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 void close( |
| JavaFileManager fileManager, |
| Iterable<? extends JavaFileObject> compilationUnits) { |
| try { |
| fileManager.close(); |
| } catch (IOException e) { |
| LOG.warn(e, "Unable to close java filemanager. We may be leaking memory."); |
| } |
| |
| for (JavaFileObject unit : compilationUnits) { |
| if (!(unit instanceof ZipEntryJavaFileObject)) { |
| continue; |
| } |
| try { |
| ((ZipEntryJavaFileObject) unit).close(); |
| } catch (IOException e) { |
| LOG.warn(e, "Unable to close zipfile. We may be leaking memory."); |
| } |
| } |
| } |
| |
| private ProcessorBundle prepareProcessors( |
| ClassLoader compilerClassLoader, |
| @Nullable BuildTarget target, |
| List<String> options) { |
| String processorClassPath = null; |
| String processorNames = null; |
| |
| Iterator<String> iterator = options.iterator(); |
| while (iterator.hasNext()) { |
| String curr = iterator.next(); |
| if ("-processorpath".equals(curr) && iterator.hasNext()) { |
| processorClassPath = iterator.next(); |
| } else if ("-processor".equals(curr) && iterator.hasNext()) { |
| processorNames = iterator.next(); |
| } |
| } |
| |
| ProcessorBundle processorBundle = new ProcessorBundle(); |
| if (processorClassPath == null || processorNames == null) { |
| return processorBundle; |
| } |
| |
| // N.B. You might think that we could avoid some overhead by using the same classloader every |
| // time we create an instance of annotation processor. In an ideal world, that would work well, |
| // but many annotation processors aren't thread-safe, and they store state in class-static |
| // variables. In the interest of maximum safety, we'll create a new ClassLoader every time we |
| // need an annotation processor. |
| |
| Iterable<String> rawPaths = Splitter.on(File.pathSeparator) |
| .omitEmptyStrings() |
| .split(processorClassPath); |
| URL[] urls = FluentIterable.from(rawPaths) |
| .transform( |
| new Function<String, URL>() { |
| @Override |
| public URL apply(String pathRelativeToProjectRoot) { |
| try { |
| return Paths.get(pathRelativeToProjectRoot).toUri().toURL(); |
| } catch (MalformedURLException e) { |
| // The paths we're being given should have all been resolved from the file |
| // system already. We'd need to be unfortunate to get here. Bubble up a runtime |
| // exception. |
| throw new RuntimeException(e); |
| } |
| } |
| }) |
| .toArray(URL.class); |
| processorBundle.classLoader = new URLClassLoader( |
| urls, |
| compilerClassLoader); |
| |
| Iterable<String> names = Splitter.on(",") |
| .trimResults() |
| .omitEmptyStrings() |
| .split(processorNames); |
| for (String name : names) { |
| try { |
| LOG.debug("Loading %s from own classloader", name); |
| |
| Class<? extends Processor> aClass = |
| Preconditions.checkNotNull(processorBundle.classLoader) |
| .loadClass(name) |
| .asSubclass(Processor.class); |
| processorBundle.processors.add(aClass.newInstance()); |
| } catch (ReflectiveOperationException e) { |
| // If this happens, then the build is really in trouble. Better warn the user. |
| throw new HumanReadableException( |
| "%s: javac unable to load annotation processor: %s", |
| target != null ? target.getFullyQualifiedName() : "unknown target", |
| name); |
| } |
| } |
| |
| return processorBundle; |
| } |
| |
| private Iterable<? extends JavaFileObject> createCompilationUnits( |
| StandardJavaFileManager fileManager, |
| Function<Path, Path> absolutifier, |
| Set<Path> javaSourceFilePaths) throws IOException { |
| List<JavaFileObject> compilationUnits = Lists.newArrayList(); |
| for (Path path : javaSourceFilePaths) { |
| if (path.toString().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 (path.toString().endsWith(SRC_ZIP)) { |
| // For a Zip of .java files, create a JavaFileObject for each .java entry. |
| ZipFile zipFile = new ZipFile(absolutifier.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; |
| } |
| |
| private void handleMissingSymbolError( |
| BuildTarget invokingRule, |
| Diagnostic<? extends JavaFileObject> diagnostic, |
| ExecutionContext context) { |
| JavacErrorParser javacErrorParser = new JavacErrorParser( |
| context.getProjectFilesystem(), |
| context.getJavaPackageFinder()); |
| Optional<String> symbol = |
| javacErrorParser.getMissingSymbolFromCompilerError(diagnostic.toString()); |
| if (!symbol.isPresent()) { |
| // This error wasn't related to a missing symbol, as far as we can tell. |
| return; |
| } |
| MissingSymbolEvent event = MissingSymbolEvent.create( |
| invokingRule, |
| symbol.get(), |
| MissingSymbolEvent.SymbolType.Java); |
| context.getBuckEventBus().post(event); |
| } |
| |
| private static class ProcessorBundle { |
| @Nullable |
| public ClassLoader classLoader; |
| public List<Processor> processors = Lists.newArrayList(); |
| } |
| } |