diff --git a/.palantir/revapi.yml b/.palantir/revapi.yml index 430942cd5..bea1f15f9 100644 --- a/.palantir/revapi.yml +++ b/.palantir/revapi.yml @@ -22,3 +22,34 @@ acceptedBreaks: new: "method java.lang.String com.palantir.javaformat.java.FormatterService::fixImports(java.lang.String)\ \ throws com.palantir.javaformat.java.FormatterException" justification: "new fixImports only method added to spi" + "2.83.0": + com.palantir.javaformat:gradle-palantir-java-format: + - code: "java.annotation.removed" + old: "method void com.palantir.javaformat.gradle.SpotlessInterop::(java.util.function.Supplier)" + new: "method void com.palantir.javaformat.gradle.SpotlessInterop::()" + justification: "Changed method signatures PalantirJavaFormatStep::create and\ + \ SpotlessInterop:: are not used outside of this project, SpotlessInterop::getWorkerExecutor()\ + \ is additive and so should not cause issues and SpotlessInterop is not used\ + \ outside of this project" + - code: "java.method.abstractMethodAdded" + new: "method org.gradle.workers.WorkerExecutor com.palantir.javaformat.gradle.SpotlessInterop::getWorkerExecutor()" + justification: "Changed method signatures PalantirJavaFormatStep::create and\ + \ SpotlessInterop:: are not used outside of this project, SpotlessInterop::getWorkerExecutor()\ + \ is additive and so should not cause issues and SpotlessInterop is not used\ + \ outside of this project" + - code: "java.method.numberOfParametersChanged" + old: "method void com.palantir.javaformat.gradle.SpotlessInterop::(java.util.function.Supplier)" + new: "method void com.palantir.javaformat.gradle.SpotlessInterop::()" + justification: "Changed method signatures PalantirJavaFormatStep::create and\ + \ SpotlessInterop:: are not used outside of this project, SpotlessInterop::getWorkerExecutor()\ + \ is additive and so should not cause issues and SpotlessInterop is not used\ + \ outside of this project" + - code: "java.method.parameterTypeChanged" + old: "parameter com.diffplug.spotless.FormatterStep com.palantir.javaformat.gradle.spotless.PalantirJavaFormatStep::create(org.gradle.api.artifacts.Configuration,\ + \ ===java.util.function.Supplier===)" + new: "parameter com.diffplug.spotless.FormatterStep com.palantir.javaformat.gradle.spotless.PalantirJavaFormatStep::create(org.gradle.api.artifacts.Configuration,\ + \ ===org.gradle.workers.WorkerExecutor===)" + justification: "Changed method signatures PalantirJavaFormatStep::create and\ + \ SpotlessInterop:: are not used outside of this project, SpotlessInterop::getWorkerExecutor()\ + \ is additive and so should not cause issues and SpotlessInterop is not used\ + \ outside of this project" diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java index fad8d62f3..6f87371ba 100644 --- a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java @@ -17,8 +17,6 @@ import com.diffplug.gradle.spotless.SpotlessExtension; import com.google.common.collect.ImmutableList; -import com.palantir.javaformat.java.FormatterService; -import java.util.function.Supplier; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -32,10 +30,7 @@ public void apply(Project project) { Project rootProject = project.getRootProject(); rootProject.getPluginManager().apply(PalantirJavaFormatProviderPlugin.class); - Supplier memoizedService = - rootProject.getExtensions().getByType(JavaFormatExtension.class)::serviceLoad; - - SpotlessInterop spotlessInterop = rootProject.getObjects().newInstance(SpotlessInterop.class, memoizedService); + SpotlessInterop spotlessInterop = rootProject.getObjects().newInstance(SpotlessInterop.class); project.getPluginManager().withPlugin("java", _javaPlugin -> { SPOTLESS_PLUGINS.forEach( spotlessPluginId -> project.getPluginManager().withPlugin(spotlessPluginId, _spotlessPlugin -> { diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java index 98b4dedaa..6f92e4e5e 100644 --- a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java @@ -19,8 +19,6 @@ import com.diffplug.spotless.FormatterStep; import com.palantir.javaformat.gradle.spotless.NativePalantirJavaFormatStep; import com.palantir.javaformat.gradle.spotless.PalantirJavaFormatStep; -import com.palantir.javaformat.java.FormatterService; -import java.util.function.Supplier; import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.JavaVersion; @@ -28,6 +26,7 @@ import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; import org.gradle.api.tasks.Nested; +import org.gradle.workers.WorkerExecutor; /** * Class that exists only to encapsulate accessing spotless classes, so that Gradle can generate a decorated class for @@ -36,8 +35,6 @@ public abstract class SpotlessInterop implements Action { private static final Logger logger = Logging.getLogger(SpotlessInterop.class); - private final Supplier formatterService; - @Nested protected abstract NativeImageSupport getNativeImageSupport(); @@ -45,9 +42,7 @@ public abstract class SpotlessInterop implements Action { protected abstract ConfigurationContainer getConfigurations(); @Inject - public SpotlessInterop(Supplier formatterService) { - this.formatterService = formatterService; - } + protected abstract WorkerExecutor getWorkerExecutor(); @Override public void execute(JavaExtension java) { @@ -62,8 +57,9 @@ private FormatterStep spotlessJavaFormatStep() { return NativePalantirJavaFormatStep.create( getConfigurations().getByName(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME)); } - logger.info("Using the Java-based formatter {}", JavaVersion.current()); + logger.info("Using the Java-based formatter with Worker API {}", JavaVersion.current()); return PalantirJavaFormatStep.create( - getConfigurations().getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME), formatterService); + getConfigurations().getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME), + getWorkerExecutor()); } } diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/FormatJavaParameters.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/FormatJavaParameters.java new file mode 100644 index 000000000..1c0a943ab --- /dev/null +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/FormatJavaParameters.java @@ -0,0 +1,30 @@ +/* + * (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.spotless; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.workers.WorkParameters; + +public interface FormatJavaParameters extends WorkParameters { + + RegularFileProperty getInputFile(); + + RegularFileProperty getOutputFile(); + + ConfigurableFileCollection getFormatterClasspath(); +} diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/FormatJavaWorkAction.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/FormatJavaWorkAction.java new file mode 100644 index 000000000..647c83d64 --- /dev/null +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/FormatJavaWorkAction.java @@ -0,0 +1,68 @@ +/* + * (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.spotless; + +import com.google.common.base.Suppliers; +import com.palantir.javaformat.java.FormatterService; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.ServiceLoader; +import java.util.function.Supplier; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.workers.WorkAction; + +public abstract class FormatJavaWorkAction implements WorkAction { + + private static final Logger logger = Logging.getLogger(FormatJavaWorkAction.class); + + private static final Supplier formatterService = Suppliers.memoize(() -> { + logger.info("Loading FormatterService in worker daemon"); + return ServiceLoader.load(FormatterService.class, FormatJavaWorkAction.class.getClassLoader()) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "No FormatterService found. Ensure palantir-java-format is on the worker classpath.")); + }); + + @Override + public void execute() { + FormatJavaParameters params = getParameters(); + + try { + File inputFile = params.getInputFile().get().getAsFile(); + File outputFile = params.getOutputFile().get().getAsFile(); + + String input = Files.readString(inputFile.toPath()); + + logger.debug("Formatting file with worker process: {}", inputFile.getName()); + + String output = formatterService.get().formatSourceReflowStringsAndFixImports(input); + + Files.writeString(outputFile.toPath(), output); + + } catch (IOException e) { + throw new UncheckedIOException("Failed to format Java file", e); + } catch (Exception e) { + throw new RuntimeException( + "Formatting failed in worker daemon. This may indicate a bug in palantir-java-format " + + "or insufficient resources. Original error: " + e.getMessage(), + e); + } + } +} diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/PalantirJavaFormatStep.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/PalantirJavaFormatStep.java index 9b209a1fe..c33304278 100644 --- a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/PalantirJavaFormatStep.java +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/PalantirJavaFormatStep.java @@ -18,13 +18,15 @@ import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; -import com.palantir.javaformat.java.FormatterService; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.io.UncheckedIOException; +import java.nio.file.Files; import java.util.function.Supplier; import org.gradle.api.artifacts.Configuration; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; public final class PalantirJavaFormatStep { @@ -35,10 +37,10 @@ private PalantirJavaFormatStep() {} private static final String NAME = "palantir-java-format"; /** Creates a step which formats everything - code, import order, and unused imports. */ - public static FormatterStep create(Configuration palantirJavaFormat, Supplier memoizedService) { + public static FormatterStep create(Configuration palantirJavaFormat, WorkerExecutor workerExecutor) { ensureImplementationNotDirectlyLoadable(); return FormatterStep.createLazy( - NAME, () -> new State(palantirJavaFormat::getFiles, memoizedService), State::createFormat); + NAME, () -> new State(palantirJavaFormat::getFiles, workerExecutor), State::createFormat); } static final class State implements Serializable { @@ -65,19 +67,19 @@ static final class State implements Serializable { private final transient Supplier> jarsSupplier; - // Transient as this is not serializable. - private final transient Supplier memoizedFormatter; + private final transient WorkerExecutor workerExecutor; /** - * Build a cacheable state for spotless from the given jars, that uses the given {@link FormatterService}. + * Build a cacheable state for spotless from the given jars, using the Worker API for process isolation. * * @param jarsSupplier Supplies the jars that contain the palantir-java-format implementation. This is only used for caching and * up-to-dateness purposes. + * @param workerExecutor WorkerExecutor for process isolation. */ @SuppressWarnings("for-rollout:NullAway") - State(Supplier> jarsSupplier, Supplier memoizedFormatter) { + State(Supplier> jarsSupplier, WorkerExecutor workerExecutor) { this.jarsSupplier = jarsSupplier; - this.memoizedFormatter = memoizedFormatter; + this.workerExecutor = workerExecutor; } @SuppressWarnings("NullableProblems") @@ -91,12 +93,55 @@ FormatterFunc createFormat() { // https://github.com/diffplug/spotless/blob/228eb10af382b19e130d8d9479f7a95238cb4358/lib/src/main/java/com/diffplug/spotless/FileSignature.java#L138-L143 this.jarsSignature = FileSignature.signAsSet(jars); - return memoizedFormatter.get().formatSourceReflowStringsAndFixImports(input); + return formatWithWorker(input, jars); } catch (IOException e) { throw new UncheckedIOException(e); } }; } + + private String formatWithWorker(String input, Iterable jars) throws IOException { + File inputFile = + Files.createTempFile("palantir-java-format-input-", ".java").toFile(); + File outputFile = Files.createTempFile("palantir-java-format-output-", ".java") + .toFile(); + + try { + Files.writeString(inputFile.toPath(), input); + + WorkQueue workQueue = workerExecutor.processIsolation(workerSpec -> { + workerSpec.getClasspath().from(jars); + workerSpec.forkOptions(options -> { + options.jvmArgs( + "--add-exports", + "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports", + "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports", + "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports", + "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports", + "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"); + }); + }); + + workQueue.submit(FormatJavaWorkAction.class, parameters -> { + parameters.getInputFile().set(inputFile); + parameters.getOutputFile().set(outputFile); + parameters.getFormatterClasspath().from(jars); + }); + + workQueue.await(); + + return Files.readString(outputFile.toPath()); + } catch (Exception e) { + throw new RuntimeException("Formatting failed using Worker API", e); + } finally { + inputFile.delete(); + outputFile.delete(); + } + } } private static void ensureImplementationNotDirectlyLoadable() { diff --git a/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.groovy b/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.groovy index 2c966aafe..fb82c4818 100644 --- a/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.groovy +++ b/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPluginTest.groovy @@ -84,13 +84,8 @@ class PalantirJavaFormatSpotlessPluginTest extends IntegrationTestKitSpec { } """.stripIndent(true) - // Add jvm args to allow spotless and formatter gradle plugins to run with Java 16+ + // Worker API now handles JVM args directly, no need to set them in gradle.properties file('gradle.properties') << ''' - org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED palantir.jdk.setup.enabled=true '''.stripIndent(true) file('gradle.properties') << extraGradleProperties