Skip to content

Commit c1fb2fb

Browse files
committed
Resolve the native image in the project that formats
The root project resolved palantirJavaFormatNative through its own repositories, so a build that declares them in its subprojects only failed (#20), and the image ran from the output of ExecutableTransform. The root project now offers palantirJavaFormatNative as a variant, openJavaFormatNativeElements, as it offers palantirJavaFormat, and every project with formatDiff resolves it through openJavaFormatNativeImage, with its own repositories: a build behind a mirror of Maven Central declares the mirror where it declares its other repositories. The default dependency on the image of the plugin's version and platform stays, and so does declaring another. formatDiff copies the image it resolved into <Gradle user home>/open-java-format-caches/<hash>/, next to the jars of the formatter inside the plugin, makes the copy executable and runs it from there. NativeImageAtomicCopy is back for that, without its file lock: it writes through AtomicCacheDirectory, which BundledFormatter now uses too. The hash is of the image's contents, kept per daemon by path, size and time, as the image is some 60 MB. ExecutableTransform goes; the copy sets the executable bit. NativeImageResolutionTest passes.
1 parent f18e3d4 commit c1fb2fb

8 files changed

Lines changed: 450 additions & 199 deletions

File tree

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
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;
@@ -28,7 +27,6 @@
2827
import org.gradle.api.file.RegularFileProperty;
2928
import org.gradle.api.logging.Logger;
3029
import org.gradle.api.logging.Logging;
31-
import org.gradle.api.provider.Provider;
3230
import org.gradle.api.tasks.Classpath;
3331
import org.gradle.api.tasks.InputFile;
3432
import org.gradle.api.tasks.Internal;
@@ -53,28 +51,22 @@ public void apply(Project project) {
5351

5452
project.getPlugins().withId("java", p -> {
5553
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 {
6464
task.getFormatterClasspath().from(formatterClasspath);
65-
task.getGradleUserHome().set(project.getGradle().getGradleUserHomeDir());
6665
}
6766
});
6867
});
6968
}
7069

