Groovy-Eclipse compiler support back-end (IDEA-52379)
[idea/community.git] / plugins / groovy / jps-plugin / src / org / jetbrains / jps / incremental / groovy / GreclipseBuilder.java
1 /*
2  * Copyright 2000-2014 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.groovy;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Key;
20 import com.intellij.openapi.util.io.FileUtil;
21 import com.intellij.openapi.util.text.StringUtil;
22 import com.intellij.util.ArrayUtil;
23 import com.intellij.util.containers.ContainerUtil;
24 import org.jetbrains.annotations.NotNull;
25 import org.jetbrains.jps.ModuleChunk;
26 import org.jetbrains.jps.ProjectPaths;
27 import org.jetbrains.jps.builders.DirtyFilesHolder;
28 import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
29 import org.jetbrains.jps.incremental.*;
30 import org.jetbrains.jps.incremental.java.JavaBuilder;
31 import org.jetbrains.jps.incremental.messages.BuildMessage;
32 import org.jetbrains.jps.incremental.messages.CompilerMessage;
33 import org.jetbrains.jps.incremental.messages.ProgressMessage;
34 import org.jetbrains.jps.model.java.JpsJavaExtensionService;
35 import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerConfiguration;
36 import org.jetbrains.jps.model.java.compiler.ProcessorConfigProfile;
37 import org.jetbrains.jps.model.module.JpsModule;
38
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.PrintWriter;
42 import java.io.StringWriter;
43 import java.lang.reflect.Constructor;
44 import java.lang.reflect.Method;
45 import java.util.*;
46
47 /**
48  * @author peter
49  */
50 public class GreclipseBuilder extends ModuleLevelBuilder {
51   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.groovy.GreclipseBuilder");
52   private static final Key<Boolean> COMPILER_VERSION_INFO = Key.create("_greclipse_compiler_info_");
53   private final ClassLoader myGreclipseLoader;
54
55   protected GreclipseBuilder(@NotNull ClassLoader greclipseLoader) {
56     super(BuilderCategory.TRANSLATOR);
57     myGreclipseLoader = greclipseLoader;
58   }
59
60   @Override
61   public List<String> getCompilableFileExtensions() {
62     return Arrays.asList("groovy", "java");
63   }
64
65   @Override
66   public void buildStarted(CompileContext context) {
67     JavaBuilder.IS_ENABLED.set(context, Boolean.FALSE);
68   }
69
70   @Override
71   public ExitCode build(final CompileContext context,
72                         ModuleChunk chunk,
73                         DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
74                         OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
75     try {
76       final List<File> toCompile = GroovyBuilder.collectChangedFiles(context, dirtyFilesHolder, false, true);
77       if (toCompile.isEmpty()) {
78         return ExitCode.NOTHING_DONE;
79       }
80
81       Map<ModuleBuildTarget, String> outputDirs = GroovyBuilder.getCanonicalModuleOutputs(context, chunk, this);
82       if (outputDirs == null) {
83         return ExitCode.ABORT;
84       }
85
86       final JpsJavaExtensionService javaExt = JpsJavaExtensionService.getInstance();
87       final JpsJavaCompilerConfiguration compilerConfig = javaExt.getCompilerConfiguration(context.getProjectDescriptor().getProject());
88       assert compilerConfig != null;
89
90       final Set<JpsModule> modules = chunk.getModules();
91       ProcessorConfigProfile profile = null;
92       if (modules.size() == 1) {
93         profile = compilerConfig.getAnnotationProcessingProfile(modules.iterator().next());
94       }
95       else {
96         String message = JavaBuilder.validateCycle(chunk, javaExt, compilerConfig, modules);
97         if (message != null) {
98           context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, message));
99           return ExitCode.ABORT;
100         }
101       }
102
103
104       String mainOutputDir = outputDirs.get(chunk.representativeTarget());
105       final List<String> args = createCommandLine(context, chunk, toCompile, mainOutputDir, profile);
106
107       if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
108         LOG.debug("Compiling with args: " + args);
109       }
110
111       Boolean notified = COMPILER_VERSION_INFO.get(context);
112       if (notified != Boolean.TRUE) {
113         context.processMessage(new CompilerMessage("", BuildMessage.Kind.INFO, "Using Groovy-Eclipse to compile Java & Groovy sources"));
114         COMPILER_VERSION_INFO.set(context, Boolean.TRUE);
115       }
116
117       context.processMessage(new ProgressMessage("Compiling java & groovy [" + chunk.getPresentableShortName() + "]"));
118
119       StringWriter out = new StringWriter();
120       StringWriter err = new StringWriter();
121       HashMap<String, List<String>> outputMap = ContainerUtil.newHashMap();
122
123       boolean success = performCompilation(args, out, err, outputMap, context, chunk);
124       
125       List<GroovycOSProcessHandler.OutputItem> items = ContainerUtil.newArrayList();
126       for (String src : outputMap.keySet()) {
127         //noinspection ConstantConditions
128         for (String classFile : outputMap.get(src)) {
129           items.add(new GroovycOSProcessHandler.OutputItem(FileUtil.toSystemIndependentName(mainOutputDir + classFile),
130                                                            FileUtil.toSystemIndependentName(src)));
131         }
132       }
133       Map<ModuleBuildTarget, Collection<GroovycOSProcessHandler.OutputItem>> successfullyCompiled =
134         GroovyBuilder.processCompiledFiles(context, chunk, outputDirs, mainOutputDir, items);
135
136       EclipseOutputParser parser = new EclipseOutputParser(getPresentableName(), chunk);
137       List<CompilerMessage> messages = ContainerUtil.concat(parser.parseMessages(out.toString()), parser.parseMessages(err.toString()));
138       boolean hasError = false;
139       for (CompilerMessage message : messages) {
140         if (message.getKind() == BuildMessage.Kind.ERROR) {
141           hasError = true;
142         }
143         context.processMessage(message);
144       }
145
146       if (!success && !hasError) {
147         context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, "Compilation failed"));
148       }
149
150       if (GroovyBuilder.updateDependencies(context, chunk, dirtyFilesHolder, toCompile, successfullyCompiled, outputConsumer, this)) {
151         return ExitCode.ADDITIONAL_PASS_REQUIRED;
152       }
153       return ExitCode.OK;
154
155     }
156     catch (Exception e) {
157       throw new ProjectBuildException(e);
158     }
159   }
160
161   private boolean performCompilation(List<String> args, StringWriter out, StringWriter err, Map<String, List<String>> outputs, CompileContext context, ModuleChunk chunk) {
162     try {
163       Class<?> mainClass = Class.forName(GreclipseMain.class.getName(), true, myGreclipseLoader);
164       Constructor<?> constructor = mainClass.getConstructor(PrintWriter.class, PrintWriter.class, Map.class, Map.class);
165       Method compileMethod = mainClass.getMethod("compile", String[].class);
166
167       HashMap<String, Object> customDefaultOptions = ContainerUtil.newHashMap();
168       // without this greclipse won't load AST transformations
169       customDefaultOptions.put("org.eclipse.jdt.core.compiler.groovy.groovyClassLoaderPath", getClasspathString(chunk));
170
171       // used by greclipse to cache transform loaders
172       // names should be different for production & tests
173       customDefaultOptions.put("org.eclipse.jdt.core.compiler.groovy.groovyProjectName", chunk.getPresentableShortName());
174
175       Object main = constructor.newInstance(new PrintWriter(out), new PrintWriter(err), customDefaultOptions, outputs);
176       return (Boolean)compileMethod.invoke(main, new Object[]{ArrayUtil.toStringArray(args)});
177     }
178     catch (Exception e) {
179       context.processMessage(new CompilerMessage(getPresentableName(), e));
180       return false;
181     }
182   }
183
184   private static List<String> createCommandLine(CompileContext context,
185                                                 ModuleChunk chunk,
186                                                 List<File> srcFiles,
187                                                 String mainOutputDir, ProcessorConfigProfile profile) {
188     final List<String> args = new ArrayList<String>();
189
190     args.add("-cp");
191     args.add(getClasspathString(chunk));
192
193     JavaBuilder.addCompilationOptions(args, context, chunk, profile);
194
195     args.add("-d");
196     args.add(mainOutputDir);
197
198     for (File file : srcFiles) {
199       args.add(file.getPath());
200     }
201
202     return args;
203   }
204
205   private static String getClasspathString(ModuleChunk chunk) {
206     final Set<String> cp = new LinkedHashSet<String>();
207     for (File file : ProjectPaths.getCompilationClasspathFiles(chunk, chunk.containsTests(), false, false)) {
208       if (file.exists()) {
209         cp.add(FileUtil.toCanonicalPath(file.getPath()));
210       }
211     }
212     return StringUtil.join(cp, File.pathSeparator);
213   }
214
215   @NotNull
216   @Override
217   public String getPresentableName() {
218     return "Groovy-Eclipse";
219   }
220 }