Skip to content

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

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-1306.v2.yml
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 {
Copy link
Contributor Author

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


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use project.getGradle().getGradleUserHomeDir() instead

}

private NativeImageAtomicCopy() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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> {

Expand All @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the configureLegacyIdea logic completely. I see that we did that in gradle-idea-configuration as well but happy to bring it back if you think this might be problematic for some usecases.

Choose a reason for hiding this comment

The 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);
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure here - do we still need to check the idea.active and run this when the idea plugin is applied ?

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));
}
}
Loading