Merge remote-tracking branch 'origin/master'
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / incremental / IncProjectBuilder.java
1 package org.jetbrains.jps.incremental;
2
3 import com.intellij.openapi.diagnostic.Logger;
4 import com.intellij.openapi.util.io.FileUtil;
5 import com.intellij.util.io.PersistentEnumerator;
6 import org.jetbrains.jps.*;
7 import org.jetbrains.jps.api.CanceledStatus;
8 import org.jetbrains.jps.api.RequestFuture;
9 import org.jetbrains.jps.incremental.java.ExternalJavacDescriptor;
10 import org.jetbrains.jps.incremental.java.JavaBuilder;
11 import org.jetbrains.jps.incremental.messages.BuildMessage;
12 import org.jetbrains.jps.incremental.messages.CompilerMessage;
13 import org.jetbrains.jps.incremental.messages.ProgressMessage;
14 import org.jetbrains.jps.incremental.storage.BuildDataManager;
15 import org.jetbrains.jps.incremental.storage.SourceToFormMapping;
16 import org.jetbrains.jps.incremental.storage.SourceToOutputMapping;
17 import org.jetbrains.jps.incremental.storage.TimestampStorage;
18 import org.jetbrains.jps.server.ProjectDescriptor;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.lang.reflect.Field;
23 import java.util.*;
24 import java.util.concurrent.ExecutionException;
25
26 /**
27  * @author Eugene Zhuravlev
28  *         Date: 9/17/11
29  */
30 public class IncProjectBuilder {
31   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.IncProjectBuilder");
32
33   public static final String JPS_SERVER_NAME = "JPS BUILD";
34   private static final String CANCELED_MESSAGE = "The build has been canceled";
35
36   private final ProjectDescriptor myProjectDescriptor;
37   private final BuilderRegistry myBuilderRegistry;
38   private final CanceledStatus myCancelStatus;
39   private ProjectChunks myProductionChunks;
40   private ProjectChunks myTestChunks;
41   private final List<MessageHandler> myMessageHandlers = new ArrayList<MessageHandler>();
42   private final MessageHandler myMessageDispatcher = new MessageHandler() {
43     public void processMessage(BuildMessage msg) {
44       for (MessageHandler h : myMessageHandlers) {
45         h.processMessage(msg);
46       }
47     }
48   };
49
50   private float myModulesProcessed = 0.0f;
51   private final float myTotalModulesWork;
52   private final int myTotalBuilderCount;
53
54   public IncProjectBuilder(ProjectDescriptor pd, BuilderRegistry builderRegistry, CanceledStatus cs) {
55     myProjectDescriptor = pd;
56     myBuilderRegistry = builderRegistry;
57     myCancelStatus = cs;
58     myProductionChunks = new ProjectChunks(pd.project, ClasspathKind.PRODUCTION_COMPILE);
59     myTestChunks = new ProjectChunks(pd.project, ClasspathKind.TEST_COMPILE);
60     myTotalModulesWork = (float)pd.rootsIndex.getTotalModuleCount() * 2;  /* multiply by 2 to reflect production and test sources */
61     myTotalBuilderCount = builderRegistry.getTotalBuilderCount();
62   }
63
64   public void addMessageHandler(MessageHandler handler) {
65     myMessageHandlers.add(handler);
66   }
67
68   public void build(CompileScope scope, final boolean isMake, final boolean isProjectRebuild) {
69     CompileContext context = null;
70     try {
71       try {
72         context = createContext(scope, isMake, isProjectRebuild);
73         runBuild(context);
74       }
75       catch (ProjectBuildException e) {
76         if (e.getCause() instanceof PersistentEnumerator.CorruptedException) {
77           // force rebuild
78           myMessageDispatcher.processMessage(new CompilerMessage(JPS_SERVER_NAME, BuildMessage.Kind.INFO,
79                                                                  "Internal caches are corrupted or have outdated format, forcing project rebuild: " +
80                                                                  e.getMessage()));
81           flushContext(context);
82           context = createContext(new AllProjectScope(scope.getProject(), true), false, true);
83           runBuild(context);
84         }
85         else {
86           throw e;
87         }
88       }
89     }
90     catch (ProjectBuildException e) {
91       final Throwable cause = e.getCause();
92       if (cause == null) {
93         myMessageDispatcher.processMessage(new ProgressMessage(e.getMessage()));
94       }
95       else {
96         myMessageDispatcher.processMessage(new CompilerMessage(JPS_SERVER_NAME, cause));
97       }
98     }
99     finally {
100       flushContext(context);
101     }
102   }
103
104   private static void flushContext(CompileContext context) {
105     final ExternalJavacDescriptor descriptor = ExternalJavacDescriptor.KEY.get(context);
106     if (descriptor != null) {
107       try {
108         final RequestFuture future = descriptor.client.sendShutdownRequest();
109         future.get();
110       }
111       catch (InterruptedException ignored) {
112       }
113       catch (ExecutionException ignored) {
114       }
115       finally {
116         // ensure process is not running
117         descriptor.process.destroyProcess();
118       }
119       ExternalJavacDescriptor.KEY.set(context, null);
120     }
121     cleanupJavacNameTable();
122   }
123
124   private static boolean ourClenupFailed = false;
125
126   private static void cleanupJavacNameTable() {
127     try {
128       if (JavaBuilder.USE_EMBEDDED_JAVAC && !ourClenupFailed) {
129         final Field freelistField = Class.forName("com.sun.tools.javac.util.Name$Table").getDeclaredField("freelist");
130         freelistField.setAccessible(true);
131         freelistField.set(null, com.sun.tools.javac.util.List.nil());
132       }
133     }
134     catch (Throwable e) {
135       ourClenupFailed = true;
136       LOG.info(e);
137     }
138   }
139
140   private float updateFractionBuilderFinished(final float delta) {
141     myModulesProcessed += delta;
142     return myModulesProcessed / myTotalModulesWork;
143   }
144
145   private void runBuild(CompileContext context) throws ProjectBuildException {
146     context.setDone(0.0f);
147
148     if (context.isProjectRebuild()) {
149       cleanOutputRoots(context);
150     }
151
152     context.processMessage(new ProgressMessage("Running 'before' tasks"));
153     runTasks(context, myBuilderRegistry.getBeforeTasks());
154
155     context.setCompilingTests(false);
156     context.processMessage(new ProgressMessage("Building production sources"));
157     buildChunks(context, myProductionChunks);
158
159     context.setCompilingTests(true);
160     context.processMessage(new ProgressMessage("Building test sources"));
161     buildChunks(context, myTestChunks);
162
163     context.processMessage(new ProgressMessage("Running 'after' tasks"));
164     runTasks(context, myBuilderRegistry.getAfterTasks());
165   }
166
167   private CompileContext createContext(CompileScope scope, boolean isMake, final boolean isProjectRebuild) throws ProjectBuildException {
168     final TimestampStorage tsStorage = myProjectDescriptor.timestamps.getStorage();
169     final FSState fsState = myProjectDescriptor.fsState;
170     final ModuleRootsIndex rootsIndex = myProjectDescriptor.rootsIndex;
171     final BuildDataManager dataManager = myProjectDescriptor.dataManager;
172     return new CompileContext(scope, isMake, isProjectRebuild, myProductionChunks, myTestChunks, fsState, dataManager, tsStorage,
173                               myMessageDispatcher, rootsIndex, myCancelStatus);
174   }
175
176   private void cleanOutputRoots(CompileContext context) throws ProjectBuildException {
177     // whole project is affected
178     try {
179       myProjectDescriptor.timestamps.clean();
180     }
181     catch (IOException e) {
182       throw new ProjectBuildException("Error cleaning timestamps storage", e);
183     }
184     try {
185       context.getDataManager().clean();
186     }
187     catch (IOException e) {
188       throw new ProjectBuildException("Error cleaning compiler storages", e);
189     }
190     myProjectDescriptor.fsState.onRebuild();
191
192     final Collection<Module> modulesToClean = context.getProject().getModules().values();
193     final Set<File> rootsToDelete = new HashSet<File>();
194     final Set<File> allSourceRoots = new HashSet<File>();
195
196     for (Module module : modulesToClean) {
197       final File out = context.getProjectPaths().getModuleOutputDir(module, false);
198       if (out != null) {
199         rootsToDelete.add(out);
200       }
201       final File testOut = context.getProjectPaths().getModuleOutputDir(module, true);
202       if (testOut != null) {
203         rootsToDelete.add(testOut);
204       }
205       final List<RootDescriptor> moduleRoots = context.getModuleRoots(module);
206       for (RootDescriptor d : moduleRoots) {
207         allSourceRoots.add(d.root);
208       }
209     }
210
211     // check that output and source roots are not overlapping
212     final List<File> filesToDelete = new ArrayList<File>();
213     for (File outputRoot : rootsToDelete) {
214       if (myCancelStatus.isCanceled()) {
215         throw new ProjectBuildException(CANCELED_MESSAGE);
216       }
217       boolean okToDelete = true;
218       if (PathUtil.isUnder(allSourceRoots, outputRoot)) {
219         okToDelete = false;
220       }
221       else {
222         final Set<File> _outRoot = Collections.singleton(outputRoot);
223         for (File srcRoot : allSourceRoots) {
224           if (PathUtil.isUnder(_outRoot, srcRoot)) {
225             okToDelete = false;
226             break;
227           }
228         }
229       }
230       if (okToDelete) {
231         // do not delete output root itself to avoid lots of unnecessary "roots_changed" events in IDEA
232         final File[] children = outputRoot.listFiles();
233         if (children != null) {
234           filesToDelete.addAll(Arrays.asList(children));
235         }
236       }
237       else {
238         context.processMessage(new CompilerMessage(JPS_SERVER_NAME, BuildMessage.Kind.WARNING, "Output path " +
239                                                                                                outputRoot.getPath() +
240                                                                                                " intersects with a source root. The output cannot be cleaned."));
241       }
242     }
243
244     context.processMessage(new ProgressMessage("Cleaning output directories..."));
245     FileUtil.asyncDelete(filesToDelete);
246   }
247
248   private static void runTasks(CompileContext context, final List<BuildTask> tasks) throws ProjectBuildException {
249     for (BuildTask task : tasks) {
250       task.build(context);
251     }
252   }
253
254   private void buildChunks(CompileContext context, ProjectChunks chunks) throws ProjectBuildException {
255     final CompileScope scope = context.getScope();
256     for (ModuleChunk chunk : chunks.getChunkList()) {
257       if (scope.isAffected(chunk)) {
258         buildChunk(context, chunk);
259       }
260       else {
261         final float fraction = updateFractionBuilderFinished(chunk.getModules().size());
262         context.setDone(fraction);
263       }
264     }
265   }
266
267   private void buildChunk(CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
268     try {
269       context.ensureFSStateInitialized(chunk);
270       if (context.isMake()) {
271         // cleanup outputs
272         final Set<String> allChunkRemovedSources = new HashSet<String>();
273         final SourceToFormMapping sourceToFormMap = context.getDataManager().getSourceToFormMap();
274
275         for (Module module : chunk.getModules()) {
276           final Collection<String> deletedPaths = myProjectDescriptor.fsState.getDeletedPaths(module, context.isCompilingTests());
277           allChunkRemovedSources.addAll(deletedPaths);
278
279           final String moduleName = module.getName().toLowerCase(Locale.US);
280           final SourceToOutputMapping sourceToOutputStorage =
281             context.getDataManager().getSourceToOutputMap(moduleName, context.isCompilingTests());
282           // actually delete outputs associated with removed paths
283           for (String deletedSource : deletedPaths) {
284             // deleting outputs corresponding to non-existing source
285             final Collection<String> outputs = sourceToOutputStorage.getState(deletedSource);
286             if (outputs != null) {
287               for (String output : outputs) {
288                 FileUtil.delete(new File(output));
289               }
290               sourceToOutputStorage.remove(deletedSource);
291             }
292             // check if deleted source was associated with a form
293             final String formPath = sourceToFormMap.getState(deletedSource);
294             if (formPath != null) {
295               final File formFile = new File(formPath);
296               if (formFile.exists()) {
297                 context.markDirty(formFile);
298               }
299               sourceToFormMap.remove(deletedSource);
300             }
301           }
302         }
303         Paths.CHUNK_REMOVED_SOURCES_KEY.set(context, allChunkRemovedSources);
304         for (Module module : chunk.getModules()) {
305           myProjectDescriptor.fsState.clearDeletedPaths(module, context.isCompilingTests());
306         }
307       }
308
309       context.onChunkBuildStart(chunk);
310
311       for (BuilderCategory category : BuilderCategory.values()) {
312         runBuilders(context, chunk, category);
313       }
314     }
315     catch (ProjectBuildException e) {
316       throw e;
317     }
318     catch (Exception e) {
319       throw new ProjectBuildException(e);
320     }
321     finally {
322       try {
323         for (BuilderCategory category : BuilderCategory.values()) {
324           for (ModuleLevelBuilder builder : myBuilderRegistry.getBuilders(category)) {
325             builder.cleanupResources(context, chunk);
326           }
327         }
328       }
329       finally {
330         try {
331           context.onChunkBuildComplete(chunk);
332         }
333         catch (Exception e) {
334           throw new ProjectBuildException(e);
335         }
336         finally {
337           Paths.CHUNK_REMOVED_SOURCES_KEY.set(context, null);
338         }
339       }
340     }
341   }
342
343   private void runBuilders(final CompileContext context, ModuleChunk chunk, BuilderCategory category) throws ProjectBuildException {
344     final List<ModuleLevelBuilder> builders = myBuilderRegistry.getBuilders(category);
345     if (builders.isEmpty()) {
346       return;
347     }
348
349     float stageCount = myTotalBuilderCount;
350     int stagesPassed = 0;
351     final int modulesInChunk = chunk.getModules().size();
352
353     boolean nextPassRequired;
354     do {
355       nextPassRequired = false;
356       context.beforeNextCompileRound(chunk);
357
358       if (!context.isProjectRebuild()) {
359         syncOutputFiles(context, chunk);
360       }
361
362       for (ModuleLevelBuilder builder : builders) {
363         final ModuleLevelBuilder.ExitCode buildResult = builder.build(context, chunk);
364
365         if (buildResult == ModuleLevelBuilder.ExitCode.ABORT) {
366           throw new ProjectBuildException("Builder " + builder.getDescription() + " requested build stop");
367         }
368         if (myCancelStatus.isCanceled()) {
369           throw new ProjectBuildException(CANCELED_MESSAGE);
370         }
371         if (buildResult == ModuleLevelBuilder.ExitCode.ADDITIONAL_PASS_REQUIRED) {
372           if (!nextPassRequired) {
373             // recalculate basis
374             myModulesProcessed -= (stagesPassed * modulesInChunk) / stageCount;
375             stageCount += myTotalBuilderCount;
376             myModulesProcessed += (stagesPassed * modulesInChunk) / stageCount;
377           }
378           nextPassRequired = true;
379         }
380
381         stagesPassed++;
382         final float fraction = updateFractionBuilderFinished(modulesInChunk / (stageCount));
383         context.setDone(fraction);
384       }
385     }
386     while (nextPassRequired);
387   }
388
389   private static void syncOutputFiles(final CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
390     final BuildDataManager dataManager = context.getDataManager();
391     final boolean compilingTests = context.isCompilingTests();
392     try {
393       final Collection<String> allOutputs = new LinkedList<String>();
394
395       context.processFilesToRecompile(chunk, new FileProcessor() {
396         private final Map<Module, SourceToOutputMapping> storageMap = new HashMap<Module, SourceToOutputMapping>();
397
398         @Override
399         public boolean apply(Module module, File file, String sourceRoot) throws Exception {
400           SourceToOutputMapping srcToOut = storageMap.get(module);
401           if (srcToOut == null) {
402             srcToOut = dataManager.getSourceToOutputMap(module.getName().toLowerCase(Locale.US), compilingTests);
403             storageMap.put(module, srcToOut);
404           }
405           final String srcPath = FileUtil.toSystemIndependentName(file.getPath());
406           final Collection<String> outputs = srcToOut.getState(srcPath);
407
408           if (outputs != null) {
409             for (String output : outputs) {
410               if (LOG.isDebugEnabled()) {
411                 allOutputs.add(output);
412               }
413               FileUtil.delete(new File(output));
414             }
415             srcToOut.remove(srcPath);
416           }
417           return true;
418         }
419       });
420
421       if (LOG.isDebugEnabled()) {
422         if (context.isMake() && allOutputs.size() > 0) {
423           LOG.info("Cleaning output files:");
424           final String[] buffer = new String[allOutputs.size()];
425           int i = 0;
426           for (String output : allOutputs) {
427             buffer[i++] = output;
428           }
429           Arrays.sort(buffer);
430           for (String output : buffer) {
431             LOG.info(output);
432           }
433           LOG.info("End of files");
434         }
435       }
436     }
437     catch (Exception e) {
438       throw new ProjectBuildException(e);
439     }
440   }
441 }