blob: 2da227776cd0ff43fcf2062fd1234ae351617125 [file] [log] [blame]
/*
* Copyright 2014 The Kythe 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.kythe.extractors.java;
import static com.google.common.base.StandardSystemProperty.JAVA_HOME;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.io.ByteStreams;
import com.google.devtools.kythe.extractors.shared.CompilationDescription;
import com.google.devtools.kythe.extractors.shared.ExtractionException;
import com.google.devtools.kythe.extractors.shared.ExtractorUtils;
import com.google.devtools.kythe.extractors.shared.FileVNames;
import com.google.devtools.kythe.platform.java.JavacOptionsUtils.ModifiableOptions;
import com.google.devtools.kythe.proto.Analysis.CompilationUnit;
import com.google.devtools.kythe.proto.Analysis.CompilationUnit.FileInput;
import com.google.devtools.kythe.proto.Analysis.FileData;
import com.google.devtools.kythe.proto.Buildinfo.BuildDetails;
import com.google.devtools.kythe.proto.Java.JavaDetails;
import com.google.devtools.kythe.proto.Storage.VName;
import com.google.devtools.kythe.util.DeleteRecursively;
import com.google.protobuf.Any;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.api.JavacTaskImpl;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.file.CacheFSInfo;
import com.sun.tools.javac.file.FSInfo;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.util.Context;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.processing.Processor;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
/**
* Extracts all required information (set of source files, class paths, and compiler options) from a
* java compilation command and stores the information to replay the compilation.
*
* <p>The extractor runs the Javac compiler to get a exact description of all files required for the
* compilation as a whole. It then creates CompilationUnit entries for each source file. We do not
* do this on a per file basis as the Java Compiler takes too long to do this.
*/
public class JavaCompilationUnitExtractor {
public static final String JAVA_DETAILS_URL = "kythe.io/proto/kythe.proto.JavaDetails";
public static final String BUILD_DETAILS_URL = "kythe.io/proto/kythe.proto.BuildDetails";
private static final Logger logger =
Logger.getLogger(JavaCompilationUnitExtractor.class.getName());
private static final String JDK_MODULE_PREFIX = "/modules/java.";
private static final String MODULE_INFO_NAME = "module-info";
private static final String SOURCE_JAR_ROOT = "!SOURCE_JAR!";
private static String classJarRoot(Location location) {
return String.format("!%s_JAR!", location);
}
// TODO(shahms): Use the proper methods when we can rely on JDK 9.
private static final ClassLoader moduleClassLoader;
static {
ClassLoader loader = null;
try {
Object thisModule =
Class.class.getMethod("getModule").invoke(JavaCompilationUnitExtractor.class);
thisModule
.getClass()
.getMethod("addUses", Class.class)
.invoke(thisModule, JavaCompiler.class);
loader = (ClassLoader) thisModule.getClass().getMethod("getClassLoader").invoke(thisModule);
} catch (ReflectiveOperationException e) {
logger.info("Running on non-modular JDK, fallback compiler unavailable.");
}
moduleClassLoader = loader;
}
private static final String JAR_SCHEME = "jar";
private final String jdkJar;
private final String rootDirectory;
private final FileVNames fileVNames;
/**
* Creates an instance of the JavaExtractor to store java compilation information in an .kindex
* file.
*/
public JavaCompilationUnitExtractor(String corpus) throws ExtractionException {
this(corpus, ExtractorUtils.getCurrentWorkingDirectory());
}
/**
* Creates an instance of the JavaExtractor to store java compilation information in an .kindex
* file.
*/
public JavaCompilationUnitExtractor(String corpus, String rootDirectory)
throws ExtractionException {
this(FileVNames.staticCorpus(corpus), rootDirectory);
}
/**
* Creates an instance of the JavaExtractor to store java compilation information in an .kindex
* file.
*/
public JavaCompilationUnitExtractor(FileVNames fileVNames) throws ExtractionException {
this(fileVNames, ExtractorUtils.getCurrentWorkingDirectory());
}
public JavaCompilationUnitExtractor(FileVNames fileVNames, String rootDirectory)
throws ExtractionException {
this.fileVNames = fileVNames;
Path javaHome = Paths.get(JAVA_HOME.value()).getParent();
try {
// Remove trailing dots. Interesting trivia: in some build systems,
// the java.home variable is terminated with "/bin/..".
// However, this is not the case for the class files
// that we are trying to filter.
this.jdkJar = javaHome.toRealPath(NOFOLLOW_LINKS).toString();
} catch (IOException e) {
throw new ExtractionException("JDK path not found: " + javaHome, e, false);
}
try {
this.rootDirectory = Paths.get(rootDirectory).toRealPath().toString();
} catch (IOException ioe) {
throw new ExtractionException("Root directory does not exist", ioe, false);
}
}
private CompilationUnit buildCompilationUnit(
String target,
Iterable<String> options,
Iterable<FileInput> requiredInputs,
boolean hasErrors,
Set<String> newSourcePath,
Set<String> newClassPath,
Iterable<String> newBootClassPath,
List<String> sourceFiles,
String outputPath) {
CompilationUnit.Builder unit = CompilationUnit.newBuilder();
unit.setVName(VName.newBuilder().setSignature(target).setLanguage("java"));
unit.addAllArgument(options);
unit.setHasCompileErrors(hasErrors);
unit.addAllRequiredInput(requiredInputs);
for (String sourceFile : sourceFiles) {
unit.addSourceFile(sourceFile);
}
unit.setOutputKey(outputPath);
unit.addDetails(
Any.newBuilder()
.setTypeUrl(JAVA_DETAILS_URL)
.setValue(
JavaDetails.newBuilder()
.addAllClasspath(newClassPath)
.addAllBootclasspath(newBootClassPath)
.addAllSourcepath(newSourcePath)
.build()
.toByteString()));
unit.addDetails(
Any.newBuilder()
.setTypeUrl(BUILD_DETAILS_URL)
.setValue(BuildDetails.newBuilder().setBuildTarget(target).build().toByteString()));
return unit.build();
}
/**
* Indexes a compilation unit to the bigtable. The extraction process will try to build a minimum
* set of what is needed to replay the compilation. To do this it runs the java compiler, and
* tracks all .class & .java files that are needed. It then builds up a new classpath & sourcepath
* that only contains the minimum set of paths required to replay the compilation.
*
* <p>New classpath: because we extract classes from the jars into a temp path that needs to be
* set. Also we only use the classpaths that are actually used, not the ones that are provided.
* New sourcepath: as we're doing a partial compilation, we need to set up the source path to
* correctly load any files that are not the main compilation but are needed to perform
* compilation. This is not required when doing a full compilation as all sources are
* automatically loaded.
*
* <p>Next we store all required files in the bigtable and writes the CompilationUnit to the
* bigtable.
*
* @throws ExtractionException if anything blocks the indexing to be completed.
*/
public CompilationDescription extract(
String target,
Iterable<String> sources,
Iterable<String> classpath,
Iterable<String> bootclasspath,
Iterable<String> sourcepath,
Iterable<String> processorpath,
Iterable<String> processors,
Optional<Path> genSrcDir,
Iterable<String> options,
String outputPath)
throws ExtractionException {
Preconditions.checkNotNull(target);
Preconditions.checkNotNull(sources);
Preconditions.checkNotNull(classpath);
Preconditions.checkNotNull(bootclasspath);
Preconditions.checkNotNull(sourcepath);
Preconditions.checkNotNull(processorpath);
Preconditions.checkNotNull(processors);
Preconditions.checkNotNull(genSrcDir);
Preconditions.checkNotNull(options);
Preconditions.checkNotNull(outputPath);
AnalysisResults results;
if (sources.iterator().hasNext()) {
results =
runJavaAnalysisToExtractCompilationDetails(
sources,
classpath,
bootclasspath,
sourcepath,
processorpath,
processors,
genSrcDir,
options);
} else {
results = new AnalysisResults();
}
List<FileData> fileContents = ExtractorUtils.convertBytesToFileDatas(results.fileContents);
List<FileInput> compilationFileInputs =
ExtractorUtils.toFileInputs(fileVNames, results.relativePaths::get, fileContents).stream()
.map(
input -> {
String sourceBasename = results.sourceFileNames.get(input.getInfo().getPath());
VName vname = input.getVName();
if (sourceBasename != null
&& vname.getPath().endsWith(".java")
&& !vname.getPath().endsWith(sourceBasename)) {
Path fixedPath = Paths.get(vname.getPath()).resolveSibling(sourceBasename);
vname = vname.toBuilder().setPath(fixedPath.toString()).build();
return input.toBuilder().setVName(vname).build();
}
return input;
})
.collect(toImmutableList());
CompilationUnit compilationUnit =
buildCompilationUnit(
target,
removeDestDirOptions(options),
compilationFileInputs,
results.hasErrors,
results.newSourcePath,
results.newClassPath,
results.newBootClassPath,
results.explicitSources,
ExtractorUtils.tryMakeRelative(rootDirectory, outputPath));
return new CompilationDescription(compilationUnit, fileContents);
}
/**
* Returns a new list with the same options except header/source destination directory options.
*/
private static ImmutableList<String> removeDestDirOptions(Iterable<String> options) {
// TODO(#3671): Option.D needs to remain in for module support, fix either here or in indexing.
return ModifiableOptions.of(options)
.removeOptions(EnumSet.of(Option.D, Option.S, Option.H))
.build();
}
/**
* If the code has wildcard imports (e.g. import foo.bar.*) but doesn't actually use any of the
* imports, errors will happen. We don't get callbacks for file open of these files (since they
* aren't used) but when java runs it will report errors if it can't find any files to match the
* wildcard. So we add one matching file here.
*/
private void findOnDemandImportedFiles(
Iterable<? extends CompilationUnitTree> compilationUnits,
UsageAsInputReportingFileManager fileManager)
throws ExtractionException {
// Maps package names to source files that wildcard import them.
Multimap<String, String> pkgs = HashMultimap.create();
// Javac synthesizes an "import java.lang.*" for every compilation unit.
pkgs.put("java.lang", "*.java");
for (CompilationUnitTree unit : compilationUnits) {
for (ImportTree importTree : unit.getImports()) {
if (importTree.isStatic()) {
continue;
}
String qualifiedIdentifier = importTree.getQualifiedIdentifier().toString();
if (!qualifiedIdentifier.endsWith(".*")) {
continue;
}
pkgs.put(
qualifiedIdentifier.substring(0, qualifiedIdentifier.length() - 2),
unit.getSourceFile().getName());
}
}
for (Map.Entry<String, Collection<String>> pkg : pkgs.asMap().entrySet()) {
try {
JavaFileObject firstClass =
Iterables.getFirst(
fileManager.list(
StandardLocation.CLASS_PATH, pkg.getKey(), EnumSet.of(Kind.CLASS), false),
null);
if (firstClass == null) {
firstClass =
Iterables.getFirst(
fileManager.list(
StandardLocation.PLATFORM_CLASS_PATH,
pkg.getKey(),
EnumSet.of(Kind.CLASS),
false),
null);
}
if (firstClass != null) {
firstClass.getCharContent(true);
}
JavaFileObject firstSource =
Iterables.getFirst(
fileManager.list(
StandardLocation.SOURCE_PATH, pkg.getKey(), EnumSet.of(Kind.SOURCE), false),
null);
if (firstSource != null) {
firstSource.getCharContent(true);
}
} catch (IOException e) {
throw new ExtractionException(
String.format(
"Unable to extract files used for on demand imports in {%s}",
Joiner.on(", ").join(pkg.getValue())),
e,
false);
}
}
}
/**
* Determines -sourcepath arguments to add to the compilation unit based on the package name. This
* is needed as the sharded analysis will need to resolve dependent source files. Also locates
* sources that do not follow the package == path convention and list them as explicit sources.
*/
private void getAdditionalSourcePaths(
Iterable<? extends CompilationUnitTree> compilationUnits, AnalysisResults results) {
for (CompilationUnitTree compilationUnit : compilationUnits) {
ExpressionTree packageExpression = compilationUnit.getPackageName();
if (packageExpression != null) {
String packageName = packageExpression.toString();
if (!Strings.isNullOrEmpty(packageName)) {
// If the source file specifies a package, we try to find that
// package name in the path to the source file and assume
// the correct sourcepath to add is the directory containing that
// package.
String packageSubDir = packageName.replace('.', '/');
String path = compilationUnit.getSourceFile().toUri().getPath();
// This needs to be lastIndexOf as there are source jars that
// contain the same package name in the files contained in them
// as the path the source jars live in. As we extract the source
// jars, we end up with a double named path.
int index = path.lastIndexOf(packageSubDir);
if (index >= 0) {
String root = ExtractorUtils.tryMakeRelative(rootDirectory, path.substring(0, index));
results.newSourcePath.add(root);
}
}
}
}
}
private void findRequiredFiles(
UsageAsInputReportingFileManager fileManager,
Map<URI, String> sourceFiles,
Optional<Path> genSrcDir,
AnalysisResults results)
throws ExtractionException {
for (InputUsageRecord input : fileManager.getUsages()) {
processRequiredInput(
input.fileObject(), input.location(), fileManager, sourceFiles, genSrcDir, results);
}
}
private void processRequiredInput(
JavaFileObject requiredInput,
Location location,
UsageAsInputReportingFileManager fileManager,
Map<URI, String> sourceFiles,
Optional<Path> genSrcDir,
AnalysisResults results)
throws ExtractionException {
URI uri = requiredInput.toUri();
String entryPath;
String jarPath = null;
boolean isJarPath = false;
{
URLConnection conn;
URL url;
try {
url = uri.toURL();
conn = url.openConnection();
} catch (IOException e) {
throw new IOError(e);
}
if (conn instanceof JarURLConnection) {
isJarPath = true;
JarURLConnection jarConn = ((JarURLConnection) conn);
jarPath = jarConn.getJarFileURL().getFile();
// jar entries don't have a leading '/', and we expect
// paths like "!CLASS_PATH_JAR!/com/foo/Bar.class"
entryPath = "/" + jarConn.getEntryName();
} else {
entryPath = url.getFile();
}
}
if (uri.getScheme().equals(JAR_SCHEME)) {
isJarPath = true;
uri = URI.create(uri.getRawSchemeSpecificPart());
}
switch (requiredInput.getKind()) {
case CLASS:
case SOURCE:
break;
case OTHER:
if (uri.getPath().endsWith(".meta")) {
break;
}
throw new IllegalStateException(String.format("Unsupported OTHER file kind: '%s'", uri));
default:
throw new IllegalStateException(
String.format(
"Unsupported java file kind: '%s' for '%s'", requiredInput.getKind().name(), uri));
}
String path = uri.getRawSchemeSpecificPart();
// If the file was part of the JDK we do not store it as the JDK is tied
// to the analyzer we'll run on this information later on.
if ((isJarPath && jarPath.startsWith(jdkJar)) || path.startsWith(JDK_MODULE_PREFIX)) {
return;
}
// Make the path relative to the indexer (e.g. a subdir of corpus/).
// If not possible, we store the fullpath.
String relativePath = ExtractorUtils.tryMakeRelative(rootDirectory, path);
String strippedPath = relativePath;
if (isJarPath) {
// If the file came from a jar file, we strip that out as we don't care where it came from, it
// was not as a source file in source control. We turn it in to a fake path. This is done so
// we do not need to download the entire jar file for each file's compilation (e.g. instead of
// downloading 200MB we only download 60K for analyzing the Kythe java indexer).
switch (requiredInput.getKind()) {
case CLASS:
String root = classJarRoot(location);
strippedPath = root + entryPath;
(location == StandardLocation.PLATFORM_CLASS_PATH
? results.newBootClassPath
: results.newClassPath)
.add(root);
break;
case SOURCE:
results.newSourcePath.add(SOURCE_JAR_ROOT);
strippedPath = SOURCE_JAR_ROOT + entryPath;
break;
default:
// TODO(#1845): we shouldn't need to throw an exception here because the above switch
// statement means that we never hit this default, but the static analysis tools can't
// figure that out. Try to refactor this code to remove this issue.
throw new IllegalStateException(
String.format(
"Unsupported java file kind: '%s' for '%s'",
requiredInput.getKind().name(), uri));
}
} else {
// If the class file was on disk, we need to infer the correct classpath to add.
String binaryName = getBinaryNameForClass(fileManager, requiredInput);
if (binaryName != null) {
// Java package names map to folders on disk, so if we want to find the right directory
// we replace each . with a /.
String csubdir = binaryName.replace('.', '/');
int cindex = strippedPath.indexOf(csubdir);
if (cindex <= 0) {
throw new ExtractionException(
String.format(
"unable to infer classpath for %s from %s, %s",
strippedPath, csubdir, binaryName),
false);
} else {
(location == StandardLocation.PLATFORM_CLASS_PATH
? results.newBootClassPath
: results.newClassPath)
.add(strippedPath.substring(0, cindex));
}
}
}
// Identify generated sources by checking if the source file is under the genSrcDir.
if (genSrcDir.isPresent()
&& !isJarPath
&& requiredInput.getKind() == Kind.SOURCE
&& Paths.get(relativePath).startsWith(genSrcDir.get())) {
results.explicitSources.add(strippedPath);
results.newSourcePath.add(genSrcDir.get().toString());
}
if (!results.fileContents.containsKey(strippedPath)) {
try {
// Retrieve the contents of the file.
InputStream stream = requiredInput.openInputStream();
if (stream.markSupported()) {
// The stream has already been read by the compiler, we need to reset it
// so we can read it as well.
stream.reset();
}
byte[] data = ByteStreams.toByteArray(stream);
if (data.length == 0) {
logger.warning(String.format("Empty java source file: %s", strippedPath));
}
results.fileContents.put(strippedPath, data);
results.relativePaths.put(strippedPath, relativePath);
if (sourceFiles.containsKey(requiredInput.toUri())) {
results.sourceFileNames.put(strippedPath, sourceFiles.get(requiredInput.toUri()));
}
} catch (IOException e) {
throw new ExtractionException(
String.format("Unable to read file content of %s", strippedPath), false);
}
}
}
/** {@link Location}s that may contain class files. */
private static final ImmutableSet<Location> CLASS_LOCATIONS =
ImmutableSet.<Location>of(
StandardLocation.CLASS_OUTPUT,
StandardLocation.CLASS_PATH,
StandardLocation.PLATFORM_CLASS_PATH);
/**
* Returns the location and binary name of a class file, or {@code null} if the file object is not
* a class.
*/
private static String getBinaryNameForClass(
UsageAsInputReportingFileManager fileManager, JavaFileObject fileObject)
throws ExtractionException {
if (fileObject.getKind() != Kind.CLASS) {
return null;
}
String binaryName;
for (Location location : CLASS_LOCATIONS) {
if ((binaryName = fileManager.inferBinaryName(location, fileObject)) != null) {
return binaryName;
}
}
if (fileObject.isNameCompatible(MODULE_INFO_NAME, Kind.CLASS)) {
// Ignore automatic module-info.class files
return null;
}
throw new ExtractionException(
String.format("unable to infer classpath for %s", fileObject.getName()), false);
}
private static class AnalysisResults {
// Map from strippedPath to an input's relative path to the corpus root.
final Map<String, String> relativePaths = new LinkedHashMap<>();
// Map from strippedPath to an input's contents.
final Map<String, byte[]> fileContents = new LinkedHashMap<>();
// Map from strippedPath to an input's true source basename. This is usually only needed for
// non-public top-level classes where their filename does not match the path derived from their
// fully-qualified name.
final Map<String, String> sourceFileNames = new HashMap<>();
// We build a new sourcepath & classpath that contain the minimum set of paths
// as well as the modified set of paths that are needed to analyze the single compilation unit.
// This is done to speed up analysis.
final Set<String> newSourcePath = new LinkedHashSet<>();
final Set<String> newClassPath = new LinkedHashSet<>();
final Set<String> newBootClassPath = new LinkedHashSet<>();
final List<String> explicitSources = new ArrayList<>();
boolean hasErrors = false;
}
// Install NonResolvingCacheFSInfo into Context to avoid resolving symlinks.
private void setupFSInfo(StandardJavaFileManager fileManager) {
Context context = new Context();
NonResolvingCacheFSInfo.preRegister(context);
((JavacFileManager) fileManager).setContext(context);
}
// FSInfo class which does not resolve symlinks when canonicalizing paths.
private static class NonResolvingCacheFSInfo extends CacheFSInfo {
public static void preRegister(Context context) {
context.put(
FSInfo.class,
new Context.Factory<FSInfo>() {
@Override
public FSInfo make(Context c) {
FSInfo instance = new NonResolvingCacheFSInfo();
c.put(FSInfo.class, instance);
return instance;
}
});
}
@Override
public Path getCanonicalFile(Path file) {
return file.toAbsolutePath();
}
}
private AnalysisResults runJavaAnalysisToExtractCompilationDetails(
Iterable<String> sources,
Iterable<String> classpath,
Iterable<String> bootclasspath,
Iterable<String> sourcepath,
Iterable<String> processorpath,
Iterable<String> processors,
Optional<Path> genSrcDir,
Iterable<String> options)
throws ExtractionException {
AnalysisResults results = new AnalysisResults();
// We will initialize and run the Javac compiler to detect which dependencies
// the current compilation has.
JavaCompiler compiler = findJavaCompiler();
if (compiler == null) {
// TODO(schroederc): provide link to further context
throw new IllegalStateException(
"Could not get system Java compiler; are you missing the JDK?");
}
DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>();
StandardJavaFileManager standardFileManager =
compiler.getStandardFileManager(diagnosticsCollector, null, null);
setupFSInfo(standardFileManager);
// We insert a filemanager that wraps the standard filemanager and records the compiler's
// usage of .java & .class files.
final UsageAsInputReportingFileManager fileManager =
new UsageAsInputReportingFileManager(standardFileManager);
Iterable<? extends JavaFileObject> sourceFiles = fileManager.getJavaFileForSources(sources);
// Generate class files in a temporary directory
Path tempDir;
try {
tempDir = Files.createTempDirectory("javac_extractor");
} catch (IOException ioe) {
throw new ExtractionException(
"Unable to create temporary .class output directory", ioe, true);
}
List<String> completeOptions =
completeCompilerOptions(
standardFileManager,
options,
classpath,
bootclasspath,
sourcepath,
processorpath,
tempDir);
final List<CompilationUnitTree> compilationUnits = new ArrayList<>();
Symtab syms;
try {
// Launch the java compiler with our modified settings and the filemanager wrapper
CompilationTask task =
compiler.getTask(
null, fileManager, diagnosticsCollector, completeOptions, null, sourceFiles);
ClassLoader loader = processingClassloader(classpath, processorpath);
List<Processor> procs = new ArrayList<>();
// Add any processors passed as flags.
for (String processor : processors) {
try {
procs.add(
loader
.loadClass(processor)
.asSubclass(Processor.class)
.getConstructor()
.newInstance());
} catch (Throwable e) {
throw new ExtractionException("Bad processor entry: " + processor, e, false);
}
}
if (procs.isEmpty()) {
// If no --processors were passed, add any processors registered in the META-INF/services
// configuration.
for (Processor proc : ServiceLoader.load(Processor.class, loader)) {
procs.add(proc);
}
}
procs.add(new ProcessAnnotation(fileManager));
JavacTask javacTask = (JavacTask) task;
javacTask.setProcessors(procs);
syms = Symtab.instance(((JavacTaskImpl) javacTask).getContext());
javacTask.addTaskListener(
new TaskListener() {
@Override
public void finished(TaskEvent e) {
if (e.getKind() == TaskEvent.Kind.PARSE) {
compilationUnits.add(e.getCompilationUnit());
}
}
@Override
public void started(TaskEvent e) {}
});
try {
// In order for the compiler to load all required .java & .class files we need to have it go
// through parsing, analysis & generate phases. Unfortunately the latter is needed to get a
// complete list, this was found as we were breaking on analyzing certain files.
// JavacTask#call() subsumes parse() and generate(), but calling those methods directly may
// silently ignore fatal errors.
results.hasErrors = !javacTask.call();
} catch (com.sun.tools.javac.util.Abort e) {
// Type resolution issues, the diagnostics will give hints on what's going wrong.
for (Diagnostic<? extends JavaFileObject> diagnostic :
diagnosticsCollector.getDiagnostics()) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
logger.severe("Fatal error in compiler: " + diagnostic.getMessage(Locale.ENGLISH));
}
}
throw new ExtractionException("Fatal error while running javac compiler.", e, false);
}
// If we encountered any compilation errors, we report them even though we
// still store the compilation information for this set of sources.
for (Diagnostic<? extends JavaFileObject> diag : diagnosticsCollector.getDiagnostics()) {
if (diag.getKind() == Diagnostic.Kind.ERROR) {
results.hasErrors = true;
if (diag.getSource() != null) {
logger.severe(
String.format(
"compiler error: %s(%d): %s",
diag.getSource().getName(),
diag.getLineNumber(),
diag.getMessage(Locale.ENGLISH)));
} else {
logger.severe("compiler error: " + diag.getMessage(Locale.ENGLISH));
}
}
}
// Ensure generated source directory is relative to root.
genSrcDir =
genSrcDir.transform(
p -> Paths.get(ExtractorUtils.tryMakeRelative(rootDirectory, p.toString())));
for (String source : sources) {
results.explicitSources.add(ExtractorUtils.tryMakeRelative(rootDirectory, source));
}
getAdditionalSourcePaths(compilationUnits, results);
// Find files potentially used for resolving .* imports.
findOnDemandImportedFiles(compilationUnits, fileManager);
// We accumulate all file contents from the java compiler so we can store it in the bigtable.
findRequiredFiles(fileManager, mapClassesToSources(syms), genSrcDir, results);
} finally {
try {
DeleteRecursively.delete(tempDir);
} catch (IOException ioe) {
logger.log(Level.SEVERE, "Failed to delete temporary directory " + tempDir, ioe);
}
}
return results;
}
/** Sets the given location using command-line flags and the FileManager API. */
private static void setLocation(
ModifiableOptions options,
StandardJavaFileManager fileManager,
Iterable<String> searchpath,
String flag,
StandardLocation location)
throws ExtractionException {
String joined = Joiner.on(":").join(searchpath);
if (!joined.isEmpty()) {
options.add(flag);
options.add(joined);
try {
List<File> files = new ArrayList<>();
for (String elt : searchpath) {
files.add(new File(elt));
}
fileManager.setLocation(location, files);
} catch (IOException e) {
throw new ExtractionException(String.format("Couldn't set %s", location), e, false);
}
}
}
/** Create the ClassLoader to use for annotation processors. */
private static ClassLoader processingClassloader(
Iterable<String> classpath, Iterable<String> processorpath) throws ExtractionException {
// If javac is run with -processor set and -processorpath *unset*, it will fall back to
// searching the regular classpath for annotation processors.
if (Iterables.isEmpty(processorpath)) {
processorpath = classpath;
}
List<URL> urls = new ArrayList<>();
for (String path : processorpath) {
try {
urls.add(new File(path).toURI().toURL());
} catch (MalformedURLException e) {
throw new ExtractionException("Bad processorpath entry", e, false);
}
}
ClassLoader parent = new MaskedClassLoader();
return new URLClassLoader(Iterables.toArray(urls, URL.class), parent);
}
/** Isolated classloader for annotation processors, to avoid skew with the ambient classpath. */
private static class MaskedClassLoader extends ClassLoader {
private MaskedClassLoader() {
// delegate only to the bootclasspath
super(getPlatformClassLoader());
}
// TODO(#2451): remove reflection and call ClassLoader.getPlatformClassLoader() directly
// once JDK 8 compatibility is no longer required.
public static ClassLoader getPlatformClassLoader() {
try {
// In JDK 9, all platform classes are visible to the platform class loader:
// https://docs.oracle.com/javase/9/docs/api/java/lang/ClassLoader.html#getPlatformClassLoader--
return (ClassLoader) ClassLoader.class.getMethod("getPlatformClassLoader").invoke(null);
} catch (ReflectiveOperationException e) {
// In earlier releases, set 'null' as the parent to delegate to the boot class loader.
return null;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.startsWith("com.sun.source.") || name.startsWith("com.sun.tools.")) {
return JavaCompilationUnitExtractor.class.getClassLoader().loadClass(name);
}
throw new ClassNotFoundException(name);
}
}
/**
* Completes the given raw compiler options with the given classpath, sourcepath, and temporary
* destination directory. Only options supported by the Java compiler will be within the returned
* {@link List}.
*/
private static ImmutableList<String> completeCompilerOptions(
StandardJavaFileManager standardFileManager,
Iterable<String> rawOptions,
Iterable<String> classpath,
Iterable<String> bootclasspath,
Iterable<String> sourcepath,
Iterable<String> processorpath,
Path tempDestinationDir)
throws ExtractionException {
ModifiableOptions completeOptions =
ModifiableOptions.of(rawOptions)
.removeUnsupportedOptions()
.ensureEncodingSet(StandardCharsets.UTF_8);
// Android always uses this option.
// TODO(asmundak): needs more work before contributing upstream.
completeOptions.add("-XDstringConcat=inline");
setLocation(
completeOptions, standardFileManager, classpath, "-cp", StandardLocation.CLASS_PATH);
setLocation(
completeOptions,
standardFileManager,
sourcepath,
"-sourcepath",
StandardLocation.SOURCE_PATH);
setLocation(
completeOptions,
standardFileManager,
processorpath,
"-processorpath",
StandardLocation.ANNOTATION_PROCESSOR_PATH);
setLocation(
completeOptions,
standardFileManager,
bootclasspath,
"-bootclasspath",
StandardLocation.PLATFORM_CLASS_PATH);
return completeOptions
.removeOptions(EnumSet.of(Option.D))
.add("-d")
.add(tempDestinationDir.toString())
.build();
}
private static JavaCompiler findJavaCompiler() {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null && moduleClassLoader != null) {
// This is all a bit of a hack to be able to extract OpenJDK itself, which
// uses a bootstrap compiler and a lot of JDK options to compile itself.
// Notably, when using modules the system compiler is inhibited and the actual compiler
// resides in jdk.compiler.iterim. Rather than hard-code this, just fall back to the first
// JavaCompiler we can find.
logger.warning("Unable to find system compiler, using first available.");
for (JavaCompiler found : ServiceLoader.load(JavaCompiler.class, moduleClassLoader)) {
return found;
}
}
return compiler;
}
/** Returns a map from a classfile's {@link URI} to its sourcefile path's basename. */
private static Map<URI, String> mapClassesToSources(Symtab syms) {
Map<URI, String> sourceBaseNames = new HashMap<>();
for (ClassSymbol sym : syms.getAllClasses()) {
if (sym.sourcefile != null && sym.classfile != null) {
String path = sym.sourcefile.toUri().getPath();
if (path != null) {
String basename = Paths.get(path).getFileName().toString();
if (!basename.endsWith(".java") && !basename.endsWith(".kt")) {
logger.warning(String.format("Invalid sourcefile name: '%s'", basename));
}
sourceBaseNames.put(sym.classfile.toUri(), basename);
}
}
}
return sourceBaseNames;
}
}