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