Skip to content

Commit 5349e35

Browse files
committed
Merge branch '21.X.X' of github.com:openjavaformat/open-java-format into abashev/Fix-error-prone-issues
2 parents a89db5b + 78ad845 commit 5349e35

31 files changed

Lines changed: 2262 additions & 324 deletions

‎gradle-open-java-format/build.gradle‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ gradlePlugin {
7070
openJavaFormatProvider {
7171
id = 'dev.openjavaformat.java-format-provider'
7272
implementationClass = 'com.palantir.javaformat.gradle.PalantirJavaFormatProviderPlugin'
73-
description = 'Exposes a configuration containing the open-java-format jars'
73+
description = 'Declares palantirJavaFormat, which overrides the formatter that comes inside the open-java-format plugins'
7474
displayName = 'Open Java Format Provider'
7575
tags.set(['java', 'style'])
7676
}
@@ -117,8 +117,9 @@ tasks.register("copyNativeImage", Copy.class) {
117117
into("$buildDir/nativeImage")
118118
}
119119

120-
// Split in two so the jar build does not need a GraalVM image: every test reads
121-
// impl.classpath, only the four that drive the binary read nativeImage.path.
120+
// Split in two so the jar build does not need a GraalVM image: the tests that declare the
121+
// formatter themselves read impl.classpath, which is also what bundleFormatter puts inside the
122+
// plugin; only the ones that drive the binary read nativeImage.path.
122123
//
123124
// Everything the doLast bodies touch is captured here, at configuration time: reaching for
124125
// `configurations` or `tasks` from inside them would pull the whole script into the task and
@@ -136,6 +137,31 @@ def writeImplClasspath = tasks.register("writeImplClasspath") {
136137
}
137138
}
138139

140+
// The Java-based formatter travels inside the plugin, so that a build needs no repository and no dependency for it,
141+
// and runs the formatter of the plugin's own commit (#20). The jars go in unchanged, each with its own LICENSE and
142+
// NOTICE, next to jars.txt: a line per jar, "<sha256> <name>", in the order of the classpath. BundledFormatter
143+
// extracts them on first use.
144+
def bundledFormatterDir = layout.buildDirectory.dir("generated/bundledFormatter")
145+
def bundledFormatterPath = "open-java-format/formatter"
146+
def bundleFormatter = tasks.register("bundleFormatter", Sync) {
147+
into(bundledFormatterDir)
148+
into(bundledFormatterPath) {
149+
from(implFiles)
150+
}
151+
doLast {
152+
def lines = implFiles.files.collect { jar ->
153+
"${java.security.MessageDigest.getInstance('SHA-256').digest(jar.bytes).encodeHex()} ${jar.name}"
154+
}
155+
bundledFormatterDir.get().file("${bundledFormatterPath}/jars.txt").asFile.text = lines.join('\n') + '\n'
156+
}
157+
}
158+
sourceSets.main.resources.srcDir(bundleFormatter)
159+
160+
// The sources jar takes the resources too; the formatter's jars are not the plugin's sources.
161+
tasks.named('sourcesJar', Jar) {
162+
exclude("${bundledFormatterPath}/**")
163+
}
164+
139165
def writeNativeImagePath = tasks.register("writeNativeImagePath") {
140166
inputs.files(tasks.named("copyNativeImage"))
141167
outputs.file(nativeImagePathFile)

‎gradle-open-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPlugin.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public void apply(Project rootProject) {
4646
.from(rootProject
4747
.getConfigurations()
4848
.getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME));
49+
task.getBundledFormatter().set(BundledFormatter.hash());
50+
task.getGradleUserHome().set(rootProject.getGradle().getGradleUserHomeDir());
4951
});
5052

5153
TaskProvider<UpdateWorkspaceXmlFile> updateWorkspaceXml = rootProject

‎gradle-open-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatPlugin.java‎

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,16 @@
1717
package com.palantir.javaformat.gradle;
1818

