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