Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -114,65 +89,19 @@ public void grinder(Action<Img<?>> 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<File> 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<File, File> 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 {
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/com/diffplug/gradle/imagegrinder/Img.java
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
}

Expand Down
36 changes: 35 additions & 1 deletion src/main/java/com/diffplug/gradle/imagegrinder/Subpath.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,19 +18,36 @@

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('\\', '/');
Preconditions.checkArgument(childPath.startsWith(rootPath), "%s needs to start with %s", childPath, rootPath);
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;
}
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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", "[email protected]");

// 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", "[email protected]", "refresh.png", "[email protected]");

// remove a file, and only it is removed
delete("src/refresh.svg");
runAndAssert(TaskOutcome.SUCCESS).containsExactly("clean: refresh.svg");
assertFolderContent("dst").containsExactly("diffpluglogo.png", "[email protected]");

// 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", "[email protected]", "refresh.png", "[email protected]");
}
}