1919
import com.palantir.javaformat.bootstrap.NativeImageFormatterService;
20-
import java.io.File;
2120
import java.io.IOException;
2221
import org.gradle.api.DefaultTask;
2322
import org.gradle.api.Plugin;
2423
import org.gradle.api.Project;
25-
import org.gradle.api.artifacts.Configuration;
2624
import org.gradle.api.file.ConfigurableFileCollection;
2725
import org.gradle.api.file.DirectoryProperty;
2826
import org.gradle.api.file.FileCollection;
2927
import org.gradle.api.file.RegularFileProperty;
3028
import org.gradle.api.logging.Logger;
3129
import org.gradle.api.logging.Logging;
32-
import org.gradle.api.provider.Provider;
3330
import org.gradle.api.tasks.Classpath;
3431
import org.gradle.api.tasks.InputFile;
3532
import org.gradle.api.tasks.Internal;
@@ -53,31 +50,23 @@ public void apply(Project project) {
5350
project.getPlugins().apply(PalantirJavaFormatSpotlessPlugin.class);
5451

5552
project.getPlugins().withId("java", p -> {
53+
var formatterClasspath = FormatterClasspath.of(project);
54+
var nativeImage = FormatterClasspath.nativeImageOf(project);
5655

5756
// TODO(dfox): in the future we may want to offer a simple 'format' task so people don't need to use
5857
// spotless to try out our formatter
5958
project.getTasks().register("formatDiff", FormatDiffTask.class, task -> {
6059
task.getProjectDirectory().set(project.getLayout().getProjectDirectory());
60+
task.getGradleUserHome().set(project.getGradle().getGradleUserHomeDir());
6161
if (getNativeImageSupport().isNativeImageConfigured()) {
62-
task.getNativeImage().fileProvider(getNativeImplConfiguration(project));
62+
task.getNativeImage().fileProvider(nativeImage.map(FileCollection::getSingleFile));
6363
} else {
64-
task.getFormatterClasspath().from(getImplConfiguration(project));
64+
task.getFormatterClasspath().from(formatterClasspath);
6565
}
6666
});
6767
});
6868
}
6969

