Skip to content

Commit b48ba48

Browse files
authored
Merge pull request #79 from openjavaformat/format-diff-javac-exports-message
Say how to run the formatter when the Gradle JVM hides javac
2 parents a427d59 + 2dc5197 commit b48ba48

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,35 @@
1717
package com.palantir.javaformat.gradle;
1818

1919
import com.google.common.base.Suppliers;
20+
import com.google.common.collect.ImmutableList;
2021
import com.google.common.collect.Iterables;
2122
import com.palantir.javaformat.java.FormatterService;
2223
import java.io.UncheckedIOException;
2324
import java.net.MalformedURLException;
2425
import java.net.URL;
2526
import java.net.URLClassLoader;
27+
import java.util.List;
2628
import java.util.ServiceLoader;
2729
import java.util.function.Supplier;
30+
import java.util.stream.Collectors;
31+
import org.gradle.api.GradleException;
2832
import org.gradle.api.artifacts.Configuration;
2933

3034
public class JavaFormatExtension {
35+
/**
36+
* The javac packages the formatter reads. A plain Gradle JVM exports none of them, and the formatter then fails
37+
* on its first file with an IllegalAccessError that names a class and a module and nothing the user can change.
38+
* The check below fails before the first file instead, with the two settings that make it work.
39+
*/
40+
private static final ImmutableList<String> JAVAC_PACKAGES = ImmutableList.of(
41+
"com.sun.tools.javac.api",
42+
"com.sun.tools.javac.file",
43+
"com.sun.tools.javac.parser",
44+
"com.sun.tools.javac.tree",
45+
"com.sun.tools.javac.util");
46+
47+
private static final String DOCS = "https://openjavaformat.dev/get-started/gradle/#choose-how-the-formatter-runs";
48+
3149
private final Configuration configuration;
3250
private final Supplier<FormatterService> memoizedService;
3351

@@ -53,6 +71,42 @@ private FormatterService serviceLoadInternal() {
5371
.toArray(URL[]::new);
5472

5573
ClassLoader classLoader = new URLClassLoader(jarUris, FormatterService.class.getClassLoader());
74+
checkJavacIsExportedTo(classLoader);
5675
return Iterables.getOnlyElement(ServiceLoader.load(FormatterService.class, classLoader));
5776
}
77+
78+
/**
79+
* Fails with the fix spelt out when this JVM does not export javac's internals to the formatter's class loader:
80+
* either the native binary, which runs outside the JVM, or the {@code --add-exports} flags on the Gradle JVM.
81+
*/
82+
private static void checkJavacIsExportedTo(ClassLoader formatterClassLoader) {
83+
Module jdkCompiler = ModuleLayer.boot()
84+
.findModule("jdk.compiler")
85+
.orElseThrow(() -> new GradleException("open-java-format needs the module jdk.compiler, which this"
86+
+ " Gradle JVM does not have: run Gradle on a JDK, not a JRE. See " + DOCS));
87+
Module formatter = formatterClassLoader.getUnnamedModule();
88+
List<String> notExported = JAVAC_PACKAGES.stream()
89+
.filter(javacPackage -> !jdkCompiler.isExported(javacPackage, formatter))
90+
.collect(Collectors.toList());
91+
if (notExported.isEmpty()) {
92+
return;
93+
}
94+
String addExports = JAVAC_PACKAGES.stream()
95+
.map(javacPackage -> "--add-exports jdk.compiler/" + javacPackage + "=ALL-UNNAMED")
96+
.collect(Collectors.joining(" "));
97+
throw new GradleException("""
98+
open-java-format cannot run inside this Gradle JVM: module jdk.compiler does not export %s to it. \
99+
Set one of these in gradle.properties:
100+
101+
openjavaformat.native.formatter=true
102+
runs the formatter as a native binary, outside the Gradle JVM (Linux with glibc, macOS, \
103+
Windows on x86-64)
104+
105+
org.gradle.jvmargs=%s
106+
opens javac's packages to the Gradle JVM; if the file already sets org.gradle.jvmargs, add \
107+
the flags to that line
108+
109+
See %s\
110+
""".formatted(String.join(", ", notExported), addExports, DOCS));
111+
}
58112
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* (c) Copyright 2026 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+
package com.palantir.javaformat.gradle;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import com.palantir.javaformat.gradle.testing.GradleTestProject;
21+
import java.io.File;
22+
import java.io.IOException;
23+
import java.nio.file.Path;
24+
import org.gradle.testkit.runner.BuildResult;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.io.TempDir;
27+
28+
/**
29+
* A build without the JVM flags used to fail on the first file with a raw IllegalAccessError from the formatter's
30+
* internals (#19). It fails before the first file now, naming the two settings that make the formatter run. This
31+
* class runs in the default test task; PalantirJavaFormatPluginTest does not, because it needs the native binary.
32+
*/
33+
class FormatDiffWithoutJavacExportsTest {
34+
35+
/** ./gradlew writeImplClasspath generates this file. Forward slashes: the path goes into a Groovy string. */
36+
private static final String CLASSPATH_FILE =
37+
new File("build/impl.classpath").getAbsolutePath().replace('\\', '/');
38+
39+
private static final String MAIN_JAVA = "src/main/java/Main.java";
40+
41+
@TempDir
42+
private Path projectDir;
43+
44+
@Test
45+
void formatDiff_names_both_settings_when_the_gradle_jvm_does_not_export_javac()
46+
throws IOException, InterruptedException {
47+
GradleTestProject project = new GradleTestProject(projectDir)
48+
.plugins("java", "dev.openjavaformat.java-format")
49+
.buildGradle(
50+
"""
51+
dependencies {
52+
palantirJavaFormat files(file("%s").text.split(File.pathSeparator))
53+
}
54+
""",
55+
CLASSPATH_FILE);
56+
57+
git(project, "init");
58+
git(project, "config", "user.name", "Foo");
59+
git(project, "config", "user.email", "foo@bar.com");
60+
// The repository this runs in may sign commits; a throwaway one has no key to sign with.
61+
git(project, "config", "commit.gpgsign", "false");
62+
project.writeFile(MAIN_JAVA, "class Main {}\n");
63+
git(project, "add", ".");
64+
git(project, "commit", "-m", "Commit");
65+
project.writeFile(MAIN_JAVA, "class Main { int x; }\n");
66+
67+
BuildResult result = project.fails("formatDiff");
68+
69+
// Which of the five packages the message lists depends on what the Gradle daemon exports on its own;
70+
// com.sun.tools.javac.parser, the one the old error named, is never among those.
71+
assertThat(result.getOutput())
72+
.contains("open-java-format cannot run inside this Gradle JVM: module jdk.compiler does not export ")
73+
.contains("com.sun.tools.javac.parser")
74+
.contains("openjavaformat.native.formatter=true")
75+
.contains("org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"
76+
+ " --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED"
77+
+ " --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED"
78+
+ " --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"
79+
+ " --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED")
80+
.contains("https://openjavaformat.dev/get-started/gradle/#choose-how-the-formatter-runs")
81+
.doesNotContain("IllegalAccessError")
82+
.doesNotContain("Formatting ");
83+
// The check runs before the first file, so nothing was written.
84+
assertThat(project.readFile(MAIN_JAVA)).isEqualTo("class Main { int x; }\n");
85+
}
86+
87+
private static void git(GradleTestProject project, String... args) throws IOException, InterruptedException {
88+
String[] command = new String[args.length + 1];
89+
command[0] = "git";
90+
System.arraycopy(args, 0, command, 1, args.length);
91+
Process process = new ProcessBuilder(command).directory(project.path().toFile()).start();
92+
int exitCode = process.waitFor();
93+
if (exitCode != 0) {
94+
throw new RuntimeException("git " + String.join(" ", args) + " failed with exit code " + exitCode);
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)