Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).

## [Unreleased]
### Fixed
- Concurrent P2 provisioning (parallel multi-project Gradle fingerprinting of `eclipse()` / `greclipse()` steps) no longer races Solstice's on-disk cache; also `ConfigurationCacheHackList.toString()` no longer evaluates step state (which could re-trigger provisioning while Gradle reports "cannot be serialized"). ([#3004](https://github.com/diffplug/spotless/issues/3004))

## [4.9.0] - 2026-07-27
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,42 @@ List<File> provisionP2Dependencies(
Provisioner mavenProvisioner,
@Nullable File cacheDirectory) throws IOException;

/** Creates a non-caching P2Provisioner for simple use cases. */
/**
* Creates a non-caching P2Provisioner for simple use cases.
* <p>
* All queries are serialized on {@code P2Provisioner.class}. Gradle may fingerprint
* many Spotless tasks in parallel; each fingerprint serializes the equality
* {@code ConfigurationCacheHackList}, which eagerly resolves Eclipse/P2 jars.
* Concurrent Solstice queries race on the on-disk cache and fail with
* {@code Failed to provision P2 dependencies}, reported by Gradle as
* "ConfigurationCacheHackList cannot be serialized"
* (<a href="https://github.com/diffplug/spotless/issues/3004">#3004</a>,
* <a href="https://github.com/diffplug/spotless/issues/2331">#2331</a>).
*/
static P2Provisioner createDefault() {
return (modelWrapper, mavenProvisioner, cacheDirectory) -> {
try {
if (cacheDirectory != null) {
CacheLocations.override_p2data = cacheDirectory;
}
P2Model model = modelWrapper.unwrap();
P2QueryResult query = model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW);
var classpath = new ArrayList<File>();
var mavenDeps = new ArrayList<String>();
mavenDeps.add("dev.equo.ide:solstice:1.8.1");
mavenDeps.add("com.diffplug.durian:durian-swt.os:4.3.1");
mavenDeps.addAll(query.getJarsOnMavenCentral());
classpath.addAll(mavenProvisioner.provisionWithTransitives(false, mavenDeps));
classpath.addAll(query.getJarsNotOnMavenCentral());
for (var nested : NestedJars.inFiles(query.getJarsNotOnMavenCentral()).extractAllNestedJars()) {
classpath.add(nested.getValue());
// Serialize all P2 queries in this JVM — Solstice's cache is not concurrent-safe.
synchronized (P2Provisioner.class) {
try {
if (cacheDirectory != null) {
CacheLocations.override_p2data = cacheDirectory;
}
P2Model model = modelWrapper.unwrap();
P2QueryResult query = model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW);
var classpath = new ArrayList<File>();
var mavenDeps = new ArrayList<String>();
mavenDeps.add("dev.equo.ide:solstice:1.8.1");
mavenDeps.add("com.diffplug.durian:durian-swt.os:4.3.1");
mavenDeps.addAll(query.getJarsOnMavenCentral());
classpath.addAll(mavenProvisioner.provisionWithTransitives(false, mavenDeps));
classpath.addAll(query.getJarsNotOnMavenCentral());
for (var nested : NestedJars.inFiles(query.getJarsNotOnMavenCentral()).extractAllNestedJars()) {
classpath.add(nested.getValue());
}
return classpath;
} catch (Exception e) {
throw new IOException("Failed to provision P2 dependencies", e);
}
return classpath;
} catch (Exception e) {
throw new IOException("Failed to provision P2 dependencies", e);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 DiffPlug
* Copyright 2024-2026 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 @@ -150,4 +150,18 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(optimizeForEquality, backingList);
}

/**
* Must not call {@link #hashCode()} — that fingerprints every step and may provision
* P2/Maven deps. Gradle includes this value in "cannot be serialized" messages; a
* side-effecting {@code toString} re-triggers provisioning and masks the real cause
* (see <a href="https://github.com/diffplug/spotless/issues/3004">#3004</a>).
*/
@Override
public String toString() {
return getClass().getName()
+ "@" + Integer.toHexString(System.identityHashCode(this))
+ "[optimizeForEquality=" + optimizeForEquality
+ ", size=" + backingList.size() + "]";
}
}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).

