From d7fb9759439849cee30ae44208ce2dcad445c82b Mon Sep 17 00:00:00 2001 From: Sergey Morgunov Date: Mon, 11 Sep 2023 16:24:18 +0300 Subject: [PATCH] [Gradle] Support incremental compilation --- .../java/play/twirl/gradle/TwirlCompile.java | 49 ++++--- .../java/play/twirl/gradle/TwirlPlugin.java | 2 +- .../gradle/internal/TwirlCompileAction.java | 48 ++++++- .../gradle/internal/TwirlCompileParams.java | 3 + .../twirl/gradle/AbstractFunctionalTest.java | 12 +- .../play/twirl/gradle/SimpleProjectTest.java | 128 ++++++++++++++++++ .../gradle/TwirlPluginFunctionalTest.java | 66 --------- 7 files changed, 215 insertions(+), 93 deletions(-) create mode 100644 gradle-twirl/src/test/java/play/twirl/gradle/SimpleProjectTest.java delete mode 100644 gradle-twirl/src/test/java/play/twirl/gradle/TwirlPluginFunctionalTest.java diff --git a/gradle-twirl/src/main/java/play/twirl/gradle/TwirlCompile.java b/gradle-twirl/src/main/java/play/twirl/gradle/TwirlCompile.java index ed38d79c..c6615f85 100644 --- a/gradle-twirl/src/main/java/play/twirl/gradle/TwirlCompile.java +++ b/gradle-twirl/src/main/java/play/twirl/gradle/TwirlCompile.java @@ -4,32 +4,45 @@ package play.twirl.gradle; import java.io.File; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import javax.inject.Inject; +import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileType; +import org.gradle.api.file.RelativePath; import org.gradle.api.internal.file.RelativeFile; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.IgnoreEmptyDirectories; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.SourceTask; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.TaskAction; import org.gradle.internal.FileUtils; +import org.gradle.work.FileChange; +import org.gradle.work.Incremental; +import org.gradle.work.InputChanges; import org.gradle.workers.WorkQueue; import org.gradle.workers.WorkerExecutor; import play.twirl.gradle.internal.TwirlCompileAction; /** Gradle task for compiling Twirl templates into Scala code. */ -public abstract class TwirlCompile extends SourceTask { +public abstract class TwirlCompile extends DefaultTask { @InputFiles + @Incremental + @IgnoreEmptyDirectories + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getSource(); + + @Classpath public abstract ConfigurableFileCollection getTwirlClasspath(); @OutputDirectory @@ -51,16 +64,20 @@ public abstract class TwirlCompile extends SourceTask { public abstract WorkerExecutor getWorkerExecutor(); @TaskAction - void compile() { - WorkQueue workQueue = - getWorkerExecutor() - .classLoaderIsolation(spec -> spec.getClasspath().from(getTwirlClasspath())); + void compile(InputChanges changes) { + for (FileChange change : changes.getFileChanges(getSource())) { + if (change.getFileType() == FileType.DIRECTORY) continue; + WorkQueue workQueue = + getWorkerExecutor() + .classLoaderIsolation(spec -> spec.getClasspath().from(getTwirlClasspath())); - Map templateFormats = getTemplateFormats().get(); - for (RelativeFile sourceFile : getSourceAsRelativeFiles()) { + Map templateFormats = getTemplateFormats().get(); + RelativeFile sourceFile = + new RelativeFile(change.getFile(), RelativePath.parse(true, change.getNormalizedPath())); workQueue.submit( TwirlCompileAction.class, parameters -> { + parameters.getChangeType().set(change.getChangeType()); parameters.getSourceFile().set(sourceFile.getFile()); parameters.getSourceDirectory().set(sourceFile.getBaseDir()); parameters.getDestinationDirectory().set(getDestinationDirectory()); @@ -86,16 +103,4 @@ private String getFormatterType(Map formats, File file) { "Unknown template format of '%s'. Possible extentions: [%s]", file.getName(), String.join(", ", formats.keySet())))); } - - private Iterable getSourceAsRelativeFiles() { - List relativeFiles = new ArrayList<>(); - getSource() - .visit( - fvd -> { - if (fvd.getFile().isFile()) { - relativeFiles.add(new RelativeFile(fvd.getFile(), fvd.getRelativePath())); - } - }); - return relativeFiles; - } } diff --git a/gradle-twirl/src/main/java/play/twirl/gradle/TwirlPlugin.java b/gradle-twirl/src/main/java/play/twirl/gradle/TwirlPlugin.java index 5e7215f6..ec873cff 100644 --- a/gradle-twirl/src/main/java/play/twirl/gradle/TwirlPlugin.java +++ b/gradle-twirl/src/main/java/play/twirl/gradle/TwirlPlugin.java @@ -121,7 +121,7 @@ private TaskProvider createTwirlCompileTask( twirlCompile -> { twirlCompile.setDescription("Compiles the " + twirlSource + "."); twirlCompile.getTwirlClasspath().setFrom(twirlConfiguration); - twirlCompile.setSource(twirlSource); + twirlCompile.getSource().setFrom(twirlSource); twirlCompile.getTemplateFormats().convention(twirlSource.getTemplateFormats()); twirlCompile.getTemplateImports().convention(twirlSource.getTemplateImports()); twirlCompile.getSourceEncoding().convention(twirlSource.getSourceEncoding()); diff --git a/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileAction.java b/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileAction.java index 1fd642fe..bcbdd55d 100644 --- a/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileAction.java +++ b/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileAction.java @@ -8,17 +8,59 @@ import java.util.List; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; +import org.gradle.work.ChangeType; import org.gradle.workers.WorkAction; import play.japi.twirl.compiler.TwirlCompiler; +import play.twirl.compiler.TwirlCompiler$; import scala.io.Codec; -/** Gradle work action that compile one Twirl template. */ +/** Gradle work action that compile or delete one Twirl template. */ public abstract class TwirlCompileAction implements WorkAction { private static final Logger LOGGER = Logging.getLogger(TwirlCompileAction.class); @Override public void execute() { + if (getParameters().getChangeType().get() == ChangeType.REMOVED) { + delete(); + } else { + compile(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void delete() { + try { + File sourceFile = getParameters().getSourceFile().getAsFile().get(); + File sourceDirectory = getParameters().getSourceDirectory().getAsFile().get(); + File destinationDirectory = getParameters().getDestinationDirectory().getAsFile().get(); + String sourceEncoding = getParameters().getSourceEncoding().get(); + // WA: Need to create a source file temporarily for correct calculate path of compiled + // template to delete + sourceFile.createNewFile(); + File compiledTemplate = + TwirlCompiler$.MODULE$ + .generatedFile( + sourceFile, + Codec.string2codec(sourceEncoding), + sourceDirectory, + destinationDirectory, + false) + ._2 + .file(); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Delete Twirl template {}", compiledTemplate.getCanonicalPath()); + } + // Delete temporary empty source file + sourceFile.delete(); + compiledTemplate.delete(); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + private void compile() { try { File sourceFile = getParameters().getSourceFile().getAsFile().get(); File sourceDirectory = getParameters().getSourceDirectory().getAsFile().get(); @@ -28,8 +70,8 @@ public void execute() { Collection imports = getParameters().getTemplateImports().get(); List constructorAnnotations = getParameters().getConstructorAnnotations().get(); String sourceEncoding = getParameters().getSourceEncoding().get(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug( + if (LOGGER.isInfoEnabled()) { + LOGGER.info( "Compile Twirl template [{}/{}] {} from {} into {}", formatterType, sourceEncoding, diff --git a/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileParams.java b/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileParams.java index d52911a5..1ec44e27 100644 --- a/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileParams.java +++ b/gradle-twirl/src/main/java/play/twirl/gradle/internal/TwirlCompileParams.java @@ -8,11 +8,14 @@ import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; +import org.gradle.work.ChangeType; import org.gradle.workers.WorkParameters; /** Parameters of compilation work action. */ public interface TwirlCompileParams extends WorkParameters { + Property getChangeType(); + RegularFileProperty getSourceFile(); DirectoryProperty getSourceDirectory(); diff --git a/gradle-twirl/src/test/java/play/twirl/gradle/AbstractFunctionalTest.java b/gradle-twirl/src/test/java/play/twirl/gradle/AbstractFunctionalTest.java index 7396a90a..d13826b2 100644 --- a/gradle-twirl/src/test/java/play/twirl/gradle/AbstractFunctionalTest.java +++ b/gradle-twirl/src/test/java/play/twirl/gradle/AbstractFunctionalTest.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; +import org.gradle.api.JavaVersion; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.BeforeEach; @@ -61,7 +62,12 @@ protected Path projectBuildPath(String path) { @BeforeEach void init() throws IOException { projectSourceDir = getProjectSourceDir(); - runner = GradleRunner.create().withProjectDir(projectDir).withPluginClasspath().forwardOutput(); + runner = + GradleRunner.create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withDebug(true) + .forwardOutput(); initFreemarker(); @@ -96,6 +102,10 @@ static Stream gradleVersions() { if (getScalaVersion().equals("3")) { // Gradle 7.5+ return Stream.of("7.6.2", "8.0.2", "8.3"); } + // https://docs.gradle.org/current/userguide/compatibility.html + if (JavaVersion.current().compareTo(JavaVersion.VERSION_17) >= 0) { // Gradle 7.3+ + return Stream.of("7.6.2", "8.0.2", "8.3"); + } return Stream.of("7.1.1", "7.6.2", "8.0.2", "8.3"); } } diff --git a/gradle-twirl/src/test/java/play/twirl/gradle/SimpleProjectTest.java b/gradle-twirl/src/test/java/play/twirl/gradle/SimpleProjectTest.java new file mode 100644 index 00000000..1725fd91 --- /dev/null +++ b/gradle-twirl/src/test/java/play/twirl/gradle/SimpleProjectTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ +package play.twirl.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.apache.commons.io.FileUtils; +import org.apache.groovy.util.Maps; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** A simple functional test to check a Twirl Gradle Plugin. */ +@DisplayName("Simple Gradle project with Twirl HTML template") +public class SimpleProjectTest extends AbstractFunctionalTest { + + @Override + protected File getProjectSourceDir() { + return new File("src/test/resources/simple"); + } + + @Override + protected String getBuildFileContent() { + Map params = + Maps.of( + "scalaVersion", getScalaVersion(), + "twirlVersion", getTwirlVersion()); + return templateProcess("build.gradle.kts.ftlh", params); + } + + @ParameterizedTest + @MethodSource("gradleVersions") + @DisplayName("Test common build") + void testCommonBuild(String gradleVersion) throws IOException { + File simpleSources = projectPath("src").toFile(); + FileUtils.copyDirectory(projectSourcePath("src").toFile(), simpleSources); + + BuildResult result = build(gradleVersion, "build"); + + BuildTask compileTwirlResult = result.task(":compileTwirl"); + assertThat(compileTwirlResult).isNotNull(); + assertThat(compileTwirlResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(projectBuildPath("generated/sources/twirl/main/a/b/html/c.template.scala")) + .isNotEmptyFile() + .binaryContent() + .asString() + .contains("import java.lang._", "class c @java.lang.Deprecated()"); + + BuildTask compileScalaResult = result.task(":compileScala"); + assertThat(compileScalaResult).isNotNull(); + assertThat(compileScalaResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(projectBuildPath("classes/scala/main/a/b/html/c.class")).isNotEmptyFile(); + } + + @ParameterizedTest + @MethodSource("gradleVersions") + @DisplayName("Test UP-TO-DATE behavior for build without changes in templates") + void testUpToDateBuild(String gradleVersion) throws IOException { + File simpleSources = projectPath("src").toFile(); + FileUtils.copyDirectory(projectSourcePath("src").toFile(), simpleSources); + + build(gradleVersion, "build"); + + BuildResult result = build(gradleVersion, "build"); + + BuildTask compileTwirlResult = result.task(":compileTwirl"); + assertThat(compileTwirlResult).isNotNull(); + assertThat(compileTwirlResult.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); + + BuildTask compileScalaResult = result.task(":compileScala"); + assertThat(compileScalaResult).isNotNull(); + assertThat(compileScalaResult.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); + } + + @ParameterizedTest + @MethodSource("gradleVersions") + @DisplayName("Test incremental compilation after add and delete template") + void testIncrementalBuild(String gradleVersion) throws IOException { + File simpleSources = projectPath("src").toFile(); + FileUtils.copyDirectory(projectSourcePath("src").toFile(), simpleSources); + + build(gradleVersion, "build"); + + // Add new Twirl template + Path newTemplate = projectPath("src/main/twirl/a/b/d.scala.html"); + Files.copy(projectSourcePath("src/main/twirl/a/b/c.scala.html"), newTemplate); + + BuildResult result = build(gradleVersion, "build"); + + BuildTask compileTwirlResult = result.task(":compileTwirl"); + assertThat(compileTwirlResult).isNotNull(); + assertThat(compileTwirlResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + + assertThat(projectBuildPath("generated/sources/twirl/main/a/b/html/d.template.scala")) + .isNotEmptyFile(); + + BuildTask compileScalaResult = result.task(":compileScala"); + assertThat(compileScalaResult).isNotNull(); + assertThat(compileScalaResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(projectBuildPath("classes/scala/main/a/b/html/d.class")).isNotEmptyFile(); + + // Delete twirl template + Files.delete(newTemplate); + + result = build(gradleVersion, "build"); + + compileTwirlResult = result.task(":compileTwirl"); + assertThat(compileTwirlResult).isNotNull(); + assertThat(compileTwirlResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + + assertThat(projectBuildPath("generated/sources/twirl/main/a/b/html/d.template.scala")) + .doesNotExist(); + + compileScalaResult = result.task(":compileScala"); + assertThat(compileScalaResult).isNotNull(); + assertThat(compileScalaResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(projectBuildPath("classes/scala/main/a/b/html/d.class")).doesNotExist(); + } +} diff --git a/gradle-twirl/src/test/java/play/twirl/gradle/TwirlPluginFunctionalTest.java b/gradle-twirl/src/test/java/play/twirl/gradle/TwirlPluginFunctionalTest.java deleted file mode 100644 index de5cb1e9..00000000 --- a/gradle-twirl/src/test/java/play/twirl/gradle/TwirlPluginFunctionalTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. - */ -package play.twirl.gradle; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.File; -import java.io.IOException; -import java.util.Map; -import org.apache.commons.io.FileUtils; -import org.apache.groovy.util.Maps; -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.BuildTask; -import org.gradle.testkit.runner.TaskOutcome; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -/** A simple functional test to check a Twirl Gradle Plugin. */ -public class TwirlPluginFunctionalTest extends AbstractFunctionalTest { - - @Override - protected File getProjectSourceDir() { - return new File("src/test/resources/simple"); - } - - @Override - protected String getBuildFileContent() { - Map params = - Maps.of( - "scalaVersion", getScalaVersion(), - "twirlVersion", getTwirlVersion()); - return templateProcess("build.gradle.kts.ftlh", params); - } - - @ParameterizedTest - @MethodSource("gradleVersions") - @DisplayName("Test simple Gradle project with Twirl HTML template") - void testSimpleGradleProject(String gradleVersion) throws IOException { - File simpleSources = projectPath("src").toFile(); - FileUtils.copyDirectory(projectSourcePath("src").toFile(), simpleSources); - - BuildResult result = build(gradleVersion, "build"); - - BuildTask compileTwirlResult = result.task(":compileTwirl"); - assertThat(compileTwirlResult).isNotNull(); - assertThat(compileTwirlResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(projectBuildPath("generated/sources/twirl/main/a/b/html/c.template.scala")) - .isNotEmptyFile() - .binaryContent() - .asString() - .contains("import java.lang._", "class c @java.lang.Deprecated()"); - - BuildTask compileScalaResult = result.task(":compileScala"); - assertThat(compileScalaResult).isNotNull(); - assertThat(compileScalaResult.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(projectBuildPath("classes/scala/main/a/b/html/c.class")).isNotEmptyFile(); - - result = build(gradleVersion, "build"); - - compileTwirlResult = result.task(":compileTwirl"); - assertThat(compileTwirlResult).isNotNull(); - assertThat(compileTwirlResult.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); - } -}