diff --git a/subprojects/language-groovy/src/main/java/org/gradle/api/tasks/compile/GroovyCompile.java b/subprojects/language-groovy/src/main/java/org/gradle/api/tasks/compile/GroovyCompile.java index e0288b5e5d3a..171d200d73d9 100644 --- a/subprojects/language-groovy/src/main/java/org/gradle/api/tasks/compile/GroovyCompile.java +++ b/subprojects/language-groovy/src/main/java/org/gradle/api/tasks/compile/GroovyCompile.java @@ -262,6 +262,7 @@ private GroovyJavaJointCompileSpec createSpec() { spec.setSourcesRoots(sourceRoots); spec.setSourceFiles(stableSourcesAsFileTree); spec.setDestinationDir(getDestinationDirectory().getAsFile().get()); + spec.setOriginalDestinationDir(spec.getDestinationDir()); spec.setWorkingDir(getProjectLayout().getProjectDirectory().getAsFile()); spec.setTempDir(getTemporaryDir()); spec.setCompileClasspath(ImmutableList.copyOf(determineGroovyCompileClasspath())); diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/BaseIncrementalCompilationAfterFailureIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/BaseIncrementalCompilationAfterFailureIntegrationTest.groovy index 7f212816706f..887f0e9471d2 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/BaseIncrementalCompilationAfterFailureIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/BaseIncrementalCompilationAfterFailureIntegrationTest.groovy @@ -19,6 +19,8 @@ package org.gradle.java.compile.incremental import org.gradle.api.internal.cache.StringInterner import org.gradle.api.internal.tasks.compile.incremental.recomp.PreviousCompilationAccess import org.gradle.integtests.fixtures.CompiledLanguage +import org.gradle.util.Requires +import org.gradle.util.TestPrecondition import spock.lang.Issue import static org.junit.Assume.assumeFalse @@ -222,6 +224,56 @@ abstract class BaseIncrementalCompilationAfterFailureIntegrationTest extends Abs class JavaIncrementalCompilationAfterFailureIntegrationTest extends BaseIncrementalCompilationAfterFailureIntegrationTest { CompiledLanguage language = CompiledLanguage.JAVA + + @Requires(TestPrecondition.JDK9_OR_LATER) + def "incremental compilation after failure works with modules #description"() { + file("impl/build.gradle") << """ + def layout = project.layout + tasks.compileJava { + modularity.inferModulePath = $inferModulePath + options.compilerArgs.addAll($compileArgs) + doFirst { + $doFirst + } + } + """ + source "package a; import b.B; public class A {}", + "package b; public class B {}", + "package c; public class C {}" + file("src/main/${language.name}/module-info.${language.name}").text = """ + module impl { + exports a; + exports b; + exports c; + } + """ + succeeds language.compileTaskName + outputs.recompiledClasses("A", "B", "C", "module-info") + + when: + outputs.snapshot { + source "package a; import b.B; public class A { void m1() {}; }", + "package b; import a.A; public class B { A m1() { return new B(); } }" + } + + then: + fails language.compileTaskName + + when: + outputs.snapshot { + source "package a; import b.B; public class A { void m1() {}; }", + "package b; import a.A; public class B { A m1() { return new A(); } }" + } + succeeds language.compileTaskName + + then: + outputs.recompiledClasses("A", "B", "module-info") + + where: + description | inferModulePath | compileArgs | doFirst + "with inferred module-path" | "true" | "[]" | "" + "with manual module-path" | "false" | "[\"--module-path=\${classpath.join(File.pathSeparator)}\"]" | "classpath = layout.files()" + } } class GroovyIncrementalCompilationAfterFailureIntegrationTest extends BaseIncrementalCompilationAfterFailureIntegrationTest { diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/CrossTaskIncrementalJavaCompilationIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/CrossTaskIncrementalJavaCompilationIntegrationTest.groovy index 59ea4ad43798..26340b051a90 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/CrossTaskIncrementalJavaCompilationIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/CrossTaskIncrementalJavaCompilationIntegrationTest.groovy @@ -20,6 +20,7 @@ package org.gradle.java.compile.incremental import org.gradle.integtests.fixtures.CompiledLanguage import org.gradle.util.Requires import org.gradle.util.TestPrecondition +import spock.lang.Issue abstract class CrossTaskIncrementalJavaCompilationIntegrationTest extends AbstractCrossTaskIncrementalCompilationIntegrationTest { CompiledLanguage language = CompiledLanguage.JAVA @@ -72,6 +73,103 @@ abstract class CrossTaskIncrementalJavaCompilationIntegrationTest extends Abstra result.hasErrorOutput("package a is not visible") } + @Requires(TestPrecondition.JDK9_OR_LATER) + @Issue("https://github.com/gradle/gradle/issues/23067") + def "incremental compilation works with modules #description"() { + file("impl/build.gradle") << """ + def layout = project.layout + tasks.compileJava { + modularity.inferModulePath = $inferModulePath + options.compilerArgs.addAll($compileArgs) + doFirst { + $doFirst + } + } + """ + source api: ["package a; public class A {}"] + file("api/src/main/${language.name}/module-info.${language.name}").text = """ + module api { + exports a; + } + """ + source impl: [ + "package b; import a.A; import c.C; public class B extends A {}", + "package c; public class C {}", + "package c.d; public class D {}" + ] + file("impl/src/main/${language.name}/module-info.${language.name}").text = """ + module impl { + requires api; + exports b; + exports c; + exports c.d; + } + """ + succeeds "impl:${language.compileTaskName}" + + when: + impl.snapshot { source api: "package a; public class A { void m1() {} }" } + + then: + succeeds "impl:${language.compileTaskName}", "--info" + impl.recompiledClasses("B", "module-info") + + where: + description | inferModulePath | compileArgs | doFirst + "with inferred module-path" | "true" | "[]" | "" + "with manual module-path" | "false" | "[\"--module-path=\${classpath.join(File.pathSeparator)}\"]" | "classpath = layout.files()" + } + + @Requires(TestPrecondition.JDK9_OR_LATER) + def "incremental compilation works for multi-module project with manual module paths"() { + file("impl/build.gradle") << """ + def layout = project.layout + tasks.compileJava { + modularity.inferModulePath = false + options.compilerArgs << "--module-path=\${classpath.join(File.pathSeparator)}" \ + << "--module-source-path" << file("src/main/$languageName") + doFirst { + classpath = layout.files() + } + } + """ + source api: "package a; public class A {}" + def moduleInfo = file("api/src/main/${language.name}/module-info.${language.name}") + moduleInfo.text = """ + module api { + exports a; + } + """ + file("impl/src/main/${language.name}/my.module.first/b/B.java").text = "package b; import a.A; public class B extends A {}" + file("impl/src/main/${language.name}/my.module.first/module-info.${language.name}").text = """ + module my.module.first { + requires api; + exports b; + } + """ + file("impl/src/main/${language.name}/my.module.second/c/C.java").text = "package c; import b.B; class C extends B {}" + file("impl/src/main/${language.name}/my.module.second/module-info.${language.name}").text = """ + module my.module.second { + requires my.module.first; + } + """ + file("impl/src/main/${language.name}/my.module.unrelated/unrelated/Unrelated.java").text = "package unrelated; class Unrelated {}" + file("impl/src/main/${language.name}/my.module.unrelated/module-info.${language.name}").text = """ + module my.module.unrelated { + exports unrelated; + } + """ + succeeds "impl:${language.compileTaskName}" + + when: + impl.snapshot { source api: "package a; public class A { public void m1() {} }" } + + then: + succeeds "impl:${language.compileTaskName}" + // We recompile all module-info.java also for unrelated modules, but we don't recompile unrelated classes + impl.recompiledFqn("my.module.first.b.B", "my.module.second.c.C", "my.module.first.module-info", "my.module.second.module-info", "my.module.unrelated.module-info") + } + @Requires(TestPrecondition.JDK9_OR_LATER) def "recompiles when upstream module-info changes"() { given: diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpec.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpec.java index 80c953838756..5bffca0835dd 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpec.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpec.java @@ -16,15 +16,17 @@ package org.gradle.api.internal.tasks.compile; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import org.gradle.api.internal.tasks.compile.processing.AnnotationProcessorDeclaration; import org.gradle.api.tasks.compile.CompileOptions; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Set; +import static com.google.common.collect.ImmutableList.toImmutableList; + public class DefaultJavaCompileSpec extends DefaultJvmLanguageCompileSpec implements JavaCompileSpec { private MinimalJavaCompileOptions compileOptions; private List annotationProcessorPath; @@ -32,6 +34,7 @@ public class DefaultJavaCompileSpec extends DefaultJvmLanguageCompileSpec implem private Set classes; private List modulePath; private List sourceRoots; + private boolean isIncrementalCompilationOfJavaModule; @Override public MinimalJavaCompileOptions getCompileOptions() { @@ -75,17 +78,25 @@ public void setClasses(Set classes) { @Override public List getModulePath() { if (modulePath == null || modulePath.isEmpty()) { - int i = compileOptions.getCompilerArgs().indexOf("--module-path"); - if (i >= 0) { - // This is kept for backward compatibility - may be removed in the future - String[] modules = compileOptions.getCompilerArgs().get(i + 1).split(File.pathSeparator); - modulePath = Lists.newArrayListWithCapacity(modules.length); - for (String module : modules) { - modulePath.add(new File(module)); + // This is kept for backward compatibility - may be removed in the future + int i = 0; + List modulePaths = new ArrayList<>(); + // Some arguments can also be a GString, that is why use Object.toString() + for (Object argObj : compileOptions.getCompilerArgs()) { + String arg = argObj.toString(); + if ((arg.equals("--module-path") || arg.equals("-p")) && (i + 1) < compileOptions.getCompilerArgs().size()) { + Object argValue = compileOptions.getCompilerArgs().get(++i); + String[] modules = argValue.toString().split(File.pathSeparator); + modulePaths.addAll(Arrays.asList(modules)); + } else if (arg.startsWith("--module-path=")) { + String[] modules = arg.replace("--module-path=", "").split(File.pathSeparator); + modulePaths.addAll(Arrays.asList(modules)); } - } else if (modulePath == null) { - modulePath = ImmutableList.of(); + i++; } + modulePath = modulePaths.stream() + .map(File::new) + .collect(toImmutableList()); } return modulePath; } @@ -95,6 +106,16 @@ public void setModulePath(List modulePath) { this.modulePath = modulePath; } + @Override + public boolean isIncrementalCompilationOfJavaModule() { + return isIncrementalCompilationOfJavaModule; + } + + @Override + public void setIsIncrementalCompilationOfJavaModule(boolean isIncrementalCompilationOfJavaModule) { + this.isIncrementalCompilationOfJavaModule = isIncrementalCompilationOfJavaModule; + } + @Override public List getSourceRoots() { return sourceRoots; diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JavaCompileSpec.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JavaCompileSpec.java index 9dec56c8df51..758dbe33ad34 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JavaCompileSpec.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JavaCompileSpec.java @@ -47,6 +47,10 @@ public interface JavaCompileSpec extends JvmLanguageCompileSpec { void setModulePath(List modulePath); + boolean isIncrementalCompilationOfJavaModule(); + + void setIsIncrementalCompilationOfJavaModule(boolean isIncrementalCompilationOfJavaModule); + default boolean annotationProcessingConfigured() { return !getAnnotationProcessorPath().isEmpty() && !getCompileOptions().getCompilerArgs().contains("-proc:none"); } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JdkJavaCompiler.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JdkJavaCompiler.java index 82bf7102bc2e..c0c7cadb0a67 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JdkJavaCompiler.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/JdkJavaCompiler.java @@ -25,11 +25,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; +import java.io.File; import java.io.Serializable; import java.nio.charset.Charset; import java.util.Iterator; @@ -66,7 +68,8 @@ private JavaCompiler.CompilationTask createCompileTask(JavaCompileSpec spec, Api StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, charset); Iterable compilationUnits = standardFileManager.getJavaFileObjectsFromFiles(spec.getSourceFiles()); boolean hasEmptySourcepaths = JavaVersion.current().isJava9Compatible() && emptySourcepathIn(options); - JavaFileManager fileManager = GradleStandardJavaFileManager.wrap(standardFileManager, DefaultClassPath.of(spec.getAnnotationProcessorPath()), hasEmptySourcepaths); + File previousClassOutput = maybeGetPreviousClassOutput(spec); + JavaFileManager fileManager = GradleStandardJavaFileManager.wrap(standardFileManager, DefaultClassPath.of(spec.getAnnotationProcessorPath()), hasEmptySourcepaths, previousClassOutput); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, spec.getClasses(), compilationUnits); if (compiler instanceof IncrementalCompilationAwareJavaCompiler) { task = ((IncrementalCompilationAwareJavaCompiler) compiler).makeIncremental(task, result.getSourceClassesMapping(), result.getConstantsAnalysisResult(), new CompilationSourceDirs(spec)); @@ -87,4 +90,12 @@ private static boolean emptySourcepathIn(List options) { } return false; } + + @Nullable + private static File maybeGetPreviousClassOutput(JavaCompileSpec spec) { + if (JavaVersion.current().isJava9Compatible() && spec.isIncrementalCompilationOfJavaModule() && !spec.getDestinationDir().equals(spec.getOriginalDestinationDir())) { + return spec.getOriginalDestinationDir(); + } + return null; + } } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/AbstractRecompilationSpecProvider.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/AbstractRecompilationSpecProvider.java index cfee23b4c155..c062422c325e 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/AbstractRecompilationSpecProvider.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/AbstractRecompilationSpecProvider.java @@ -42,6 +42,10 @@ import java.util.Set; abstract class AbstractRecompilationSpecProvider implements RecompilationSpecProvider { + + private static final String MODULE_INFO_CLASS_NAME = "module-info"; + private static final String PACKAGE_INFO_CLASS_NAME = "package-info"; + private final Deleter deleter; private final FileOperations fileOperations; private final FileTree sourceTree; @@ -64,19 +68,20 @@ public AbstractRecompilationSpecProvider( @Override public RecompilationSpec provideRecompilationSpec(CurrentCompilation current, PreviousCompilation previous) { - RecompilationSpec spec = new RecompilationSpec(); + RecompilationSpec recompilationSpec = new RecompilationSpec(); SourceFileClassNameConverter sourceFileClassNameConverter = new FileNameDerivingClassNameConverter(previous.getSourceToClassConverter(), getFileExtensions()); - processClasspathChanges(current, previous, spec); + processClasspathChanges(current, previous, recompilationSpec); SourceFileChangeProcessor sourceFileChangeProcessor = new SourceFileChangeProcessor(previous); - processSourceChanges(current, sourceFileChangeProcessor, spec, sourceFileClassNameConverter); - collectAllSourcePathsAndIndependentClasses(sourceFileChangeProcessor, spec, sourceFileClassNameConverter); + processSourceChanges(current, sourceFileChangeProcessor, recompilationSpec, sourceFileClassNameConverter); + collectAllSourcePathsAndIndependentClasses(sourceFileChangeProcessor, recompilationSpec, sourceFileClassNameConverter); - Set typesToReprocess = previous.getTypesToReprocess(spec.getClassesToCompile()); - processTypesToReprocess(typesToReprocess, spec, sourceFileClassNameConverter); + Set typesToReprocess = previous.getTypesToReprocess(recompilationSpec.getClassesToCompile()); + processTypesToReprocess(typesToReprocess, recompilationSpec, sourceFileClassNameConverter); + addModuleInfoToCompile(recompilationSpec, sourceFileClassNameConverter); - return spec; + return recompilationSpec; } protected abstract Set getFileExtensions(); @@ -177,7 +182,7 @@ private static Set collectIndependentClassesForSourcePath(String sourceP private static void processTypesToReprocess(Set typesToReprocess, RecompilationSpec spec, SourceFileClassNameConverter sourceFileClassNameConverter) { for (String typeToReprocess : typesToReprocess) { - if (typeToReprocess.endsWith("package-info") || typeToReprocess.equals("module-info")) { + if (typeToReprocess.endsWith(PACKAGE_INFO_CLASS_NAME) || typeToReprocess.equals(MODULE_INFO_CLASS_NAME)) { // Fixes: https://github.com/gradle/gradle/issues/17572 // package-info classes cannot be passed as classes to reprocess to the Java compiler. // Therefore, we need to recompile them every time anything changes if they are processed by an aggregating annotation processor. @@ -189,6 +194,19 @@ private static void processTypesToReprocess(Set typesToReprocess, Recomp } } + private static void addModuleInfoToCompile(RecompilationSpec spec, SourceFileClassNameConverter sourceFileClassNameConverter) { + Set moduleInfoSources = sourceFileClassNameConverter.getRelativeSourcePaths(MODULE_INFO_CLASS_NAME); + if (!moduleInfoSources.isEmpty()) { + // Always recompile module-info.java if present. + // This solves case for incremental compilation for manual --module-path when not combined with --module-source-path or --source-path, + // since compiled module-info is not in the output after we change compile outputs in the CompileTransaction. + // Alternative would be, that we would move/copy the module-info class to transaction outputs and add transaction outputs to classpath. + // First part of fix for: https://github.com/gradle/gradle/issues/23067 + spec.addClassToCompile(MODULE_INFO_CLASS_NAME); + spec.addSourcePaths(moduleInfoSources); + } + } + @Override public CompileTransaction initCompilationSpecAndTransaction(JavaCompileSpec spec, RecompilationSpec recompilationSpec) { if (!recompilationSpec.isBuildNeeded()) { @@ -205,6 +223,7 @@ public CompileTransaction initCompilationSpecAndTransaction(JavaCompileSpec spec includePreviousCompilationOutputOnClasspath(spec); addClassesToProcess(spec, recompilationSpec); Map resourcesToDelete = prepareResourcePatterns(recompilationSpec.getResourcesToGenerate(), fileOperations); + spec.setIsIncrementalCompilationOfJavaModule(recompilationSpec.hasClassToCompile(MODULE_INFO_CLASS_NAME)); return new CompileTransaction(spec, classesToDelete, resourcesToDelete, fileOperations, deleter); } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpec.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpec.java index e47c008fd3aa..24df64e4436a 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpec.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpec.java @@ -55,6 +55,10 @@ public Set getClassesToCompile() { return Collections.unmodifiableSet(classesToCompile); } + public boolean hasClassToCompile(String className) { + return classesToCompile.contains(className); + } + public void addClassToReprocess(String classToReprocess) { classesToProcess.add(classToReprocess); } @@ -98,5 +102,4 @@ public String getFullRebuildCause() { public void setFullRebuildCause(String description) { fullRebuildCause = description; } - } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransaction.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransaction.java index 1e03e25841bb..6cba9fbb3748 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransaction.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransaction.java @@ -102,9 +102,7 @@ public T execute(Function function) { moveCompileOutputToOriginalFolders(stagedOutputs); return result; } catch (CompilationFailedException t) { - if (spec.getCompileOptions().supportsIncrementalCompilationAfterFailure()) { - rollbackStash(stashResult.stashedFiles); - } + rollbackStash(stashResult.stashedFiles); throw t; } finally { restoreSpecOutputs(stagedOutputs); @@ -233,15 +231,25 @@ private void moveCompileOutputToOriginalFolders(List stagedOutputs } private void rollbackStash(List stashedFiles) { - stashedFiles.forEach(StashedFile::unstash); + if (supportsIncrementalCompilationAfterFailure()) { + stashedFiles.forEach(StashedFile::unstash); + } } private void setupSpecOutputs(List stagedOutputs) { - stagedOutputs.forEach(StagedOutput::setupSpecOutput); + if (supportsIncrementalCompilationAfterFailure()) { + stagedOutputs.forEach(StagedOutput::setupSpecOutput); + } } private void restoreSpecOutputs(List stagedOutputs) { - stagedOutputs.forEach(StagedOutput::restoreSpecOutput); + if (supportsIncrementalCompilationAfterFailure()) { + stagedOutputs.forEach(StagedOutput::restoreSpecOutput); + } + } + + private boolean supportsIncrementalCompilationAfterFailure() { + return spec.getCompileOptions().supportsIncrementalCompilationAfterFailure(); } private static void moveFile(File sourceFile, File destinationFile) { diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/reflect/GradleStandardJavaFileManager.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/reflect/GradleStandardJavaFileManager.java index 71caa4d60eeb..c221a1e527ee 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/reflect/GradleStandardJavaFileManager.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/reflect/GradleStandardJavaFileManager.java @@ -16,15 +16,20 @@ package org.gradle.api.internal.tasks.compile.reflect; +import com.google.common.collect.Iterables; import org.gradle.internal.classpath.ClassPath; +import javax.annotation.Nullable; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; +import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URLClassLoader; +import java.util.Collections; import java.util.Set; import static org.gradle.api.internal.tasks.compile.filter.AnnotationProcessorFilter.getFilteredClassLoader; @@ -32,19 +37,32 @@ public class GradleStandardJavaFileManager extends ForwardingJavaFileManager { private final ClassPath annotationProcessorPath; private final boolean hasEmptySourcePaths; + private final boolean hasPreviousClassOutput; - private GradleStandardJavaFileManager(StandardJavaFileManager fileManager, ClassPath annotationProcessorPath, boolean hasEmptySourcePaths) { + private GradleStandardJavaFileManager(StandardJavaFileManager fileManager, ClassPath annotationProcessorPath, boolean hasEmptySourcePaths, @Nullable File previousClassOutput) { super(fileManager); this.annotationProcessorPath = annotationProcessorPath; this.hasEmptySourcePaths = hasEmptySourcePaths; + this.hasPreviousClassOutput = previousClassOutput != null; + registerPreviousClassOutput(previousClassOutput); + } + + private void registerPreviousClassOutput(@Nullable File previousClassOutput) { + if (previousClassOutput != null) { + try { + fileManager.setLocation(GradleLocation.PREVIOUS_CLASS_OUTPUT, Collections.singleton(previousClassOutput)); + } catch (IOException e) { + throw new UncheckedIOException("Problem registering previous class output location", e); + } + } } /** * Overrides particular methods to prevent javac from accessing source files outside of Gradle's understanding or * classloaders outside of Gradle's control. */ - public static JavaFileManager wrap(StandardJavaFileManager delegate, ClassPath annotationProcessorPath, boolean hasEmptySourcePaths) { - return new GradleStandardJavaFileManager(delegate, annotationProcessorPath, hasEmptySourcePaths); + public static JavaFileManager wrap(StandardJavaFileManager delegate, ClassPath annotationProcessorPath, boolean hasEmptySourcePaths, @Nullable File previousClassOutput) { + return new GradleStandardJavaFileManager(delegate, annotationProcessorPath, hasEmptySourcePaths, previousClassOutput); } @Override @@ -78,6 +96,20 @@ public Iterable list(Location location, String packageName, Set< kinds.remove(JavaFileObject.Kind.SOURCE); } } + + if (hasPreviousClassOutput && location.equals(StandardLocation.CLASS_OUTPUT)) { + // For Java module compilation we list also previous class output as class output. + // This is needed for incremental compilation after a failure where we change output folders. + // With that we make sure that all module classes/packages are found by javac. + // In case one of --module-source-path or --source-path is provided, this makes sure, that javac won't automatically recompile + // classes that are not in CLASS_OUTPUT. And in case when --module-source-path or --source-path are not provided, + // this makes sure that javac doesn't fail on missing packages or classes that are not in CLASS_OUTPUT. + // Second and last part of fix for: https://github.com/gradle/gradle/issues/23067 + Iterable previousClassOutput = super.list(GradleLocation.PREVIOUS_CLASS_OUTPUT, packageName, kinds, recurse); + Iterable classOutput = super.list(location, packageName, kinds, recurse); + return Iterables.concat(previousClassOutput, classOutput); + } + return super.list(location, packageName, kinds, recurse); } @@ -92,4 +124,18 @@ public ClassLoader getClassLoader(Location location) { return classLoader; } + + private enum GradleLocation implements Location { + PREVIOUS_CLASS_OUTPUT; + + @Override + public String getName() { + return name(); + } + + @Override + public boolean isOutputLocation() { + return false; + } + } } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java b/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java index a9b9ebf1b9ee..5aa22f684848 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java @@ -301,6 +301,7 @@ DefaultJavaCompileSpec createSpec() { final DefaultJavaCompileSpec spec = createBaseSpec(); spec.setDestinationDir(getDestinationDirectory().getAsFile().get()); + spec.setOriginalDestinationDir(spec.getDestinationDir()); spec.setWorkingDir(getProjectLayout().getProjectDirectory().getAsFile()); spec.setTempDir(getTemporaryDir()); spec.setCompileClasspath(ImmutableList.copyOf(javaModuleDetector.inferClasspath(isModule, getClasspath()))); diff --git a/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpecTest.groovy b/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpecTest.groovy new file mode 100644 index 000000000000..8b450ba20c02 --- /dev/null +++ b/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/DefaultJavaCompileSpecTest.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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.gradle.api.internal.tasks.compile + + +import org.gradle.api.tasks.compile.CompileOptions +import org.gradle.util.TestUtil +import spock.lang.Specification + +class DefaultJavaCompileSpecTest extends Specification { + + def "parses module-path from compileArgs with #description"() { + given: + CompileOptions options = TestUtil.objectFactory().newInstance(CompileOptions) + options.compilerArgs.addAll(modulePathParameters) + DefaultJavaCompileSpec compileSpec = new DefaultJavaCompileSpec() + compileSpec.setCompileOptions(options) + + when: + def modulePath = compileSpec.modulePath + + then: + modulePath == [new File("/some/path"), new File("/some/path2")] + + where: + description | modulePathParameters + "--module-path=" | ["--module-path=/some/path$File.pathSeparator/some/path2"] + "--module-path " | ["--module-path", "/some/path$File.pathSeparator/some/path2"] + "-p " | ["-p", "/some/path$File.pathSeparator/some/path2"] + } +} diff --git a/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransactionTest.groovy b/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransactionTest.groovy index 05edee9272d5..5be5d4874dc0 100644 --- a/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransactionTest.groovy +++ b/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/incremental/transaction/CompileTransactionTest.groovy @@ -30,6 +30,7 @@ import spock.lang.TempDir import java.nio.file.Files import java.nio.file.Path +import java.util.function.Function import java.util.stream.Stream import static com.google.common.base.Preconditions.checkNotNull @@ -327,23 +328,27 @@ class CompileTransactionTest extends Specification { } where: - originalDir | stagingDir + originalDir | stagingDir "classes" | "compile-output" "annotation-generated-sources" | "annotation-output" "header-sources" | "header-output" } - def "modifies spec output directories before execution and restores them after execution"() { + def "#description modify spec outputs and restores them when incremental compilation after failure is set to #incrementalCompilationAfterFilure"() { spec.setDestinationDir(file("classes")) spec.getCompileOptions().setAnnotationProcessorGeneratedSourcesDirectory(file("annotation-generated-sources")) spec.getCompileOptions().setHeaderOutputDirectory(file("header-sources")) + spec.getCompileOptions().setSupportsIncrementalCompilationAfterFailure(incrementalCompilationAfterFilure) expect: - // Modify output folders before + // Check if output folders were modified newCompileTransaction().execute { - assert spec.getDestinationDir() == fileInTransactionDir("compile-output") - assert spec.getCompileOptions().getAnnotationProcessorGeneratedSourcesDirectory() == fileInTransactionDir("annotation-output") - assert spec.getCompileOptions().getHeaderOutputDirectory() == fileInTransactionDir("header-output") + def fileGenerator = incrementalCompilationAfterFilure + ? { String fileName -> fileInTransactionDir(fileName) } as Function + : { String fileName -> file(fileName) } as Function + assert spec.getDestinationDir() == fileGenerator.apply(destinationDir) + assert spec.getCompileOptions().getAnnotationProcessorGeneratedSourcesDirectory() == fileGenerator.apply(annotationProcessorDir) + assert spec.getCompileOptions().getHeaderOutputDirectory() == fileGenerator.apply(headerDir) return DID_WORK } @@ -351,6 +356,11 @@ class CompileTransactionTest extends Specification { spec.getDestinationDir() == file("classes") spec.getCompileOptions().getAnnotationProcessorGeneratedSourcesDirectory() == file("annotation-generated-sources") spec.getCompileOptions().getHeaderOutputDirectory() == file("header-sources") + + where: + description | incrementalCompilationAfterFilure | destinationDir | annotationProcessorDir | headerDir + "doesn't" | false | "classes" | "annotation-generated-sources" | "header-sources" + "does" | true | "compile-output" | "annotation-output" | "header-output" } private File fileInTransactionDir(String path) { diff --git a/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJvmLanguageCompileSpec.java b/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJvmLanguageCompileSpec.java index 17c9565256be..3828fe5c35c9 100644 --- a/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJvmLanguageCompileSpec.java +++ b/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/DefaultJvmLanguageCompileSpec.java @@ -28,6 +28,7 @@ public class DefaultJvmLanguageCompileSpec implements JvmLanguageCompileSpec, Se private File tempDir; private List classpath; private File destinationDir; + private File originalDestinationDir; private Iterable sourceFiles; private Integer release; private String sourceCompatibility; @@ -54,6 +55,16 @@ public void setDestinationDir(File destinationDir) { this.destinationDir = destinationDir; } + @Override + public File getOriginalDestinationDir() { + return originalDestinationDir; + } + + @Override + public void setOriginalDestinationDir(File originalDestinationDir) { + this.originalDestinationDir = originalDestinationDir; + } + @Override public File getTempDir() { return tempDir; diff --git a/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/JvmLanguageCompileSpec.java b/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/JvmLanguageCompileSpec.java index c000e4bbbde2..05a24bdc9d71 100644 --- a/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/JvmLanguageCompileSpec.java +++ b/subprojects/language-jvm/src/main/java/org/gradle/api/internal/tasks/compile/JvmLanguageCompileSpec.java @@ -35,6 +35,10 @@ public interface JvmLanguageCompileSpec extends CompileSpec { void setDestinationDir(File destinationDir); + File getOriginalDestinationDir(); + + void setOriginalDestinationDir(File originalDestinationDir); + Iterable getSourceFiles(); void setSourceFiles(Iterable sourceFiles); diff --git a/subprojects/scala/src/main/java/org/gradle/language/scala/tasks/AbstractScalaCompile.java b/subprojects/scala/src/main/java/org/gradle/language/scala/tasks/AbstractScalaCompile.java index 10ef563978ab..d21cdeeacb2a 100644 --- a/subprojects/scala/src/main/java/org/gradle/language/scala/tasks/AbstractScalaCompile.java +++ b/subprojects/scala/src/main/java/org/gradle/language/scala/tasks/AbstractScalaCompile.java @@ -175,6 +175,7 @@ protected ScalaJavaJointCompileSpec createSpec() { DefaultScalaJavaJointCompileSpec spec = new DefaultScalaJavaJointCompileSpecFactory(compileOptions, getToolchain()).create(); spec.setSourceFiles(getSource().getFiles()); spec.setDestinationDir(getDestinationDirectory().getAsFile().get()); + spec.setOriginalDestinationDir(spec.getDestinationDir()); spec.setWorkingDir(getProjectLayout().getProjectDirectory().getAsFile()); spec.setTempDir(getTemporaryDir()); spec.setCompileClasspath(ImmutableList.copyOf(getClasspath()));