blob: 94b00d9838a1260574d313609477f2d49b43963e [file] [log] [blame]
/*
* Copyright 2000-2013 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.incremental.java;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.SystemProperties;
import com.intellij.util.concurrency.SequentialTaskExecutor;
import com.intellij.util.io.PersistentEnumeratorBase;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.ModuleChunk;
import org.jetbrains.jps.ProjectPaths;
import org.jetbrains.jps.builders.BuildRootIndex;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.builders.FileProcessor;
import org.jetbrains.jps.builders.impl.java.JavacCompilerTool;
import org.jetbrains.jps.builders.java.JavaBuilderExtension;
import org.jetbrains.jps.builders.java.JavaBuilderUtil;
import org.jetbrains.jps.builders.java.JavaCompilingTool;
import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
import org.jetbrains.jps.builders.java.dependencyView.Callbacks;
import org.jetbrains.jps.builders.java.dependencyView.Mappings;
import org.jetbrains.jps.builders.logging.ProjectBuilderLogger;
import org.jetbrains.jps.builders.storage.BuildDataCorruptedException;
import org.jetbrains.jps.cmdline.ProjectDescriptor;
import org.jetbrains.jps.incremental.*;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.ProgressMessage;
import org.jetbrains.jps.javac.*;
import org.jetbrains.jps.model.JpsDummyElement;
import org.jetbrains.jps.model.JpsProject;
import org.jetbrains.jps.model.java.JpsJavaExtensionService;
import org.jetbrains.jps.model.java.JpsJavaSdkType;
import org.jetbrains.jps.model.java.LanguageLevel;
import org.jetbrains.jps.model.java.compiler.*;
import org.jetbrains.jps.model.library.sdk.JpsSdk;
import org.jetbrains.jps.model.module.JpsModule;
import org.jetbrains.jps.model.module.JpsModuleType;
import org.jetbrains.jps.service.JpsServiceManager;
import javax.tools.*;
import java.io.*;
import java.net.ServerSocket;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Eugene Zhuravlev
* Date: 9/21/11
*/
public class JavaBuilder extends ModuleLevelBuilder {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.java.JavaBuilder");
public static final String BUILDER_NAME = "java";
private static final String JAVA_EXTENSION = "java";
private static final String DOT_JAVA_EXTENSION = "." + JAVA_EXTENSION;
private static final Key<Integer> JAVA_COMPILER_VERSION_KEY = Key.create("_java_compiler_version_");
public static final Key<Boolean> IS_ENABLED = Key.create("_java_compiler_enabled_");
private static final Key<JavaCompilingTool> COMPILING_TOOL = Key.create("_java_compiling_tool_");
private static final Key<AtomicReference<String>> COMPILER_VERSION_INFO = Key.create("_java_compiler_version_info_");
private static final Set<String> FILTERED_OPTIONS = new HashSet<String>(Arrays.<String>asList(
"-target"
));
private static final Set<String> FILTERED_SINGLE_OPTIONS = new HashSet<String>(Arrays.<String>asList(
"-g", "-deprecation", "-nowarn", "-verbose", "-proc:none", "-proc:only", "-proceedOnError"
));
public static final FileFilter JAVA_SOURCES_FILTER =
SystemInfo.isFileSystemCaseSensitive?
new FileFilter() {
public boolean accept(File file) {
return file.getPath().endsWith(DOT_JAVA_EXTENSION);
}
} :
new FileFilter() {
public boolean accept(File file) {
return StringUtil.endsWithIgnoreCase(file.getPath(), DOT_JAVA_EXTENSION);
}
};
private static final String RT_JAR_PATH_SUFFIX = File.separator + "rt.jar";
private final Executor myTaskRunner;
private static final List<ClassPostProcessor> ourClassProcessors = new ArrayList<ClassPostProcessor>();
private static final Set<JpsModuleType<?>> ourCompilableModuleTypes;
@Nullable
private static final File ourDefaultRtJar;
static {
ourCompilableModuleTypes = new HashSet<JpsModuleType<?>>();
for (JavaBuilderExtension extension : JpsServiceManager.getInstance().getExtensions(JavaBuilderExtension.class)) {
ourCompilableModuleTypes.addAll(extension.getCompilableModuleTypes());
}
File rtJar = null;
StringTokenizer tokenizer = new StringTokenizer(System.getProperty("sun.boot.class.path", ""), File.pathSeparator, false);
while (tokenizer.hasMoreTokens()) {
final String path = tokenizer.nextToken();
if (isRtJarPath(path)) {
rtJar = new File(path);
break;
}
}
ourDefaultRtJar = rtJar;
}
private static boolean isRtJarPath(String path) {
if (StringUtil.endsWithIgnoreCase(path, RT_JAR_PATH_SUFFIX)) {
return true;
}
return RT_JAR_PATH_SUFFIX.charAt(0) != '/' && StringUtil.endsWithIgnoreCase(path, "/rt.jar");
}
public static void registerClassPostProcessor(ClassPostProcessor processor) {
ourClassProcessors.add(processor);
}
public JavaBuilder(Executor tasksExecutor) {
super(BuilderCategory.TRANSLATOR);
myTaskRunner = new SequentialTaskExecutor(tasksExecutor);
//add here class processors in the sequence they should be executed
}
@NotNull
public String getPresentableName() {
return BUILDER_NAME;
}
@Override
public void buildStarted(CompileContext context) {
final JpsProject project = context.getProjectDescriptor().getProject();
final JpsJavaCompilerConfiguration config = JpsJavaExtensionService.getInstance().getCompilerConfiguration(project);
final String compilerId = config == null? JavaCompilers.JAVAC_ID : config.getJavaCompilerId();
if (LOG.isDebugEnabled()) {
LOG.debug("Java compiler ID: " + compilerId);
}
JavaCompilingTool compilingTool = JavaBuilderUtil.findCompilingTool(compilerId);
COMPILING_TOOL.set(context, compilingTool);
String messageText = compilingTool != null ? "Using " + compilingTool.getDescription() + " to compile java sources" : null;
COMPILER_VERSION_INFO.set(context, new AtomicReference<String>(messageText));
}
@Override
public List<String> getCompilableFileExtensions() {
return Collections.singletonList(JAVA_EXTENSION);
}
public ExitCode build(@NotNull CompileContext context,
@NotNull ModuleChunk chunk,
@NotNull DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
@NotNull OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
JavaCompilingTool compilingTool = COMPILING_TOOL.get(context);
if (!IS_ENABLED.get(context, Boolean.TRUE) || compilingTool == null) {
return ExitCode.NOTHING_DONE;
}
return doBuild(context, chunk, dirtyFilesHolder, outputConsumer, compilingTool);
}
public ExitCode doBuild(@NotNull CompileContext context,
@NotNull ModuleChunk chunk,
@NotNull DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
@NotNull OutputConsumer outputConsumer, JavaCompilingTool compilingTool) throws ProjectBuildException, IOException {
try {
final Set<File> filesToCompile = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
dirtyFilesHolder.processDirtyFiles(new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
public boolean apply(ModuleBuildTarget target, File file, JavaSourceRootDescriptor descriptor) throws IOException {
if (JAVA_SOURCES_FILTER.accept(file) && ourCompilableModuleTypes.contains(target.getModule().getModuleType())) {
filesToCompile.add(file);
}
return true;
}
});
if (JavaBuilderUtil.isCompileJavaIncrementally(context)) {
final ProjectBuilderLogger logger = context.getLoggingManager().getProjectBuilderLogger();
if (logger.isEnabled()) {
if (filesToCompile.size() > 0) {
logger.logCompiledFiles(filesToCompile, BUILDER_NAME, "Compiling files:");
}
}
}
return compile(context, chunk, dirtyFilesHolder, filesToCompile, outputConsumer, compilingTool);
}
catch (BuildDataCorruptedException e) {
throw e;
}
catch (ProjectBuildException e) {
throw e;
}
catch (PersistentEnumeratorBase.CorruptedException e) {
throw e;
}
catch (Exception e) {
LOG.info(e);
String message = e.getMessage();
if (message == null) {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final PrintStream stream = new PrintStream(out);
try {
e.printStackTrace(stream);
}
finally {
stream.close();
}
message = "Internal error: \n" + out.toString();
}
context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, message));
throw new StopBuildException();
}
}
private ExitCode compile(final CompileContext context,
ModuleChunk chunk,
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
Collection<File> files,
OutputConsumer outputConsumer, @NotNull JavaCompilingTool compilingTool)
throws Exception {
ExitCode exitCode = ExitCode.NOTHING_DONE;
final boolean hasSourcesToCompile = !files.isEmpty();
if (!hasSourcesToCompile && !dirtyFilesHolder.hasRemovedFiles()) {
return exitCode;
}
final ProjectDescriptor pd = context.getProjectDescriptor();
JavaBuilderUtil.ensureModuleHasJdk(chunk.representativeTarget().getModule(), context, BUILDER_NAME);
final Collection<File> classpath = ProjectPaths.getCompilationClasspath(chunk, false/*context.isProjectRebuild()*/);
final Collection<File> platformCp = ProjectPaths.getPlatformCompilationClasspath(chunk, false/*context.isProjectRebuild()*/);
// begin compilation round
final Mappings delta = pd.dataManager.getMappings().createDelta();
final Callbacks.Backend mappingsCallback = delta.getCallback();
final OutputFilesSink outputSink = new OutputFilesSink(context, outputConsumer, mappingsCallback, chunk.getPresentableShortName());
try {
if (hasSourcesToCompile) {
final AtomicReference<String> ref = COMPILER_VERSION_INFO.get(context);
final String versionInfo = ref.getAndSet(null); // display compiler version info only once per compile session
if (versionInfo != null) {
LOG.info(versionInfo);
context.processMessage(new CompilerMessage("", BuildMessage.Kind.INFO, versionInfo));
}
exitCode = ExitCode.OK;
final Set<File> srcPath = new HashSet<File>();
final BuildRootIndex index = pd.getBuildRootIndex();
for (ModuleBuildTarget target : chunk.getTargets()) {
for (JavaSourceRootDescriptor rd : index.getTempTargetRoots(target, context)) {
srcPath.add(rd.root);
}
}
final DiagnosticSink diagnosticSink = new DiagnosticSink(context);
final String chunkName = chunk.getName();
context.processMessage(new ProgressMessage("Parsing java... [" + chunk.getPresentableShortName() + "]"));
final int filesCount = files.size();
boolean compiledOk = true;
if (filesCount > 0) {
LOG.info("Compiling " + filesCount + " java files; module: " + chunkName + (chunk.containsTests() ? " (tests)" : ""));
if (LOG.isDebugEnabled()) {
for (File file : files) {
LOG.debug("Compiling " + file.getPath());
}
LOG.debug(" classpath for " + chunkName + ":");
for (File file : classpath) {
LOG.debug(" " + file.getAbsolutePath());
}
LOG.debug(" platform classpath for " + chunkName + ":");
for (File file : platformCp) {
LOG.debug(" " + file.getAbsolutePath());
}
}
try {
compiledOk = compileJava(context, chunk, files, classpath, platformCp, srcPath, diagnosticSink, outputSink, compilingTool);
}
finally {
// heuristic: incorrect paths data recovery, so that the next make should not contain non-existing sources in 'recompile' list
for (File file : diagnosticSink.getFilesWithErrors()) {
if (!file.exists()) {
FSOperations.markDeleted(context, file);
}
}
}
}
context.checkCanceled();
if (!compiledOk && diagnosticSink.getErrorCount() == 0) {
diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, "Compilation failed: internal java compiler error"));
}
if (!Utils.PROCEED_ON_ERROR_KEY.get(context, Boolean.FALSE) && diagnosticSink.getErrorCount() > 0) {
if (!compiledOk) {
diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.OTHER, "Errors occurred while compiling module '" + chunkName + "'"));
}
throw new StopBuildException(
"Compilation failed: errors: " + diagnosticSink.getErrorCount() + "; warnings: " + diagnosticSink.getWarningCount()
);
}
}
}
finally {
if (JavaBuilderUtil.updateMappings(context, delta, dirtyFilesHolder, chunk, files, outputSink.getSuccessfullyCompiled())) {
exitCode = ExitCode.ADDITIONAL_PASS_REQUIRED;
}
}
return exitCode;
}
private boolean compileJava(
final CompileContext context,
ModuleChunk chunk,
Collection<File> files,
Collection<File> classpath,
Collection<File> platformCp,
Collection<File> sourcePath,
DiagnosticOutputConsumer diagnosticSink,
final OutputFileConsumer outputSink, JavaCompilingTool compilingTool) throws Exception {
final TasksCounter counter = new TasksCounter();
COUNTER_KEY.set(context, counter);
final JpsJavaExtensionService javaExt = JpsJavaExtensionService.getInstance();
final JpsJavaCompilerConfiguration compilerConfig = javaExt.getCompilerConfiguration(context.getProjectDescriptor().getProject());
assert compilerConfig != null;
final Set<JpsModule> modules = chunk.getModules();
ProcessorConfigProfile profile = null;
if (modules.size() == 1) {
final JpsModule module = modules.iterator().next();
profile = compilerConfig.getAnnotationProcessingProfile(module);
}
else {
// perform cycle-related validations
Pair<String, LanguageLevel> pair = null;
for (JpsModule module : modules) {
final LanguageLevel moduleLevel = javaExt.getLanguageLevel(module);
if (pair == null) {
pair = Pair.create(module.getName(), moduleLevel); // first value
}
else {
if (!Comparing.equal(pair.getSecond(), moduleLevel)) {
final String message = "Modules " + pair.getFirst()+ " and " +module.getName() + " must have the same language level because of cyclic dependencies between them";
diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, message));
return true;
}
}
}
// check that all chunk modules are excluded from annotation processing
for (JpsModule module : modules) {
final ProcessorConfigProfile prof = compilerConfig.getAnnotationProcessingProfile(module);
if (prof.isEnabled()) {
final String message = "Annotation processing is not supported for module cycles. Please ensure that all modules from cycle [" + chunk.getName() + "] are excluded from annotation processing";
diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, message));
return true;
}
}
}
final Map<File, Set<File>> outs = buildOutputDirectoriesMap(context, chunk);
final List<String> options = getCompilationOptions(context, chunk, profile, compilingTool);
final ClassProcessingConsumer classesConsumer = new ClassProcessingConsumer(context, outputSink);
if (LOG.isDebugEnabled()) {
LOG.debug("Compiling chunk [" + chunk.getName() + "] with options: \"" + StringUtil.join(options, " ") + "\"");
}
try {
final boolean rc;
if (!shouldForkCompilerProcess(context, chunk, compilingTool)) {
final Collection<File> _platformCp = calcEffectivePlatformCp(platformCp, options, compilingTool);
if (_platformCp == null) {
diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR,
"Compact compilation profile was requested, but target platform for module \"" + chunk.getName() + "\" differs from javac's platform (" + System.getProperty("java.version") + ")\nCompilation profiles are not supported for such configuration"
));
return true;
}
rc = JavacMain.compile(
options, files, classpath, _platformCp, sourcePath, outs, diagnosticSink, classesConsumer, context.getCancelStatus(), compilingTool
);
}
else {
// fork external javac
final String sdkHome = getChunkSdkHome(chunk);
if (sdkHome == null) {
diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, "Cannot start javac process for " + chunk.getName() + ": unknown JDK home path.\nPlease check project configuration."));
return true;
}
final List<String> vmOptions = getCompilationVMOptions(context, compilingTool);
final ExternalJavacServer server = ensureJavacServerStarted(context);
rc = server.forkJavac(
context, options, vmOptions, files, classpath, platformCp, sourcePath, outs, diagnosticSink, classesConsumer, sdkHome, compilingTool
);
}
return rc;
}
finally {
counter.await();
}
}
private static boolean shouldForkCompilerProcess(CompileContext context, ModuleChunk chunk, JavaCompilingTool tool) {
final int compilerSdkVersion = getCompilerSdkVersion(context);
if (compilerSdkVersion < 9) {
// javac up to version 9 supports all previous releases
return false;
}
final int chunkSdkVersion = getChunkSdkVersion(chunk);
if (chunkSdkVersion < 0) {
return false; // was not able to determine jdk version, assuming in-process compiler
}
// according to JEP 182: Retiring javac "one plus three back" policy
return Math.abs(compilerSdkVersion - chunkSdkVersion) > 3;
}
// If platformCp of the build process is the same as the target plafform, do not specify platformCp explicitly
// this will allow javac to resolve against ct.sym file, which is required for the "compilation profiles" feature
@Nullable
private static Collection<File> calcEffectivePlatformCp(Collection<File> platformCp, List<String> options, JavaCompilingTool compilingTool) {
if (ourDefaultRtJar == null || !(compilingTool instanceof JavacCompilerTool)) {
return platformCp;
}
boolean profileFeatureRequested = false;
for (String option : options) {
if ("-profile".equalsIgnoreCase(option)) {
profileFeatureRequested = true;
break;
}
}
if (!profileFeatureRequested) {
return platformCp;
}
boolean isTargetPlatformSameAsBuildRuntime = false;
for (File file : platformCp) {
if (FileUtil.filesEqual(file, ourDefaultRtJar)) {
isTargetPlatformSameAsBuildRuntime = true;
break;
}
}
if (!isTargetPlatformSameAsBuildRuntime) {
// compact profile was requested, but we have to use alternative platform classpath to meet project settings
// consider this a compile error and let user re-configure the project
return null;
}
// returning empty list will force default behaviour for platform classpath calculation
// javac will resolve against its own bootclasspath and use ct.sym file when available
return Collections.emptyList();
}
private void submitAsyncTask(final CompileContext context, final Runnable taskRunnable) {
final TasksCounter counter = COUNTER_KEY.get(context);
assert counter != null;
counter.incTaskCount();
myTaskRunner.execute(new Runnable() {
public void run() {
try {
taskRunnable.run();
}
catch (Throwable e) {
context.processMessage(new CompilerMessage(BUILDER_NAME, e));
}
finally {
counter.decTaskCounter();
}
}
});
}
private static synchronized ExternalJavacServer ensureJavacServerStarted(@NotNull CompileContext context) throws Exception {
ExternalJavacServer server = ExternalJavacServer.KEY.get(context);
if (server != null) {
return server;
}
final int listenPort = findFreePort();
server = new ExternalJavacServer();
server.start(listenPort);
ExternalJavacServer.KEY.set(context, server);
return server;
}
private static int convertToNumber(String ver) {
if (ver == null) {
return 0;
}
final int quoteBegin = ver.indexOf("\"");
if (quoteBegin >= 0) {
final int quoteEnd = ver.indexOf("\"", quoteBegin + 1);
if (quoteEnd > quoteBegin) {
ver = ver.substring(quoteBegin + 1, quoteEnd);
}
}
if (ver.isEmpty()) {
return 0;
}
final String prefix = "1.";
final int parseBegin = ver.startsWith(prefix)? prefix.length() : 0;
final int parseEnd = ver.indexOf(".", parseBegin);
if (parseEnd > 0) {
ver = ver.substring(parseBegin, parseEnd);
}
else {
ver = ver.substring(parseBegin);
}
try {
return Integer.parseInt(ver);
}
catch (NumberFormatException ignored) {
}
return 0;
}
private static int findFreePort() {
try {
final ServerSocket serverSocket = new ServerSocket(0);
try {
return serverSocket.getLocalPort();
}
finally {
//workaround for linux : calling close() immediately after opening socket
//may result that socket is not closed
synchronized (serverSocket) {
try {
serverSocket.wait(1);
}
catch (Throwable ignored) {
}
}
serverSocket.close();
}
}
catch (IOException e) {
e.printStackTrace(System.err);
return ExternalJavacServer.DEFAULT_SERVER_PORT;
}
}
private static final Key<List<String>> JAVAC_OPTIONS = Key.create("_javac_options_");
private static final Key<List<String>> JAVAC_VM_OPTIONS = Key.create("_javac_vm_options_");
private static final Key<String> USER_DEFINED_BYTECODE_TARGET = Key.create("_user_defined_bytecode_target_");
private static List<String> getCompilationVMOptions(CompileContext context, JavaCompilingTool compilingTool) {
List<String> cached = JAVAC_VM_OPTIONS.get(context);
if (cached == null) {
loadCommonJavacOptions(context, compilingTool);
cached = JAVAC_VM_OPTIONS.get(context);
}
return cached;
}
private static List<String> getCompilationOptions(CompileContext context,
ModuleChunk chunk,
@Nullable ProcessorConfigProfile profile,
@NotNull JavaCompilingTool compilingTool) {
List<String> cached = JAVAC_OPTIONS.get(context);
if (cached == null) {
loadCommonJavacOptions(context, compilingTool);
cached = JAVAC_OPTIONS.get(context);
assert cached != null : context;
}
List<String> options = new ArrayList<String>(cached);
addCompilationOptions(options, context, chunk, profile);
return options;
}
public static void addCompilationOptions(List<String> options, CompileContext context, ModuleChunk chunk, @Nullable ProcessorConfigProfile profile) {
if (!isEncodingSet(options)) {
final CompilerEncodingConfiguration config = context.getProjectDescriptor().getEncodingConfiguration();
final String encoding = config.getPreferredModuleChunkEncoding(chunk);
if (config.getAllModuleChunkEncodings(chunk).size() > 1) {
final StringBuilder msgBuilder = new StringBuilder();
msgBuilder.append("Multiple encodings set for module chunk ").append(chunk.getName());
if (encoding != null) {
msgBuilder.append("\n\"").append(encoding).append("\" will be used by compiler");
}
context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.INFO, msgBuilder.toString()));
}
if (!StringUtil.isEmpty(encoding)) {
options.add("-encoding");
options.add(encoding);
}
}
final String langLevel = getLanguageLevel(chunk.getModules().iterator().next());
if (!StringUtil.isEmpty(langLevel)) {
options.add("-source");
options.add(langLevel);
}
final JpsJavaCompilerConfiguration compilerConfiguration = JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(
context.getProjectDescriptor().getProject()
);
String bytecodeTarget = null;
for (JpsModule module : chunk.getModules()) {
final String moduleTarget = compilerConfiguration.getByteCodeTargetLevel(module.getName());
if (moduleTarget == null) {
continue;
}
if (bytecodeTarget == null) {
bytecodeTarget = moduleTarget;
}
else {
if (moduleTarget.compareTo(bytecodeTarget) < 0) {
bytecodeTarget = moduleTarget; // use the lower possible target among modules that form the chunk
}
}
}
if (bytecodeTarget == null) {
// last resort and backward compatibility:
// check if user explicitly defined bytecode target in additional compiler options
bytecodeTarget = USER_DEFINED_BYTECODE_TARGET.get(context);
}
final int compilerSdkVersion = getCompilerSdkVersion(context);
final int chunkSdkVersion = getChunkSdkVersion(chunk);
if (bytecodeTarget != null) {
options.add("-target");
if (chunkSdkVersion > 0 && compilerSdkVersion > chunkSdkVersion) {
// if compiler is newer than module JDK
final int userSpecifiedTargetVersion = convertToNumber(bytecodeTarget);
if (userSpecifiedTargetVersion > 0 && userSpecifiedTargetVersion <= compilerSdkVersion) {
// if user-specified bytecode version can be determined and is supported by compiler
if (userSpecifiedTargetVersion > chunkSdkVersion) {
// and user-specified bytecode target level is higher than the highest one supported by the target JDK,
// force compiler to use highest-available bytecode target version that is supported by the chunk JDK.
bytecodeTarget = "1." + chunkSdkVersion;
}
}
// otherwise let compiler display compilation error about incorrectly set bytecode target version
}
options.add(bytecodeTarget);
}
else {
if (chunkSdkVersion > 0 && compilerSdkVersion > chunkSdkVersion) {
// force lower bytecode target level to match the version of sdk assigned to this chunk
options.add("-target");
options.add("1." + chunkSdkVersion);
}
}
if (profile != null && profile.isEnabled()) {
// configuring annotation processing
if (!profile.isObtainProcessorsFromClasspath()) {
final String processorsPath = profile.getProcessorPath();
options.add("-processorpath");
options.add(processorsPath == null? "" : FileUtil.toSystemDependentName(processorsPath.trim()));
}
final Set<String> processors = profile.getProcessors();
if (!processors.isEmpty()) {
options.add("-processor");
options.add(StringUtil.join(processors, ","));
}
for (Map.Entry<String, String> optionEntry : profile.getProcessorOptions().entrySet()) {
options.add("-A" + optionEntry.getKey() + "=" + optionEntry.getValue());
}
final File srcOutput = ProjectPaths.getAnnotationProcessorGeneratedSourcesOutputDir(
chunk.getModules().iterator().next(), chunk.containsTests(), profile
);
if (srcOutput != null) {
srcOutput.mkdirs();
options.add("-s");
options.add(srcOutput.getPath());
}
}
else {
options.add("-proc:none");
}
}
private static String getLanguageLevel(JpsModule module) {
final LanguageLevel level = JpsJavaExtensionService.getInstance().getLanguageLevel(module);
if (level != null) {
switch (level) {
case JDK_1_3: return "1.3";
case JDK_1_4: return "1.4";
case JDK_1_5: return "1.5";
case JDK_1_6: return "1.6";
case JDK_1_7: return "1.7";
case JDK_1_8: return "8";
case JDK_1_9: return "9";
}
}
return null;
}
private static boolean isEncodingSet(List<String> options) {
for (String option : options) {
if ("-encoding".equals(option)) {
return true;
}
}
return false;
}
private static int getCompilerSdkVersion(CompileContext context) {
final Integer cached = JAVA_COMPILER_VERSION_KEY.get(context);
if (cached != null) {
return cached;
}
int javaVersion = convertToNumber(SystemProperties.getJavaVersion());
JAVA_COMPILER_VERSION_KEY.set(context, javaVersion);
return javaVersion;
}
private static int getChunkSdkVersion(ModuleChunk chunk) {
int chunkSdkVersion = -1;
for (JpsModule module : chunk.getModules()) {
final JpsSdk<JpsDummyElement> sdk = module.getSdk(JpsJavaSdkType.INSTANCE);
if (sdk != null) {
final int moduleSdkVersion = convertToNumber(sdk.getVersionString());
if (moduleSdkVersion != 0 /*could determine the version*/&& (chunkSdkVersion < 0 || chunkSdkVersion > moduleSdkVersion)) {
chunkSdkVersion = moduleSdkVersion;
}
}
}
return chunkSdkVersion;
}
private static String getChunkSdkHome(ModuleChunk chunk) {
for (JpsModule module : chunk.getModules()) {
final JpsSdk<JpsDummyElement> sdk = module.getSdk(JpsJavaSdkType.INSTANCE);
if (sdk != null) {
return sdk.getHomePath();
}
}
return null;
}
private static void loadCommonJavacOptions(@NotNull CompileContext context, @NotNull JavaCompilingTool compilingTool) {
final List<String> options = new ArrayList<String>();
final List<String> vmOptions = new ArrayList<String>();
final JpsProject project = context.getProjectDescriptor().getProject();
final JpsJavaCompilerConfiguration compilerConfig = JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(project);
final JpsJavaCompilerOptions compilerOptions = compilerConfig.getCurrentCompilerOptions();
if (compilerOptions.DEBUGGING_INFO) {
options.add("-g");
}
if (compilerOptions.DEPRECATION) {
options.add("-deprecation");
}
if (compilerOptions.GENERATE_NO_WARNINGS) {
options.add("-nowarn");
}
if (compilerOptions instanceof EclipseCompilerOptions) {
final EclipseCompilerOptions eclipseOptions = (EclipseCompilerOptions)compilerOptions;
if (eclipseOptions.PROCEED_ON_ERROR) {
options.add("-proceedOnError");
}
}
final String customArgs = compilerOptions.ADDITIONAL_OPTIONS_STRING;
if (customArgs != null) {
final StringTokenizer customOptsTokenizer = new StringTokenizer(customArgs, " \t\r\n");
boolean skip = false;
boolean targetOptionFound = false;
while (customOptsTokenizer.hasMoreTokens()) {
final String userOption = customOptsTokenizer.nextToken();
if (FILTERED_OPTIONS.contains(userOption)) {
skip = true;
targetOptionFound = "-target".equals(userOption);
continue;
}
if (skip) {
skip = false;
if (targetOptionFound) {
targetOptionFound = false;
USER_DEFINED_BYTECODE_TARGET.set(context, userOption);
}
}
else {
if (!FILTERED_SINGLE_OPTIONS.contains(userOption)) {
if (userOption.startsWith("-J-")) {
vmOptions.add(userOption.substring("-J".length()));
}
else {
options.add(userOption);
}
}
}
}
}
compilingTool.processCompilerOptions(context, options);
JAVAC_OPTIONS.set(context, options);
JAVAC_VM_OPTIONS.set(context, vmOptions);
}
@Override
public void chunkBuildFinished(CompileContext context, ModuleChunk chunk) {
JavaBuilderUtil.cleanupChunkResources(context);
}
private static Map<File, Set<File>> buildOutputDirectoriesMap(CompileContext context, ModuleChunk chunk) {
final Map<File, Set<File>> map = new THashMap<File, Set<File>>(FileUtil.FILE_HASHING_STRATEGY);
for (ModuleBuildTarget target : chunk.getTargets()) {
final File outputDir = target.getOutputDir();
if (outputDir == null) {
continue;
}
final Set<File> roots = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
for (JavaSourceRootDescriptor descriptor : context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context)) {
roots.add(descriptor.root);
}
map.put(outputDir, roots);
}
return map;
}
private static class DiagnosticSink implements DiagnosticOutputConsumer {
private final CompileContext myContext;
private volatile int myErrorCount = 0;
private volatile int myWarningCount = 0;
private final Set<File> myFilesWithErrors = new HashSet<File>();
public DiagnosticSink(CompileContext context) {
myContext = context;
}
@Override
public void javaFileLoaded(File file) {
}
public void registerImports(final String className, final Collection<String> imports, final Collection<String> staticImports) {
//submitAsyncTask(myContext, new Runnable() {
// public void run() {
// final Callbacks.Backend callback = DELTA_MAPPINGS_CALLBACK_KEY.get(myContext);
// if (callback != null) {
// callback.registerImports(className, imports, staticImports);
// }
// }
//});
}
public void outputLineAvailable(String line) {
if (!StringUtil.isEmpty(line)) {
if (line.contains("java.lang.OutOfMemoryError")) {
myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "OutOfMemoryError: insufficient memory"));
myErrorCount++;
}
else {
final BuildMessage.Kind kind = getKindByMessageText(line);
if (kind == BuildMessage.Kind.ERROR) {
myErrorCount++;
}
else if (kind == BuildMessage.Kind.WARNING) {
myWarningCount++;
}
myContext.processMessage(new CompilerMessage(BUILDER_NAME, kind, line));
}
}
}
private static BuildMessage.Kind getKindByMessageText(String line) {
final String lowercasedLine = line.toLowerCase(Locale.US);
if (lowercasedLine.contains("error") || lowercasedLine.contains("requires target release")) {
return BuildMessage.Kind.ERROR;
}
return BuildMessage.Kind.INFO;
}
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
final CompilerMessage.Kind kind;
switch (diagnostic.getKind()) {
case ERROR:
kind = BuildMessage.Kind.ERROR;
myErrorCount++;
break;
case MANDATORY_WARNING:
case WARNING:
kind = BuildMessage.Kind.WARNING;
myWarningCount++;
break;
case NOTE:
default:
kind = BuildMessage.Kind.INFO;
}
File sourceFile = null;
try {
// for eclipse compiler just an attempt to call getSource() may lead to an NPE,
// so calling this method under try/catch to avoid induced compiler errors
final JavaFileObject source = diagnostic.getSource();
sourceFile = source != null ? Utils.convertToFile(source.toUri()) : null;
}
catch (Exception e) {
LOG.info(e);
}
final String srcPath;
if (sourceFile != null) {
myFilesWithErrors.add(sourceFile);
srcPath = FileUtil.toSystemIndependentName(sourceFile.getPath());
}
else {
srcPath = null;
}
String message = diagnostic.getMessage(Locale.US);
if (Utils.IS_TEST_MODE) {
LOG.info(message);
}
myContext.processMessage(new CompilerMessage(
BUILDER_NAME, kind, message, srcPath, diagnostic.getStartPosition(),
diagnostic.getEndPosition(), diagnostic.getPosition(), diagnostic.getLineNumber(),
diagnostic.getColumnNumber()
));
}
public int getErrorCount() {
return myErrorCount;
}
public int getWarningCount() {
return myWarningCount;
}
public Collection<File> getFilesWithErrors() {
return myFilesWithErrors;
}
}
private class ClassProcessingConsumer implements OutputFileConsumer {
private final CompileContext myContext;
private final OutputFileConsumer myDelegateOutputFileSink;
public ClassProcessingConsumer(CompileContext context, OutputFileConsumer sink) {
myContext = context;
myDelegateOutputFileSink = sink != null ? sink : new OutputFileConsumer() {
public void save(@NotNull OutputFileObject fileObject) {
throw new RuntimeException("Output sink for compiler was not specified");
}
};
}
public void save(@NotNull final OutputFileObject fileObject) {
// generated files must be saved synchronously, because some compilers (e.g. eclipse)
// may want to read them for further compilation
try {
final BinaryContent content = fileObject.getContent();
final File file = fileObject.getFile();
if (content != null) {
content.saveToFile(file);
}
else {
myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING, "Missing content for file " + file.getPath()));
}
}
catch (IOException e) {
myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
}
submitAsyncTask(myContext, new Runnable() {
public void run() {
try {
for (ClassPostProcessor processor : ourClassProcessors) {
processor.process(myContext, fileObject);
}
}
finally {
myDelegateOutputFileSink.save(fileObject);
}
}
});
}
}
private static final Key<TasksCounter> COUNTER_KEY = Key.create("_async_task_counter_");
private static final class TasksCounter {
private int myCounter = 0;
public synchronized void incTaskCount() {
myCounter++;
}
public synchronized void decTaskCounter() {
myCounter = Math.max(0, myCounter - 1);
if (myCounter == 0) {
notifyAll();
}
}
public synchronized void await() {
while (myCounter > 0) {
try {
wait();
}
catch (InterruptedException e) {
}
}
}
}
}