1 package org.jetbrains.jps.incremental;
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;
21 import java.io.IOException;
22 import java.lang.reflect.Field;
24 import java.util.concurrent.ExecutionException;
27 * @author Eugene Zhuravlev
30 public class IncProjectBuilder {
31 private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.IncProjectBuilder");
33 public static final String JPS_SERVER_NAME = "JPS BUILD";
34 private static final String CANCELED_MESSAGE = "The build has been canceled";
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);
50 private float myModulesProcessed = 0.0f;
51 private final float myTotalModulesWork;
52 private final int myTotalBuilderCount;
54 public IncProjectBuilder(ProjectDescriptor pd, BuilderRegistry builderRegistry, CanceledStatus cs) {
55 myProjectDescriptor = pd;
56 myBuilderRegistry = builderRegistry;
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();
64 public void addMessageHandler(MessageHandler handler) {
65 myMessageHandlers.add(handler);
68 public void build(CompileScope scope, final boolean isMake, final boolean isProjectRebuild) {
69 CompileContext context = null;
72 context = createContext(scope, isMake, isProjectRebuild);
75 catch (ProjectBuildException e) {
76 if (e.getCause() instanceof PersistentEnumerator.CorruptedException) {
78 myMessageDispatcher.processMessage(new CompilerMessage(JPS_SERVER_NAME, BuildMessage.Kind.INFO,
79 "Internal caches are corrupted or have outdated format, forcing project rebuild: " +
81 flushContext(context);
82 context = createContext(new AllProjectScope(scope.getProject(), true), false, true);
90 catch (ProjectBuildException e) {
91 final Throwable cause = e.getCause();
93 myMessageDispatcher.processMessage(new ProgressMessage(e.getMessage()));
96 myMessageDispatcher.processMessage(new CompilerMessage(JPS_SERVER_NAME, cause));
100 flushContext(context);
104 private static void flushContext(CompileContext context) {
105 if (context != null) {
106 context.getTimestampStorage().force();
107 context.getDataManager().flush(false);
109 final ExternalJavacDescriptor descriptor = ExternalJavacDescriptor.KEY.get(context);
110 if (descriptor != null) {
112 final RequestFuture future = descriptor.client.sendShutdownRequest();
115 catch (InterruptedException ignored) {
117 catch (ExecutionException ignored) {
120 // ensure process is not running
121 descriptor.process.destroyProcess();
123 ExternalJavacDescriptor.KEY.set(context, null);
125 cleanupJavacNameTable();
128 private static boolean ourClenupFailed = false;
130 private static void cleanupJavacNameTable() {
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());
138 catch (Throwable e) {
139 ourClenupFailed = true;
144 private float updateFractionBuilderFinished(final float delta) {
145 myModulesProcessed += delta;
146 return myModulesProcessed / myTotalModulesWork;
149 private void runBuild(CompileContext context) throws ProjectBuildException {
150 context.setDone(0.0f);
152 if (context.isProjectRebuild()) {
153 cleanOutputRoots(context);
156 context.processMessage(new ProgressMessage("Running 'before' tasks"));
157 runTasks(context, myBuilderRegistry.getBeforeTasks());
159 context.setCompilingTests(false);
160 context.processMessage(new ProgressMessage("Building production sources"));
161 buildChunks(context, myProductionChunks);
163 context.setCompilingTests(true);
164 context.processMessage(new ProgressMessage("Building test sources"));
165 buildChunks(context, myTestChunks);
167 context.processMessage(new ProgressMessage("Running 'after' tasks"));
168 runTasks(context, myBuilderRegistry.getAfterTasks());
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);
180 private void cleanOutputRoots(CompileContext context) throws ProjectBuildException {
181 // whole project is affected
183 myProjectDescriptor.timestamps.clean();
185 catch (IOException e) {
186 throw new ProjectBuildException("Error cleaning timestamps storage", e);
189 context.getDataManager().clean();
191 catch (IOException e) {
192 throw new ProjectBuildException("Error cleaning compiler storages", e);
194 myProjectDescriptor.fsState.onRebuild();
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>();
200 for (Module module : modulesToClean) {
201 final File out = context.getProjectPaths().getModuleOutputDir(module, false);
203 rootsToDelete.add(out);
205 final File testOut = context.getProjectPaths().getModuleOutputDir(module, true);
206 if (testOut != null) {
207 rootsToDelete.add(testOut);
209 final List<RootDescriptor> moduleRoots = context.getModuleRoots(module);
210 for (RootDescriptor d : moduleRoots) {
211 allSourceRoots.add(d.root);
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);
221 boolean okToDelete = true;
222 if (PathUtil.isUnder(allSourceRoots, outputRoot)) {
226 final Set<File> _outRoot = Collections.singleton(outputRoot);
227 for (File srcRoot : allSourceRoots) {
228 if (PathUtil.isUnder(_outRoot, srcRoot)) {
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));
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."));
248 context.processMessage(new ProgressMessage("Cleaning output directories..."));
249 FileUtil.asyncDelete(filesToDelete);
252 private static void runTasks(CompileContext context, final List<BuildTask> tasks) throws ProjectBuildException {
253 for (BuildTask task : tasks) {
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);
265 final float fraction = updateFractionBuilderFinished(chunk.getModules().size());
266 context.setDone(fraction);
271 private void buildChunk(CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
273 context.ensureFSStateInitialized(chunk);
274 if (context.isMake()) {
276 final Set<String> allChunkRemovedSources = new HashSet<String>();
277 final SourceToFormMapping sourceToFormMap = context.getDataManager().getSourceToFormMap();
279 for (Module module : chunk.getModules()) {
280 final Collection<String> deletedPaths = myProjectDescriptor.fsState.getDeletedPaths(module, context.isCompilingTests());
281 allChunkRemovedSources.addAll(deletedPaths);
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));
294 sourceToOutputStorage.remove(deletedSource);
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);
303 sourceToFormMap.remove(deletedSource);
307 Paths.CHUNK_REMOVED_SOURCES_KEY.set(context, allChunkRemovedSources);
308 for (Module module : chunk.getModules()) {
309 myProjectDescriptor.fsState.clearDeletedPaths(module, context.isCompilingTests());
313 context.onChunkBuildStart(chunk);
315 for (BuilderCategory category : BuilderCategory.values()) {
316 runBuilders(context, chunk, category);
319 catch (ProjectBuildException e) {
322 catch (Exception e) {
323 throw new ProjectBuildException(e);
327 for (BuilderCategory category : BuilderCategory.values()) {
328 for (ModuleLevelBuilder builder : myBuilderRegistry.getBuilders(category)) {
329 builder.cleanupResources(context, chunk);
335 context.onChunkBuildComplete(chunk);
337 catch (Exception e) {
338 throw new ProjectBuildException(e);
341 Paths.CHUNK_REMOVED_SOURCES_KEY.set(context, null);
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()) {
353 float stageCount = myTotalBuilderCount;
354 int stagesPassed = 0;
355 final int modulesInChunk = chunk.getModules().size();
357 boolean nextPassRequired;
359 nextPassRequired = false;
360 context.beforeNextCompileRound(chunk);
362 if (!context.isProjectRebuild()) {
363 syncOutputFiles(context, chunk);
366 for (ModuleLevelBuilder builder : builders) {
367 final ModuleLevelBuilder.ExitCode buildResult = builder.build(context, chunk);
369 if (buildResult == ModuleLevelBuilder.ExitCode.ABORT) {
370 throw new ProjectBuildException("Builder " + builder.getDescription() + " requested build stop");
372 if (myCancelStatus.isCanceled()) {
373 throw new ProjectBuildException(CANCELED_MESSAGE);
375 if (buildResult == ModuleLevelBuilder.ExitCode.ADDITIONAL_PASS_REQUIRED) {
376 if (!nextPassRequired) {
378 myModulesProcessed -= (stagesPassed * modulesInChunk) / stageCount;
379 stageCount += myTotalBuilderCount;
380 myModulesProcessed += (stagesPassed * modulesInChunk) / stageCount;
382 nextPassRequired = true;
386 final float fraction = updateFractionBuilderFinished(modulesInChunk / (stageCount));
387 context.setDone(fraction);
390 while (nextPassRequired);
393 private static void syncOutputFiles(final CompileContext context, ModuleChunk chunk) throws ProjectBuildException {
394 final BuildDataManager dataManager = context.getDataManager();
395 final boolean compilingTests = context.isCompilingTests();
397 final Collection<String> allOutputs = new LinkedList<String>();
399 context.processFilesToRecompile(chunk, new FileProcessor() {
400 private final Map<Module, SourceToOutputMapping> storageMap = new HashMap<Module, SourceToOutputMapping>();
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);
409 final String srcPath = FileUtil.toSystemIndependentName(file.getPath());
410 final Collection<String> outputs = srcToOut.getState(srcPath);
412 if (outputs != null) {
413 for (String output : outputs) {
414 if (LOG.isDebugEnabled()) {
415 allOutputs.add(output);
417 FileUtil.delete(new File(output));
419 srcToOut.remove(srcPath);
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()];
430 for (String output : allOutputs) {
431 buffer[i++] = output;
434 for (String output : buffer) {
437 LOG.info("End of files");
441 catch (Exception e) {
442 throw new ProjectBuildException(e);