Groovy-Eclipse compiler support back-end (IDEA-52379)
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / incremental / java / JavaBuilder.java
1 /*
2  * Copyright 2000-2013 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.jps.incremental.java;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Comparing;
20 import com.intellij.openapi.util.Key;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.util.SystemProperties;
26 import com.intellij.util.concurrency.SequentialTaskExecutor;
27 import com.intellij.util.io.PersistentEnumeratorBase;
28 import gnu.trove.THashMap;
29 import gnu.trove.THashSet;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32 import org.jetbrains.jps.ModuleChunk;
33 import org.jetbrains.jps.ProjectPaths;
34 import org.jetbrains.jps.builders.BuildRootIndex;
35 import org.jetbrains.jps.builders.DirtyFilesHolder;
36 import org.jetbrains.jps.builders.FileProcessor;
37 import org.jetbrains.jps.builders.impl.java.JavacCompilerTool;
38 import org.jetbrains.jps.builders.java.JavaBuilderExtension;
39 import org.jetbrains.jps.builders.java.JavaBuilderUtil;
40 import org.jetbrains.jps.builders.java.JavaCompilingTool;
41 import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
42 import org.jetbrains.jps.builders.java.dependencyView.Callbacks;
43 import org.jetbrains.jps.builders.java.dependencyView.Mappings;
44 import org.jetbrains.jps.builders.logging.ProjectBuilderLogger;
45 import org.jetbrains.jps.builders.storage.BuildDataCorruptedException;
46 import org.jetbrains.jps.cmdline.ProjectDescriptor;
47 import org.jetbrains.jps.incremental.*;
48 import org.jetbrains.jps.incremental.messages.BuildMessage;
49 import org.jetbrains.jps.incremental.messages.CompilerMessage;
50 import org.jetbrains.jps.incremental.messages.ProgressMessage;
51 import org.jetbrains.jps.javac.*;
52 import org.jetbrains.jps.model.JpsDummyElement;
53 import org.jetbrains.jps.model.JpsProject;
54 import org.jetbrains.jps.model.java.JpsJavaExtensionService;
55 import org.jetbrains.jps.model.java.JpsJavaSdkType;
56 import org.jetbrains.jps.model.java.LanguageLevel;
57 import org.jetbrains.jps.model.java.compiler.*;
58 import org.jetbrains.jps.model.library.sdk.JpsSdk;
59 import org.jetbrains.jps.model.module.JpsModule;
60 import org.jetbrains.jps.model.module.JpsModuleType;
61 import org.jetbrains.jps.service.JpsServiceManager;
62
63 import javax.tools.*;
64 import java.io.*;
65 import java.net.ServerSocket;
66 import java.util.*;
67 import java.util.concurrent.Executor;
68 import java.util.concurrent.atomic.AtomicReference;
69
70 /**
71  * @author Eugene Zhuravlev
72  *         Date: 9/21/11
73  */
74 public class JavaBuilder extends ModuleLevelBuilder {
75   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.java.JavaBuilder");
76   public static final String BUILDER_NAME = "java";
77   private static final String JAVA_EXTENSION = "java";
78   private static final String DOT_JAVA_EXTENSION = "." + JAVA_EXTENSION;
79   private static final Key<Integer> JAVA_COMPILER_VERSION_KEY = Key.create("_java_compiler_version_");
80   public static final Key<Boolean> IS_ENABLED = Key.create("_java_compiler_enabled_");
81   private static final Key<JavaCompilingTool> COMPILING_TOOL = Key.create("_java_compiling_tool_");
82   private static final Key<AtomicReference<String>> COMPILER_VERSION_INFO = Key.create("_java_compiler_version_info_");
83
84   private static final Set<String> FILTERED_OPTIONS = new HashSet<String>(Arrays.<String>asList(
85     "-target"
86   ));
87   private static final Set<String> FILTERED_SINGLE_OPTIONS = new HashSet<String>(Arrays.<String>asList(
88     "-g", "-deprecation", "-nowarn", "-verbose", "-proc:none", "-proc:only", "-proceedOnError"
89   ));
90
91   public static final FileFilter JAVA_SOURCES_FILTER =
92     SystemInfo.isFileSystemCaseSensitive?
93     new FileFilter() {
94       public boolean accept(File file) {
95         return file.getPath().endsWith(DOT_JAVA_EXTENSION);
96       }
97     } :
98     new FileFilter() {
99       public boolean accept(File file) {
100         return StringUtil.endsWithIgnoreCase(file.getPath(), DOT_JAVA_EXTENSION);
101       }
102     };
103   private static final String RT_JAR_PATH_SUFFIX = File.separator + "rt.jar";
104
105   private final Executor myTaskRunner;
106   private static final List<ClassPostProcessor> ourClassProcessors = new ArrayList<ClassPostProcessor>();
107   private static final Set<JpsModuleType<?>> ourCompilableModuleTypes;
108   @Nullable
109   private static final File ourDefaultRtJar;
110   static {
111     ourCompilableModuleTypes = new HashSet<JpsModuleType<?>>();
112     for (JavaBuilderExtension extension : JpsServiceManager.getInstance().getExtensions(JavaBuilderExtension.class)) {
113       ourCompilableModuleTypes.addAll(extension.getCompilableModuleTypes());
114     }
115     File rtJar = null;
116     StringTokenizer tokenizer = new StringTokenizer(System.getProperty("sun.boot.class.path", ""), File.pathSeparator, false);
117     while (tokenizer.hasMoreTokens()) {
118       final String path = tokenizer.nextToken();
119       if (isRtJarPath(path)) {
120         rtJar = new File(path);
121         break;
122       }
123     }
124     ourDefaultRtJar = rtJar;
125   }
126   
127   private static boolean isRtJarPath(String path) {
128     if (StringUtil.endsWithIgnoreCase(path, RT_JAR_PATH_SUFFIX)) {
129       return true;
130     }
131     return RT_JAR_PATH_SUFFIX.charAt(0) != '/' && StringUtil.endsWithIgnoreCase(path, "/rt.jar");
132   }
133
134   public static void registerClassPostProcessor(ClassPostProcessor processor) {
135     ourClassProcessors.add(processor);
136   }
137
138   public JavaBuilder(Executor tasksExecutor) {
139     super(BuilderCategory.TRANSLATOR);
140     myTaskRunner = new SequentialTaskExecutor(tasksExecutor);
141     //add here class processors in the sequence they should be executed
142   }
143
144   @NotNull
145   public String getPresentableName() {
146     return BUILDER_NAME;
147   }
148
149   @Override
150   public void buildStarted(CompileContext context) {
151     final JpsProject project = context.getProjectDescriptor().getProject();
152     final JpsJavaCompilerConfiguration config = JpsJavaExtensionService.getInstance().getCompilerConfiguration(project);
153     final String compilerId = config == null? JavaCompilers.JAVAC_ID : config.getJavaCompilerId();
154     if (LOG.isDebugEnabled()) {
155       LOG.debug("Java compiler ID: " + compilerId);
156     }
157     JavaCompilingTool compilingTool = JavaBuilderUtil.findCompilingTool(compilerId);
158     COMPILING_TOOL.set(context, compilingTool);
159     String messageText = compilingTool != null ? "Using " + compilingTool.getDescription() + " to compile java sources" : null;
160     COMPILER_VERSION_INFO.set(context, new AtomicReference<String>(messageText));
161   }
162
163   @Override
164   public List<String> getCompilableFileExtensions() {
165     return Collections.singletonList(JAVA_EXTENSION);
166   }
167
168   public ExitCode build(@NotNull CompileContext context,
169                         @NotNull ModuleChunk chunk,
170                         @NotNull DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
171                         @NotNull OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
172     JavaCompilingTool compilingTool = COMPILING_TOOL.get(context);
173     if (!IS_ENABLED.get(context, Boolean.TRUE) || compilingTool == null) {
174       return ExitCode.NOTHING_DONE;
175     }
176     return doBuild(context, chunk, dirtyFilesHolder, outputConsumer, compilingTool);
177   }
178
179   public ExitCode doBuild(@NotNull CompileContext context,
180                           @NotNull ModuleChunk chunk,
181                           @NotNull DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
182                           @NotNull OutputConsumer outputConsumer, JavaCompilingTool compilingTool) throws ProjectBuildException, IOException {
183     try {
184       final Set<File> filesToCompile = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
185
186       dirtyFilesHolder.processDirtyFiles(new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
187         public boolean apply(ModuleBuildTarget target, File file, JavaSourceRootDescriptor descriptor) throws IOException {
188           if (JAVA_SOURCES_FILTER.accept(file) && ourCompilableModuleTypes.contains(target.getModule().getModuleType())) {
189             filesToCompile.add(file);
190           }
191           return true;
192         }
193       });
194
195       if (JavaBuilderUtil.isCompileJavaIncrementally(context)) {
196         final ProjectBuilderLogger logger = context.getLoggingManager().getProjectBuilderLogger();
197         if (logger.isEnabled()) {
198           if (filesToCompile.size() > 0) {
199             logger.logCompiledFiles(filesToCompile, BUILDER_NAME, "Compiling files:");
200           }
201         }
202       }
203
204       return compile(context, chunk, dirtyFilesHolder, filesToCompile, outputConsumer, compilingTool);
205     }
206     catch (BuildDataCorruptedException e) {
207       throw e;
208     }
209     catch (ProjectBuildException e) {
210       throw e;
211     }
212     catch (PersistentEnumeratorBase.CorruptedException e) {
213       throw e;
214     }
215     catch (Exception e) {
216       LOG.info(e);
217       String message = e.getMessage();
218       if (message == null) {
219         final ByteArrayOutputStream out = new ByteArrayOutputStream();
220         final PrintStream stream = new PrintStream(out);
221         try {
222           e.printStackTrace(stream);
223         }
224         finally {
225           stream.close();
226         }
227         message = "Internal error: \n" + out.toString();
228       }
229       context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, message));
230       throw new StopBuildException();
231     }
232   }
233
234   private ExitCode compile(final CompileContext context,
235                            ModuleChunk chunk,
236                            DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
237                            Collection<File> files,
238                            OutputConsumer outputConsumer, @NotNull JavaCompilingTool compilingTool)
239     throws Exception {
240     ExitCode exitCode = ExitCode.NOTHING_DONE;
241
242     final boolean hasSourcesToCompile = !files.isEmpty();
243
244     if (!hasSourcesToCompile && !dirtyFilesHolder.hasRemovedFiles()) {
245       return exitCode;
246     }
247
248     final ProjectDescriptor pd = context.getProjectDescriptor();
249
250     JavaBuilderUtil.ensureModuleHasJdk(chunk.representativeTarget().getModule(), context, BUILDER_NAME);
251     final Collection<File> classpath = ProjectPaths.getCompilationClasspath(chunk, false/*context.isProjectRebuild()*/);
252     final Collection<File> platformCp = ProjectPaths.getPlatformCompilationClasspath(chunk, false/*context.isProjectRebuild()*/);
253
254     // begin compilation round
255     final Mappings delta = pd.dataManager.getMappings().createDelta();
256     final Callbacks.Backend mappingsCallback = delta.getCallback();
257     final OutputFilesSink outputSink = new OutputFilesSink(context, outputConsumer, mappingsCallback, chunk.getPresentableShortName());
258     try {
259       if (hasSourcesToCompile) {
260         final AtomicReference<String> ref = COMPILER_VERSION_INFO.get(context);
261         final String versionInfo = ref.getAndSet(null); // display compiler version info only once per compile session
262         if (versionInfo != null) {
263           LOG.info(versionInfo);
264           context.processMessage(new CompilerMessage("", BuildMessage.Kind.INFO, versionInfo));
265         }
266         exitCode = ExitCode.OK;
267
268         final Set<File> srcPath = new HashSet<File>();
269         final BuildRootIndex index = pd.getBuildRootIndex();
270         for (ModuleBuildTarget target : chunk.getTargets()) {
271           for (JavaSourceRootDescriptor rd : index.getTempTargetRoots(target, context)) {
272             srcPath.add(rd.root);
273           }
274         }
275         final DiagnosticSink diagnosticSink = new DiagnosticSink(context);
276         
277         final String chunkName = chunk.getName();
278         context.processMessage(new ProgressMessage("Parsing java... [" + chunk.getPresentableShortName() + "]"));
279
280         final int filesCount = files.size();
281         boolean compiledOk = true;
282         if (filesCount > 0) {
283           LOG.info("Compiling " + filesCount + " java files; module: " + chunkName + (chunk.containsTests() ? " (tests)" : ""));
284           if (LOG.isDebugEnabled()) {
285             for (File file : files) {
286               LOG.debug("Compiling " + file.getPath());
287             }
288             LOG.debug(" classpath for " + chunkName + ":");
289             for (File file : classpath) {
290               LOG.debug("  " + file.getAbsolutePath());
291             }
292             LOG.debug(" platform classpath for " + chunkName + ":");
293             for (File file : platformCp) {
294               LOG.debug("  " + file.getAbsolutePath());
295             }
296           }
297           try {
298             compiledOk = compileJava(context, chunk, files, classpath, platformCp, srcPath, diagnosticSink, outputSink, compilingTool);
299           }
300           finally {
301             // heuristic: incorrect paths data recovery, so that the next make should not contain non-existing sources in 'recompile' list
302             for (File file : diagnosticSink.getFilesWithErrors()) {
303               if (!file.exists()) {
304                 FSOperations.markDeleted(context, file);
305               }
306             }
307           }
308         }
309
310         context.checkCanceled();
311
312         if (!compiledOk && diagnosticSink.getErrorCount() == 0) {
313           diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, "Compilation failed: internal java compiler error"));
314         }
315         if (!Utils.PROCEED_ON_ERROR_KEY.get(context, Boolean.FALSE) && diagnosticSink.getErrorCount() > 0) {
316           if (!compiledOk) {
317             diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.OTHER, "Errors occurred while compiling module '" + chunkName + "'"));
318           }
319           throw new StopBuildException(
320             "Compilation failed: errors: " + diagnosticSink.getErrorCount() + "; warnings: " + diagnosticSink.getWarningCount()
321           );
322         }
323       }
324     }
325     finally {
326       if (JavaBuilderUtil.updateMappings(context, delta, dirtyFilesHolder, chunk, files, outputSink.getSuccessfullyCompiled())) {
327         exitCode = ExitCode.ADDITIONAL_PASS_REQUIRED;
328       }
329     }
330
331     return exitCode;
332   }
333
334   private boolean compileJava(
335     final CompileContext context,
336     ModuleChunk chunk,
337     Collection<File> files,
338     Collection<File> classpath,
339     Collection<File> platformCp,
340     Collection<File> sourcePath,
341     DiagnosticOutputConsumer diagnosticSink,
342     final OutputFileConsumer outputSink, JavaCompilingTool compilingTool) throws Exception {
343
344     final TasksCounter counter = new TasksCounter();
345     COUNTER_KEY.set(context, counter);
346
347     final JpsJavaExtensionService javaExt = JpsJavaExtensionService.getInstance();
348     final JpsJavaCompilerConfiguration compilerConfig = javaExt.getCompilerConfiguration(context.getProjectDescriptor().getProject());
349     assert compilerConfig != null;
350
351     final Set<JpsModule> modules = chunk.getModules();
352     ProcessorConfigProfile profile = null;
353     if (modules.size() == 1) {
354       profile = compilerConfig.getAnnotationProcessingProfile(modules.iterator().next());
355     }
356     else {
357       String message = validateCycle(chunk, javaExt, compilerConfig, modules);
358       if (message != null) {
359         diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, message));
360         return true;
361       }
362     }
363
364     final Map<File, Set<File>> outs = buildOutputDirectoriesMap(context, chunk);
365     final List<String> options = getCompilationOptions(context, chunk, profile, compilingTool);
366     final ClassProcessingConsumer classesConsumer = new ClassProcessingConsumer(context, outputSink);
367     if (LOG.isDebugEnabled()) {
368       LOG.debug("Compiling chunk [" + chunk.getName() + "] with options: \"" + StringUtil.join(options, " ") + "\"");
369     }
370     try {
371       final boolean rc;
372       if (!shouldForkCompilerProcess(context, chunk, compilingTool)) {
373         final Collection<File> _platformCp = calcEffectivePlatformCp(platformCp, options, compilingTool);
374         if (_platformCp == null) {
375           diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, 
376             "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"
377           ));
378           return true;
379         }
380         rc = JavacMain.compile(
381           options, files, classpath, _platformCp, sourcePath, outs, diagnosticSink, classesConsumer, context.getCancelStatus(), compilingTool
382         );
383       }
384       else {
385         // fork external javac
386         final String sdkHome = getChunkSdkHome(chunk);
387         if (sdkHome == null) {
388           diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, "Cannot start javac process for " + chunk.getName() + ": unknown JDK home path.\nPlease check project configuration."));
389           return true;
390         }
391
392         final List<String> vmOptions = getCompilationVMOptions(context, compilingTool);
393         final ExternalJavacServer server = ensureJavacServerStarted(context);
394         rc = server.forkJavac(
395           context, options, vmOptions, files, classpath, platformCp, sourcePath, outs, diagnosticSink, classesConsumer, sdkHome, compilingTool
396         );
397       }
398       return rc;
399     }
400     finally {
401       counter.await();
402     }
403   }
404
405   @Nullable
406   public static String validateCycle(ModuleChunk chunk,
407                                      JpsJavaExtensionService javaExt,
408                                      JpsJavaCompilerConfiguration compilerConfig, Set<JpsModule> modules) {
409     Pair<String, LanguageLevel> pair = null;
410     for (JpsModule module : modules) {
411       final LanguageLevel moduleLevel = javaExt.getLanguageLevel(module);
412       if (pair == null) {
413         pair = Pair.create(module.getName(), moduleLevel); // first value
414       }
415       else {
416         if (!Comparing.equal(pair.getSecond(), moduleLevel)) {
417           return "Modules " +
418                  pair.getFirst() +
419                  " and " +
420                  module.getName() +
421                  " must have the same language level because of cyclic dependencies between them";
422         }
423       }
424     }
425
426     // check that all chunk modules are excluded from annotation processing
427     for (JpsModule module : modules) {
428       final ProcessorConfigProfile prof = compilerConfig.getAnnotationProcessingProfile(module);
429       if (prof.isEnabled()) {
430         return "Annotation processing is not supported for module cycles. Please ensure that all modules from cycle [" +
431                chunk.getName() +
432                "] are excluded from annotation processing";
433       }
434     }
435     return null;
436   }
437
438   private static boolean shouldForkCompilerProcess(CompileContext context, ModuleChunk chunk, JavaCompilingTool tool) {
439     final int compilerSdkVersion = getCompilerSdkVersion(context);
440     if (compilerSdkVersion < 9) {
441       // javac up to version 9 supports all previous releases
442       return false;
443     }
444     final int chunkSdkVersion = getChunkSdkVersion(chunk);
445     if (chunkSdkVersion < 0) {
446       return false; // was not able to determine jdk version, assuming in-process compiler
447     }
448     // according to JEP 182: Retiring javac "one plus three back" policy
449     return Math.abs(compilerSdkVersion - chunkSdkVersion) > 3; 
450   }
451
452   // If platformCp of the build process is the same as the target plafform, do not specify platformCp explicitly
453   // this will allow javac to resolve against ct.sym file, which is required for the "compilation profiles" feature
454   @Nullable
455   private static Collection<File> calcEffectivePlatformCp(Collection<File> platformCp, List<String> options, JavaCompilingTool compilingTool) {
456     if (ourDefaultRtJar == null || !(compilingTool instanceof JavacCompilerTool)) {
457       return platformCp;
458     }
459     boolean profileFeatureRequested = false;
460     for (String option : options) {
461       if ("-profile".equalsIgnoreCase(option)) {
462         profileFeatureRequested = true;
463         break;
464       }
465     }
466     if (!profileFeatureRequested) {
467       return platformCp;
468     }
469     boolean isTargetPlatformSameAsBuildRuntime = false;
470     for (File file : platformCp) {
471       if (FileUtil.filesEqual(file, ourDefaultRtJar)) {
472         isTargetPlatformSameAsBuildRuntime = true;
473         break;
474       }
475     }
476     if (!isTargetPlatformSameAsBuildRuntime) {
477       // compact profile was requested, but we have to use alternative platform classpath to meet project settings
478       // consider this a compile error and let user re-configure the project 
479       return null;
480     }
481     // returning empty list will force default behaviour for platform classpath calculation 
482     // javac will resolve against its own bootclasspath and use ct.sym file when available 
483     return Collections.emptyList();
484   }
485
486   private void submitAsyncTask(final CompileContext context, final Runnable taskRunnable) {
487     final TasksCounter counter = COUNTER_KEY.get(context);
488
489     assert counter != null;
490
491     counter.incTaskCount();
492     myTaskRunner.execute(new Runnable() {
493       public void run() {
494         try {
495           taskRunnable.run();
496         }
497         catch (Throwable e) {
498           context.processMessage(new CompilerMessage(BUILDER_NAME, e));
499         }
500         finally {
501           counter.decTaskCounter();
502         }
503       }
504     });
505   }
506
507   private static synchronized ExternalJavacServer ensureJavacServerStarted(@NotNull CompileContext context) throws Exception {
508     ExternalJavacServer server = ExternalJavacServer.KEY.get(context);
509     if (server != null) {
510       return server;
511     }
512     final int listenPort = findFreePort();
513     server = new ExternalJavacServer();
514     server.start(listenPort);
515     ExternalJavacServer.KEY.set(context, server);
516     return server;
517   }
518
519   private static int convertToNumber(String ver) {
520     if (ver == null) {
521       return 0;
522     }
523     final int quoteBegin = ver.indexOf("\"");
524     if (quoteBegin >= 0) {
525       final int quoteEnd = ver.indexOf("\"", quoteBegin + 1);
526       if (quoteEnd > quoteBegin) {
527         ver = ver.substring(quoteBegin + 1, quoteEnd);
528       }
529     }
530     if (ver.isEmpty()) {
531       return 0;
532     }
533
534     final String prefix = "1.";
535     final int parseBegin = ver.startsWith(prefix)? prefix.length() : 0;
536
537     final int parseEnd = ver.indexOf(".", parseBegin);
538     if (parseEnd > 0) {
539       ver = ver.substring(parseBegin, parseEnd);
540     }
541     else {
542       ver = ver.substring(parseBegin);
543     }
544
545     try {
546       return Integer.parseInt(ver);
547     }
548     catch (NumberFormatException ignored) {
549     }
550     return 0;
551   }
552
553   private static int findFreePort() {
554     try {
555       final ServerSocket serverSocket = new ServerSocket(0);
556       try {
557         return serverSocket.getLocalPort();
558       }
559       finally {
560         //workaround for linux : calling close() immediately after opening socket
561         //may result that socket is not closed
562         synchronized (serverSocket) {
563           try {
564             serverSocket.wait(1);
565           }
566           catch (Throwable ignored) {
567           }
568         }
569         serverSocket.close();
570       }
571     }
572     catch (IOException e) {
573       e.printStackTrace(System.err);
574       return ExternalJavacServer.DEFAULT_SERVER_PORT;
575     }
576   }
577
578   private static final Key<List<String>> JAVAC_OPTIONS = Key.create("_javac_options_");
579   private static final Key<List<String>> JAVAC_VM_OPTIONS = Key.create("_javac_vm_options_");
580   private static final Key<String> USER_DEFINED_BYTECODE_TARGET = Key.create("_user_defined_bytecode_target_");
581
582   private static List<String> getCompilationVMOptions(CompileContext context, JavaCompilingTool compilingTool) {
583     List<String> cached = JAVAC_VM_OPTIONS.get(context);
584     if (cached == null) {
585       loadCommonJavacOptions(context, compilingTool);
586       cached = JAVAC_VM_OPTIONS.get(context);
587     }
588     return cached;
589   }
590
591   private static List<String> getCompilationOptions(CompileContext context,
592                                                     ModuleChunk chunk,
593                                                     @Nullable ProcessorConfigProfile profile,
594                                                     @NotNull JavaCompilingTool compilingTool) {
595     List<String> cached = JAVAC_OPTIONS.get(context);
596     if (cached == null) {
597       loadCommonJavacOptions(context, compilingTool);
598       cached = JAVAC_OPTIONS.get(context);
599       assert cached != null : context;
600     }
601
602     List<String> options = new ArrayList<String>(cached);
603     addCompilationOptions(options, context, chunk, profile);
604     return options;
605   }
606
607   public static void addCompilationOptions(List<String> options, CompileContext context, ModuleChunk chunk, @Nullable ProcessorConfigProfile profile) {
608     if (!isEncodingSet(options)) {
609       final CompilerEncodingConfiguration config = context.getProjectDescriptor().getEncodingConfiguration();
610       final String encoding = config.getPreferredModuleChunkEncoding(chunk);
611       if (config.getAllModuleChunkEncodings(chunk).size() > 1) {
612         final StringBuilder msgBuilder = new StringBuilder();
613         msgBuilder.append("Multiple encodings set for module chunk ").append(chunk.getName());
614         if (encoding != null) {
615           msgBuilder.append("\n\"").append(encoding).append("\" will be used by compiler");
616         }
617         context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.INFO, msgBuilder.toString()));
618       }
619       if (!StringUtil.isEmpty(encoding)) {
620         options.add("-encoding");
621         options.add(encoding);
622       }
623     }
624
625     final String langLevel = getLanguageLevel(chunk.getModules().iterator().next());
626     if (!StringUtil.isEmpty(langLevel)) {
627       options.add("-source");
628       options.add(langLevel);
629     }
630
631     final JpsJavaCompilerConfiguration compilerConfiguration = JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(
632       context.getProjectDescriptor().getProject()
633     );
634
635     String bytecodeTarget = null;
636     for (JpsModule module : chunk.getModules()) {
637       final String moduleTarget = compilerConfiguration.getByteCodeTargetLevel(module.getName());
638       if (moduleTarget == null) {
639         continue;
640       }
641       if (bytecodeTarget == null) {
642         bytecodeTarget = moduleTarget;
643       }
644       else {
645         if (moduleTarget.compareTo(bytecodeTarget) < 0) {
646           bytecodeTarget = moduleTarget; // use the lower possible target among modules that form the chunk
647         }
648       }
649     }
650     
651     if (bytecodeTarget == null) {
652       // last resort and backward compatibility: 
653       // check if user explicitly defined bytecode target in additional compiler options
654       bytecodeTarget = USER_DEFINED_BYTECODE_TARGET.get(context);
655     }
656
657     final int compilerSdkVersion = getCompilerSdkVersion(context);
658     final int chunkSdkVersion = getChunkSdkVersion(chunk);
659     
660     if (bytecodeTarget != null) {
661       options.add("-target");
662       if (chunkSdkVersion > 0 && compilerSdkVersion > chunkSdkVersion) { 
663         // if compiler is newer than module JDK
664         final int userSpecifiedTargetVersion = convertToNumber(bytecodeTarget);
665         if (userSpecifiedTargetVersion > 0 && userSpecifiedTargetVersion <= compilerSdkVersion) {
666           // if user-specified bytecode version can be determined and is supported by compiler
667           if (userSpecifiedTargetVersion > chunkSdkVersion) {
668             // and user-specified bytecode target level is higher than the highest one supported by the target JDK,
669             // force compiler to use highest-available bytecode target version that is supported by the chunk JDK.
670             bytecodeTarget = "1." + chunkSdkVersion;
671           }
672         }
673         // otherwise let compiler display compilation error about incorrectly set bytecode target version
674       }
675       options.add(bytecodeTarget);
676     }
677     else {
678       if (chunkSdkVersion > 0 && compilerSdkVersion > chunkSdkVersion) {
679         // force lower bytecode target level to match the version of sdk assigned to this chunk
680         options.add("-target");
681         options.add("1." + chunkSdkVersion);
682       }
683     }
684
685     if (profile != null && profile.isEnabled()) {
686       // configuring annotation processing
687       if (!profile.isObtainProcessorsFromClasspath()) {
688         final String processorsPath = profile.getProcessorPath();
689         options.add("-processorpath");
690         options.add(processorsPath == null? "" : FileUtil.toSystemDependentName(processorsPath.trim()));
691       }
692
693       final Set<String> processors = profile.getProcessors();
694       if (!processors.isEmpty()) {
695         options.add("-processor");
696         options.add(StringUtil.join(processors, ","));
697       }
698
699       for (Map.Entry<String, String> optionEntry : profile.getProcessorOptions().entrySet()) {
700         options.add("-A" + optionEntry.getKey() + "=" + optionEntry.getValue());
701       }
702
703       final File srcOutput = ProjectPaths.getAnnotationProcessorGeneratedSourcesOutputDir(
704         chunk.getModules().iterator().next(), chunk.containsTests(), profile
705       );
706       if (srcOutput != null) {
707         srcOutput.mkdirs();
708         options.add("-s");
709         options.add(srcOutput.getPath());
710       }
711     }
712     else {
713       options.add("-proc:none");
714     }
715   }
716
717   private static String getLanguageLevel(JpsModule module) {
718     final LanguageLevel level = JpsJavaExtensionService.getInstance().getLanguageLevel(module);
719     if (level != null) {
720       switch (level) {
721         case JDK_1_3: return "1.3";
722         case JDK_1_4: return "1.4";
723         case JDK_1_5: return "1.5";
724         case JDK_1_6: return "1.6";
725         case JDK_1_7: return "1.7";
726         case JDK_1_8: return "8";
727         case JDK_1_9: return "9";
728       }
729     }
730     return null;
731   }
732
733   private static boolean isEncodingSet(List<String> options) {
734     for (String option : options) {
735       if ("-encoding".equals(option)) {
736         return true;
737       }
738     }
739     return false;
740   }
741
742   private static int getCompilerSdkVersion(CompileContext context) {
743     final Integer cached = JAVA_COMPILER_VERSION_KEY.get(context);
744     if (cached != null) {
745       return cached;
746     }
747     int javaVersion = convertToNumber(SystemProperties.getJavaVersion());
748     JAVA_COMPILER_VERSION_KEY.set(context, javaVersion);
749     return javaVersion;
750   }
751
752   private static int getChunkSdkVersion(ModuleChunk chunk) {
753     int chunkSdkVersion = -1;
754     for (JpsModule module : chunk.getModules()) {
755       final JpsSdk<JpsDummyElement> sdk = module.getSdk(JpsJavaSdkType.INSTANCE);
756       if (sdk != null) {
757         final int moduleSdkVersion = convertToNumber(sdk.getVersionString());
758         if (moduleSdkVersion != 0 /*could determine the version*/&& (chunkSdkVersion < 0 || chunkSdkVersion > moduleSdkVersion)) {
759           chunkSdkVersion = moduleSdkVersion;
760         }
761       }
762     }
763     return chunkSdkVersion;
764   }
765
766   private static String getChunkSdkHome(ModuleChunk chunk) {
767     for (JpsModule module : chunk.getModules()) {
768       final JpsSdk<JpsDummyElement> sdk = module.getSdk(JpsJavaSdkType.INSTANCE);
769       if (sdk != null) {
770         return sdk.getHomePath();
771       }
772     }
773     return null;
774   }
775
776   private static void loadCommonJavacOptions(@NotNull CompileContext context, @NotNull JavaCompilingTool compilingTool) {
777     final List<String> options = new ArrayList<String>();
778     final List<String> vmOptions = new ArrayList<String>();
779
780     final JpsProject project = context.getProjectDescriptor().getProject();
781     final JpsJavaCompilerConfiguration compilerConfig = JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(project);
782     final JpsJavaCompilerOptions compilerOptions = compilerConfig.getCurrentCompilerOptions();
783     if (compilerOptions.DEBUGGING_INFO) {
784       options.add("-g");
785     }
786     if (compilerOptions.DEPRECATION) {
787       options.add("-deprecation");
788     }
789     if (compilerOptions.GENERATE_NO_WARNINGS) {
790       options.add("-nowarn");
791     }
792     if (compilerOptions instanceof EclipseCompilerOptions) {
793       final EclipseCompilerOptions eclipseOptions = (EclipseCompilerOptions)compilerOptions;
794       if (eclipseOptions.PROCEED_ON_ERROR) {
795         options.add("-proceedOnError");
796       }
797     }
798     final String customArgs = compilerOptions.ADDITIONAL_OPTIONS_STRING;
799     if (customArgs != null) {
800       final StringTokenizer customOptsTokenizer = new StringTokenizer(customArgs, " \t\r\n");
801       boolean skip = false;
802       boolean targetOptionFound = false;
803       while (customOptsTokenizer.hasMoreTokens()) {
804         final String userOption = customOptsTokenizer.nextToken();
805         if (FILTERED_OPTIONS.contains(userOption)) {
806           skip = true;
807           targetOptionFound = "-target".equals(userOption);
808           continue;
809         }
810         if (skip) {
811           skip = false;
812           if (targetOptionFound) {
813             targetOptionFound = false;
814             USER_DEFINED_BYTECODE_TARGET.set(context, userOption);
815           }
816         }
817         else {
818           if (!FILTERED_SINGLE_OPTIONS.contains(userOption)) {
819             if (userOption.startsWith("-J-")) {
820               vmOptions.add(userOption.substring("-J".length()));
821             }
822             else {
823               options.add(userOption);
824             }
825           }
826         }
827       }
828     }
829
830     compilingTool.processCompilerOptions(context, options);
831
832     JAVAC_OPTIONS.set(context, options);
833     JAVAC_VM_OPTIONS.set(context, vmOptions);
834   }
835
836   @Override
837   public void chunkBuildFinished(CompileContext context, ModuleChunk chunk) {
838     JavaBuilderUtil.cleanupChunkResources(context);
839   }
840
841   private static Map<File, Set<File>> buildOutputDirectoriesMap(CompileContext context, ModuleChunk chunk) {
842     final Map<File, Set<File>> map = new THashMap<File, Set<File>>(FileUtil.FILE_HASHING_STRATEGY);
843     for (ModuleBuildTarget target : chunk.getTargets()) {
844       final File outputDir = target.getOutputDir();
845       if (outputDir == null) {
846         continue;
847       }
848       final Set<File> roots = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
849       for (JavaSourceRootDescriptor descriptor : context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context)) {
850         roots.add(descriptor.root);
851       }
852       map.put(outputDir, roots);
853     }
854     return map;
855   }
856
857   private static class DiagnosticSink implements DiagnosticOutputConsumer {
858     private final CompileContext myContext;
859     private volatile int myErrorCount = 0;
860     private volatile int myWarningCount = 0;
861     private final Set<File> myFilesWithErrors = new HashSet<File>();
862
863     public DiagnosticSink(CompileContext context) {
864       myContext = context;
865     }
866
867     @Override
868     public void javaFileLoaded(File file) {
869     }
870
871     public void registerImports(final String className, final Collection<String> imports, final Collection<String> staticImports) {
872       //submitAsyncTask(myContext, new Runnable() {
873       //  public void run() {
874       //    final Callbacks.Backend callback = DELTA_MAPPINGS_CALLBACK_KEY.get(myContext);
875       //    if (callback != null) {
876       //      callback.registerImports(className, imports, staticImports);
877       //    }
878       //  }
879       //});
880     }
881
882     public void outputLineAvailable(String line) {
883       if (!StringUtil.isEmpty(line)) {
884         if (line.contains("java.lang.OutOfMemoryError")) {
885           myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "OutOfMemoryError: insufficient memory"));
886           myErrorCount++;
887         }
888         else {
889           final BuildMessage.Kind kind = getKindByMessageText(line);
890           if (kind == BuildMessage.Kind.ERROR) {
891             myErrorCount++;
892           }
893           else if (kind == BuildMessage.Kind.WARNING) {
894             myWarningCount++;
895           }
896           myContext.processMessage(new CompilerMessage(BUILDER_NAME, kind, line));
897         }
898       }
899     }
900
901     private static BuildMessage.Kind getKindByMessageText(String line) {
902       final String lowercasedLine = line.toLowerCase(Locale.US);
903       if (lowercasedLine.contains("error") || lowercasedLine.contains("requires target release")) {
904         return BuildMessage.Kind.ERROR;
905       }
906       return BuildMessage.Kind.INFO;
907     }
908
909     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
910       final CompilerMessage.Kind kind;
911       switch (diagnostic.getKind()) {
912         case ERROR:
913           kind = BuildMessage.Kind.ERROR;
914           myErrorCount++;
915           break;
916         case MANDATORY_WARNING:
917         case WARNING:
918           kind = BuildMessage.Kind.WARNING;
919           myWarningCount++;
920           break;
921         case NOTE:
922         default:
923           kind = BuildMessage.Kind.INFO;
924       }
925       File sourceFile = null;
926       try {
927         // for eclipse compiler just an attempt to call getSource() may lead to an NPE,
928         // so calling this method under try/catch to avoid induced compiler errors
929         final JavaFileObject source = diagnostic.getSource();
930         sourceFile = source != null ? Utils.convertToFile(source.toUri()) : null;
931       }
932       catch (Exception e) {
933         LOG.info(e);
934       }
935       final String srcPath;
936       if (sourceFile != null) {
937         myFilesWithErrors.add(sourceFile);
938         srcPath = FileUtil.toSystemIndependentName(sourceFile.getPath());
939       }
940       else {
941         srcPath = null;
942       }
943       String message = diagnostic.getMessage(Locale.US);
944       if (Utils.IS_TEST_MODE) {
945         LOG.info(message);
946       }
947       myContext.processMessage(new CompilerMessage(
948         BUILDER_NAME, kind, message, srcPath, diagnostic.getStartPosition(),
949         diagnostic.getEndPosition(), diagnostic.getPosition(), diagnostic.getLineNumber(),
950         diagnostic.getColumnNumber()
951       ));
952     }
953
954     public int getErrorCount() {
955       return myErrorCount;
956     }
957
958     public int getWarningCount() {
959       return myWarningCount;
960     }
961
962     public Collection<File> getFilesWithErrors() {
963       return myFilesWithErrors;
964     }
965   }
966
967   private class ClassProcessingConsumer implements OutputFileConsumer {
968     private final CompileContext myContext;
969     private final OutputFileConsumer myDelegateOutputFileSink;
970
971     public ClassProcessingConsumer(CompileContext context, OutputFileConsumer sink) {
972       myContext = context;
973       myDelegateOutputFileSink = sink != null ? sink : new OutputFileConsumer() {
974         public void save(@NotNull OutputFileObject fileObject) {
975           throw new RuntimeException("Output sink for compiler was not specified");
976         }
977       };
978     }
979
980     public void save(@NotNull final OutputFileObject fileObject) {
981       // generated files must be saved synchronously, because some compilers (e.g. eclipse)
982       // may want to read them for further compilation
983       try {
984         final BinaryContent content = fileObject.getContent();
985         final File file = fileObject.getFile();
986         if (content != null) {
987           content.saveToFile(file);
988         }
989         else {
990           myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING, "Missing content for file " + file.getPath()));
991         }
992       }
993       catch (IOException e) {
994         myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
995       }
996
997       submitAsyncTask(myContext, new Runnable() {
998         public void run() {
999           try {
1000             for (ClassPostProcessor processor : ourClassProcessors) {
1001               processor.process(myContext, fileObject);
1002             }
1003           }
1004           finally {
1005             myDelegateOutputFileSink.save(fileObject);
1006           }
1007         }
1008       });
1009     }
1010   }
1011
1012
1013   private static final Key<TasksCounter> COUNTER_KEY = Key.create("_async_task_counter_");
1014
1015   private static final class TasksCounter {
1016     private int myCounter = 0;
1017
1018     public synchronized void incTaskCount() {
1019       myCounter++;
1020     }
1021
1022     public synchronized void decTaskCounter() {
1023       myCounter = Math.max(0, myCounter - 1);
1024       if (myCounter == 0) {
1025         notifyAll();
1026       }
1027     }
1028
1029     public synchronized void await() {
1030       while (myCounter > 0) {
1031         try {
1032           wait();
1033         }
1034         catch (InterruptedException e) {
1035         }
1036       }
1037     }
1038   }
1039 }