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); + } + } + +}