Skip to content

Commit ea6344e

Browse files
authored
Support configuration cache within a single daemon (JvmLocalCache) (#986)
2 parents 24d1ea5 + ca3560e commit ea6344e

File tree

9 files changed

+199
-90
lines changed

9 files changed

+199
-90
lines changed

plugin-gradle/CHANGES.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Support for Gradle Configuration Cache* ([#982](https://github.com/diffplug/spotless/pull/982), [#986](https://github.com/diffplug/spotless/pull/986))
8+
* *Spotless must run on the same daemon that wrote the configuration cache. If it isn't, you'll get this error message:
9+
* ```
10+
Spotless JVM-local cache is stale. Regenerate the cache with
11+
rm -rf .gradle/configuration-cache
12+
```
13+
* To make this daemon-restriction obsolete, please see and upvote [#987](https://github.com/diffplug/spotless/issues/987).
614
### Changed
7-
* **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.
15+
* **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))
16+
* 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).
817
* **BREAKING** `createIndepentApplyTask(String taskName)` now requires that `taskName` does not end with `Apply`
918
* Bump minimum required Gradle from `6.1` to `6.1.1`.
1019

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,7 @@ protected void setupTask(SpotlessTask task) {
734734
task.setSteps(steps);
735735
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget));
736736
spotless.getRegisterDependenciesTask().hookSubprojectTask(task);
737-
if (getRatchetFrom() != null) {
738-
task.setupRatchet(getRatchetFrom());
739-
}
737+
task.setupRatchet(getRatchetFrom() != null ? getRatchetFrom() : "");
740738
}
741739

742740
/** Returns the project that this extension is attached to. */
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2021 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.gradle.spotless;
17+
18+
import java.io.File;
19+
import java.io.Serializable;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Objects;
24+
25+
import org.gradle.api.GradleException;
26+
import org.gradle.api.Task;
27+
28+
import com.diffplug.spotless.FileSignature;
29+
30+
class JvmLocalCache {
31+
private static GradleException cacheIsStale() {
32+
return new GradleException("Spotless JVM-local cache is stale. Regenerate the cache with\n" +
33+
" " + (FileSignature.machineIsWin() ? "rmdir /q /s" : "rm -rf") + " .gradle/configuration-cache\n" +
34+
"To make this workaround obsolete, please upvote https://github.com/diffplug/spotless/issues/987");
35+
}
36+
37+
interface LiveCache<T> {
38+
T get();
39+
40+
void set(T value);
41+
}
42+
43+
static <T> LiveCache<T> createLive(Task task, String propertyName) {
44+
return new LiveCacheKeyImpl<T>(new InternalCacheKey(task.getProject().getProjectDir(), task.getPath(), propertyName));
45+
}
46+
47+
static class LiveCacheKeyImpl<T> implements LiveCache<T>, Serializable {
48+
InternalCacheKey internalKey;
49+
50+
LiveCacheKeyImpl(InternalCacheKey internalKey) {
51+
this.internalKey = internalKey;
52+
}
53+
54+
@Override
55+
public void set(T value) {
56+
daemonState.put(internalKey, value);
57+
}
58+
59+
@Override
60+
public T get() {
61+
Object value = daemonState.get(internalKey);
62+
if (value == null) {
63+
// TODO: throw TriggerConfigurationException(); (see https://github.com/diffplug/spotless/issues/987)
64+
throw cacheIsStale();
65+
} else {
66+
return (T) value;
67+
}
68+
}
69+
}
70+
71+
private static Map<InternalCacheKey, Object> daemonState = Collections.synchronizedMap(new HashMap<>());
72+
73+
private static class InternalCacheKey implements Serializable {
74+
private File projectDir;
75+
private String taskPath;
76+
private String propertyName;
77+
78+
InternalCacheKey(File projectDir, String taskPath, String keyName) {
79+
this.projectDir = projectDir;
80+
this.taskPath = taskPath;
81+
this.propertyName = keyName;
82+
}
83+
84+
@Override
85+
public boolean equals(Object o) {
86+
if (this == o)
87+
return true;
88+
if (o == null || getClass() != o.getClass())
89+
return false;
90+
InternalCacheKey that = (InternalCacheKey) o;
91+
return projectDir.equals(that.projectDir) && taskPath.equals(that.taskPath) && propertyName.equals(that.propertyName);
92+
}
93+
94+
@Override
95+
public int hashCode() {
96+
return Objects.hash(projectDir, taskPath, propertyName);
97+
}
98+
}
99+
}

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,16 @@
1515
*/
1616
package com.diffplug.gradle.spotless;
1717

18-
import java.io.File;
19-
import java.io.IOException;
20-
import java.nio.charset.StandardCharsets;
21-
2218
import javax.inject.Inject;
2319

2420
import org.gradle.api.DefaultTask;
2521
import org.gradle.api.provider.Property;
2622
import org.gradle.api.services.BuildServiceRegistry;
2723
import org.gradle.api.tasks.Internal;
28-
import org.gradle.api.tasks.OutputFile;
2924
import org.gradle.api.tasks.TaskAction;
3025
import org.gradle.build.event.BuildEventsListenerRegistry;
3126

3227
import com.diffplug.common.base.Preconditions;
33-
import com.diffplug.common.io.Files;
3428

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

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

