diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java
index c06c62488db..24dd8ea3121 100644
--- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java
+++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java
@@ -37,8 +37,12 @@
import java.nio.file.StandardOpenOption;
import java.security.AccessController;
import java.security.PrivilegedAction;
+import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.Random;
import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
import jdk.internal.jimage.decompressor.Decompressor;
/**
@@ -326,6 +330,56 @@ public String[] getEntryNames() {
.toArray(String[]::new);
}
+ /**
+ * Returns the "raw" API for accessing underlying jimage resource entries.
+ *
+ *
This is only meaningful for use by code dealing directly with jimage
+ * files, and cannot be used to reliably lookup resources used at runtime.
+ *
+ *
This API remains valid until the image reader from which it was
+ * obtained is closed.
+ */
+ // Package visible for use by ImageReader.
+ ResourceEntries getResourceEntries() {
+ return new ResourceEntries() {
+ @Override
+ public Stream getEntryNames(String module) {
+ if (module.isEmpty() || module.equals("modules") || module.equals("packages")) {
+ throw new IllegalArgumentException("Invalid module name: " + module);
+ }
+ return IntStream.range(0, offsets.capacity())
+ .map(offsets::get)
+ .filter(offset -> offset != 0)
+ // Reusing a location instance or getting the module
+ // offset directly would save a lot of allocations here.
+ .mapToObj(offset -> ImageLocation.readFrom(BasicImageReader.this, offset))
+ // Reverse lookup of module offset would be faster here.
+ .filter(loc -> module.equals(loc.getModule()))
+ .map(ImageLocation::getFullName);
+ }
+
+ private ImageLocation getResourceLocation(String name) {
+ if (!name.startsWith("/modules/") && !name.startsWith("/packages/")) {
+ ImageLocation location = BasicImageReader.this.findLocation(name);
+ if (location != null) {
+ return location;
+ }
+ }
+ throw new NoSuchElementException("No such resource entry: " + name);
+ }
+
+ @Override
+ public long getSize(String name) {
+ return getResourceLocation(name).getUncompressedSize();
+ }
+
+ @Override
+ public byte[] getBytes(String name) {
+ return BasicImageReader.this.getResource(getResourceLocation(name));
+ }
+ };
+ }
+
ImageLocation getLocation(int offset) {
return ImageLocation.readFrom(this, offset);
}
diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java
index a8ab2ff465b..323561eab00 100644
--- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java
+++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java
@@ -230,6 +230,11 @@ public ByteBuffer getResourceBuffer(Node node) {
return reader.getResourceBuffer(node.getLocation());
}
+ // Package protected for use only by SystemImageReader.
+ ResourceEntries getResourceEntries() {
+ return reader.getResourceEntries();
+ }
+
private static final class SharedImageReader extends BasicImageReader {
// There are >30,000 nodes in a complete jimage tree, and even relatively
// common tasks (e.g. starting up javac) load somewhere in the region of
diff --git a/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java b/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java
new file mode 100644
index 00000000000..25f5d484e71
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2025, 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 jdk.internal.jimage;
+
+import java.io.InputStream;
+import java.util.stream.Stream;
+
+/**
+ * Accesses the underlying resource entries in a jimage file.
+ *
+ * This API is designed only for use by the jlink classes, which read the raw
+ * jimage files. Use the {@link ImageReader} API to read jimage contents at
+ * runtime to correctly account for preview mode.
+ *
+ *
This API ignores the {@code previewMode} of the {@link ImageReader} from
+ * which it is obtained, and returns an unmapped view of entries (e.g. allowing
+ * for direct access of resources in the {@code META-INF/preview/...} namespace).
+ *
+ *
It disallows access to resource directories (i.e. {@code "/modules/..."})
+ * or packages entries (i.e. {@code "/packages/..."}).
+ *
+ * @implNote This class needs to maintain JDK 8 source compatibility.
+ *
+ * It is used internally in the JDK to implement jimage/jrtfs access,
+ * but also compiled and delivered as part of the jrtfs.jar to support access
+ * to the jimage file provided by the shipped JDK by tools running on JDK 8.
+ */
+public interface ResourceEntries {
+ /**
+ * Returns the jimage names for all resources in the given module, in
+ * random order. Entry names will always be prefixed by the given module
+ * name (e.g. {@code "//..."}).
+ */
+ Stream getEntryNames(String module);
+
+ /**
+ * Returns the (uncompressed) size of a resource given its jimage name.
+ *
+ * @throws java.util.NoSuchElementException if the resource does not exist.
+ */
+ long getSize(String name);
+
+ /**
+ * Returns a copy of a resource's content given its jimage name.
+ *
+ * @throws java.util.NoSuchElementException if the resource does not exist.
+ */
+ byte[] getBytes(String name);
+}
diff --git a/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java
index 38bf786e533..e5ed3d13fd8 100644
--- a/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java
+++ b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java
@@ -73,5 +73,15 @@ public static ImageReader get() {
return SYSTEM_IMAGE_READER;
}
+ /**
+ * Returns the "raw" API for accessing underlying jimage resource entries.
+ *
+ * This is only meaningful for use by code dealing directly with jimage
+ * files, and cannot be used to reliably lookup resources used at runtime.
+ */
+ public static ResourceEntries getResourceEntries() {
+ return get().getResourceEntries();
+ }
+
private SystemImageReader() {}
}
diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java
index c56346b6994..29f1e4173f4 100644
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java
@@ -24,6 +24,7 @@
*/
package jdk.tools.jlink.internal;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
@@ -34,7 +35,7 @@
* An Archive of all content, classes, resources, configuration files, and
* other, for a module.
*/
-public interface Archive {
+public interface Archive extends Closeable {
/**
* Entry is contained in an Archive
@@ -59,11 +60,12 @@ public static enum EntryType {
private final String path;
/**
- * Constructs an entry of the given archive
- * @param archive archive
- * @param path
- * @param name an entry name that does not contain the module name
- * @param type
+ * Constructs an entry of the given archive.
+ *
+ * @param archive the archive in which this entry exists.
+ * @param path the complete path of the entry, including the module.
+ * @param name an entry name relative to its containing module.
+ * @param type the entry type.
*/
public Entry(Archive archive, String path, String name, EntryType type) {
this.archive = Objects.requireNonNull(archive);
@@ -72,10 +74,6 @@ public Entry(Archive archive, String path, String name, EntryType type) {
this.type = Objects.requireNonNull(type);
}
- public final Archive archive() {
- return archive;
- }
-
public final EntryType type() {
return type;
}
@@ -134,5 +132,6 @@ public String toString() {
/*
* Close the archive
*/
+ @Override
void close() throws IOException;
}
diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java
index aac220e5b94..f30fe9a14d9 100644
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Red Hat, Inc.
+ * Copyright (c) 2025, 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
@@ -32,7 +33,6 @@
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.module.ModuleFinder;
-import java.lang.module.ModuleReference;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -51,6 +51,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import jdk.internal.jimage.ResourceEntries;
+import jdk.internal.jimage.SystemImageReader;
import jdk.internal.util.OperatingSystem;
import jdk.tools.jlink.internal.Archive.Entry.EntryType;
import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
@@ -63,10 +65,9 @@
* associated files from the filesystem of the JDK installation.
*/
public class JRTArchive implements Archive {
-
private final String module;
private final Path path;
- private final ModuleReference ref;
+ private final ResourceEntries imageResources;
// The collection of files of this module
private final List files = new ArrayList<>();
// Files not part of the lib/modules image of the JDK install.
@@ -99,12 +100,11 @@ public class JRTArchive implements Archive {
Set upgradeableFiles) {
this.module = module;
this.path = path;
- this.ref = ModuleFinder.ofSystem()
- .find(module)
- .orElseThrow(() ->
- new IllegalArgumentException(
- "Module " + module +
- " not part of the JDK install"));
+ ModuleFinder.ofSystem()
+ .find(module)
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Module " + module + " not part of the JDK install"));
+ this.imageResources = SystemImageReader.getResourceEntries();
this.errorOnModifiedFile = errorOnModifiedFile;
this.otherRes = readModuleResourceFile(module);
this.resDiff = Objects.requireNonNull(perModDiff).stream()
@@ -159,52 +159,35 @@ public boolean equals(Object obj) {
Objects.equals(path, other.path));
}
+ private boolean isNormalOrModifiedDiff(String name) {
+ ResourceDiff rd = resDiff.get(name);
+ // Filter all resources with a resource diff of kind MODIFIED.
+ // Note that REMOVED won't happen since in that case the module listing
+ // won't have the resource anyway.
+ // Note as well that filter removes files of kind ADDED. Those files are
+ // not in the packaged modules, so ought not to get returned from the
+ // pipeline.
+ return (rd == null || rd.getKind() == ResourceDiff.Kind.MODIFIED);
+ }
+
private void collectFiles() throws IOException {
if (files.isEmpty()) {
addNonClassResources();
+
// Add classes/resources from the run-time image,
// patched with the run-time image diff
- files.addAll(ref.open().list()
- .filter(i -> {
- String lookupKey = String.format("/%s/%s", module, i);
- ResourceDiff rd = resDiff.get(lookupKey);
- // Filter all resources with a resource diff
- // that are of kind MODIFIED.
- // Note that REMOVED won't happen since in
- // that case the module listing won't have
- // the resource anyway.
- // Note as well that filter removes files
- // of kind ADDED. Those files are not in
- // the packaged modules, so ought not to
- // get returned from the pipeline.
- return (rd == null ||
- rd.getKind() == ResourceDiff.Kind.MODIFIED);
- })
- .map(s -> {
- String lookupKey = String.format("/%s/%s", module, s);
- return new JRTArchiveFile(JRTArchive.this, s,
- EntryType.CLASS_OR_RESOURCE,
- null /* hashOrTarget */,
- false /* symlink */,
- resDiff.get(lookupKey));
- })
- .toList());
+ imageResources.getEntryNames(module)
+ .filter(this::isNormalOrModifiedDiff)
+ .sorted()
+ .map(name -> new JrtModuleFile(this, name, resDiff.get(name)))
+ .forEach(files::add);
+
// Finally add all files only present in the resource diff
// That is, removed items in the run-time image.
files.addAll(resDiff.values().stream()
- .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED)
- .map(s -> {
- int secondSlash = s.getName().indexOf("/", 1);
- assert secondSlash != -1;
- String pathWithoutModule = s.getName().substring(secondSlash + 1);
- return new JRTArchiveFile(JRTArchive.this,
- pathWithoutModule,
- EntryType.CLASS_OR_RESOURCE,
- null /* hashOrTarget */,
- false /* symlink */,
- s);
- })
- .toList());
+ .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED)
+ .map(rd -> new JrtModuleFile(this, rd.getName(), rd))
+ .toList());
}
}
@@ -234,15 +217,10 @@ private void addNonClassResources() {
}
}
- return new JRTArchiveFile(JRTArchive.this,
- m.resPath,
- toEntryType(m.resType),
- m.hashOrTarget,
- m.symlink,
- /* diff only for resources */
- null);
- })
- .toList());
+ return new JrtOtherFile(
+ this, m.resPath, toEntryType(m.resType), m.hashOrTarget, m.symlink);
+ })
+ .toList());
}
}
@@ -323,11 +301,11 @@ public String encodeToString() {
resPath);
}
- /**
+ /*
* line: |||
*
- * Take the integer before '|' convert it to a Type. The second
- * token is an integer representing symlinks (or not). The third token is
+ * Take the integer before '|' convert it to a Type. The second token
+ * is an integer representing symlinks (or not). The third token is
* a hash sum (sha512) of the file denoted by the fourth token (path).
*/
static ResourceFileEntry decodeFromString(String line) {
@@ -437,47 +415,75 @@ interface JRTFile {
Entry toEntry();
}
- record JRTArchiveFile(Archive archive,
- String resPath,
- EntryType resType,
- String sha,
- boolean symlink,
- ResourceDiff diff) implements JRTFile {
+ record JrtModuleFile(
+ JRTArchive archive,
+ String resPath,
+ ResourceDiff diff) implements JRTFile {
+ @Override
+ public Entry toEntry() {
+ assert resPath.startsWith("/" + archive.moduleName() + "/");
+ String resName = resPath.substring(archive.moduleName().length() + 2);
+
+ // If the resource has a diff to the packaged modules, use the diff.
+ // Diffs of kind ADDED have been filtered out in collectFiles();
+ if (diff != null) {
+ assert diff.getKind() != ResourceDiff.Kind.ADDED;
+ assert diff.getName().equals(resPath);
+
+ return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) {
+ @Override
+ public long size() {
+ return diff.getResourceBytes().length;
+ }
+ @Override
+ public InputStream stream() {
+ return new ByteArrayInputStream(diff.getResourceBytes());
+ }
+ };
+ } else {
+ return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) {
+ @Override
+ public long size() {
+ return archive.imageResources.getSize(resPath);
+ }
+
+ @Override
+ public InputStream stream() {
+ // Byte content could be cached in the entry if needed.
+ return new ByteArrayInputStream(archive.imageResources.getBytes(resPath));
+ }
+ };
+ }
+ }
+ }
+
+ record JrtOtherFile(
+ JRTArchive archive,
+ String resPath,
+ EntryType resType,
+ String sha,
+ boolean symlink) implements JRTFile {
+
+ // Read from the base JDK image, special casing
+ // symlinks, which have the link target in the
+ // hashOrTarget field.
+ Path targetPath() {
+ return BASE.resolve(symlink ? sha : resPath);
+ }
+
public Entry toEntry() {
- return new Entry(archive,
- String.format("/%s/%s",
- archive.moduleName(),
- resPath),
- resPath,
- resType) {
+ assert resType != EntryType.CLASS_OR_RESOURCE;
+
+ return new Entry(
+ archive,
+ String.format("/%s/%s", archive.moduleName(), resPath),
+ resPath,
+ resType) {
+
@Override
public long size() {
try {
- if (resType != EntryType.CLASS_OR_RESOURCE) {
- // Read from the base JDK image, special casing
- // symlinks, which have the link target in the
- // hashOrTarget field
- if (symlink) {
- return Files.size(BASE.resolve(sha));
- }
- return Files.size(BASE.resolve(resPath));
- } else {
- if (diff != null) {
- // If the resource has a diff to the
- // packaged modules, use the diff. Diffs of kind
- // ADDED have been filtered out in collectFiles();
- assert diff.getKind() != ResourceDiff.Kind.ADDED;
- assert diff.getName().equals(String.format("/%s/%s",
- archive.moduleName(),
- resPath));
- return diff.getResourceBytes().length;
- }
- // Read from the module image. This works, because
- // the underlying base path is a JrtPath with the
- // JrtFileSystem underneath which is able to handle
- // this size query.
- return Files.size(archive.getPath().resolve(resPath));
- }
+ return Files.size(targetPath());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -485,28 +491,8 @@ public long size() {
@Override
public InputStream stream() throws IOException {
- if (resType != EntryType.CLASS_OR_RESOURCE) {
- // Read from the base JDK image.
- Path path = symlink ? BASE.resolve(sha) : BASE.resolve(resPath);
- return Files.newInputStream(path);
- } else {
- // Read from the module image. Use the diff to the
- // packaged modules if we have one. Diffs of kind
- // ADDED have been filtered out in collectFiles();
- if (diff != null) {
- assert diff.getKind() != ResourceDiff.Kind.ADDED;
- assert diff.getName().equals(String.format("/%s/%s",
- archive.moduleName(),
- resPath));
- return new ByteArrayInputStream(diff.getResourceBytes());
- }
- String module = archive.moduleName();
- ModuleReference mRef = ModuleFinder.ofSystem()
- .find(module).orElseThrow();
- return mRef.open().open(resPath).orElseThrow();
- }
+ return Files.newInputStream(targetPath());
}
-
};
}
}
diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java
index 928b9a47934..0fa4cb4b142 100644
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025, 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
@@ -349,21 +349,22 @@ public static void createImage(JlinkConfiguration config,
plugins = plugins == null ? new PluginsConfiguration() : plugins;
// First create the image provider
- ImageProvider imageProvider =
- createImageProvider(config,
- null,
- IGNORE_SIGNING_DEFAULT,
- false,
- null,
- false,
- new OptionsValues(),
- null);
-
- // Then create the Plugin Stack
- ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
-
- //Ask the stack to proceed;
- stack.operate(imageProvider);
+ try (ImageHelper imageProvider =
+ createImageProvider(config,
+ null,
+ IGNORE_SIGNING_DEFAULT,
+ false,
+ null,
+ false,
+ new OptionsValues(),
+ null)) {
+
+ // Then create the Plugin Stack
+ ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
+
+ // Ask the stack to proceed;
+ stack.operate(imageProvider);
+ }
}
// the token for "all modules on the module path"
@@ -486,22 +487,24 @@ private void createImage(JlinkConfiguration config) throws Exception {
}
// First create the image provider
- ImageHelper imageProvider = createImageProvider(config,
- options.packagedModulesPath,
- options.ignoreSigning,
- options.bindServices,
- options.endian,
- options.verbose,
- options,
- log);
-
- // Then create the Plugin Stack
- ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
- taskHelper.getPluginsConfig(options.output, options.launchers,
- imageProvider.targetPlatform));
-
- //Ask the stack to proceed
- stack.operate(imageProvider);
+ try (ImageHelper imageProvider = createImageProvider(config,
+ options.packagedModulesPath,
+ options.ignoreSigning,
+ options.bindServices,
+ options.endian,
+ options.verbose,
+ options,
+ log)) {
+ // Then create the Plugin Stack
+ ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
+ taskHelper.getPluginsConfig(
+ options.output,
+ options.launchers,
+ imageProvider.targetPlatform));
+
+ //Ask the stack to proceed
+ stack.operate(imageProvider);
+ }
}
/**
@@ -1027,10 +1030,11 @@ private String getSaveOpts() {
return sb.toString();
}
- private static record ImageHelper(Set archives,
- Platform targetPlatform,
- Path packagedModulesPath,
- boolean generateRuntimeImage) implements ImageProvider {
+ private record ImageHelper(Set archives,
+ Platform targetPlatform,
+ Path packagedModulesPath,
+ boolean generateRuntimeImage)
+ implements ImageProvider, AutoCloseable {
@Override
public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
ExecutableImage image = ImageFileCreator.create(archives,
@@ -1046,5 +1050,25 @@ public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
}
return image;
}
+
+ @Override
+ public void close() throws IOException {
+ List thrown = null;
+ for (Archive archive : archives) {
+ try {
+ archive.close();
+ } catch (IOException ex) {
+ if (thrown == null) {
+ thrown = new ArrayList<>();
+ }
+ thrown.add(ex);
+ }
+ }
+ if (thrown != null) {
+ IOException ex = new IOException("Archives could not be closed", thrown.getFirst());
+ thrown.subList(1, thrown.size()).forEach(ex::addSuppressed);
+ throw ex;
+ }
+ }
}
}
diff --git a/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java b/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java
index ef7030d2e62..3ce58fe5684 100644
--- a/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java
+++ b/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Red Hat, Inc.
+ * Copyright (c) 2025, 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,6 +32,10 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
import tests.Helper;
import tests.JImageGenerator;
@@ -47,9 +52,10 @@
* jdk.jlink/jdk.tools.jlink.internal
* jdk.jlink/jdk.tools.jlink.plugin
* jdk.jlink/jdk.tools.jimage
+ * jdk.jlink
* @build tests.* jdk.test.lib.process.OutputAnalyzer
* jdk.test.lib.process.ProcessTools
- * @run main/othervm/timeout=1200 -Xmx1g PackagedModulesVsRuntimeImageLinkTest
+ * @run main/othervm/timeout=1200 -ea -esa -DDISABLE_PREVIEW_PATCHING=false -Xmx1g PackagedModulesVsRuntimeImageLinkTest
*/
public class PackagedModulesVsRuntimeImageLinkTest extends AbstractLinkableRuntimeTest {
@@ -70,7 +76,6 @@ void runTest(Helper helper, boolean isLinkableRuntime) throws Exception {
builder.setLinkableRuntime();
}
Path javaSEruntimeLink = createJavaImageRuntimeLink(builder.build());
-
// create a java.se using packaged modules (jmod-full)
Path javaSEJmodFull = JImageGenerator.getJLinkTask()
.output(helper.createNewImageDir("java-se-jmodfull"))
@@ -120,9 +125,8 @@ private static void compareRecursively(Path javaSEJmodLess,
Path jimageJmodFull = javaSEJmodFull.resolve(Path.of("lib")).resolve(Path.of("modules"));
List jimageContentJmodLess = JImageHelper.listContents(jimageJmodLess);
List jimageContentJmodFull = JImageHelper.listContents(jimageJmodFull);
- if (jimageContentJmodLess.size() != jimageContentJmodFull.size()) {
- throw new AssertionError(String.format("Size of jimage content differs for jmod-less (%d) v. jmod-full (%d)", jimageContentJmodLess.size(), jimageContentJmodFull.size()));
- }
+ assertSameContent("jmod-less", jimageContentJmodLess, "jmod-full", jimageContentJmodFull);
+ // Both lists are same size, with same names, so enumerate either.
for (int i = 0; i < jimageContentJmodFull.size(); i++) {
if (!jimageContentJmodFull.get(i).equals(jimageContentJmodLess.get(i))) {
throw new AssertionError(String.format("Jimage content differs at index %d: jmod-full was: '%s' jmod-less was: '%s'",
@@ -145,8 +149,37 @@ private static void compareRecursively(Path javaSEJmodLess,
}
}
+ // Helper to assert the content of two jimage files are the same and provide
+ // useful debug information otherwise.
+ private static void assertSameContent(
+ String lhsLabel, List lhsNames, String rhsLabel, List rhsNames) {
+
+ List lhsOnly =
+ lhsNames.stream().filter(Predicate.not(Set.copyOf(rhsNames)::contains)).toList();
+ List rhsOnly =
+ rhsNames.stream().filter(Predicate.not(Set.copyOf(lhsNames)::contains)).toList();
+ if (!lhsOnly.isEmpty() || !rhsOnly.isEmpty()) {
+ String message = String.format(
+ "jimage content differs for %s (%d) v. %s (%d)",
+ lhsLabel, lhsNames.size(), rhsLabel, rhsNames.size());
+ if (!lhsOnly.isEmpty()) {
+ message += "\nOnly in " + lhsLabel + ":\n\t" + String.join("\n\t", lhsOnly);
+ }
+ if (!rhsOnly.isEmpty()) {
+ message += "\nOnly in " + rhsLabel + ":\n\t" + String.join("\n\t", rhsOnly);
+ }
+ throw new AssertionError(message);
+ }
+ }
+
private static boolean isTreeInfoResource(String path) {
- return path.startsWith("/packages") || path.startsWith("/modules");
+ return pathStartsWith(path, "/packages") || pathStartsWith(path, "/modules");
+ }
+
+ // Handle both "" and "/...".
+ private static boolean pathStartsWith(String path, String prefix) {
+ int plen = prefix.length();
+ return path.startsWith(prefix) && (path.length() == plen || path.charAt(plen) == '/');
}
private static void handleFileMismatch(Path a, Path b) {