diff --git a/CHANGES.md b/CHANGES.md index 23772c0..8cee10f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,10 @@ # ImageGrinder ## [Unreleased] +### Fixed +- Now supports the Gradle Configuration Cache, but we had to lose support for incremental build ([#9](https://github.com/diffplug/image-grinder/pull/9)). -## [2.2.0] - 2021-09-04 +## [2.2.0] - 2021-09-04 [YANKED] ### Added - Now supports the Gradle Configuration Cache ([#8](https://github.com/diffplug/image-grinder/pull/8)). - This required bumping our minimum required Gradle from `5.6` to `6.0`. diff --git a/README.md b/README.md index a63d0f9..25f44cc 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ imageGrinder { Every single file in `srcDir` needs to be an image that ImageGrinder can parse. Each image will be parsed, and wrapped into an [`Img`](https://javadoc.io/doc/com.diffplug.gradle/image-grinder/2.2.0/com/diffplug/gradle/imagegrinder/Img.html). Call its methods to grind it into whatever you need in the `dstDir`. -ImageGrinder uses the gradle [Worker API](https://docs.gradle.org/6.0/userguide/custom_tasks.html#worker_api) introduced in Gradle 5.6 to use all your CPU cores for grinding. It also uses gradle's [incremental task](https://docs.gradle.org/6.0/userguide/custom_tasks.html#incremental_tasks) support to do the minimum amount of grinding required. And if you're using the [configuration cache](https://docs.gradle.org/6.6/userguide/configuration_cache.html) introduced in Gradle 6.6, that'll work too for near-instant startup times. +ImageGrinder uses the gradle [Worker API](https://docs.gradle.org/6.6/userguide/custom_tasks.html#worker_api) to use all your CPU cores for grinding, the [buildcache](https://docs.gradle.org/6.6/userguide/build_cache.html) to minimize the necessary work, and it also supports the [configuration cache](https://docs.gradle.org/6.6/userguide/configuration_cache.html) for near-instant startup times. It does not currently support [incremental update](https://docs.gradle.org/6.0/userguide/custom_tasks.html#incremental_tasks), but if you go back to `2.1.3` you can get that back in return for losing the configuration cache (see [#9](https://github.com/diffplug/image-grinder/pull/9) for details). + ## Configuration avoidance diff --git a/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderPlugin.java b/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderPlugin.java index 655c6b1..2a08dd2 100644 --- a/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderPlugin.java +++ b/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderPlugin.java @@ -33,7 +33,6 @@ public void apply(Project project) { @Override public ImageGrinderTask create(String name) { ImageGrinderTask task = project.getTasks().create(name, ImageGrinderTask.class); - task.getBuildDir().set(project.getBuildDir()); if (name.startsWith("process")) { Task processResources = project.getTasks().getByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME); processResources.dependsOn(task); diff --git a/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderTask.java b/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderTask.java index 4c20e4e..03e4caa 100644 --- a/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderTask.java +++ b/src/main/java/com/diffplug/gradle/imagegrinder/ImageGrinderTask.java @@ -16,32 +16,24 @@ package com.diffplug.gradle.imagegrinder; -import com.diffplug.common.collect.HashMultimap; import java.io.File; import java.io.Serializable; import java.util.Objects; import java.util.Random; -import java.util.Set; import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileSystemOperations; -import org.gradle.api.file.FileType; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; -import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.TaskAction; -import org.gradle.work.ChangeType; -import org.gradle.work.FileChange; -import org.gradle.work.Incremental; -import org.gradle.work.InputChanges; import org.gradle.workers.WorkAction; import org.gradle.workers.WorkParameters; import org.gradle.workers.WorkQueue; @@ -57,19 +49,6 @@ * Worker requires that all arguments to its worker runnables ({@link ImageGrinderTask} * in this case) be Serializable. There's no way to serialize our {@link #grinder(Action)}, so we had * to use {@link SerializableRef} to sneakily pass our task to the worker. - * - * ## Tedious thing #2: Removal handling - * - * Tedious thing #2: .java to .class has a 1:1 mapping. But that is not true for these images - a pipeline - * might create two images from one source, and the number of outputs might even change based on the content - * of the input (e.g. skip hi-res versions of very large images). - * - * That means that when the user removes or changes an image, we need to remember exactly which files it - * created last time, or else we might end up with stale results lying around. So, this task has the - * {@link #map} field which is a multimap from source file to the dst files it created. When the task starts, - * it reads this map from disk, and when the task finishes, it writes it to disk. Whenever an {@link Img} is - * rendered, the filename that was written is saved to this map via the {@link Img#registerDstFile(String)} - * method. */ @CacheableTask public abstract class ImageGrinderTask extends DefaultTask { @@ -80,10 +59,6 @@ public ImageGrinderTask(WorkerExecutor workerExecutor) { this.workerExecutor = workerExecutor; } - @Internal - public abstract DirectoryProperty getBuildDir(); - - @Incremental @PathSensitive(PathSensitivity.RELATIVE) @InputDirectory public abstract DirectoryProperty getSrcDir(); @@ -114,65 +89,19 @@ public void grinder(Action> grinder) { public abstract FileSystemOperations getFs(); @TaskAction - public void performAction(InputChanges inputChanges) throws Exception { + public void performAction() throws Exception { Objects.requireNonNull(grinder, "grinder"); + getFs().delete(deleteSpec -> deleteSpec.delete(getDstDir())); - File cache = new File(getBuildDir().getAsFile().get(), "cache" + getName()); - if (!inputChanges.isIncremental()) { - getFs().delete(deleteSpec -> deleteSpec.delete(getDstDir().getAsFile().get())); - map = HashMultimap.create(); - } else { - readFromCache(cache); - } WorkQueue queue = workerExecutor.noIsolation(); - for (FileChange fileChange : inputChanges.getFileChanges(getSrcDir())) { - if (fileChange.getFileType() == FileType.DIRECTORY) { - continue; - } - boolean modifiedOrRemoved = fileChange.getChangeType() == ChangeType.MODIFIED || fileChange.getChangeType() == ChangeType.REMOVED; - boolean modifiedOrAdded = fileChange.getChangeType() == ChangeType.MODIFIED || fileChange.getChangeType() == ChangeType.ADDED; - if (modifiedOrRemoved) { - logger.info("clean: " + fileChange.getNormalizedPath()); - remove(fileChange.getFile()); - } - if (modifiedOrAdded) { - logger.info("render: " + fileChange.getNormalizedPath()); - queue.submit(RenderSvg.class, params -> { - params.getSourceFile().set(fileChange.getFile()); - params.getTaskRef().set(SerializableRef.create(ImageGrinderTask.this)); - }); - } - } - queue.await(); - writeToCache(cache); - } - - private void remove(File srcFile) { - synchronized (map) { - Set toDelete = map.removeAll(srcFile); - getFs().delete(spec -> { - spec.delete(toDelete.toArray()); + getSrcDir().get().getAsFileTree().visit(fileVisit -> { + logger.info("render: " + fileVisit.getRelativePath()); + queue.submit(RenderSvg.class, params -> { + params.getSourceFile().set(fileVisit.getFile()); + params.getTaskRef().set(SerializableRef.create(ImageGrinderTask.this)); }); - } - } - - public boolean debug = false; - - HashMultimap map; - - @SuppressWarnings("unchecked") - private void readFromCache(File file) { - if (file.exists()) { - map = SerializableMisc.fromFile(HashMultimap.class, file); - } else { - map = HashMultimap.create(); - } - } - - private void writeToCache(File file) { - synchronized (map) { - SerializableMisc.toFile(map, file); - } + }); + queue.await(); } public interface RenderSvgParams extends WorkParameters { diff --git a/src/main/java/com/diffplug/gradle/imagegrinder/Img.java b/src/main/java/com/diffplug/gradle/imagegrinder/Img.java index 587326f..2af2b42 100644 --- a/src/main/java/com/diffplug/gradle/imagegrinder/Img.java +++ b/src/main/java/com/diffplug/gradle/imagegrinder/Img.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright (C) 2020-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,9 +83,10 @@ public void renderFull(String fullPath, Size size) { File registerDstFile(String fullPath) { File file = new File(task.getDstDir().getAsFile().get(), fullPath); FileMisc.mkdirs(file.getParentFile()); - synchronized (task.map) { - task.map.put(new File(task.getSrcDir().getAsFile().get(), subpath.full()), file); - } + // TODO: useful for incremental build in the future, perhaps + // synchronized (task.map) { + // task.map.put(new File(task.getSrcDir().getAsFile().get(), subpath.full()), file); + // } return file; } diff --git a/src/main/java/com/diffplug/gradle/imagegrinder/Subpath.java b/src/main/java/com/diffplug/gradle/imagegrinder/Subpath.java index 08763d3..a223768 100644 --- a/src/main/java/com/diffplug/gradle/imagegrinder/Subpath.java +++ b/src/main/java/com/diffplug/gradle/imagegrinder/Subpath.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright (C) 2020-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,21 @@ import com.diffplug.common.base.Preconditions; import java.io.File; +import org.gradle.api.file.FileSystemLocationProperty; public class Subpath { private final String full; private final String extension; private final String withoutExtension; + public static Subpath from(FileSystemLocationProperty root, FileSystemLocationProperty child) { + return from(root.getAsFile().get(), child.getAsFile().get()); + } + + public static Subpath from(FileSystemLocationProperty root, File child) { + return from(root.getAsFile().get(), child); + } + public static Subpath from(File root, File child) { String rootPath = root.getAbsolutePath().replace('\\', '/') + '/'; String childPath = child.getAbsolutePath().replace('\\', '/'); @@ -31,6 +40,14 @@ public static Subpath from(File root, File child) { return new Subpath(childPath.substring(rootPath.length())); } + public File resolve(File root) { + return new File(root, full); + } + + public File resolve(FileSystemLocationProperty root) { + return resolve(root.getAsFile().get()); + } + public String full() { return full; } @@ -62,4 +79,21 @@ static String extension(String subpath) { Preconditions.checkArgument(idx < subpath.length() - 1, "'%s' can't end in '.'", subpath); return subpath.substring(idx + 1); } + + @Override + public int hashCode() { + return full.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof Subpath) { + Subpath sub = (Subpath) other; + return sub.full.equals(full); + } else { + return false; + } + } } diff --git a/src/test/java/com/diffplug/gradle/imagegrinder/ImageGrinderPluginTest.java b/src/test/java/com/diffplug/gradle/imagegrinder/ImageGrinderPluginTest.java index 5824415..be354ee 100644 --- a/src/test/java/com/diffplug/gradle/imagegrinder/ImageGrinderPluginTest.java +++ b/src/test/java/com/diffplug/gradle/imagegrinder/ImageGrinderPluginTest.java @@ -117,35 +117,4 @@ public void testUpToDate() throws Exception { runAndAssert(TaskOutcome.SUCCESS); runAndAssert(TaskOutcome.UP_TO_DATE); } - - @Test - public void testIncremental() throws Exception { - writeBuild(); - - // one file - write("src/refresh.svg", readTestResource("refresh.svg")); - runAndAssert(TaskOutcome.SUCCESS).containsExactly("render: refresh.svg"); - assertFolderContent("dst").containsExactly("refresh.png", "refresh@2x.png"); - - // add a file, and only it changes - write("src/diffpluglogo.svg", readTestResource("diffpluglogo.svg")); - runAndAssert(TaskOutcome.SUCCESS).containsExactly("render: diffpluglogo.svg"); - assertFolderContent("dst").containsExactly("diffpluglogo.png", "diffpluglogo@2x.png", "refresh.png", "refresh@2x.png"); - - // remove a file, and only it is removed - delete("src/refresh.svg"); - runAndAssert(TaskOutcome.SUCCESS).containsExactly("clean: refresh.svg"); - assertFolderContent("dst").containsExactly("diffpluglogo.png", "diffpluglogo@2x.png"); - - // remove another file, and we end up with an empty directory - delete("src/diffpluglogo.svg"); - runAndAssert(TaskOutcome.SUCCESS).containsExactly("clean: diffpluglogo.svg"); - assertFolderContent("dst").isEmpty(); - - // add them both, and they're both rendered - write("src/refresh.svg", readTestResource("refresh.svg")); - write("src/diffpluglogo.svg", readTestResource("diffpluglogo.svg")); - runAndAssert(TaskOutcome.SUCCESS).containsExactlyInAnyOrder("render: refresh.svg", "render: diffpluglogo.svg"); - assertFolderContent("dst").containsExactly("diffpluglogo.png", "diffpluglogo@2x.png", "refresh.png", "refresh@2x.png"); - } }