From 04f7cb6b9f721a88b447026fd26585499e29a57d Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Fri, 29 Oct 2021 08:31:19 -0400 Subject: [PATCH] Goethe re-bootstraps itself for jdk16+ support (#53) --- changelog/@unreleased/pr-53.v2.yml | 5 + .../goethe/BootstrappingFormatterFacade.java | 91 +++++++++++++++++++ .../goethe/DirectFormatterFacade.java | 70 ++++++++++++++ .../com/palantir/goethe/FormatterFacade.java | 22 +++++ .../goethe/FormatterFacadeFactory.java | 36 ++++++++ .../main/java/com/palantir/goethe/Goethe.java | 48 +--------- .../com/palantir/goethe/GoetheException.java | 4 + .../java/com/palantir/goethe/GoetheMain.java | 43 +++++++++ 8 files changed, 273 insertions(+), 46 deletions(-) create mode 100644 changelog/@unreleased/pr-53.v2.yml create mode 100644 goethe/src/main/java/com/palantir/goethe/BootstrappingFormatterFacade.java create mode 100644 goethe/src/main/java/com/palantir/goethe/DirectFormatterFacade.java create mode 100644 goethe/src/main/java/com/palantir/goethe/FormatterFacade.java create mode 100644 goethe/src/main/java/com/palantir/goethe/FormatterFacadeFactory.java create mode 100644 goethe/src/main/java/com/palantir/goethe/GoetheMain.java diff --git a/changelog/@unreleased/pr-53.v2.yml b/changelog/@unreleased/pr-53.v2.yml new file mode 100644 index 0000000..057dbd6 --- /dev/null +++ b/changelog/@unreleased/pr-53.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: Goethe re-bootstraps itself for jdk16+ support + links: + - https://github.com/palantir/goethe/pull/53 diff --git a/goethe/src/main/java/com/palantir/goethe/BootstrappingFormatterFacade.java b/goethe/src/main/java/com/palantir/goethe/BootstrappingFormatterFacade.java new file mode 100644 index 0000000..04dac54 --- /dev/null +++ b/goethe/src/main/java/com/palantir/goethe/BootstrappingFormatterFacade.java @@ -0,0 +1,91 @@ +/* + * (c) Copyright 2021 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.goethe; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** A {@link FormatterFacade} implementation which spawns new java processes with {@link #EXPORTS} applied. */ +final class BootstrappingFormatterFacade implements FormatterFacade { + + static final ImmutableList EXPORTS = ImmutableList.of( + "--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"); + + @Override + public String formatSource(String className, String unformattedSource) throws GoetheException { + try { + Process process = new ProcessBuilder(ImmutableList.builder() + .add(new File(System.getProperty("java.home"), "bin/java").getAbsolutePath()) + .addAll(EXPORTS) + .add( // Classpath + "-cp", + System.getProperty("java.class.path"), + // Main class + GoetheMain.class.getName(), + // Args + className) + .build()) + .start(); + try (OutputStream outputStream = process.getOutputStream()) { + outputStream.write(unformattedSource.getBytes(StandardCharsets.UTF_8)); + } + byte[] data; + try (InputStream inputStream = process.getInputStream()) { + data = ByteStreams.toByteArray(inputStream); + } + int exitStatus = process.waitFor(); + if (exitStatus != 0) { + throw new GoetheException(String.format( + "Formatter exited non-zero (%d) formatting class %s:\n%s", + exitStatus, className, getErrorOutput(process))); + } + return new String(data, StandardCharsets.UTF_8); + } catch (IOException | InterruptedException e) { + throw new GoetheException("Failed to bootstrap jdk", e); + } + } + + private static String getErrorOutput(Process process) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream inputStream = process.getErrorStream()) { + ByteStreams.copy(inputStream, baos); + } catch (IOException | RuntimeException e) { + String diagnostic = ""; + try { + baos.write(diagnostic.getBytes(StandardCharsets.UTF_8)); + } catch (IOException ignored) { + // should not happen + } + } + return baos.toString(StandardCharsets.UTF_8); + } +} diff --git a/goethe/src/main/java/com/palantir/goethe/DirectFormatterFacade.java b/goethe/src/main/java/com/palantir/goethe/DirectFormatterFacade.java new file mode 100644 index 0000000..1f193e7 --- /dev/null +++ b/goethe/src/main/java/com/palantir/goethe/DirectFormatterFacade.java @@ -0,0 +1,70 @@ +/* + * (c) Copyright 2021 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.goethe; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.palantir.javaformat.java.Formatter; +import com.palantir.javaformat.java.FormatterDiagnostic; +import com.palantir.javaformat.java.FormatterException; +import com.palantir.javaformat.java.JavaFormatterOptions; +import java.util.List; + +final class DirectFormatterFacade implements FormatterFacade { + + private final Formatter formatter = Formatter.createFormatter(JavaFormatterOptions.builder() + .style(JavaFormatterOptions.Style.PALANTIR) + .build()); + + @Override + public String formatSource(String className, String unformattedSource) throws GoetheException { + try { + return formatter.formatSource(unformattedSource); + } catch (FormatterException e) { + throw new GoetheException(generateMessage(className, unformattedSource, e.diagnostics()), e); + } + } + + /** + * Attempt to provide as much actionable information as possible to understand why formatting is failing. This + * is common when generated code is incorrect and cannot compile, so we mustn't make it difficult to understand + * the problem. + */ + private static String generateMessage( + String className, String unformattedSource, List formatterDiagnostics) { + try { + List lines = Splitter.on('\n').splitToList(unformattedSource); + StringBuilder failureText = new StringBuilder(); + failureText.append("Failed to format '").append(className).append("'\n"); + for (FormatterDiagnostic formatterDiagnostic : formatterDiagnostics) { + failureText + .append(formatterDiagnostic.message()) + .append("\n") + // Diagnostic values are one-indexed, while our list is zero-indexed. + .append(lines.get(formatterDiagnostic.line() - 1)) + .append('\n') + // Offset by two to convert from one-indexed to zero indexed values, and account for the caret. + .append(Strings.repeat(" ", Math.max(0, formatterDiagnostic.column() - 2))) + .append("^\n\n"); + } + return CharMatcher.is('\n').trimFrom(failureText.toString()); + } catch (RuntimeException e) { + return "Failed to format:\n" + unformattedSource; + } + } +} diff --git a/goethe/src/main/java/com/palantir/goethe/FormatterFacade.java b/goethe/src/main/java/com/palantir/goethe/FormatterFacade.java new file mode 100644 index 0000000..617789e --- /dev/null +++ b/goethe/src/main/java/com/palantir/goethe/FormatterFacade.java @@ -0,0 +1,22 @@ +/* + * (c) Copyright 2021 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.goethe; + +interface FormatterFacade { + + String formatSource(String className, String unformattedSource) throws GoetheException; +} diff --git a/goethe/src/main/java/com/palantir/goethe/FormatterFacadeFactory.java b/goethe/src/main/java/com/palantir/goethe/FormatterFacadeFactory.java new file mode 100644 index 0000000..4dd67e3 --- /dev/null +++ b/goethe/src/main/java/com/palantir/goethe/FormatterFacadeFactory.java @@ -0,0 +1,36 @@ +/* + * (c) Copyright 2021 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.goethe; + +import java.lang.management.ManagementFactory; + +final class FormatterFacadeFactory { + private FormatterFacadeFactory() {} + + static FormatterFacade create() { + if (Runtime.version().feature() < 16 || currentJvmHasExportArgs()) { + return new DirectFormatterFacade(); + } + return new BootstrappingFormatterFacade(); + } + + private static boolean currentJvmHasExportArgs() { + return ManagementFactory.getRuntimeMXBean() + .getInputArguments() + .containsAll(BootstrappingFormatterFacade.EXPORTS); + } +} diff --git a/goethe/src/main/java/com/palantir/goethe/Goethe.java b/goethe/src/main/java/com/palantir/goethe/Goethe.java index a461d65..2d783d7 100644 --- a/goethe/src/main/java/com/palantir/goethe/Goethe.java +++ b/goethe/src/main/java/com/palantir/goethe/Goethe.java @@ -16,21 +16,14 @@ package com.palantir.goethe; -import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; -import com.google.common.base.Strings; -import com.palantir.javaformat.java.Formatter; -import com.palantir.javaformat.java.FormatterDiagnostic; -import com.palantir.javaformat.java.FormatterException; -import com.palantir.javaformat.java.JavaFormatterOptions; import com.squareup.javapoet.JavaFile; import java.io.File; import java.io.IOException; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import javax.annotation.processing.Filer; import javax.lang.model.element.Element; import javax.tools.JavaFileObject; @@ -46,9 +39,7 @@ */ public final class Goethe { - private static final Formatter JAVA_FORMATTER = Formatter.createFormatter(JavaFormatterOptions.builder() - .style(JavaFormatterOptions.Style.PALANTIR) - .build()); + private static final FormatterFacade JAVA_FORMATTER = FormatterFacadeFactory.create(); /** * Format a {@link JavaFile javapoet java file} into a {@link String}. @@ -60,9 +51,7 @@ public static String formatAsString(JavaFile file) { StringBuilder rawSource = new StringBuilder(); try { file.writeTo(rawSource); - return JAVA_FORMATTER.formatSource(rawSource.toString()); - } catch (FormatterException e) { - throw new GoetheException(generateMessage(file, rawSource.toString(), e.diagnostics()), e); + return JAVA_FORMATTER.formatSource(file.packageName + '.' + file.typeSpec.name, rawSource.toString()); } catch (IOException e) { throw new GoetheException("Formatting failed", e); } @@ -115,39 +104,6 @@ public static Path formatAndEmit(JavaFile file, Path baseDir) { } } - /** - * Attempt to provide as much actionable information as possible to understand why formatting is failing. This - * is common when generated code is incorrect and cannot compile, so we mustn't make it difficult to understand - * the problem. - */ - private static String generateMessage( - JavaFile file, String unformattedSource, List formatterDiagnostics) { - try { - List lines = Splitter.on('\n').splitToList(unformattedSource); - StringBuilder failureText = new StringBuilder(); - failureText - .append("Failed to format '") - .append(file.packageName) - .append('.') - .append(file.typeSpec.name) - .append("'\n"); - for (FormatterDiagnostic formatterDiagnostic : formatterDiagnostics) { - failureText - .append(formatterDiagnostic.message()) - .append("\n") - // Diagnostic values are one-indexed, while our list is zero-indexed. - .append(lines.get(formatterDiagnostic.line() - 1)) - .append('\n') - // Offset by two to convert from one-indexed to zero indexed values, and account for the caret. - .append(Strings.repeat(" ", Math.max(0, formatterDiagnostic.column() - 2))) - .append("^\n\n"); - } - return CharMatcher.is('\n').trimFrom(failureText.toString()); - } catch (RuntimeException e) { - return "Failed to format:\n" + unformattedSource; - } - } - private static String className(JavaFile file) { return file.packageName.isEmpty() ? file.typeSpec.name : file.packageName + "." + file.typeSpec.name; } diff --git a/goethe/src/main/java/com/palantir/goethe/GoetheException.java b/goethe/src/main/java/com/palantir/goethe/GoetheException.java index 9662799..2e0d4cd 100644 --- a/goethe/src/main/java/com/palantir/goethe/GoetheException.java +++ b/goethe/src/main/java/com/palantir/goethe/GoetheException.java @@ -18,6 +18,10 @@ /** Marker exception describing failures emitted from the Goethe library. */ public final class GoetheException extends IllegalStateException { + GoetheException(String message) { + super(message); + } + GoetheException(String message, Throwable cause) { super(message, cause); } diff --git a/goethe/src/main/java/com/palantir/goethe/GoetheMain.java b/goethe/src/main/java/com/palantir/goethe/GoetheMain.java new file mode 100644 index 0000000..04d8a0e --- /dev/null +++ b/goethe/src/main/java/com/palantir/goethe/GoetheMain.java @@ -0,0 +1,43 @@ +/* + * (c) Copyright 2021 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.goethe; + +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** Main class used internally to bootstrap the formatter with additional jvm args for compiler class access. */ +@SuppressWarnings({"checkstyle:BanSystemErr", "checkstyle:BanSystemOut"}) +final class GoetheMain { + + private GoetheMain() {} + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Class name argument is required"); + System.exit(1); + } + String className = args[0]; + String input = new String(ByteStreams.toByteArray(System.in), StandardCharsets.UTF_8); + try { + System.out.print(new DirectFormatterFacade().formatSource(className, input)); + } catch (GoetheException e) { + System.err.println(e.getMessage()); + System.exit(1); + } + } +}