From 8ce7ea59c0b47e2dc6f090c923b9c893979095f0 Mon Sep 17 00:00:00 2001 From: Stefan Oehme Date: Wed, 22 Aug 2018 14:35:16 +0200 Subject: [PATCH] Recompile when a resource file changes We can't tell what annotation processors might be doing with a resource file, as the processing API does not provide any way of associating resources with outputs. Therefor the only safe option for now is doing a full recompile. We no longer force a full recompile when a processor reads a resource. We only recompile if the resource changes. This makes it even easier to make a lot of processors incremental. --- .../docs/src/docs/userguide/java_plugin.adoc | 4 +- ...AnnotationProcessingIntegrationTest.groovy | 21 +++++++++++ ...AnnotationProcessingIntegrationTest.groovy | 20 +--------- ...AnnotationProcessingIntegrationTest.groovy | 20 +--------- ...entalJavaCompilationIntegrationTest.groovy | 21 ++++++++++- .../incremental/recomp/InputChangeAction.java | 15 ++++++-- .../recomp/RecompilationSpecProvider.java | 3 +- .../recomp/ResourceChangeProcessor.java | 37 +++++++++++++++++++ .../IncrementalProcessingStrategy.java | 7 +++- .../processing/IncrementalFilerTest.groovy | 10 +---- 10 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/ResourceChangeProcessor.java diff --git a/subprojects/docs/src/docs/userguide/java_plugin.adoc b/subprojects/docs/src/docs/userguide/java_plugin.adoc index f50ea0784bac..60e2c20c460b 100644 --- a/subprojects/docs/src/docs/userguide/java_plugin.adoc +++ b/subprojects/docs/src/docs/userguide/java_plugin.adoc @@ -551,6 +551,8 @@ To help you understand how incremental compilation works, the following provides ==== Known issues * If a compile task fails due to a compile error, it will do a full compilation again the next time it is invoked. +* If you are using an annotation processor that reads resources (e.g. a configuration file), you need to declare those resources as an input of the compile task. +* If a resource file is changed, Gradle will trigger a full recompilation. [[sec:incremental_annotation_processing]] === Incremental annotation processing @@ -599,8 +601,6 @@ Both categories have the following limitations: * They must not depend on compiler-specific APIs like https://docs.oracle.com/javase/8/docs/jdk/api/javac/tree/com/sun/source/util/Trees.html[com.sun.source.util.Trees]. Gradle wraps the processing APIs, so attempts to cast to compiler-specific types will fail. If your processor does this, it cannot be incremental, unless you have some fallback mechanism. -* If they use https://docs.oracle.com/javase/10/docs/api/javax/annotation/processing/Filer.html#getResource(javax.tools.JavaFileManager.Location,java.lang.CharSequence,java.lang.CharSequence)[Filer#getResource], Gradle will recompile all source files. - See link:https://github.com/gradle/gradle/issues/4701[gradle/issues/4701] * If they use link:https://docs.oracle.com/javase/10/docs/api/javax/annotation/processing/Filer.html#createResource(javax.tools.JavaFileManager.Location,java.lang.CharSequence,java.lang.CharSequence,javax.lang.model.element.Element++...++)[Filer#createResource], Gradle will recompile all source files. See https://github.com/gradle/gradle/issues/4702[gradle/issues/4702] diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AbstractIncrementalAnnotationProcessingIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AbstractIncrementalAnnotationProcessingIntegrationTest.groovy index df93e80c0851..3e8fc32634b1 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AbstractIncrementalAnnotationProcessingIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AbstractIncrementalAnnotationProcessingIntegrationTest.groovy @@ -90,4 +90,25 @@ abstract class AbstractIncrementalAnnotationProcessingIntegrationTest extends Ab fails("compileJava") failure.assertHasCause("Annotation processor 'unknown.Processor' not found") } + + def "recompiles when a resource changes"() { + given: + buildFile << """ + compileJava.inputs.dir 'src/main/resources' + """ + java("class A {}") + java("class B {}") + def resource = file("src/main/resources/foo.txt") + resource.text = 'foo' + + outputs.snapshot { succeeds 'compileJava' } + + when: + resource.text = 'bar' + + then: + succeeds 'compileJava' + outputs.recompiledClasses("A", "B") + } + } diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AggregatingIncrementalAnnotationProcessingIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AggregatingIncrementalAnnotationProcessingIntegrationTest.groovy index 2fdb743470e7..d6405dc6396f 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AggregatingIncrementalAnnotationProcessingIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/AggregatingIncrementalAnnotationProcessingIntegrationTest.groovy @@ -178,23 +178,7 @@ class AggregatingIncrementalAnnotationProcessingIntegrationTest extends Abstract outputs.deletedClasses("ServiceRegistry") } - def "processors can't read resources"() { - given: - withProcessor(new NonIncrementalProcessorFixture().readingResources().withDeclaredType(IncrementalAnnotationProcessorType.AGGREGATING)) - def a = java "@Thing class A {}" - outputs.snapshot { succeeds "compileJava" } - - when: - a.text = "@Thing class A { void foo() {} }" - - then: - succeeds "compileJava", "--info" - - and: - outputContains("Full recompilation is required because incremental annotation processors are not allowed to read resources.") - } - - def "processors can't write resources"() { + def "writing resources triggers a full recompilation"() { given: withProcessor(new NonIncrementalProcessorFixture().writingResources().withDeclaredType(IncrementalAnnotationProcessorType.AGGREGATING)) def a = java "@Thing class A {}" @@ -207,7 +191,7 @@ class AggregatingIncrementalAnnotationProcessingIntegrationTest extends Abstract succeeds "compileJava", "--info" and: - outputContains("Full recompilation is required because incremental annotation processors are not allowed to create resources.") + outputContains("Full recompilation is required because an annotation processor generated a resource.") } def "an isolating processor is also a valid aggregating processor"() { diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/IsolatingIncrementalAnnotationProcessingIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/IsolatingIncrementalAnnotationProcessingIntegrationTest.groovy index 34406beaaadb..230a23640d37 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/IsolatingIncrementalAnnotationProcessingIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/IsolatingIncrementalAnnotationProcessingIntegrationTest.groovy @@ -237,23 +237,7 @@ class IsolatingIncrementalAnnotationProcessingIntegrationTest extends AbstractIn outputContains("Full recompilation is required because the generated type 'AThing' must have exactly one originating element, but had 0.") } - def "processors can't read resources"() { - given: - withProcessor(new NonIncrementalProcessorFixture().readingResources().withDeclaredType(IncrementalAnnotationProcessorType.ISOLATING)) - def a = java "@Thing class A {}" - outputs.snapshot { succeeds "compileJava" } - - when: - a.text = "@Thing class A { void foo() {} }" - - then: - succeeds "compileJava", "--info" - - and: - outputContains("Full recompilation is required because incremental annotation processors are not allowed to read resources.") - } - - def "processors can't write resources"() { + def "writing resources triggers a full recompilation"() { given: withProcessor(new NonIncrementalProcessorFixture().writingResources().withDeclaredType(IncrementalAnnotationProcessorType.ISOLATING)) def a = java "@Thing class A {}" @@ -266,7 +250,7 @@ class IsolatingIncrementalAnnotationProcessingIntegrationTest extends AbstractIn succeeds "compileJava", "--info" and: - outputContains("Full recompilation is required because incremental annotation processors are not allowed to create resources.") + outputContains("Full recompilation is required because an annotation processor generated a resource.") } def "processors cannot provide multiple originating elements"() { diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/SourceIncrementalJavaCompilationIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/SourceIncrementalJavaCompilationIntegrationTest.groovy index a1d36ecb873d..c3b01dcd9921 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/SourceIncrementalJavaCompilationIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/java/compile/incremental/SourceIncrementalJavaCompilationIntegrationTest.groovy @@ -981,7 +981,6 @@ dependencies { compile 'net.sf.ehcache:ehcache:2.10.2' } file("build/headers/java/main/Bar.h").assertExists() } - def "recompiles all dependents when no jar analysis is present"() { given: java """class A { @@ -1005,4 +1004,24 @@ dependencies { compile 'com.google.guava:guava:21.0' } succeeds "compileJava" outputs.recompiledClasses("A", "B") } + + def "does not recompile when a resource changes"() { + given: + buildFile << """ + compileJava.inputs.dir 'src/main/resources' + """ + java("class A {}") + java("class B {}") + def resource = file("src/main/resources/foo.txt") + resource.text = 'foo' + + outputs.snapshot { succeeds 'compileJava' } + + when: + resource.text = 'bar' + + then: + succeeds 'compileJava' + outputs.noneRecompiled() + } } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/InputChangeAction.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/InputChangeAction.java index 2cd166bfb14a..92b085fc2694 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/InputChangeAction.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/InputChangeAction.java @@ -19,17 +19,21 @@ import org.gradle.api.Action; import org.gradle.api.tasks.incremental.InputFileDetails; +import java.io.File; + import static org.gradle.internal.FileUtils.hasExtension; class InputChangeAction implements Action { private final RecompilationSpec spec; private final JavaChangeProcessor javaChangeProcessor; private final AnnotationProcessorChangeProcessor annotationProcessorChangeProcessor; + private final ResourceChangeProcessor resourceChangeProcessor; - InputChangeAction(RecompilationSpec spec, JavaChangeProcessor javaChangeProcessor, AnnotationProcessorChangeProcessor annotationProcessorChangeProcessor) { + InputChangeAction(RecompilationSpec spec, JavaChangeProcessor javaChangeProcessor, AnnotationProcessorChangeProcessor annotationProcessorChangeProcessor, ResourceChangeProcessor resourceChangeProcessor) { this.spec = spec; this.javaChangeProcessor = javaChangeProcessor; this.annotationProcessorChangeProcessor = annotationProcessorChangeProcessor; + this.resourceChangeProcessor = resourceChangeProcessor; } @Override @@ -38,10 +42,13 @@ public void execute(InputFileDetails input) { return; } - annotationProcessorChangeProcessor.processChange(input, spec); - - if (hasExtension(input.getFile(), ".java")) { + File file = input.getFile(); + if (hasExtension(file, ".java")) { javaChangeProcessor.processChange(input, spec); + } else if (hasExtension(file, ".jar") || hasExtension(file, ".class")) { + annotationProcessorChangeProcessor.processChange(input, spec); + } else { + resourceChangeProcessor.processChange(input, spec); } } } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpecProvider.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpecProvider.java index 89d0f1bd1f7f..5a2a6aadd069 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpecProvider.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/RecompilationSpecProvider.java @@ -79,7 +79,8 @@ private void processClasspathChanges(CurrentCompilation current, PreviousCompila private void processOtherChanges(CurrentCompilation current, PreviousCompilation previous, RecompilationSpec spec) { JavaChangeProcessor javaChangeProcessor = new JavaChangeProcessor(previous, sourceToNameConverter); AnnotationProcessorChangeProcessor annotationProcessorChangeProcessor = new AnnotationProcessorChangeProcessor(current, previous); - InputChangeAction action = new InputChangeAction(spec, javaChangeProcessor, annotationProcessorChangeProcessor); + ResourceChangeProcessor resourceChangeProcessor = new ResourceChangeProcessor(current.getAnnotationProcessorPath()); + InputChangeAction action = new InputChangeAction(spec, javaChangeProcessor, annotationProcessorChangeProcessor, resourceChangeProcessor); current.visitChanges(action); } diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/ResourceChangeProcessor.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/ResourceChangeProcessor.java new file mode 100644 index 000000000000..9680e6ce99f5 --- /dev/null +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/recomp/ResourceChangeProcessor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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.incremental.recomp; + +import org.gradle.api.tasks.incremental.InputFileDetails; + +import java.io.File; +import java.util.Collection; + +class ResourceChangeProcessor { + private final Collection processorPath; + + ResourceChangeProcessor(Collection processorPath) { + this.processorPath = processorPath; + } + + public void processChange(InputFileDetails input, RecompilationSpec spec) { + if (processorPath.isEmpty()) { + return; + } + spec.setFullRebuildCause("A resource changed", input.getFile()); + } +} diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/processing/IncrementalProcessingStrategy.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/processing/IncrementalProcessingStrategy.java index 65e5693f5f4d..2412b6c491fa 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/processing/IncrementalProcessingStrategy.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/processing/IncrementalProcessingStrategy.java @@ -39,10 +39,13 @@ abstract class IncrementalProcessingStrategy { public abstract void recordGeneratedType(CharSequence name, Element[] originatingElements); public final void recordGeneratedResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName, Element[] originatingElements) { - result.setFullRebuildCause("incremental annotation processors are not allowed to create resources"); + result.setFullRebuildCause("an annotation processor generated a resource"); } + /** + * We don't trigger a full recompile on resource reads, because we already trigger a full recompile when any + * resource changes. + */ public final void recordAccessedResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName) { - result.setFullRebuildCause("incremental annotation processors are not allowed to read resources"); } } diff --git a/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/processing/IncrementalFilerTest.groovy b/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/processing/IncrementalFilerTest.groovy index b5348e089a78..4ff3d7a2656c 100644 --- a/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/processing/IncrementalFilerTest.groovy +++ b/subprojects/language-java/src/test/groovy/org/gradle/api/internal/tasks/compile/processing/IncrementalFilerTest.groovy @@ -37,20 +37,12 @@ abstract class IncrementalFilerTest extends Specification { abstract IncrementalProcessingStrategy getStrategy(AnnotationProcessingResult result) - def "does a full rebuild when trying to read resources"() { - when: - filer.getResource(StandardLocation.SOURCE_OUTPUT, "", "foo.txt") - - then: - result.fullRebuildCause == "incremental annotation processors are not allowed to read resources" - } - def "does a full rebuild when trying to write resources"() { when: filer.createResource(StandardLocation.SOURCE_OUTPUT, "", "foo.txt") then: - result.fullRebuildCause == "incremental annotation processors are not allowed to create resources" + result.fullRebuildCause == "an annotation processor generated a resource" } PackageElement pkg(String packageName) {