|
| 1 | +/* |
| 2 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | + * you may not use this file except in compliance with the License. |
| 4 | + * You may obtain a copy of the License at |
| 5 | + * |
| 6 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 7 | + * |
| 8 | + * Unless required by applicable law or agreed to in writing, software |
| 9 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 10 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 11 | + * See the License for the specific language governing permissions and |
| 12 | + * limitations under the License. |
| 13 | + */ |
| 14 | + |
| 15 | +package com.palantir.javaformat.gradle; |
| 16 | + |
| 17 | +import com.google.common.collect.Iterables; |
| 18 | +import com.palantir.javaformat.java.FormatterService; |
| 19 | +import java.io.File; |
| 20 | +import java.io.UncheckedIOException; |
| 21 | +import java.net.MalformedURLException; |
| 22 | +import java.net.URL; |
| 23 | +import java.net.URLClassLoader; |
| 24 | +import java.util.Collection; |
| 25 | +import java.util.HashMap; |
| 26 | +import java.util.List; |
| 27 | +import java.util.Map; |
| 28 | +import java.util.ServiceLoader; |
| 29 | +import java.util.stream.Collectors; |
| 30 | +import org.gradle.api.GradleException; |
| 31 | + |
| 32 | +/** |
| 33 | + * Loads the Java-based formatter from its jars, in a class loader of its own, so that the formatter's dependencies do |
| 34 | + * not meet the build's. |
| 35 | + * |
| 36 | + * <p>The formatter stays loaded for as long as its jars stay the same, across the tasks and builds of one daemon. The |
| 37 | + * tasks have nowhere else to keep it: a build that reuses its configuration cache entry creates no project and no |
| 38 | + * {@link JavaFormatExtension}, and the Spotless step comes back from the cache with nothing that does not serialize. |
| 39 | + */ |
| 40 | +public final class FormatterServiceLoader { |
| 41 | + /** |
| 42 | + * The javac packages the formatter reads. A plain Gradle JVM exports none of them, and the formatter then fails |
| 43 | + * on its first file with an IllegalAccessError that names a class and a module and nothing the user can change. |
| 44 | + * The check below fails before the first file instead, with the two settings that make it work. |
| 45 | + */ |
| 46 | + private static final List<String> JAVAC_PACKAGES = List.of( |
| 47 | + "com.sun.tools.javac.api", |
| 48 | + "com.sun.tools.javac.file", |
| 49 | + "com.sun.tools.javac.parser", |
| 50 | + "com.sun.tools.javac.tree", |
| 51 | + "com.sun.tools.javac.util"); |
| 52 | + |
| 53 | + private static final String DOCS = "https://openjavaformat.dev/get-started/gradle/#choose-how-the-formatter-runs"; |
| 54 | + |
| 55 | + /** |
| 56 | + * The formatter loaded last, by the jars it came from. One at a time: a build has one formatter classpath, and a |
| 57 | + * daemon that outlives a change of formatter version should not keep the old one. |
| 58 | + */ |
| 59 | + private static final Map<List<Jar>, FormatterService> LOADED = new HashMap<>(); |
| 60 | + |
| 61 | + private FormatterServiceLoader() {} |
| 62 | + |
| 63 | + /** The formatter in these jars: the one loaded last if they have not changed since, a new one otherwise. */ |
| 64 | + public static synchronized FormatterService load(Collection<File> jars) { |
| 65 | + var key = jars.stream().map(Jar::of).toList(); |
| 66 | + if (!LOADED.containsKey(key)) { |
| 67 | + LOADED.clear(); |
| 68 | + } |
| 69 | + return LOADED.computeIfAbsent(key, _key -> loadUncached(jars)); |
| 70 | + } |
| 71 | + |
| 72 | + @SuppressWarnings("for-rollout:NullAway") |
| 73 | + private static FormatterService loadUncached(Collection<File> jars) { |
| 74 | + URL[] jarUris = jars.stream() |
| 75 | + .map(file -> { |
| 76 | + try { |
| 77 | + return file.toURI().toURL(); |
| 78 | + } catch (MalformedURLException e) { |
| 79 | + throw new UncheckedIOException("Unable to convert URI to URL: " + file, e); |
| 80 | + } |
| 81 | + }) |
| 82 | + .toArray(URL[]::new); |
| 83 | + |
| 84 | + ClassLoader classLoader = new URLClassLoader(jarUris, FormatterService.class.getClassLoader()); |
| 85 | + checkJavacIsExportedTo(classLoader); |
| 86 | + return Iterables.getOnlyElement(ServiceLoader.load(FormatterService.class, classLoader)); |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Fails with the fix spelt out when this JVM does not export javac's internals to the formatter's class loader: |
| 91 | + * either the native binary, which runs outside the JVM, or the {@code --add-exports} flags on the Gradle JVM. |
| 92 | + */ |
| 93 | + private static void checkJavacIsExportedTo(ClassLoader formatterClassLoader) { |
| 94 | + Module jdkCompiler = ModuleLayer.boot() |
| 95 | + .findModule("jdk.compiler") |
| 96 | + .orElseThrow(() -> new GradleException("open-java-format needs the module jdk.compiler, which this" |
| 97 | + + " Gradle JVM does not have: run Gradle on a JDK, not a JRE. See " + DOCS)); |
| 98 | + Module formatter = formatterClassLoader.getUnnamedModule(); |
| 99 | + List<String> notExported = JAVAC_PACKAGES.stream() |
| 100 | + .filter(javacPackage -> !jdkCompiler.isExported(javacPackage, formatter)) |
| 101 | + .toList(); |
| 102 | + if (notExported.isEmpty()) { |
| 103 | + return; |
| 104 | + } |
| 105 | + String addExports = JAVAC_PACKAGES.stream() |
| 106 | + .map(javacPackage -> "--add-exports jdk.compiler/" + javacPackage + "=ALL-UNNAMED") |
| 107 | + .collect(Collectors.joining(" ")); |
| 108 | + throw new GradleException(""" |
| 109 | + open-java-format cannot run inside this Gradle JVM: module jdk.compiler does not export %s to it. \ |
| 110 | + Set one of these in gradle.properties: |
| 111 | +
|
| 112 | + openjavaformat.native.formatter=true |
| 113 | + runs the formatter as a native binary, outside the Gradle JVM (Linux with glibc, macOS, \ |
| 114 | + Windows on x86-64) |
| 115 | +
|
| 116 | + org.gradle.jvmargs=%s |
| 117 | + opens javac's packages to the Gradle JVM; if the file already sets org.gradle.jvmargs, add \ |
| 118 | + the flags to that line |
| 119 | +
|
| 120 | + See %s\ |
| 121 | + """.formatted(String.join(", ", notExported), addExports, DOCS)); |
| 122 | + } |
| 123 | + |
| 124 | + /** A jar as it was when the formatter was loaded from it: rebuilt in place, it changes size or time. */ |
| 125 | + private record Jar(File file, long length, long lastModified) { |
| 126 | + static Jar of(File file) { |
| 127 | + return new Jar(file, file.length(), file.lastModified()); |
| 128 | + } |
| 129 | + } |
| 130 | +} |
0 commit comments