1dc5c7c2f563363ea39aa366cbde0c756014435b
[idea/community.git] / plugins / groovy / rt / src / org / jetbrains / jps / incremental / groovy / GroovyBuilder.java
1 package org.jetbrains.jps.incremental.groovy;
2
3
4 import com.intellij.openapi.diagnostic.Logger;
5 import com.intellij.openapi.util.Key;
6 import com.intellij.openapi.util.io.FileUtil;
7 import com.intellij.util.ArrayUtil;
8 import com.intellij.util.Consumer;
9 import com.intellij.util.SystemProperties;
10 import groovy.util.CharsetToolkit;
11 import org.jetbrains.annotations.Nullable;
12 import org.jetbrains.asm4.ClassReader;
13 import org.jetbrains.ether.dependencyView.Callbacks;
14 import org.jetbrains.ether.dependencyView.Mappings;
15 import org.jetbrains.groovy.compiler.rt.GroovyCompilerWrapper;
16 import org.jetbrains.jps.ModuleChunk;
17 import org.jetbrains.jps.cmdline.ClasspathBootstrap;
18 import org.jetbrains.jps.incremental.*;
19 import org.jetbrains.jps.incremental.fs.RootDescriptor;
20 import org.jetbrains.jps.incremental.java.ClassPostProcessor;
21 import org.jetbrains.jps.incremental.java.JavaBuilder;
22 import org.jetbrains.jps.incremental.messages.BuildMessage;
23 import org.jetbrains.jps.incremental.messages.CompilerMessage;
24 import org.jetbrains.jps.incremental.messages.FileGeneratedEvent;
25 import org.jetbrains.jps.incremental.messages.ProgressMessage;
26 import org.jetbrains.jps.incremental.storage.SourceToOutputMapping;
27 import org.jetbrains.jps.javac.OutputFileObject;
28 import org.jetbrains.jps.model.java.JpsJavaClasspathKind;
29 import org.jetbrains.jps.model.java.JpsJavaSdkType;
30 import org.jetbrains.jps.model.library.JpsSdkProperties;
31 import org.jetbrains.jps.model.library.JpsTypedLibrary;
32 import org.jetbrains.jps.model.module.JpsModule;
33
34 import java.io.File;
35 import java.io.IOException;
36 import java.util.*;
37 import java.util.concurrent.ConcurrentHashMap;
38
39 /**
40  * @author Eugene Zhuravlev
41  *         Date: 10/25/11
42  */
43 public class GroovyBuilder extends ModuleLevelBuilder {
44   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.groovy.GroovyBuilder");
45   public static final String BUILDER_NAME = "groovy";
46   private static final Key<Boolean> CHUNK_REBUILD_ORDERED = Key.create("CHUNK_REBUILD_ORDERED");
47   private static final Key<Map<String, String>> STUB_TO_SRC = Key.create("STUB_TO_SRC");
48   private final boolean myForStubs;
49   private final String myBuilderName;
50
51   public GroovyBuilder(boolean forStubs) {
52     super(forStubs ? BuilderCategory.SOURCE_GENERATOR : BuilderCategory.OVERWRITING_TRANSLATOR);
53     myForStubs = forStubs;
54     myBuilderName = BUILDER_NAME + (forStubs ? "-stubs" : "-classes");
55   }
56
57   static {
58     JavaBuilder.registerClassPostProcessor(new RecompileStubSources());
59   }
60
61   public String getName() {
62     return myBuilderName;
63   }
64
65   public ModuleLevelBuilder.ExitCode build(final CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
66     try {
67       final List<File> toCompile = collectChangedFiles(context, chunk);
68       if (toCompile.isEmpty()) {
69         return ExitCode.NOTHING_DONE;
70       }
71
72       Map<JpsModule, String> finalOutputs = getCanonicalModuleOutputs(context, chunk);
73       if (finalOutputs == null) {
74         return ExitCode.ABORT;
75       }
76       Map<JpsModule, String> generationOutputs = getGenerationOutputs(chunk, finalOutputs);
77
78       final Set<String> toCompilePaths = new LinkedHashSet<String>();
79       for (File file : toCompile) {
80         toCompilePaths.add(FileUtil.toSystemIndependentName(file.getPath()));
81       }
82       
83       Map<String, String> class2Src = buildClassToSourceMap(chunk, context, toCompilePaths, finalOutputs);
84
85       final String encoding = context.getProjectDescriptor().getEncodingConfiguration().getPreferredModuleChunkEncoding(chunk);
86       List<String> patchers = Collections.emptyList(); //todo patchers
87       String compilerOutput = generationOutputs.get(chunk.representativeModule());
88       final File tempFile = GroovycOSProcessHandler.fillFileWithGroovycParameters(
89         compilerOutput, toCompilePaths, FileUtil.toSystemDependentName(finalOutputs.get(chunk.representativeModule())), class2Src, encoding, patchers
90       );
91
92       //todo xmx
93       final List<String> cmd = ExternalProcessUtil.buildJavaCommandLine(
94         getJavaExecutable(chunk),
95         "org.jetbrains.groovy.compiler.rt.GroovycRunner",
96         Collections.<String>emptyList(), new ArrayList<String>(generateClasspath(context, chunk)),
97         Arrays.asList("-Xmx384m",
98                       "-Dfile.encoding=" + CharsetToolkit.getDefaultSystemCharset().name()/*,
99                       "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5239"*/),
100         Arrays.<String>asList(myForStubs ? "stubs" : "groovyc",
101                               tempFile.getPath())
102       );
103
104       final Process process = Runtime.getRuntime().exec(ArrayUtil.toStringArray(cmd));
105       GroovycOSProcessHandler handler = GroovycOSProcessHandler.runGroovyc(process, new Consumer<String>() {
106         public void consume(String s) {
107           context.processMessage(new ProgressMessage(s));
108         }
109       });
110
111       if (!context.isProjectRebuild() && handler.shouldRetry()) {
112         if (CHUNK_REBUILD_ORDERED.get(context) != null) {
113           CHUNK_REBUILD_ORDERED.set(context, null);
114         } else {
115           CHUNK_REBUILD_ORDERED.set(context, Boolean.TRUE);
116           LOG.info("Order chunk rebuild");
117           return ExitCode.CHUNK_REBUILD_REQUIRED;
118         }
119       }
120
121       if (myForStubs) {
122         final ModuleRootsIndex rootsIndex = context.getProjectDescriptor().rootsIndex;
123         for (JpsModule module : generationOutputs.keySet()) {
124           File root = new File(generationOutputs.get(module));
125           rootsIndex.associateRoot(context, root, module, context.isCompilingTests());
126         }
127       }
128
129       for (CompilerMessage message : handler.getCompilerMessages()) {
130         context.processMessage(message);
131       }
132
133
134       List<GroovycOSProcessHandler.OutputItem> compiled = new ArrayList<GroovycOSProcessHandler.OutputItem>();
135       for (GroovycOSProcessHandler.OutputItem item : handler.getSuccessfullyCompiled()) {
136         compiled.add(ensureCorrectOutput(context, chunk, item, generationOutputs, compilerOutput));
137       }
138
139       if (myForStubs) {
140         Map<String, String> stubToSrc = STUB_TO_SRC.get(context);
141         if (stubToSrc == null) {
142           STUB_TO_SRC.set(context, stubToSrc = new ConcurrentHashMap<String, String>());
143         }
144         for (GroovycOSProcessHandler.OutputItem item : handler.getSuccessfullyCompiled()) {
145           stubToSrc.put(FileUtil.toSystemIndependentName(item.outputPath), item.sourcePath);
146         }
147       }
148
149       if (!myForStubs && updateDependencies(context, chunk, toCompile, generationOutputs, compiled)) {
150         return ExitCode.ADDITIONAL_PASS_REQUIRED;
151       }
152       return ExitCode.OK;
153     }
154     catch (Exception e) {
155       throw new ProjectBuildException(e);
156     }
157   }
158
159   @Override
160   public void cleanupResources(CompileContext context, ModuleChunk chunk) {
161     super.cleanupResources(context, chunk);
162     STUB_TO_SRC.set(context, null);
163   }
164
165   private Map<JpsModule, String> getGenerationOutputs(ModuleChunk chunk, Map<JpsModule, String> finalOutputs) throws IOException {
166     Map<JpsModule, String> generationOutputs = new HashMap<JpsModule, String>();
167     for (JpsModule module : chunk.getModules()) {
168       generationOutputs.put(module, myForStubs ? FileUtil.createTempDirectory("groovyStubs", "__" + module.getName()).getPath() : finalOutputs.get(module));
169     }
170     return generationOutputs;
171   }
172
173   @Nullable private static Map<JpsModule, String> getCanonicalModuleOutputs(CompileContext context, ModuleChunk chunk) {
174     Map<JpsModule, String> finalOutputs = new HashMap<JpsModule, String>();
175     for (JpsModule module : chunk.getModules()) {
176       File moduleOutputDir = context.getProjectPaths().getModuleOutputDir(module, context.isCompilingTests());
177       if (moduleOutputDir == null) {
178         context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "Output directory not specified for module " + module.getName()));
179         return null;
180       }
181       String moduleOutputPath = FileUtil.toCanonicalPath(moduleOutputDir.getPath());
182       assert moduleOutputPath != null;
183       finalOutputs.put(module, moduleOutputPath.endsWith("/") ? moduleOutputPath : moduleOutputPath + "/");
184     }
185     return finalOutputs;
186   }
187
188   private static GroovycOSProcessHandler.OutputItem ensureCorrectOutput(CompileContext context,
189                                                                         ModuleChunk chunk,
190                                                                         GroovycOSProcessHandler.OutputItem item, Map<JpsModule, String> generationOutputs, String compilerOutput) throws IOException {
191     if (chunk.getModules().size() > 1) {
192       final ModuleRootsIndex rootsIndex = context.getProjectDescriptor().rootsIndex;
193       RootDescriptor descriptor = rootsIndex.getModuleAndRoot(context, new File(item.sourcePath));
194       if (descriptor != null) {
195         JpsModule srcModule = rootsIndex.getModuleByName(descriptor.module);
196         if (srcModule != null && srcModule != chunk.representativeModule()) {
197           File output = new File(item.outputPath);
198
199           //todo honor package prefixes
200           File correctRoot = new File(generationOutputs.get(srcModule));
201           File correctOutput = new File(correctRoot, FileUtil.getRelativePath(new File(compilerOutput), output));
202
203           FileUtil.rename(output, correctOutput);
204           return new GroovycOSProcessHandler.OutputItem(correctOutput.getPath(), item.sourcePath);
205         }
206       }
207     }
208     return item;
209   }
210
211   private static String getJavaExecutable(ModuleChunk chunk) {
212     JpsTypedLibrary<JpsSdkProperties> sdk = chunk.getModules().iterator().next().getSdk(JpsJavaSdkType.INSTANCE);
213     if (sdk != null) {
214       return JpsJavaSdkType.getJavaExecutable(sdk.getProperties());
215     }
216     return SystemProperties.getJavaHome() + "/bin/java";
217   }
218
219   @Override
220   public boolean shouldHonorFileEncodingForCompilation(File file) {
221     return isGroovyFile(file.getAbsolutePath());
222   }
223
224   private static List<File> collectChangedFiles(CompileContext context, ModuleChunk chunk) throws IOException {
225     final ResourcePatterns patterns = ResourcePatterns.KEY.get(context);
226     assert patterns != null;
227     final List<File> toCompile = new ArrayList<File>();
228     FSOperations.processFilesToRecompile(context, chunk, new FileProcessor() {
229       public boolean apply(JpsModule module, File file, String sourceRoot) throws IOException {
230         final String path = file.getPath();
231         if (isGroovyFile(path) && !patterns.isResourceFile(file, sourceRoot)) { //todo file type check
232           toCompile.add(file);
233         }
234         return true;
235       }
236     });
237     return toCompile;
238   }
239
240   private boolean updateDependencies(CompileContext context,
241                                   ModuleChunk chunk,
242                                   List<File> toCompile,
243                                   Map<JpsModule, String> generationOutputs,
244                                   List<GroovycOSProcessHandler.OutputItem> successfullyCompiled) throws IOException {
245     final Mappings delta = context.getProjectDescriptor().dataManager.getMappings().createDelta();
246     final List<File> successfullyCompiledFiles = new ArrayList<File>();
247     if (!successfullyCompiled.isEmpty()) {
248
249       final Callbacks.Backend callback = delta.getCallback();
250       final FileGeneratedEvent generatedEvent = new FileGeneratedEvent();
251
252       for (GroovycOSProcessHandler.OutputItem item : successfullyCompiled) {
253         final String sourcePath = FileUtil.toSystemIndependentName(item.sourcePath);
254         final String outputPath = FileUtil.toSystemIndependentName(item.outputPath);
255         final RootDescriptor moduleAndRoot = context.getProjectDescriptor().rootsIndex.getModuleAndRoot(context, new File(sourcePath));
256         if (moduleAndRoot != null) {
257           final String moduleName = moduleAndRoot.module;
258           context.getProjectDescriptor().dataManager.getSourceToOutputMap(moduleName, moduleAndRoot.isTestRoot).appendData(sourcePath, outputPath);
259           String moduleOutputPath = generationOutputs.get(context.getProjectDescriptor().rootsIndex.getModuleByName(moduleName));
260           generatedEvent.add(moduleOutputPath, FileUtil.getRelativePath(moduleOutputPath, outputPath, '/'));
261         }
262         callback.associate(outputPath, sourcePath, new ClassReader(FileUtil.loadFileBytes(new File(outputPath))));
263         successfullyCompiledFiles.add(new File(sourcePath));
264       }
265
266       context.processMessage(generatedEvent);
267     }
268
269
270     return updateMappings(context, delta, chunk, toCompile, successfullyCompiledFiles);
271   }
272
273   private static List<String> generateClasspath(CompileContext context, ModuleChunk chunk) {
274     final Set<String> cp = new LinkedHashSet<String>();
275     //groovy_rt.jar
276     // IMPORTANT! must be the first in classpath
277     cp.add(ClasspathBootstrap.getResourcePath(GroovyCompilerWrapper.class).getPath());
278
279     for (File file : context.getProjectPaths().getClasspathFiles(chunk, JpsJavaClasspathKind.compile(context.isCompilingTests()), false)) {
280       cp.add(FileUtil.toCanonicalPath(file.getPath()));
281     }
282     for (File file : context.getProjectPaths().getClasspathFiles(chunk, JpsJavaClasspathKind.runtime(context.isCompilingTests()), false)) {
283       cp.add(FileUtil.toCanonicalPath(file.getPath()));
284     }
285     return new ArrayList<String>(cp);
286   }
287
288   private static boolean isGroovyFile(String path) {
289     return path.endsWith(".groovy") || path.endsWith(".gpp");
290   }
291
292   private static Map<String, String> buildClassToSourceMap(ModuleChunk chunk, CompileContext context, Set<String> toCompilePaths, Map<JpsModule, String> finalOutputs) throws IOException {
293     final Map<String, String> class2Src = new HashMap<String, String>();
294     for (JpsModule module : chunk.getModules()) {
295       String moduleOutputPath = finalOutputs.get(module);
296       final SourceToOutputMapping srcToOut = context.getProjectDescriptor().dataManager.getSourceToOutputMap(module.getName(), context.isCompilingTests());
297       for (String src : srcToOut.getKeys()) {
298         if (!toCompilePaths.contains(src) && isGroovyFile(src) &&
299             !context.getProjectDescriptor().project.getCompilerConfiguration().getExcludes().isExcluded(new File(src))) {
300           final Collection<String> outs = srcToOut.getState(src);
301           if (outs != null) {
302             for (String out : outs) {
303               if (out.endsWith(".class") && out.startsWith(moduleOutputPath)) {
304                 final String className = out.substring(moduleOutputPath.length(), out.length() - ".class".length()).replace('/', '.');
305                 class2Src.put(className, src);
306               }
307             }
308           }
309         }
310       }
311     }
312     return class2Src;
313   }
314
315   @Override
316   public String toString() {
317     return "GroovyBuilder{" +
318            "myForStubs=" + myForStubs +
319            '}';
320   }
321
322   public String getDescription() {
323     return "Groovy builder";
324   }
325
326   private static class RecompileStubSources implements ClassPostProcessor {
327
328     public void process(CompileContext context, OutputFileObject out) {
329       Map<String, String> stubToSrc = STUB_TO_SRC.get(context);
330       if (stubToSrc == null) {
331         return;
332       }
333
334       File src = out.getSourceFile();
335       if (src == null) {
336         return;
337       }
338       String groovy = stubToSrc.get(FileUtil.toSystemIndependentName(src.getPath()));
339       if (groovy == null) {
340         return;
341       }
342
343       try {
344         FSOperations.markDirty(context, new File(groovy));
345       }
346       catch (IOException e) {
347         LOG.error(e);
348       }
349     }
350   }
351 }