62-
File unitOutput;
63-
64-
@OutputFile
65-
public File getUnitOutput() {
66-
return unitOutput;
67-
}
68-
6953
void setup() {
7054
Preconditions.checkArgument(getProject().getRootProject() == getProject(), "Can only be used on the root project");
71-
unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies");
7255

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

7861
@TaskAction
79-
public void trivialFunction() throws IOException {
80-
Files.createParentDirs(unitOutput);
81-
Files.write(Integer.toString(1), unitOutput, StandardCharsets.UTF_8);
62+
public void trivialFunction() {
63+
// nothing to do :)
8264
}
8365

8466
@Internal

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.gradle.api.tasks.PathSensitivity;
3939
import org.gradle.work.Incremental;
4040

41+
import com.diffplug.gradle.spotless.JvmLocalCache.LiveCache;
4142
import com.diffplug.spotless.FormatExceptionPolicy;
4243
import com.diffplug.spotless.FormatExceptionPolicyStrict;
4344
import com.diffplug.spotless.Formatter;
@@ -48,6 +49,10 @@ public abstract class SpotlessTask extends DefaultTask {
4849
@Internal
4950
abstract Property<SpotlessTaskService> getTaskService();
5051

52+
protected <T> LiveCache<T> createLive(String keyName) {
53+
return JvmLocalCache.createLive(this, keyName);
54+
}
55+
5156
// set by SpotlessExtension, but possibly overridden by FormatExtension
5257
protected String encoding = "UTF-8";
5358

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

63-
protected transient LineEnding.Policy lineEndingsPolicy;
68+
protected final LiveCache<LineEnding.Policy> lineEndingsPolicy = createLive("lineEndingsPolicy");
6469

6570
@Input
6671
public LineEnding.Policy getLineEndingsPolicy() {
67-
return lineEndingsPolicy;
72+
return lineEndingsPolicy.get();
6873
}
6974

7075
public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
71-
this.lineEndingsPolicy = Objects.requireNonNull(lineEndingsPolicy);
76+
this.lineEndingsPolicy.set(lineEndingsPolicy);
7277
}
7378

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

8693
public void setupRatchet(String ratchetFrom) {
87-
ratchet = getTaskService().get().getRatchet();
88-
File projectDir = getProjectDir().get().getAsFile();
89-
rootTreeSha = ratchet.rootTreeShaOf(projectDir, ratchetFrom);
90-
subtreeSha = ratchet.subtreeShaOf(projectDir, rootTreeSha);
94+
this.ratchetFrom = ratchetFrom;
95+
if (!ratchetFrom.isEmpty()) {
96+
ratchet = getTaskService().get().getRatchet();
97+
File projectDir = getProjectDir().get().getAsFile();
98+
rootTreeSha = ratchet.rootTreeShaOf(projectDir, ratchetFrom);
99+
subtreeSha = ratchet.subtreeShaOf(projectDir, rootTreeSha);
100+
} else {
101+
subtreeSha = ObjectId.zeroId();
102+
}
91103
}
92104

93105
@Internal
@@ -105,6 +117,9 @@ ObjectId getRootTreeSha() {
105117

106118
@Input
107119
public ObjectId getRatchetSha() {
120+
if (subtreeSha == null) {
121+
setupRatchet(ratchetFrom);
122+
}
108123
return subtreeSha;
109124
}
110125

@@ -143,19 +158,22 @@ public File getOutputDirectory() {
143158
return outputDirectory;
144159
}
145160

146-
protected transient List<FormatterStep> steps = new ArrayList<>();
161+
protected final LiveCache<List<FormatterStep>> steps = createLive("steps");
162+
{
163+
steps.set(new ArrayList<FormatterStep>());
164+
}
147165

148166
@Input
149167
public List<FormatterStep> getSteps() {
150-
return Collections.unmodifiableList(steps);
168+
return Collections.unmodifiableList(steps.get());
151169
}
152170

153171
public void setSteps(List<FormatterStep> steps) {
154-
this.steps = PluginGradlePreconditions.requireElementsNonNull(steps);
172+
this.steps.set(PluginGradlePreconditions.requireElementsNonNull(steps));
155173
}
156174

157175
public boolean addStep(FormatterStep step) {
158-
return this.steps.add(Objects.requireNonNull(step));
176+
return this.steps.get().add(Objects.requireNonNull(step));
159177
}
160178

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

171189
Formatter buildFormatter() {
172190
return Formatter.builder()
173-
.lineEndingsPolicy(lineEndingsPolicy)
191+
.lineEndingsPolicy(lineEndingsPolicy.get())
174192
.encoding(Charset.forName(encoding))
175193
.rootDir(getProjectDir().get().getAsFile().toPath())
176-
.steps(steps)
194+
.steps(steps.get())
177195
.exceptionPolicy(exceptionPolicy)
178196
.build();
179197
}

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,17 @@ public void performAction(InputChanges inputs) throws Exception {
6464
Files.createDirectories(outputDirectory.toPath());
6565
}
6666

67-
if (lineEndingsPolicy != null) {
68-
try (Formatter formatter = buildFormatter()) {
69-
for (FileChange fileChange : inputs.getFileChanges(target)) {
70-
File input = fileChange.getFile();
71-
if (fileChange.getChangeType() == ChangeType.REMOVED) {
72-
deletePreviousResult(input);
73-
} else {
74-
if (input.isFile()) {
75-
processInputFile(formatter, input);
76-
}
67+
try (Formatter formatter = buildFormatter()) {
68+
for (FileChange fileChange : inputs.getFileChanges(target)) {
69+
File input = fileChange.getFile();
70+
if (fileChange.getChangeType() == ChangeType.REMOVED) {
71+
deletePreviousResult(input);
72+
} else {
73+
if (input.isFile()) {
74+
processInputFile(formatter, input);
7775
}
7876
}
7977
}
80-
} else {
81-
throw new GradleException("Spotless doesn't support configuration cache yet.\n" +
82-
"Rerun with --no-configuration-cache");
8378
}
8479
}
8580

0 commit comments

Comments
 (0)