Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support configuration cache within a single daemon (JvmLocalCache) #986

Merged
merged 15 commits into from
Nov 9, 2021
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
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