/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.jps.builders.java; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.containers.ContainerUtil; import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jps.ModuleChunk; import org.jetbrains.jps.builders.BuildRootIndex; import org.jetbrains.jps.builders.BuildTarget; import org.jetbrains.jps.builders.BuildTargetIndex; import org.jetbrains.jps.builders.DirtyFilesHolder; import org.jetbrains.jps.builders.java.dependencyView.Callbacks; import org.jetbrains.jps.builders.java.dependencyView.Mappings; import org.jetbrains.jps.builders.storage.BuildDataCorruptedException; import org.jetbrains.jps.incremental.*; import org.jetbrains.jps.incremental.fs.CompilationRound; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; import org.jetbrains.jps.incremental.messages.ProgressMessage; import org.jetbrains.jps.model.JpsDummyElement; import org.jetbrains.jps.model.java.JpsJavaSdkType; import org.jetbrains.jps.model.library.JpsTypedLibrary; import org.jetbrains.jps.model.library.sdk.JpsSdk; import org.jetbrains.jps.model.library.sdk.JpsSdkReference; import org.jetbrains.jps.model.module.JpsModule; import org.jetbrains.jps.service.JpsServiceManager; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.*; /** * @author nik */ public class JavaBuilderUtil { public static final Key<Callbacks.ConstantAffectionResolver> CONSTANT_SEARCH_SERVICE = Key.create("_constant_search_service_"); private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.Builder"); private static final Key<Set<File>> ALL_AFFECTED_FILES_KEY = Key.create("_all_affected_files_"); private static final Key<Set<File>> ALL_COMPILED_FILES_KEY = Key.create("_all_compiled_files_"); private static final Key<Set<File>> FILES_TO_COMPILE_KEY = Key.create("_files_to_compile_"); private static final Key<Set<File>> COMPILED_WITH_ERRORS_KEY = Key.create("_compiled_with_errors_"); private static final Key<Set<File>> SUCCESSFULLY_COMPILED_FILES_KEY = Key.create("_successfully_compiled_files_"); private static final Key<List<FileFilter>> SKIP_MARKING_DIRTY_FILTERS_KEY = Key.create("_skip_marking_dirty_filters_"); private static final Key<Pair<Mappings, Callbacks.Backend>> MAPPINGS_DELTA_KEY = Key.create("_mappings_delta_"); public static void registerFileToCompile(CompileContext context, File file) { registerFilesToCompile(context, Collections.singleton(file)); } public static void registerFilesToCompile(CompileContext context, Collection<File> files) { getFilesContainer(context, FILES_TO_COMPILE_KEY).addAll(files); } public static void registerFilesWithErrors(CompileContext context, Collection<File> files) { getFilesContainer(context, COMPILED_WITH_ERRORS_KEY).addAll(files); } public static void registerSuccessfullyCompiled(CompileContext context, File file) { registerSuccessfullyCompiled(context, Collections.singleton(file)); } public static void registerSuccessfullyCompiled(CompileContext context, Collection<File> files) { getFilesContainer(context, SUCCESSFULLY_COMPILED_FILES_KEY).addAll(files); } /** * The files accepted by {@code filter} won't be marked dirty by {@link #updateMappings} method when this compilation round finishes. * Use this method to register a filter accepting files of your language if you compute and mark as dirty affected files yourself in your builder. */ public static void registerFilterToSkipMarkingAffectedFileDirty(@NotNull CompileContext context, @NotNull FileFilter filter) { List<FileFilter> filters = SKIP_MARKING_DIRTY_FILTERS_KEY.get(context); if (filters == null) { SKIP_MARKING_DIRTY_FILTERS_KEY.set(context, filters = new ArrayList<>()); } filters.add(filter); } @NotNull public static Callbacks.Backend getDependenciesRegistrar(CompileContext context) { Pair<Mappings, Callbacks.Backend> pair = MAPPINGS_DELTA_KEY.get(context); if (pair == null) { final Mappings delta = context.getProjectDescriptor().dataManager.getMappings().createDelta(); pair = Pair.create(delta, delta.getCallback()); MAPPINGS_DELTA_KEY.set(context, pair); } return pair.second; } public static boolean updateMappingsOnRoundCompletion( CompileContext context, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, ModuleChunk chunk) throws IOException { Mappings delta = null; final Pair<Mappings, Callbacks.Backend> pair = MAPPINGS_DELTA_KEY.get(context); if (pair != null) { MAPPINGS_DELTA_KEY.set(context, null); delta = pair.getFirst(); } if (delta == null) { return false; } final Set<File> compiledFiles = getFilesContainer(context, FILES_TO_COMPILE_KEY); FILES_TO_COMPILE_KEY.set(context, null); final Set<File> successfullyCompiled = getFilesContainer(context, SUCCESSFULLY_COMPILED_FILES_KEY); SUCCESSFULLY_COMPILED_FILES_KEY.set(context, null); FileFilter filter = createOrFilter(SKIP_MARKING_DIRTY_FILTERS_KEY.get(context)); SKIP_MARKING_DIRTY_FILTERS_KEY.set(context, null); return updateMappings(context, delta, dirtyFilesHolder, chunk, compiledFiles, successfullyCompiled, CompilationRound.NEXT, filter); } /** * @deprecated this method isn't supposed to be called by plugins anymore, the mappings are updated * by the build process infrastructure automatically. Use {@link #getDependenciesRegistrar(CompileContext)}, * {@link #registerFilesToCompile(CompileContext, Collection)}, or * {@link #registerSuccessfullyCompiled(CompileContext, Collection)} instead. */ public static boolean updateMappings(CompileContext context, final Mappings delta, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, ModuleChunk chunk, Collection<File> filesToCompile, Collection<File> successfullyCompiled) throws IOException { return updateMappings(context, delta, dirtyFilesHolder, chunk, filesToCompile, successfullyCompiled, CompilationRound.NEXT, null); } public static void markDirtyDependenciesForInitialRound(CompileContext context, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dfh, ModuleChunk chunk) throws IOException { if (hasRemovedPaths(chunk, dfh)) { final Mappings delta = context.getProjectDescriptor().dataManager.getMappings().createDelta(); final Set<File> empty = Collections.emptySet(); updateMappings(context, delta, dfh, chunk, empty, empty, CompilationRound.CURRENT, null); } } /** * @param filesToCompile files compiled in this round * @param markDirtyRound compilation round at which dirty files should be visible to builders * @return true if additional compilation pass is required, false otherwise * @throws Exception */ private static boolean updateMappings(CompileContext context, final Mappings delta, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, ModuleChunk chunk, Collection<File> filesToCompile, Collection<File> successfullyCompiled, final CompilationRound markDirtyRound, @Nullable FileFilter skipMarkingDirtyFilter) throws IOException { try { boolean performIntegrate = true; boolean additionalPassRequired = false; final Set<String> removedPaths = getRemovedPaths(chunk, dirtyFilesHolder); final Mappings globalMappings = context.getProjectDescriptor().dataManager.getMappings(); final boolean errorsDetected = Utils.errorsDetected(context); if (!isForcedRecompilationAllJavaModules(context)) { if (context.shouldDifferentiate(chunk)) { context.processMessage(new ProgressMessage("Checking dependencies... [" + chunk.getPresentableShortName() + "]")); final Set<File> allCompiledFiles = getFilesContainer(context, ALL_COMPILED_FILES_KEY); final Set<File> allAffectedFiles = getFilesContainer(context, ALL_AFFECTED_FILES_KEY); // mark as affected all files that were dirty before compilation allAffectedFiles.addAll(filesToCompile); // accumulate all successfully compiled in this round allCompiledFiles.addAll(successfullyCompiled); // unmark as affected all successfully compiled allAffectedFiles.removeAll(successfullyCompiled); final Set<File> affectedBeforeDif = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY); affectedBeforeDif.addAll(allAffectedFiles); final Set<File> compiledWithErrors = getFilesContainer(context, COMPILED_WITH_ERRORS_KEY); COMPILED_WITH_ERRORS_KEY.set(context, null); final ModulesBasedFileFilter moduleBasedFilter = new ModulesBasedFileFilter(context, chunk); final boolean incremental = globalMappings.differentiateOnIncrementalMake( delta, removedPaths, filesToCompile, compiledWithErrors, allCompiledFiles, allAffectedFiles, moduleBasedFilter, CONSTANT_SEARCH_SERVICE.get(context) ); if (LOG.isDebugEnabled()) { LOG.debug("Differentiate Results:"); LOG.debug(" Compiled Files:"); for (final File c : allCompiledFiles) { LOG.debug(" " + c.getAbsolutePath()); } LOG.debug(" Affected Files:"); for (final File c : allAffectedFiles) { LOG.debug(" " + c.getAbsolutePath()); } LOG.debug("End Of Differentiate Results."); } if (incremental) { final Set<File> newlyAffectedFiles = new HashSet<>(allAffectedFiles); newlyAffectedFiles.removeAll(affectedBeforeDif); final String infoMessage = "Dependency analysis found " + newlyAffectedFiles.size() + " affected files"; LOG.info(infoMessage); context.processMessage(new ProgressMessage(infoMessage)); removeFilesAcceptedByFilter(newlyAffectedFiles, skipMarkingDirtyFilter); if (!newlyAffectedFiles.isEmpty()) { if (LOG.isDebugEnabled()) { for (File file : newlyAffectedFiles) { LOG.debug("affected file: " + file.getPath()); } final List<Pair<File, JpsModule>> wrongFiles = checkAffectedFilesInCorrectModules(context, newlyAffectedFiles, moduleBasedFilter); if (!wrongFiles.isEmpty()) { LOG.debug("Wrong affected files for module chunk " + chunk.getName() + ": "); for (Pair<File, JpsModule> pair : wrongFiles) { final String name = pair.second != null ? pair.second.getName() : "null"; LOG.debug("\t[" + name + "] " + pair.first.getPath()); } } } for (File file : newlyAffectedFiles) { FSOperations.markDirtyIfNotDeleted(context, markDirtyRound, file); } additionalPassRequired = isCompileJavaIncrementally(context) && chunkContainsAffectedFiles(context, chunk, newlyAffectedFiles); } } else { // non-incremental mode final String messageText = "Marking " + chunk.getPresentableShortName() + " and direct dependants for recompilation"; LOG.info("Non-incremental mode: " + messageText); context.processMessage(new ProgressMessage(messageText)); final boolean alreadyMarkedDirty = FSOperations.isMarkedDirty(context, chunk); additionalPassRequired = isCompileJavaIncrementally(context) && !alreadyMarkedDirty; if (alreadyMarkedDirty) { // need this to make sure changes data stored in Delta is complete globalMappings.differentiateOnNonIncrementalMake(delta, removedPaths, filesToCompile); } else { performIntegrate = false; } FileFilter toBeMarkedFilter = skipMarkingDirtyFilter == null ? null : new NegationFileFilter(skipMarkingDirtyFilter); FSOperations.markDirtyRecursively(context, markDirtyRound, chunk, toBeMarkedFilter); } } else { if (!errorsDetected) { // makes sense only if we are going to integrate changes globalMappings.differentiateOnNonIncrementalMake(delta, removedPaths, filesToCompile); } } } else { if (!errorsDetected) { // makes sense only if we are going to integrate changes globalMappings.differentiateOnRebuild(delta); } } if (errorsDetected) { // important: perform dependency analysis and mark found dependencies even if there were errors during the first phase of make. // Integration of changes should happen only if the corresponding phase of make succeeds // In case of errors this wil ensure that all dependencies marked after the first phase // will be compiled during the first phase of the next make return false; } if (performIntegrate) { context.processMessage(new ProgressMessage("Updating dependency information... [" + chunk.getPresentableShortName() + "]")); globalMappings.integrate(delta); } return additionalPassRequired; } catch (BuildDataCorruptedException e) { throw e.getCause(); } finally { context.processMessage(new ProgressMessage("")); // clean progress messages } } private static FileFilter createOrFilter(final List<FileFilter> filters) { if (filters == null || filters.isEmpty()) return null; return pathname -> { for (FileFilter filter : filters) { if (filter.accept(pathname)) { return true; } } return false; }; } private static void removeFilesAcceptedByFilter(@NotNull Set<File> files, @Nullable FileFilter filter) { if (filter == null) return; Iterator<File> iterator = files.iterator(); while (iterator.hasNext()) { File next = iterator.next(); if (filter.accept(next)) { iterator.remove(); } } } public static boolean isForcedRecompilationAllJavaModules(CompileContext context) { CompileScope scope = context.getScope(); return scope.isBuildForcedForAllTargets(JavaModuleBuildTargetType.PRODUCTION) && scope.isBuildForcedForAllTargets(JavaModuleBuildTargetType.TEST); } public static boolean isCompileJavaIncrementally(CompileContext context) { CompileScope scope = context.getScope(); return scope.isBuildIncrementally(JavaModuleBuildTargetType.PRODUCTION) || scope.isBuildIncrementally(JavaModuleBuildTargetType.TEST); } private static List<Pair<File, JpsModule>> checkAffectedFilesInCorrectModules(CompileContext context, Collection<File> affected, ModulesBasedFileFilter moduleBasedFilter) { if (affected.isEmpty()) { return Collections.emptyList(); } final List<Pair<File, JpsModule>> result = new ArrayList<>(); for (File file : affected) { if (!moduleBasedFilter.accept(file)) { final JavaSourceRootDescriptor moduleAndRoot = context.getProjectDescriptor().getBuildRootIndex().findJavaRootDescriptor(context, file); result.add(Pair.create(file, moduleAndRoot != null ? moduleAndRoot.target.getModule() : null)); } } return result; } private static boolean chunkContainsAffectedFiles(CompileContext context, ModuleChunk chunk, final Set<File> affected) throws IOException { final Set<JpsModule> chunkModules = chunk.getModules(); if (!chunkModules.isEmpty()) { for (File file : affected) { final JavaSourceRootDescriptor moduleAndRoot = context.getProjectDescriptor().getBuildRootIndex().findJavaRootDescriptor(context, file); if (moduleAndRoot != null && chunkModules.contains(moduleAndRoot.target.getModule())) { return true; } } } return false; } @NotNull private static Set<File> getFilesContainer(CompileContext context, final Key<Set<File>> dataKey) { Set<File> files = dataKey.get(context); if (files == null) { files = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY); dataKey.set(context, files); } return files; } private static Set<String> getRemovedPaths(ModuleChunk chunk, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder) { if (!dirtyFilesHolder.hasRemovedFiles()) { return Collections.emptySet(); } final Set<String> removed = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY); for (ModuleBuildTarget target : chunk.getTargets()) { removed.addAll(dirtyFilesHolder.getRemovedFiles(target)); } return removed; } private static boolean hasRemovedPaths(ModuleChunk chunk, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder) { if (dirtyFilesHolder.hasRemovedFiles()) { for (ModuleBuildTarget target : chunk.getTargets()) { if (!dirtyFilesHolder.getRemovedFiles(target).isEmpty()) { return true; } } } return false; } public static void cleanupChunkResources(CompileContext context) { ALL_AFFECTED_FILES_KEY.set(context, null); ALL_COMPILED_FILES_KEY.set(context, null); } @NotNull public static JpsSdk<JpsDummyElement> ensureModuleHasJdk(JpsModule module, CompileContext context, final String compilerName) throws ProjectBuildException { JpsSdkReference<JpsDummyElement> reference = module.getSdkReference(JpsJavaSdkType.INSTANCE); if (reference == null) { context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, "JDK isn't specified for module '" + module.getName() + "'")); throw new StopBuildException(); } JpsTypedLibrary<JpsSdk<JpsDummyElement>> sdkLibrary = reference.resolve(); if (sdkLibrary == null) { context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, "Cannot find JDK '" + reference.getSdkName() + "' for module '" + module.getName() + "'")); throw new StopBuildException(); } return sdkLibrary.getProperties(); } @Nullable public static JavaCompilingTool findCompilingTool(@NotNull String compilerId) { for (JavaCompilingTool tool : JpsServiceManager.getInstance().getExtensions(JavaCompilingTool.class)) { if (compilerId.equals(tool.getId()) || compilerId.equals(tool.getAlternativeId())) { return tool; } } return null; } private static class ModulesBasedFileFilter implements Mappings.DependentFilesFilter { private final CompileContext myContext; private final Set<? extends BuildTarget<?>> myChunkTargets; private final Map<BuildTarget<?>, Set<BuildTarget<?>>> myCache = new HashMap<>(); private final BuildRootIndex myBuildRootIndex; private final BuildTargetIndex myBuildTargetIndex; private ModulesBasedFileFilter(CompileContext context, ModuleChunk chunk) { myContext = context; myChunkTargets = chunk.getTargets(); myBuildRootIndex = context.getProjectDescriptor().getBuildRootIndex(); myBuildTargetIndex = context.getProjectDescriptor().getBuildTargetIndex(); } @Override public boolean accept(File file) { final JavaSourceRootDescriptor rd = myBuildRootIndex.findJavaRootDescriptor(myContext, file); if (rd == null) { return true; } final ModuleBuildTarget targetOfFile = rd.target; if (myChunkTargets.contains(targetOfFile)) { return true; } Set<BuildTarget<?>> targetOfFileWithDependencies = myCache.get(targetOfFile); if (targetOfFileWithDependencies == null) { targetOfFileWithDependencies = myBuildTargetIndex.getDependenciesRecursively(targetOfFile, myContext); myCache.put(targetOfFile, targetOfFileWithDependencies); } return ContainerUtil.intersects(targetOfFileWithDependencies, myChunkTargets); } @Override public boolean belongsToCurrentTargetChunk(File file) { final JavaSourceRootDescriptor rd = myBuildRootIndex.findJavaRootDescriptor(myContext, file); return rd != null && myChunkTargets.contains(rd.target); } } private static class NegationFileFilter implements FileFilter { private final FileFilter myFilter; public NegationFileFilter(FileFilter filter) { myFilter = filter; } @Override public boolean accept(File pathname) { return !myFilter.accept(pathname); } } }