Skip to content

Commit

Permalink
Support configuration cache within a single daemon (JvmLocalCache) (#986
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nedtwigg authored Nov 9, 2021
2 parents 24d1ea5 + ca3560e commit ea6344e
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 90 deletions.
11 changes: 10 additions & 1 deletion plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).

## [Unreleased]
### Added
* Support for Gradle Configuration Cache* ([#982](https://github.com/diffplug/spotless/pull/982), [#986](https://github.com/diffplug/spotless/pull/986))
* *Spotless must run on the same daemon that wrote the configuration cache. If it isn't, you'll get this error message:
* ```
Spotless JVM-local cache is stale. Regenerate the cache with
rm -rf .gradle/configuration-cache
```
* To make this daemon-restriction obsolete, please see and upvote [#987](https://github.com/diffplug/spotless/issues/987).
### Changed
* **BREAKING** Previously, many projects required `buildscript { repositories { mavenCentral() }}` at the top of their root project, because Spotless resolved its dependencies using the buildscript repositories. Spotless now resolves its dependencies from the normal project repositories of each project with a `spotless {...}` block. This means that you can remove the `buildscript {}` block, but you still need a `repositories { mavenCentral() }` (or similar) in each project which is using Spotless.
* **BREAKING** Previously, many projects required `buildscript { repositories { mavenCentral() }}` at the top of their root project, because Spotless resolved its dependencies using the buildscript repositories. Spotless now resolves its dependencies from the normal project repositories of each project with a `spotless {...}` block. This means that you can remove the `buildscript {}` block, but you still need a `repositories { mavenCentral() }` (or similar) in each project which is using Spotless. ([#980](https://github.com/diffplug/spotless/pull/980), [#983](https://github.com/diffplug/spotless/pull/983))
* If you prefer the old behavior, we are open to adding that back as a new feature, see [#984 predeclared dependencies](https://github.com/diffplug/spotless/issues/984).
* **BREAKING** `createIndepentApplyTask(String taskName)` now requires that `taskName` does not end with `Apply`
* Bump minimum required Gradle from `6.1` to `6.1.1`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -734,9 +734,7 @@ protected void setupTask(SpotlessTask task) {
task.setSteps(steps);
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget));
spotless.getRegisterDependenciesTask().hookSubprojectTask(task);
if (getRatchetFrom() != null) {
task.setupRatchet(getRatchetFrom());
}
task.setupRatchet(getRatchetFrom() != null ? getRatchetFrom() : "");
}

/** Returns the project that this extension is attached to. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2021 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.gradle.spotless;

import java.io.File;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.gradle.api.GradleException;
import org.gradle.api.Task;

import com.diffplug.spotless.FileSignature;

class JvmLocalCache {
private static GradleException cacheIsStale() {
return new GradleException("Spotless JVM-local cache is stale. Regenerate the cache with\n" +
" " + (FileSignature.machineIsWin() ? "rmdir /q /s" : "rm -rf") + " .gradle/configuration-cache\n" +
"To make this workaround obsolete, please upvote https://github.com/diffplug/spotless/issues/987");
}

interface LiveCache<T> {
T get();

void set(T value);
}

static <T> LiveCache<T> createLive(Task task, String propertyName) {
return new LiveCacheKeyImpl<T>(new InternalCacheKey(task.getProject().getProjectDir(), task.getPath(), propertyName));
}

static class LiveCacheKeyImpl<T> implements LiveCache<T>, Serializable {
InternalCacheKey internalKey;

LiveCacheKeyImpl(InternalCacheKey internalKey) {
this.internalKey = internalKey;
}

@Override
public void set(T value) {
daemonState.put(internalKey, value);
}

@Override
public T get() {
Object value = daemonState.get(internalKey);
if (value == null) {
// TODO: throw TriggerConfigurationException(); (see https://github.com/diffplug/spotless/issues/987)
throw cacheIsStale();
} else {
return (T) value;
}
}
}

private static Map<InternalCacheKey, Object> daemonState = Collections.synchronizedMap(new HashMap<>());

private static class InternalCacheKey implements Serializable {
private File projectDir;
private String taskPath;
private String propertyName;

InternalCacheKey(File projectDir, String taskPath, String keyName) {
this.projectDir = projectDir;
this.taskPath = taskPath;
this.propertyName = keyName;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
InternalCacheKey that = (InternalCacheKey) o;
return projectDir.equals(that.projectDir) && taskPath.equals(that.taskPath) && propertyName.equals(that.propertyName);
}

@Override
public int hashCode() {
return Objects.hash(projectDir, taskPath, propertyName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,16 @@
*/
package com.diffplug.gradle.spotless;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import javax.inject.Inject;

import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.services.BuildServiceRegistry;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.build.event.BuildEventsListenerRegistry;

import com.diffplug.common.base.Preconditions;
import com.diffplug.common.io.Files;

/**
* NOT AN END-USER TASK, DO NOT USE FOR ANYTHING!
Expand All @@ -46,39 +40,27 @@ public abstract class RegisterDependenciesTask extends DefaultTask {
static final String TASK_NAME = "spotlessInternalRegisterDependencies";

void hookSubprojectTask(SpotlessTask task) {
// TODO: in the future, we might use this hook to add an optional perf improvement
// spotlessRoot {
// TODO: in the future, we might use this hook to implement #984
// spotlessSetup {
// java { googleJavaFormat('1.2') }
// ...etc
// }
// The point would be to reuse configurations from the root project,
// with the restriction that you have to declare every formatter in
// the root, and you'd get an error if you used a formatter somewhere
// which you didn't declare in the root. That's a problem for the future
// though, not today!
// it's also needed to make sure that jvmLocalCache gets set
// in the SpotlessTaskService before any spotless tasks run
task.dependsOn(this);
}

File unitOutput;

@OutputFile
public File getUnitOutput() {
return unitOutput;
}

void setup() {
Preconditions.checkArgument(getProject().getRootProject() == getProject(), "Can only be used on the root project");
unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies");

BuildServiceRegistry buildServices = getProject().getGradle().getSharedServices();
getTaskService().set(buildServices.registerIfAbsent("SpotlessTaskService", SpotlessTaskService.class, spec -> {}));
getBuildEventsListenerRegistry().onTaskCompletion(getTaskService());
}

@TaskAction
public void trivialFunction() throws IOException {
Files.createParentDirs(unitOutput);
Files.write(Integer.toString(1), unitOutput, StandardCharsets.UTF_8);
public void trivialFunction() {
// nothing to do :)
}

@Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.work.Incremental;

import com.diffplug.gradle.spotless.JvmLocalCache.LiveCache;
import com.diffplug.spotless.FormatExceptionPolicy;
import com.diffplug.spotless.FormatExceptionPolicyStrict;
import com.diffplug.spotless.Formatter;
Expand All @@ -48,6 +49,10 @@ public abstract class SpotlessTask extends DefaultTask {
@Internal
abstract Property<SpotlessTaskService> getTaskService();

protected <T> LiveCache<T> createLive(String keyName) {
return JvmLocalCache.createLive(this, keyName);
}

// set by SpotlessExtension, but possibly overridden by FormatExtension
protected String encoding = "UTF-8";

Expand All @@ -60,15 +65,15 @@ public void setEncoding(String encoding) {
this.encoding = Objects.requireNonNull(encoding);
}

protected transient LineEnding.Policy lineEndingsPolicy;
protected final LiveCache<LineEnding.Policy> lineEndingsPolicy = createLive("lineEndingsPolicy");

@Input
public LineEnding.Policy getLineEndingsPolicy() {
return lineEndingsPolicy;
return lineEndingsPolicy.get();
}

public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
this.lineEndingsPolicy = Objects.requireNonNull(lineEndingsPolicy);
this.lineEndingsPolicy.set(lineEndingsPolicy);
}

/*** API which performs git up-to-date tasks. */
Expand All @@ -82,12 +87,19 @@ public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
* compared to using the project root.
*/
private transient ObjectId subtreeSha = ObjectId.zeroId();
/** Stored so that the configuration cache can recreate the GitRatchetGradle state. */
protected String ratchetFrom;

public void setupRatchet(String ratchetFrom) {
ratchet = getTaskService().get().getRatchet();
File projectDir = getProjectDir().get().getAsFile();
rootTreeSha = ratchet.rootTreeShaOf(projectDir, ratchetFrom);
subtreeSha = ratchet.subtreeShaOf(projectDir, rootTreeSha);
this.ratchetFrom = ratchetFrom;
if (!ratchetFrom.isEmpty()) {
ratchet = getTaskService().get().getRatchet();
File projectDir = getProjectDir().get().getAsFile();
rootTreeSha = ratchet.rootTreeShaOf(projectDir, ratchetFrom);
subtreeSha = ratchet.subtreeShaOf(projectDir, rootTreeSha);
} else {
subtreeSha = ObjectId.zeroId();
}
}

@Internal
Expand All @@ -105,6 +117,9 @@ ObjectId getRootTreeSha() {

@Input
public ObjectId getRatchetSha() {
if (subtreeSha == null) {
setupRatchet(ratchetFrom);
}
return subtreeSha;
}

Expand Down Expand Up @@ -143,19 +158,22 @@ public File getOutputDirectory() {
return outputDirectory;
}

protected transient List<FormatterStep> steps = new ArrayList<>();
protected final LiveCache<List<FormatterStep>> steps = createLive("steps");
{
steps.set(new ArrayList<FormatterStep>());
}

@Input
public List<FormatterStep> getSteps() {
return Collections.unmodifiableList(steps);
return Collections.unmodifiableList(steps.get());
}

public void setSteps(List<FormatterStep> steps) {
this.steps = PluginGradlePreconditions.requireElementsNonNull(steps);
this.steps.set(PluginGradlePreconditions.requireElementsNonNull(steps));
}

public boolean addStep(FormatterStep step) {
return this.steps.add(Objects.requireNonNull(step));
return this.steps.get().add(Objects.requireNonNull(step));
}

/** Returns the name of this format. */
Expand All @@ -170,10 +188,10 @@ String formatName() {

Formatter buildFormatter() {
return Formatter.builder()
.lineEndingsPolicy(lineEndingsPolicy)
.lineEndingsPolicy(lineEndingsPolicy.get())
.encoding(Charset.forName(encoding))
.rootDir(getProjectDir().get().getAsFile().toPath())
.steps(steps)
.steps(steps.get())
.exceptionPolicy(exceptionPolicy)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,17 @@ public void performAction(InputChanges inputs) throws Exception {
Files.createDirectories(outputDirectory.toPath());
}

if (lineEndingsPolicy != null) {
try (Formatter formatter = buildFormatter()) {
for (FileChange fileChange : inputs.getFileChanges(target)) {
File input = fileChange.getFile();
if (fileChange.getChangeType() == ChangeType.REMOVED) {
deletePreviousResult(input);
} else {
if (input.isFile()) {
processInputFile(formatter, input);
}
try (Formatter formatter = buildFormatter()) {
for (FileChange fileChange : inputs.getFileChanges(target)) {
File input = fileChange.getFile();
if (fileChange.getChangeType() == ChangeType.REMOVED) {
deletePreviousResult(input);
} else {
if (input.isFile()) {
processInputFile(formatter, input);
}
}
}
} else {
throw new GradleException("Spotless doesn't support configuration cache yet.\n" +
"Rerun with --no-configuration-cache");
}
}

Expand Down
Loading

0 comments on commit ea6344e

Please sign in to comment.