reduce the number of force() calls on storages
[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     if (context != null) {
106       context.getTimestampStorage().force();
107       context.getDataManager().flush(false);
108     }
109     final ExternalJavacDescriptor descriptor = ExternalJavacDescriptor.KEY.get(context);
110     if (descriptor != null) {
111       try {
112         final RequestFuture future = descriptor.client.sendShutdownRequest();
113         future.get();
114       }
115       catch (InterruptedException ignored) {
116       }
117       catch (ExecutionException ignored) {
118       }
119       finally {
120         // ensure process is not running
121         descriptor.process.destroyProcess();
122       }
123       ExternalJavacDescriptor.KEY.set(context, null);
124     }
125     cleanupJavacNameTable();
126   }
127
128   private static boolean ourClenupFailed = false;
129
130   private static void cleanupJavacNameTable() {
131     try {
132       if (JavaBuilder.USE_EMBEDDED_JAVAC && !ourClenupFailed) {
133         final Field freelistField = Class.forName("com.sun.tools.javac.util.Name$Table").getDeclaredField("freelist");
134         freelistField.setAccessible(true);
135         freelistField.set(null, com.sun.tools.javac.util.List.nil());
136       }
137     }
138     catch (Throwable e) {
139       ourClenupFailed = true;
140       LOG.info(e);
141     }
142   }
143
144   private float updateFractionBuilderFinished(final float delta) {
145     myModulesProcessed += delta;
146     return myModulesProcessed / myTotalModulesWork;
147   }
148
149   private void runBuild(CompileContext context) throws ProjectBuildException {
150     context.setDone(0.0f);
151
152     if (context.isProjectRebuild()) {
153       cleanOutputRoots(context);
154     }
155
156     context.processMessage(new ProgressMessage("Running 'before' tasks"));
157     runTasks(context, myBuilderRegistry.getBeforeTasks());
158
159     context.setCompilingTests(false);
160     context.processMessage(new ProgressMessage("Building production sources"));
161     buildChunks(context, myProductionChunks);
162
163     context.setCompilingTests(true);
164     context.processMessage(new ProgressMessage("Building test sources"));
165     buildChunks(context, myTestChunks);
166
167     context.processMessage(new ProgressMessage("Running 'after' tasks"));
168     runTasks(context, myBuilderRegistry.getAfterTasks());
169   }
170
171   private CompileContext createContext(CompileScope scope, boolean isMake, final boolean isProjectRebuild) throws ProjectBuildException {
172     final TimestampStorage tsStorage = myProjectDescriptor.timestamps.getStorage();
173     final FSState fsState = myProjectDescriptor.fsState;
174     final ModuleRootsIndex rootsIndex = myProjectDescriptor.rootsIndex;
175     final BuildDataManager dataManager = myProjectDescriptor.dataManager;
176     return new CompileContext(scope, isMake, isProjectRebuild, myProductionChunks, myTestChunks, fsState, dataManager, tsStorage,
177                               myMessageDispatcher, rootsIndex, myCancelStatus);
178   }
179
180   private void cleanOutputRoots(CompileContext context) throws ProjectBuildException {
181     // whole project is affected
182     try {
183       myProjectDescriptor.timestamps.clean();
184     }
185     catch (IOException e) {
186       throw new ProjectBuildException("Error cleaning timestamps storage", e);
187     }
188     try {
189       context.getDataManager().clean();
190     }
191     catch (IOException e) {
192       throw new ProjectBuildException("Error cleaning compiler storages", e);
193     }
194     myProjectDescriptor.fsState.onRebuild();
195
196     final Collection<Module> modulesToClean = context.getProject().getModules().values();
197     final Set<File> rootsToDelete = new HashSet<File>();
198     final Set<File> allSourceRoots = new HashSet<File>();
199
200     for (Module module : modulesToClean) {
201       final File out = context.getProjectPaths().getModuleOutputDir(module, false);
202       if (out != null) {
203         rootsToDelete.add(out);
204       }
205       final File testOut = context.getProjectPaths().getModuleOutputDir(module, true);
206       if (testOut != null) {
207         rootsToDelete.add(testOut);
208       }
209       final List<RootDescriptor> moduleRoots = context.getModuleRoots(module);
210       for (RootDescriptor d : moduleRoots) {
211         allSourceRoots.add(d.root);
212       }
213     }
214
215     // check that output and source roots are not overlapping
216     final List<File> filesToDelete = new ArrayList<File>();
217     for (File outputRoot : rootsToDelete) {
218       if (myCancelStatus.isCanceled()) {
219         throw new ProjectBuildException(CANCELED_MESSAGE);
220       }
221       boolean okToDelete = true;
222       if (PathUtil.isUnder(allSourceRoots, outputRoot)) {
223         okToDelete = false;
224       }
225       else {
226         final Set<File> _outRoot = Collections.singleton(outputRoot);
227         for (File srcRoot : allSourceRoots) {
228           if (PathUtil.isUnder(_outRoot, srcRoot)) {
229             okToDelete = false;
230             break;
231           }
232         }
233       }
234       if (okToDelete) {
235         // do not delete output root itself to avoid lots of unnecessary "roots_changed" events in IDEA
236         final File[] children = outputRoot.listFiles();
237         if (children != null) {
238           filesToDelete.addAll(Arrays.asList(children));
239         }
240       }
241       else {
242         context.processMessage(new CompilerMessage(JPS_SERVER_NAME, BuildMessage.Kind.WARNING, "Output path " +
243                                                                                                outputRoot.getPath() +
244                                                                                                " intersects with a source root. The output cannot be cleaned."));
245       }
246     }
247
248     context.processMessage(new ProgressMessage("Cleaning output directories..."));
249     FileUtil.asyncDelete(filesToDelete);
250   }
251
252   private static void runTasks(CompileContext context, final List<BuildTask> tasks) throws ProjectBuildException {
253     for (BuildTask task : tasks) {
254       task.build(context);
255     }
256   }
257
258   private void buildChunks(CompileContext context, ProjectChunks chunks) throws ProjectBuildException {
259     final CompileScope scope = context.getScope();
260     for (ModuleChunk chunk : chunks.getChunkList()) {
261       if (scope.isAffected(chunk)) {
262         buildChunk(context, chunk);
263       }
264       else {
265         final float fraction = updateFractionBuilderFinished(chunk.getModules().size());
266         context.setDone(fraction);
267       }
268     }
269   }
270
271   private void buildChunk(CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
272     try {
273       context.ensureFSStateInitialized(chunk);
274       if (context.isMake()) {
275         // cleanup outputs
276         final Set<String> allChunkRemovedSources = new HashSet<String>();
277         final SourceToFormMapping sourceToFormMap = context.getDataManager().getSourceToFormMap();
278
279         for (Module module : chunk.getModules()) {
280           final Collection<String> deletedPaths = myProjectDescriptor.fsState.getDeletedPaths(module, context.isCompilingTests());
281           allChunkRemovedSources.addAll(deletedPaths);
282
283           final String moduleName = module.getName().toLowerCase(Locale.US);
284           final SourceToOutputMapping sourceToOutputStorage =
285             context.getDataManager().getSourceToOutputMap(moduleName, context.isCompilingTests());
286           // actually delete outputs associated with removed paths
287           for (String deletedSource : deletedPaths) {
288             // deleting outputs corresponding to non-existing source
289             final Collection<String> outputs = sourceToOutputStorage.getState(deletedSource);
290             if (outputs != null) {
291               for (String output : outputs) {
292                 FileUtil.delete(new File(output));
293               }
294               sourceToOutputStorage.remove(deletedSource);
295             }
296             // check if deleted source was associated with a form
297             final String formPath = sourceToFormMap.getState(deletedSource);
298             if (formPath != null) {
299               final File formFile = new File(formPath);
300               if (formFile.exists()) {
301                 context.markDirty(formFile);
302               }
303               sourceToFormMap.remove(deletedSource);
304             }
305           }
306         }
307         Paths.CHUNK_REMOVED_SOURCES_KEY.set(context, allChunkRemovedSources);
308         for (Module module : chunk.getModules()) {
309           myProjectDescriptor.fsState.clearDeletedPaths(module, context.isCompilingTests());
310         }
311       }
312
313       context.onChunkBuildStart(chunk);
314
315       for (BuilderCategory category : BuilderCategory.values()) {
316         runBuilders(context, chunk, category);
317       }
318     }
319     catch (ProjectBuildException e) {
320       throw e;
321     }
322     catch (Exception e) {
323       throw new ProjectBuildException(e);
324     }
325     finally {
326       try {
327         for (BuilderCategory category : BuilderCategory.values()) {
328           for (ModuleLevelBuilder builder : myBuilderRegistry.getBuilders(category)) {
329             builder.cleanupResources(context, chunk);
330           }
331         }
332       }
333       finally {
334         try {
335           context.onChunkBuildComplete(chunk);
336         }
337         catch (Exception e) {
338           throw new ProjectBuildException(e);
339         }
340         finally {
341           Paths.CHUNK_REMOVED_SOURCES_KEY.set(context, null);
342         }
343       }
344     }
345   }
346
347   private void runBuilders(final CompileContext context, ModuleChunk chunk, BuilderCategory category) throws ProjectBuildException {
348     final List<ModuleLevelBuilder> builders = myBuilderRegistry.getBuilders(category);
349     if (builders.isEmpty()) {
350       return;
351     }
352
353     float stageCount = myTotalBuilderCount;
354     int stagesPassed = 0;
355     final int modulesInChunk = chunk.getModules().size();
356
357     boolean nextPassRequired;
358     do {
359       nextPassRequired = false;
360       context.beforeNextCompileRound(chunk);
361
362       if (!context.isProjectRebuild()) {
363         syncOutputFiles(context, chunk);
364       }
365
366       for (ModuleLevelBuilder builder : builders) {
367         final ModuleLevelBuilder.ExitCode buildResult = builder.build(context, chunk);
368
369         if (buildResult == ModuleLevelBuilder.ExitCode.ABORT) {
370           throw new ProjectBuildException("Builder " + builder.getDescription() + " requested build stop");
371         }
372         if (myCancelStatus.isCanceled()) {
373           throw new ProjectBuildException(CANCELED_MESSAGE);
374         }
375         if (buildResult == ModuleLevelBuilder.ExitCode.ADDITIONAL_PASS_REQUIRED) {
376           if (!nextPassRequired) {
377             // recalculate basis
378             myModulesProcessed -= (stagesPassed * modulesInChunk) / stageCount;
379             stageCount += myTotalBuilderCount;
380             myModulesProcessed += (stagesPassed * modulesInChunk) / stageCount;
381           }
382           nextPassRequired = true;
383         }
384
385         stagesPassed++;
386         final float fraction = updateFractionBuilderFinished(modulesInChunk / (stageCount));
387         context.setDone(fraction);
388       }
389     }
390     while (nextPassRequired);
391   }
392
393   private static void syncOutputFiles(final CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
394     final BuildDataManager dataManager = context.getDataManager();
395     final boolean compilingTests = context.isCompilingTests();
396     try {
397       final Collection<String> allOutputs = new LinkedList<String>();
398
399       context.processFilesToRecompile(chunk, new FileProcessor() {
400         private final Map<Module, SourceToOutputMapping> storageMap = new HashMap<Module, SourceToOutputMapping>();
401
402         @Override
403         public boolean apply(Module module, File file, String sourceRoot) throws Exception {
404           SourceToOutputMapping srcToOut = storageMap.get(module);
405           if (srcToOut == null) {
406             srcToOut = dataManager.getSourceToOutputMap(module.getName().toLowerCase(Locale.US), compilingTests);
407             storageMap.put(module, srcToOut);
408           }
409           final String srcPath = FileUtil.toSystemIndependentName(file.getPath());
410           final Collection<String> outputs = srcToOut.getState(srcPath);
411
412           if (outputs != null) {
413             for (String output : outputs) {
414               if (LOG.isDebugEnabled()) {
415                 allOutputs.add(output);
416               }
417               FileUtil.delete(new File(output));
418             }
419             srcToOut.remove(srcPath);
420           }
421           return true;
422         }
423       });
424
425       if (LOG.isDebugEnabled()) {
426         if (context.isMake() && allOutputs.size() > 0) {
427           LOG.info("Cleaning output files:");
428           final String[] buffer = new String[allOutputs.size()];
429           int i = 0;
430           for (String output : allOutputs) {
431             buffer[i++] = output;
432           }
433           Arrays.sort(buffer);
434           for (String output : buffer) {
435             LOG.info(output);
436           }
437           LOG.info("End of files");
438         }
439       }
440     }
441     catch (Exception e) {
442       throw new ProjectBuildException(e);
443     }
444   }
445 }