70-
private static Provider<Configuration> getImplConfiguration(Project project) {
71-
return project.getRootProject().getConfigurations().named(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME);
72-
}
73-
74-
private static Provider<File> getNativeImplConfiguration(Project project) {
75-
return (project.getRootProject()
76-
.getConfigurations()
77-
.named(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME)
78-
.map(FileCollection::getSingleFile));
79-
}
80-
8170
@DisableCachingByDefault(
8271
because = "Reformats whatever the working tree currently differs by; there is no reusable output")
8372
public abstract static class FormatDiffTask extends DefaultTask {
@@ -89,10 +78,17 @@ public abstract static class FormatDiffTask extends DefaultTask {
8978
@PathSensitive(PathSensitivity.NAME_ONLY)
9079
abstract RegularFileProperty getNativeImage();
9180

92-
/** The jars of the Java-based formatter, which runs when the native image is not configured. */
81+
/**
82+
* The jars a build declared for the Java-based formatter, which runs when the native image is not configured.
83+
* Most builds declare none, and the task then runs the one inside the plugin.
84+
*/
9385
@Classpath
9486
abstract ConfigurableFileCollection getFormatterClasspath();
9587

88+
/** Where the formatter inside the plugin is extracted to, and the native image copied to. */
89+
@Internal
90+
abstract DirectoryProperty getGradleUserHome();
91+
9692
/**
9793
* The project whose changed lines are formatted. The task reads it here rather than from {@code getProject()},
9894
* which a task must not call when it runs with the configuration cache.
@@ -112,13 +108,16 @@ public final void formatDiff() throws IOException, InterruptedException {
112108
log.info("Using the native-image formatter");
113109
FormatDiff.formatDiff(
114110
projectDir,
115-
new NativeImageFormatterService(
116-
getNativeImage().get().getAsFile().toPath()));
111+
new NativeImageFormatterService(NativeImageAtomicCopy.copyToCache(
112+
getNativeImage().get().getAsFile().toPath(),
113+
getGradleUserHome().get().getAsFile())));
117114
} else {
118115
log.info("Using the Java-based formatter");
119116
FormatDiff.formatDiff(
120117
projectDir,
121-
FormatterServiceLoader.load(getFormatterClasspath().getFiles()));
118+
FormatterServiceLoader.load(BundledFormatter.jarsUnlessOverridden(
119+
getFormatterClasspath().getFiles(),
120+
getGradleUserHome().get().getAsFile())));
122121
}
123122
}
124123
}

‎gradle-open-java-format/src/main/groovy/com/palantir/javaformat/gradle/UpdatePalantirJavaFormatIdeaXmlFile.java‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,69 @@
2020
import java.net.URI;
2121
import java.util.List;
2222
import org.gradle.api.DefaultTask;
23+
import org.gradle.api.Task;
2324
import org.gradle.api.file.ConfigurableFileCollection;
25+
import org.gradle.api.file.DirectoryProperty;
2426
import org.gradle.api.file.RegularFileProperty;
27+
import org.gradle.api.provider.Property;
28+
import org.gradle.api.specs.Spec;
2529
import org.gradle.api.tasks.Classpath;
30+
import org.gradle.api.tasks.Input;
2631
import org.gradle.api.tasks.InputFiles;
32+
import org.gradle.api.tasks.Internal;
2733
import org.gradle.api.tasks.OutputFile;
2834
import org.gradle.api.tasks.TaskAction;
2935
import org.gradle.work.DisableCachingByDefault;
3036

3137
@DisableCachingByDefault(because = "Writes the developer's local .idea configuration; nothing to cache")
3238
public abstract class UpdatePalantirJavaFormatIdeaXmlFile extends DefaultTask {
3339

40+
/**
41+
* The jars a build declared for the formatter. Most builds declare none, and the IDE then gets the formatter inside
42+
* the plugin.
43+
*/
3444
@InputFiles
3545
@Classpath
3646
public abstract ConfigurableFileCollection getImplementationConfig();
3747

48+
/** The hash of the formatter inside the plugin, which names the directory its jars are extracted into. */
49+
@Input
50+
public abstract Property<String> getBundledFormatter();
51+
52+
/** Where the formatter inside the plugin is extracted to. */
53+
@Internal
54+
public abstract DirectoryProperty getGradleUserHome();
55+
56+
public UpdatePalantirJavaFormatIdeaXmlFile() {
57+
// The IDE loads the jars from where the XML says: while they are gone, the task is not up to date.
58+
getOutputs().upToDateWhen(new BundledFormatterIsExtracted());
59+
}
60+
3861
@org.gradle.api.tasks.Optional
3962
@OutputFile
4063
public abstract RegularFileProperty getXmlOutputFile();
4164

4265
@TaskAction
4366
public final void updateXml() {
44-
List<URI> uris =
45-
getImplementationConfig().getFiles().stream().map(File::toURI).toList();
67+
List<URI> uris = BundledFormatter.jarsUnlessOverridden(
68+
getImplementationConfig().getFiles(),
69+
getGradleUserHome().get().getAsFile())
70+
.stream()
71+
.map(File::toURI)
72+
.toList();
4673
XmlUtils.updateIdeaXmlFile(
4774
getXmlOutputFile().getAsFile().get(),
4875
node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris));
4976
}
77+
78+
/** A class, not a lambda: the configuration cache stores it with the task. */
79+
private static final class BundledFormatterIsExtracted implements Spec<Task> {
80+
@Override
81+
public boolean isSatisfiedBy(Task task) {
82+
var xml = (UpdatePalantirJavaFormatIdeaXmlFile) task;
83+
return !xml.getImplementationConfig().isEmpty()
84+
|| BundledFormatter.isExtracted(
85+
xml.getGradleUserHome().get().getAsFile());
86+
}
87+
}
5088
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.palantir.javaformat.gradle;
16+
17+
import java.io.IOException;
18+
import java.nio.file.AtomicMoveNotSupportedException;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.nio.file.StandardCopyOption;
22+
import java.util.Comparator;
23+
import java.util.function.Predicate;
24+
import java.util.stream.Stream;
25+
26+
/**
27+
* A directory of open-java-format-caches, written once and never changed after: its name says what is in it.
28+
*
29+
* <p>Builds and daemons may write it at the same time. Each writes into a directory of its own next to the target and
30+
* renames it into place, which fails when the target is already there; the build that loses deletes its copy and uses
31+
* the one that won. There is no file lock: FileChannel.lock fails in a daemon whose other build holds the same file
32+
* through another class loader of the plugin.
33+
*/
34+
final class AtomicCacheDirectory {
35+
36+
/** Writes what the directory holds. */
37+
interface Contents {
38+
void writeInto(Path dir) throws IOException;
39+
}
40+
41+
private AtomicCacheDirectory() {}
42+
43+
/**
44+
* Creates {@code target} with these contents, unless another build has. A target that is not complete, as when
45+
* someone deleted a file out of it, is replaced; whoever still runs from it keeps the files it has open.
46+
*/
47+
static void create(Path target, Contents contents, Predicate<Path> isComplete) throws IOException {
48+
var caches = target.getParent();
49+
Files.createDirectories(caches);
50+
var copy = Files.createTempDirectory(caches, target.getFileName() + "-");
51+
try {
52+
contents.writeInto(copy);
53+
if (Files.exists(target) && !isComplete.test(target)) {
54+
delete(target);
55+
}
56+
moveIntoPlace(copy, target);
57+
} finally {
58+
delete(copy);
59+
}
60+
}
61+
62+
/** Renames the copy into place, unless another build got there first: then the copy is left to be deleted. */
63+
private static void moveIntoPlace(Path copy, Path target) throws IOException {
64+
try {
65+
Files.move(copy, target, StandardCopyOption.ATOMIC_MOVE);
66+
} catch (AtomicMoveNotSupportedException e) {
67+
// Both are in the same directory, so the move is a rename all the same.
68+
moveUnlessThere(copy, target);
69+
} catch (IOException e) {
70+
if (!Files.isDirectory(target)) {
71+
throw e;
72+
}
73+
}
74+
}
75+
76+
private static void moveUnlessThere(Path copy, Path target) throws IOException {
77+
try {
78+
Files.move(copy, target);
79+
} catch (IOException e) {
80+
if (!Files.isDirectory(target)) {
81+
throw e;
82+
}
83+
}
84+
}
85+
86+
private static void delete(Path dir) throws IOException {
87+
if (!Files.exists(dir)) {
88+
return;
89+
}
90+
try (Stream<Path> paths = Files.walk(dir)) {
91+
for (var path : paths.sorted(Comparator.reverseOrder()).toList()) {
92+
Files.deleteIfExists(path);
93+
}
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)