Skip to content

Commit

Permalink
Recompile when a resource file changes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
oehme committed Aug 23, 2018
1 parent ebc68b1 commit 8ce7ea5
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 55 deletions.
4 changes: 2 additions & 2 deletions subprojects/docs/src/docs/userguide/java_plugin.adoc
Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down
Expand Up @@ -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")
}

}
Expand Up @@ -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 {}"
Expand All @@ -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"() {
Expand Down
Expand Up @@ -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 {}"
Expand All @@ -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"() {
Expand Down
Expand Up @@ -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 {
Expand All @@ -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()
}
}
Expand Up @@ -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<InputFileDetails> {
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
Expand All @@ -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);
}
}
}
Expand Up @@ -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);
}

Expand Down
@@ -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<File> processorPath;

ResourceChangeProcessor(Collection<File> processorPath) {
this.processorPath = processorPath;
}

public void processChange(InputFileDetails input, RecompilationSpec spec) {
if (processorPath.isEmpty()) {
return;
}
spec.setFullRebuildCause("A resource changed", input.getFile());
}
}
Expand Up @@ -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");
}
}
Expand Up @@ -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) {
Expand Down

0 comments on commit 8ce7ea5

Please sign in to comment.