Skip to content

Commit e58c1d9

Browse files
authored
Intellij uses the native image from .gradle/palantir-java-formatter-caches (#1306)
Intellij uses the native image from `.gradle/palantir-java-formatter-caches`
1 parent 963a78b commit e58c1d9

File tree

8 files changed

+359
-110
lines changed

8 files changed

+359
-110
lines changed

changelog/@unreleased/pr-1306.v2.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type: fix
2+
fix:
3+
description: Intellij uses the native image from `.gradle/palantir-java-formatter-caches`
4+
links:
5+
- https://github.com/palantir/palantir-java-format/pull/1306
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
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+
17+
package com.palantir.javaformat.gradle;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.util.Comparator;
23+
import java.util.stream.Stream;
24+
25+
public final class FileUtils {
26+
27+
public static void delete(Path path) {
28+
if (!Files.exists(path)) {
29+
return;
30+
}
31+
if (Files.isDirectory(path)) {
32+
deleteDirectory(path);
33+
} else {
34+
deleteFile(path);
35+
}
36+
}
37+
38+
private static void deleteDirectory(Path dir) {
39+
try (Stream<Path> paths = Files.walk(dir)) {
40+
paths.sorted(Comparator.reverseOrder()).forEach(FileUtils::deleteFile);
41+
} catch (IOException e) {
42+
throw new RuntimeException("Failed to delete directory", e);
43+
}
44+
}
45+
46+
private static void deleteFile(Path targetPath) {
47+
try {
48+
Files.delete(targetPath);
49+
} catch (IOException e) {
50+
throw new RuntimeException(String.format("Failed to delete path %s", targetPath), e);
51+
}
52+
}
53+
54+
private FileUtils() {}
55+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
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+
17+
package com.palantir.javaformat.gradle;
18+
19+
import java.io.IOException;
20+
import java.net.URI;
21+
import java.nio.channels.FileChannel;
22+
import java.nio.file.AtomicMoveNotSupportedException;
23+
import java.nio.file.FileAlreadyExistsException;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.StandardCopyOption;
27+
import java.nio.file.StandardOpenOption;
28+
import org.gradle.api.logging.Logger;
29+
import org.gradle.api.logging.Logging;
30+
31+
public class NativeImageAtomicCopy {
32+
33+
private static Logger logger = Logging.getLogger(PalantirJavaFormatIdeaPlugin.class);
34+
35+
public static URI copyToCacheDir(Path src, Path dst) {
36+
if (Files.exists(dst)) {
37+
logger.info("Native image at path {} already exists", dst);
38+
return dst.toUri();
39+
}
40+
Path lockFile = dst.getParent().resolve(dst.getFileName() + ".lock");
41+
try (FileChannel channel = FileChannel.open(
42+
lockFile, StandardOpenOption.READ, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
43+
channel.lock();
44+
// double-check, now that we hold the lock
45+
if (Files.exists(dst)) {
46+
logger.info("Native image at path {} already exists", dst);
47+
return dst.toUri();
48+
}
49+
try {
50+
// Attempt an atomic move first to avoid broken partial states.
51+
// Failing that, we use the replace_existing option such that
52+
// the results of a successful move operation are consistent.
53+
// This provides a helpful property in a race where the slower
54+
// process doesn't risk attempting to use the native image before
55+
// it has been fully moved.
56+
Path tempCopyDir = Files.createTempDirectory("tempDir");
57+
Path tmpFile = tempCopyDir.resolve(src.getFileName());
58+
try {
59+
Files.copy(src, tmpFile, StandardCopyOption.REPLACE_EXISTING);
60+
Files.move(tmpFile, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
61+
} catch (AtomicMoveNotSupportedException ignored) {
62+
Files.move(tmpFile, dst, StandardCopyOption.REPLACE_EXISTING);
63+
} finally {
64+
FileUtils.delete(tempCopyDir);
65+
}
66+
} catch (FileAlreadyExistsException e) {
67+
// This means another process has successfully installed this native image, and we can just use theirs.
68+
// Should be unreachable using REPLACE_EXISTING, however kept around to prevent issues with potential
69+
// future refactors.
70+
}
71+
return dst.toUri();
72+
} catch (IOException e) {
73+
throw new RuntimeException(String.format("Failed to copy the native image to path %s", dst), e);
74+
}
75+
}
76+
77+
private NativeImageAtomicCopy() {}
78+
}

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

Lines changed: 45 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,21 @@
1717
package com.palantir.javaformat.gradle;
1818

1919
import com.google.common.base.Preconditions;
20-
import com.google.common.collect.ImmutableMap;
21-
import com.google.common.io.Files;
20+
import com.google.common.collect.ImmutableList;
2221
import com.palantir.gradle.ideaconfiguration.IdeaConfigurationExtension;
2322
import com.palantir.gradle.ideaconfiguration.IdeaConfigurationPlugin;
24-
import groovy.util.Node;
25-
import groovy.util.XmlNodePrinter;
26-
import groovy.util.XmlParser;
27-
import java.io.BufferedWriter;
28-
import java.io.File;
29-
import java.io.IOException;
30-
import java.io.PrintWriter;
31-
import java.net.URI;
32-
import java.nio.charset.Charset;
23+
import java.nio.file.Paths;
3324
import java.util.List;
3425
import java.util.Optional;
35-
import java.util.function.Consumer;
36-
import java.util.stream.Collectors;
26+
import java.util.stream.Stream;
3727
import javax.inject.Inject;
38-
import javax.xml.parsers.ParserConfigurationException;
28+
import org.gradle.StartParameter;
3929
import org.gradle.api.Plugin;
4030
import org.gradle.api.Project;
4131
import org.gradle.api.artifacts.Configuration;
4232
import org.gradle.api.artifacts.ConfigurationContainer;
4333
import org.gradle.api.tasks.Nested;
44-
import org.gradle.plugins.ide.idea.model.IdeaModel;
45-
import org.xml.sax.SAXException;
34+
import org.gradle.api.tasks.TaskProvider;
4635

4736
public abstract class PalantirJavaFormatIdeaPlugin implements Plugin<Project> {
4837

@@ -62,13 +51,46 @@ public void apply(Project rootProject) {
6251

6352
rootProject.getPlugins().apply(PalantirJavaFormatProviderPlugin.class);
6453
rootProject.getPluginManager().withPlugin("idea", ideaPlugin -> {
65-
Configuration implConfiguration =
66-
rootProject.getConfigurations().getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME);
67-
68-
Optional<Configuration> nativeImplConfiguration = maybeGetNativeImplConfiguration();
69-
70-
configureLegacyIdea(rootProject, implConfiguration, nativeImplConfiguration);
71-
configureIntelliJImport(rootProject, implConfiguration, nativeImplConfiguration);
54+
TaskProvider<UpdatePalantirJavaFormatIdeaXmlFile> updatePalantirJavaFormatXml = rootProject
55+
.getTasks()
56+
.register("updatePalantirJavaFormatXml", UpdatePalantirJavaFormatIdeaXmlFile.class, task -> {
57+
task.getXmlOutputFile().set(rootProject.file(".idea/palantir-java-format.xml"));
58+
task.getImplementationConfig()
59+
.from(rootProject
60+
.getConfigurations()
61+
.getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME));
62+
maybeGetNativeImplConfiguration().ifPresent(config -> {
63+
task.getNativeImageConfig().from(config);
64+
task.getNativeImageOutputFile().fileProvider(rootProject.provider(() -> rootProject
65+
.getGradle()
66+
.getGradleUserHomeDir()
67+
.toPath()
68+
.resolve("palantir-java-format-caches/")
69+
.resolve(Paths.get(task.getNativeImageConfig()
70+
.getSingleFile()
71+
.toURI())
72+
.getFileName()
73+
.toString())
74+
.toFile()));
75+
});
76+
});
77+
78+
TaskProvider<UpdateWorkspaceXmlFile> updateWorkspaceXml = rootProject
79+
.getTasks()
80+
.register("updateWorkspaceXml", UpdateWorkspaceXmlFile.class, task -> {
81+
task.getOutputFile().set(rootProject.file(".idea/workspace.xml"));
82+
});
83+
84+
// Add the task to the Gradle start parameters so it executes automatically.
85+
StartParameter startParameter = rootProject.getGradle().getStartParameter();
86+
List<String> updateTasks = Stream.of(updatePalantirJavaFormatXml, updateWorkspaceXml)
87+
.map(taskProvider -> String.format(":%s", taskProvider.getName()))
88+
.toList();
89+
List<String> taskNames = ImmutableList.<String>builder()
90+
.addAll(startParameter.getTaskNames())
91+
.addAll(updateTasks)
92+
.build();
93+
startParameter.setTaskNames(taskNames);
7294
});
7395

7496
rootProject.getPluginManager().apply(IdeaConfigurationPlugin.class);
@@ -83,87 +105,4 @@ private Optional<Configuration> maybeGetNativeImplConfiguration() {
83105
? Optional.of(getConfigurations().getByName(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME))
84106
: Optional.empty();
85107
}
86-
87-
private static void configureLegacyIdea(
88-
Project project, Configuration implConfiguration, Optional<Configuration> nativeImplConfiguration) {
89-
IdeaModel ideaModel = project.getExtensions().getByType(IdeaModel.class);
90-
ideaModel.getProject().getIpr().withXml(xmlProvider -> {
91-
// this block is lazy
92-
List<URI> uris =
93-
implConfiguration.getFiles().stream().map(File::toURI).collect(Collectors.toList());
94-
Optional<URI> nativeUri =
95-
nativeImplConfiguration.map(conf -> conf.getSingleFile().toURI());
96-
ConfigureJavaFormatterXml.configureJavaFormat(xmlProvider.asNode(), uris, nativeUri);
97-
});
98-
99-
ideaModel.getWorkspace().getIws().withXml(xmlProvider -> {
100-
ConfigureJavaFormatterXml.configureWorkspaceXml(xmlProvider.asNode());
101-
});
102-
}
103-
104-
private static void configureIntelliJImport(
105-
Project project, Configuration implConfiguration, Optional<Configuration> nativeImplConfiguration) {
106-
// Note: we tried using 'org.jetbrains.gradle.plugin.idea-ext' and afterSync triggers, but these are currently
107-
// very hard to manage as the tasks feel disconnected from the Sync operation, and you can't remove them once
108-
// you've added them. For that reason, we accept that we have to resolve this configuration at
109-
// configuration-time, but only do it when part of an IDEA import.
110-
if (!Boolean.getBoolean("idea.active")) {
111-
return;
112-
}
113-
project.getGradle().projectsEvaluated(gradle -> {
114-
List<URI> uris =
115-
implConfiguration.getFiles().stream().map(File::toURI).collect(Collectors.toList());
116-
117-
Optional<URI> nativeImageUri =
118-
nativeImplConfiguration.map(conf -> conf.getSingleFile().toURI());
119-
120-
createOrUpdateIdeaXmlFile(
121-
project.file(".idea/palantir-java-format.xml"),
122-
node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeImageUri));
123-
createOrUpdateIdeaXmlFile(
124-
project.file(".idea/workspace.xml"), ConfigureJavaFormatterXml::configureWorkspaceXml);
125-
126-
// Still configure legacy idea if using intellij import
127-
updateIdeaXmlFileIfExists(project.file(project.getName() + ".ipr"), node -> {
128-
ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeImageUri);
129-
});
130-
updateIdeaXmlFileIfExists(
131-
project.file(project.getName() + ".iws"), ConfigureJavaFormatterXml::configureWorkspaceXml);
132-
});
133-
}
134-
135-
private static void createOrUpdateIdeaXmlFile(File configurationFile, Consumer<Node> configure) {
136-
updateIdeaXmlFile(configurationFile, configure, true);
137-
}
138-
139-
private static void updateIdeaXmlFileIfExists(File configurationFile, Consumer<Node> configure) {
140-
updateIdeaXmlFile(configurationFile, configure, false);
141-
}
142-
143-
private static void updateIdeaXmlFile(File configurationFile, Consumer<Node> configure, boolean createIfAbsent) {
144-
Node rootNode;
145-
if (configurationFile.isFile()) {
146-
try {
147-
rootNode = new XmlParser().parse(configurationFile);
148-
} catch (IOException | SAXException | ParserConfigurationException e) {
149-
throw new RuntimeException("Couldn't parse existing configuration file: " + configurationFile, e);
150-
}
151-
} else {
152-
if (!createIfAbsent) {
153-
return;
154-
}
155-
rootNode = new Node(null, "project", ImmutableMap.of("version", "4"));
156-
}
157-
158-
configure.accept(rootNode);
159-
160-
try (BufferedWriter writer = Files.newWriter(configurationFile, Charset.defaultCharset());
161-
PrintWriter printWriter = new PrintWriter(writer)) {
162-
XmlNodePrinter nodePrinter = new XmlNodePrinter(printWriter);
163-
nodePrinter.setPreserveWhitespace(true);
164-
nodePrinter.print(rootNode);
165-
} catch (IOException e) {
166-
throw new RuntimeException("Failed to write back to configuration file: " + configurationFile, e);
167-
}
168-
}
169108
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
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+
17+
package com.palantir.javaformat.gradle;
18+
19+
import java.io.File;
20+
import java.net.URI;
21+
import java.nio.file.Paths;
22+
import java.util.List;
23+
import java.util.Optional;
24+
import org.gradle.api.DefaultTask;
25+
import org.gradle.api.file.ConfigurableFileCollection;
26+
import org.gradle.api.file.RegularFileProperty;
27+
import org.gradle.api.tasks.Classpath;
28+
import org.gradle.api.tasks.InputFiles;
29+
import org.gradle.api.tasks.OutputFile;
30+
import org.gradle.api.tasks.TaskAction;
31+
32+
public abstract class UpdatePalantirJavaFormatIdeaXmlFile extends DefaultTask {
33+
34+
@InputFiles
35+
@Classpath
36+
public abstract ConfigurableFileCollection getImplementationConfig();
37+
38+
@org.gradle.api.tasks.Optional
39+
@InputFiles
40+
public abstract ConfigurableFileCollection getNativeImageConfig();
41+
42+
@org.gradle.api.tasks.Optional
43+
@OutputFile
44+
public abstract RegularFileProperty getXmlOutputFile();
45+
46+
@org.gradle.api.tasks.Optional
47+
@OutputFile
48+
public abstract RegularFileProperty getNativeImageOutputFile();
49+
50+
@TaskAction
51+
public final void updateXml() {
52+
List<URI> uris =
53+
getImplementationConfig().getFiles().stream().map(File::toURI).toList();
54+
Optional<URI> nativeUri = getNativeImageConfig().getFiles().stream()
55+
.findFirst()
56+
.map(File::toURI)
57+
.map(uri -> NativeImageAtomicCopy.copyToCacheDir(
58+
Paths.get(uri),
59+
getNativeImageOutputFile().getAsFile().get().toPath()));
60+
XmlUtils.updateIdeaXmlFile(
61+
getXmlOutputFile().getAsFile().get(),
62+
node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeUri));
63+
}
64+
}

0 commit comments

Comments
 (0)