Skip to content

Commit

Permalink
[Gradle] Support incremental compilation
Browse files Browse the repository at this point in the history
  • Loading branch information
ihostage committed Sep 11, 2023
1 parent d0494a2 commit d7fb975
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 93 deletions.
49 changes: 27 additions & 22 deletions gradle-twirl/src/main/java/play/twirl/gradle/TwirlCompile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, String> templateFormats = getTemplateFormats().get();
for (RelativeFile sourceFile : getSourceAsRelativeFiles()) {
Map<String, String> 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());
Expand All @@ -86,16 +103,4 @@ private String getFormatterType(Map<String, String> formats, File file) {
"Unknown template format of '%s'. Possible extentions: [%s]",
file.getName(), String.join(", ", formats.keySet()))));
}

private Iterable<RelativeFile> getSourceAsRelativeFiles() {
List<RelativeFile> relativeFiles = new ArrayList<>();
getSource()
.visit(
fvd -> {
if (fvd.getFile().isFile()) {
relativeFiles.add(new RelativeFile(fvd.getFile(), fvd.getRelativePath()));
}
});
return relativeFiles;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private TaskProvider<TwirlCompile> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TwirlCompileParams> {

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();
Expand All @@ -28,8 +70,8 @@ public void execute() {
Collection<String> imports = getParameters().getTemplateImports().get();
List<String> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChangeType> getChangeType();

RegularFileProperty getSourceFile();

DirectoryProperty getSourceDirectory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -96,6 +102,10 @@ static Stream<String> 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");
}
}
128 changes: 128 additions & 0 deletions gradle-twirl/src/test/java/play/twirl/gradle/SimpleProjectTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/
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<String, Object> 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();
}
}
Loading

0 comments on commit d7fb975

Please sign in to comment.