diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java
new file mode 100644
index 000000000..bb0c0f4ce
--- /dev/null
+++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.itest.tests;
+
+import static org.gradle.testkit.runner.TaskOutcome.SUCCESS;
+import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.gradle.testkit.runner.BuildResult;
+import org.gradle.testkit.runner.GradleRunner;
+import org.gradle.testkit.runner.TaskOutcome;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * This is supposed to be run from the mapstruct root project folder.
+ * Otherwise, use -Dmapstruct_root=path_to_project
.
+ */
+@RunWith( Parameterized.class )
+public class GradleIncrementalCompilationTest {
+ private static Path rootPath;
+ private static String projectDir = "integrationtest/src/test/resources/gradleIncrementalCompilationTest";
+ private static String compileTaskName = "compileJava";
+
+ @Rule
+ public final TemporaryFolder testBuildDir = new TemporaryFolder();
+ @Rule
+ public final TemporaryFolder testProjectDir = new TemporaryFolder();
+
+ private String gradleVersion;
+ private GradleRunner runner;
+ private File sourceDirectory;
+ private List compileArgs; // Gradle compile task arguments
+
+ public GradleIncrementalCompilationTest(String gradleVersion) {
+ this.gradleVersion = gradleVersion;
+ }
+
+ @Parameters( name = "Gradle {0}" )
+ public static List gradleVersions() {
+ return Arrays.asList( "5.0", "6.0" );
+ }
+
+ private void replaceInFile(File file, CharSequence target, CharSequence replacement) throws IOException {
+ String content = FileUtils.readFileToString( file, Charset.defaultCharset() );
+ FileUtils.writeStringToFile( file, content.replace( target, replacement ), Charset.defaultCharset() );
+ }
+
+ private GradleRunner getRunner(String... additionalArguments) {
+ List fullArguments = new ArrayList( compileArgs );
+ fullArguments.addAll( Arrays.asList( additionalArguments ) );
+ return runner.withArguments( fullArguments );
+ }
+
+ private void assertCompileOutcome(BuildResult result, TaskOutcome outcome) {
+ assertEquals( outcome, result.task( ":" + compileTaskName ).getOutcome() );
+ }
+
+ private void assertRecompiled(BuildResult result, int recompiledCount) {
+ assertCompileOutcome( result, recompiledCount > 0 ? SUCCESS : UP_TO_DATE );
+ assertThat(
+ result.getOutput(),
+ containsString( String.format( "Incremental compilation of %d classes completed", recompiledCount ) ) );
+ }
+
+ private List buildCompileArgs() {
+ // Make Gradle use the temporary build folder by overriding the buildDir property
+ String buildDirPropertyArg = "-PbuildDir=" + testBuildDir.getRoot().getAbsolutePath();
+
+ // Inject the path to the folder containing the mapstruct-processor JAR
+ String jarDirectoryArg = "-PmapstructRootPath=" + rootPath.toString();
+ return Arrays.asList( compileTaskName, buildDirPropertyArg, jarDirectoryArg );
+ }
+
+ @BeforeClass
+ public static void setupClass() throws Exception {
+ rootPath = Paths.get( System.getProperty( "mapstruct_root", "." ) ).toAbsolutePath();
+ }
+
+ @Before
+ public void setup() throws IOException {
+ // Copy test project files to the temp dir
+ Path gradleProjectPath = rootPath.resolve( projectDir );
+ FileUtils.copyDirectory( gradleProjectPath.toFile(), testProjectDir.getRoot() );
+ compileArgs = buildCompileArgs();
+ sourceDirectory = new File( testProjectDir.getRoot(), "src/main/java" );
+ runner = GradleRunner.create().withGradleVersion( gradleVersion ).withProjectDir( testProjectDir.getRoot() );
+ }
+
+ @Test
+ public void testBuildSucceeds() throws IOException {
+ // Make sure the test build setup actually compiles
+ BuildResult buildResult = getRunner().build();
+ assertCompileOutcome( buildResult, SUCCESS );
+ }
+
+ @Test
+ public void testUpToDate() throws IOException {
+ getRunner().build();
+ BuildResult secondBuildResult = getRunner().build();
+ assertCompileOutcome( secondBuildResult, UP_TO_DATE );
+ }
+
+ @Test
+ public void testChangeConstant() throws IOException {
+ getRunner().build();
+ // Change return value in class Target
+ File targetFile = new File( sourceDirectory, "org/mapstruct/itest/gradle/model/Target.java" );
+ replaceInFile( targetFile, "original", "changed" );
+ BuildResult secondBuildResult = getRunner( "--info" ).build();
+
+ // 3 classes should be recompiled: Target -> TestMapper -> TestMapperImpl
+ assertRecompiled( secondBuildResult, 3 );
+ }
+
+ @Test
+ public void testChangeTargetField() throws IOException {
+ getRunner().build();
+ // Change target field in mapper interface
+ File mapperFile = new File( sourceDirectory, "org/mapstruct/itest/gradle/lib/TestMapper.java" );
+ replaceInFile( mapperFile, "field", "otherField" );
+ BuildResult secondBuildResult = getRunner( "--info" ).build();
+
+ // 2 classes should be recompiled: TestMapper -> TestMapperImpl
+ assertRecompiled( secondBuildResult, 2 );
+ }
+
+ @Test
+ public void testChangeUnrelatedFile() throws IOException {
+ getRunner().build();
+ File unrelatedFile = new File( sourceDirectory, "org/mapstruct/itest/gradle/lib/UnrelatedComponent.java" );
+ replaceInFile( unrelatedFile, "true", "false" );
+ BuildResult secondBuildResult = getRunner( "--info" ).build();
+
+ // Only the UnrelatedComponent class should be recompiled
+ assertRecompiled( secondBuildResult, 1 );
+ }
+}
diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle
new file mode 100644
index 000000000..e62a8d087
--- /dev/null
+++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle
@@ -0,0 +1,29 @@
+plugins {
+ id 'java'
+}
+
+if (!project.hasProperty('mapstructRootPath'))
+ throw new IllegalArgumentException("Missing required property: mapstructRootPath")
+
+repositories {
+ jcenter()
+ mavenLocal()
+ flatDir {
+ dirs "${mapstructRootPath}/processor/target"
+ dirs "${mapstructRootPath}/core/target"
+ }
+}
+
+// Extract version and artifactId values
+def apPom = new XmlParser().parse(file("${mapstructRootPath}/processor/pom.xml"))
+ext.apArtifactId = apPom.artifactId[0].text()
+ext.apVersion = apPom.parent[0].version[0].text()
+
+def libPom = new XmlParser().parse(file("${mapstructRootPath}/core/pom.xml"))
+ext.libArtifactId = libPom.artifactId[0].text()
+ext.libVersion = libPom.parent[0].version[0].text()
+
+dependencies {
+ annotationProcessor name: "${apArtifactId}-${apVersion}"
+ implementation name: "${libArtifactId}-${libVersion}"
+}
diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle
new file mode 100644
index 000000000..f62a77ab7
--- /dev/null
+++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'gradle-incremental-compilation-test'
\ No newline at end of file
diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java
new file mode 100644
index 000000000..13ae2ef5f
--- /dev/null
+++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.itest.gradle.lib;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ReportingPolicy;
+
+import org.mapstruct.itest.gradle.model.Target;
+import org.mapstruct.itest.gradle.model.Source;
+
+@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface TestMapper {
+ @Mapping(source = "value", target = "field")
+ public Target toTarget(Source source);
+}
diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/UnrelatedComponent.java b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/UnrelatedComponent.java
new file mode 100644
index 000000000..9f1095850
--- /dev/null
+++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/UnrelatedComponent.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.itest.gradle.lib;
+
+public class UnrelatedComponent {
+ public boolean unrelatedMethod() {
+ return true;
+ }
+}
diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/model/Source.java b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/model/Source.java
new file mode 100644
index 000000000..c7103aace
--- /dev/null
+++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/model/Source.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.itest.gradle.model;
+
+public class Source {
+ private int value;
+
+ public void setValue(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+}
diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/model/Target.java b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/model/Target.java
new file mode 100644
index 000000000..4b5a17110
--- /dev/null
+++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/model/Target.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.itest.gradle.model;
+
+public class Target {
+ private String field = getDefaultValue();
+ private String otherField;
+
+ public void setField(String field) {
+ this.field = field;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public void setOtherField(String otherField) {
+ this.otherField = otherField;
+ }
+
+ public String getOtherField() {
+ return otherField;
+ }
+
+ public String getDefaultValue() {
+ return "original";
+ }
+}
diff --git a/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors b/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors
new file mode 100644
index 000000000..172d1bb77
--- /dev/null
+++ b/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors
@@ -0,0 +1 @@
+org.mapstruct.ap.MappingProcessor,isolating