Skip to content

Commit 2ec663d

Browse files
authored
Fix remote build cache (#2298)
2 parents 93cf18a + 76c5bb3 commit 2ec663d

File tree

5 files changed

+206
-21
lines changed

5 files changed

+206
-21
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1313
### Added
1414
* Support for `rdf` ([#2261](https://github.com/diffplug/spotless/pull/2261))
1515
* Support for `buf` on maven plugin ([#2291](https://github.com/diffplug/spotless/pull/2291))
16+
* `ConfigurationCacheHack` so we can support Gradle's configuration cache and remote build cache at the same time. ([TODO]()fixes [#2168](https://github.com/diffplug/spotless/issues/2168))
1617
### Changed
1718
* Support configuring the Equo P2 cache. ([#2238](https://github.com/diffplug/spotless/pull/2238))
1819
* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2024 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.spotless;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.Objects;
22+
23+
/**
24+
* Gradle requires three things:
25+
* - Gradle defines cache equality based on your serialized representation
26+
* - Combined with remote build cache, you cannot have any absolute paths in
27+
* your serialized representation
28+
* - Combined with configuration cache, you must be able to roundtrip yourself
29+
* through serialization
30+
*
31+
* These requirements are at odds with each other, as described in these issues
32+
* - Gradle issue to define custom equality
33+
* https://github.com/gradle/gradle/issues/29816
34+
* - Spotless plea for developer cache instead of configuration cache
35+
* https://github.com/diffplug/spotless/issues/987
36+
* - Spotless cache miss bug fixed by this class
37+
* https://github.com/diffplug/spotless/issues/2168
38+
*
39+
* This class is a `List<FormatterStep>` which can optimize the
40+
* serialized representation for either
41+
* - roundtrip integrity
42+
* - OR
43+
* - equality
44+
*
45+
* Because it is not possible to provide both at the same time.
46+
* It is a horrific hack, but it works, and it's the only way I can figure
47+
* to make Spotless work with all of Gradle's cache systems at once.
48+
*/
49+
public class ConfigurationCacheHackList implements java.io.Serializable {
50+
private static final long serialVersionUID = 1L;
51+
private final boolean optimizeForEquality;
52+
private final ArrayList<Object> backingList = new ArrayList<>();
53+
54+
public static ConfigurationCacheHackList forEquality() {
55+
return new ConfigurationCacheHackList(true);
56+
}
57+
58+
public static ConfigurationCacheHackList forRoundtrip() {
59+
return new ConfigurationCacheHackList(false);
60+
}
61+
62+
private ConfigurationCacheHackList(boolean optimizeForEquality) {
63+
this.optimizeForEquality = optimizeForEquality;
64+
}
65+
66+
public void clear() {
67+
backingList.clear();
68+
}
69+
70+
public void addAll(Collection<? extends FormatterStep> c) {
71+
for (FormatterStep step : c) {
72+
if (step instanceof FormatterStepSerializationRoundtrip) {
73+
var clone = ((FormatterStepSerializationRoundtrip) step).hackClone(optimizeForEquality);
74+
backingList.add(clone);
75+
} else {
76+
backingList.add(step);
77+
}
78+
}
79+
}
80+
81+
public List<FormatterStep> getSteps() {
82+
var result = new ArrayList<FormatterStep>(backingList.size());
83+
for (Object obj : backingList) {
84+
if (obj instanceof FormatterStepSerializationRoundtrip.HackClone) {
85+
result.add(((FormatterStepSerializationRoundtrip.HackClone) obj).rehydrate());
86+
} else {
87+
result.add((FormatterStep) obj);
88+
}
89+
}
90+
return result;
91+
}
92+
93+
@Override
94+
public boolean equals(Object o) {
95+
if (this == o)
96+
return true;
97+
if (o == null || getClass() != o.getClass())
98+
return false;
99+
ConfigurationCacheHackList stepList = (ConfigurationCacheHackList) o;
100+
return optimizeForEquality == stepList.optimizeForEquality &&
101+
backingList.equals(stepList.backingList);
102+
}
103+
104+
@Override
105+
public int hashCode() {
106+
return Objects.hash(optimizeForEquality, backingList);
107+
}
108+
}

lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java

+81-13
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,23 @@
1616
package com.diffplug.spotless;
1717

1818
import java.io.IOException;
19-
import java.io.ObjectStreamException;
2019
import java.io.Serializable;
20+
import java.util.Objects;
2121

2222
import edu.umd.cs.findbugs.annotations.Nullable;
23+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2324

24-
class FormatterStepSerializationRoundtrip<RoundtripState extends Serializable, EqualityState extends Serializable> extends FormatterStepEqualityOnStateSerialization<EqualityState> {
25+
final class FormatterStepSerializationRoundtrip<RoundtripState extends Serializable, EqualityState extends Serializable> extends FormatterStepEqualityOnStateSerialization<EqualityState> {
2526
private static final long serialVersionUID = 1L;
2627
private final String name;
28+
@SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "HackClone")
2729
private final transient ThrowingEx.Supplier<RoundtripState> initializer;
2830
private @Nullable RoundtripState roundtripStateInternal;
31+
private @Nullable EqualityState equalityStateInternal;
2932
private final SerializedFunction<RoundtripState, EqualityState> equalityStateExtractor;
3033
private final SerializedFunction<EqualityState, FormatterFunc> equalityStateToFormatter;
3134

32-
public FormatterStepSerializationRoundtrip(String name, ThrowingEx.Supplier<RoundtripState> initializer, SerializedFunction<RoundtripState, EqualityState> equalityStateExtractor, SerializedFunction<EqualityState, FormatterFunc> equalityStateToFormatter) {
35+
FormatterStepSerializationRoundtrip(String name, ThrowingEx.Supplier<RoundtripState> initializer, SerializedFunction<RoundtripState, EqualityState> equalityStateExtractor, SerializedFunction<EqualityState, FormatterFunc> equalityStateToFormatter) {
3336
this.name = name;
3437
this.initializer = initializer;
3538
this.equalityStateExtractor = equalityStateExtractor;
@@ -41,32 +44,97 @@ public String getName() {
4144
return name;
4245
}
4346

44-
@Override
45-
protected EqualityState stateSupplier() throws Exception {
47+
private RoundtripState roundtripStateSupplier() throws Exception {
4648
if (roundtripStateInternal == null) {
4749
roundtripStateInternal = initializer.get();
4850
}
49-
return equalityStateExtractor.apply(roundtripStateInternal);
51+
return roundtripStateInternal;
52+
}
53+
54+
@Override
55+
protected EqualityState stateSupplier() throws Exception {
56+
if (equalityStateInternal == null) {
57+
equalityStateInternal = equalityStateExtractor.apply(roundtripStateSupplier());
58+
}
59+
return equalityStateInternal;
5060
}
5161

5262
@Override
5363
protected FormatterFunc stateToFormatter(EqualityState equalityState) throws Exception {
5464
return equalityStateToFormatter.apply(equalityState);
5565
}
5666

57-
// override serialize output
5867
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
59-
if (roundtripStateInternal == null) {
60-
roundtripStateInternal = ThrowingEx.get(initializer::get);
68+
if (initializer == null) {
69+
// then this instance was created by Gradle's ConfigurationCacheHackList and the following will hold true
70+
if (roundtripStateInternal == null && equalityStateInternal == null) {
71+
throw new IllegalStateException("If the initializer was null, then one of roundtripStateInternal or equalityStateInternal should be non-null, and neither was");
72+
}
73+
} else {
74+
// this was a normal instance, which means we need to encode to roundtripStateInternal (since the initializer might not be serializable)
75+
// and there's no reason to keep equalityStateInternal since we can always recompute it
76+
if (roundtripStateInternal == null) {
77+
roundtripStateInternal = ThrowingEx.get(this::roundtripStateSupplier);
78+
}
79+
equalityStateInternal = null;
6180
}
6281
out.defaultWriteObject();
6382
}
6483

65-
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
66-
in.defaultReadObject();
84+
HackClone<?, ?> hackClone(boolean optimizeForEquality) {
85+
return new HackClone<>(this, optimizeForEquality);
6786
}
6887

69-
private void readObjectNoData() throws ObjectStreamException {
70-
throw new UnsupportedOperationException();
88+
/**
89+
* This class has one setting (optimizeForEquality) and two pieces of data
90+
* - the original step, which is marked transient so it gets discarded during serialization
91+
* - the cleaned step, which is lazily created during serialization, and the serialized form is optimized for either equality or roundtrip integrity
92+
*
93+
* It works in conjunction with ConfigurationCacheHackList to allow Spotless to work with all of Gradle's cache systems.
94+
*/
95+
static class HackClone<RoundtripState extends Serializable, EqualityState extends Serializable> implements Serializable {
96+
private static final long serialVersionUID = 1L;
97+
@SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "HackClone")
98+
transient FormatterStepSerializationRoundtrip<?, ?> original;
99+
boolean optimizeForEquality;
100+
@Nullable
101+
FormatterStepSerializationRoundtrip cleaned;
102+
103+
HackClone(@Nullable FormatterStepSerializationRoundtrip<RoundtripState, EqualityState> original, boolean optimizeForEquality) {
104+
this.original = original;
105+
this.optimizeForEquality = optimizeForEquality;
106+
}
107+
108+
@SuppressFBWarnings(value = "NP_NONNULL_PARAM_VIOLATION", justification = "HackClone")
109+
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
110+
if (cleaned == null) {
111+
cleaned = new FormatterStepSerializationRoundtrip(original.name, null, original.equalityStateExtractor, original.equalityStateToFormatter);
112+
if (optimizeForEquality) {
113+
cleaned.equalityStateInternal = ThrowingEx.get(original::stateSupplier);
114+
} else {
115+
cleaned.roundtripStateInternal = ThrowingEx.get(original::roundtripStateSupplier);
116+
}
117+
}
118+
out.defaultWriteObject();
119+
}
120+
121+
public FormatterStep rehydrate() {
122+
return original != null ? original : Objects.requireNonNull(cleaned, "how is clean null if this has been serialized?");
123+
}
124+
125+
@Override
126+
public boolean equals(Object o) {
127+
if (this == o)
128+
return true;
129+
if (o == null || getClass() != o.getClass())
130+
return false;
131+
HackClone<?, ?> that = (HackClone<?, ?>) o;
132+
return optimizeForEquality == that.optimizeForEquality && rehydrate().equals(that.rehydrate());
133+
}
134+
135+
@Override
136+
public int hashCode() {
137+
return rehydrate().hashCode() ^ Boolean.hashCode(optimizeForEquality);
138+
}
71139
}
72140
}

plugin-gradle/CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1717
* Bump default `cleanthat` version to latest `2.21` -> `2.22`. ([#2296](https://github.com/diffplug/spotless/pull/2296))
1818
### Fixed
1919
* Java import order, ignore duplicate group entries. ([#2293](https://github.com/diffplug/spotless/pull/2293))
20+
* Remote build cache shouldn't have cache misses anymore. ([TODO]()fixes [#2168](https://github.com/diffplug/spotless/issues/2168))
2021

2122
## [7.0.0.BETA2] - 2024-08-25
2223
### Changed

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

+15-8
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
import java.io.File;
1919
import java.nio.charset.Charset;
20-
import java.util.ArrayList;
21-
import java.util.Collections;
2220
import java.util.List;
2321
import java.util.Locale;
2422
import java.util.Objects;
@@ -37,6 +35,7 @@
3735
import org.gradle.api.tasks.PathSensitivity;
3836
import org.gradle.work.Incremental;
3937

38+
import com.diffplug.spotless.ConfigurationCacheHackList;
4039
import com.diffplug.spotless.FormatExceptionPolicy;
4140
import com.diffplug.spotless.FormatExceptionPolicyStrict;
4241
import com.diffplug.spotless.Formatter;
@@ -150,17 +149,25 @@ public File getOutputDirectory() {
150149
return outputDirectory;
151150
}
152151

153-
protected final List<FormatterStep> steps = new ArrayList<>();
152+
private final ConfigurationCacheHackList stepsInternalRoundtrip = ConfigurationCacheHackList.forRoundtrip();
153+
private final ConfigurationCacheHackList stepsInternalEquality = ConfigurationCacheHackList.forEquality();
154+
155+
@Internal
156+
public ConfigurationCacheHackList getStepsInternalRoundtrip() {
157+
return stepsInternalRoundtrip;
158+
}
154159

155160
@Input
156-
public List<FormatterStep> getSteps() {
157-
return Collections.unmodifiableList(steps);
161+
public ConfigurationCacheHackList getStepsInternalEquality() {
162+
return stepsInternalEquality;
158163
}
159164

160165
public void setSteps(List<FormatterStep> steps) {
161166
PluginGradlePreconditions.requireElementsNonNull(steps);
162-
this.steps.clear();
163-
this.steps.addAll(steps);
167+
this.stepsInternalRoundtrip.clear();
168+
this.stepsInternalEquality.clear();
169+
this.stepsInternalRoundtrip.addAll(steps);
170+
this.stepsInternalEquality.addAll(steps);
164171
}
165172

166173
/** Returns the name of this format. */
@@ -179,7 +186,7 @@ Formatter buildFormatter() {
179186
.lineEndingsPolicy(getLineEndingsPolicy().get())
180187
.encoding(Charset.forName(encoding))
181188
.rootDir(getProjectDir().get().getAsFile().toPath())
182-
.steps(steps)
189+
.steps(stepsInternalRoundtrip.getSteps())
183190
.exceptionPolicy(exceptionPolicy)
184191
.build();
185192
}

0 commit comments

Comments
 (0)