From e530bbe8cdd2d3a0492fa3675deea62143936474 Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Tue, 22 Sep 2026 15:22:50 +0300 Subject: [PATCH 1/3] Run the native formatter from the Gradle plugin on Windows openjavaformat.native.formatter=true now takes effect on Windows on x86-64 too, where 2.98.0.2 published a binary, nativeImage-windows_x86-64.exe. Before, the plugin ignored the property on Windows and ran the Java formatter. ExecutableTransform made that impossible: it asked for POSIX permissions, which a Windows file system does not have, so Files.getPosixFilePermissions would have thrown. It now skips them where the file system has no POSIX view; an .exe needs none to run. It also keeps an .exe name instead of appending .executable, and since the transformed name also names the copy in the IntelliJ IDEA cache, the binary ends in .exe everywhere the plugin hands it on. The tests could not have run on Windows either. The generated build scripts took paths inside Groovy strings, where a Windows backslash starts an escape, so those paths are written with forward slashes. The classpath was split on ':', which cuts a Windows path after its drive letter, so it is split on File.pathSeparator. FormatDiffTest ran bin/java without .exe, where FormatterServicesTest already added it. And the copy of the binary the tests use was renamed to .bin, the artifact type of every other platform, while the plugin resolves the Windows binary as exe, so that copy keeps .exe. The native CI job on Windows now runs the plugin tests against the image, as on the other platforms. On macOS the native job's tests, 8 in jdk-bootstrap and 31 in the Gradle plugin, and ./gradlew test pass. Windows is for CI to show. --- .github/workflows/ci.yml | 3 --- gradle-open-java-format/build.gradle | 4 +++- .../javaformat/gradle/ExecutableTransform.java | 8 +++++++- .../javaformat/gradle/NativeImageSupport.java | 11 ++++++----- .../palantir/javaformat/gradle/FormatDiffTest.java | 9 +++++++-- .../gradle/PalantirJavaFormatIdeaPluginTest.java | 4 +++- .../gradle/PalantirJavaFormatPluginTest.java | 9 ++++++--- .../gradle/PalantirJavaFormatSpotlessPluginTest.java | 9 ++++++--- .../javaformat/gradle/SpotlessExcludesTest.java | 6 ++++-- .../gradle/SupportsCurrentSpotlessTest.java | 6 ++++-- .../javaformat/bootstrap/FormatterServicesTest.java | 3 ++- 11 files changed, 48 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c18ae62d..5c62b6298 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,6 @@ jobs: JDK21_HOME: ${{ steps.jdk21.outputs.path }} # The binary itself on a file that needs formatting, a formatted one and one that does not parse. - # For the Windows binary this is the only check. - name: Smoke-test the binary run: | binary="$PWD/$(ls open-java-format-native/build/native/nativeCompile/open-java-format-* | grep -v '\.txt$')" @@ -102,9 +101,7 @@ jobs: set +e; "$binary" B.java; status=$?; set -e test "$status" -eq 2 - # The Gradle plugin does not run a native image on Windows, and its tests have never run there. - name: Test the plugins against the image - if: runner.os != 'Windows' run: ./gradlew -PnativeImage=true :open-java-format-jdk-bootstrap:test :gradle-open-java-format:test env: JDK21_HOME: ${{ steps.jdk21.outputs.path }} diff --git a/gradle-open-java-format/build.gradle b/gradle-open-java-format/build.gradle index adcc4b2b0..fa42af32d 100644 --- a/gradle-open-java-format/build.gradle +++ b/gradle-open-java-format/build.gradle @@ -109,8 +109,10 @@ dependencies { tasks.register("copyNativeImage", Copy.class) { from(configurations.formatterNativeImage) + // Named like the published artifact, whose extension is its artifact type: bin, and exe on Windows. + // The plugin's ExecutableTransform starts from that type, so a Windows binary must keep .exe. rename { fileName -> - String.format("%s.bin", fileName) + fileName.endsWith('.exe') ? fileName : String.format("%s.bin", fileName) } into("$buildDir/nativeImage") } diff --git a/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java b/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java index 440613656..947685bf8 100644 --- a/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java +++ b/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java @@ -56,7 +56,9 @@ public abstract class ExecutableTransform implements TransformAction existingPermissions = Files.getPosixFilePermissions(pathToExe); Files.setPosixFilePermissions( diff --git a/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageSupport.java b/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageSupport.java index cbe6a3f1a..b0b3317ef 100644 --- a/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageSupport.java +++ b/gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageSupport.java @@ -16,6 +16,7 @@ package com.palantir.javaformat.gradle; +import com.palantir.platform.Architecture; import com.palantir.platform.GradleOperatingSystem; import com.palantir.platform.OperatingSystem; import javax.inject.Inject; @@ -37,14 +38,14 @@ public boolean isNativeImageConfigured() { /** * The platforms a native image is published for, and therefore the only ones where it can be * resolved. macOS is supported on both architectures: the x86-64 image used to be excluded - * because nobody built it, and .github/workflows/ci.yml now does. musl is still absent for the - * same reason — no job produces it. Windows x86-64 is built and published, but not used here yet: - * {@link ExecutableTransform} sets POSIX permissions, which NTFS does not have, and the tests of - * this plugin have never run on Windows. + * because nobody built it, and .github/workflows/ci.yml now does. Windows has an image for + * x86-64 only, and musl none: no job produces them. */ private boolean isNativeImageSupported() { return getOs().getOperatingSystem() - .map(os -> os.equals(OperatingSystem.LINUX_GLIBC) || os.equals(OperatingSystem.MACOS)) + .map(os -> os.equals(OperatingSystem.LINUX_GLIBC) + || os.equals(OperatingSystem.MACOS) + || (os.equals(OperatingSystem.WINDOWS) && Architecture.get() == Architecture.X86_64)) .get(); } diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java index 06c85655d..28cc8d6c2 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java @@ -32,6 +32,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -112,7 +113,7 @@ private static Stream getFormatters() throws IOException { } private static List getClasspath() throws IOException { - return Splitter.on(':') + return Splitter.on(File.pathSeparatorChar) .trimResults() .omitEmptyStrings() .splitToStream(Files.readString(CLASSPATH_FILE.toPath())) @@ -122,6 +123,10 @@ private static List getClasspath() throws IOException { private static Path javaBinPath() { String javaHome = Preconditions.checkNotNull(System.getProperty("java.home"), "java.home property not set"); - return Path.of(javaHome).resolve("bin").resolve("java"); + return Path.of(javaHome).resolve("bin").resolve("java" + (isWindows() ? ".exe" : "")); + } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("windows"); } } diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPluginTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPluginTest.java index 2f0420e08..b17dd1464 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPluginTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPluginTest.java @@ -37,7 +37,9 @@ class PalantirJavaFormatIdeaPluginTest { - private static final String NATIVE_IMAGE_FILE = new File("build/nativeImage.path").getAbsolutePath(); + // Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape. + private static final String NATIVE_IMAGE_FILE = + new File("build/nativeImage.path").getAbsolutePath().replace('\\', '/'); private static final String NATIVE_CONFIG = "palantirJavaFormatNative files(file(\"" + NATIVE_IMAGE_FILE + "\").text)"; diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatPluginTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatPluginTest.java index 6dfaea0c1..71fe18407 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatPluginTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatPluginTest.java @@ -29,9 +29,12 @@ class PalantirJavaFormatPluginTest { /** ./gradlew writeImplClasspath generates this file. */ - private static final String CLASSPATH_FILE = new File("build/impl.classpath").getAbsolutePath(); + // Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape. + private static final String CLASSPATH_FILE = + new File("build/impl.classpath").getAbsolutePath().replace('\\', '/'); - private static final String NATIVE_IMAGE_FILE = new File("build/nativeImage.path").getAbsolutePath(); + private static final String NATIVE_IMAGE_FILE = + new File("build/nativeImage.path").getAbsolutePath().replace('\\', '/'); private static final String NATIVE_CONFIG = "palantirJavaFormatNative files(file(\"" + NATIVE_IMAGE_FILE + "\").text)"; @@ -59,7 +62,7 @@ void formatDiff_updates_only_lines_changed_in_git_diff(String extraGradlePropert .buildGradle( """ dependencies { - palantirJavaFormat files(file("%s").text.split(':')) + palantirJavaFormat files(file("%s").text.split(File.pathSeparator)) %s } """, diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java index 6b5ee3fc5..a1e15f8c1 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java @@ -28,9 +28,12 @@ class PalantirJavaFormatSpotlessPluginTest { /** ./gradlew writeImplClasspath generates this file. */ - private static final String CLASSPATH_FILE = new File("build/impl.classpath").getAbsolutePath(); + // Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape. + private static final String CLASSPATH_FILE = + new File("build/impl.classpath").getAbsolutePath().replace('\\', '/'); - private static final String NATIVE_IMAGE_FILE = new File("build/nativeImage.path").getAbsolutePath(); + private static final String NATIVE_IMAGE_FILE = + new File("build/nativeImage.path").getAbsolutePath().replace('\\', '/'); private static final String NATIVE_CONFIG = "palantirJavaFormatNative files(file(\"" + NATIVE_IMAGE_FILE + "\").text)"; @@ -78,7 +81,7 @@ void formats_with_spotless_when_spotless_is_applied( .buildGradle( """ dependencies { - palantirJavaFormat files(file("%s").text.split(':')) + palantirJavaFormat files(file("%s").text.split(File.pathSeparator)) %s } """, diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SpotlessExcludesTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SpotlessExcludesTest.java index 259376175..b710f5131 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SpotlessExcludesTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SpotlessExcludesTest.java @@ -29,7 +29,9 @@ class SpotlessExcludesTest { - private static final String CLASSPATH_FILE = new File("build/impl.classpath").getAbsolutePath(); + // Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape. + private static final String CLASSPATH_FILE = + new File("build/impl.classpath").getAbsolutePath().replace('\\', '/'); private static final String SOURCE_FILE = """ @@ -51,7 +53,7 @@ void setup() { .buildGradle( """ dependencies { - palantirJavaFormat files(file("%s").text.split(':')) + palantirJavaFormat files(file("%s").text.split(File.pathSeparator)) } """, CLASSPATH_FILE); diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SupportsCurrentSpotlessTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SupportsCurrentSpotlessTest.java index 3a58dd3eb..99a80668d 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SupportsCurrentSpotlessTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/SupportsCurrentSpotlessTest.java @@ -33,7 +33,9 @@ */ class SupportsCurrentSpotlessTest { - private static final String CLASSPATH_FILE = new File("build/impl.classpath").getAbsolutePath(); + // Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape. + private static final String CLASSPATH_FILE = + new File("build/impl.classpath").getAbsolutePath().replace('\\', '/'); @TempDir private Path projectDir; @@ -47,7 +49,7 @@ void palantirjavaformatplugin_works_with_current_spotless() { .buildGradle( """ dependencies { - palantirJavaFormat files(file("%s").text.split(':')) + palantirJavaFormat files(file("%s").text.split(File.pathSeparator)) } // Forces realization of the spotlessJava task, creating the spotless steps. Any diff --git a/open-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java b/open-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java index c85ebbf8a..fe7944af3 100644 --- a/open-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java +++ b/open-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java @@ -26,6 +26,7 @@ import com.palantir.javaformat.java.FormatterException; import com.palantir.javaformat.java.FormatterService; import com.palantir.javaformat.java.Replacement; +import java.io.File; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; @@ -116,7 +117,7 @@ private String getTestResourceContent(String resourceName) { private static List getClasspath() { String classpath = System.getProperty("java.class.path"); - return Splitter.on(':') + return Splitter.on(File.pathSeparatorChar) .trimResults() .omitEmptyStrings() .splitToStream(classpath) From a60e060e62dc711e318f233d73a9e64ed2d1bed0 Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Tue, 22 Sep 2026 15:40:02 +0300 Subject: [PATCH 2/3] Compare line-ending-neutral in the two tests that failed on Windows The first Windows run of the plugin tests failed three cases, none of them in the native formatter. The Windows checkout gives example1.patch CRLF line endings, so parsing it as git output found no files; git itself prints a diff with LF, so the test now reads the fixture that way. And Spotless writes the platform's line endings, CRLF on Windows, which the Spotless test compared against a text block with LF; it now compares ignoring line endings. --- .../java/com/palantir/javaformat/gradle/FormatDiffTest.java | 4 +++- .../gradle/PalantirJavaFormatSpotlessPluginTest.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java index 28cc8d6c2..389ebb8dc 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java @@ -51,8 +51,10 @@ class FormatDiffTest { @Test void parsing_git_diff_output_works() throws IOException { + // A Windows checkout gives the fixture CRLF line endings, while git prints a diff with LF. String example1 = Files.readString( - Paths.get("src/test/resources/com/palantir/javaformat/java/FormatDiffCliTest/example1.patch")); + Paths.get("src/test/resources/com/palantir/javaformat/java/FormatDiffCliTest/example1.patch")) + .replace("\r\n", "\n"); List strings = FormatDiff.parseGitDiffOutput(example1) .map(FormatDiff.SingleFileDiff::toString) diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java index a1e15f8c1..f191c45c3 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.java @@ -90,7 +90,8 @@ palantirJavaFormat files(file("%s").text.split(File.pathSeparator)) BuildResult result = project.succeeds("spotlessApply", "--info"); - assertThat(project.readFile(MAIN_JAVA)).isEqualTo(validJavaFile()); + // Spotless writes the platform's line endings, CRLF on Windows. + assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(validJavaFile()); assertThat(result.getOutput()).contains(expectedOutput); } From e96548eebbf8293b1322a6c438cb53506b64723a Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Tue, 22 Sep 2026 15:56:22 +0300 Subject: [PATCH 3/3] Expect a platform path in the git diff parsing test With the fixture read as LF, the second Windows run parsed both files, and the test failed only on how it wrote the expected path: SingleFileDiff keeps a Path, which prints with backslashes on Windows. The expected string now builds that path with Path.of, so it prints the platform's way on every system. --- .../java/com/palantir/javaformat/gradle/FormatDiffTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java index 389ebb8dc..2c800e272 100644 --- a/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java +++ b/gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/FormatDiffTest.java @@ -62,8 +62,9 @@ void parsing_git_diff_output_works() throws IOException { assertThat(strings) .containsExactly( "SingleFileDiff{path=build.gradle, lineRanges=[[24..25), [29..30)]}", - "SingleFileDiff{path=tracing/src/test/java/com/palantir/tracing/TracersTest.java, " - + "lineRanges=[[659..660), [675..676)]}"); + // The path is a Path, which prints with backslashes on Windows. + "SingleFileDiff{path=" + Path.of("tracing/src/test/java/com/palantir/tracing/TracersTest.java") + + ", lineRanges=[[659..660), [675..676)]}"); } @ParameterizedTest