f0737f19321f956bac7ee3790de50f8dc30394e1
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / incremental / FSOperations.java
1 /*
2  * Copyright 2000-2012 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.jps.incremental;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Ref;
20 import com.intellij.openapi.util.io.FileUtil;
21 import com.intellij.util.containers.ContainerUtil;
22 import gnu.trove.THashSet;
23 import org.jetbrains.annotations.NotNull;
24 import org.jetbrains.annotations.Nullable;
25 import org.jetbrains.jps.ModuleChunk;
26 import org.jetbrains.jps.builders.*;
27 import org.jetbrains.jps.builders.impl.BuildTargetChunk;
28 import org.jetbrains.jps.builders.java.JavaBuilderUtil;
29 import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
30 import org.jetbrains.jps.cmdline.ProjectDescriptor;
31 import org.jetbrains.jps.incremental.fs.CompilationRound;
32 import org.jetbrains.jps.incremental.storage.StampsStorage;
33 import org.jetbrains.jps.model.java.JpsJavaClasspathKind;
34 import org.jetbrains.jps.model.java.JpsJavaExtensionService;
35 import org.jetbrains.jps.model.module.JpsModule;
36
37 import java.io.File;
38 import java.io.FileFilter;
39 import java.io.IOException;
40 import java.nio.file.*;
41 import java.nio.file.attribute.BasicFileAttributes;
42 import java.util.*;
43
44 /**
45  * @author Eugene Zhuravlev
46  */
47 public class FSOperations {
48   private static final Logger LOG = Logger.getInstance(FSOperations.class);
49   public static final GlobalContextKey<Set<File>> ALL_OUTPUTS_KEY = GlobalContextKey.create("_all_project_output_dirs_");
50   private static final GlobalContextKey<Set<BuildTarget<?>>> TARGETS_COMPLETELY_MARKED_DIRTY = GlobalContextKey.create("_targets_completely_marked_dirty_");
51
52   /**
53    * @param context
54    * @param round
55    * @param file
56    * @return true if file is marked as "dirty" in the specified compilation round
57    * @throws IOException
58    */
59   public static boolean isMarkedDirty(CompileContext context, final CompilationRound round, final File file) throws IOException {
60     final JavaSourceRootDescriptor rd = context.getProjectDescriptor().getBuildRootIndex().findJavaRootDescriptor(context, file);
61     if (rd != null) {
62       final ProjectDescriptor pd = context.getProjectDescriptor();
63       return pd.fsState.isMarkedForRecompilation(context, round, rd, file);
64     }
65     return false;
66   }
67
68   /**
69    * @deprecated use markDirty(CompileContext context, final CompilationRound round, final File file)
70    *
71    * Note: marked file will well be visible as "dirty" only on the <b>next</b> compilation round!
72    * @throws IOException
73    *
74    */
75   @Deprecated
76   public static void markDirty(CompileContext context, final File file) throws IOException {
77     markDirty(context, CompilationRound.NEXT, file);
78   }
79
80   public static void markDirty(CompileContext context, final CompilationRound round, final File file) throws IOException {
81     final JavaSourceRootDescriptor rd = context.getProjectDescriptor().getBuildRootIndex().findJavaRootDescriptor(context, file);
82     if (rd != null) {
83       final ProjectDescriptor pd = context.getProjectDescriptor();
84       pd.fsState.markDirty(context, round, file, rd, pd.getProjectStamps().getStampStorage(), false);
85     }
86   }
87
88   public static void markDirtyIfNotDeleted(CompileContext context, final CompilationRound round, final File file) throws IOException {
89     final JavaSourceRootDescriptor rd = context.getProjectDescriptor().getBuildRootIndex().findJavaRootDescriptor(context, file);
90     if (rd != null) {
91       final ProjectDescriptor pd = context.getProjectDescriptor();
92       pd.fsState.markDirtyIfNotDeleted(context, round, file, rd, pd.getProjectStamps().getStampStorage());
93     }
94   }
95
96   public interface DirtyFilesHolderBuilder<R extends BuildRootDescriptor, T extends BuildTarget<R>> {
97     /**
98      * Marks specified files dirty if the file is not deleted
99      * If the file was marked dirty as a result of this operation or had been already marked dirty,
100      * the file is stored internally in the builder
101      */
102     DirtyFilesHolderBuilder<R, T> markDirtyFile(T target, File file) throws IOException;
103
104     /**
105      * @return an object accumulating information about files marked with this builder
106      * Use returned object for further processing of marked files. For example, the object can be passed to
107      * {@link BuildOperations#cleanOutputsCorrespondingToChangedFiles(CompileContext, DirtyFilesHolder)}
108      * to clean outputs corresponding marked sources
109      */
110     DirtyFilesHolder<R, T> create();
111   }
112
113   /**
114    * @param context
115    * @param round desired compilation round at which these dirty marks should be visible
116    * @return a builder object that marks dirty files and collects data about files marked
117    */
118   public static <R extends BuildRootDescriptor, T extends BuildTarget<R>> DirtyFilesHolderBuilder<R, T> createDirtyFilesHolderBuilder(CompileContext context, final CompilationRound round) {
119     return new DirtyFilesHolderBuilder<R, T>() {
120       private final Map<T, Map<R, Set<File>>> dirtyFiles = new HashMap<>();
121       @Override
122       public DirtyFilesHolderBuilder<R, T> markDirtyFile(T target, File file) throws IOException {
123         final ProjectDescriptor pd = context.getProjectDescriptor();
124         final R rd = pd.getBuildRootIndex().findParentDescriptor(file, Collections.singleton(target.getTargetType()), context);
125         if (rd != null) {
126           if (pd.fsState.markDirtyIfNotDeleted(context, round, file, rd, pd.getProjectStamps().getStampStorage()) || pd.fsState.isMarkedForRecompilation(context, round, rd, file)) {
127             Map<R, Set<File>> targetFiles = dirtyFiles.get(target);
128             if (targetFiles == null) {
129               targetFiles = new HashMap<>();
130               dirtyFiles.put(target, targetFiles);
131             }
132             Set<File> rootFiles = targetFiles.get(rd);
133             if (rootFiles == null) {
134               rootFiles = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
135               targetFiles.put(rd, rootFiles);
136             }
137             rootFiles.add(file);
138           }
139         }
140         return this;
141       }
142
143       @Override
144       public DirtyFilesHolder<R, T> create() {
145         return new DirtyFilesHolder<R, T>() {
146           @Override
147           public void processDirtyFiles(@NotNull FileProcessor<R, T> processor) throws IOException {
148             for (Map.Entry<T, Map<R, Set<File>>> entry : dirtyFiles.entrySet()) {
149               final T target = entry.getKey();
150               for (Map.Entry<R, Set<File>>  targetEntry: entry.getValue().entrySet()) {
151                 final R rd = targetEntry.getKey();
152                 for (File file : targetEntry.getValue()) {
153                   processor.apply(target, file, rd);
154                 }
155               }
156             }
157           }
158
159           @Override
160           public boolean hasDirtyFiles() {
161             return !dirtyFiles.isEmpty();
162           }
163
164           @Override
165           public boolean hasRemovedFiles() {
166             return false;
167           }
168
169           @Override
170           public @NotNull Collection<String> getRemovedFiles(@NotNull T target) {
171             return Collections.emptyList();
172           }
173         };
174       }
175     };
176   }
177
178   public static void markDeleted(CompileContext context, File file) throws IOException {
179     final JavaSourceRootDescriptor rd = context.getProjectDescriptor().getBuildRootIndex().findJavaRootDescriptor(context, file);
180     if (rd != null) {
181       final ProjectDescriptor pd = context.getProjectDescriptor();
182       pd.fsState.registerDeleted(context, rd.target, file, pd.getProjectStamps().getStampStorage());
183     }
184   }
185
186   public static void markDirty(CompileContext context, final CompilationRound round, final ModuleChunk chunk, @Nullable FileFilter filter) throws IOException {
187     for (ModuleBuildTarget target : chunk.getTargets()) {
188       markDirty(context, round, target, filter);
189     }
190   }
191
192   public static void markDirty(CompileContext context, final CompilationRound round, final ModuleBuildTarget target, @Nullable FileFilter filter) throws IOException {
193     final ProjectDescriptor pd = context.getProjectDescriptor();
194     markDirtyFiles(context, target, round, pd.getProjectStamps().getStampStorage(), true, null, filter);
195   }
196
197   public static void markDirtyRecursively(CompileContext context, final CompilationRound round, ModuleChunk chunk) throws IOException {
198     markDirtyRecursively(context, round, chunk, null);
199   }
200
201   public static void markDirtyRecursively(CompileContext context, final CompilationRound round, ModuleChunk chunk, @Nullable FileFilter filter) throws IOException {
202     Set<JpsModule> modules = chunk.getModules();
203     Set<ModuleBuildTarget> targets = chunk.getTargets();
204     final Set<ModuleBuildTarget> dirtyTargets = new HashSet<>(targets);
205
206     // now mark all modules that depend on dirty modules
207     final JpsJavaClasspathKind classpathKind = JpsJavaClasspathKind.compile(chunk.containsTests());
208     boolean found = false;
209     for (BuildTargetChunk targetChunk : context.getProjectDescriptor().getBuildTargetIndex().getSortedTargetChunks(context)) {
210       if (!found) {
211         if (targetChunk.getTargets().equals(chunk.getTargets())) {
212           found = true;
213         }
214       }
215       else {
216         for (final BuildTarget<?> target : targetChunk.getTargets()) {
217           if (target instanceof ModuleBuildTarget) {
218             final Set<JpsModule> deps = getDependentModulesRecursively(((ModuleBuildTarget)target).getModule(), classpathKind);
219             if (ContainerUtil.intersects(deps, modules)) {
220               for (BuildTarget<?> buildTarget : targetChunk.getTargets()) {
221                 if (buildTarget instanceof ModuleBuildTarget) {
222                   dirtyTargets.add((ModuleBuildTarget)buildTarget);
223                 }
224               }
225               break;
226             }
227           }
228         }
229       }
230     }
231
232     if (JavaBuilderUtil.isCompileJavaIncrementally(context)) {
233       // mark as non-incremental only the module that triggered non-incremental change
234       for (ModuleBuildTarget target : targets) {
235         if (!isMarkedDirty(context, target)) {
236           // if the target was marked dirty already, all its files were compiled, so
237           // it makes no sense to mark it non-incremental
238           context.markNonIncremental(target);
239         }
240       }
241     }
242
243     removeTargetsAlreadyMarkedDirty(context, dirtyTargets);
244
245     final StampsStorage<? extends StampsStorage.Stamp> stampsStorage = context.getProjectDescriptor().getProjectStamps().getStampStorage();
246     for (ModuleBuildTarget target : dirtyTargets) {
247       markDirtyFiles(context, target, round, stampsStorage, true, null, filter);
248     }
249
250   }
251
252   private static Set<JpsModule> getDependentModulesRecursively(final JpsModule module, final JpsJavaClasspathKind kind) {
253     return JpsJavaExtensionService.dependencies(module).includedIn(kind).recursivelyExportedOnly().getModules();
254   }
255
256   public static void processFilesToRecompile(CompileContext context, ModuleChunk chunk, FileProcessor<JavaSourceRootDescriptor, ? super ModuleBuildTarget> processor) throws IOException {
257     for (ModuleBuildTarget target : chunk.getTargets()) {
258       processFilesToRecompile(context, target, processor);
259     }
260   }
261
262   public static void processFilesToRecompile(CompileContext context, @NotNull ModuleBuildTarget target, FileProcessor<JavaSourceRootDescriptor, ? super ModuleBuildTarget> processor) throws IOException {
263     context.getProjectDescriptor().fsState.processFilesToRecompile(context, target, processor);
264   }
265
266   static void markDirtyFiles(CompileContext context,
267                              BuildTarget<?> target,
268                              final CompilationRound round,
269                              StampsStorage<? extends StampsStorage.Stamp> stampsStorage,
270                              boolean forceMarkDirty,
271                              @Nullable Set<? super File> currentFiles,
272                              @Nullable FileFilter filter) throws IOException {
273     boolean completelyMarkedDirty = true;
274     for (BuildRootDescriptor rd : context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context)) {
275       if (!rd.getRootFile().exists() ||
276           //temp roots are managed by compilers themselves
277           (rd instanceof JavaSourceRootDescriptor && ((JavaSourceRootDescriptor)rd).isTemp)) {
278         continue;
279       }
280       if (filter == null) {
281         context.getProjectDescriptor().fsState.clearRecompile(rd);
282       }
283       //final FSCache fsCache = rd.canUseFileCache() ? context.getProjectDescriptor().getFSCache() : FSCache.NO_CACHE;
284       completelyMarkedDirty &= traverseRecursively(context, rd, round, rd.getRootFile(), stampsStorage, forceMarkDirty, currentFiles, filter);
285     }
286
287     if (completelyMarkedDirty) {
288       addCompletelyMarkedDirtyTarget(context, target);
289     }
290   }
291
292   /**
293    * Marks changed files under {@code file} as dirty.
294    * @return {@code true} if all compilable files were marked dirty and {@code false} if some of them were skipped because they weren't accepted
295    * by {@code filter} or wasn't modified
296    */
297   private static boolean traverseRecursively(CompileContext context,
298                                              final BuildRootDescriptor rd,
299                                              final CompilationRound round,
300                                              final File file,
301                                              @NotNull final StampsStorage<? extends StampsStorage.Stamp> stampStorage,
302                                              final boolean forceDirty,
303                                              @Nullable Set<? super File> currentFiles, @Nullable FileFilter filter) throws IOException {
304
305     final BuildRootIndex rootIndex = context.getProjectDescriptor().getBuildRootIndex();
306     final Ref<Boolean> allFilesMarked = Ref.create(Boolean.TRUE);
307
308     Files.walkFileTree(file.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
309       @Override
310       public FileVisitResult visitFileFailed(Path file, IOException e) throws IOException {
311         if (e instanceof FileSystemLoopException) {
312           LOG.info(e);
313           // in some cases (e.g. Google Drive File Stream) loop detection for directories works incorrectly
314           // fallback: try to traverse in the old IO-way
315           final boolean marked = traverseRecursivelyIO(context, rd, round, file.toFile(), stampStorage, forceDirty, currentFiles, filter);
316           if (!marked) {
317             allFilesMarked.set(Boolean.FALSE);
318           }
319           return FileVisitResult.SKIP_SUBTREE;
320         }
321         return super.visitFileFailed(file, e);
322       }
323
324       @Override
325       public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
326         return rootIndex.isDirectoryAccepted(dir.toFile(), rd)? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
327       }
328
329       @Override
330       public FileVisitResult visitFile(Path f, BasicFileAttributes attrs) throws IOException {
331         final File _file = f.toFile();
332         if (!rootIndex.isFileAccepted(_file, rd)) { // ignored file
333           return FileVisitResult.CONTINUE;
334         }
335         if (filter != null && !filter.accept(_file)) {
336           allFilesMarked.set(Boolean.FALSE);
337         }
338         else {
339           boolean markDirty = forceDirty;
340           if (!markDirty) {
341             markDirty = stampStorage.isDirtyStamp(stampStorage.getPreviousStamp(_file, rd.getTarget()), _file, attrs);
342           }
343           if (markDirty) {
344             // if it is full project rebuild, all storages are already completely cleared;
345             // so passing null because there is no need to access the storage to clear non-existing data
346             final StampsStorage<? extends StampsStorage.Stamp> marker = context.isProjectRebuild() ? null : stampStorage;
347             context.getProjectDescriptor().fsState.markDirty(context, round, _file, rd, marker, false);
348           }
349           if (currentFiles != null) {
350             currentFiles.add(_file);
351           }
352           if (!markDirty) {
353             allFilesMarked.set(Boolean.FALSE);
354           }
355         }
356         return FileVisitResult.CONTINUE;
357       }
358
359     });
360
361     return allFilesMarked.get();
362   }
363
364   private static boolean traverseRecursivelyIO(CompileContext context,
365                                              final BuildRootDescriptor rd,
366                                              final CompilationRound round,
367                                              final File file,
368                                              @NotNull final StampsStorage<? extends StampsStorage.Stamp> stampsStorage,
369                                              final boolean forceDirty,
370                                              @Nullable Set<? super File> currentFiles, @Nullable FileFilter filter) throws IOException {
371     BuildRootIndex rootIndex = context.getProjectDescriptor().getBuildRootIndex();
372     final File[] children = file.listFiles();
373     if (children != null) { // is directory
374       boolean allMarkedDirty = true;
375       if (children.length > 0 && rootIndex.isDirectoryAccepted(file, rd)) {
376         for (File child : children) {
377           allMarkedDirty &= traverseRecursivelyIO(context, rd, round, child, stampsStorage, forceDirty, currentFiles, filter);
378         }
379       }
380       return allMarkedDirty;
381     }
382     // is file
383
384     if (!rootIndex.isFileAccepted(file, rd)) {
385       return true;
386     }
387     if (filter != null && !filter.accept(file)) {
388       return false;
389     }
390
391     boolean markDirty = forceDirty;
392     if (!markDirty) {
393       markDirty = stampsStorage.isDirtyStamp(stampsStorage.getPreviousStamp(file, rd.getTarget()), file);
394     }
395     if (markDirty) {
396       // if it is full project rebuild, all storages are already completely cleared;
397       // so passing null because there is no need to access the storage to clear non-existing data
398       final StampsStorage<? extends StampsStorage.Stamp> marker = context.isProjectRebuild() ? null : stampsStorage;
399       context.getProjectDescriptor().fsState.markDirty(context, round, file, rd, marker, false);
400     }
401     if (currentFiles != null) {
402       currentFiles.add(file);
403     }
404     return markDirty;
405   }
406
407   public static void pruneEmptyDirs(CompileContext context, @Nullable final Set<File> dirsToDelete) {
408     if (dirsToDelete == null || dirsToDelete.isEmpty()) return;
409
410     Set<File> doNotDelete = ALL_OUTPUTS_KEY.get(context);
411     if (doNotDelete == null) {
412       doNotDelete = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
413       for (BuildTarget<?> target : context.getProjectDescriptor().getBuildTargetIndex().getAllTargets()) {
414         doNotDelete.addAll(target.getOutputRoots(context));
415       }
416       ALL_OUTPUTS_KEY.set(context, doNotDelete);
417     }
418
419     Set<File> additionalDirs = null;
420     Set<File> toDelete = dirsToDelete;
421     while (toDelete != null) {
422       for (File file : toDelete) {
423         // important: do not force deletion if the directory is not empty!
424         final boolean deleted = !doNotDelete.contains(file) && file.delete();
425         if (deleted) {
426           final File parentFile = file.getParentFile();
427           if (parentFile != null) {
428             if (additionalDirs == null) {
429               additionalDirs = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
430             }
431             additionalDirs.add(parentFile);
432           }
433         }
434       }
435       toDelete = additionalDirs;
436       additionalDirs = null;
437     }
438   }
439
440   public static boolean isMarkedDirty(CompileContext context, ModuleChunk chunk) {
441     synchronized (TARGETS_COMPLETELY_MARKED_DIRTY) {
442       Set<BuildTarget<?>> marked = TARGETS_COMPLETELY_MARKED_DIRTY.get(context);
443       return marked != null && marked.containsAll(chunk.getTargets());
444     }
445   }
446
447   public static long lastModified(File file) {
448     return lastModified(file.toPath());
449   }
450
451   private static long lastModified(Path path) {
452     try {
453       return Files.getLastModifiedTime(path).toMillis();
454     }
455     catch (IOException e) {
456       LOG.warn(e);
457     }
458     return 0L;
459   }
460
461   public static void copy(File fromFile, File toFile) throws IOException {
462     final Path from = fromFile.toPath();
463     final Path to = toFile.toPath();
464     try {
465       try {
466         Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
467       }
468       catch (AccessDeniedException e) {
469         if (!Files.isWritable(to) && toFile.setWritable(true)) {
470           Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); // repeat once the file seems to be writable again
471         }
472         else {
473           throw e;
474         }
475       }
476       catch (NoSuchFileException e) {
477         final File parent = toFile.getParentFile();
478         if (parent != null && parent.mkdirs()) {
479           Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); // repeat on successful target dir creation
480         }
481         else {
482           throw e;
483         }
484       }
485     }
486     catch (IOException e) {
487       // fallback: trying 'classic' copying via streams
488       LOG.info("Error copying "+ fromFile.getPath() + " to " + toFile.getPath() + " with NIO API", e);
489       FileUtil.copyContent(fromFile, toFile);
490     }
491   }
492
493   public static boolean isMarkedDirty(CompileContext context, BuildTarget<?> target) {
494     synchronized (TARGETS_COMPLETELY_MARKED_DIRTY) {
495       Set<BuildTarget<?>> marked = TARGETS_COMPLETELY_MARKED_DIRTY.get(context);
496       return marked != null && marked.contains(target);
497     }
498   }
499
500   private static void addCompletelyMarkedDirtyTarget(CompileContext context, BuildTarget<?> target) {
501     synchronized (TARGETS_COMPLETELY_MARKED_DIRTY) {
502       Set<BuildTarget<?>> marked = TARGETS_COMPLETELY_MARKED_DIRTY.get(context);
503       if (marked == null) {
504         marked = new HashSet<>();
505         TARGETS_COMPLETELY_MARKED_DIRTY.set(context, marked);
506       }
507       marked.add(target);
508     }
509   }
510
511   private static void removeTargetsAlreadyMarkedDirty(CompileContext context, Set<ModuleBuildTarget> targetsSetToFilter) {
512     synchronized (TARGETS_COMPLETELY_MARKED_DIRTY) {
513       Set<BuildTarget<?>> marked = TARGETS_COMPLETELY_MARKED_DIRTY.get(context);
514       if (marked != null) {
515         targetsSetToFilter.removeAll(marked);
516       }
517     }
518   }
519 }