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