71-
private static Provider<File> getNativeImplConfiguration(Project project) {
72-
return (project.getRootProject()
73-
.getConfigurations()
74-
.named(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME)
75-
.map(FileCollection::getSingleFile));
76-
}
77-
7870
@DisableCachingByDefault(
7971
because = "Reformats whatever the working tree currently differs by; there is no reusable output")
8072
public abstract static class FormatDiffTask extends DefaultTask {
@@ -93,7 +85,7 @@ public abstract static class FormatDiffTask extends DefaultTask {
9385
@Classpath
9486
abstract ConfigurableFileCollection getFormatterClasspath();
9587

96-
/** Where the formatter inside the plugin is extracted to. */
88+
/** Where the formatter inside the plugin is extracted to, and the native image copied to. */
9789
@Internal
9890
abstract DirectoryProperty getGradleUserHome();
9991

@@ -116,8 +108,9 @@ public final void formatDiff() throws IOException, InterruptedException {
116108
log.info("Using the native-image formatter");
117109
FormatDiff.formatDiff(
118110
projectDir,
119-
new NativeImageFormatterService(
120-
getNativeImage().get().getAsFile().toPath()));
111+
new NativeImageFormatterService(NativeImageAtomicCopy.copyToCache(
112+
getNativeImage().get().getAsFile().toPath(),
113+
getGradleUserHome().get().getAsFile())));
121114
} else {
122115
log.info("Using the Java-based formatter");
123116
FormatDiff.formatDiff(
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+
}

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

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,14 @@
1919
import java.io.InputStream;
2020
import java.io.UncheckedIOException;
2121
import java.nio.charset.StandardCharsets;
22-
import java.nio.file.AtomicMoveNotSupportedException;
2322
import java.nio.file.Files;
2423
import java.nio.file.Path;
25-
import java.nio.file.StandardCopyOption;
2624
import java.security.DigestInputStream;
2725
import java.security.MessageDigest;
2826
import java.security.NoSuchAlgorithmException;
2927
import java.util.Collection;
30-
import java.util.Comparator;
3128
import java.util.HexFormat;
3229
import java.util.List;
33-
import java.util.stream.Stream;
3430

3531
/**
3632
* The Java-based formatter that comes inside the plugin: the jars of its runtime classpath, which the build puts under
@@ -42,9 +38,7 @@
4238
* changes. That is also why a daemon holding the jars open, as Windows does not let a file be replaced, gets in nobody's
4339
* way.
4440
*
45-
* <p>Builds and daemons may extract at the same time. Each extracts into a directory of its own next to the target and
46-
* renames it into place, which fails when the target is already there; the build that loses deletes its copy and uses
47-
* the one that won.
41+
* <p>Builds and daemons may extract at the same time: see {@link AtomicCacheDirectory}.
4842
*/
4943
public final class BundledFormatter {
5044

@@ -84,10 +78,9 @@ static String hash(ClassLoader plugin) {
8478

8579
static List<File> jars(Path gradleUserHome, ClassLoader plugin) {
8680
var list = list(plugin);
87-
var caches = gradleUserHome.resolve(CACHES);
88-
var target = caches.resolve(hash(plugin));
81+
var target = gradleUserHome.resolve(CACHES).resolve(hash(plugin));
8982
if (!isComplete(target, list)) {
90-
extract(plugin, list, caches, target);
83+
extract(plugin, list, target);
9184
}
9285
return list.stream().map(jar -> target.resolve(jar.name()).toFile()).toList();
9386
}
@@ -103,22 +96,16 @@ private static boolean isComplete(Path dir, List<Jar> list) {
10396
return list.stream().allMatch(jar -> Files.isRegularFile(dir.resolve(jar.name())));
10497
}
10598

106-
private static void extract(ClassLoader plugin, List<Jar> list, Path caches, Path target) {
99+
private static void extract(ClassLoader plugin, List<Jar> list, Path target) {
107100
try {
108-
Files.createDirectories(caches);
109-
var copy = Files.createTempDirectory(caches, target.getFileName() + "-");
110-
try {
111-
for (var jar : list) {
112-
copy(plugin, jar, copy.resolve(jar.name()));
113-
}
114-
if (Files.exists(target) && !isComplete(target, list)) {
115-
// Someone deleted a jar out of it. Whoever still runs from it keeps the files it has open.
116-
delete(target);
117-
}
118-
moveIntoPlace(copy, target);
119-
} finally {
120-
delete(copy);
121-
}
101+
AtomicCacheDirectory.create(
102+
target,
103+
dir -> {
104+
for (var jar : list) {
105+
copy(plugin, jar, dir.resolve(jar.name()));
106+
}
107+
},
108+
existing -> isComplete(existing, list));
122109
} catch (IOException e) {
123110
throw new UncheckedIOException("Could not extract the formatter's jars into " + target, e);
124111
}
@@ -138,41 +125,6 @@ private static void copy(ClassLoader plugin, Jar jar, Path file) throws IOExcept
138125
}
139126
}
140127

141-
/** Renames the copy into place, unless another build got there first: then the copy is left to be deleted. */
142-
private static void moveIntoPlace(Path copy, Path target) throws IOException {
143-
try {
144-
Files.move(copy, target, StandardCopyOption.ATOMIC_MOVE);
145-
} catch (AtomicMoveNotSupportedException e) {
146-
// Both are in the same directory, so the move is a rename all the same.
147-
moveUnlessThere(copy, target);
148-
} catch (IOException e) {
149-
if (!Files.isDirectory(target)) {
150-
throw e;
151-
}
152-
}
153-
}
154-
155-
private static void moveUnlessThere(Path copy, Path target) throws IOException {
156-
try {
157-
Files.move(copy, target);
158-
} catch (IOException e) {
159-
if (!Files.isDirectory(target)) {
160-
throw e;
161-
}
162-
}
163-
}
164-
165-
private static void delete(Path dir) throws IOException {
166-
if (!Files.exists(dir)) {
167-
return;
168-
}
169-
try (Stream<Path> paths = Files.walk(dir)) {
170-
for (var path : paths.sorted(Comparator.reverseOrder()).toList()) {
171-
Files.deleteIfExists(path);
172-
}
173-
}
174-
}
175-
176128
private static byte[] resource(ClassLoader plugin, String name) {
177129
try (InputStream in = open(plugin, name)) {
178130
return in.readAllBytes();

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

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)