blob: ac4f4a0b78fc8dac2e4336a20c2c7068d91eec3c [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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 org.jetbrains.jps.javac;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.api.CanceledStatus;
import org.jetbrains.jps.builders.impl.java.JavacCompilerTool;
import org.jetbrains.jps.builders.java.CannotCreateJavaCompilerException;
import org.jetbrains.jps.builders.java.JavaCompilingTool;
import org.jetbrains.jps.builders.java.JavaSourceTransformer;
import org.jetbrains.jps.cmdline.ClasspathBootstrap;
import org.jetbrains.jps.incremental.LineOutputWriter;
import javax.tools.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author Eugene Zhuravlev
* Date: 1/21/12
*/
public class JavacMain {
private static final boolean IS_VM_6_VERSION = System.getProperty("java.version", "1.6").contains("1.6");
//private static final boolean ECLIPSE_COMPILER_SINGLE_THREADED_MODE = Boolean.parseBoolean(System.getProperty("jdt.compiler.useSingleThread", "false"));
private static final Set<String> FILTERED_OPTIONS = new HashSet<String>(Arrays.<String>asList(
"-d", "-classpath", "-cp", "-bootclasspath"
));
private static final Set<String> FILTERED_SINGLE_OPTIONS = new HashSet<String>(Arrays.<String>asList(
/*javac options*/ "-verbose", "-proc:only", "-implicit:class", "-implicit:none", "-Xprefer:newer", "-Xprefer:source"
));
public static boolean compile(Collection<String> options,
final Collection<File> sources,
Collection<File> classpath,
Collection<File> platformClasspath,
Collection<File> sourcePath,
Map<File, Set<File>> outputDirToRoots,
final DiagnosticOutputConsumer diagnosticConsumer,
final OutputFileConsumer outputSink,
CanceledStatus canceledStatus, @NotNull JavaCompilingTool compilingTool) {
JavaCompiler compiler;
try {
compiler = compilingTool.createCompiler();
}
catch (CannotCreateJavaCompilerException e) {
diagnosticConsumer.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, e.getMessage()));
return false;
}
for (File outputDir : outputDirToRoots.keySet()) {
outputDir.mkdirs();
}
final List<JavaSourceTransformer> transformers = getSourceTransformers();
final boolean usingJavac = compilingTool instanceof JavacCompilerTool;
final JavacFileManager fileManager = new JavacFileManager(new ContextImpl(compiler, diagnosticConsumer, outputSink, canceledStatus, usingJavac), transformers);
if (!platformClasspath.isEmpty()) {
// for javac6 this will prevent lazy initialization of Paths.bootClassPathRtJar
// and thus usage of symbol file for resolution, when this file is not expected to be used
fileManager.handleOption("-bootclasspath", Collections.singleton("").iterator());
}
fileManager.handleOption("-extdirs", Collections.singleton("").iterator()); // this will clear cached stuff
fileManager.handleOption("-endorseddirs", Collections.singleton("").iterator()); // this will clear cached stuff
final Collection<String> _options = prepareOptions(options, compilingTool);
try {
// to be on the safe side, we'll have to apply all options _before_ calling any of manager's methods
// i.e. getJavaFileObjectsFromFiles()
// This way the manager will be properly initialized. Namely, the encoding will be set correctly
// Note that due to lazy initialization in various components inside javac, handleOption() should be called before setLocation() and others
for (Iterator<String> iterator = _options.iterator(); iterator.hasNext(); ) {
fileManager.handleOption(iterator.next(), iterator);
}
try {
fileManager.setOutputDirectories(outputDirToRoots);
}
catch (IOException e) {
fileManager.getContext().reportMessage(Diagnostic.Kind.ERROR, e.getMessage());
return false;
}
if (!classpath.isEmpty()) {
try {
fileManager.setLocation(StandardLocation.CLASS_PATH, classpath);
if (!usingJavac && !isOptionSet(options, "-processorpath")) {
// for non-javac file manager ensure annotation processor path defaults to classpath
fileManager.setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, classpath);
}
}
catch (IOException e) {
fileManager.getContext().reportMessage(Diagnostic.Kind.ERROR, e.getMessage());
return false;
}
}
if (!platformClasspath.isEmpty()) {
try {
fileManager.handleOption("-bootclasspath", Collections.singleton("").iterator()); // this will clear cached stuff
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, buildPlatformClasspath(platformClasspath, _options));
}
catch (IOException e) {
fileManager.getContext().reportMessage(Diagnostic.Kind.ERROR, e.getMessage());
return false;
}
}
try {
// ensure the source path is set;
// otherwise, if not set, javac attempts to search both classes and sources in classpath;
// so if some classpath jars contain sources, it will attempt to compile them
fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePath);
}
catch (IOException e) {
fileManager.getContext().reportMessage(Diagnostic.Kind.ERROR, e.getMessage());
return false;
}
//noinspection IOResourceOpenedButNotSafelyClosed
final LineOutputWriter out = new LineOutputWriter() {
protected void lineAvailable(String line) {
if (usingJavac) {
diagnosticConsumer.outputLineAvailable(line);
}
else {
// todo: filter too verbose eclipse output?
}
}
};
final JavaCompiler.CompilationTask task = compiler.getTask(
out, fileManager, diagnosticConsumer, _options, null, fileManager.getJavaFileObjectsFromFiles(sources)
);
compilingTool.prepareCompilationTask(task, _options);
//if (!IS_VM_6_VERSION) { //todo!
// // Do not add the processor for JDK 1.6 because of the bugs in javac
// // The processor's presence may lead to NPE and resolve bugs in compiler
// final JavacASTAnalyser analyzer = new JavacASTAnalyser(outConsumer, !annotationProcessingEnabled);
// task.setProcessors(Collections.singleton(analyzer));
//}
return task.call();
}
catch(IllegalArgumentException e) {
diagnosticConsumer.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, e.getMessage()));
}
catch (CompilationCanceledException ignored) {
diagnosticConsumer.report(new PlainMessageDiagnostic(Diagnostic.Kind.OTHER, "Compilation was canceled"));
}
finally {
fileManager.close();
if (usingJavac) {
cleanupJavacNameTable();
}
}
return false;
}
private static List<JavaSourceTransformer> getSourceTransformers() {
final Class<JavaSourceTransformer> transformerClass = JavaSourceTransformer.class;
final ServiceLoader<JavaSourceTransformer> loader = ServiceLoader.load(transformerClass, transformerClass.getClassLoader());
final List<JavaSourceTransformer> transformers = new SmartList<JavaSourceTransformer>();
for (JavaSourceTransformer t : loader) {
transformers.add(t);
}
return transformers;
}
private static boolean isAnnotationProcessingEnabled(final Collection<String> options) {
for (String option : options) {
if ("-proc:none".equals(option)) {
return false;
}
}
return true;
}
private static boolean isOptionSet(final Collection<String> options, String option) {
for (String opt : options) {
if (option.equals(opt)) {
return true;
}
}
return false;
}
private static Collection<String> prepareOptions(final Collection<String> options, @NotNull JavaCompilingTool compilingTool) {
final List<String> result = new ArrayList<String>();
result.addAll(compilingTool.getDefaultCompilerOptions());
boolean skip = false;
for (String option : options) {
if (FILTERED_OPTIONS.contains(option)) {
skip = true;
continue;
}
if (!skip) {
if (!FILTERED_SINGLE_OPTIONS.contains(option) && !compilingTool.getDefaultCompilerOptions().contains(option)) {
result.add(option);
}
}
skip = false;
}
return result;
}
private static Collection<File> buildPlatformClasspath(Collection<File> platformClasspath, Collection<String> options) {
final Map<PathOption, String> argsMap = new HashMap<PathOption, String>();
for (Iterator<String> iterator = options.iterator(); iterator.hasNext(); ) {
final String arg = iterator.next();
for (PathOption pathOption : PathOption.values()) {
if (pathOption.parse(argsMap, arg, iterator)) {
break;
}
}
}
if (argsMap.isEmpty()) {
return platformClasspath;
}
final List<File> result = new ArrayList<File>();
appendFiles(argsMap, PathOption.PREPEND_CP, result, false);
appendFiles(argsMap, PathOption.ENDORSED, result, true);
appendFiles(argsMap, PathOption.D_ENDORSED, result, true);
result.addAll(platformClasspath);
appendFiles(argsMap, PathOption.APPEND_CP, result, false);
appendFiles(argsMap, PathOption.EXTDIRS, result, true);
appendFiles(argsMap, PathOption.D_EXTDIRS, result, true);
return result;
}
private static void appendFiles(Map<PathOption, String> args, PathOption option, Collection<File> container, boolean listDir) {
final String path = args.get(option);
if (path == null) {
return;
}
final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator, false);
while (tokenizer.hasMoreTokens()) {
final File file = new File(tokenizer.nextToken());
if (listDir) {
final File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
final String fName = f.getName();
if (fName.endsWith(".jar") || fName.endsWith(".zip")) {
container.add(f);
}
}
}
}
else {
container.add(file);
}
}
}
enum PathOption {
PREPEND_CP("-Xbootclasspath/p:"),
ENDORSED("-endorseddirs"), D_ENDORSED("-Djava.endorsed.dirs="),
APPEND_CP("-Xbootclasspath/a:"),
EXTDIRS("-extdirs"), D_EXTDIRS("-Djava.ext.dirs=");
private final String myArgName;
private final boolean myIsSuffix;
PathOption(String name) {
myArgName = name;
myIsSuffix = name.endsWith("=") || name.endsWith(":");
}
public boolean parse(Map<PathOption, String> container, String arg, Iterator<String> rest) {
if (myIsSuffix) {
if (arg.startsWith(myArgName)) {
container.put(this, arg.substring(myArgName.length()));
return true;
}
}
else {
if (arg.equals(myArgName)) {
if (rest.hasNext()) {
container.put(this, rest.next());
}
return true;
}
}
return false;
}
}
private static class ContextImpl implements JavacFileManager.Context {
private final StandardJavaFileManager myStdManager;
@Nullable
private final Method myCacheClearMethod;
private final DiagnosticOutputConsumer myOutConsumer;
private final OutputFileConsumer myOutputFileSink;
private final CanceledStatus myCanceledStatus;
private static final AtomicBoolean ourOptimizedManagerMissingReported = new AtomicBoolean(false);
public ContextImpl(@NotNull JavaCompiler compiler,
@NotNull DiagnosticOutputConsumer outConsumer,
@NotNull OutputFileConsumer sink,
CanceledStatus canceledStatus, boolean canUseOptimizedmanager) {
myOutConsumer = outConsumer;
myOutputFileSink = sink;
myCanceledStatus = canceledStatus;
StandardJavaFileManager optimizedManager = null;
Method cacheClearMethod = null;
if (canUseOptimizedmanager) {
final Class<StandardJavaFileManager> optimizedManagerClass = ClasspathBootstrap.getOptimizedFileManagerClass();
if (optimizedManagerClass != null) {
try {
final Constructor<StandardJavaFileManager> constructor = optimizedManagerClass.getConstructor();
// if optimizedManagerClass is loaded by another classloader, cls.newInstance() will not work
// that's why we need to call setAccessible() to ensure access
constructor.setAccessible(true);
optimizedManager = constructor.newInstance();
cacheClearMethod = ClasspathBootstrap.getOptimizedFileManagerCacheClearMethod();
}
catch (Throwable e) {
if (SystemInfo.isWindows) {
reportMissingOptimizedManager(outConsumer, e.getMessage());
}
}
}
else {
reportMissingOptimizedManager(outConsumer, null);
}
}
myCacheClearMethod = cacheClearMethod;
if (optimizedManager != null) {
myStdManager = optimizedManager;
}
else {
myStdManager = compiler.getStandardFileManager(outConsumer, Locale.US, null);
}
}
private static void reportMissingOptimizedManager(DiagnosticOutputConsumer outConsumer, String message) {
if (!ourOptimizedManagerMissingReported.getAndSet(true)) {
if (message == null) {
message = ClasspathBootstrap.getOptimizedFileManagerLoadError();
if (message == null) {
message = "";
}
}
outConsumer.report(new PlainMessageDiagnostic(Diagnostic.Kind.OTHER, "JPS build failed to load optimized file manager for javac:\n" + message));
}
}
public boolean isCanceled() {
return myCanceledStatus.isCanceled();
}
public StandardJavaFileManager getStandardFileManager() {
return myStdManager;
}
public void reportMessage(final Diagnostic.Kind kind, String message) {
myOutConsumer.report(new PlainMessageDiagnostic(kind, message));
}
public void consumeOutputFile(@NotNull final OutputFileObject cls) {
try {
myOutputFileSink.save(cls);
}
finally {
final Method cacheClearMethod = myCacheClearMethod;
if (cacheClearMethod != null) {
try {
cacheClearMethod.invoke(myStdManager, cls.getFile());
}
catch (Throwable e) {
//noinspection UseOfSystemOutOrSystemErr
e.printStackTrace(System.err);
}
}
}
}
}
private static final class NameTableCleanupDataHolder {
static final Object emptyList;
static final Field freelistField;
static {
try {
final ClassLoader loader = ToolProvider.getSystemToolClassLoader();
if (loader == null) {
throw new RuntimeException("no tools provided");
}
final Class<?> listClass = Class.forName("com.sun.tools.javac.util.List", true, loader);
final Method nilMethod = listClass.getDeclaredMethod("nil");
emptyList = nilMethod.invoke(null);
Field freelistRef;
try {
// trying jdk 6
freelistRef = Class.forName("com.sun.tools.javac.util.Name$Table", true, loader).getDeclaredField("freelist");
}
catch (Exception e) {
// trying jdk 7
freelistRef = Class.forName("com.sun.tools.javac.util.SharedNameTable", true, loader).getDeclaredField("freelist");
}
freelistRef.setAccessible(true);
freelistField = freelistRef;
}
catch(RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private static void cleanupJavacNameTable() {
try {
final Field freelistField = NameTableCleanupDataHolder.freelistField;
final Object emptyList = NameTableCleanupDataHolder.emptyList;
// both parameters should be non-null if properly initialized
if (freelistField != null && emptyList != null) {
// the access to static 'freeList' field is synchronized inside javac, so we must use "synchronized" too
synchronized (freelistField.getDeclaringClass()) {
freelistField.set(null, emptyList);
}
}
}
catch (Throwable ignored) {
}
}
private static class ZipFileIndexCleanupDataHolder {
@Nullable
static final Method cacheInstanceGetter;
@Nullable
static final Method cacheClearMethod;
static {
Method getterMethod = null;
Method clearMethod = null;
try {
//trying JDK 6
clearMethod = Class.forName("com.sun.tools.javac.zip.ZipFileIndex").getDeclaredMethod("clearCache");
clearMethod.setAccessible(true);
}
catch (Throwable e) {
try {
final Class<?> cacheClass = Class.forName("com.sun.tools.javac.file.ZipFileIndexCache");
clearMethod = cacheClass.getDeclaredMethod("clearCache");
getterMethod = cacheClass.getDeclaredMethod("getSharedInstance");
clearMethod.setAccessible(true);
getterMethod.setAccessible(true);
}
catch (Throwable ignored2) {
clearMethod = null;
getterMethod = null;
}
}
cacheInstanceGetter = getterMethod;
cacheClearMethod = clearMethod;
}
}
private static volatile boolean zipCacheCleanupPossible = true;
public static void clearCompilerZipFileCache() {
if (zipCacheCleanupPossible) {
final Method clearMethod = ZipFileIndexCleanupDataHolder.cacheClearMethod;
if (clearMethod != null) {
final Method getter = ZipFileIndexCleanupDataHolder.cacheInstanceGetter;
try {
Object instance = getter != null? getter.invoke(null) : null;
clearMethod.invoke(instance);
}
catch (Throwable e) {
zipCacheCleanupPossible = false;
}
}
else {
zipCacheCleanupPossible = false;
}
}
}
}