-
Notifications
You must be signed in to change notification settings - Fork 56
Intellij uses the native image from .gradle/palantir-java-formatter-caches
#1306
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
base: develop
Are you sure you want to change the base?
Changes from all commits
81ad7f3
f7a0ba6
7b4f77d
ca1472e
c07fb52
00b038c
8508f87
05c1e2a
0a9f080
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type: fix | ||
fix: | ||
description: Intellij uses the native image from `.gradle/palantir-java-formatter-caches` | ||
links: | ||
- https://github.com/palantir/palantir-java-format/pull/1306 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* 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.palantir.javaformat.gradle; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.Comparator; | ||
import java.util.stream.Stream; | ||
|
||
public final class FileUtils { | ||
|
||
public static void delete(Path path) { | ||
if (!Files.exists(path)) { | ||
return; | ||
} | ||
if (Files.isDirectory(path)) { | ||
deleteDirectory(path); | ||
} else { | ||
deleteFile(path); | ||
} | ||
} | ||
|
||
private static void deleteDirectory(Path dir) { | ||
try (Stream<Path> paths = Files.walk(dir)) { | ||
paths.sorted(Comparator.reverseOrder()).forEach(FileUtils::deleteFile); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to delete directory", e); | ||
} | ||
} | ||
|
||
private static void deleteFile(Path targetPath) { | ||
try { | ||
Files.delete(targetPath); | ||
} catch (IOException e) { | ||
throw new RuntimeException(String.format("Failed to delete path %s", targetPath), e); | ||
} | ||
} | ||
|
||
private FileUtils() {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* 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.palantir.javaformat.gradle; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.nio.channels.FileChannel; | ||
import java.nio.file.AtomicMoveNotSupportedException; | ||
import java.nio.file.FileAlreadyExistsException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.nio.file.StandardCopyOption; | ||
import java.nio.file.StandardOpenOption; | ||
import java.util.Optional; | ||
import org.gradle.api.logging.Logger; | ||
import org.gradle.api.logging.Logging; | ||
|
||
public class NativeImageAtomicCopy { | ||
|
||
private static Logger logger = Logging.getLogger(PalantirJavaFormatIdeaPlugin.class); | ||
|
||
public static URI copyToCacheDir(URI srcUri, File cacheDir) { | ||
Path src = Paths.get(srcUri); | ||
Path dst = cacheDir.toPath().resolve(src.getFileName()); | ||
if (Files.exists(dst)) { | ||
logger.info("Native image at path {} already exists", dst); | ||
return dst.toUri(); | ||
} | ||
Path lockFile = dst.getParent().resolve(dst.getFileName() + ".lock"); | ||
try (FileChannel channel = FileChannel.open( | ||
lockFile, StandardOpenOption.READ, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { | ||
channel.lock(); | ||
// double-check, now that we hold the lock | ||
if (Files.exists(dst)) { | ||
logger.info("Native image at path {} already exists", dst); | ||
return dst.toUri(); | ||
} | ||
try { | ||
// Attempt an atomic move first to avoid broken partial states. | ||
// Failing that, we use the replace_existing option such that | ||
// the results of a successful move operation are consistent. | ||
// This provides a helpful property in a race where the slower | ||
// process doesn't risk attempting to use the native image before | ||
// it has been fully moved. | ||
Path tempCopyDir = Files.createTempDirectory("tempDir"); | ||
Path tmpFile = tempCopyDir.resolve(src.getFileName()); | ||
try { | ||
Files.copy(src, tmpFile, StandardCopyOption.REPLACE_EXISTING); | ||
Files.move(tmpFile, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); | ||
} catch (AtomicMoveNotSupportedException ignored) { | ||
Files.move(tmpFile, dst, StandardCopyOption.REPLACE_EXISTING); | ||
} finally { | ||
FileUtils.delete(tempCopyDir); | ||
} | ||
} catch (FileAlreadyExistsException e) { | ||
// This means another process has successfully installed this native image, and we can just use theirs. | ||
// Should be unreachable using REPLACE_EXISTING, however kept around to prevent issues with potential | ||
// future refactors. | ||
} | ||
return dst.toUri(); | ||
} catch (IOException e) { | ||
throw new RuntimeException(String.format("Failed to copy the native image to path %s", dst), e); | ||
} | ||
} | ||
|
||
private static Path getGradleCacheDir() { | ||
return Path.of(Optional.ofNullable(System.getenv("GRADLE_USER_HOME")) | ||
.orElseGet(() -> System.getenv("HOME") + "/.gradle")); | ||
Comment on lines
+83
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can use |
||
} | ||
|
||
private NativeImageAtomicCopy() {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,29 +17,17 @@ | |
package com.palantir.javaformat.gradle; | ||
|
||
import com.google.common.base.Preconditions; | ||
import com.google.common.collect.ImmutableMap; | ||
import com.google.common.io.Files; | ||
import com.google.common.collect.ImmutableList; | ||
import com.palantir.gradle.ideaconfiguration.IdeaConfigurationExtension; | ||
import com.palantir.gradle.ideaconfiguration.IdeaConfigurationPlugin; | ||
import groovy.util.Node; | ||
import groovy.util.XmlNodePrinter; | ||
import groovy.util.XmlParser; | ||
import java.io.BufferedWriter; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.PrintWriter; | ||
import java.net.URI; | ||
import java.nio.charset.Charset; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.function.Consumer; | ||
import java.util.stream.Collectors; | ||
import javax.xml.parsers.ParserConfigurationException; | ||
import java.util.stream.Stream; | ||
import org.gradle.StartParameter; | ||
import org.gradle.api.Plugin; | ||
import org.gradle.api.Project; | ||
import org.gradle.api.artifacts.Configuration; | ||
import org.gradle.plugins.ide.idea.model.IdeaModel; | ||
import org.xml.sax.SAXException; | ||
import org.gradle.api.tasks.TaskProvider; | ||
|
||
public final class PalantirJavaFormatIdeaPlugin implements Plugin<Project> { | ||
|
||
|
@@ -53,13 +41,41 @@ public void apply(Project rootProject) { | |
|
||
rootProject.getPlugins().apply(PalantirJavaFormatProviderPlugin.class); | ||
rootProject.getPluginManager().withPlugin("idea", ideaPlugin -> { | ||
Configuration implConfiguration = | ||
rootProject.getConfigurations().getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME); | ||
|
||
Optional<Configuration> nativeImplConfiguration = maybeGetNativeImplConfiguration(rootProject); | ||
|
||
configureLegacyIdea(rootProject, implConfiguration, nativeImplConfiguration); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's drop it! |
||
configureIntelliJImport(rootProject, implConfiguration, nativeImplConfiguration); | ||
TaskProvider<UpdatePalantirJavaFormatIdeaXmlFile> updatePalantirJavaFormatXml = rootProject | ||
.getTasks() | ||
.register("updatePalantirJavaFormatXml", UpdatePalantirJavaFormatIdeaXmlFile.class, task -> { | ||
task.getOutputFile().set(rootProject.file(".idea/palantir-java-format.xml")); | ||
task.getCacheDir() | ||
.set(rootProject | ||
.getGradle() | ||
.getGradleUserHomeDir() | ||
.toPath() | ||
.resolve("palantir-java-format-caches") | ||
.toFile()); | ||
task.getImplementationConfig() | ||
.from(rootProject | ||
.getConfigurations() | ||
.getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME)); | ||
maybeGetNativeImplConfiguration(rootProject) | ||
.ifPresent(config -> task.getNativeImageConfig().from(config)); | ||
}); | ||
|
||
TaskProvider<UpdateWorkspaceXmlFile> updateWorkspaceXml = rootProject | ||
.getTasks() | ||
.register("updateWorkspaceXml", UpdateWorkspaceXmlFile.class, task -> { | ||
task.getOutputFile().set(rootProject.file(".idea/workspace.xml")); | ||
}); | ||
|
||
// Add the task to the Gradle start parameters so it executes automatically. | ||
StartParameter startParameter = rootProject.getGradle().getStartParameter(); | ||
List<String> updateTasks = Stream.of(updatePalantirJavaFormatXml, updateWorkspaceXml) | ||
.map(taskProvider -> String.format(":%s", taskProvider.getName())) | ||
.toList(); | ||
List<String> taskNames = ImmutableList.<String>builder() | ||
.addAll(startParameter.getTaskNames()) | ||
.addAll(updateTasks) | ||
.build(); | ||
startParameter.setTaskNames(taskNames); | ||
}); | ||
|
||
rootProject.getPluginManager().apply(IdeaConfigurationPlugin.class); | ||
|
@@ -76,87 +92,4 @@ private static Optional<Configuration> maybeGetNativeImplConfiguration(Project r | |
.getByName(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME)) | ||
: Optional.empty(); | ||
} | ||
|
||
private static void configureLegacyIdea( | ||
Project project, Configuration implConfiguration, Optional<Configuration> nativeImplConfiguration) { | ||
IdeaModel ideaModel = project.getExtensions().getByType(IdeaModel.class); | ||
ideaModel.getProject().getIpr().withXml(xmlProvider -> { | ||
// this block is lazy | ||
List<URI> uris = | ||
implConfiguration.getFiles().stream().map(File::toURI).collect(Collectors.toList()); | ||
Optional<URI> nativeUri = | ||
nativeImplConfiguration.map(conf -> conf.getSingleFile().toURI()); | ||
ConfigureJavaFormatterXml.configureJavaFormat(xmlProvider.asNode(), uris, nativeUri); | ||
}); | ||
|
||
ideaModel.getWorkspace().getIws().withXml(xmlProvider -> { | ||
ConfigureJavaFormatterXml.configureWorkspaceXml(xmlProvider.asNode()); | ||
}); | ||
} | ||
|
||
private static void configureIntelliJImport( | ||
Project project, Configuration implConfiguration, Optional<Configuration> nativeImplConfiguration) { | ||
// Note: we tried using 'org.jetbrains.gradle.plugin.idea-ext' and afterSync triggers, but these are currently | ||
// very hard to manage as the tasks feel disconnected from the Sync operation, and you can't remove them once | ||
// you've added them. For that reason, we accept that we have to resolve this configuration at | ||
// configuration-time, but only do it when part of an IDEA import. | ||
if (!Boolean.getBoolean("idea.active")) { | ||
return; | ||
} | ||
Comment on lines
-103
to
-105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure here - do we still need to check the |
||
project.getGradle().projectsEvaluated(gradle -> { | ||
List<URI> uris = | ||
implConfiguration.getFiles().stream().map(File::toURI).collect(Collectors.toList()); | ||
|
||
Optional<URI> nativeImageUri = | ||
nativeImplConfiguration.map(conf -> conf.getSingleFile().toURI()); | ||
|
||
createOrUpdateIdeaXmlFile( | ||
project.file(".idea/palantir-java-format.xml"), | ||
node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeImageUri)); | ||
createOrUpdateIdeaXmlFile( | ||
project.file(".idea/workspace.xml"), ConfigureJavaFormatterXml::configureWorkspaceXml); | ||
|
||
// Still configure legacy idea if using intellij import | ||
updateIdeaXmlFileIfExists(project.file(project.getName() + ".ipr"), node -> { | ||
ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeImageUri); | ||
}); | ||
updateIdeaXmlFileIfExists( | ||
project.file(project.getName() + ".iws"), ConfigureJavaFormatterXml::configureWorkspaceXml); | ||
}); | ||
} | ||
|
||
private static void createOrUpdateIdeaXmlFile(File configurationFile, Consumer<Node> configure) { | ||
updateIdeaXmlFile(configurationFile, configure, true); | ||
} | ||
|
||
private static void updateIdeaXmlFileIfExists(File configurationFile, Consumer<Node> configure) { | ||
updateIdeaXmlFile(configurationFile, configure, false); | ||
} | ||
|
||
private static void updateIdeaXmlFile(File configurationFile, Consumer<Node> configure, boolean createIfAbsent) { | ||
Node rootNode; | ||
if (configurationFile.isFile()) { | ||
try { | ||
rootNode = new XmlParser().parse(configurationFile); | ||
} catch (IOException | SAXException | ParserConfigurationException e) { | ||
throw new RuntimeException("Couldn't parse existing configuration file: " + configurationFile, e); | ||
} | ||
} else { | ||
if (!createIfAbsent) { | ||
return; | ||
} | ||
rootNode = new Node(null, "project", ImmutableMap.of("version", "4")); | ||
} | ||
|
||
configure.accept(rootNode); | ||
|
||
try (BufferedWriter writer = Files.newWriter(configurationFile, Charset.defaultCharset()); | ||
PrintWriter printWriter = new PrintWriter(writer)) { | ||
XmlNodePrinter nodePrinter = new XmlNodePrinter(printWriter); | ||
nodePrinter.setPreserveWhitespace(true); | ||
nodePrinter.print(rootNode); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to write back to configuration file: " + configurationFile, e); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* 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.palantir.javaformat.gradle; | ||
|
||
import java.io.File; | ||
import java.net.URI; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
import org.gradle.api.DefaultTask; | ||
import org.gradle.api.file.ConfigurableFileCollection; | ||
import org.gradle.api.file.RegularFileProperty; | ||
import org.gradle.api.tasks.Classpath; | ||
import org.gradle.api.tasks.InputFiles; | ||
import org.gradle.api.tasks.Optional; | ||
import org.gradle.api.tasks.OutputDirectory; | ||
import org.gradle.api.tasks.OutputFile; | ||
import org.gradle.api.tasks.TaskAction; | ||
|
||
public abstract class UpdatePalantirJavaFormatIdeaXmlFile extends DefaultTask { | ||
|
||
@InputFiles | ||
@Classpath | ||
public abstract ConfigurableFileCollection getImplementationConfig(); | ||
|
||
@Optional | ||
@InputFiles | ||
public abstract ConfigurableFileCollection getNativeImageConfig(); | ||
|
||
@Optional | ||
@OutputFile | ||
public abstract RegularFileProperty getOutputFile(); | ||
|
||
@OutputDirectory | ||
public abstract RegularFileProperty getCacheDir(); | ||
|
||
@TaskAction | ||
public final void updateXml() { | ||
List<URI> uris = | ||
getImplementationConfig().getFiles().stream().map(File::toURI).collect(Collectors.toList()); | ||
java.util.Optional<URI> nativeUri = getNativeImageConfig().isEmpty() | ||
? java.util.Optional.empty() | ||
: java.util.Optional.of(getNativeImageConfig().getSingleFile().toURI()) | ||
.map(uri -> NativeImageAtomicCopy.copyToCacheDir( | ||
uri, getCacheDir().getAsFile().get())); | ||
XmlUtils.updateIdeaXmlFile( | ||
getOutputFile().getAsFile().get(), | ||
node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeUri)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as in gradle-jdks