2 * Copyright 2000-2016 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.jps.incremental.java;
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;
68 import java.net.ServerSocket;
70 import java.util.concurrent.Executor;
71 import java.util.concurrent.Future;
72 import java.util.concurrent.atomic.AtomicReference;
75 * @author Eugene Zhuravlev
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_");
87 private static final Set<String> FILTERED_OPTIONS = new HashSet<String>(Collections.singletonList(
90 private static final Set<String> FILTERED_SINGLE_OPTIONS = new HashSet<String>(Arrays.asList(
91 "-g", "-deprecation", "-nowarn", "-verbose", "-proc:none", "-proc:only", "-proceedOnError"
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";
97 private final Executor myTaskRunner;
98 private static final List<ClassPostProcessor> ourClassProcessors = new ArrayList<ClassPostProcessor>();
99 private static final Set<JpsModuleType<?>> ourCompilableModuleTypes;
101 private static final File ourDefaultRtJar;
103 ourCompilableModuleTypes = new HashSet<JpsModuleType<?>>();
104 for (JavaBuilderExtension extension : JpsServiceManager.getInstance().getExtensions(JavaBuilderExtension.class)) {
105 ourCompilableModuleTypes.addAll(extension.getCompilableModuleTypes());
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);
116 ourDefaultRtJar = rtJar;
119 private static boolean isRtJarPath(String path) {
120 if (StringUtil.endsWithIgnoreCase(path, RT_JAR_PATH_SUFFIX)) {
123 return RT_JAR_PATH_SUFFIX.charAt(0) != '/' && StringUtil.endsWithIgnoreCase(path, "/rt.jar");
126 public static void registerClassPostProcessor(ClassPostProcessor processor) {
127 ourClassProcessors.add(processor);
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
138 public String getPresentableName() {
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);
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));
157 public List<String> getCompilableFileExtensions() {
158 return Collections.singletonList(JAVA_EXTENSION);
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;
170 return doBuild(context, chunk, dirtyFilesHolder, outputConsumer, compilingTool);
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 {
179 final Set<File> filesToCompile = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
181 dirtyFilesHolder.processDirtyFiles(new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
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);
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));
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:");
208 return compile(context, chunk, dirtyFilesHolder, filesToCompile, outputConsumer, compilingTool);
210 catch (BuildDataCorruptedException e) {
213 catch (ProjectBuildException e) {
216 catch (PersistentEnumeratorBase.CorruptedException e) {
219 catch (Exception e) {
221 String message = e.getMessage();
222 if (message == null) {
223 final ByteArrayOutputStream out = new ByteArrayOutputStream();
224 final PrintStream stream = new PrintStream(out);
226 e.printStackTrace(stream);
231 message = "Internal error: \n" + out;
233 context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, message));
234 throw new StopBuildException();
238 private ExitCode compile(CompileContext context,
240 DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
241 Collection<File> files,
242 OutputConsumer outputConsumer,
243 JavaCompilingTool compilingTool)
245 ExitCode exitCode = ExitCode.NOTHING_DONE;
247 final boolean hasSourcesToCompile = !files.isEmpty();
249 if (!hasSourcesToCompile && !dirtyFilesHolder.hasRemovedFiles()) {
253 final ProjectDescriptor pd = context.getProjectDescriptor();
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()*/);
259 // begin compilation round
260 final OutputFilesSink outputSink = new OutputFilesSink(context, outputConsumer, JavaBuilderUtil.getDependenciesRegistrar(context), chunk.getPresentableShortName());
261 Collection<File> filesWithErrors = null;
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));
270 exitCode = ExitCode.OK;
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);
279 final DiagnosticSink diagnosticSink = new DiagnosticSink(context);
281 final String chunkName = chunk.getName();
282 context.processMessage(new ProgressMessage("Parsing java... [" + chunk.getPresentableShortName() + "]"));
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());
292 LOG.debug(" classpath for " + chunkName + ":");
293 for (File file : classpath) {
294 LOG.debug(" " + file.getAbsolutePath());
296 LOG.debug(" platform classpath for " + chunkName + ":");
297 for (File file : platformCp) {
298 LOG.debug(" " + file.getAbsolutePath());
302 compiledOk = compileJava(context, chunk, files, classpath, platformCp, srcPath, diagnosticSink, outputSink, compilingTool);
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);
315 context.checkCanceled();
317 if (!compiledOk && diagnosticSink.getErrorCount() == 0) {
318 diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, "Compilation failed: internal java compiler error"));
320 if (!Utils.PROCEED_ON_ERROR_KEY.get(context, Boolean.FALSE) && diagnosticSink.getErrorCount() > 0) {
322 diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.OTHER, "Errors occurred while compiling module '" + chunkName + "'"));
324 throw new StopBuildException(
325 "Compilation failed: errors: " + diagnosticSink.getErrorCount() + "; warnings: " + diagnosticSink.getWarningCount()
331 JavaBuilderUtil.registerFilesToCompile(context, files);
332 if (filesWithErrors != null) {
333 JavaBuilderUtil.registerFilesWithErrors(context, filesWithErrors);
335 JavaBuilderUtil.registerSuccessfullyCompiled(context, outputSink.getSuccessfullyCompiled());
341 private boolean compileJava(
342 final CompileContext context,
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 {
351 final TasksCounter counter = new TasksCounter();
352 COUNTER_KEY.set(context, counter);
354 final JpsJavaExtensionService javaExt = JpsJavaExtensionService.getInstance();
355 final JpsJavaCompilerConfiguration compilerConfig = javaExt.getCompilerConfiguration(context.getProjectDescriptor().getProject());
356 assert compilerConfig != null;
358 final Set<JpsModule> modules = chunk.getModules();
359 ProcessorConfigProfile profile = null;
360 if (modules.size() == 1) {
361 profile = compilerConfig.getAnnotationProcessingProfile(modules.iterator().next());
364 String message = validateCycle(chunk, javaExt, compilerConfig, modules);
365 if (message != null) {
366 diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, message));
371 final Map<File, Set<File>> outs = buildOutputDirectoriesMap(context, chunk);
373 final int targetLanguageLevel = JpsJavaSdkType.parseVersion(getLanguageLevel(chunk.getModules().iterator().next()));
374 final boolean shouldForkJavac = shouldForkCompilerProcess(context, targetLanguageLevel);
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."));
386 final int compilerSdkVersion = forkSdk == null? getCompilerSdkVersion(context) : forkSdk.getSecond();
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, " ") + "\"");
393 Collection<File> _platformCp = calcEffectivePlatformCp(platformCp, options, compilingTool);
394 if (_platformCp == null) {
395 context.processMessage(
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"
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);
415 _platformCp = Collections.emptyList();
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/")) {
426 joined.addAll(classpath);
428 _platformCp = Collections.emptyList();
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();
442 final ClassProcessingConsumer classesConsumer = new ClassProcessingConsumer(context, outputSink);
444 if (!shouldForkJavac) {
445 rc = JavacMain.compile(
446 options, files, classpath, _platformCp, modulePath, sourcePath, outs, diagnosticSink, classesConsumer, context.getCancelStatus(), compilingTool
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(
455 getExternalJavacHeapSize(context),
456 vmOptions, options, _platformCp, classpath, modulePath, sourcePath,
457 files, outs, diagnosticSink, classesConsumer, compilingTool, context.getCancelStatus()
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;
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);
481 pair = Pair.create(module.getName(), moduleLevel); // first value
484 if (!Comparing.equal(pair.getSecond(), moduleLevel)) {
489 " must have the same language level because of cyclic dependencies between them";
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 [" +
500 "] are excluded from annotation processing";
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
513 // chunkSdkVersion >= 9, so we have no rt.jar anymore and '-release' is the only cross-compilation option available
519 private static boolean shouldForkCompilerProcess(CompileContext context, int chunkLanguageLevel) {
520 if (!isJavac(COMPILING_TOOL.get(context))) {
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
529 // compilerSdkVersion is 9+ here, so applying JEP 182 "Retiring javac 'one plus three back'" policy
530 return Math.abs(compilerSdkVersion - chunkLanguageLevel) > 3;
533 private static boolean isJavac(final JavaCompilingTool compilingTool) {
534 return compilingTool != null && (compilingTool.getId() == JavaCompilers.JAVAC_ID || compilingTool.getId() == JavaCompilers.JAVAC_API_ID);
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
540 private static Collection<File> calcEffectivePlatformCp(Collection<File> platformCp, List<String> options, JavaCompilingTool compilingTool) {
541 if (ourDefaultRtJar == null || !isJavac(compilingTool)) {
544 boolean profileFeatureRequested = false;
545 for (String option : options) {
546 if ("-profile".equalsIgnoreCase(option)) {
547 profileFeatureRequested = true;
551 if (!profileFeatureRequested) {
554 boolean isTargetPlatformSameAsBuildRuntime = false;
555 for (File file : platformCp) {
556 if (FileUtil.filesEqual(file, ourDefaultRtJar)) {
557 isTargetPlatformSameAsBuildRuntime = true;
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
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();
571 private void submitAsyncTask(final CompileContext context, final Runnable taskRunnable) {
572 final TasksCounter counter = COUNTER_KEY.get(context);
574 assert counter != null;
576 counter.incTaskCount();
577 myTaskRunner.execute(new Runnable() {
583 catch (Throwable e) {
584 context.processMessage(new CompilerMessage(BUILDER_NAME, e));
587 counter.decTaskCounter();
593 private static synchronized ExternalJavacManager ensureJavacServerStarted(@NotNull CompileContext context) throws Exception {
594 ExternalJavacManager server = ExternalJavacManager.KEY.get(context);
595 if (server != null) {
598 final int listenPort = findFreePort();
599 server = new ExternalJavacManager(Utils.getSystemRoot()) {
601 protected ExternalJavacProcessHandler createProcessHandler(@NotNull Process process, @NotNull String commandLine) {
602 return new ExternalJavacProcessHandler(process, commandLine) {
605 protected Future<?> executeOnPooledThread(@NotNull Runnable task) {
606 return SharedThreadPool.getInstance().executeOnPooledThread(task);
611 server.start(listenPort);
612 ExternalJavacManager.KEY.set(context, server);
616 private static int findFreePort() {
618 final ServerSocket serverSocket = new ServerSocket(0);
620 return serverSocket.getLocalPort();
623 //workaround for linux : calling close() immediately after opening socket
624 //may result that socket is not closed
625 synchronized (serverSocket) {
627 serverSocket.wait(1);
629 catch (Throwable ignored) {
632 serverSocket.close();
635 catch (IOException e) {
636 e.printStackTrace(System.err);
637 return ExternalJavacManager.DEFAULT_SERVER_PORT;
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_");
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);
654 private static List<String> getCompilationOptions(
655 final int compilerSdkVersion, CompileContext context, ModuleChunk chunk, @Nullable ProcessorConfigProfile profile, @NotNull JavaCompilingTool compilingTool) {
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;
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));
677 options.addAll(cached);
679 addCompilationOptions(compilerSdkVersion, options, context, chunk, profile);
683 public static void addCompilationOptions(List<String> options, CompileContext context, ModuleChunk chunk, @Nullable ProcessorConfigProfile profile) {
684 addCompilationOptions(getCompilerSdkVersion(context), options, context, chunk, profile);
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");
697 context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.INFO, msgBuilder.toString()));
699 if (!StringUtil.isEmpty(encoding)) {
700 options.add("-encoding");
701 options.add(encoding);
705 addCrossCompilationOptions(compilerSdkVersion, options, context, chunk);
707 if (addAnnotationProcessingOptions(options, profile)) {
708 final File srcOutput = ProjectPaths.getAnnotationProcessorGeneratedSourcesOutputDir(
709 chunk.getModules().iterator().next(), chunk.containsTests(), profile
711 if (srcOutput != null) {
714 options.add(srcOutput.getPath());
722 * @return true if annotation processing is enabled and corresponding options were added, false if profile is null or disabled
724 public static boolean addAnnotationProcessingOptions(List<String> options, @Nullable AnnotationProcessingConfiguration profile) {
725 if (profile == null || !profile.isEnabled()) {
726 options.add("-proc:none");
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()));
737 final Set<String> processors = profile.getProcessors();
738 if (!processors.isEmpty()) {
739 options.add("-processor");
740 options.add(StringUtil.join(processors, ","));
743 for (Map.Entry<String, String> optionEntry : profile.getProcessorOptions().entrySet()) {
744 options.add("-A" + optionEntry.getKey() + "=" + optionEntry.getValue());
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()
755 final String langLevel = getLanguageLevel(chunk.getModules().iterator().next());
756 final int chunkSdkVersion = getChunkSdkVersion(chunk);
758 final int targetLanguageLevel = JpsJavaSdkType.parseVersion(langLevel);
759 if (shouldUseReleaseOption(context, compilerSdkVersion, chunkSdkVersion, targetLanguageLevel)) {
760 options.add(getReleaseOptionName());
761 options.add(String.valueOf(targetLanguageLevel));
764 // using older -source, -target and -bootclasspath options
765 if (!StringUtil.isEmpty(langLevel)) {
766 options.add("-source");
767 options.add(langLevel);
770 String bytecodeTarget = null;
771 for (JpsModule module : chunk.getModules()) {
772 final String moduleTarget = compilerConfiguration.getByteCodeTargetLevel(module.getName());
773 if (moduleTarget == null) {
776 if (bytecodeTarget == null) {
777 bytecodeTarget = moduleTarget;
780 if (moduleTarget.compareTo(bytecodeTarget) < 0) {
781 bytecodeTarget = moduleTarget; // use the lower possible target among modules that form the chunk
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;
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);
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;
811 // otherwise let compiler display compilation error about incorrectly set bytecode target version
813 options.add(bytecodeTarget);
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);
824 // todo: after java9 release the method can be deleted
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("+");
832 final int minorVersion = Integer.parseInt(versionString.substring(start + 1));
833 if (minorVersion < 135) {
837 catch (Throwable ignored) {
844 private static String getLanguageLevel(JpsModule module) {
845 final LanguageLevel level = JpsJavaExtensionService.getInstance().getLanguageLevel(module);
846 return level != null ? level.getComplianceOption() : null;
849 private static boolean isEncodingSet(List<String> options) {
850 for (String option : options) {
851 if ("-encoding".equals(option)) {
858 private static int getCompilerSdkVersion(CompileContext context) {
859 final Integer cached = JAVA_COMPILER_VERSION_KEY.get(context);
860 if (cached != null) {
863 int javaVersion = JpsJavaSdkType.parseVersion(SystemProperties.getJavaVersion());
864 JAVA_COMPILER_VERSION_KEY.set(context, javaVersion);
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);
873 final int moduleSdkVersion = JpsJavaSdkType.parseVersion(sdk.getVersionString());
874 if (moduleSdkVersion != 0 /*could determine the version*/&& (chunkSdkVersion < 0 || chunkSdkVersion > moduleSdkVersion)) {
875 chunkSdkVersion = moduleSdkVersion;
879 return chunkSdkVersion;
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);
887 final int version = JpsJavaSdkType.parseVersion(sdk.getVersionString());
889 if (version >= 9 && Math.abs(version - targetLanguageLevel) > 3) {
890 continue; // current javac compiler does not support required language level
892 return Pair.create(sdk.getHomePath(), version);
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)");
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)");
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.");
911 return Pair.create(fallbackJdkHome, fallbackVersion);
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>();
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) {
924 if (compilerOptions.DEPRECATION) {
925 options.add("-deprecation");
927 if (compilerOptions.GENERATE_NO_WARNINGS) {
928 options.add("-nowarn");
930 if (compilerOptions instanceof EclipseCompilerOptions) {
931 final EclipseCompilerOptions eclipseOptions = (EclipseCompilerOptions)compilerOptions;
932 if (eclipseOptions.PROCEED_ON_ERROR) {
933 options.add("-proceedOnError");
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)) {
945 targetOptionFound = "-target".equals(userOption);
950 if (targetOptionFound) {
951 targetOptionFound = false;
952 USER_DEFINED_BYTECODE_TARGET.set(context, userOption);
956 if (!FILTERED_SINGLE_OPTIONS.contains(userOption)) {
957 if (userOption.startsWith("-J-")) {
958 vmOptions.add(userOption.substring("-J".length()));
961 options.add(userOption);
968 compilingTool.processCompilerOptions(context, options);
970 JAVAC_OPTIONS.set(context, options);
971 JAVAC_VM_OPTIONS.set(context, vmOptions);
975 public void chunkBuildFinished(CompileContext context, ModuleChunk chunk) {
976 JavaBuilderUtil.cleanupChunkResources(context);
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) {
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);
990 map.put(outputDir, roots);
995 private static JavaModuleIndex getJavaModuleIndex(CompileContext context) {
996 File storageRoot = context.getProjectDescriptor().dataManager.getDataPaths().getDataStorageRoot();
997 return JpsJavaExtensionService.getInstance().getJavaModuleIndex(storageRoot);
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);
1006 private DiagnosticSink(CompileContext context) {
1007 myContext = context;
1011 public void javaFileLoaded(File file) {
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);
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);
1033 else if (line.startsWith(ExternalJavacManager.STDERR_LINE_PREFIX)) {
1034 //noinspection UseOfSystemOutOrSystemErr
1035 System.err.println(line);
1037 else if (line.contains("java.lang.OutOfMemoryError")) {
1038 myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "OutOfMemoryError: insufficient memory"));
1042 final BuildMessage.Kind kind = getKindByMessageText(line);
1043 if (kind == BuildMessage.Kind.ERROR) {
1046 else if (kind == BuildMessage.Kind.WARNING) {
1049 myContext.processMessage(new CompilerMessage(BUILDER_NAME, kind, line));
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;
1059 return BuildMessage.Kind.INFO;
1063 public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
1064 final CompilerMessage.Kind kind;
1065 switch (diagnostic.getKind()) {
1067 kind = BuildMessage.Kind.ERROR;
1070 case MANDATORY_WARNING:
1072 kind = BuildMessage.Kind.WARNING;
1077 kind = BuildMessage.Kind.INFO;
1079 File sourceFile = null;
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;
1086 catch (Exception e) {
1089 final String srcPath;
1090 if (sourceFile != null) {
1091 if (kind == BuildMessage.Kind.ERROR) {
1092 myFilesWithErrors.add(sourceFile);
1094 srcPath = FileUtil.toSystemIndependentName(sourceFile.getPath());
1099 String message = diagnostic.getMessage(Locale.US);
1100 if (Utils.IS_TEST_MODE) {
1103 final CompilerMessage compilerMsg = new CompilerMessage(
1104 BUILDER_NAME, kind, message, srcPath, diagnostic.getStartPosition(),
1105 diagnostic.getEndPosition(), diagnostic.getPosition(), diagnostic.getLineNumber(),
1106 diagnostic.getColumnNumber()
1108 if (LOG.isDebugEnabled()) {
1109 LOG.debug(compilerMsg.toString());
1111 myContext.processMessage(compilerMsg);
1114 public int getErrorCount() {
1115 return myErrorCount;
1118 public int getWarningCount() {
1119 return myWarningCount;
1123 public Collection<File> getFilesWithErrors() {
1124 return myFilesWithErrors;
1128 private class ClassProcessingConsumer implements OutputFileConsumer {
1129 private final CompileContext myContext;
1130 private final OutputFileConsumer myDelegateOutputFileSink;
1132 private ClassProcessingConsumer(CompileContext context, OutputFileConsumer sink) {
1133 myContext = context;
1134 myDelegateOutputFileSink = sink != null ? sink : new OutputFileConsumer() {
1136 public void save(@NotNull OutputFileObject fileObject) {
1137 throw new RuntimeException("Output sink for compiler was not specified");
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
1147 final BinaryContent content = fileObject.getContent();
1148 final File file = fileObject.getFile();
1149 if (content != null) {
1150 content.saveToFile(file);
1153 myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING, "Missing content for file " + file.getPath()));
1156 catch (IOException e) {
1157 myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
1160 submitAsyncTask(myContext, new Runnable() {
1164 for (ClassPostProcessor processor : ourClassProcessors) {
1165 processor.process(myContext, fileObject);
1169 myDelegateOutputFileSink.save(fileObject);
1177 private static final Key<TasksCounter> COUNTER_KEY = Key.create("_async_task_counter_");
1179 private static final class TasksCounter {
1180 private int myCounter;
1182 private synchronized void incTaskCount() {
1186 private synchronized void decTaskCounter() {
1187 myCounter = Math.max(0, myCounter - 1);
1188 if (myCounter == 0) {
1193 public synchronized void await() {
1194 while (myCounter > 0) {
1198 catch (InterruptedException ignored) {