diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/TypeConfiguration.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/TypeConfiguration.java index 23e3ecbe7068..b90efed5dfdb 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/TypeConfiguration.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/TypeConfiguration.java @@ -143,7 +143,7 @@ public void printJson(JsonWriter writer) throws IOException { @Override public ConfigurationParser createParser() { - return new ReflectionConfigurationParser<>(new ParserConfigurationAdapter(this), true); + return new ReflectionConfigurationParser<>(new ParserConfigurationAdapter(this), true, false); } @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java index 82f3774c9b25..e087ccd0e199 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java @@ -81,10 +81,12 @@ public static final class Options { public static final HostedOptionKey SerializationDenyConfigurationResources = new HostedOptionKey<>( LocatableMultiOptionValue.Strings.buildWithCommaDelimiter()); - @Option(help = "Files describing Java resources to be included in the image.", type = OptionType.User)// + @Option(help = "Files describing Java resources to be included in the image according to the schema at " + + "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/resource-config-schema-v1.0.0.json", type = OptionType.User)// @BundleMember(role = BundleMember.Role.Input)// public static final HostedOptionKey ResourceConfigurationFiles = new HostedOptionKey<>(LocatableMultiOptionValue.Paths.buildWithCommaDelimiter()); - @Option(help = "Resources describing Java resources to be included in the image.", type = OptionType.User)// + @Option(help = "Resources describing Java resources to be included in the image according to the schema at " + + "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/resource-config-schema-v1.0.0.json", type = OptionType.User)// public static final HostedOptionKey ResourceConfigurationResources = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.buildWithCommaDelimiter()); @Option(help = "Files describing program elements to be made accessible via JNI (for syntax, see ReflectionConfigurationFiles)", type = OptionType.User)// @@ -99,15 +101,20 @@ public static final class Options { @Option(help = "Resources describing stubs allowing foreign calls.", type = OptionType.User)// public static final HostedOptionKey ForeignResources = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.buildWithCommaDelimiter()); - @Option(help = "Files describing predefined classes that can be loaded at runtime.", type = OptionType.User)// + @Option(help = "Files describing predefined classes that can be loaded at runtime according to the schema at " + + "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/predefined-classes-config-schema-v1.0.0.json", type = OptionType.User)// @BundleMember(role = BundleMember.Role.Input)// public static final HostedOptionKey PredefinedClassesConfigurationFiles = new HostedOptionKey<>(LocatableMultiOptionValue.Paths.buildWithCommaDelimiter()); - @Option(help = "Resources describing predefined classes that can be loaded at runtime.", type = OptionType.User)// + @Option(help = "Resources describing predefined classes that can be loaded at runtime according to the schema at " + + "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/predefined-classes-config-schema-v1.0.0.json", type = OptionType.User)// public static final HostedOptionKey PredefinedClassesConfigurationResources = new HostedOptionKey<>( LocatableMultiOptionValue.Strings.buildWithCommaDelimiter()); - @Option(help = "Causes unknown attributes in configuration objects to abort the image build instead of emitting a warning.")// + @Option(help = "When configuration files do not match their schema, abort the image build instead of emitting a warning.")// public static final HostedOptionKey StrictConfiguration = new HostedOptionKey<>(false); + + @Option(help = "Warn when reflection and JNI configuration files have elements that could not be found on the classpath or modulepath.", type = OptionType.Expert)// + public static final HostedOptionKey WarnAboutMissingReflectionOrJNIMetadataElements = new HostedOptionKey<>(false); } public static List findConfigurationFiles(String fileName) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java index 94b0f1a8e783..49b77297caf5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java @@ -62,10 +62,10 @@ public static InputStream openStream(URI uri) throws IOException { public static final String CONDITIONAL_KEY = "condition"; public static final String TYPE_REACHABLE_KEY = "typeReachable"; private final Map> seenUnknownAttributesByType = new HashMap<>(); - private final boolean strictConfiguration; + private final boolean strictSchema; protected ConfigurationParser(boolean strictConfiguration) { - this.strictConfiguration = strictConfiguration; + this.strictSchema = strictConfiguration; } public void parseAndRegister(URI uri) throws IOException { @@ -121,14 +121,20 @@ protected void checkAttributes(EconomicMap map, String type, Col if (unknownAttributes.size() > 0) { String message = "Unknown attribute(s) [" + String.join(", ", unknownAttributes) + "] in " + type; - warnOrFail(message); + warnOrFailOnSchemaError(message); Set unknownAttributesForType = seenUnknownAttributesByType.computeIfAbsent(type, key -> new HashSet<>()); unknownAttributesForType.addAll(unknownAttributes); } } - protected void warnOrFail(String message) { - if (strictConfiguration) { + /** + * Used to warn about schema errors in configuration files. Should never be used if the type is + * missing. + * + * @param message message to be displayed. + */ + protected void warnOrFailOnSchemaError(String message) { + if (strictSchema) { throw new JSONParserException(message); } else { LogUtils.warning(message); @@ -182,7 +188,7 @@ protected ConfigurationCondition parseCondition(EconomicMap data if (conditionType instanceof String) { return ConfigurationCondition.create((String) conditionType); } else { - warnOrFail("'" + TYPE_REACHABLE_KEY + "' should be of type string"); + warnOrFailOnSchemaError("'" + TYPE_REACHABLE_KEY + "' should be of type string"); } } return ConfigurationCondition.alwaysTrue(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java index e012aabbef26..166a67838b48 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java @@ -52,13 +52,15 @@ public final class ReflectionConfigurationParser extends ConfigurationParser "allDeclaredClasses", "allRecordComponents", "allPermittedSubclasses", "allNestMembers", "allSigners", "allPublicClasses", "methods", "queriedMethods", "fields", CONDITIONAL_KEY, "queryAllDeclaredConstructors", "queryAllPublicConstructors", "queryAllDeclaredMethods", "queryAllPublicMethods", "unsafeAllocated"); + private final boolean printMissingElements; public ReflectionConfigurationParser(ReflectionConfigurationParserDelegate delegate) { - this(delegate, true); + this(delegate, true, false); } - public ReflectionConfigurationParser(ReflectionConfigurationParserDelegate delegate, boolean strictConfiguration) { + public ReflectionConfigurationParser(ReflectionConfigurationParserDelegate delegate, boolean strictConfiguration, boolean printMissingElements) { super(strictConfiguration); + this.printMissingElements = printMissingElements; this.delegate = delegate; } @@ -91,7 +93,7 @@ private void parseClass(EconomicMap data) { */ TypeResult result = delegate.resolveType(condition, className, true); if (!result.isPresent()) { - handleError("Could not resolve class " + className + " for reflection configuration.", result.getException()); + handleMissingElement("Could not resolve class " + className + " for reflection configuration.", result.getException()); return; } T clazz = result.get(); @@ -199,7 +201,7 @@ private void parseClass(EconomicMap data) { break; } } catch (LinkageError e) { - handleError("Could not register " + delegate.getTypeName(clazz) + ": " + name + " for reflection.", e); + handleMissingElement("Could not register " + delegate.getTypeName(clazz) + ": " + name + " for reflection.", e); } } } @@ -218,9 +220,9 @@ private void parseField(EconomicMap data, T clazz) { try { delegate.registerField(clazz, fieldName, allowWrite); } catch (NoSuchFieldException e) { - handleError("Field " + formatField(clazz, fieldName) + " not found."); + handleMissingElement("Field " + formatField(clazz, fieldName) + " not found."); } catch (LinkageError e) { - handleError("Could not register field " + formatField(clazz, fieldName) + " for reflection.", e); + handleMissingElement("Could not register field " + formatField(clazz, fieldName) + " for reflection.", e); } } @@ -251,9 +253,9 @@ private void parseMethod(boolean queriedOnly, EconomicMap data, delegate.registerMethod(queriedOnly, clazz, methodName, methodParameterTypes); } } catch (NoSuchMethodException e) { - handleError("Method " + formatMethod(clazz, methodName, methodParameterTypes) + " not found."); + handleMissingElement("Method " + formatMethod(clazz, methodName, methodParameterTypes) + " not found."); } catch (LinkageError e) { - handleError("Could not register method " + formatMethod(clazz, methodName, methodParameterTypes) + " for reflection.", e); + handleMissingElement("Could not register method " + formatMethod(clazz, methodName, methodParameterTypes) + " for reflection.", e); } } else { try { @@ -267,7 +269,7 @@ private void parseMethod(boolean queriedOnly, EconomicMap data, throw new JSONParserException("Method " + formatMethod(clazz, methodName) + " not found"); } } catch (LinkageError e) { - handleError("Could not register method " + formatMethod(clazz, methodName) + " for reflection.", e); + handleMissingElement("Could not register method " + formatMethod(clazz, methodName) + " for reflection.", e); } } } @@ -278,7 +280,7 @@ private List parseMethodParameters(T clazz, String methodName, List t String typeName = asString(type, "types"); TypeResult typeResult = delegate.resolveType(ConfigurationCondition.alwaysTrue(), typeName, true); if (!typeResult.isPresent()) { - handleError("Could not register method " + formatMethod(clazz, methodName) + " for reflection.", typeResult.getException()); + handleMissingElement("Could not register method " + formatMethod(clazz, methodName) + " for reflection.", typeResult.getException()); return null; } result.add(typeResult.get()); @@ -303,15 +305,17 @@ private String formatMethod(T clazz, String methodName, List paramTypes) { return delegate.getTypeName(clazz) + '.' + methodName + '(' + parameterTypeNames + ')'; } - private static void handleError(String message) { - handleError(message, null); + private void handleMissingElement(String message) { + handleMissingElement(message, null); } - private static void handleError(String msg, Throwable cause) { - String message = msg; - if (cause != null) { - message += " Reason: " + formatError(cause) + '.'; + private void handleMissingElement(String msg, Throwable cause) { + if (printMissingElements) { + String message = msg; + if (cause != null) { + message += " Reason: " + formatError(cause) + '.'; + } + LogUtils.warning(message); } - LogUtils.warning(message); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ProxyConfigurationFilesHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ProxyConfigurationFilesHelp.txt index 6c2c517abc0a..cf0217313abe 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ProxyConfigurationFilesHelp.txt +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ProxyConfigurationFilesHelp.txt @@ -1,10 +1,26 @@ One or several (comma-separated) paths to JSON files that specify lists of interfaces that define Java proxy classes. -The structure is an array of arrays of fully qualified interface names. +The JSON structure is described in the following schema: -Example: + https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/proxy-config-schema-v1.0.0.json + +An example file contents follows: [ - ["java.lang.AutoCloseable", "java.util.Comparator"], - ["java.util.Comparator"], - ["java.util.List"] + { + "condition" : { + "typeReachable" : "org.example.CallingClass" + }, + "interfaces" : [ + "java.lang.AutoCloseable", + "java.util.Comparator" + ] + }, + { + "condition" : { + "typeReachable" : "org.example.CallingClass" + }, + "interfaces" : [ + "java.util.Comparator" + ] + } ] diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ReflectionConfigurationFilesHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ReflectionConfigurationFilesHelp.txt index 41f3e0ffc505..aaf1027fdb4b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ReflectionConfigurationFilesHelp.txt +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/ReflectionConfigurationFilesHelp.txt @@ -1,27 +1,15 @@ One or several (comma-separated) paths to JSON files that specify which program elements should be made available via reflection. -The JSON object schema is: +The JSON object schema is described at: - { - String name; // fully qualified class name - boolean allDeclaredConstructors; // include all declared constructors, see Class.getDeclaredConstructors() - boolean allPublicConstructors; // include all public constructors, see Class.getConstructors() - boolean allDeclaredMethods; // include all declared methods, see Class.getDeclaredMethods() - boolean allPublicMethods; // include all public methods, see Class.getMethods() - boolean allDeclaredFields; // include all declared fields, see Class.getDeclaredFields() - boolean allPublicFields; // include all public fields, see Class.getFields() - { - String name; // method name - String[] parameterTypes; // parameter types (optional, use if ambiguous) - }[] methods; - { - String name; // field name - }[] fields; - }[]; + https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/reflect-config-schema-v1.0.0.json Example: [ { + "condition" : { + "typeReachable" : "org.example.CallingClass" + }, "name" : "java.lang.Class", "allDeclaredConstructors" : "true", "allPublicConstructors" : "true", @@ -29,6 +17,9 @@ Example: "allPublicMethods" : "true" }, { + "condition" : { + "typeReachable" : "org.example.CallingClass" + }, "name" : "java.lang.String", "fields" : [ { "name" : "value" }, @@ -42,6 +33,9 @@ Example: ] }, { + "condition" : { + "typeReachable" : "org.example.CallingClass" + }, "name" : "java.lang.String$CaseInsensitiveComparator", "methods" : [ { "name" : "compare" } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/SerializationConfigurationFilesHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/SerializationConfigurationFilesHelp.txt index cbcccd03d9ca..e199d657abc5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/SerializationConfigurationFilesHelp.txt +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/doc-files/SerializationConfigurationFilesHelp.txt @@ -1,23 +1,31 @@ One or several (comma-separated) paths to JSON files that specify lists of serialization configurations. -The structure is an array of elements specifying the target serialization/deserialization class. +The structure is described in the following schema: + + https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/serialization-config-schema-v1.0.0.json Example: [ { - "condition":{"typeReachable":"app.DataSerializer"}, - "name":"java.util.ArrayList" + "condition" : { + "typeReachable" : "app.DataSerializer" + }, + "name" : "java.util.ArrayList" } ] For deserializing lambda classes, the capturing class of the lambda needs to be specified in a separate section of the configuration file, for example: [ - "types": [ - {"name":"java.lang.Object"} + "types" : [ + { + "name" : "java.lang.Object" + } ], - "lambdaCapturingTypes": [ - {"name":"java.util.Comparator"} + "lambdaCapturingTypes" : [ + { + "name" : "java.util.Comparator" + } ] ] @@ -35,8 +43,10 @@ serialization-config.json: [ { - "condition":{"typeReachable":"org.apache.spark.SparkContext"}, - "name":"org.apache.spark.SparkContext$$anonfun$hadoopFile$1", - "customTargetConstructorClass":"java.lang.Object" + "condition" : { + "typeReachable" : "org.apache.spark.SparkContext" + }, + "name" : "org.apache.spark.SparkContext$$anonfun$hadoopFile$1", + "customTargetConstructorClass" : "java.lang.Object" } ] diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java index 55e89f7fda2c..fde6041471ee 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java @@ -24,8 +24,6 @@ */ package com.oracle.svm.hosted.config; -import static com.oracle.svm.common.option.CommonOptions.PrintFlags; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -49,7 +47,6 @@ import com.oracle.svm.core.configure.ReflectionConfigurationParser; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.option.LocatableMultiOptionValue; -import com.oracle.svm.core.option.SubstrateOptionsParser; import com.oracle.svm.core.util.UserError; import com.oracle.svm.hosted.ImageClassLoader; @@ -57,7 +54,8 @@ public final class ConfigurationParserUtils { public static ReflectionConfigurationParser>> create(ReflectionRegistry registry, ImageClassLoader imageClassLoader) { return new ReflectionConfigurationParser<>(RegistryAdapter.create(registry, imageClassLoader), - ConfigurationFiles.Options.StrictConfiguration.getValue()); + ConfigurationFiles.Options.StrictConfiguration.getValue(), + ConfigurationFiles.Options.WarnAboutMissingReflectionOrJNIMetadataElements.getValue()); } /** @@ -74,12 +72,11 @@ public static int parseAndRegisterConfigurations(ConfigurationParser parser, Ima List paths = configFilesOption.getValue().values(); List resourceValues = configResourcesOption.getValue().values(); - var configFilesOptionName = configFilesOption.getName(); - return parseAndRegisterConfigurations(parser, classLoader, featureName, configFilesOptionName, configResourcesOption.getName(), directoryFileName, paths, resourceValues); + return parseAndRegisterConfigurations(parser, classLoader, featureName, directoryFileName, paths, resourceValues); } public static int parseAndRegisterConfigurations(ConfigurationParser parser, ImageClassLoader classLoader, - String featureName, String configFilesOptionName, String configResourcesOptionName, + String featureName, String directoryFileName, List paths, List resourceValues) { int parsedCount = 0; @@ -89,7 +86,7 @@ public static int parseAndRegisterConfigurations(ConfigurationParser parser, Ima if (!Files.exists(path)) { throw UserError.abort("The %s configuration file \"%s\" does not exist.", featureName, path); } - doParseAndRegister(parser, featureName, path, configFilesOptionName); + doParseAndRegister(parser, featureName, path); return 1; }).sum(); @@ -116,13 +113,13 @@ public boolean tryAdvance(Consumer action) { }); Stream resources = Stream.concat(configResourcesFromOption, ConfigurationFiles.findConfigurationResources(directoryFileName, classLoader.getClassLoader()).stream()); parsedCount += resources.mapToInt(url -> { - doParseAndRegister(parser, featureName, url, configResourcesOptionName); + doParseAndRegister(parser, featureName, url); return 1; }).sum(); return parsedCount; } - private static void doParseAndRegister(ConfigurationParser parser, String featureName, Object location, String optionName) { + private static void doParseAndRegister(ConfigurationParser parser, String featureName, Object location) { try { URI uri; if (location instanceof Path) { @@ -136,8 +133,10 @@ private static void doParseAndRegister(ConfigurationParser parser, String featur if (errorMessage == null || errorMessage.isEmpty()) { errorMessage = e.toString(); } - throw UserError.abort("Error parsing %s configuration in %s:%n%s%nVerify that the configuration matches the schema described in the %s output for option %s.", - featureName, location, errorMessage, SubstrateOptionsParser.commandArgument(PrintFlags, "+"), optionName); + throw UserError.abort( + "Error parsing %s configuration in %s:%n%s%nVerify that the configuration matches the corresponding schema at " + + "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/", + featureName, location, errorMessage); } } }