## [Unreleased]
### Fixed
- Parallel multi-project builds no longer intermittently fail with "Cannot fingerprint input property 'stepsInternalEquality': ConfigurationCacheHackList cannot be serialized" / "Failed to provision P2 dependencies" when using `eclipse()` (or other P2-backed steps). Subprojects now share one deduping P2 provisioner and P2 queries are serialized process-wide. ([#3004](https://github.com/diffplug/spotless/issues/3004))

## [8.9.0] - 2026-07-27
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ public abstract class SpotlessTaskService implements BuildService<BuildServicePa
private final Map<String, SpotlessApply> apply = Collections.synchronizedMap(new HashMap<>());
private final Map<String, SpotlessTask> source = Collections.synchronizedMap(new HashMap<>());
private final Map<String, Provisioner> provisioner = Collections.synchronizedMap(new HashMap<>());
private final Map<String, P2Provisioner> p2Provisioner = Collections.synchronizedMap(new HashMap<>());

@Nullable GradleProvisioner.DedupingProvisioner predeclaredProvisioner;
@Nullable GradleProvisioner.DedupingP2Provisioner predeclaredP2Provisioner;
/** Shared across subprojects so parallel fingerprinting reuses one P2 cache + lock. */
@Nullable private volatile GradleProvisioner.DedupingP2Provisioner sharedP2Provisioner;
@Nullable RegisterDependenciesTask registerDependenciesTask;

Provisioner provisionerFor(SpotlessExtension spotless) {
Expand All @@ -84,12 +85,31 @@ P2Provisioner p2ProvisionerFor(SpotlessExtension spotless) {
if (predeclaredP2Provisioner != null) {
return predeclaredP2Provisioner.cachedOnly;
} else {
return p2Provisioner.computeIfAbsent(spotless.project.getPath(),
unused -> new GradleProvisioner.DedupingP2Provisioner(P2Provisioner.createDefault(), GradleProvisioner.defaultP2CacheDirectory(spotless.project)));
// One DedupingP2Provisioner for the whole build (not per-project). Parallel
// multi-project fingerprinting of eclipse()/greclipse() steps otherwise races
// on Solstice's on-disk P2 cache — Gradle then reports
// "ConfigurationCacheHackList cannot be serialized" (#3004).
return sharedP2Provisioner(spotless.project);
}
}
}

private GradleProvisioner.DedupingP2Provisioner sharedP2Provisioner(Project project) {
GradleProvisioner.DedupingP2Provisioner local = sharedP2Provisioner;
if (local == null) {
synchronized (this) {
local = sharedP2Provisioner;
if (local == null) {
local = new GradleProvisioner.DedupingP2Provisioner(
P2Provisioner.createDefault(),
GradleProvisioner.defaultP2CacheDirectory(project));
sharedP2Provisioner = local;
}
}
}
return local;
}

void registerSourceAlreadyRan(SpotlessTask task) {
source.put(task.getPath(), task);
}
Expand Down
2 changes: 2 additions & 0 deletions plugin-maven/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).

## [Unreleased]
### Fixed
- Concurrent P2 provisioning no longer races Solstice's on-disk cache (affects Eclipse-based formatters under parallel builds). ([#3004](https://github.com/diffplug/spotless/issues/3004))

## [3.9.0] - 2026-07-27
### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2024-2026 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.spotless;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.Test;

class ConfigurationCacheHackListTest {

/** Step whose equality/hashCode/serialization forces state evaluation. */
private static FormatterStep lazyStep(String name, AtomicInteger stateEvals, Serializable state) {
return FormatterStep.createLazy(name,
() -> {
stateEvals.incrementAndGet();
return state;
},
SerializedFunction.identity(),
eq -> (FormatterFunc) (s -> s));
}

@Test
void toStringDoesNotEvaluateStepState() {
AtomicInteger evals = new AtomicInteger();
ConfigurationCacheHackList list = ConfigurationCacheHackList.forEquality();
list.addAll(List.of(lazyStep("expensive", evals, "state")));

// Gradle includes this value in "cannot be serialized" error messages.
// Default Object.toString() calls hashCode(), which fingerprints steps and
// may provision P2 deps — re-triggering the failure being reported (#3004).
String text = list.toString();
assertThat(text).contains("ConfigurationCacheHackList");
assertThat(text).contains("optimizeForEquality=true");
assertThat(text).contains("size=1");
assertThat(evals.get()).as("toString must not evaluate step state").isZero();
}

@Test
void equalityListRoundtripsThroughJavaSerialization() throws Exception {
AtomicInteger evals = new AtomicInteger();
ConfigurationCacheHackList original = ConfigurationCacheHackList.forEquality();
original.addAll(List.of(lazyStep("plain", evals, "eq-state")));

ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(original);
}
assertThat(evals.get()).as("serializing equality list evaluates state once").isEqualTo(1);

ConfigurationCacheHackList restored;
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
restored = (ConfigurationCacheHackList) in.readObject();
}
assertThat(restored.getSteps()).hasSize(1);
assertThat(restored.getSteps().get(0).getName()).isEqualTo("plain");
// toString after restore must still be side-effect free
assertThatCode(restored::toString).doesNotThrowAnyException();
}
}