diff --git a/conjure-core/src/main/java/com/palantir/conjure/parser/types/ConjureType.java b/conjure-core/src/main/java/com/palantir/conjure/parser/types/ConjureType.java index d6cbb17c8..cca60fbe9 100644 --- a/conjure-core/src/main/java/com/palantir/conjure/parser/types/ConjureType.java +++ b/conjure-core/src/main/java/com/palantir/conjure/parser/types/ConjureType.java @@ -16,6 +16,7 @@ package com.palantir.conjure.parser.types; +import com.fasterxml.jackson.annotation.JsonCreator; import com.palantir.parsec.ParseException; /** @@ -25,6 +26,7 @@ public interface ConjureType { T visit(ConjureTypeVisitor visitor); + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) static ConjureType fromString(String value) throws ParseException { return TypeParser.INSTANCE.parse(value); } diff --git a/conjure/build.gradle b/conjure/build.gradle index eabc95a77..ccfecc3d6 100644 --- a/conjure/build.gradle +++ b/conjure/build.gradle @@ -14,6 +14,10 @@ * limitations under the License. */ +plugins { + id 'com.palantir.graal' version '0.7.2' +} + apply from: "$rootDir/gradle/publish-dist.gradle" mainClassName = 'com.palantir.conjure.cli.ConjureCli' @@ -29,4 +33,29 @@ dependencies { annotationProcessor 'org.immutables:value' compileOnly 'org.immutables:value::annotations' + compileOnly 'org.graalvm.sdk:graal-sdk' +} + + +graal { + mainClass mainClassName + javaVersion '11' + outputName 'conjure' + graalVersion '20.3.0' + // Fail if a native image cannot be created. Otherwise a fallback image which relies + // on a hotspot jdk is required. + option '--no-fallback' + option '--static' + // Missing classes result in runtime failures rather than compile time. Many libraries + // have optional dependencies that may be excluded. Take the log4j-core disruptor + // dependency for example. + option '--allow-incomplete-classpath' + // Allows optional dependencies to fail at runtime, not build time. This feature is + // equivalent to 'allow-incomplete-classpath' for unsupported java features, + // methodHandles for example. + option '--report-unsupported-elements-at-runtime' + // Easier debugging on failure + option '-H:+ReportExceptionStackTraces' + // Discover reflection info at build time + option '--features=com.palantir.conjure.cli.RuntimeReflectionRegistrationFeature' } diff --git a/conjure/src/main/java/com/palantir/conjure/cli/RuntimeReflectionRegistrationFeature.java b/conjure/src/main/java/com/palantir/conjure/cli/RuntimeReflectionRegistrationFeature.java new file mode 100644 index 000000000..e2d81f448 --- /dev/null +++ b/conjure/src/main/java/com/palantir/conjure/cli/RuntimeReflectionRegistrationFeature.java @@ -0,0 +1,197 @@ +/* + * (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.conjure.cli; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +@SuppressWarnings({"CatchAndPrintStackTrace", "JdkObsolete"}) +@Platforms(Platform.HOSTED_ONLY.class) +final class RuntimeReflectionRegistrationFeature implements Feature { + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + // Common standard library classes + registerClassForReflection(String.class); + registerClassForReflection(HashSet.class); + registerClassForReflection(LinkedHashSet.class); + registerClassForReflection(HashMap.class); + registerClassForReflection(LinkedHashMap.class); + registerClassForReflection(ArrayList.class); + registerClassForReflection(LinkedList.class); + registerClassForReflection(ConcurrentHashMap.class); + try { + registerClassForReflection(com.fasterxml.jackson.databind.ext.Java7Handlers.class); + registerClassForReflection(com.fasterxml.jackson.databind.ext.Java7HandlersImpl.class); + } catch (Throwable t) { + t.printStackTrace(); + } + for (Path jarPath : access.getApplicationClassPath()) { + String jarFileName = jarPath.getFileName().toString(); + if (!jarFileName.endsWith(".jar")) { + // Only scan jars + continue; + } + try (JarFile jar = new JarFile(jarPath.toFile())) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.endsWith(".class") + // Exclude multirelease files, the default jvm version class name is sufficient. + && !name.startsWith("META-INF") + // Exclude graal components + && !name.contains("com/oracle/svm") + // Includes incorrect repackaging with malformed signatures. These break the build. + && !name.contains("repackaged") + && !name.contains("shaded") + && !name.contains("glassfish") + && !name.startsWith("javax") + && !name.startsWith("jakarta")) { + String className = name.replace("/", ".").substring(0, name.length() - 6); + try { + maybeRegisterClassForReflection(access.findClassByName(className)); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + } catch (IOException | RuntimeException e) { + e.printStackTrace(); + } + } + } + + private static void maybeRegisterClassForReflection(Class clazz) { + if (clazz != null) { + try { + if (!isAnyElementGraalAnnotated(clazz) && isAnyElementRuntimeAnnotated(clazz)) { + registerClassForReflection(clazz); + } + } catch (NoClassDefFoundError e) { + System.err.printf("NoClassDefFoundError: %s While inspecting %s\n", e.getMessage(), clazz.getName()); + } catch (Throwable t) { + t.printStackTrace(); + // ignored + } + } + } + + private static void registerClassForReflection(Class clazz) { + try { + Method[] declaredMethods = clazz.getDeclaredMethods(); + Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); + Field[] declaredFields = clazz.getDeclaredFields(); + Class[] declaredClasses = clazz.getDeclaredClasses(); + RuntimeReflection.register(clazz); + RuntimeReflection.register(declaredMethods); + RuntimeReflection.register(declaredConstructors); + RuntimeReflection.register(declaredFields); + for (Class declaredClass : declaredClasses) { + registerClassForReflection(declaredClass); + } + } catch (NoClassDefFoundError e) { + System.err.printf("NoClassDefFoundError: %s While registering %s\n", e.getMessage(), clazz.getName()); + } catch (Throwable t) { + t.printStackTrace(); + // ignored + } + } + + private static boolean isAnyElementGraalAnnotated(Class clazz) { + return isAnyElementAnnotated(clazz, Platforms.class::isInstance); + } + + private static boolean isAnyElementRuntimeAnnotated(Class clazz) { + return isAnyElementAnnotated(clazz, RuntimeAnnotationFilter.INSTANCE); + } + + private static boolean isAnyElementAnnotated(Class clazz, Predicate filter) { + try { + return isAnnotated(clazz, filter) + || isAnyAnnotated(clazz.getDeclaredMethods(), filter) + || isAnyAnnotated(clazz.getDeclaredConstructors(), filter) + || isAnyAnnotated(clazz.getDeclaredFields(), filter); + } catch (Throwable t) { + return false; + } + } + + private static boolean isAnyAnnotated(AnnotatedElement[] elements, Predicate filter) { + for (AnnotatedElement element : elements) { + if (isAnnotated(element, filter)) { + return true; + } + } + return false; + } + + private static boolean isAnnotated(AnnotatedElement element, Predicate filter) { + for (Annotation annotation : element.getAnnotations()) { + if (filter.test(annotation)) { + return true; + } + } + return false; + } + + private enum RuntimeAnnotationFilter implements Predicate { + INSTANCE; + + @Override + public boolean test(Annotation annotation) { + // javax nullness. Are these used for validation at runtime? + if (annotation instanceof Nullable + || annotation instanceof ParametersAreNonnullByDefault + || annotation instanceof Nonnull + || annotation instanceof CheckReturnValue) { + // Deprecation annotations are common and provide little signal. + return false; + } + String name = annotation.annotationType().getName(); + // Errorprone annotations are used at compile time, not runtime + return !name.startsWith("com.google.") + && !name.startsWith("org.checkerframework") + // Deprecated, Documented, Target, Retention, FunctionalInterface, + // SafeVarargs, Inherited, Repeatable + && !name.startsWith("java.lang."); + } + } +} diff --git a/versions.lock b/versions.lock index 7db6e1b28..e70824a6a 100644 --- a/versions.lock +++ b/versions.lock @@ -34,6 +34,7 @@ org.glassfish.hk2.external:aopalliance-repackaged:2.5.0-b32 (2 constraints: 0519 org.glassfish.hk2.external:javax.inject:2.5.0-b32 (2 constraints: 451f3a01) org.glassfish.jersey.bundles.repackaged:jersey-guava:2.25.1 (1 constraints: 251120d4) org.glassfish.jersey.core:jersey-common:2.25.1 (1 constraints: 3c05433b) +org.graalvm.sdk:graal-sdk:20.3.0 (1 constraints: 3705363b) org.immutables:value:2.8.8 (1 constraints: 14051536) org.javassist:javassist:3.20.0-GA (1 constraints: 4f0d1a40) org.slf4j:slf4j-api:1.7.30 (3 constraints: 451d0579) diff --git a/versions.props b/versions.props index 8c925e789..e059429a7 100644 --- a/versions.props +++ b/versions.props @@ -17,6 +17,7 @@ org.hamcrest:hamcrest-core = 2.2 org.immutables:value = 2.8.8 org.mockito:mockito-core = 3.6.28 org.slf4j:* = 1.7.30 +org.graalvm.sdk:graal-sdk = 20.3.0 # conflict resolution javax.annotation:javax.annotation-api = 1.3.2