diff --git a/docs/reference-manual/native-image/BuildOutput.md b/docs/reference-manual/native-image/BuildOutput.md
index dffb2bf6e16d..e93037baa458 100644
--- a/docs/reference-manual/native-image/BuildOutput.md
+++ b/docs/reference-manual/native-image/BuildOutput.md
@@ -218,6 +218,9 @@ Therefore, this can also include `byte[]` objects from application code.
##### Embedded Resources Stored in `byte[]`
The total size of all `byte[]` objects used for storing resources (for example, files accessed via `Class.getResource()`) within the native binary.
The number of resources is shown in the [Heap](#glossary-image-heap) section.
+A list of all resources including additional information such as their module, name, origin, and size are included in the [build reports](BuildOptions.md#build-output-and-build-report).
+This information can also be requested in the JSON format using the `-H:+GenerateEmbeddedResourcesFile` option.
+Such a JSON file validates against the JSON schema defined in [`embedded-resources-schema-v1.0.0.json`](https://github.com/oracle/graal/tree/master/docs/reference-manual/native-image/assets/embedded-resources-schema-v1.0.0.json).
##### Code Metadata Stored in `byte[]`
The total size of all `byte[]` objects used for metadata for the [code area](#glossary-code-area).
diff --git a/docs/reference-manual/native-image/assets/embedded-resources-schema-v1.0.0.json b/docs/reference-manual/native-image/assets/embedded-resources-schema-v1.0.0.json
new file mode 100644
index 000000000000..3d508d07ff77
--- /dev/null
+++ b/docs/reference-manual/native-image/assets/embedded-resources-schema-v1.0.0.json
@@ -0,0 +1,56 @@
+{
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/embedded-resources-schema-v1.0.0.json",
+ "default": [],
+ "items": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name of the resource that was registered"
+ },
+ "module": {
+ "type": "string",
+ "title": "Module of the resource that was registered"
+ },
+ "is_directory": {
+ "type": "boolean",
+ "default": false,
+ "title": "Describes whether the registered resource is a directory or not"
+ },
+ "is_missing": {
+ "type": "boolean",
+ "default": false,
+ "title": "Describes whether the resource is missing on the system or not"
+ },
+ "entries": {
+ "default": [],
+ "items": {
+ "properties": {
+ "origin": {
+ "type": "string",
+ "title": "Resource path"
+ },
+ "size": {
+ "type": "integer",
+ "title": "Size of the resource expressed in bytes"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "title": "Source of the resource defined with name and module properties"
+ },
+ "type": "array",
+ "title": "List of sources for the resource defined with name and module properties"
+ }
+ },
+ "required": [
+ "name",
+ "entries"
+ ],
+ "additionalProperties": false,
+ "type": "object",
+ "title": "Resource that was registered"
+ },
+ "type": "array",
+ "title": "JSON schema for the embedded-resources.json that shows all resources that were registered."
+}
\ No newline at end of file
diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md
index 8b3c5b39c908..e9c595f4dea1 100644
--- a/substratevm/CHANGELOG.md
+++ b/substratevm/CHANGELOG.md
@@ -16,6 +16,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-47832) Experimental support for upcalls from foreign code and other improvements to our implementation of the [Foreign Function & Memory API](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ForeignInterface.md) (part of "Project Panama", [JEP 454](https://openjdk.org/jeps/454)) on AMD64. Must be enabled with `-H:+ForeignAPISupport` (requiring `-H:+UnlockExperimentalVMOptions`).
* (GR-52314) `-XX:MissingRegistrationReportingMode` can now be used on program invocation instead of as a build option, to avoid a rebuild when debugging missing registration errors.
* (GR-51086) Introduce a new `--static-nolibc` API option as a replacement for the experimental `-H:±StaticExecutableWithDynamicLibC` option.
+* (GR-52578) Print information about embedded resources into `embedded-resources.json` using the `-H:+GenerateEmbeddedResourcesFile` option.
## GraalVM for JDK 22 (Internal Version 24.0.0)
* (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics.
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java
index 949f1a7660ab..c96b02f132d6 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java
@@ -43,7 +43,7 @@
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.MapCursor;
-import org.graalvm.collections.Pair;
+import org.graalvm.nativeimage.ImageInfo;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
@@ -81,18 +81,21 @@ public static Resources singleton() {
}
/**
- * The hosted map used to collect registered resources. Using a {@link Pair} of (module,
- * resourceName) provides implementations for {@code hashCode()} and {@code equals()} needed for
- * the map keys. Hosted module instances differ to runtime instances, so the map that ends up in
- * the image heap is computed after the runtime module instances have been computed {see
- * com.oracle.svm.hosted.ModuleLayerFeature}.
+ * The hosted map used to collect registered resources. Using a {@link ModuleResourceKey} of
+ * (module, resourceName) provides implementations for {@code hashCode()} and {@code equals()}
+ * needed for the map keys. Hosted module instances differ to runtime instances, so the map that
+ * ends up in the image heap is computed after the runtime module instances have been computed
+ * {see com.oracle.svm.hosted.ModuleLayerFeature}.
*/
- private final EconomicMap, ResourceStorageEntryBase> resources = ImageHeapMap.create();
+ private final EconomicMap resources = ImageHeapMap.create();
private final EconomicMap requestedPatterns = ImageHeapMap.create();
public record RequestedPattern(String module, String resource) {
}
+ public record ModuleResourceKey(Module module, String resource) {
+ }
+
/**
* The object used to mark a resource as reachable according to the metadata. It can be obtained
* when accessing the {@link Resources#resources} map, and it means that even though the
@@ -118,7 +121,7 @@ public record RequestedPattern(String module, String resource) {
Resources() {
}
- public EconomicMap, ResourceStorageEntryBase> getResourceStorage() {
+ public EconomicMap getResourceStorage() {
return resources;
}
@@ -138,14 +141,19 @@ public static String moduleName(Module module) {
return module == null ? null : module.getName();
}
- private static Pair createStorageKey(Module module, String resourceName) {
+ public static ModuleResourceKey createStorageKey(Module module, String resourceName) {
Module m = module != null && module.isNamed() ? module : null;
- return Pair.create(m, resourceName);
+ if (ImageInfo.inImageBuildtimeCode()) {
+ if (m != null) {
+ m = RuntimeModuleSupport.instance().getRuntimeModuleForHostedModule(m);
+ }
+ }
+ return new ModuleResourceKey(m, resourceName);
}
public static Set getIncludedResourcesModules() {
return StreamSupport.stream(singleton().resources.getKeys().spliterator(), false)
- .map(Pair::getLeft)
+ .map(ModuleResourceKey::module)
.filter(Objects::nonNull)
.map(Module::getName)
.collect(Collectors.toSet());
@@ -169,11 +177,8 @@ private void updateTimeStamp() {
private void addEntry(Module module, String resourceName, boolean isDirectory, byte[] data, boolean fromJar, boolean isNegativeQuery) {
VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished(), "Trying to add a resource entry after analysis.");
Module m = module != null && module.isNamed() ? module : null;
- if (m != null) {
- m = RuntimeModuleSupport.instance().getRuntimeModuleForHostedModule(m);
- }
synchronized (resources) {
- Pair key = createStorageKey(m, resourceName);
+ ModuleResourceKey key = createStorageKey(m, resourceName);
ResourceStorageEntryBase entry = resources.get(key);
if (isNegativeQuery) {
if (entry == null) {
@@ -187,7 +192,7 @@ private void addEntry(Module module, String resourceName, boolean isDirectory, b
entry = new ResourceStorageEntry(isDirectory, fromJar);
resources.put(key, entry);
} else {
- if (key.getLeft() != null) {
+ if (key.module() != null) {
// if the entry already exists, and it comes from a module, it is the same entry
// that we registered at some point before
return;
@@ -232,7 +237,7 @@ public void registerIOException(Module module, String resourceName, IOException
LogUtils.warning("Resource " + resourceName + " from module " + moduleName(module) + " produced the following IOException: " + e.getClass().getTypeName() + ": " + e.getMessage());
}
}
- Pair key = createStorageKey(module, resourceName);
+ ModuleResourceKey key = createStorageKey(module, resourceName);
synchronized (resources) {
updateTimeStamp();
resources.put(key, new ResourceExceptionEntry(e));
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java
index 7e8d6bceca69..8321db7613cd 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -85,7 +85,6 @@
import java.util.regex.Pattern;
import org.graalvm.collections.MapCursor;
-import org.graalvm.collections.Pair;
import com.oracle.svm.core.MissingRegistrationUtils;
import com.oracle.svm.core.jdk.Resources;
@@ -657,9 +656,9 @@ private void update(Entry e) {
}
private void readAllEntries() {
- MapCursor, ResourceStorageEntryBase> entries = Resources.singleton().getResourceStorage().getEntries();
+ MapCursor entries = Resources.singleton().getResourceStorage().getEntries();
while (entries.advance()) {
- byte[] name = getBytes(entries.getKey().getRight());
+ byte[] name = getBytes(entries.getKey().resource());
ResourceStorageEntryBase entry = entries.getValue();
if (entry.hasData()) {
IndexNode newIndexNode = new IndexNode(name, entry.isDirectory(), true);
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/EmbeddedResourceExporter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/EmbeddedResourceExporter.java
new file mode 100644
index 000000000000..73fc8ac37d29
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/EmbeddedResourceExporter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.hosted;
+
+import static com.oracle.svm.core.jdk.Resources.NEGATIVE_QUERY_MARKER;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.graalvm.collections.EconomicMap;
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+
+import com.oracle.svm.core.jdk.Resources;
+import com.oracle.svm.core.jdk.resources.ResourceStorageEntryBase;
+import com.oracle.svm.core.util.VMError;
+import com.oracle.svm.core.util.json.JsonPrinter;
+import com.oracle.svm.core.util.json.JsonWriter;
+import com.oracle.svm.util.LogUtils;
+
+@Platforms(Platform.HOSTED_ONLY.class)
+public class EmbeddedResourceExporter {
+
+ public record SourceSizePair(String source, int size) {
+ }
+
+ public record ResourceReportEntry(Module module, String resourceName, List entries, boolean isDirectory, boolean isMissing) {
+ }
+
+ public static void printReport(JsonWriter writer) throws IOException {
+ JsonPrinter.printCollection(writer,
+ getResourceReportEntryList(EmbeddedResourcesInfo.singleton().getRegisteredResources()),
+ Comparator.comparing(EmbeddedResourceExporter.ResourceReportEntry::resourceName),
+ EmbeddedResourceExporter::resourceReportElement);
+ }
+
+ private static void resourceReportElement(ResourceReportEntry p, JsonWriter w) throws IOException {
+ w.indent().newline();
+ w.appendObjectStart().newline();
+ w.appendKeyValue("name", p.resourceName()).appendSeparator();
+ w.newline();
+ if (p.module() != null) {
+ w.appendKeyValue("module", p.module().getName()).appendSeparator();
+ w.newline();
+ }
+
+ if (p.isDirectory()) {
+ w.appendKeyValue("is_directory", true).appendSeparator();
+ w.newline();
+ }
+
+ if (p.isMissing()) {
+ w.appendKeyValue("is_missing", true).appendSeparator();
+ w.newline();
+ }
+
+ w.quote("entries").append(":");
+ JsonPrinter.printCollection(w, p.entries(), Comparator.comparing(SourceSizePair::source), EmbeddedResourceExporter::sourceElement);
+ w.unindent().newline().appendObjectEnd();
+ }
+
+ private static void sourceElement(SourceSizePair p, JsonWriter w) throws IOException {
+ w.indent().newline();
+ w.appendObjectStart().newline();
+ w.appendKeyValue("origin", p.source()).appendSeparator();
+ w.newline();
+ w.appendKeyValue("size", p.size());
+ w.newline().appendObjectEnd();
+ w.unindent();
+ }
+
+ private static List getResourceReportEntryList(ConcurrentHashMap> collection) {
+ if (collection.isEmpty()) {
+ LogUtils.warning("Attempting to write information about resources without data being collected. " +
+ "Either the GenerateEmbeddedResourcesFile hosted option is disabled " +
+ "or the application doesn't have any resource registered");
+
+ return Collections.emptyList();
+ }
+
+ List resourceInfoList = new ArrayList<>();
+ EconomicMap resourceStorage = Resources.singleton().getResourceStorage();
+ resourceStorage.getKeys().forEach(key -> {
+ Module module = key.module();
+ String resourceName = key.resource();
+
+ ResourceStorageEntryBase storageEntry = resourceStorage.get(key);
+ List registeredEntrySources = collection.get(key);
+
+ if (registeredEntrySources == null && storageEntry != NEGATIVE_QUERY_MARKER) {
+ throw VMError.shouldNotReachHere("Resource: " + resourceName +
+ " from module: " + module +
+ " wasn't register from ResourcesFeature. It should never happen except for NEGATIVE_QUERIES in some cases");
+ }
+
+ if (storageEntry == NEGATIVE_QUERY_MARKER) {
+ resourceInfoList.add(new ResourceReportEntry(module, resourceName, new ArrayList<>(), false, true));
+ return;
+ }
+
+ List sources = new ArrayList<>();
+ for (int i = 0; i < registeredEntrySources.size(); i++) {
+ String source = registeredEntrySources.get(i);
+ int size = storageEntry.getData().get(i).length;
+ sources.add(new SourceSizePair(source, size));
+ }
+
+ boolean isDirectory = storageEntry.isDirectory();
+ resourceInfoList.add(new ResourceReportEntry(module, resourceName, sources, isDirectory, false));
+ });
+
+ return resourceInfoList;
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/EmbeddedResourcesInfo.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/EmbeddedResourcesInfo.java
new file mode 100644
index 000000000000..3e2078fafc0c
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/EmbeddedResourcesInfo.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.hosted;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.graalvm.nativeimage.ImageSingletons;
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+
+import com.oracle.svm.core.jdk.Resources;
+
+@Platforms(Platform.HOSTED_ONLY.class)
+public class EmbeddedResourcesInfo {
+
+ private final ConcurrentHashMap> registeredResources = new ConcurrentHashMap<>();
+
+ public ConcurrentHashMap> getRegisteredResources() {
+ return registeredResources;
+ }
+
+ public static EmbeddedResourcesInfo singleton() {
+ return ImageSingletons.lookup(EmbeddedResourcesInfo.class);
+ }
+
+ public void declareResourceAsRegistered(Module module, String resource, String source) {
+ if (!ResourcesFeature.Options.GenerateEmbeddedResourcesFile.getValue()) {
+ return;
+ }
+
+ Resources.ModuleResourceKey key = Resources.createStorageKey(module, resource);
+ registeredResources.compute(key, (k, v) -> {
+ if (v == null) {
+ ArrayList newValue = new ArrayList<>();
+ newValue.add(source);
+ return newValue;
+ }
+
+ /*
+ * We have to avoid duplicated sources here. In case when declaring resource that comes
+ * from module as registered, we don't have information whether the resource is already
+ * registered or not. That check is performed later in {@link Resources.java#addEntry},
+ * so we have to perform same check here, to avoid duplicates when collecting
+ * information about resource.
+ */
+ if (!v.contains(source)) {
+ v.add(source);
+ }
+ return v;
+ });
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java
index f8642070a2db..693eb3cb4e32 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -31,12 +31,10 @@
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.net.JarURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -48,24 +46,22 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
import java.util.Set;
-import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.RuntimeResourceAccess;
import org.graalvm.nativeimage.impl.ConfigurationCondition;
import org.graalvm.nativeimage.impl.RuntimeResourceSupport;
+import com.oracle.svm.core.BuildArtifacts;
import com.oracle.svm.core.ClassLoaderSupport;
import com.oracle.svm.core.ClassLoaderSupport.ResourceCollector;
import com.oracle.svm.core.MissingRegistrationUtils;
@@ -84,15 +80,18 @@
import com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystem;
import com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystemProvider;
import com.oracle.svm.core.option.HostedOptionKey;
+import com.oracle.svm.core.option.HostedOptionValues;
import com.oracle.svm.core.option.LocatableMultiOptionValue;
import com.oracle.svm.core.option.OptionMigrationMessage;
import com.oracle.svm.core.util.UserError;
import com.oracle.svm.core.util.VMError;
+import com.oracle.svm.core.util.json.JsonWriter;
import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport;
import com.oracle.svm.hosted.config.ConfigurationParserUtils;
import com.oracle.svm.hosted.jdk.localization.LocalizationFeature;
import com.oracle.svm.hosted.reflect.NativeImageConditionResolver;
import com.oracle.svm.hosted.snippets.SubstrateGraphBuilderPlugins;
+import com.oracle.svm.hosted.util.ResourcesUtils;
import com.oracle.svm.util.LogUtils;
import com.oracle.svm.util.ModuleSupport;
import com.oracle.svm.util.ReflectionUtil;
@@ -147,6 +146,11 @@ public static class Options {
@Option(help = "Regexp to match names of resources to be excluded from the image.", type = OptionType.User)//
public static final HostedOptionKey ExcludeResources = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build());
+
+ private static final String EMBEDDED_RESOURCES_FILE_NAME = "embedded-resources.json";
+ @Option(help = "Create a " + EMBEDDED_RESOURCES_FILE_NAME + " file in the build directory. The output conforms to the JSON schema located at: " +
+ "docs/reference-manual/native-image/assets/embedded-resources-schema-v1.0.0.json", type = OptionType.User)//
+ public static final HostedOptionKey GenerateEmbeddedResourcesFile = new HostedOptionKey<>(false);
}
private boolean sealed = false;
@@ -159,6 +163,7 @@ private record CompiledConditionalPattern(ConfigurationCondition condition, Reso
private Set resourcePatternWorkSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set excludedResourcePatterns = Collections.newSetFromMap(new ConcurrentHashMap<>());
+
private int loadedConfigurations;
private ImageClassLoader imageClassLoader;
@@ -193,6 +198,7 @@ public void addResource(Module module, String resourcePath) {
@Override
public void injectResource(Module module, String resourcePath, byte[] resourceContent) {
+ EmbeddedResourcesInfo.singleton().declareResourceAsRegistered(module, resourcePath, "INJECTED");
Resources.singleton().registerResource(module, resourcePath, resourceContent);
}
@@ -268,12 +274,18 @@ private void processResourceFromModule(Module module, String resourcePath) {
boolean isDirectory = Files.isDirectory(Path.of(resourcePath));
if (isDirectory) {
- String content = getDirectoryContent(resourcePath, false);
+ String content = ResourcesUtils.getDirectoryContent(resourcePath, false);
Resources.singleton().registerDirectoryResource(module, resourcePath, content, false);
} else {
InputStream is = module.getResourceAsStream(resourcePath);
registerResource(module, resourcePath, false, is);
}
+
+ var resolvedModule = module.getLayer().configuration().findModule(module.getName());
+ if (resolvedModule.isPresent()) {
+ Optional location = resolvedModule.get().reference().location();
+ location.ifPresent(uri -> EmbeddedResourcesInfo.singleton().declareResourceAsRegistered(module, resourcePath, uri.toString()));
+ }
} catch (IOException e) {
Resources.singleton().registerIOException(module, resourcePath, e, LinkAtBuildTimeSupport.singleton().packageOrClassAtBuildTime(resourcePath));
}
@@ -303,14 +315,17 @@ private void processResourceFromClasspath(String resourcePath) {
alreadyProcessedResources.add(url.toString());
try {
boolean fromJar = url.getProtocol().equalsIgnoreCase("jar");
- boolean isDirectory = resourceIsDirectory(url, fromJar, resourcePath);
+ boolean isDirectory = ResourcesUtils.resourceIsDirectory(url, fromJar, resourcePath);
if (isDirectory) {
- String content = getDirectoryContent(fromJar ? url.toString() : Paths.get(url.toURI()).toString(), fromJar);
+ String content = ResourcesUtils.getDirectoryContent(fromJar ? url.toString() : Paths.get(url.toURI()).toString(), fromJar);
Resources.singleton().registerDirectoryResource(null, resourcePath, content, fromJar);
} else {
InputStream is = url.openStream();
registerResource(null, resourcePath, fromJar, is);
}
+
+ String source = ResourcesUtils.getResourceSource(url, resourcePath, fromJar);
+ EmbeddedResourcesInfo.singleton().declareResourceAsRegistered(null, resourcePath, source);
} catch (IOException e) {
Resources.singleton().registerIOException(null, resourcePath, e, LinkAtBuildTimeSupport.singleton().packageOrClassAtBuildTime(resourcePath));
return;
@@ -322,6 +337,7 @@ private void processResourceFromClasspath(String resourcePath) {
private void registerResource(Module module, String resourcePath, boolean fromJar, InputStream is) {
if (is == null) {
+ Resources.singleton().registerNegativeQuery(module, resourcePath);
return;
}
@@ -333,74 +349,6 @@ private void registerResource(Module module, String resourcePath, boolean fromJa
throw new RuntimeException(e);
}
}
-
- /* Util functions for resource attributes calculations */
- private String urlToJarPath(URL url) {
- try {
- return ((JarURLConnection) url.openConnection()).getJarFileURL().toURI().getPath();
- } catch (IOException | URISyntaxException e) {
- throw new RuntimeException(e);
- }
- }
-
- private boolean resourceIsDirectory(URL url, boolean fromJar, String resourcePath) throws IOException, URISyntaxException {
- if (fromJar) {
- try (JarFile jf = new JarFile(urlToJarPath(url))) {
- return jf.getEntry(resourcePath).isDirectory();
- }
- } else {
- return Files.isDirectory(Path.of(url.toURI()));
- }
- }
-
- private String getDirectoryContent(String path, boolean fromJar) throws IOException {
- Set content = new TreeSet<>();
- if (fromJar) {
- try (JarFile jf = new JarFile(urlToJarPath(URI.create(path).toURL()))) {
- String pathSeparator = FileSystems.getDefault().getSeparator();
- String directoryPath = path.split("!")[1];
-
- // we are removing leading slash because jar entry names don't start with slash
- if (directoryPath.startsWith(pathSeparator)) {
- directoryPath = directoryPath.substring(1);
- }
-
- Enumeration entries = jf.entries();
- while (entries.hasMoreElements()) {
- String entry = entries.nextElement().getName();
- if (entry.startsWith(directoryPath)) {
- String contentEntry = entry.substring(directoryPath.length());
-
- // remove the leading slash
- if (contentEntry.startsWith(pathSeparator)) {
- contentEntry = contentEntry.substring(1);
- }
-
- // prevent adding empty strings as a content
- if (!contentEntry.isEmpty()) {
- // get top level content only
- int firstSlash = contentEntry.indexOf(pathSeparator);
- if (firstSlash != -1) {
- content.add(contentEntry.substring(0, firstSlash));
- } else {
- content.add(contentEntry);
- }
- }
- }
- }
-
- }
- } else {
- try (Stream contentStream = Files.list(Path.of(path))) {
- content = new TreeSet<>(contentStream
- .map(Path::getFileName)
- .map(Path::toString)
- .toList());
- }
- }
-
- return String.join(System.lineSeparator(), content);
- }
}
@Override
@@ -410,6 +358,8 @@ public void afterRegistration(AfterRegistrationAccess a) {
ResourcesRegistryImpl resourcesRegistry = new ResourcesRegistryImpl();
ImageSingletons.add(ResourcesRegistry.class, resourcesRegistry);
ImageSingletons.add(RuntimeResourceSupport.class, resourcesRegistry);
+ EmbeddedResourcesInfo embeddedResourcesInfo = new EmbeddedResourcesInfo();
+ ImageSingletons.add(EmbeddedResourcesInfo.class, embeddedResourcesInfo);
}
private static ResourcesRegistryImpl resourceRegistryImpl() {
@@ -442,7 +392,7 @@ public void beforeAnalysis(BeforeAnalysisAccess access) {
includePatterns.stream()
.map(pattern -> pattern.compiledPattern)
.forEach(resourcePattern -> {
- Resources.singleton().registerIncludePattern(resourcePattern.moduleName, resourcePattern.pattern.pattern());
+ Resources.singleton().registerIncludePattern(resourcePattern.moduleName(), resourcePattern.pattern.pattern());
});
}
ResourcePattern[] excludePatterns = compilePatterns(excludedResourcePatterns);
@@ -556,6 +506,7 @@ public void registerIOException(Module module, String resourceName, IOException
@Override
public void registerNegativeQuery(Module module, String resourceName) {
+ EmbeddedResourcesInfo.singleton().declareResourceAsRegistered(module, resourceName, "");
Resources.singleton().registerNegativeQuery(module, resourceName);
}
}
@@ -596,6 +547,16 @@ boolean moduleNameMatches(String resourceContainerModuleName) {
@Override
public void afterAnalysis(AfterAnalysisAccess access) {
sealed = true;
+ if (Options.GenerateEmbeddedResourcesFile.getValue()) {
+ Path reportLocation = NativeImageGenerator.generatedFiles(HostedOptionValues.singleton()).resolve(Options.EMBEDDED_RESOURCES_FILE_NAME);
+ try (JsonWriter writer = new JsonWriter(reportLocation)) {
+ EmbeddedResourceExporter.printReport(writer);
+ } catch (IOException e) {
+ throw VMError.shouldNotReachHere("Json writer cannot write to: " + reportLocation, e);
+ }
+
+ BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.BUILD_INFO, reportLocation);
+ }
}
@Override
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/util/ResourcesUtils.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/util/ResourcesUtils.java
new file mode 100644
index 000000000000..efa633d00ace
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/util/ResourcesUtils.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.hosted.util;
+
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
+
+import com.oracle.svm.core.util.VMError;
+
+public class ResourcesUtils {
+
+ /**
+ * Returns jar path from the given url.
+ */
+ private static String urlToJarPath(URL url) {
+ return urlToJarUri(url).getPath();
+ }
+
+ /**
+ * Returns directory that contains resource on the given url.
+ */
+ public static String getResourceSource(URL url, String resource, boolean fromJar) {
+ try {
+ String source = fromJar ? urlToJarUri(url).toString() : url.toURI().toString();
+
+ if (!fromJar) {
+ // -1 removes trailing slash from path of directory that contains resource
+ source = source.substring(0, source.length() - resource.length() - 1);
+ if (source.endsWith("/")) {
+ // if resource was directory we still have one slash at the end
+ source = source.substring(0, source.length() - 1);
+ }
+ }
+
+ return source;
+ } catch (URISyntaxException e) {
+ throw VMError.shouldNotReachHere("Cannot get uri from: " + url, e);
+ }
+ }
+
+ /**
+ * Returns whether the given resource is directory or not.
+ */
+ public static boolean resourceIsDirectory(URL url, boolean fromJar, String resource) throws IOException, URISyntaxException {
+ if (fromJar) {
+ try (JarFile jf = new JarFile(urlToJarPath(url))) {
+ return jf.getEntry(resource).isDirectory();
+ }
+ } else {
+ return Files.isDirectory(Path.of(url.toURI()));
+ }
+ }
+
+ /**
+ * Returns directory content of the resource from the given path.
+ */
+ public static String getDirectoryContent(String path, boolean fromJar) throws IOException {
+ Set content = new TreeSet<>();
+ if (fromJar) {
+ try (JarFile jf = new JarFile(urlToJarPath(URI.create(path).toURL()))) {
+ String pathSeparator = FileSystems.getDefault().getSeparator();
+ String directoryPath = path.split("!")[1];
+
+ // we are removing leading slash because jar entry names don't start with slash
+ if (directoryPath.startsWith(pathSeparator)) {
+ directoryPath = directoryPath.substring(1);
+ }
+
+ Enumeration entries = jf.entries();
+ while (entries.hasMoreElements()) {
+ String entry = entries.nextElement().getName();
+ if (entry.startsWith(directoryPath)) {
+ String contentEntry = entry.substring(directoryPath.length());
+
+ // remove the leading slash
+ if (contentEntry.startsWith(pathSeparator)) {
+ contentEntry = contentEntry.substring(1);
+ }
+
+ // prevent adding empty strings as a content
+ if (!contentEntry.isEmpty()) {
+ // get top level content only
+ int firstSlash = contentEntry.indexOf(pathSeparator);
+ if (firstSlash != -1) {
+ content.add(contentEntry.substring(0, firstSlash));
+ } else {
+ content.add(contentEntry);
+ }
+ }
+ }
+ }
+
+ }
+ } else {
+ try (Stream contentStream = Files.list(Path.of(path))) {
+ content = new TreeSet<>(contentStream
+ .map(Path::getFileName)
+ .map(Path::toString)
+ .toList());
+ }
+ }
+
+ return String.join(System.lineSeparator(), content);
+ }
+
+ private static URI urlToJarUri(URL url) {
+ try {
+ return ((JarURLConnection) url.openConnection()).getJarFileURL().toURI();
+ } catch (IOException | URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}